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:
Raphaël Bosi
2025-03-12 15:26:14 +01:00
committed by GitHub
parent e030fc8917
commit bfc542290b
22 changed files with 620 additions and 174 deletions

View File

@ -47,6 +47,7 @@ export const CommandMenuContextChipGroupsWithRecordSelection = ({
),
Icons: Avatars,
onClick: contextChips.length > 0 ? openRootCommandMenu : undefined,
withIconBackground: false,
}
: undefined;

View File

@ -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;
};

View File

@ -32,7 +32,6 @@ export const CommandMenuContextRecordChipAvatars = ({
objectNameSingular: objectMetadataItem.nameSingular,
record,
});
const { Icon, IconColor } = useGetStandardObjectIcon(
objectMetadataItem.nameSingular,
);

View File

@ -1,4 +1,5 @@
import { CommandMenuContainer } from '@/command-menu/components/CommandMenuContainer';
import { CommandMenuContextChipRecordSetterEffect } from '@/command-menu/components/CommandMenuContextChipRecordSetterEffect';
import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar';
import { COMMAND_MENU_PAGES_CONFIG } from '@/command-menu/constants/CommandMenuPagesConfig';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
@ -30,6 +31,7 @@ export const CommandMenuRouter = () => {
return (
<CommandMenuContainer>
<CommandMenuContextChipRecordSetterEffect />
<CommandMenuPageComponentInstanceContext.Provider
value={{ instanceId: commandMenuPageInfo.instanceId }}
>

View File

@ -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_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
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 { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
@ -16,7 +16,7 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { AnimatePresence, motion } from 'framer-motion';
import { useMemo, useRef } from 'react';
import { useRef } from 'react';
import { useLocation } from 'react-router-dom';
import { useRecoilState, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
@ -100,11 +100,7 @@ export const CommandMenuTopBar = () => {
const isMobile = useIsMobile();
const {
closeCommandMenu,
goBackFromCommandMenu,
navigateCommandMenuHistory,
} = useCommandMenu();
const { closeCommandMenu, goBackFromCommandMenu } = useCommandMenu();
const contextStoreCurrentObjectMetadataItem = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataItemComponentState,
@ -112,42 +108,13 @@ export const CommandMenuTopBar = () => {
const commandMenuPage = useRecoilValue(commandMenuPageState);
const commandMenuNavigationStack = useRecoilValue(
commandMenuNavigationStackState,
);
const theme = useTheme();
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const contextChips = useMemo(() => {
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 { contextChips } = useCommandMenuContextChips();
const location = useLocation();
const isButtonVisible =