361 create a navigation stack for the command menu (#9995)
Closes https://github.com/twentyhq/core-team-issues/issues/361 - Created navigation stack state - Created navigation functions inside the `useCommandMenu` hook - Added tests
This commit is contained in:
@ -1,25 +1,17 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { IconSearch } from 'twenty-ui';
|
||||
|
||||
export const useSearchRecordsRecordAgnosticAction = () => {
|
||||
const { openCommandMenu } = useCommandMenu();
|
||||
const { navigateCommandMenu } = useCommandMenu();
|
||||
|
||||
const onClick = useRecoilCallback(
|
||||
({ set }) =>
|
||||
() => {
|
||||
set(commandMenuPageState, CommandMenuPages.SearchRecords);
|
||||
set(commandMenuPageInfoState, {
|
||||
title: 'Search',
|
||||
Icon: IconSearch,
|
||||
});
|
||||
openCommandMenu();
|
||||
},
|
||||
[openCommandMenu],
|
||||
);
|
||||
const onClick = () => {
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
onClick,
|
||||
|
||||
@ -4,7 +4,12 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
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 { IconSearch } from 'twenty-ui';
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
@ -22,10 +27,18 @@ const renderHooks = () => {
|
||||
() => {
|
||||
const commandMenu = useCommandMenu();
|
||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||
const commandMenuNavigationStack = useRecoilValue(
|
||||
commandMenuNavigationStackState,
|
||||
);
|
||||
const commandMenuPage = useRecoilValue(commandMenuPageState);
|
||||
const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState);
|
||||
|
||||
return {
|
||||
commandMenu,
|
||||
isCommandMenuOpened,
|
||||
commandMenuNavigationStack,
|
||||
commandMenuPage,
|
||||
commandMenuPageInfo,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -69,4 +82,142 @@ describe('useCommandMenu', () => {
|
||||
|
||||
expect(result.current.isCommandMenuOpened).toBe(false);
|
||||
});
|
||||
|
||||
it('should navigate to a page', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
expect(result.current.commandMenuNavigationStack).toEqual([]);
|
||||
expect(result.current.commandMenuPage).toBe(CommandMenuPages.Root);
|
||||
expect(result.current.commandMenuPageInfo).toEqual({
|
||||
title: undefined,
|
||||
Icon: undefined,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenu({
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.commandMenuNavigationStack).toEqual([
|
||||
{
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
},
|
||||
]);
|
||||
expect(result.current.commandMenuPage).toBe(CommandMenuPages.SearchRecords);
|
||||
expect(result.current.commandMenuPageInfo).toEqual({
|
||||
title: 'Search',
|
||||
Icon: IconSearch,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenu({
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: 'View Record',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.commandMenuNavigationStack).toEqual([
|
||||
{
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
},
|
||||
{
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: 'View Record',
|
||||
},
|
||||
]);
|
||||
expect(result.current.commandMenuPage).toBe(CommandMenuPages.ViewRecord);
|
||||
expect(result.current.commandMenuPageInfo).toEqual({
|
||||
title: 'View Record',
|
||||
Icon: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should go back from a page', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenu({
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenu({
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: 'View Record',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.commandMenuNavigationStack).toEqual([
|
||||
{
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
},
|
||||
{
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: 'View Record',
|
||||
},
|
||||
]);
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.goBackFromCommandMenu();
|
||||
});
|
||||
|
||||
expect(result.current.commandMenuNavigationStack).toEqual([
|
||||
{
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
},
|
||||
]);
|
||||
expect(result.current.commandMenuPage).toBe(CommandMenuPages.SearchRecords);
|
||||
expect(result.current.commandMenuPageInfo).toEqual({
|
||||
title: 'Search',
|
||||
Icon: IconSearch,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.goBackFromCommandMenu();
|
||||
});
|
||||
|
||||
expect(result.current.commandMenuNavigationStack).toEqual([]);
|
||||
expect(result.current.commandMenuPage).toBe(CommandMenuPages.Root);
|
||||
expect(result.current.commandMenuPageInfo).toEqual({
|
||||
title: undefined,
|
||||
Icon: undefined,
|
||||
});
|
||||
expect(result.current.isCommandMenuOpened).toBe(false);
|
||||
});
|
||||
|
||||
it('should navigate to a page in history', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenu({
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenuHistory(0);
|
||||
});
|
||||
|
||||
expect(result.current.commandMenuPage).toBe(CommandMenuPages.SearchRecords);
|
||||
expect(result.current.commandMenuPageInfo).toEqual({
|
||||
title: 'Search',
|
||||
Icon: IconSearch,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -7,6 +7,10 @@ import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||
|
||||
import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates';
|
||||
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
|
||||
import {
|
||||
CommandMenuNavigationStackItem,
|
||||
commandMenuNavigationStackState,
|
||||
} from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
@ -19,6 +23,7 @@ import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { IconSearch } from 'twenty-ui';
|
||||
import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState';
|
||||
|
||||
@ -73,6 +78,7 @@ export const useCommandMenu = () => {
|
||||
});
|
||||
set(isCommandMenuOpenedState, false);
|
||||
set(commandMenuSearchState, '');
|
||||
set(commandMenuNavigationStackState, []);
|
||||
resetSelectedItem();
|
||||
goBackToPreviousHotkeyScope();
|
||||
|
||||
@ -100,32 +106,108 @@ export const useCommandMenu = () => {
|
||||
[closeCommandMenu, openCommandMenu],
|
||||
);
|
||||
|
||||
const openRecordInCommandMenu = useRecoilCallback(
|
||||
({ set }) => {
|
||||
return (recordId: string, objectNameSingular: string) => {
|
||||
const navigateCommandMenu = useRecoilCallback(
|
||||
({ snapshot, set }) => {
|
||||
return ({
|
||||
page,
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
}: CommandMenuNavigationStackItem) => {
|
||||
set(commandMenuPageState, page);
|
||||
set(commandMenuPageInfoState, {
|
||||
title: pageTitle,
|
||||
Icon: pageIcon,
|
||||
});
|
||||
|
||||
const currentNavigationStack = snapshot
|
||||
.getLoadable(commandMenuNavigationStackState)
|
||||
.getValue();
|
||||
|
||||
set(commandMenuNavigationStackState, [
|
||||
...currentNavigationStack,
|
||||
{ page, pageTitle, pageIcon },
|
||||
]);
|
||||
openCommandMenu();
|
||||
set(commandMenuPageState, CommandMenuPages.ViewRecord);
|
||||
set(viewableRecordNameSingularState, objectNameSingular);
|
||||
set(viewableRecordIdState, recordId);
|
||||
};
|
||||
},
|
||||
[openCommandMenu],
|
||||
);
|
||||
|
||||
const openRecordsSearchPage = useRecoilCallback(
|
||||
({ set }) => {
|
||||
const goBackFromCommandMenu = useRecoilCallback(
|
||||
({ snapshot, set }) => {
|
||||
return () => {
|
||||
set(commandMenuPageState, CommandMenuPages.SearchRecords);
|
||||
const currentNavigationStack = snapshot
|
||||
.getLoadable(commandMenuNavigationStackState)
|
||||
.getValue();
|
||||
|
||||
const newNavigationStack = currentNavigationStack.slice(0, -1);
|
||||
const lastNavigationStackItem = newNavigationStack.at(-1);
|
||||
|
||||
if (!isDefined(lastNavigationStackItem)) {
|
||||
closeCommandMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
set(commandMenuPageState, lastNavigationStackItem.page);
|
||||
|
||||
set(commandMenuPageInfoState, {
|
||||
title: 'Search',
|
||||
Icon: IconSearch,
|
||||
title: lastNavigationStackItem.pageTitle,
|
||||
Icon: lastNavigationStackItem.pageIcon,
|
||||
});
|
||||
openCommandMenu();
|
||||
|
||||
set(commandMenuNavigationStackState, newNavigationStack);
|
||||
};
|
||||
},
|
||||
[openCommandMenu],
|
||||
[closeCommandMenu],
|
||||
);
|
||||
|
||||
const navigateCommandMenuHistory = useRecoilCallback(({ snapshot, set }) => {
|
||||
return (pageIndex: number) => {
|
||||
const currentNavigationStack = snapshot
|
||||
.getLoadable(commandMenuNavigationStackState)
|
||||
.getValue();
|
||||
|
||||
const newNavigationStack = currentNavigationStack.slice(0, pageIndex + 1);
|
||||
|
||||
set(commandMenuNavigationStackState, newNavigationStack);
|
||||
|
||||
const newNavigationStackItem = newNavigationStack.at(-1);
|
||||
|
||||
if (!isDefined(newNavigationStackItem)) {
|
||||
throw new Error(
|
||||
`No command menu navigation stack item found for index ${pageIndex}`,
|
||||
);
|
||||
}
|
||||
|
||||
set(commandMenuPageState, newNavigationStackItem?.page);
|
||||
set(commandMenuPageInfoState, {
|
||||
title: newNavigationStackItem?.pageTitle,
|
||||
Icon: newNavigationStackItem?.pageIcon,
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const openRecordInCommandMenu = useRecoilCallback(
|
||||
({ set }) => {
|
||||
return (recordId: string, objectNameSingular: string) => {
|
||||
set(viewableRecordNameSingularState, objectNameSingular);
|
||||
set(viewableRecordIdState, recordId);
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
});
|
||||
};
|
||||
},
|
||||
[navigateCommandMenu],
|
||||
);
|
||||
|
||||
const openRecordsSearchPage = () => {
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
});
|
||||
};
|
||||
|
||||
const setGlobalCommandMenuContext = useRecoilCallback(
|
||||
({ set }) => {
|
||||
return () => {
|
||||
@ -177,6 +259,9 @@ export const useCommandMenu = () => {
|
||||
return {
|
||||
openCommandMenu,
|
||||
closeCommandMenu,
|
||||
navigateCommandMenu,
|
||||
navigateCommandMenuHistory,
|
||||
goBackFromCommandMenu,
|
||||
openRecordsSearchPage,
|
||||
openRecordInCommandMenu,
|
||||
toggleCommandMenu,
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { IconComponent, createState } from 'twenty-ui';
|
||||
|
||||
export type CommandMenuNavigationStackItem = {
|
||||
page: CommandMenuPages;
|
||||
pageTitle?: string;
|
||||
pageIcon?: IconComponent;
|
||||
};
|
||||
|
||||
export const commandMenuNavigationStackState = createState<
|
||||
CommandMenuNavigationStackItem[]
|
||||
>({
|
||||
key: 'command-menu/commandMenuNavigationStackState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -4,8 +4,6 @@ import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isR
|
||||
import { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/rightDrawerCloseEventsState';
|
||||
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
|
||||
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
|
||||
import { mapRightDrawerPageToCommandMenuPage } from '@/ui/layout/right-drawer/utils/mapRightDrawerPageToCommandMenuPage';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
@ -25,7 +23,7 @@ export const useRightDrawer = () => {
|
||||
FeatureFlagKey.IsCommandMenuV2Enabled,
|
||||
);
|
||||
|
||||
const { openCommandMenu } = useCommandMenu();
|
||||
const { navigateCommandMenu } = useCommandMenu();
|
||||
|
||||
const openRightDrawer = useRecoilCallback(
|
||||
({ set }) =>
|
||||
@ -40,12 +38,12 @@ export const useRightDrawer = () => {
|
||||
const commandMenuPage =
|
||||
mapRightDrawerPageToCommandMenuPage(rightDrawerPage);
|
||||
|
||||
set(commandMenuPageState, commandMenuPage);
|
||||
set(commandMenuPageInfoState, {
|
||||
title: commandMenuPageInfo?.title,
|
||||
Icon: commandMenuPageInfo?.Icon,
|
||||
navigateCommandMenu({
|
||||
page: commandMenuPage,
|
||||
pageTitle: commandMenuPageInfo?.title,
|
||||
pageIcon: commandMenuPageInfo?.Icon,
|
||||
});
|
||||
openCommandMenu();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -53,7 +51,7 @@ export const useRightDrawer = () => {
|
||||
set(isRightDrawerOpenState, true);
|
||||
set(isRightDrawerMinimizedState, false);
|
||||
},
|
||||
[isCommandMenuV2Enabled, openCommandMenu],
|
||||
[isCommandMenuV2Enabled, navigateCommandMenu],
|
||||
);
|
||||
|
||||
const closeRightDrawer = useRecoilCallback(
|
||||
|
||||
Reference in New Issue
Block a user