545 replace objects icons and names with records avatars and labelidentifiers in command menu context chips (#10787)
Closes https://github.com/twentyhq/core-team-issues/issues/545 This PR: - Introduces `commandMenuNavigationMorphItemsState` which stores the information about the `recordId` and the `objectMetadataItemId` for each page - Creates `CommandMenuContextChipEffect`, which queries the records from the previous pages in case a record has been updated during the navigation, to keep up to date information and stores it inside `commandMenuNavigationRecordsState` - `useCommandMenuContextChips` returns the context chips information - Style updates (icons background and color) - Updates `useCommandMenu` to set and reset these new states https://github.com/user-attachments/assets/8886848a-721d-4709-9330-8e84ebc0d51e
This commit is contained in:
@ -47,6 +47,7 @@ export const CommandMenuContextChipGroupsWithRecordSelection = ({
|
|||||||
),
|
),
|
||||||
Icons: Avatars,
|
Icons: Avatars,
|
||||||
onClick: contextChips.length > 0 ? openRootCommandMenu : undefined,
|
onClick: contextChips.length > 0 ? openRootCommandMenu : undefined,
|
||||||
|
withIconBackground: false,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,117 @@
|
|||||||
|
import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState';
|
||||||
|
import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState';
|
||||||
|
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||||
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||||
|
import { usePerformCombinedFindManyRecords } from '@/object-record/multiple-objects/hooks/usePerformCombinedFindManyRecords';
|
||||||
|
import { isNonEmptyArray } from '@sniptt/guards';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
import { capitalize, isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
|
export const CommandMenuContextChipRecordSetterEffect = () => {
|
||||||
|
const commandMenuNavigationMorphItemByPage = useRecoilValue(
|
||||||
|
commandMenuNavigationMorphItemByPageState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const setCommandMenuNavigationRecords = useSetRecoilState(
|
||||||
|
commandMenuNavigationRecordsState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||||
|
|
||||||
|
const { performCombinedFindManyRecords } =
|
||||||
|
usePerformCombinedFindManyRecords();
|
||||||
|
|
||||||
|
const objectMetadataIdsUsedInCommandMenuNavigation = Array.from(
|
||||||
|
commandMenuNavigationMorphItemByPage.values(),
|
||||||
|
).map(({ objectMetadataId }) => objectMetadataId);
|
||||||
|
|
||||||
|
const searchableObjectMetadataItems = objectMetadataItems.filter(({ id }) =>
|
||||||
|
objectMetadataIdsUsedInCommandMenuNavigation.includes(id),
|
||||||
|
);
|
||||||
|
|
||||||
|
const commandMenuNavigationStack = useRecoilValue(
|
||||||
|
commandMenuNavigationStackState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchRecords = useCallback(async () => {
|
||||||
|
const filterPerMetadataItemFilteredOnRecordId = Object.fromEntries(
|
||||||
|
searchableObjectMetadataItems
|
||||||
|
.map(({ id, nameSingular }) => {
|
||||||
|
const recordIdsForMetadataItem = Array.from(
|
||||||
|
commandMenuNavigationMorphItemByPage.values(),
|
||||||
|
)
|
||||||
|
.filter(({ objectMetadataId }) => objectMetadataId === id)
|
||||||
|
.map(({ recordId }) => recordId);
|
||||||
|
|
||||||
|
if (!isNonEmptyArray(recordIdsForMetadataItem)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
`filter${capitalize(nameSingular)}`,
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
in: recordIdsForMetadataItem,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
})
|
||||||
|
.filter(isDefined),
|
||||||
|
);
|
||||||
|
|
||||||
|
const operationSignatures = searchableObjectMetadataItems
|
||||||
|
.filter(({ nameSingular }) =>
|
||||||
|
isDefined(
|
||||||
|
filterPerMetadataItemFilteredOnRecordId[
|
||||||
|
`filter${capitalize(nameSingular)}`
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map((objectMetadataItem) => ({
|
||||||
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
|
variables: {
|
||||||
|
filter:
|
||||||
|
filterPerMetadataItemFilteredOnRecordId[
|
||||||
|
`filter${capitalize(objectMetadataItem.nameSingular)}`
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (operationSignatures.length === 0) {
|
||||||
|
setCommandMenuNavigationRecords([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { result } = await performCombinedFindManyRecords({
|
||||||
|
operationSignatures,
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedRecords = Object.entries(result).flatMap(
|
||||||
|
([objectNamePlural, records]) =>
|
||||||
|
records.map((record) => ({
|
||||||
|
objectMetadataItem: searchableObjectMetadataItems.find(
|
||||||
|
({ namePlural }) => namePlural === objectNamePlural,
|
||||||
|
) as ObjectMetadataItem,
|
||||||
|
record: record as RecordGqlNode,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
setCommandMenuNavigationRecords(formattedRecords);
|
||||||
|
}, [
|
||||||
|
commandMenuNavigationMorphItemByPage,
|
||||||
|
performCombinedFindManyRecords,
|
||||||
|
searchableObjectMetadataItems,
|
||||||
|
setCommandMenuNavigationRecords,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (commandMenuNavigationStack.length > 1) {
|
||||||
|
fetchRecords();
|
||||||
|
}
|
||||||
|
}, [commandMenuNavigationStack.length, fetchRecords]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@ -32,7 +32,6 @@ export const CommandMenuContextRecordChipAvatars = ({
|
|||||||
objectNameSingular: objectMetadataItem.nameSingular,
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
record,
|
record,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { Icon, IconColor } = useGetStandardObjectIcon(
|
const { Icon, IconColor } = useGetStandardObjectIcon(
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { CommandMenuContainer } from '@/command-menu/components/CommandMenuContainer';
|
import { CommandMenuContainer } from '@/command-menu/components/CommandMenuContainer';
|
||||||
|
import { CommandMenuContextChipRecordSetterEffect } from '@/command-menu/components/CommandMenuContextChipRecordSetterEffect';
|
||||||
import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar';
|
import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar';
|
||||||
import { COMMAND_MENU_PAGES_CONFIG } from '@/command-menu/constants/CommandMenuPagesConfig';
|
import { COMMAND_MENU_PAGES_CONFIG } from '@/command-menu/constants/CommandMenuPagesConfig';
|
||||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||||
@ -30,6 +31,7 @@ export const CommandMenuRouter = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandMenuContainer>
|
<CommandMenuContainer>
|
||||||
|
<CommandMenuContextChipRecordSetterEffect />
|
||||||
<CommandMenuPageComponentInstanceContext.Provider
|
<CommandMenuPageComponentInstanceContext.Provider
|
||||||
value={{ instanceId: commandMenuPageInfo.instanceId }}
|
value={{ instanceId: commandMenuPageInfo.instanceId }}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { CommandMenuTopBarInputFocusEffect } from '@/command-menu/components/Com
|
|||||||
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
|
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
|
||||||
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
|
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
|
||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
import { useCommandMenuContextChips } from '@/command-menu/hooks/useCommandMenuContextChips';
|
||||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||||
@ -16,7 +16,7 @@ import { useTheme } from '@emotion/react';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
@ -100,11 +100,7 @@ export const CommandMenuTopBar = () => {
|
|||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const {
|
const { closeCommandMenu, goBackFromCommandMenu } = useCommandMenu();
|
||||||
closeCommandMenu,
|
|
||||||
goBackFromCommandMenu,
|
|
||||||
navigateCommandMenuHistory,
|
|
||||||
} = useCommandMenu();
|
|
||||||
|
|
||||||
const contextStoreCurrentObjectMetadataItem = useRecoilComponentValueV2(
|
const contextStoreCurrentObjectMetadataItem = useRecoilComponentValueV2(
|
||||||
contextStoreCurrentObjectMetadataItemComponentState,
|
contextStoreCurrentObjectMetadataItemComponentState,
|
||||||
@ -112,42 +108,13 @@ export const CommandMenuTopBar = () => {
|
|||||||
|
|
||||||
const commandMenuPage = useRecoilValue(commandMenuPageState);
|
const commandMenuPage = useRecoilValue(commandMenuPageState);
|
||||||
|
|
||||||
const commandMenuNavigationStack = useRecoilValue(
|
|
||||||
commandMenuNavigationStackState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const isCommandMenuV2Enabled = useIsFeatureEnabled(
|
const isCommandMenuV2Enabled = useIsFeatureEnabled(
|
||||||
FeatureFlagKey.IsCommandMenuV2Enabled,
|
FeatureFlagKey.IsCommandMenuV2Enabled,
|
||||||
);
|
);
|
||||||
|
|
||||||
const contextChips = useMemo(() => {
|
const { contextChips } = useCommandMenuContextChips();
|
||||||
const filteredCommandMenuNavigationStack =
|
|
||||||
commandMenuNavigationStack.filter(
|
|
||||||
(page) => page.page !== CommandMenuPages.Root,
|
|
||||||
);
|
|
||||||
|
|
||||||
return filteredCommandMenuNavigationStack.map((page, index) => {
|
|
||||||
const isLastChip =
|
|
||||||
index === filteredCommandMenuNavigationStack.length - 1;
|
|
||||||
|
|
||||||
return {
|
|
||||||
page,
|
|
||||||
Icons: [<page.pageIcon size={theme.icon.size.sm} />],
|
|
||||||
text: page.pageTitle,
|
|
||||||
onClick: isLastChip
|
|
||||||
? undefined
|
|
||||||
: () => {
|
|
||||||
navigateCommandMenuHistory(index);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, [
|
|
||||||
commandMenuNavigationStack,
|
|
||||||
navigateCommandMenuHistory,
|
|
||||||
theme.icon.size.sm,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isButtonVisible =
|
const isButtonVisible =
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContext
|
|||||||
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
|
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
|
||||||
import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState';
|
import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState';
|
||||||
import { workflowIdComponentState } from '@/command-menu/pages/workflow/states/workflowIdComponentState';
|
import { workflowIdComponentState } from '@/command-menu/pages/workflow/states/workflowIdComponentState';
|
||||||
|
import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState';
|
||||||
|
import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState';
|
||||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||||
@ -38,10 +40,12 @@ import { contextStoreFiltersComponentState } from '@/context-store/states/contex
|
|||||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||||
|
import { getIconColorForObjectType } from '@/object-metadata/utils/getIconColorForObjectType';
|
||||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||||
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
|
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
|
||||||
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
|
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
|
||||||
import { isDragSelectionStartEnabledState } from '@/ui/utilities/drag-select/states/internal/isDragSelectionStartEnabledState';
|
import { isDragSelectionStartEnabledState } from '@/ui/utilities/drag-select/states/internal/isDragSelectionStartEnabledState';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { capitalize, isDefined } from 'twenty-shared';
|
import { capitalize, isDefined } from 'twenty-shared';
|
||||||
@ -52,6 +56,7 @@ export type CommandMenuNavigationStackItem = {
|
|||||||
page: CommandMenuPages;
|
page: CommandMenuPages;
|
||||||
pageTitle: string;
|
pageTitle: string;
|
||||||
pageIcon: IconComponent;
|
pageIcon: IconComponent;
|
||||||
|
pageIconColor?: string;
|
||||||
pageId?: string;
|
pageId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -68,6 +73,8 @@ export const useCommandMenu = () => {
|
|||||||
|
|
||||||
const { closeDropdown } = useDropdownV2();
|
const { closeDropdown } = useDropdownV2();
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
const closeCommandMenu = useRecoilCallback(
|
const closeCommandMenu = useRecoilCallback(
|
||||||
({ set }) =>
|
({ set }) =>
|
||||||
() => {
|
() => {
|
||||||
@ -95,6 +102,8 @@ export const useCommandMenu = () => {
|
|||||||
});
|
});
|
||||||
set(isCommandMenuOpenedState, false);
|
set(isCommandMenuOpenedState, false);
|
||||||
set(commandMenuSearchState, '');
|
set(commandMenuSearchState, '');
|
||||||
|
set(commandMenuNavigationMorphItemByPageState, new Map());
|
||||||
|
set(commandMenuNavigationRecordsState, []);
|
||||||
set(commandMenuNavigationStackState, []);
|
set(commandMenuNavigationStackState, []);
|
||||||
resetSelectedItem();
|
resetSelectedItem();
|
||||||
set(hasUserSelectedCommandState, false);
|
set(hasUserSelectedCommandState, false);
|
||||||
@ -154,6 +163,7 @@ export const useCommandMenu = () => {
|
|||||||
page,
|
page,
|
||||||
pageTitle,
|
pageTitle,
|
||||||
pageIcon,
|
pageIcon,
|
||||||
|
pageIconColor,
|
||||||
pageId,
|
pageId,
|
||||||
resetNavigationStack = false,
|
resetNavigationStack = false,
|
||||||
}: CommandMenuNavigationStackItem & {
|
}: CommandMenuNavigationStackItem & {
|
||||||
@ -185,9 +195,13 @@ export const useCommandMenu = () => {
|
|||||||
page,
|
page,
|
||||||
pageTitle,
|
pageTitle,
|
||||||
pageIcon,
|
pageIcon,
|
||||||
|
pageIconColor,
|
||||||
pageId,
|
pageId,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
set(commandMenuNavigationRecordsState, []);
|
||||||
|
set(commandMenuNavigationMorphItemByPageState, new Map());
|
||||||
} else {
|
} else {
|
||||||
set(commandMenuNavigationStackState, [
|
set(commandMenuNavigationStackState, [
|
||||||
...currentNavigationStack,
|
...currentNavigationStack,
|
||||||
@ -195,6 +209,7 @@ export const useCommandMenu = () => {
|
|||||||
page,
|
page,
|
||||||
pageTitle,
|
pageTitle,
|
||||||
pageIcon,
|
pageIcon,
|
||||||
|
pageIconColor,
|
||||||
pageId,
|
pageId,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@ -255,6 +270,21 @@ export const useCommandMenu = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
set(commandMenuNavigationStackState, newNavigationStack);
|
set(commandMenuNavigationStackState, newNavigationStack);
|
||||||
|
|
||||||
|
const currentMorphItems = snapshot
|
||||||
|
.getLoadable(commandMenuNavigationMorphItemByPageState)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
if (currentNavigationStack.length > 0) {
|
||||||
|
const removedItem = currentNavigationStack.at(-1);
|
||||||
|
|
||||||
|
if (isDefined(removedItem)) {
|
||||||
|
const newMorphItems = new Map(currentMorphItems);
|
||||||
|
newMorphItems.delete(removedItem.pageId);
|
||||||
|
set(commandMenuNavigationMorphItemByPageState, newMorphItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
set(hasUserSelectedCommandState, false);
|
set(hasUserSelectedCommandState, false);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -285,6 +315,17 @@ export const useCommandMenu = () => {
|
|||||||
Icon: newNavigationStackItem?.pageIcon,
|
Icon: newNavigationStackItem?.pageIcon,
|
||||||
instanceId: newNavigationStackItem?.pageId,
|
instanceId: newNavigationStackItem?.pageId,
|
||||||
});
|
});
|
||||||
|
const currentMorphItems = snapshot
|
||||||
|
.getLoadable(commandMenuNavigationMorphItemByPageState)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
const newMorphItems = new Map(
|
||||||
|
Array.from(currentMorphItems.entries()).filter(([pageId]) =>
|
||||||
|
newNavigationStack.some((item) => item.pageId === pageId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
set(commandMenuNavigationMorphItemByPageState, newMorphItems);
|
||||||
|
|
||||||
set(hasUserSelectedCommandState, false);
|
set(hasUserSelectedCommandState, false);
|
||||||
};
|
};
|
||||||
@ -376,10 +417,31 @@ export const useCommandMenu = () => {
|
|||||||
.getValue(),
|
.getValue(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const currentMorphItems = snapshot
|
||||||
|
.getLoadable(commandMenuNavigationMorphItemByPageState)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
const morphItemToAdd = {
|
||||||
|
objectMetadataId: objectMetadataItem.id,
|
||||||
|
recordId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newMorphItems = new Map([
|
||||||
|
...currentMorphItems,
|
||||||
|
[pageComponentInstanceId, morphItemToAdd],
|
||||||
|
]);
|
||||||
|
|
||||||
|
set(commandMenuNavigationMorphItemByPageState, newMorphItems);
|
||||||
|
|
||||||
const Icon = objectMetadataItem?.icon
|
const Icon = objectMetadataItem?.icon
|
||||||
? getIcon(objectMetadataItem.icon)
|
? getIcon(objectMetadataItem.icon)
|
||||||
: getIcon('IconList');
|
: getIcon('IconList');
|
||||||
|
|
||||||
|
const IconColor = getIconColorForObjectType({
|
||||||
|
objectType: objectMetadataItem.nameSingular,
|
||||||
|
theme,
|
||||||
|
});
|
||||||
|
|
||||||
const capitalizedObjectNameSingular = capitalize(objectNameSingular);
|
const capitalizedObjectNameSingular = capitalize(objectNameSingular);
|
||||||
|
|
||||||
navigateCommandMenu({
|
navigateCommandMenu({
|
||||||
@ -388,12 +450,13 @@ export const useCommandMenu = () => {
|
|||||||
? t`New ${capitalizedObjectNameSingular}`
|
? t`New ${capitalizedObjectNameSingular}`
|
||||||
: capitalizedObjectNameSingular,
|
: capitalizedObjectNameSingular,
|
||||||
pageIcon: Icon,
|
pageIcon: Icon,
|
||||||
|
pageIconColor: IconColor,
|
||||||
pageId: pageComponentInstanceId,
|
pageId: pageComponentInstanceId,
|
||||||
resetNavigationStack: false,
|
resetNavigationStack: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[getIcon, navigateCommandMenu],
|
[getIcon, navigateCommandMenu, theme],
|
||||||
);
|
);
|
||||||
|
|
||||||
const openWorkflowTriggerTypeInCommandMenu = useRecoilCallback(
|
const openWorkflowTriggerTypeInCommandMenu = useRecoilCallback(
|
||||||
@ -522,6 +585,31 @@ export const useCommandMenu = () => {
|
|||||||
calendarEventId,
|
calendarEventId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: Uncomment this once we need to calendar event title in the navigation
|
||||||
|
// const objectMetadataItem = snapshot
|
||||||
|
// .getLoadable(objectMetadataItemsState)
|
||||||
|
// .getValue()
|
||||||
|
// .find(
|
||||||
|
// ({ nameSingular }) =>
|
||||||
|
// nameSingular === CoreObjectNameSingular.CalendarEvent,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// set(
|
||||||
|
// commandMenuNavigationMorphItemsState,
|
||||||
|
// new Map([
|
||||||
|
// ...snapshot
|
||||||
|
// .getLoadable(commandMenuNavigationMorphItemsState)
|
||||||
|
// .getValue(),
|
||||||
|
// [
|
||||||
|
// pageComponentInstanceId,
|
||||||
|
// {
|
||||||
|
// objectMetadataId: objectMetadataItem?.id,
|
||||||
|
// recordId: calendarEventId,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// ]),
|
||||||
|
// );
|
||||||
|
|
||||||
navigateCommandMenu({
|
navigateCommandMenu({
|
||||||
page: CommandMenuPages.ViewCalendarEvent,
|
page: CommandMenuPages.ViewCalendarEvent,
|
||||||
pageTitle: 'Calendar Event',
|
pageTitle: 'Calendar Event',
|
||||||
@ -545,6 +633,31 @@ export const useCommandMenu = () => {
|
|||||||
emailThreadId,
|
emailThreadId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: Uncomment this once we need to show the thread title in the navigation
|
||||||
|
// const objectMetadataItem = snapshot
|
||||||
|
// .getLoadable(objectMetadataItemsState)
|
||||||
|
// .getValue()
|
||||||
|
// .find(
|
||||||
|
// ({ nameSingular }) =>
|
||||||
|
// nameSingular === CoreObjectNameSingular.MessageThread,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// set(
|
||||||
|
// commandMenuNavigationMorphItemsState,
|
||||||
|
// new Map([
|
||||||
|
// ...snapshot
|
||||||
|
// .getLoadable(commandMenuNavigationMorphItemsState)
|
||||||
|
// .getValue(),
|
||||||
|
// [
|
||||||
|
// pageComponentInstanceId,
|
||||||
|
// {
|
||||||
|
// objectMetadataId: objectMetadataItem?.id,
|
||||||
|
// recordId: emailThreadId,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// ]),
|
||||||
|
// );
|
||||||
|
|
||||||
navigateCommandMenu({
|
navigateCommandMenu({
|
||||||
page: CommandMenuPages.ViewEmailThread,
|
page: CommandMenuPages.ViewEmailThread,
|
||||||
pageTitle: 'Email Thread',
|
pageTitle: 'Email Thread',
|
||||||
|
|||||||
@ -0,0 +1,133 @@
|
|||||||
|
import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars';
|
||||||
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
|
import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState';
|
||||||
|
import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState';
|
||||||
|
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||||
|
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||||
|
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
|
const StyledIconWrapper = styled.div`
|
||||||
|
background: ${({ theme }) => theme.background.primary};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const useCommandMenuContextChips = () => {
|
||||||
|
const commandMenuNavigationStack = useRecoilValue(
|
||||||
|
commandMenuNavigationStackState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { navigateCommandMenuHistory } = useCommandMenu();
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const commandMenuNavigationMorphItemByPage = useRecoilValue(
|
||||||
|
commandMenuNavigationMorphItemByPageState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const commandMenuNavigationRecords = useRecoilValue(
|
||||||
|
commandMenuNavigationRecordsState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const contextChips = useMemo(() => {
|
||||||
|
const filteredCommandMenuNavigationStack =
|
||||||
|
commandMenuNavigationStack.filter(
|
||||||
|
(page) => page.page !== CommandMenuPages.Root,
|
||||||
|
);
|
||||||
|
|
||||||
|
return filteredCommandMenuNavigationStack
|
||||||
|
.map((page, index) => {
|
||||||
|
const isLastChip =
|
||||||
|
index === filteredCommandMenuNavigationStack.length - 1;
|
||||||
|
|
||||||
|
const isRecordPage = page.page === CommandMenuPages.ViewRecord;
|
||||||
|
|
||||||
|
if (isRecordPage && !isLastChip) {
|
||||||
|
const commandMenuNavigationMorphItem =
|
||||||
|
commandMenuNavigationMorphItemByPage.get(page.pageId);
|
||||||
|
|
||||||
|
if (!isDefined(commandMenuNavigationMorphItem?.recordId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectMetadataItem = commandMenuNavigationRecords.find(
|
||||||
|
({ objectMetadataItem }) =>
|
||||||
|
objectMetadataItem.id ===
|
||||||
|
commandMenuNavigationMorphItem.objectMetadataId,
|
||||||
|
)?.objectMetadataItem;
|
||||||
|
|
||||||
|
const record = commandMenuNavigationRecords.find(
|
||||||
|
({ record }) =>
|
||||||
|
record.id === commandMenuNavigationMorphItem.recordId,
|
||||||
|
)?.record;
|
||||||
|
|
||||||
|
if (!isDefined(objectMetadataItem) || !isDefined(record)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = getObjectRecordIdentifier({
|
||||||
|
objectMetadataItem,
|
||||||
|
record,
|
||||||
|
}).name;
|
||||||
|
|
||||||
|
return {
|
||||||
|
page,
|
||||||
|
Icons: [
|
||||||
|
<CommandMenuContextRecordChipAvatars
|
||||||
|
objectMetadataItem={objectMetadataItem}
|
||||||
|
record={record}
|
||||||
|
/>,
|
||||||
|
],
|
||||||
|
text: name,
|
||||||
|
onClick: () => {
|
||||||
|
navigateCommandMenuHistory(index);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
page,
|
||||||
|
Icons: isLastChip
|
||||||
|
? [<page.pageIcon size={theme.icon.size.sm} />]
|
||||||
|
: [
|
||||||
|
<StyledIconWrapper>
|
||||||
|
<page.pageIcon
|
||||||
|
size={theme.icon.size.sm}
|
||||||
|
color={
|
||||||
|
isDefined(page.pageIconColor) &&
|
||||||
|
page.pageIconColor !== 'currentColor'
|
||||||
|
? page.pageIconColor
|
||||||
|
: theme.font.color.tertiary
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledIconWrapper>,
|
||||||
|
],
|
||||||
|
text: page.pageTitle,
|
||||||
|
onClick: isLastChip
|
||||||
|
? undefined
|
||||||
|
: () => {
|
||||||
|
navigateCommandMenuHistory(index);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(isDefined);
|
||||||
|
}, [
|
||||||
|
commandMenuNavigationMorphItemByPage,
|
||||||
|
commandMenuNavigationRecords,
|
||||||
|
commandMenuNavigationStack,
|
||||||
|
navigateCommandMenuHistory,
|
||||||
|
theme.font.color.tertiary,
|
||||||
|
theme.icon.size.sm,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
contextChips,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { MorphItem } from '@/object-record/multiple-objects/types/MorphItem';
|
||||||
|
import { createState } from '@ui/utilities/state/utils/createState';
|
||||||
|
|
||||||
|
export const commandMenuNavigationMorphItemByPageState = createState<
|
||||||
|
Map<string, MorphItem>
|
||||||
|
>({
|
||||||
|
key: 'command-menu/commandMenuNavigationMorphItemByPageState',
|
||||||
|
defaultValue: new Map(),
|
||||||
|
});
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { createState } from '@ui/utilities/state/utils/createState';
|
||||||
|
|
||||||
|
export const commandMenuNavigationRecordsState = createState<
|
||||||
|
{
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
record: ObjectRecord;
|
||||||
|
}[]
|
||||||
|
>({
|
||||||
|
key: 'command-menu/commandMenuNavigationRecordsState',
|
||||||
|
defaultValue: [],
|
||||||
|
});
|
||||||
@ -5,6 +5,7 @@ export type CommandMenuNavigationStackItem = {
|
|||||||
page: CommandMenuPages;
|
page: CommandMenuPages;
|
||||||
pageTitle: string;
|
pageTitle: string;
|
||||||
pageIcon: IconComponent;
|
pageIcon: IconComponent;
|
||||||
|
pageIconColor?: string;
|
||||||
pageId: string;
|
pageId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
import { renderHook } from '@testing-library/react';
|
|
||||||
import { RecoilRoot } from 'recoil';
|
|
||||||
|
|
||||||
import { useLimitPerMetadataItem } from '@/object-metadata/hooks/useLimitPerMetadataItem';
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|
||||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
|
||||||
|
|
||||||
const instanceId = 'instanceId';
|
|
||||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
||||||
<SingleRecordPickerComponentInstanceContext.Provider value={{ instanceId }}>
|
|
||||||
<RecoilRoot>{children}</RecoilRoot>
|
|
||||||
</SingleRecordPickerComponentInstanceContext.Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('useLimitPerMetadataItem', () => {
|
|
||||||
const objectData: ObjectMetadataItem[] = [
|
|
||||||
{
|
|
||||||
createdAt: 'createdAt',
|
|
||||||
id: 'id',
|
|
||||||
isActive: true,
|
|
||||||
isCustom: true,
|
|
||||||
isSystem: true,
|
|
||||||
isRemote: false,
|
|
||||||
isSearchable: true,
|
|
||||||
labelPlural: 'labelPlural',
|
|
||||||
labelSingular: 'labelSingular',
|
|
||||||
namePlural: 'namePlural',
|
|
||||||
nameSingular: 'nameSingular',
|
|
||||||
labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1',
|
|
||||||
updatedAt: 'updatedAt',
|
|
||||||
isLabelSyncedWithName: false,
|
|
||||||
fields: [],
|
|
||||||
indexMetadatas: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
it('should return object with nameSingular and default limit', async () => {
|
|
||||||
const { result } = renderHook(
|
|
||||||
() => useLimitPerMetadataItem({ objectMetadataItems: objectData }),
|
|
||||||
{
|
|
||||||
wrapper: Wrapper,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.current.limitPerMetadataItem).toStrictEqual({
|
|
||||||
limitNameSingular: 60,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return an object with nameSingular and specified limit', async () => {
|
|
||||||
const { result } = renderHook(
|
|
||||||
() =>
|
|
||||||
useLimitPerMetadataItem({ objectMetadataItems: objectData, limit: 30 }),
|
|
||||||
{
|
|
||||||
wrapper: Wrapper,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.current.limitPerMetadataItem).toStrictEqual({
|
|
||||||
limitNameSingular: 30,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,36 +1,16 @@
|
|||||||
|
import { getIconColorForObjectType } from '@/object-metadata/utils/getIconColorForObjectType';
|
||||||
|
import { getIconForObjectType } from '@/object-metadata/utils/getIconForObjectType';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import { IconCheckbox, IconComponent, IconNotes } from 'twenty-ui';
|
|
||||||
|
|
||||||
export const useGetStandardObjectIcon = (objectNameSingular: string) => {
|
export const useGetStandardObjectIcon = (objectNameSingular: string) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const getIconForObjectType = (
|
|
||||||
objectType: string,
|
|
||||||
): IconComponent | undefined => {
|
|
||||||
switch (objectType) {
|
|
||||||
case 'note':
|
|
||||||
return IconNotes;
|
|
||||||
case 'task':
|
|
||||||
return IconCheckbox;
|
|
||||||
default:
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getIconColorForObjectType = (objectType: string): string => {
|
|
||||||
switch (objectType) {
|
|
||||||
case 'note':
|
|
||||||
return theme.color.yellow;
|
|
||||||
case 'task':
|
|
||||||
return theme.color.blue;
|
|
||||||
default:
|
|
||||||
return 'currentColor';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { Icon, IconColor } = {
|
const { Icon, IconColor } = {
|
||||||
Icon: getIconForObjectType(objectNameSingular),
|
Icon: getIconForObjectType(objectNameSingular),
|
||||||
IconColor: getIconColorForObjectType(objectNameSingular),
|
IconColor: getIconColorForObjectType({
|
||||||
|
objectType: objectNameSingular,
|
||||||
|
theme,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { Icon, IconColor };
|
return { Icon, IconColor };
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|
||||||
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
|
|
||||||
import { capitalize, isDefined } from 'twenty-shared';
|
|
||||||
|
|
||||||
export const useLimitPerMetadataItem = ({
|
|
||||||
objectMetadataItems,
|
|
||||||
limit = DEFAULT_SEARCH_REQUEST_LIMIT,
|
|
||||||
}: {
|
|
||||||
objectMetadataItems: ObjectMetadataItem[];
|
|
||||||
limit?: number;
|
|
||||||
}) => {
|
|
||||||
const limitPerMetadataItem = Object.fromEntries(
|
|
||||||
objectMetadataItems
|
|
||||||
.map(({ nameSingular }) => {
|
|
||||||
return [`limit${capitalize(nameSingular)}`, limit];
|
|
||||||
})
|
|
||||||
.filter(isDefined),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
limitPerMetadataItem,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { Theme } from '@emotion/react';
|
||||||
|
|
||||||
|
export const getIconColorForObjectType = ({
|
||||||
|
objectType,
|
||||||
|
theme,
|
||||||
|
}: {
|
||||||
|
objectType: string;
|
||||||
|
theme: Theme;
|
||||||
|
}): string => {
|
||||||
|
switch (objectType) {
|
||||||
|
case 'note':
|
||||||
|
return theme.color.yellow;
|
||||||
|
case 'task':
|
||||||
|
return theme.color.blue;
|
||||||
|
default:
|
||||||
|
return 'currentColor';
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { IconCheckbox, IconComponent, IconNotes } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const getIconForObjectType = (
|
||||||
|
objectType: string,
|
||||||
|
): IconComponent | undefined => {
|
||||||
|
switch (objectType) {
|
||||||
|
case 'note':
|
||||||
|
return IconNotes;
|
||||||
|
case 'task':
|
||||||
|
return IconCheckbox;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -3,9 +3,9 @@ import { useQuery } from '@apollo/client';
|
|||||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||||
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
|
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
|
||||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||||
import { useCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecordsQueryVariables';
|
|
||||||
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
|
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
|
||||||
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
|
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
|
||||||
|
import { generateCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/utils/generateCombinedFindManyRecordsQueryVariables';
|
||||||
|
|
||||||
export const useCombinedFindManyRecords = ({
|
export const useCombinedFindManyRecords = ({
|
||||||
operationSignatures,
|
operationSignatures,
|
||||||
@ -18,7 +18,7 @@ export const useCombinedFindManyRecords = ({
|
|||||||
operationSignatures,
|
operationSignatures,
|
||||||
});
|
});
|
||||||
|
|
||||||
const queryVariables = useCombinedFindManyRecordsQueryVariables({
|
const queryVariables = generateCombinedFindManyRecordsQueryVariables({
|
||||||
operationSignatures,
|
operationSignatures,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,153 @@
|
|||||||
|
import { ApolloClient, gql, useApolloClient } from '@apollo/client';
|
||||||
|
import { isUndefined } from '@sniptt/guards';
|
||||||
|
import { capitalize } from 'twenty-shared';
|
||||||
|
|
||||||
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||||
|
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||||
|
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
|
||||||
|
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||||
|
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||||
|
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
|
||||||
|
import { generateCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/utils/generateCombinedFindManyRecordsQueryVariables';
|
||||||
|
import { getCombinedFindManyRecordsQueryFilteringPart } from '@/object-record/multiple-objects/utils/getCombinedFindManyRecordsQueryFilteringPart';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
export const usePerformCombinedFindManyRecords = () => {
|
||||||
|
const client = useApolloClient();
|
||||||
|
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||||
|
|
||||||
|
const generateCombinedFindManyRecordsQuery = (
|
||||||
|
operationSignatures: RecordGqlOperationSignature[],
|
||||||
|
objectMetadataItemsValue: ObjectMetadataItem[],
|
||||||
|
) => {
|
||||||
|
const filterPerMetadataItemArray = operationSignatures
|
||||||
|
.map(
|
||||||
|
({ objectNameSingular }) =>
|
||||||
|
`$filter${capitalize(objectNameSingular)}: ${capitalize(
|
||||||
|
objectNameSingular,
|
||||||
|
)}FilterInput`,
|
||||||
|
)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
const orderByPerMetadataItemArray = operationSignatures
|
||||||
|
.map(
|
||||||
|
({ objectNameSingular }) =>
|
||||||
|
`$orderBy${capitalize(objectNameSingular)}: [${capitalize(
|
||||||
|
objectNameSingular,
|
||||||
|
)}OrderByInput]`,
|
||||||
|
)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
const cursorFilteringPerMetadataItemArray = operationSignatures
|
||||||
|
.map(
|
||||||
|
({ objectNameSingular }) =>
|
||||||
|
`$after${capitalize(objectNameSingular)}: String, $before${capitalize(objectNameSingular)}: String, $first${capitalize(objectNameSingular)}: Int, $last${capitalize(objectNameSingular)}: Int`,
|
||||||
|
)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
const limitPerMetadataItemArray = operationSignatures
|
||||||
|
.map(
|
||||||
|
({ objectNameSingular }) =>
|
||||||
|
`$limit${capitalize(objectNameSingular)}: Int`,
|
||||||
|
)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
const queryOperationSignatureWithObjectMetadataItemArray =
|
||||||
|
operationSignatures.map((operationSignature) => {
|
||||||
|
const objectMetadataItem = objectMetadataItemsValue.find(
|
||||||
|
(objectMetadataItem) =>
|
||||||
|
objectMetadataItem.nameSingular ===
|
||||||
|
operationSignature.objectNameSingular,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isUndefined(objectMetadataItem)) {
|
||||||
|
throw new Error(
|
||||||
|
`Object metadata item not found for object name singular: ${operationSignature.objectNameSingular}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { operationSignature, objectMetadataItem };
|
||||||
|
});
|
||||||
|
|
||||||
|
return gql`
|
||||||
|
query CombinedFindManyRecords(
|
||||||
|
${filterPerMetadataItemArray},
|
||||||
|
${orderByPerMetadataItemArray},
|
||||||
|
${cursorFilteringPerMetadataItemArray},
|
||||||
|
${limitPerMetadataItemArray}
|
||||||
|
) {
|
||||||
|
${queryOperationSignatureWithObjectMetadataItemArray
|
||||||
|
.map(
|
||||||
|
({ objectMetadataItem, operationSignature }) =>
|
||||||
|
`${getCombinedFindManyRecordsQueryFilteringPart(
|
||||||
|
objectMetadataItem,
|
||||||
|
)} {
|
||||||
|
edges {
|
||||||
|
node ${mapObjectMetadataToGraphQLQuery({
|
||||||
|
objectMetadataItems: objectMetadataItemsValue,
|
||||||
|
objectMetadataItem,
|
||||||
|
recordGqlFields:
|
||||||
|
operationSignature.fields ??
|
||||||
|
generateDepthOneRecordGqlFields({
|
||||||
|
objectMetadataItem,
|
||||||
|
}),
|
||||||
|
})}
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
hasPreviousPage
|
||||||
|
startCursor
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
totalCount
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
.join('\n')}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const performCombinedFindManyRecords = async ({
|
||||||
|
operationSignatures,
|
||||||
|
client: customClient,
|
||||||
|
}: {
|
||||||
|
operationSignatures: RecordGqlOperationSignature[];
|
||||||
|
client?: ApolloClient<object>;
|
||||||
|
}) => {
|
||||||
|
const apolloClient = customClient || client;
|
||||||
|
|
||||||
|
const findManyQuery = generateCombinedFindManyRecordsQuery(
|
||||||
|
operationSignatures,
|
||||||
|
objectMetadataItems,
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryVariables = generateCombinedFindManyRecordsQueryVariables({
|
||||||
|
operationSignatures,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data, loading } =
|
||||||
|
await apolloClient.query<CombinedFindManyRecordsQueryResult>({
|
||||||
|
query: findManyQuery ?? EMPTY_QUERY,
|
||||||
|
variables: queryVariables,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultWithoutConnection = Object.fromEntries(
|
||||||
|
Object.entries(data ?? {}).map(([namePlural, objectRecordConnection]) => [
|
||||||
|
namePlural,
|
||||||
|
getRecordsFromRecordConnection({
|
||||||
|
recordConnection: objectRecordConnection,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: resultWithoutConnection,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return { performCombinedFindManyRecords };
|
||||||
|
};
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
export type MorphItem = {
|
||||||
|
recordId: string;
|
||||||
|
objectMetadataId: string;
|
||||||
|
};
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
||||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||||
import { useCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecordsQueryVariables';
|
import { generateCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/utils/generateCombinedFindManyRecordsQueryVariables';
|
||||||
|
|
||||||
describe('useCombinedFindManyRecordsQueryVariables', () => {
|
describe('useCombinedFindManyRecordsQueryVariables', () => {
|
||||||
it('should generate variables with after cursor and first limit', () => {
|
it('should generate variables with after cursor and first limit', () => {
|
||||||
@ -26,7 +26,7 @@ describe('useCombinedFindManyRecordsQueryVariables', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = useCombinedFindManyRecordsQueryVariables({
|
const result = generateCombinedFindManyRecordsQueryVariables({
|
||||||
operationSignatures,
|
operationSignatures,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ describe('useCombinedFindManyRecordsQueryVariables', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = useCombinedFindManyRecordsQueryVariables({
|
const result = generateCombinedFindManyRecordsQueryVariables({
|
||||||
operationSignatures,
|
operationSignatures,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ describe('useCombinedFindManyRecordsQueryVariables', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = useCombinedFindManyRecordsQueryVariables({
|
const result = generateCombinedFindManyRecordsQueryVariables({
|
||||||
operationSignatures,
|
operationSignatures,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -125,7 +125,7 @@ describe('useCombinedFindManyRecordsQueryVariables', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = useCombinedFindManyRecordsQueryVariables({
|
const result = generateCombinedFindManyRecordsQueryVariables({
|
||||||
operationSignatures,
|
operationSignatures,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ describe('useCombinedFindManyRecordsQueryVariables', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle empty operation signatures', () => {
|
it('should handle empty operation signatures', () => {
|
||||||
const result = useCombinedFindManyRecordsQueryVariables({
|
const result = generateCombinedFindManyRecordsQueryVariables({
|
||||||
operationSignatures: [],
|
operationSignatures: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -157,7 +157,7 @@ describe('useCombinedFindManyRecordsQueryVariables', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = useCombinedFindManyRecordsQueryVariables({
|
const result = generateCombinedFindManyRecordsQueryVariables({
|
||||||
operationSignatures,
|
operationSignatures,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -180,7 +180,7 @@ describe('useCombinedFindManyRecordsQueryVariables', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = useCombinedFindManyRecordsQueryVariables({
|
const result = generateCombinedFindManyRecordsQueryVariables({
|
||||||
operationSignatures,
|
operationSignatures,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -3,7 +3,7 @@ import { isNonEmptyString } from '@sniptt/guards';
|
|||||||
import { capitalize, isDefined } from 'twenty-shared';
|
import { capitalize, isDefined } from 'twenty-shared';
|
||||||
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
|
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
|
||||||
|
|
||||||
export const useCombinedFindManyRecordsQueryVariables = ({
|
export const generateCombinedFindManyRecordsQueryVariables = ({
|
||||||
operationSignatures,
|
operationSignatures,
|
||||||
}: {
|
}: {
|
||||||
operationSignatures: RecordGqlOperationSignature[];
|
operationSignatures: RecordGqlOperationSignature[];
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { capitalize } from 'twenty-shared';
|
||||||
|
|
||||||
|
export const getLimitPerMetadataItem = (
|
||||||
|
objectMetadataItems: Pick<ObjectMetadataItem, 'nameSingular'>[],
|
||||||
|
limit: number,
|
||||||
|
) => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
objectMetadataItems.map(({ nameSingular }) => {
|
||||||
|
return [`limit${capitalize(nameSingular)}`, limit];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
|
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
|
||||||
import { generateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery';
|
import { generateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery';
|
||||||
|
import { getLimitPerMetadataItem } from '@/object-record/multiple-objects/utils/getLimitPerMetadataItem';
|
||||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
||||||
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
|
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
|
||||||
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
|
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
|
||||||
@ -222,12 +223,9 @@ const performSearchForPickedRecords = async ({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const limitPerMetadataItem = Object.fromEntries(
|
const limitPerMetadataItem = getLimitPerMetadataItem(
|
||||||
searchableObjectMetadataItems
|
searchableObjectMetadataItemsFilteredOnPickedRecordId,
|
||||||
.map(({ nameSingular }) => {
|
10,
|
||||||
return [`limit${capitalize(nameSingular)}`, 10];
|
|
||||||
})
|
|
||||||
.filter(isDefined),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: combinedSearchRecordFilteredOnPickedRecordsQueryResult } =
|
const { data: combinedSearchRecordFilteredOnPickedRecordsQueryResult } =
|
||||||
@ -309,12 +307,9 @@ const performSearchExcludingPickedRecords = async ({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const limitPerMetadataItem = Object.fromEntries(
|
const limitPerMetadataItem = getLimitPerMetadataItem(
|
||||||
searchableObjectMetadataItems
|
searchableObjectMetadataItems,
|
||||||
.map(({ nameSingular }) => {
|
10,
|
||||||
return [`limit${capitalize(nameSingular)}`, 10];
|
|
||||||
})
|
|
||||||
.filter(isDefined),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: combinedSearchRecordExcludingPickedRecordsQueryResult } =
|
const { data: combinedSearchRecordExcludingPickedRecordsQueryResult } =
|
||||||
|
|||||||
Reference in New Issue
Block a user