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,
|
||||
onClick: contextChips.length > 0 ? openRootCommandMenu : undefined,
|
||||
withIconBackground: false,
|
||||
}
|
||||
: 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,
|
||||
record,
|
||||
});
|
||||
|
||||
const { Icon, IconColor } = useGetStandardObjectIcon(
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
@ -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 }}
|
||||
>
|
||||
|
||||
@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user