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:
@ -5,8 +5,8 @@ import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { IconList, IconSearch } from 'twenty-ui';
|
||||
@ -91,6 +91,7 @@ describe('useCommandMenu', () => {
|
||||
expect(result.current.commandMenuPageInfo).toEqual({
|
||||
title: undefined,
|
||||
Icon: undefined,
|
||||
instanceId: '',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
@ -98,6 +99,7 @@ describe('useCommandMenu', () => {
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
pageId: '1',
|
||||
});
|
||||
});
|
||||
|
||||
@ -106,12 +108,14 @@ describe('useCommandMenu', () => {
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
pageId: '1',
|
||||
},
|
||||
]);
|
||||
expect(result.current.commandMenuPage).toBe(CommandMenuPages.SearchRecords);
|
||||
expect(result.current.commandMenuPageInfo).toEqual({
|
||||
title: 'Search',
|
||||
Icon: IconSearch,
|
||||
instanceId: '1',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
@ -119,6 +123,7 @@ describe('useCommandMenu', () => {
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: 'Company',
|
||||
pageIcon: IconList,
|
||||
pageId: '2',
|
||||
});
|
||||
});
|
||||
|
||||
@ -127,17 +132,20 @@ describe('useCommandMenu', () => {
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
pageId: '1',
|
||||
},
|
||||
{
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: 'Company',
|
||||
pageIcon: IconList,
|
||||
pageId: '2',
|
||||
},
|
||||
]);
|
||||
expect(result.current.commandMenuPage).toBe(CommandMenuPages.ViewRecord);
|
||||
expect(result.current.commandMenuPageInfo).toEqual({
|
||||
title: 'Company',
|
||||
Icon: IconList,
|
||||
instanceId: '2',
|
||||
});
|
||||
});
|
||||
|
||||
@ -149,6 +157,7 @@ describe('useCommandMenu', () => {
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
pageId: '1',
|
||||
});
|
||||
});
|
||||
|
||||
@ -157,6 +166,7 @@ describe('useCommandMenu', () => {
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: 'Company',
|
||||
pageIcon: IconList,
|
||||
pageId: '2',
|
||||
});
|
||||
});
|
||||
|
||||
@ -165,11 +175,13 @@ describe('useCommandMenu', () => {
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
pageId: '1',
|
||||
},
|
||||
{
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: 'Company',
|
||||
pageIcon: IconList,
|
||||
pageId: '2',
|
||||
},
|
||||
]);
|
||||
|
||||
@ -182,12 +194,14 @@ describe('useCommandMenu', () => {
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
pageId: '1',
|
||||
},
|
||||
]);
|
||||
expect(result.current.commandMenuPage).toBe(CommandMenuPages.SearchRecords);
|
||||
expect(result.current.commandMenuPageInfo).toEqual({
|
||||
title: 'Search',
|
||||
Icon: IconSearch,
|
||||
instanceId: '1',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
@ -199,6 +213,7 @@ describe('useCommandMenu', () => {
|
||||
expect(result.current.commandMenuPage).toBe(CommandMenuPages.Root);
|
||||
expect(result.current.commandMenuPageInfo).toEqual({
|
||||
title: undefined,
|
||||
instanceId: '',
|
||||
Icon: undefined,
|
||||
});
|
||||
expect(result.current.isCommandMenuOpened).toBe(false);
|
||||
@ -212,6 +227,7 @@ describe('useCommandMenu', () => {
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
pageId: '1',
|
||||
});
|
||||
});
|
||||
|
||||
@ -223,6 +239,7 @@ describe('useCommandMenu', () => {
|
||||
expect(result.current.commandMenuPageInfo).toEqual({
|
||||
title: 'Search',
|
||||
Icon: IconSearch,
|
||||
instanceId: '1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -5,19 +5,25 @@ import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objec
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||
import { IconDotsVertical, IconSearch, useIcons } from 'twenty-ui';
|
||||
import {
|
||||
IconCalendarEvent,
|
||||
IconComponent,
|
||||
IconDotsVertical,
|
||||
IconMail,
|
||||
IconSearch,
|
||||
useIcons,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/command-menu/constants/CommandMenuContextChipGroupsDropdownId';
|
||||
import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId';
|
||||
import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates';
|
||||
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
|
||||
import {
|
||||
CommandMenuNavigationStackItem,
|
||||
commandMenuNavigationStackState,
|
||||
} from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
|
||||
import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
|
||||
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
|
||||
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
@ -29,17 +35,23 @@ import { contextStoreFiltersComponentState } from '@/context-store/states/contex
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { RIGHT_DRAWER_RECORD_INSTANCE_ID } from '@/object-record/record-right-drawer/constants/RightDrawerRecordInstanceId';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
|
||||
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
|
||||
import { isDragSelectionStartEnabledState } from '@/ui/utilities/drag-select/states/internal/isDragSelectionStartEnabledState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useCallback } from 'react';
|
||||
import { capitalize, isDefined } from 'twenty-shared';
|
||||
import { v4 } from 'uuid';
|
||||
import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState';
|
||||
|
||||
export type CommandMenuNavigationStackItem = {
|
||||
page: CommandMenuPages;
|
||||
pageTitle: string;
|
||||
pageIcon: IconComponent;
|
||||
pageId?: string;
|
||||
};
|
||||
|
||||
export const useCommandMenu = () => {
|
||||
const { resetSelectedItem } = useSelectableList('command-menu-list');
|
||||
const {
|
||||
@ -76,6 +88,7 @@ export const useCommandMenu = () => {
|
||||
set(commandMenuPageInfoState, {
|
||||
title: undefined,
|
||||
Icon: undefined,
|
||||
instanceId: '',
|
||||
});
|
||||
set(isCommandMenuOpenedState, false);
|
||||
set(commandMenuSearchState, '');
|
||||
@ -138,15 +151,21 @@ export const useCommandMenu = () => {
|
||||
page,
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
pageId,
|
||||
resetNavigationStack = false,
|
||||
}: CommandMenuNavigationStackItem & {
|
||||
resetNavigationStack?: boolean;
|
||||
}) => {
|
||||
if (!pageId) {
|
||||
pageId = v4();
|
||||
}
|
||||
|
||||
openCommandMenu();
|
||||
set(commandMenuPageState, page);
|
||||
set(commandMenuPageInfoState, {
|
||||
title: pageTitle,
|
||||
Icon: pageIcon,
|
||||
instanceId: pageId,
|
||||
});
|
||||
|
||||
const isCommandMenuClosing = snapshot
|
||||
@ -157,16 +176,24 @@ export const useCommandMenu = () => {
|
||||
? []
|
||||
: snapshot.getLoadable(commandMenuNavigationStackState).getValue();
|
||||
|
||||
const itemIsAlreadyInStack = currentNavigationStack.some(
|
||||
(item) => item.page === page,
|
||||
);
|
||||
|
||||
if (resetNavigationStack || itemIsAlreadyInStack) {
|
||||
set(commandMenuNavigationStackState, [{ page, pageTitle, pageIcon }]);
|
||||
if (resetNavigationStack) {
|
||||
set(commandMenuNavigationStackState, [
|
||||
{
|
||||
page,
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
pageId,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
set(commandMenuNavigationStackState, [
|
||||
...currentNavigationStack,
|
||||
{ page, pageTitle, pageIcon },
|
||||
{
|
||||
page,
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
pageId,
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
@ -221,6 +248,7 @@ export const useCommandMenu = () => {
|
||||
set(commandMenuPageInfoState, {
|
||||
title: lastNavigationStackItem.pageTitle,
|
||||
Icon: lastNavigationStackItem.pageIcon,
|
||||
instanceId: lastNavigationStackItem.pageId,
|
||||
});
|
||||
|
||||
set(commandMenuNavigationStackState, newNavigationStack);
|
||||
@ -252,6 +280,7 @@ export const useCommandMenu = () => {
|
||||
set(commandMenuPageInfoState, {
|
||||
title: newNavigationStackItem?.pageTitle,
|
||||
Icon: newNavigationStackItem?.pageIcon,
|
||||
instanceId: newNavigationStackItem?.pageId,
|
||||
});
|
||||
|
||||
set(hasUserSelectedCommandState, false);
|
||||
@ -269,7 +298,20 @@ export const useCommandMenu = () => {
|
||||
objectNameSingular: string;
|
||||
isNewRecord?: boolean;
|
||||
}) => {
|
||||
set(viewableRecordNameSingularState, objectNameSingular);
|
||||
const pageComponentInstanceId = v4();
|
||||
|
||||
set(
|
||||
viewableRecordNameSingularComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
objectNameSingular,
|
||||
);
|
||||
set(
|
||||
viewableRecordIdComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
recordId,
|
||||
);
|
||||
set(viewableRecordIdState, recordId);
|
||||
|
||||
const objectMetadataItem = snapshot
|
||||
@ -289,14 +331,14 @@ export const useCommandMenu = () => {
|
||||
|
||||
set(
|
||||
contextStoreCurrentObjectMetadataItemComponentState.atomFamily({
|
||||
instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID,
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
objectMetadataItem,
|
||||
);
|
||||
|
||||
set(
|
||||
contextStoreTargetedRecordsRuleComponentState.atomFamily({
|
||||
instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID,
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
{
|
||||
mode: 'selection',
|
||||
@ -306,21 +348,21 @@ export const useCommandMenu = () => {
|
||||
|
||||
set(
|
||||
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
|
||||
instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID,
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
1,
|
||||
);
|
||||
|
||||
set(
|
||||
contextStoreCurrentViewTypeComponentState.atomFamily({
|
||||
instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID,
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
ContextStoreViewType.ShowPage,
|
||||
);
|
||||
|
||||
set(
|
||||
contextStoreCurrentViewIdComponentState.atomFamily({
|
||||
instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID,
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
snapshot
|
||||
.getLoadable(
|
||||
@ -343,8 +385,8 @@ export const useCommandMenu = () => {
|
||||
? t`New ${capitalizedObjectNameSingular}`
|
||||
: capitalizedObjectNameSingular,
|
||||
pageIcon: Icon,
|
||||
// TODO: remove this once we can store the navigation stack page states
|
||||
resetNavigationStack: true,
|
||||
pageId: pageComponentInstanceId,
|
||||
resetNavigationStack: false,
|
||||
});
|
||||
};
|
||||
},
|
||||
@ -356,9 +398,56 @@ export const useCommandMenu = () => {
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
pageId: v4(),
|
||||
});
|
||||
};
|
||||
|
||||
const openCalendarEventInCommandMenu = useRecoilCallback(
|
||||
({ set }) => {
|
||||
return (calendarEventId: string) => {
|
||||
const pageComponentInstanceId = v4();
|
||||
|
||||
set(
|
||||
viewableRecordIdComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
calendarEventId,
|
||||
);
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.ViewCalendarEvent,
|
||||
pageTitle: 'Calendar Event',
|
||||
pageIcon: IconCalendarEvent,
|
||||
pageId: pageComponentInstanceId,
|
||||
});
|
||||
};
|
||||
},
|
||||
[navigateCommandMenu],
|
||||
);
|
||||
|
||||
const openEmailThreadInCommandMenu = useRecoilCallback(
|
||||
({ set }) => {
|
||||
return (emailThreadId: string) => {
|
||||
const pageComponentInstanceId = v4();
|
||||
|
||||
set(
|
||||
viewableRecordIdComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
emailThreadId,
|
||||
);
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.ViewEmailThread,
|
||||
pageTitle: 'Email Thread',
|
||||
pageIcon: IconMail,
|
||||
pageId: pageComponentInstanceId,
|
||||
});
|
||||
};
|
||||
},
|
||||
[navigateCommandMenu],
|
||||
);
|
||||
|
||||
const setGlobalCommandMenuContext = useRecoilCallback(
|
||||
({ set }) => {
|
||||
return () => {
|
||||
@ -401,6 +490,7 @@ export const useCommandMenu = () => {
|
||||
set(commandMenuPageInfoState, {
|
||||
title: undefined,
|
||||
Icon: undefined,
|
||||
instanceId: '',
|
||||
});
|
||||
|
||||
set(hasUserSelectedCommandState, false);
|
||||
@ -420,5 +510,7 @@ export const useCommandMenu = () => {
|
||||
openRecordInCommandMenu,
|
||||
toggleCommandMenu,
|
||||
setGlobalCommandMenuContext,
|
||||
openCalendarEventInCommandMenu,
|
||||
openEmailThreadInCommandMenu,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user