491 save the page component instance id for side panel navigation (#10700)

Closes https://github.com/twentyhq/core-team-issues/issues/491

This PR:
- Duplicates the right drawer pages for the command menu and replace all
the states used in these pages by component states (The right drawer
pages will be deleted when we deprecate the command menu v1)
- Wraps those pages into a component instance provider
- We store the component instance id upon navigation to restore the
states when we navigate back to a page

The only pages which are not updated for now are the pages related to
the workflow objects, this will be done in another PR.
In another PR, to improve the navigation experience I will replace the
icons and titles of the chips by the label identifier and the avatar if
the page is a record page.


https://github.com/user-attachments/assets/a76d3345-01f3-4db9-8a55-331cca8b87e0

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Raphaël Bosi
2025-03-06 17:09:40 +01:00
committed by GitHub
parent 37d7c0c994
commit e86116aa57
30 changed files with 1255 additions and 80 deletions

View File

@ -1,6 +1,7 @@
import { COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/command-menu/constants/CommandMenuContextChipGroupsDropdownId';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { isDefined } from 'twenty-shared';
import { MenuItem } from 'twenty-ui';
@ -14,6 +15,8 @@ export const CommandMenuContextChipGroups = ({
}: {
contextChips: CommandMenuContextChipProps[];
}) => {
const { closeDropdown } = useDropdownV2();
if (contextChips.length === 0) {
return null;
}
@ -34,6 +37,7 @@ export const CommandMenuContextChipGroups = ({
}
const firstChips = contextChips.slice(0, -1);
const firstThreeChips = firstChips.slice(0, 3);
const lastChip = contextChips.at(-1);
return (
@ -42,8 +46,9 @@ export const CommandMenuContextChipGroups = ({
<Dropdown
clickableComponent={
<CommandMenuContextChip
Icons={firstChips.map((chip) => chip.Icons?.[0])}
Icons={firstThreeChips.map((chip) => chip.Icons?.[0])}
onClick={() => {}}
text={`${firstChips.length}`}
/>
}
dropdownComponents={
@ -52,7 +57,10 @@ export const CommandMenuContextChipGroups = ({
<MenuItem
LeftComponent={chip.Icons}
text={chip.text}
onClick={chip.onClick}
onClick={() => {
closeDropdown(COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID);
chip.onClick?.();
}}
/>
))}
</DropdownMenuItemsContainer>

View File

@ -4,7 +4,7 @@ import { getSelectedRecordsContextText } from '@/command-menu/utils/getRecordCon
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
export const CommandMenuContextRecordChip = ({
export const CommandMenuContextRecordsChip = ({
objectMetadataItemId,
instanceId,
}: {

View File

@ -1,7 +1,9 @@
import { CommandMenuContainer } from '@/command-menu/components/CommandMenuContainer';
import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar';
import { COMMAND_MENU_PAGES_CONFIG } from '@/command-menu/constants/CommandMenuPagesConfig';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
@ -16,6 +18,8 @@ const StyledCommandMenuContent = styled.div`
export const CommandMenuRouter = () => {
const commandMenuPage = useRecoilValue(commandMenuPageState);
const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState);
const commandMenuPageComponent = isDefined(commandMenuPage) ? (
COMMAND_MENU_PAGES_CONFIG.get(commandMenuPage)
) : (
@ -26,20 +30,24 @@ export const CommandMenuRouter = () => {
return (
<CommandMenuContainer>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
duration: theme.animation.duration.instant,
delay: 0.1,
}}
<CommandMenuPageComponentInstanceContext.Provider
value={{ instanceId: commandMenuPageInfo.instanceId }}
>
<CommandMenuTopBar />
</motion.div>
<StyledCommandMenuContent>
{commandMenuPageComponent}
</StyledCommandMenuContent>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
duration: theme.animation.duration.instant,
delay: 0.1,
}}
>
<CommandMenuTopBar />
</motion.div>
<StyledCommandMenuContent>
{commandMenuPageComponent}
</StyledCommandMenuContent>
</CommandMenuPageComponentInstanceContext.Provider>
</CommandMenuContainer>
);
};

View File

@ -128,16 +128,21 @@ export const CommandMenuTopBar = () => {
(page) => page.page !== CommandMenuPages.Root,
);
return filteredCommandMenuNavigationStack.map((page, index) => ({
Icons: [<page.pageIcon size={theme.icon.size.sm} />],
text: page.pageTitle,
onClick:
index === filteredCommandMenuNavigationStack.length - 1
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,

View File

@ -1,4 +1,4 @@
import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip';
import { CommandMenuContextRecordsChip } from '@/command-menu/components/CommandMenuContextRecordsChip';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { RESET_CONTEXT_TO_SELECTION } from '@/command-menu/constants/ResetContextToSelection';
import { useResetPreviousCommandMenuContext } from '@/command-menu/hooks/useResetPreviousCommandMenuContext';
@ -46,7 +46,7 @@ export const ResetContextToSelectionCommandButton = () => {
Icon={IconArrowBackUp}
label={t`Reset to`}
RightComponent={
<CommandMenuContextRecordChip
<CommandMenuContextRecordsChip
objectMetadataItemId={objectMetadataItem.id}
instanceId="command-menu-previous"
/>

View File

@ -100,6 +100,7 @@ const meta: Meta<typeof CommandMenu> = {
page: CommandMenuPages.Root,
pageTitle: 'Command Menu',
pageIcon: IconDotsVertical,
pageId: '1',
},
]);

View File

@ -1,7 +1,7 @@
import { gql } from '@apollo/client';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip';
import { CommandMenuContextRecordsChip } from '@/command-menu/components/CommandMenuContextRecordsChip';
import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext';
import { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
@ -207,9 +207,9 @@ const ContextStoreDecorator: Decorator = (Story) => {
);
};
const meta: Meta<typeof CommandMenuContextRecordChip> = {
const meta: Meta<typeof CommandMenuContextRecordsChip> = {
title: 'Modules/CommandMenu/CommandMenuContextRecordChip',
component: CommandMenuContextRecordChip,
component: CommandMenuContextRecordsChip,
decorators: [
ContextStoreDecorator,
ChipGeneratorsDecorator,
@ -221,7 +221,7 @@ const meta: Meta<typeof CommandMenuContextRecordChip> = {
};
export default meta;
type Story = StoryObj<typeof CommandMenuContextRecordChip>;
type Story = StoryObj<typeof CommandMenuContextRecordsChip>;
export const Default: Story = {};