From 2680f1d6be871d7c900648e1ec2a5678a479d26c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:37:28 +0100 Subject: [PATCH] 583 refactor useCommandMenu hook (#10984) Closes https://github.com/twentyhq/core-team-issues/issues/583 - Split hook into smaller hooks - Create tests --- ...cordIndexActionMenuBarAllActionsButton.tsx | 4 +- .../calendar/components/CalendarEventRow.tsx | 5 +- .../emails/components/EmailThreadPreview.tsx | 4 +- .../hooks/useOpenCreateActivityDrawer.ts | 4 +- .../activities/notes/components/NoteCard.tsx | 4 +- .../activities/tasks/components/TaskRow.tsx | 4 +- .../activity/components/EventRowActivity.tsx | 4 +- .../components/CommandMenuContainer.tsx | 12 +- ...nuContextChipGroupsWithRecordSelection.tsx | 4 +- .../components/CommandMenuTopBar.tsx | 5 +- .../hooks/__tests__/useCommandMenu.test.tsx | 164 +---- ...MenuCloseAnimationCompleteCleanup.test.tsx | 249 +++++++ .../__tests__/useCommandMenuHistory.test.tsx | 147 ++++ .../__tests__/useNavigateCommandMenu.test.tsx | 101 +++ ...useOpenCalendarEventInCommandMenu.test.tsx | 89 +++ .../useOpenEmailThreadInCommandMenu.test.tsx | 89 +++ .../useOpenRecordInCommandMenu.test.tsx | 178 +++++ .../useSetGlobalCommandMenuContext.test.tsx | 157 ++++ .../__tests__/useWorkflowCommandMenu.test.tsx | 207 ++++++ .../command-menu/hooks/useCommandMenu.ts | 697 +----------------- ...ommandMenuCloseAnimationCompleteCleanup.ts | 69 ++ .../hooks/useCommandMenuContextChips.tsx | 4 +- .../hooks/useCommandMenuHistory.ts | 103 +++ .../hooks/useCommandMenuHotKeys.ts | 16 +- .../hooks/useNavigateCommandMenu.ts | 137 ++++ .../useOpenCalendarEventInCommandMenu.ts | 63 ++ .../hooks/useOpenEmailThreadInCommandMenu.ts | 62 ++ .../hooks/useOpenRecordInCommandMenu.ts | 158 ++++ .../useOpenRecordsSearchPageInCommandMenu.ts | 22 + .../command-menu/hooks/useSearchRecords.tsx | 4 +- .../hooks/useSetGlobalCommandMenuContext.ts | 70 ++ .../hooks/useWorkflowCommandMenu.ts | 124 ++++ ...ndMenuWorkflowSelectTriggerTypeContent.tsx | 4 +- .../components/MainNavigationDrawerItems.tsx | 4 +- .../components/MobileNavigationBar.tsx | 4 +- .../object-record/components/RecordChip.tsx | 4 +- .../components/RecordBoardCard.tsx | 4 +- .../RecordBoardColumnNewOpportunity.tsx | 4 +- .../useAddNewRecordAndOpenRightDrawer.ts | 4 +- .../hooks/useCreateNewTableRecords.ts | 4 +- .../hooks/useOpenRecordTableCellV2.ts | 4 +- .../WorkflowDiagramCanvasEditableEffect.tsx | 4 +- .../WorkflowDiagramCanvasReadonlyEffect.tsx | 4 +- .../WorkflowRunDiagramCanvasEffect.tsx | 8 +- .../hooks/useStartNodeCreation.ts | 8 +- ...DrawerWorkflowSelectTriggerTypeContent.tsx | 4 +- .../testing/jest/JestContextStoreSetter.tsx | 12 +- ...dataAndApolloMocksAndActionMenuWrapper.tsx | 2 + 48 files changed, 2120 insertions(+), 918 deletions(-) create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuCloseAnimationCompleteCleanup.test.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuHistory.test.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/__tests__/useNavigateCommandMenu.test.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenCalendarEventInCommandMenu.test.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenEmailThreadInCommandMenu.test.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenRecordInCommandMenu.test.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/__tests__/useSetGlobalCommandMenuContext.test.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/__tests__/useWorkflowCommandMenu.test.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup.ts create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHistory.ts create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/useOpenCalendarEventInCommandMenu.ts create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/useOpenEmailThreadInCommandMenu.ts create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordInCommandMenu.ts create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu.ts create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/useSetGlobalCommandMenuContext.ts create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/useWorkflowCommandMenu.ts diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarAllActionsButton.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarAllActionsButton.tsx index 572df3a1d..9ee832a68 100644 --- a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarAllActionsButton.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarAllActionsButton.tsx @@ -38,11 +38,11 @@ const StyledSeparator = styled.div<{ size: 'sm' | 'md' }>` export const RecordIndexActionMenuBarAllActionsButton = () => { const theme = useTheme(); - const { openRootCommandMenu } = useCommandMenu(); + const { openCommandMenu } = useCommandMenu(); return ( <> - + All Actions diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx index 60deb9f74..414d369e0 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx @@ -10,7 +10,7 @@ import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendar import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate'; import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useOpenCalendarEventInCommandMenu } from '@/command-menu/hooks/useOpenCalendarEventInCommandMenu'; import { isDefined } from 'twenty-shared'; import { Avatar, @@ -114,7 +114,8 @@ export const CalendarEventRow = ({ const theme = useTheme(); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const { displayCurrentEventCursor = false } = useContext(CalendarContext); - const { openCalendarEventInCommandMenu } = useCommandMenu(); + const { openCalendarEventInCommandMenu } = + useOpenCalendarEventInCommandMenu(); const startsAt = getCalendarEventStartDate(calendarEvent); const endsAt = getCalendarEventEndDate(calendarEvent); diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx index 5becd7538..383f9d538 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx @@ -3,7 +3,7 @@ import { Avatar, GRAY_SCALE } from 'twenty-ui'; import { ActivityRow } from '@/activities/components/ActivityRow'; import { EmailThreadNotShared } from '@/activities/emails/components/EmailThreadNotShared'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useOpenEmailThreadInCommandMenu } from '@/command-menu/hooks/useOpenEmailThreadInCommandMenu'; import { MessageChannelVisibility, TimelineThread } from '~/generated/graphql'; import { formatToHumanReadableDate } from '~/utils/date-utils'; @@ -68,7 +68,7 @@ type EmailThreadPreviewProps = { }; export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => { - const { openEmailThreadInCommandMenu } = useCommandMenu(); + const { openEmailThreadInCommandMenu } = useOpenEmailThreadInCommandMenu(); const visibility = thread.visibility; diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts index b4ea1537a..d1e02ce47 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts @@ -1,7 +1,6 @@ import { useSetRecoilState } from 'recoil'; import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; @@ -13,6 +12,7 @@ import { Note } from '@/activities/types/Note'; import { NoteTarget } from '@/activities/types/NoteTarget'; import { Task } from '@/activities/types/Task'; import { TaskTarget } from '@/activities/types/TaskTarget'; +import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading'; @@ -53,7 +53,7 @@ export const useOpenCreateActivityDrawer = ({ isUpsertingActivityInDBState, ); - const { openRecordInCommandMenu } = useCommandMenu(); + const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); const openCreateActivityDrawer = async ({ targetableObjects, diff --git a/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx b/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx index 62cfcda79..e3019c9fd 100644 --- a/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx +++ b/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell'; import { Note } from '@/activities/types/Note'; import { getActivityPreview } from '@/activities/utils/getActivityPreview'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFieldContext } from '@/object-record/hooks/useFieldContext'; @@ -68,7 +68,7 @@ export const NoteCard = ({ note: Note; isSingleNote: boolean; }) => { - const { openRecordInCommandMenu } = useCommandMenu(); + const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); const body = getActivityPreview(note?.bodyV2?.blocknote ?? null); diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx index 037ed8ec8..a33ce6977 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx @@ -13,7 +13,7 @@ import { beautifyExactDate, hasDatePassed } from '~/utils/date-utils'; import { ActivityRow } from '@/activities/components/ActivityRow'; import { Task } from '@/activities/types/Task'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFieldContext } from '@/object-record/hooks/useFieldContext'; import { useCompleteTask } from '../hooks/useCompleteTask'; @@ -78,7 +78,7 @@ const StyledCheckboxContainer = styled.div` export const TaskRow = ({ task }: { task: Task }) => { const theme = useTheme(); - const { openRecordInCommandMenu } = useCommandMenu(); + const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); const body = getActivitySummary(task?.bodyV2?.blocknote ?? null); diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/rows/activity/components/EventRowActivity.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/activity/components/EventRowActivity.tsx index 836cdf4a4..07a632c72 100644 --- a/packages/twenty-front/src/modules/activities/timeline-activities/rows/activity/components/EventRowActivity.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/activity/components/EventRowActivity.tsx @@ -5,7 +5,7 @@ import { StyledEventRowItemAction, StyledEventRowItemColumn, } from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { isNonEmptyString } from '@sniptt/guards'; @@ -55,7 +55,7 @@ export const EventRowActivity = ({ ? event.linkedRecordCachedName : 'Untitled'; - const { openRecordInCommandMenu } = useCommandMenu(); + const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); return ( <> diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx index af14c887d..2955ee87a 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx @@ -9,6 +9,7 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/context import { COMMAND_MENU_ANIMATION_VARIANTS } from '@/command-menu/constants/CommandMenuAnimationVariants'; import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup'; import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; @@ -54,11 +55,10 @@ export const CommandMenuContainer = ({ }: { children: React.ReactNode; }) => { - const { - toggleCommandMenu, - closeCommandMenu, - onCommandMenuCloseAnimationComplete, - } = useCommandMenu(); + const { toggleCommandMenu, closeCommandMenu } = useCommandMenu(); + + const { commandMenuCloseAnimationCompleteCleanup } = + useCommandMenuCloseAnimationCompleteCleanup(); const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState); @@ -154,7 +154,7 @@ export const CommandMenuContainer = ({ {isCommandMenuOpened && ( 0 ? openRootCommandMenu : undefined, + onClick: contextChips.length > 0 ? openCommandMenu : undefined, withIconBackground: false, } : undefined; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx index 1c981e474..7f422501a 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx @@ -7,6 +7,7 @@ import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/Command import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenuContextChips } from '@/command-menu/hooks/useCommandMenuContextChips'; +import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; @@ -91,7 +92,9 @@ export const CommandMenuTopBar = () => { const isMobile = useIsMobile(); - const { closeCommandMenu, goBackFromCommandMenu } = useCommandMenu(); + const { closeCommandMenu } = useCommandMenu(); + + const { goBackFromCommandMenu } = useCommandMenuHistory(); const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValueV2( contextStoreCurrentObjectMetadataItemIdComponentState, diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx index f3db8942b..5c54c53ae 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx @@ -8,8 +8,6 @@ import { commandMenuNavigationStackState } from '@/command-menu/states/commandMe import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; -import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; -import { IconList, IconSearch } from 'twenty-ui'; const Wrapper = ({ children }: { children: React.ReactNode }) => ( @@ -53,7 +51,7 @@ describe('useCommandMenu', () => { const { result } = renderHooks(); act(() => { - result.current.commandMenu.openRootCommandMenu(); + result.current.commandMenu.openCommandMenu(); }); expect(result.current.isCommandMenuOpened).toBe(true); @@ -82,164 +80,4 @@ 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, - instanceId: '', - }); - - act(() => { - result.current.commandMenu.navigateCommandMenu({ - page: CommandMenuPages.SearchRecords, - pageTitle: 'Search', - pageIcon: IconSearch, - pageId: '1', - }); - }); - - expect(result.current.commandMenuNavigationStack).toEqual([ - { - 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(() => { - result.current.commandMenu.navigateCommandMenu({ - page: CommandMenuPages.ViewRecord, - pageTitle: 'Company', - pageIcon: IconList, - pageId: '2', - }); - }); - - expect(result.current.commandMenuNavigationStack).toEqual([ - { - 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', - }); - }); - - it('should go back from a page', () => { - const { result } = renderHooks(); - - act(() => { - result.current.commandMenu.navigateCommandMenu({ - page: CommandMenuPages.SearchRecords, - pageTitle: 'Search', - pageIcon: IconSearch, - pageId: '1', - }); - }); - - act(() => { - result.current.commandMenu.navigateCommandMenu({ - page: CommandMenuPages.ViewRecord, - pageTitle: 'Company', - pageIcon: IconList, - pageId: '2', - }); - }); - - expect(result.current.commandMenuNavigationStack).toEqual([ - { - page: CommandMenuPages.SearchRecords, - pageTitle: 'Search', - pageIcon: IconSearch, - pageId: '1', - }, - { - page: CommandMenuPages.ViewRecord, - pageTitle: 'Company', - pageIcon: IconList, - pageId: '2', - }, - ]); - - act(() => { - result.current.commandMenu.goBackFromCommandMenu(); - }); - - expect(result.current.commandMenuNavigationStack).toEqual([ - { - 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(() => { - result.current.commandMenu.goBackFromCommandMenu(); - result.current.commandMenu.onCommandMenuCloseAnimationComplete(); - }); - - expect(result.current.commandMenuNavigationStack).toEqual([]); - expect(result.current.commandMenuPage).toBe(CommandMenuPages.Root); - expect(result.current.commandMenuPageInfo).toEqual({ - title: undefined, - instanceId: '', - 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, - pageId: '1', - }); - }); - - act(() => { - result.current.commandMenu.navigateCommandMenuHistory(0); - }); - - expect(result.current.commandMenuPage).toBe(CommandMenuPages.SearchRecords); - expect(result.current.commandMenuPageInfo).toEqual({ - title: 'Search', - Icon: IconSearch, - instanceId: '1', - }); - }); }); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuCloseAnimationCompleteCleanup.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuCloseAnimationCompleteCleanup.test.tsx new file mode 100644 index 000000000..66bd00bc6 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuCloseAnimationCompleteCleanup.test.tsx @@ -0,0 +1,249 @@ +import { renderHook } from '@testing-library/react'; +import { act } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil'; + +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 { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup'; +import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; +import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; +import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; +import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; +import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; +import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState'; +import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState'; +import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; +import { IconList } from 'twenty-ui'; + +const mockCloseDropdown = jest.fn(); +const mockResetContextStoreStates = jest.fn(); +const mockResetSelectedItem = jest.fn(); +const mockGoBackToPreviousHotkeyScope = jest.fn(); +const mockEmitRightDrawerCloseEvent = jest.fn(); + +jest.mock('@/ui/layout/dropdown/hooks/useDropdownV2', () => ({ + useDropdownV2: () => ({ + closeDropdown: mockCloseDropdown, + }), +})); + +jest.mock('@/command-menu/hooks/useResetContextStoreStates', () => ({ + useResetContextStoreStates: () => ({ + resetContextStoreStates: mockResetContextStoreStates, + }), +})); + +jest.mock('@/ui/layout/selectable-list/hooks/useSelectableList', () => ({ + useSelectableList: () => ({ + resetSelectedItem: mockResetSelectedItem, + }), +})); + +jest.mock('@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope', () => ({ + usePreviousHotkeyScope: () => ({ + goBackToPreviousHotkeyScope: mockGoBackToPreviousHotkeyScope, + }), +})); + +jest.mock('@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent', () => ({ + emitRightDrawerCloseEvent: () => { + mockEmitRightDrawerCloseEvent(); + }, +})); + +const Wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + +describe('useCommandMenuCloseAnimationCompleteCleanup', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const renderHooks = () => { + const { result } = renderHook( + () => { + const { commandMenuCloseAnimationCompleteCleanup } = + useCommandMenuCloseAnimationCompleteCleanup(); + + const commandMenuPage = useRecoilValue(commandMenuPageState); + const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState); + const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState); + const commandMenuSearch = useRecoilValue(commandMenuSearchState); + const commandMenuNavigationStack = useRecoilValue( + commandMenuNavigationStackState, + ); + const commandMenuNavigationRecords = useRecoilValue( + commandMenuNavigationRecordsState, + ); + const commandMenuNavigationMorphItemByPage = useRecoilValue( + commandMenuNavigationMorphItemByPageState, + ); + const hasUserSelectedCommand = useRecoilValue( + hasUserSelectedCommandState, + ); + const isCommandMenuClosing = useRecoilValue(isCommandMenuClosingState); + const viewableRecordId = useRecoilValue(viewableRecordIdState); + + // Get setters for state modification in tests + const setCommandMenuPage = useSetRecoilState(commandMenuPageState); + const setCommandMenuPageInfo = useSetRecoilState( + commandMenuPageInfoState, + ); + const setIsCommandMenuOpened = useSetRecoilState( + isCommandMenuOpenedState, + ); + const setCommandMenuSearch = useSetRecoilState(commandMenuSearchState); + const setCommandMenuNavigationStack = useSetRecoilState( + commandMenuNavigationStackState, + ); + const setCommandMenuNavigationRecords = useSetRecoilState( + commandMenuNavigationRecordsState, + ); + const setHasUserSelectedCommand = useSetRecoilState( + hasUserSelectedCommandState, + ); + const setIsCommandMenuClosing = useSetRecoilState( + isCommandMenuClosingState, + ); + const setViewableRecordId = useSetRecoilState(viewableRecordIdState); + + return { + commandMenuCloseAnimationCompleteCleanup, + commandMenuPage, + commandMenuPageInfo, + isCommandMenuOpened, + commandMenuSearch, + commandMenuNavigationStack, + commandMenuNavigationRecords, + commandMenuNavigationMorphItemByPage, + hasUserSelectedCommand, + isCommandMenuClosing, + viewableRecordId, + setCommandMenuPage, + setCommandMenuPageInfo, + setIsCommandMenuOpened, + setCommandMenuSearch, + setCommandMenuNavigationStack, + setCommandMenuNavigationRecords, + setHasUserSelectedCommand, + setIsCommandMenuClosing, + setViewableRecordId, + }; + }, + { + wrapper: Wrapper, + }, + ); + return { result }; + }; + + it('should reset modified states back to default values', () => { + const { result } = renderHooks(); + + act(() => { + result.current.setCommandMenuPage(CommandMenuPages.ViewRecord); + result.current.setCommandMenuPageInfo({ + title: 'Test Record', + Icon: IconList, + instanceId: 'test-id', + }); + result.current.setIsCommandMenuOpened(true); + result.current.setCommandMenuSearch('test search'); + result.current.setCommandMenuNavigationStack([ + { + page: CommandMenuPages.SearchRecords, + pageTitle: 'Search', + pageIcon: IconList, + pageId: '1', + }, + ]); + result.current.setCommandMenuNavigationRecords([ + { + objectMetadataItem: { id: '1', nameSingular: 'Record' } as any, + record: { id: '1' } as any, + }, + ]); + result.current.setHasUserSelectedCommand(true); + result.current.setIsCommandMenuClosing(true); + result.current.setViewableRecordId('record-123'); + }); + + expect(result.current.commandMenuPage).toBe(CommandMenuPages.ViewRecord); + expect(result.current.commandMenuPageInfo).toEqual({ + title: 'Test Record', + Icon: IconList, + instanceId: 'test-id', + }); + expect(result.current.isCommandMenuOpened).toBe(true); + expect(result.current.commandMenuSearch).toBe('test search'); + expect(result.current.commandMenuNavigationStack).toEqual([ + { + page: CommandMenuPages.SearchRecords, + pageTitle: 'Search', + pageIcon: IconList, + pageId: '1', + }, + ]); + expect(result.current.commandMenuNavigationRecords).toEqual([ + { + objectMetadataItem: { id: '1', nameSingular: 'Record' } as any, + record: { id: '1' } as any, + }, + ]); + expect(result.current.hasUserSelectedCommand).toBe(true); + expect(result.current.isCommandMenuClosing).toBe(true); + expect(result.current.viewableRecordId).toBe('record-123'); + + act(() => { + result.current.commandMenuCloseAnimationCompleteCleanup(); + }); + + expect(result.current.commandMenuPage).toBe(CommandMenuPages.Root); + expect(result.current.commandMenuPageInfo).toEqual({ + title: undefined, + Icon: undefined, + instanceId: '', + }); + expect(result.current.isCommandMenuOpened).toBe(false); + expect(result.current.commandMenuSearch).toBe(''); + expect(result.current.commandMenuNavigationStack).toEqual([]); + expect(result.current.commandMenuNavigationRecords).toEqual([]); + expect(result.current.hasUserSelectedCommand).toBe(false); + expect(result.current.isCommandMenuClosing).toBe(false); + expect(result.current.viewableRecordId).toBe(null); + }); + + it('should call all dependent functions correctly', () => { + const { result } = renderHooks(); + + act(() => { + result.current.commandMenuCloseAnimationCompleteCleanup(); + }); + + expect(mockCloseDropdown).toHaveBeenCalledTimes(1); + expect(mockResetContextStoreStates).toHaveBeenCalledTimes(2); + expect(mockResetSelectedItem).toHaveBeenCalledTimes(1); + expect(mockGoBackToPreviousHotkeyScope).toHaveBeenCalledTimes(1); + expect(mockEmitRightDrawerCloseEvent).toHaveBeenCalledTimes(1); + + expect(mockCloseDropdown).toHaveBeenCalledWith( + COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID, + ); + expect(mockResetContextStoreStates).toHaveBeenNthCalledWith( + 1, + COMMAND_MENU_COMPONENT_INSTANCE_ID, + ); + expect(mockResetContextStoreStates).toHaveBeenNthCalledWith( + 2, + COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID, + ); + }); +}); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuHistory.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuHistory.test.tsx new file mode 100644 index 000000000..5f1dc3ffb --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuHistory.test.tsx @@ -0,0 +1,147 @@ +import { renderHook } from '@testing-library/react'; +import { act } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { RecoilRoot, useRecoilValue } from 'recoil'; + +import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup'; +import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; +import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; +import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; +import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { IconList, IconSearch } from 'twenty-ui'; + +const Wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + +const renderHooks = () => { + const { result } = renderHook( + () => { + const commandMenu = useCommandMenu(); + const commandMenuHistory = useCommandMenuHistory(); + const commandMenuCloseAnimationCompleteCleanup = + useCommandMenuCloseAnimationCompleteCleanup(); + const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState); + const commandMenuNavigationStack = useRecoilValue( + commandMenuNavigationStackState, + ); + const commandMenuPage = useRecoilValue(commandMenuPageState); + const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState); + + return { + commandMenu, + commandMenuHistory, + isCommandMenuOpened, + commandMenuNavigationStack, + commandMenuPage, + commandMenuPageInfo, + commandMenuCloseAnimationCompleteCleanup, + }; + }, + { + wrapper: Wrapper, + }, + ); + return { result }; +}; + +describe('useCommandMenuHistory', () => { + it('should go back from a page', () => { + const { result } = renderHooks(); + + act(() => { + result.current.commandMenu.navigateCommandMenu({ + page: CommandMenuPages.SearchRecords, + pageTitle: 'Search', + pageIcon: IconSearch, + pageId: '1', + }); + }); + + act(() => { + result.current.commandMenu.navigateCommandMenu({ + page: CommandMenuPages.ViewRecord, + pageTitle: 'Company', + pageIcon: IconList, + pageId: '2', + }); + }); + + expect(result.current.commandMenuNavigationStack).toEqual([ + { + page: CommandMenuPages.SearchRecords, + pageTitle: 'Search', + pageIcon: IconSearch, + pageId: '1', + }, + { + page: CommandMenuPages.ViewRecord, + pageTitle: 'Company', + pageIcon: IconList, + pageId: '2', + }, + ]); + + act(() => { + result.current.commandMenuHistory.goBackFromCommandMenu(); + }); + + expect(result.current.commandMenuNavigationStack).toEqual([ + { + 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(() => { + result.current.commandMenuHistory.goBackFromCommandMenu(); + result.current.commandMenuCloseAnimationCompleteCleanup.commandMenuCloseAnimationCompleteCleanup(); + }); + + expect(result.current.commandMenuNavigationStack).toEqual([]); + expect(result.current.commandMenuPage).toBe(CommandMenuPages.Root); + expect(result.current.commandMenuPageInfo).toEqual({ + title: undefined, + instanceId: '', + 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, + pageId: '1', + }); + }); + + act(() => { + result.current.commandMenuHistory.navigateCommandMenuHistory(0); + }); + + expect(result.current.commandMenuPage).toBe(CommandMenuPages.SearchRecords); + expect(result.current.commandMenuPageInfo).toEqual({ + title: 'Search', + Icon: IconSearch, + instanceId: '1', + }); + }); +}); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useNavigateCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useNavigateCommandMenu.test.tsx new file mode 100644 index 000000000..37d3a06c5 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useNavigateCommandMenu.test.tsx @@ -0,0 +1,101 @@ +import { renderHook } from '@testing-library/react'; +import { act } from 'react'; +import { useRecoilValue } from 'recoil'; + +import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId'; +import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; +import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; +import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType'; +import { Icon123, useIcons } from 'twenty-ui'; +import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('mocked-uuid'), +})); + +const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', +)!; + +const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [], + componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, + contextStoreCurrentObjectMetadataNameSingular: + personMockObjectMetadataItem.nameSingular, + contextStoreCurrentViewId: 'my-view-id', + contextStoreTargetedRecordsRule: { + mode: 'selection', + selectedRecordIds: [], + }, + contextStoreNumberOfSelectedRecords: 0, + contextStoreCurrentViewType: ContextStoreViewType.Table, +}); + +const renderHooks = () => { + const { result } = renderHook( + () => { + const { navigateCommandMenu } = useNavigateCommandMenu(); + + const commandMenuPage = useRecoilValue(commandMenuPageState); + const commandMenuNavigationStack = useRecoilValue( + commandMenuNavigationStackState, + ); + const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState); + + const { getIcon } = useIcons(); + + return { + navigateCommandMenu, + commandMenuPage, + commandMenuNavigationStack, + commandMenuPageInfo, + getIcon, + }; + }, + { + wrapper, + }, + ); + return { result }; +}; + +describe('useNavigateCommandMenu', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should navigate to the correct page', () => { + const { result } = renderHooks(); + + act(() => { + result.current.navigateCommandMenu({ + page: CommandMenuPages.Root, + pageTitle: 'Root', + pageIcon: Icon123, + pageIconColor: 'red', + pageId: 'mocked-uuid', + resetNavigationStack: false, + }); + }); + + expect(result.current.commandMenuPage).toBe(CommandMenuPages.Root); + expect(result.current.commandMenuNavigationStack).toEqual([ + { + page: CommandMenuPages.Root, + pageTitle: 'Root', + pageIcon: Icon123, + pageIconColor: 'red', + pageId: 'mocked-uuid', + }, + ]); + expect(result.current.commandMenuPageInfo).toEqual({ + title: 'Root', + Icon: Icon123, + instanceId: 'mocked-uuid', + }); + }); +}); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenCalendarEventInCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenCalendarEventInCommandMenu.test.tsx new file mode 100644 index 000000000..12f136c7a --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenCalendarEventInCommandMenu.test.tsx @@ -0,0 +1,89 @@ +import { renderHook } from '@testing-library/react'; +import { act } from 'react'; + +import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId'; +import { useOpenCalendarEventInCommandMenu } from '@/command-menu/hooks/useOpenCalendarEventInCommandMenu'; +import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { IconCalendarEvent } from 'twenty-ui'; +import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('mocked-uuid'), +})); + +const mockNavigateCommandMenu = jest.fn(); +jest.mock('@/command-menu/hooks/useNavigateCommandMenu', () => ({ + useNavigateCommandMenu: () => ({ + navigateCommandMenu: mockNavigateCommandMenu, + }), +})); + +const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', +)!; + +const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [], + componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, + contextStoreCurrentObjectMetadataNameSingular: + personMockObjectMetadataItem.nameSingular, + contextStoreCurrentViewId: 'my-view-id', + contextStoreTargetedRecordsRule: { + mode: 'selection', + selectedRecordIds: [], + }, + contextStoreNumberOfSelectedRecords: 0, + contextStoreCurrentViewType: ContextStoreViewType.Table, +}); + +const renderHooks = () => { + const { result } = renderHook( + () => { + const { openCalendarEventInCommandMenu } = + useOpenCalendarEventInCommandMenu(); + + const viewableRecordId = useRecoilComponentValueV2( + viewableRecordIdComponentState, + 'mocked-uuid', + ); + + return { + openCalendarEventInCommandMenu, + viewableRecordId, + }; + }, + { + wrapper, + }, + ); + return { result }; +}; + +describe('useOpenCalendarEventInCommandMenu', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should set the correct states and navigate to the calendar event page', () => { + const { result } = renderHooks(); + + const calendarEventId = 'calendar-event-123'; + + act(() => { + result.current.openCalendarEventInCommandMenu(calendarEventId); + }); + + expect(result.current.viewableRecordId).toBe(calendarEventId); + + expect(mockNavigateCommandMenu).toHaveBeenCalledWith({ + page: CommandMenuPages.ViewCalendarEvent, + pageTitle: 'Calendar Event', + pageIcon: IconCalendarEvent, + pageId: 'mocked-uuid', + }); + }); +}); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenEmailThreadInCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenEmailThreadInCommandMenu.test.tsx new file mode 100644 index 000000000..ea0249f03 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenEmailThreadInCommandMenu.test.tsx @@ -0,0 +1,89 @@ +import { renderHook } from '@testing-library/react'; +import { act } from 'react'; + +import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId'; +import { useOpenEmailThreadInCommandMenu } from '@/command-menu/hooks/useOpenEmailThreadInCommandMenu'; +import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { IconMail } from 'twenty-ui'; +import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('mocked-uuid'), +})); + +const mockNavigateCommandMenu = jest.fn(); +jest.mock('@/command-menu/hooks/useNavigateCommandMenu', () => ({ + useNavigateCommandMenu: () => ({ + navigateCommandMenu: mockNavigateCommandMenu, + }), +})); + +const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', +)!; + +const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [], + componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, + contextStoreCurrentObjectMetadataNameSingular: + personMockObjectMetadataItem.nameSingular, + contextStoreCurrentViewId: 'my-view-id', + contextStoreTargetedRecordsRule: { + mode: 'selection', + selectedRecordIds: [], + }, + contextStoreNumberOfSelectedRecords: 0, + contextStoreCurrentViewType: ContextStoreViewType.Table, +}); + +const renderHooks = () => { + const { result } = renderHook( + () => { + const { openEmailThreadInCommandMenu } = + useOpenEmailThreadInCommandMenu(); + + const viewableRecordId = useRecoilComponentValueV2( + viewableRecordIdComponentState, + 'mocked-uuid', + ); + + return { + openEmailThreadInCommandMenu, + viewableRecordId, + }; + }, + { + wrapper, + }, + ); + return { result }; +}; + +describe('useOpenEmailThreadInCommandMenu', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should set the correct states and navigate to the email thread page', () => { + const { result } = renderHooks(); + + const emailThreadId = 'email-thread-123'; + + act(() => { + result.current.openEmailThreadInCommandMenu(emailThreadId); + }); + + expect(result.current.viewableRecordId).toBe(emailThreadId); + + expect(mockNavigateCommandMenu).toHaveBeenCalledWith({ + page: CommandMenuPages.ViewEmailThread, + pageTitle: 'Email Thread', + pageIcon: IconMail, + pageId: 'mocked-uuid', + }); + }); +}); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenRecordInCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenRecordInCommandMenu.test.tsx new file mode 100644 index 000000000..63c04f1a4 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useOpenRecordInCommandMenu.test.tsx @@ -0,0 +1,178 @@ +import { renderHook } from '@testing-library/react'; +import { act } from 'react'; +import { useRecoilValue } from 'recoil'; + +import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId'; +import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; +import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState'; +import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState'; +import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; +import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; +import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useIcons } from 'twenty-ui'; +import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('mocked-uuid'), +})); + +const mockNavigateCommandMenu = jest.fn(); +jest.mock('@/command-menu/hooks/useNavigateCommandMenu', () => ({ + useNavigateCommandMenu: () => ({ + navigateCommandMenu: mockNavigateCommandMenu, + }), +})); + +const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', +)!; + +const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [], + componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, + contextStoreCurrentObjectMetadataNameSingular: + personMockObjectMetadataItem.nameSingular, + contextStoreCurrentViewId: 'my-view-id', + contextStoreTargetedRecordsRule: { + mode: 'selection', + selectedRecordIds: [], + }, + contextStoreNumberOfSelectedRecords: 0, + contextStoreCurrentViewType: ContextStoreViewType.Table, +}); + +const renderHooks = () => { + const { result } = renderHook( + () => { + const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); + + const commandMenuPage = useRecoilValue(commandMenuPageState); + const commandMenuNavigationMorphItemByPage = useRecoilValue( + commandMenuNavigationMorphItemByPageState, + ); + + const viewableRecordId = useRecoilComponentValueV2( + viewableRecordIdComponentState, + 'mocked-uuid', + ); + const viewableRecordNameSingular = useRecoilComponentValueV2( + viewableRecordNameSingularComponentState, + 'mocked-uuid', + ); + const currentObjectMetadataItemId = useRecoilComponentValueV2( + contextStoreCurrentObjectMetadataItemIdComponentState, + 'mocked-uuid', + ); + const targetedRecordsRule = useRecoilComponentValueV2( + contextStoreTargetedRecordsRuleComponentState, + 'mocked-uuid', + ); + const numberOfSelectedRecords = useRecoilComponentValueV2( + contextStoreNumberOfSelectedRecordsComponentState, + 'mocked-uuid', + ); + const currentViewType = useRecoilComponentValueV2( + contextStoreCurrentViewTypeComponentState, + 'mocked-uuid', + ); + const { getIcon } = useIcons(); + + return { + openRecordInCommandMenu, + viewableRecordId, + commandMenuPage, + commandMenuNavigationMorphItemByPage, + viewableRecordNameSingular, + currentObjectMetadataItemId, + targetedRecordsRule, + numberOfSelectedRecords, + currentViewType, + getIcon, + }; + }, + { + wrapper, + }, + ); + return { result }; +}; + +describe('useOpenRecordInCommandMenu', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should set the correct states and navigate to the record page', () => { + const { result } = renderHooks(); + + const recordId = 'record-123'; + const objectNameSingular = 'person'; + + act(() => { + result.current.openRecordInCommandMenu({ + recordId, + objectNameSingular, + }); + }); + + expect(result.current.viewableRecordId).toBe(recordId); + expect(result.current.viewableRecordNameSingular).toBe(objectNameSingular); + expect(result.current.currentObjectMetadataItemId).toBe( + personMockObjectMetadataItem.id, + ); + expect(result.current.targetedRecordsRule).toEqual({ + mode: 'selection', + selectedRecordIds: [recordId], + }); + expect(result.current.numberOfSelectedRecords).toBe(1); + expect(result.current.currentViewType).toBe(ContextStoreViewType.ShowPage); + + expect(result.current.commandMenuNavigationMorphItemByPage.size).toBe(1); + expect( + result.current.commandMenuNavigationMorphItemByPage.get('mocked-uuid'), + ).toEqual({ + objectMetadataId: personMockObjectMetadataItem.id, + recordId, + }); + + expect(mockNavigateCommandMenu).toHaveBeenCalledWith({ + page: CommandMenuPages.ViewRecord, + pageTitle: 'Person', + pageIcon: result.current.getIcon(personMockObjectMetadataItem.icon), + pageIconColor: 'currentColor', + pageId: 'mocked-uuid', + resetNavigationStack: false, + }); + }); + + it('should set the correct page title for a new record', () => { + const { result } = renderHooks(); + + const recordId = 'new-record-123'; + const objectNameSingular = 'person'; + + act(() => { + result.current.openRecordInCommandMenu({ + recordId, + objectNameSingular, + isNewRecord: true, + }); + }); + + expect(mockNavigateCommandMenu).toHaveBeenCalledWith({ + page: CommandMenuPages.ViewRecord, + pageTitle: 'New Person', + pageIcon: result.current.getIcon(personMockObjectMetadataItem.icon), + pageIconColor: 'currentColor', + pageId: 'mocked-uuid', + resetNavigationStack: false, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useSetGlobalCommandMenuContext.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useSetGlobalCommandMenuContext.test.tsx new file mode 100644 index 000000000..c4a2e8d9d --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useSetGlobalCommandMenuContext.test.tsx @@ -0,0 +1,157 @@ +import { renderHook } from '@testing-library/react'; +import { act } from 'react'; +import { useRecoilValue } from 'recoil'; + +import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId'; +import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId'; +import { useSetGlobalCommandMenuContext } from '@/command-menu/hooks/useSetGlobalCommandMenuContext'; +import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; +import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState'; +import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; +import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; +import { getPeopleRecordConnectionMock } from '~/testing/mock-data/people'; + +const mockCopyContextStoreStates = jest.fn(); +jest.mock( + '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates', + () => ({ + useCopyContextStoreStates: () => ({ + copyContextStoreStates: mockCopyContextStoreStates, + }), + }), +); + +const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', +)!; + +const peopleMock = getPeopleRecordConnectionMock(); + +const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [], + componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, + contextStoreCurrentObjectMetadataNameSingular: + personMockObjectMetadataItem.nameSingular, + contextStoreCurrentViewId: 'my-view-id', + contextStoreTargetedRecordsRule: { + mode: 'selection', + selectedRecordIds: [peopleMock[0].id, peopleMock[1].id], + }, + contextStoreNumberOfSelectedRecords: 2, + contextStoreCurrentViewType: ContextStoreViewType.Table, + onInitializeRecoilSnapshot: (snapshot) => { + snapshot.set(recordStoreFamilyState(peopleMock[0].id), peopleMock[0]); + snapshot.set(recordStoreFamilyState(peopleMock[1].id), peopleMock[1]); + }, +}); + +describe('useSetGlobalCommandMenuContext', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should reset all command menu context states', () => { + const { result } = renderHook( + () => { + const { setGlobalCommandMenuContext } = + useSetGlobalCommandMenuContext(); + + const targetedRecordsRule = useRecoilValue( + contextStoreTargetedRecordsRuleComponentState.atomFamily({ + instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, + }), + ); + + const numberOfSelectedRecords = useRecoilValue( + contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ + instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, + }), + ); + + const filters = useRecoilValue( + contextStoreFiltersComponentState.atomFamily({ + instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, + }), + ); + + const currentViewType = useRecoilValue( + contextStoreCurrentViewTypeComponentState.atomFamily({ + instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, + }), + ); + + const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState); + + const hasUserSelectedCommand = useRecoilValue( + hasUserSelectedCommandState, + ); + + return { + setGlobalCommandMenuContext, + targetedRecordsRule, + numberOfSelectedRecords, + filters, + currentViewType, + commandMenuPageInfo, + hasUserSelectedCommand, + }; + }, + { + wrapper, + }, + ); + + expect(result.current.targetedRecordsRule).toEqual({ + mode: 'selection', + selectedRecordIds: [peopleMock[0].id, peopleMock[1].id], + }); + expect(result.current.numberOfSelectedRecords).toBe(2); + expect(result.current.filters).toEqual([]); + expect(result.current.currentViewType).toBe(ContextStoreViewType.Table); + expect(result.current.commandMenuPageInfo).toEqual({ + title: undefined, + Icon: undefined, + instanceId: '', + }); + expect(result.current.hasUserSelectedCommand).toBe(false); + + act(() => { + result.current.setGlobalCommandMenuContext(); + }); + + expect(result.current.targetedRecordsRule).toEqual({ + mode: 'selection', + selectedRecordIds: [], + }); + expect(result.current.numberOfSelectedRecords).toBe(0); + expect(result.current.filters).toEqual([]); + expect(result.current.currentViewType).toBe(ContextStoreViewType.Table); + expect(result.current.commandMenuPageInfo).toEqual({ + title: undefined, + Icon: undefined, + instanceId: '', + }); + expect(result.current.hasUserSelectedCommand).toBe(false); + }); + + it('should call copyContextStoreStates with correct parameters', () => { + const { result } = renderHook(() => useSetGlobalCommandMenuContext(), { + wrapper, + }); + + act(() => { + result.current.setGlobalCommandMenuContext(); + }); + + expect(mockCopyContextStoreStates).toHaveBeenCalledWith({ + instanceIdToCopyFrom: COMMAND_MENU_COMPONENT_INSTANCE_ID, + instanceIdToCopyTo: COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useWorkflowCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useWorkflowCommandMenu.test.tsx new file mode 100644 index 000000000..8fdae3d2c --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useWorkflowCommandMenu.test.tsx @@ -0,0 +1,207 @@ +import { renderHook } from '@testing-library/react'; +import { useRecoilValue } from 'recoil'; + +import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId'; +import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState'; +import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState'; +import { workflowIdComponentState } from '@/command-menu/pages/workflow/states/workflowIdComponentState'; +import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; +import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; +import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { t } from '@lingui/core/macro'; +import { act } from 'react'; +import { IconBolt, IconSettingsAutomation, useIcons } from 'twenty-ui'; +import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; +import { useWorkflowCommandMenu } from '../useWorkflowCommandMenu'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('mocked-uuid'), +})); + +const mockNavigateCommandMenu = jest.fn(); +jest.mock('@/command-menu/hooks/useNavigateCommandMenu', () => ({ + useNavigateCommandMenu: () => ({ + navigateCommandMenu: mockNavigateCommandMenu, + }), +})); + +const workflowMockObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'workflow', +)!; + +jest.mock('@/object-metadata/hooks/useObjectMetadataItem', () => ({ + useObjectMetadataItem: jest.fn(() => ({ + objectMetadataItem: workflowMockObjectMetadataItem, + })), +})); + +const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [], + componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, + contextStoreCurrentObjectMetadataNameSingular: + workflowMockObjectMetadataItem.nameSingular, + contextStoreCurrentViewId: 'my-view-id', + contextStoreTargetedRecordsRule: { + mode: 'selection', + selectedRecordIds: [], + }, + contextStoreNumberOfSelectedRecords: 0, + contextStoreCurrentViewType: ContextStoreViewType.Table, +}); + +const renderHooks = () => { + const { result } = renderHook( + () => { + const { + openWorkflowTriggerTypeInCommandMenu, + openStepSelectInCommandMenu, + openWorkflowEditStepInCommandMenu, + openWorkflowViewStepInCommandMenu, + } = useWorkflowCommandMenu(); + const commandMenuPage = useRecoilValue(commandMenuPageState); + const commandMenuNavigationMorphItemByPage = useRecoilValue( + commandMenuNavigationMorphItemByPageState, + ); + + const viewableRecordId = useRecoilComponentValueV2( + viewableRecordIdComponentState, + 'mocked-uuid', + ); + const viewableRecordNameSingular = useRecoilComponentValueV2( + viewableRecordNameSingularComponentState, + 'mocked-uuid', + ); + const currentObjectMetadataItemId = useRecoilComponentValueV2( + contextStoreCurrentObjectMetadataItemIdComponentState, + 'mocked-uuid', + ); + const targetedRecordsRule = useRecoilComponentValueV2( + contextStoreTargetedRecordsRuleComponentState, + 'mocked-uuid', + ); + const numberOfSelectedRecords = useRecoilComponentValueV2( + contextStoreNumberOfSelectedRecordsComponentState, + 'mocked-uuid', + ); + const currentViewType = useRecoilComponentValueV2( + contextStoreCurrentViewTypeComponentState, + 'mocked-uuid', + ); + const workflowId = useRecoilComponentValueV2( + workflowIdComponentState, + 'mocked-uuid', + ); + const { getIcon } = useIcons(); + + return { + openWorkflowTriggerTypeInCommandMenu, + openStepSelectInCommandMenu, + openWorkflowEditStepInCommandMenu, + openWorkflowViewStepInCommandMenu, + workflowId, + viewableRecordId, + commandMenuPage, + commandMenuNavigationMorphItemByPage, + viewableRecordNameSingular, + currentObjectMetadataItemId, + targetedRecordsRule, + numberOfSelectedRecords, + currentViewType, + getIcon, + }; + }, + { + wrapper, + }, + ); + return { result }; +}; + +describe('useWorkflowCommandMenu', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should navigate to the workflow step select trigger type page', () => { + const { result } = renderHooks(); + + act(() => { + result.current.openWorkflowTriggerTypeInCommandMenu('test-workflow-id'); + }); + + expect(result.current.workflowId).toBe('test-workflow-id'); + + expect(mockNavigateCommandMenu).toHaveBeenCalledWith({ + page: CommandMenuPages.WorkflowStepSelectTriggerType, + pageTitle: t`Trigger Type`, + pageIcon: IconBolt, + pageId: 'mocked-uuid', + }); + }); + + it('should navigate to the workflow step select action page', () => { + const { result } = renderHooks(); + + act(() => { + result.current.openStepSelectInCommandMenu('test-workflow-id'); + }); + + expect(result.current.workflowId).toBe('test-workflow-id'); + + expect(mockNavigateCommandMenu).toHaveBeenCalledWith({ + page: CommandMenuPages.WorkflowStepSelectAction, + pageTitle: t`Select Action`, + pageIcon: IconSettingsAutomation, + pageId: 'mocked-uuid', + }); + }); + + it('should navigate to the workflow step edit page', () => { + const { result } = renderHooks(); + + act(() => { + result.current.openWorkflowEditStepInCommandMenu( + 'test-workflow-id', + 'Edit Step', + IconSettingsAutomation, + ); + }); + + expect(result.current.workflowId).toBe('test-workflow-id'); + + expect(mockNavigateCommandMenu).toHaveBeenCalledWith({ + page: CommandMenuPages.WorkflowStepEdit, + pageTitle: 'Edit Step', + pageIcon: IconSettingsAutomation, + pageId: 'mocked-uuid', + }); + }); + + it('should navigate to the workflow step view page', () => { + const { result } = renderHooks(); + + act(() => { + result.current.openWorkflowViewStepInCommandMenu( + 'test-workflow-id', + 'View Step', + IconSettingsAutomation, + ); + }); + + expect(result.current.workflowId).toBe('test-workflow-id'); + + expect(mockNavigateCommandMenu).toHaveBeenCalledWith({ + page: CommandMenuPages.WorkflowStepView, + pageTitle: 'View Step', + pageIcon: IconSettingsAutomation, + pageId: 'mocked-uuid', + }); + }); +}); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts index ec3a0dcb3..76b6d4e20 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts @@ -1,79 +1,17 @@ import { useRecoilCallback } from 'recoil'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; -import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; -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 { - IconBolt, - IconCalendarEvent, - IconComponent, - IconDotsVertical, - IconMail, - IconSearch, - IconSettingsAutomation, - useIcons, -} from 'twenty-ui'; +import { IconDotsVertical } 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 { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState'; -import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState'; -import { workflowIdComponentState } from '@/command-menu/pages/workflow/states/workflowIdComponentState'; -import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; -import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState'; -import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; -import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; -import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; -import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState'; +import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu'; import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; -import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; -import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; -import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; -import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; -import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; -import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; -import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; -import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType'; -import { getIconColorForObjectType } from '@/object-metadata/utils/getIconColorForObjectType'; -import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; -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 { useTheme } from '@emotion/react'; -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; - pageIconColor?: string; - pageId?: string; -}; - export const useCommandMenu = () => { - const { resetSelectedItem } = useSelectableList('command-menu-list'); - const { - setHotkeyScopeAndMemorizePreviousScope, - goBackToPreviousHotkeyScope, - } = usePreviousHotkeyScope(); - const { getIcon } = useIcons(); - - const { copyContextStoreStates } = useCopyContextStoreStates(); - const { resetContextStoreStates } = useResetContextStoreStates(); - - const { closeDropdown } = useDropdownV2(); - - const theme = useTheme(); + const { navigateCommandMenu } = useNavigateCommandMenu(); const closeCommandMenu = useRecoilCallback( ({ set }) => @@ -85,141 +23,7 @@ export const useCommandMenu = () => { [], ); - const onCommandMenuCloseAnimationComplete = useRecoilCallback( - ({ set }) => - () => { - closeDropdown(COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID); - - resetContextStoreStates(COMMAND_MENU_COMPONENT_INSTANCE_ID); - resetContextStoreStates(COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID); - - set(viewableRecordIdState, null); - set(commandMenuPageState, CommandMenuPages.Root); - set(commandMenuPageInfoState, { - title: undefined, - Icon: undefined, - instanceId: '', - }); - set(isCommandMenuOpenedState, false); - set(commandMenuSearchState, ''); - set(commandMenuNavigationMorphItemByPageState, new Map()); - set(commandMenuNavigationRecordsState, []); - set(commandMenuNavigationStackState, []); - resetSelectedItem(); - set(hasUserSelectedCommandState, false); - goBackToPreviousHotkeyScope(); - - emitRightDrawerCloseEvent(); - set(isCommandMenuClosingState, false); - }, - [ - closeDropdown, - goBackToPreviousHotkeyScope, - resetContextStoreStates, - resetSelectedItem, - ], - ); - - const openCommandMenu = useRecoilCallback( - ({ snapshot, set }) => - () => { - const isCommandMenuOpened = snapshot - .getLoadable(isCommandMenuOpenedState) - .getValue(); - - const isCommandMenuClosing = snapshot - .getLoadable(isCommandMenuClosingState) - .getValue(); - - if (isCommandMenuClosing) { - onCommandMenuCloseAnimationComplete(); - } - - setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen); - - if (isCommandMenuOpened) { - return; - } - - copyContextStoreStates({ - instanceIdToCopyFrom: MAIN_CONTEXT_STORE_INSTANCE_ID, - instanceIdToCopyTo: COMMAND_MENU_COMPONENT_INSTANCE_ID, - }); - - set(isCommandMenuOpenedState, true); - set(hasUserSelectedCommandState, false); - set(isDragSelectionStartEnabledState, false); - }, - [ - copyContextStoreStates, - onCommandMenuCloseAnimationComplete, - setHotkeyScopeAndMemorizePreviousScope, - ], - ); - - const navigateCommandMenu = useRecoilCallback( - ({ snapshot, set }) => { - return ({ - page, - pageTitle, - pageIcon, - pageIconColor, - 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 - .getLoadable(isCommandMenuClosingState) - .getValue(); - - const currentNavigationStack = isCommandMenuClosing - ? [] - : snapshot.getLoadable(commandMenuNavigationStackState).getValue(); - - if (resetNavigationStack) { - set(commandMenuNavigationStackState, [ - { - page, - pageTitle, - pageIcon, - pageIconColor, - pageId, - }, - ]); - - set(commandMenuNavigationRecordsState, []); - set(commandMenuNavigationMorphItemByPageState, new Map()); - } else { - set(commandMenuNavigationStackState, [ - ...currentNavigationStack, - { - page, - pageTitle, - pageIcon, - pageIconColor, - pageId, - }, - ]); - } - }; - }, - [openCommandMenu], - ); - - const openRootCommandMenu = useCallback(() => { + const openCommandMenu = useCallback(() => { navigateCommandMenu({ page: CommandMenuPages.Root, pageTitle: 'Command Menu', @@ -240,503 +44,16 @@ export const useCommandMenu = () => { if (isCommandMenuOpened) { closeCommandMenu(); } else { - openRootCommandMenu(); + openCommandMenu(); } }, - [closeCommandMenu, openRootCommandMenu], - ); - - const goBackFromCommandMenu = useRecoilCallback( - ({ snapshot, set }) => { - return () => { - 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: lastNavigationStackItem.pageTitle, - Icon: lastNavigationStackItem.pageIcon, - instanceId: lastNavigationStackItem.pageId, - }); - - set(commandMenuNavigationStackState, newNavigationStack); - - const currentMorphItems = snapshot - .getLoadable(commandMenuNavigationMorphItemByPageState) - .getValue(); - - if (currentNavigationStack.length > 0) { - const removedItem = currentNavigationStack.at(-1); - - if (isDefined(removedItem)) { - const newMorphItems = new Map(currentMorphItems); - newMorphItems.delete(removedItem.pageId); - set(commandMenuNavigationMorphItemByPageState, newMorphItems); - } - } - - set(hasUserSelectedCommandState, false); - }; - }, - [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, - instanceId: newNavigationStackItem?.pageId, - }); - const currentMorphItems = snapshot - .getLoadable(commandMenuNavigationMorphItemByPageState) - .getValue(); - - const newMorphItems = new Map( - Array.from(currentMorphItems.entries()).filter(([pageId]) => - newNavigationStack.some((item) => item.pageId === pageId), - ), - ); - - set(commandMenuNavigationMorphItemByPageState, newMorphItems); - - set(hasUserSelectedCommandState, false); - }; - }, []); - - const openRecordInCommandMenu = useRecoilCallback( - ({ set, snapshot }) => { - return ({ - recordId, - objectNameSingular, - isNewRecord = false, - }: { - recordId: string; - objectNameSingular: string; - isNewRecord?: boolean; - }) => { - const pageComponentInstanceId = v4(); - - set( - viewableRecordNameSingularComponentState.atomFamily({ - instanceId: pageComponentInstanceId, - }), - objectNameSingular, - ); - set( - viewableRecordIdComponentState.atomFamily({ - instanceId: pageComponentInstanceId, - }), - recordId, - ); - set(viewableRecordIdState, recordId); - - const objectMetadataItem = snapshot - .getLoadable( - objectMetadataItemFamilySelector({ - objectName: objectNameSingular, - objectNameType: 'singular', - }), - ) - .getValue(); - - if (!objectMetadataItem) { - throw new Error( - `No object metadata item found for object name ${objectNameSingular}`, - ); - } - - set( - contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({ - instanceId: pageComponentInstanceId, - }), - objectMetadataItem.id, - ); - - set( - contextStoreTargetedRecordsRuleComponentState.atomFamily({ - instanceId: pageComponentInstanceId, - }), - { - mode: 'selection', - selectedRecordIds: [recordId], - }, - ); - - set( - contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ - instanceId: pageComponentInstanceId, - }), - 1, - ); - - set( - contextStoreCurrentViewTypeComponentState.atomFamily({ - instanceId: pageComponentInstanceId, - }), - ContextStoreViewType.ShowPage, - ); - - set( - contextStoreCurrentViewIdComponentState.atomFamily({ - instanceId: pageComponentInstanceId, - }), - snapshot - .getLoadable( - contextStoreCurrentViewIdComponentState.atomFamily({ - instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID, - }), - ) - .getValue(), - ); - - const currentMorphItems = snapshot - .getLoadable(commandMenuNavigationMorphItemByPageState) - .getValue(); - - const morphItemToAdd = { - objectMetadataId: objectMetadataItem.id, - recordId, - }; - - const newMorphItems = new Map([ - ...currentMorphItems, - [pageComponentInstanceId, morphItemToAdd], - ]); - - set(commandMenuNavigationMorphItemByPageState, newMorphItems); - - const Icon = objectMetadataItem?.icon - ? getIcon(objectMetadataItem.icon) - : getIcon('IconList'); - - const IconColor = getIconColorForObjectType({ - objectType: objectMetadataItem.nameSingular, - theme, - }); - - const capitalizedObjectNameSingular = capitalize(objectNameSingular); - - navigateCommandMenu({ - page: CommandMenuPages.ViewRecord, - pageTitle: isNewRecord - ? t`New ${capitalizedObjectNameSingular}` - : capitalizedObjectNameSingular, - pageIcon: Icon, - pageIconColor: IconColor, - pageId: pageComponentInstanceId, - resetNavigationStack: false, - }); - }; - }, - [getIcon, navigateCommandMenu, theme], - ); - - const openWorkflowTriggerTypeInCommandMenu = useRecoilCallback( - ({ set }) => { - return (workflowId: string) => { - const pageId = v4(); - - set( - workflowIdComponentState.atomFamily({ instanceId: pageId }), - workflowId, - ); - - navigateCommandMenu({ - page: CommandMenuPages.WorkflowStepSelectTriggerType, - pageTitle: t`Trigger Type`, - pageIcon: IconBolt, - pageId, - }); - }; - }, - [navigateCommandMenu], - ); - - const openWorkflowActionInCommandMenu = useRecoilCallback( - ({ set }) => { - return (workflowId: string) => { - const pageId = v4(); - - set( - workflowIdComponentState.atomFamily({ instanceId: pageId }), - workflowId, - ); - - navigateCommandMenu({ - page: CommandMenuPages.WorkflowStepSelectAction, - pageTitle: t`Select Action`, - pageIcon: IconSettingsAutomation, - pageId, - }); - }; - }, - [navigateCommandMenu], - ); - - const openWorkflowEditStepInCommandMenu = useRecoilCallback( - ({ set }) => { - return (workflowId: string, title: string, icon: IconComponent) => { - const pageId = v4(); - - set( - workflowIdComponentState.atomFamily({ instanceId: pageId }), - workflowId, - ); - - navigateCommandMenu({ - page: CommandMenuPages.WorkflowStepEdit, - pageTitle: title, - pageIcon: icon, - pageId, - }); - }; - }, - [navigateCommandMenu], - ); - - const openWorkflowViewStepInCommandMenu = useRecoilCallback( - ({ set }) => { - return (workflowId: string, title: string, icon: IconComponent) => { - const pageId = v4(); - - set( - workflowIdComponentState.atomFamily({ instanceId: pageId }), - workflowId, - ); - - navigateCommandMenu({ - page: CommandMenuPages.WorkflowStepView, - pageTitle: title, - pageIcon: icon, - pageId, - }); - }; - }, - [navigateCommandMenu], - ); - - const openWorkflowViewRunStepInCommandMenu = useRecoilCallback( - ({ set }) => { - return (workflowId: string, title: string, icon: IconComponent) => { - const pageId = v4(); - - set( - workflowIdComponentState.atomFamily({ instanceId: pageId }), - workflowId, - ); - - navigateCommandMenu({ - page: CommandMenuPages.WorkflowRunStepView, - pageTitle: title, - pageIcon: icon, - pageId, - }); - }; - }, - [navigateCommandMenu], - ); - - const openRecordsSearchPage = () => { - navigateCommandMenu({ - 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, - ); - - // TODO: Uncomment this once we need to calendar event title in the navigation - // const objectMetadataItem = snapshot - // .getLoadable(objectMetadataItemsState) - // .getValue() - // .find( - // ({ nameSingular }) => - // nameSingular === CoreObjectNameSingular.CalendarEvent, - // ); - - // set( - // commandMenuNavigationMorphItemsState, - // new Map([ - // ...snapshot - // .getLoadable(commandMenuNavigationMorphItemsState) - // .getValue(), - // [ - // pageComponentInstanceId, - // { - // objectMetadataId: objectMetadataItem?.id, - // recordId: 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, - ); - - // TODO: Uncomment this once we need to show the thread title in the navigation - // const objectMetadataItem = snapshot - // .getLoadable(objectMetadataItemsState) - // .getValue() - // .find( - // ({ nameSingular }) => - // nameSingular === CoreObjectNameSingular.MessageThread, - // ); - - // set( - // commandMenuNavigationMorphItemsState, - // new Map([ - // ...snapshot - // .getLoadable(commandMenuNavigationMorphItemsState) - // .getValue(), - // [ - // pageComponentInstanceId, - // { - // objectMetadataId: objectMetadataItem?.id, - // recordId: emailThreadId, - // }, - // ], - // ]), - // ); - - navigateCommandMenu({ - page: CommandMenuPages.ViewEmailThread, - pageTitle: 'Email Thread', - pageIcon: IconMail, - pageId: pageComponentInstanceId, - }); - }; - }, - [navigateCommandMenu], - ); - - const setGlobalCommandMenuContext = useRecoilCallback( - ({ set }) => { - return () => { - copyContextStoreStates({ - instanceIdToCopyFrom: COMMAND_MENU_COMPONENT_INSTANCE_ID, - instanceIdToCopyTo: COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID, - }); - - set( - contextStoreTargetedRecordsRuleComponentState.atomFamily({ - instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, - }), - { - mode: 'selection', - selectedRecordIds: [], - }, - ); - - set( - contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ - instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, - }), - 0, - ); - - set( - contextStoreFiltersComponentState.atomFamily({ - instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, - }), - [], - ); - - set( - contextStoreCurrentViewTypeComponentState.atomFamily({ - instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, - }), - ContextStoreViewType.Table, - ); - - set(commandMenuPageInfoState, { - title: undefined, - Icon: undefined, - instanceId: '', - }); - - set(hasUserSelectedCommandState, false); - }; - }, - [copyContextStoreStates], + [closeCommandMenu, openCommandMenu], ); return { - openRootCommandMenu, + openCommandMenu, closeCommandMenu, - onCommandMenuCloseAnimationComplete, navigateCommandMenu, - navigateCommandMenuHistory, - goBackFromCommandMenu, - openRecordsSearchPage, - openRecordInCommandMenu, toggleCommandMenu, - setGlobalCommandMenuContext, - openCalendarEventInCommandMenu, - openEmailThreadInCommandMenu, - openWorkflowTriggerTypeInCommandMenu, - openWorkflowActionInCommandMenu, - openWorkflowEditStepInCommandMenu, - openWorkflowViewStepInCommandMenu, - openWorkflowViewRunStepInCommandMenu, }; }; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup.ts new file mode 100644 index 000000000..2855d7cf2 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup.ts @@ -0,0 +1,69 @@ +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 { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates'; +import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; +import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; +import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; +import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; +import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; +import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState'; +import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState'; +import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; +import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2'; +import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent'; +import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; +import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { useRecoilCallback } from 'recoil'; + +export const useCommandMenuCloseAnimationCompleteCleanup = () => { + const { resetSelectedItem } = useSelectableList('command-menu-list'); + + const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope(); + + const { resetContextStoreStates } = useResetContextStoreStates(); + + const { closeDropdown } = useDropdownV2(); + + const commandMenuCloseAnimationCompleteCleanup = useRecoilCallback( + ({ set }) => + () => { + closeDropdown(COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID); + + resetContextStoreStates(COMMAND_MENU_COMPONENT_INSTANCE_ID); + resetContextStoreStates(COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID); + + set(viewableRecordIdState, null); + set(commandMenuPageState, CommandMenuPages.Root); + set(commandMenuPageInfoState, { + title: undefined, + Icon: undefined, + instanceId: '', + }); + set(isCommandMenuOpenedState, false); + set(commandMenuSearchState, ''); + set(commandMenuNavigationMorphItemByPageState, new Map()); + set(commandMenuNavigationRecordsState, []); + set(commandMenuNavigationStackState, []); + resetSelectedItem(); + set(hasUserSelectedCommandState, false); + goBackToPreviousHotkeyScope(); + + emitRightDrawerCloseEvent(); + set(isCommandMenuClosingState, false); + }, + [ + closeDropdown, + goBackToPreviousHotkeyScope, + resetContextStoreStates, + resetSelectedItem, + ], + ); + + return { + commandMenuCloseAnimationCompleteCleanup, + }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuContextChips.tsx b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuContextChips.tsx index ef16aac44..239b019c6 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuContextChips.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuContextChips.tsx @@ -1,5 +1,5 @@ import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory'; import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState'; import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; @@ -24,7 +24,7 @@ export const useCommandMenuContextChips = () => { commandMenuNavigationStackState, ); - const { navigateCommandMenuHistory } = useCommandMenu(); + const { navigateCommandMenuHistory } = useCommandMenuHistory(); const theme = useTheme(); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHistory.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHistory.ts new file mode 100644 index 000000000..fd2f7da59 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHistory.ts @@ -0,0 +1,103 @@ +import { useRecoilCallback } from 'recoil'; + +import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; +import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; +import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; +import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState'; +import { isDefined } from 'twenty-shared'; + +export const useCommandMenuHistory = () => { + const { closeCommandMenu } = useCommandMenu(); + + const goBackFromCommandMenu = useRecoilCallback( + ({ snapshot, set }) => { + return () => { + 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: lastNavigationStackItem.pageTitle, + Icon: lastNavigationStackItem.pageIcon, + instanceId: lastNavigationStackItem.pageId, + }); + + set(commandMenuNavigationStackState, newNavigationStack); + + const currentMorphItems = snapshot + .getLoadable(commandMenuNavigationMorphItemByPageState) + .getValue(); + + if (currentNavigationStack.length > 0) { + const removedItem = currentNavigationStack.at(-1); + + if (isDefined(removedItem)) { + const newMorphItems = new Map(currentMorphItems); + newMorphItems.delete(removedItem.pageId); + set(commandMenuNavigationMorphItemByPageState, newMorphItems); + } + } + + set(hasUserSelectedCommandState, false); + }; + }, + [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, + instanceId: newNavigationStackItem.pageId, + }); + const currentMorphItems = snapshot + .getLoadable(commandMenuNavigationMorphItemByPageState) + .getValue(); + + const newMorphItems = new Map( + Array.from(currentMorphItems.entries()).filter(([pageId]) => + newNavigationStack.some((item) => item.pageId === pageId), + ), + ); + + set(commandMenuNavigationMorphItemByPageState, newMorphItems); + + set(hasUserSelectedCommandState, false); + }; + }, []); + + return { + goBackFromCommandMenu, + navigateCommandMenuHistory, + }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts index 012b804a7..6ff649965 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts @@ -1,5 +1,8 @@ import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory'; +import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu'; +import { useSetGlobalCommandMenuContext } from '@/command-menu/hooks/useSetGlobalCommandMenuContext'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; @@ -13,12 +16,13 @@ import { useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; export const useCommandMenuHotKeys = () => { - const { - openRecordsSearchPage, - toggleCommandMenu, - goBackFromCommandMenu, - setGlobalCommandMenuContext, - } = useCommandMenu(); + const { toggleCommandMenu } = useCommandMenu(); + + const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu(); + + const { goBackFromCommandMenu } = useCommandMenuHistory(); + + const { setGlobalCommandMenuContext } = useSetGlobalCommandMenuContext(); const commandMenuSearch = useRecoilValue(commandMenuSearchState); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts new file mode 100644 index 000000000..c82c84f43 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/useNavigateCommandMenu.ts @@ -0,0 +1,137 @@ +import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId'; +import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup'; +import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates'; +import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; +import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; +import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; +import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; +import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState'; +import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState'; +import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; +import { isDragSelectionStartEnabledState } from '@/ui/utilities/drag-select/states/internal/isDragSelectionStartEnabledState'; +import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; +import { useRecoilCallback } from 'recoil'; +import { IconComponent } from 'twenty-ui'; +import { v4 } from 'uuid'; + +export type CommandMenuNavigationStackItem = { + page: CommandMenuPages; + pageTitle: string; + pageIcon: IconComponent; + pageIconColor?: string; + pageId?: string; +}; + +export const useNavigateCommandMenu = () => { + const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); + + const { copyContextStoreStates } = useCopyContextStoreStates(); + + const { commandMenuCloseAnimationCompleteCleanup } = + useCommandMenuCloseAnimationCompleteCleanup(); + + const openCommandMenu = useRecoilCallback( + ({ snapshot, set }) => + () => { + const isCommandMenuOpened = snapshot + .getLoadable(isCommandMenuOpenedState) + .getValue(); + + const isCommandMenuClosing = snapshot + .getLoadable(isCommandMenuClosingState) + .getValue(); + + if (isCommandMenuClosing) { + commandMenuCloseAnimationCompleteCleanup(); + } + + setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen); + + if (isCommandMenuOpened) { + return; + } + + copyContextStoreStates({ + instanceIdToCopyFrom: MAIN_CONTEXT_STORE_INSTANCE_ID, + instanceIdToCopyTo: COMMAND_MENU_COMPONENT_INSTANCE_ID, + }); + + set(isCommandMenuOpenedState, true); + set(hasUserSelectedCommandState, false); + set(isDragSelectionStartEnabledState, false); + }, + [ + copyContextStoreStates, + commandMenuCloseAnimationCompleteCleanup, + setHotkeyScopeAndMemorizePreviousScope, + ], + ); + + const navigateCommandMenu = useRecoilCallback( + ({ snapshot, set }) => { + return ({ + page, + pageTitle, + pageIcon, + pageIconColor, + pageId, + resetNavigationStack = false, + }: CommandMenuNavigationStackItem & { + resetNavigationStack?: boolean; + }) => { + const computedPageId = pageId || v4(); + + openCommandMenu(); + set(commandMenuPageState, page); + set(commandMenuPageInfoState, { + title: pageTitle, + Icon: pageIcon, + instanceId: computedPageId, + }); + + const isCommandMenuClosing = snapshot + .getLoadable(isCommandMenuClosingState) + .getValue(); + + const currentNavigationStack = isCommandMenuClosing + ? [] + : snapshot.getLoadable(commandMenuNavigationStackState).getValue(); + + if (resetNavigationStack) { + set(commandMenuNavigationStackState, [ + { + page, + pageTitle, + pageIcon, + pageIconColor, + pageId: computedPageId, + }, + ]); + + set(commandMenuNavigationRecordsState, []); + set(commandMenuNavigationMorphItemByPageState, new Map()); + } else { + set(commandMenuNavigationStackState, [ + ...currentNavigationStack, + { + page, + pageTitle, + pageIcon, + pageIconColor, + pageId: computedPageId, + }, + ]); + } + }; + }, + [openCommandMenu], + ); + + return { + navigateCommandMenu, + }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useOpenCalendarEventInCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useOpenCalendarEventInCommandMenu.ts new file mode 100644 index 000000000..3cd7a1aa1 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/useOpenCalendarEventInCommandMenu.ts @@ -0,0 +1,63 @@ +import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu'; +import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { t } from '@lingui/core/macro'; +import { useRecoilCallback } from 'recoil'; +import { IconCalendarEvent } from 'twenty-ui'; +import { v4 } from 'uuid'; + +export const useOpenCalendarEventInCommandMenu = () => { + const { navigateCommandMenu } = useNavigateCommandMenu(); + + const openCalendarEventInCommandMenu = useRecoilCallback( + ({ set }) => { + return (calendarEventId: string) => { + const pageComponentInstanceId = v4(); + + set( + viewableRecordIdComponentState.atomFamily({ + instanceId: pageComponentInstanceId, + }), + calendarEventId, + ); + + // TODO: Uncomment this once we need to calendar event title in the navigation + // const objectMetadataItem = snapshot + // .getLoadable(objectMetadataItemsState) + // .getValue() + // .find( + // ({ nameSingular }) => + // nameSingular === CoreObjectNameSingular.CalendarEvent, + // ); + + // set( + // commandMenuNavigationMorphItemsState, + // new Map([ + // ...snapshot + // .getLoadable(commandMenuNavigationMorphItemsState) + // .getValue(), + // [ + // pageComponentInstanceId, + // { + // objectMetadataId: objectMetadataItem?.id, + // recordId: calendarEventId, + // }, + // ], + // ]), + // ); + + navigateCommandMenu({ + page: CommandMenuPages.ViewCalendarEvent, + pageTitle: t`Calendar Event`, + pageIcon: IconCalendarEvent, + pageId: pageComponentInstanceId, + }); + }; + }, + [navigateCommandMenu], + ); + + return { + openCalendarEventInCommandMenu, + }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useOpenEmailThreadInCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useOpenEmailThreadInCommandMenu.ts new file mode 100644 index 000000000..f55698c39 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/useOpenEmailThreadInCommandMenu.ts @@ -0,0 +1,62 @@ +import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu'; +import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { useRecoilCallback } from 'recoil'; +import { IconMail } from 'twenty-ui'; +import { v4 } from 'uuid'; + +export const useOpenEmailThreadInCommandMenu = () => { + const { navigateCommandMenu } = useNavigateCommandMenu(); + + const openEmailThreadInCommandMenu = useRecoilCallback( + ({ set }) => { + return (emailThreadId: string) => { + const pageComponentInstanceId = v4(); + + set( + viewableRecordIdComponentState.atomFamily({ + instanceId: pageComponentInstanceId, + }), + emailThreadId, + ); + + // TODO: Uncomment this once we need to show the thread title in the navigation + // const objectMetadataItem = snapshot + // .getLoadable(objectMetadataItemsState) + // .getValue() + // .find( + // ({ nameSingular }) => + // nameSingular === CoreObjectNameSingular.MessageThread, + // ); + + // set( + // commandMenuNavigationMorphItemsState, + // new Map([ + // ...snapshot + // .getLoadable(commandMenuNavigationMorphItemsState) + // .getValue(), + // [ + // pageComponentInstanceId, + // { + // objectMetadataId: objectMetadataItem?.id, + // recordId: emailThreadId, + // }, + // ], + // ]), + // ); + + navigateCommandMenu({ + page: CommandMenuPages.ViewEmailThread, + pageTitle: 'Email Thread', + pageIcon: IconMail, + pageId: pageComponentInstanceId, + }); + }; + }, + [navigateCommandMenu], + ); + + return { + openEmailThreadInCommandMenu, + }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordInCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordInCommandMenu.ts new file mode 100644 index 000000000..2b8a02d81 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordInCommandMenu.ts @@ -0,0 +1,158 @@ +import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState'; +import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState'; +import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; +import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; +import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; +import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType'; +import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; +import { getIconColorForObjectType } from '@/object-metadata/utils/getIconColorForObjectType'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; +import { useTheme } from '@emotion/react'; +import { t } from '@lingui/core/macro'; +import { useRecoilCallback } from 'recoil'; +import { capitalize } from 'twenty-shared'; +import { useIcons } from 'twenty-ui'; +import { v4 } from 'uuid'; + +export const useOpenRecordInCommandMenu = () => { + const { navigateCommandMenu } = useCommandMenu(); + + const theme = useTheme(); + const { getIcon } = useIcons(); + + const openRecordInCommandMenu = useRecoilCallback( + ({ set, snapshot }) => { + return ({ + recordId, + objectNameSingular, + isNewRecord = false, + }: { + recordId: string; + objectNameSingular: string; + isNewRecord?: boolean; + }) => { + const pageComponentInstanceId = v4(); + + set( + viewableRecordNameSingularComponentState.atomFamily({ + instanceId: pageComponentInstanceId, + }), + objectNameSingular, + ); + set( + viewableRecordIdComponentState.atomFamily({ + instanceId: pageComponentInstanceId, + }), + recordId, + ); + set(viewableRecordIdState, recordId); + + const objectMetadataItem = snapshot + .getLoadable( + objectMetadataItemFamilySelector({ + objectName: objectNameSingular, + objectNameType: 'singular', + }), + ) + .getValue(); + + if (!objectMetadataItem) { + throw new Error( + `No object metadata item found for object name ${objectNameSingular}`, + ); + } + + set( + contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({ + instanceId: pageComponentInstanceId, + }), + objectMetadataItem.id, + ); + + set( + contextStoreTargetedRecordsRuleComponentState.atomFamily({ + instanceId: pageComponentInstanceId, + }), + { + mode: 'selection', + selectedRecordIds: [recordId], + }, + ); + + set( + contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ + instanceId: pageComponentInstanceId, + }), + 1, + ); + + set( + contextStoreCurrentViewTypeComponentState.atomFamily({ + instanceId: pageComponentInstanceId, + }), + ContextStoreViewType.ShowPage, + ); + + set( + contextStoreCurrentViewIdComponentState.atomFamily({ + instanceId: pageComponentInstanceId, + }), + snapshot + .getLoadable( + contextStoreCurrentViewIdComponentState.atomFamily({ + instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID, + }), + ) + .getValue(), + ); + + const currentMorphItems = snapshot + .getLoadable(commandMenuNavigationMorphItemByPageState) + .getValue(); + + const morphItemToAdd = { + objectMetadataId: objectMetadataItem.id, + recordId, + }; + + const newMorphItems = new Map(currentMorphItems); + newMorphItems.set(pageComponentInstanceId, morphItemToAdd); + + set(commandMenuNavigationMorphItemByPageState, newMorphItems); + + const Icon = objectMetadataItem?.icon + ? getIcon(objectMetadataItem.icon) + : getIcon('IconList'); + + const IconColor = getIconColorForObjectType({ + objectType: objectMetadataItem.nameSingular, + theme, + }); + + const capitalizedObjectNameSingular = capitalize(objectNameSingular); + + navigateCommandMenu({ + page: CommandMenuPages.ViewRecord, + pageTitle: isNewRecord + ? t`New ${capitalizedObjectNameSingular}` + : capitalizedObjectNameSingular, + pageIcon: Icon, + pageIconColor: IconColor, + pageId: pageComponentInstanceId, + resetNavigationStack: false, + }); + }; + }, + [getIcon, navigateCommandMenu, theme], + ); + + return { + openRecordInCommandMenu, + }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu.ts new file mode 100644 index 000000000..28a74b225 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu.ts @@ -0,0 +1,22 @@ +import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { t } from '@lingui/core/macro'; +import { IconSearch } from 'twenty-ui'; +import { v4 } from 'uuid'; + +export const useOpenRecordsSearchPageInCommandMenu = () => { + const { navigateCommandMenu } = useCommandMenu(); + + const openRecordsSearchPage = () => { + navigateCommandMenu({ + page: CommandMenuPages.SearchRecords, + pageTitle: t`Search`, + pageIcon: IconSearch, + pageId: v4(), + }); + }; + + return { + openRecordsSearchPage, + }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useSearchRecords.tsx b/packages/twenty-front/src/modules/command-menu/hooks/useSearchRecords.tsx index fcd678941..22690b1f1 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useSearchRecords.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/useSearchRecords.tsx @@ -1,4 +1,4 @@ -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { t } from '@lingui/core/macro'; @@ -24,7 +24,7 @@ export const useSearchRecords = () => { }, }); - const { openRecordInCommandMenu } = useCommandMenu(); + const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); const commands = useMemo(() => { return (globalSearchData?.globalSearch ?? []).map((searchRecord) => { diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useSetGlobalCommandMenuContext.ts b/packages/twenty-front/src/modules/command-menu/hooks/useSetGlobalCommandMenuContext.ts new file mode 100644 index 000000000..26a000592 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/useSetGlobalCommandMenuContext.ts @@ -0,0 +1,70 @@ +import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId'; +import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId'; +import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates'; +import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; +import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState'; +import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; +import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType'; +import { useRecoilCallback } from 'recoil'; + +export const useSetGlobalCommandMenuContext = () => { + const { copyContextStoreStates } = useCopyContextStoreStates(); + + const setGlobalCommandMenuContext = useRecoilCallback( + ({ set }) => { + return () => { + copyContextStoreStates({ + instanceIdToCopyFrom: COMMAND_MENU_COMPONENT_INSTANCE_ID, + instanceIdToCopyTo: COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID, + }); + + set( + contextStoreTargetedRecordsRuleComponentState.atomFamily({ + instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, + }), + { + mode: 'selection', + selectedRecordIds: [], + }, + ); + + set( + contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ + instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, + }), + 0, + ); + + set( + contextStoreFiltersComponentState.atomFamily({ + instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, + }), + [], + ); + + set( + contextStoreCurrentViewTypeComponentState.atomFamily({ + instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID, + }), + ContextStoreViewType.Table, + ); + + set(commandMenuPageInfoState, { + title: undefined, + Icon: undefined, + instanceId: '', + }); + + set(hasUserSelectedCommandState, false); + }; + }, + [copyContextStoreStates], + ); + + return { + setGlobalCommandMenuContext, + }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useWorkflowCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useWorkflowCommandMenu.ts new file mode 100644 index 000000000..e964cc14a --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/useWorkflowCommandMenu.ts @@ -0,0 +1,124 @@ +import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu'; +import { workflowIdComponentState } from '@/command-menu/pages/workflow/states/workflowIdComponentState'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { t } from '@lingui/core/macro'; +import { useRecoilCallback } from 'recoil'; +import { IconBolt, IconComponent, IconSettingsAutomation } from 'twenty-ui'; +import { v4 } from 'uuid'; + +export const useWorkflowCommandMenu = () => { + const { navigateCommandMenu } = useNavigateCommandMenu(); + + const openWorkflowTriggerTypeInCommandMenu = useRecoilCallback( + ({ set }) => { + return (workflowId: string) => { + const pageId = v4(); + + set( + workflowIdComponentState.atomFamily({ instanceId: pageId }), + workflowId, + ); + + navigateCommandMenu({ + page: CommandMenuPages.WorkflowStepSelectTriggerType, + pageTitle: t`Trigger Type`, + pageIcon: IconBolt, + pageId, + }); + }; + }, + [navigateCommandMenu], + ); + + const openStepSelectInCommandMenu = useRecoilCallback( + ({ set }) => { + return (workflowId: string) => { + const pageId = v4(); + + set( + workflowIdComponentState.atomFamily({ instanceId: pageId }), + workflowId, + ); + + navigateCommandMenu({ + page: CommandMenuPages.WorkflowStepSelectAction, + pageTitle: t`Select Action`, + pageIcon: IconSettingsAutomation, + pageId, + }); + }; + }, + [navigateCommandMenu], + ); + + const openWorkflowEditStepInCommandMenu = useRecoilCallback( + ({ set }) => { + return (workflowId: string, title: string, icon: IconComponent) => { + const pageId = v4(); + + set( + workflowIdComponentState.atomFamily({ instanceId: pageId }), + workflowId, + ); + + navigateCommandMenu({ + page: CommandMenuPages.WorkflowStepEdit, + pageTitle: title, + pageIcon: icon, + pageId, + }); + }; + }, + [navigateCommandMenu], + ); + + const openWorkflowViewStepInCommandMenu = useRecoilCallback( + ({ set }) => { + return (workflowId: string, title: string, icon: IconComponent) => { + const pageId = v4(); + + set( + workflowIdComponentState.atomFamily({ instanceId: pageId }), + workflowId, + ); + + navigateCommandMenu({ + page: CommandMenuPages.WorkflowStepView, + pageTitle: title, + pageIcon: icon, + pageId, + }); + }; + }, + [navigateCommandMenu], + ); + + const openWorkflowRunViewStepInCommandMenu = useRecoilCallback( + ({ set }) => { + return (workflowId: string, title: string, icon: IconComponent) => { + const pageId = v4(); + + set( + workflowIdComponentState.atomFamily({ instanceId: pageId }), + workflowId, + ); + + navigateCommandMenu({ + page: CommandMenuPages.WorkflowRunStepView, + pageTitle: title, + pageIcon: icon, + pageId, + }); + }; + }, + [navigateCommandMenu], + ); + + return { + openWorkflowTriggerTypeInCommandMenu, + openStepSelectInCommandMenu, + openWorkflowEditStepInCommandMenu, + openWorkflowViewStepInCommandMenu, + openWorkflowRunViewStepInCommandMenu, + }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/workflow/trigger-type/components/CommandMenuWorkflowSelectTriggerTypeContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/workflow/trigger-type/components/CommandMenuWorkflowSelectTriggerTypeContent.tsx index fa270cd54..c12ae9b7a 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/workflow/trigger-type/components/CommandMenuWorkflowSelectTriggerTypeContent.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/workflow/trigger-type/components/CommandMenuWorkflowSelectTriggerTypeContent.tsx @@ -1,4 +1,4 @@ -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { WorkflowTriggerType, @@ -26,7 +26,7 @@ export const CommandMenuWorkflowSelectTriggerTypeContent = ({ const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState); - const { openWorkflowEditStepInCommandMenu } = useCommandMenu(); + const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu(); const handleTriggerTypeClick = ({ type, diff --git a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx index c9a1b81a4..31f7061f8 100644 --- a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx +++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx @@ -2,7 +2,7 @@ import { useLocation } from 'react-router-dom'; import { useRecoilState, useSetRecoilState } from 'recoil'; import { IconSearch, IconSettings } from 'twenty-ui'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu'; import { CurrentWorkspaceMemberFavoritesFolders } from '@/favorites/components/CurrentWorkspaceMemberFavoritesFolders'; import { WorkspaceFavorites } from '@/favorites/components/WorkspaceFavorites'; import { NavigationDrawerOpenedSection } from '@/object-metadata/components/NavigationDrawerOpenedSection'; @@ -41,7 +41,7 @@ export const MainNavigationDrawerItems = () => { const { t } = useLingui(); - const { openRecordsSearchPage } = useCommandMenu(); + const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu(); return ( <> diff --git a/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx b/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx index 3759e4a06..ded641d49 100644 --- a/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx +++ b/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx @@ -1,4 +1,5 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { useOpenSettingsMenu } from '@/navigation/hooks/useOpenSettings'; import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; @@ -17,7 +18,8 @@ type NavigationBarItemName = 'main' | 'search' | 'tasks' | 'settings'; export const MobileNavigationBar = () => { const [isCommandMenuOpened] = useRecoilState(isCommandMenuOpenedState); - const { closeCommandMenu, openRecordsSearchPage } = useCommandMenu(); + const { closeCommandMenu } = useCommandMenu(); + const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu(); const isSettingsPage = useIsSettingsPage(); const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] = useRecoilState(isNavigationDrawerExpandedState); diff --git a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx index f49cd7bcc..6646ace6c 100644 --- a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx +++ b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx @@ -6,7 +6,7 @@ import { isModifiedEvent, } from 'twenty-ui'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage'; import { useRecordChipData } from '@/object-record/hooks/useRecordChipData'; import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState'; @@ -39,7 +39,7 @@ export const RecordChip = ({ record, }); - const { openRecordInCommandMenu } = useCommandMenu(); + const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx index 88203a1c5..c5e9e696a 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx @@ -1,13 +1,13 @@ import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState'; import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId'; import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext'; import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext'; import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState'; import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; +import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; import { RecordBoardCardBody } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBody'; import { RecordBoardCardHeader } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeader'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; @@ -81,7 +81,7 @@ export const RecordBoardCard = ({ position?: 'first' | 'last'; }) => { const navigate = useNavigateApp(); - const { openRecordInCommandMenu } = useCommandMenu(); + const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); const { recordId } = useContext(RecordBoardCardContext); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunity.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunity.tsx index 6f9243f94..1f9887736 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunity.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunity.tsx @@ -1,4 +1,4 @@ -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard'; @@ -38,7 +38,7 @@ export const RecordBoardColumnNewOpportunity = ({ viewableRecordNameSingularState, ); - const { openRecordInCommandMenu } = useCommandMenu(); + const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); const createCompanyOpportunityAndOpenCommandMenu = async ( searchInput?: string, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer.ts index 687f1baa3..e134072e9 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer.ts @@ -1,7 +1,7 @@ import { useSetRecoilState } from 'recoil'; import { v4 } from 'uuid'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; @@ -42,7 +42,7 @@ export const useAddNewRecordAndOpenRightDrawer = ({ .nameSingular ?? 'workspaceMember', }); - const { openRecordInCommandMenu } = useCommandMenu(); + const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); if ( relationObjectMetadataNameSingular === 'workspaceMember' || diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecords.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecords.ts index 7de49f058..81b8059b6 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecords.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecords.ts @@ -1,4 +1,4 @@ -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState'; @@ -39,7 +39,7 @@ export const useCreateNewTableRecord = ({ const { setActiveDropdownFocusIdAndMemorizePrevious } = useSetActiveDropdownFocusIdAndMemorizePrevious(); - const { openRecordInCommandMenu } = useCommandMenu(); + const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); const { createOneRecord } = useCreateOneRecord({ objectNameSingular: objectMetadataItem.nameSingular, diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts index c25fe586b..b17c5d6fd 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts @@ -17,7 +17,7 @@ import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useC import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { isDefined } from 'twenty-shared'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState'; @@ -75,7 +75,7 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => { const { setActiveDropdownFocusIdAndMemorizePrevious } = useSetActiveDropdownFocusIdAndMemorizePrevious(); - const { openRecordInCommandMenu } = useCommandMenu(); + const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); const { openFieldInput } = useOpenFieldInputEditMode(); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx index 51fc67c47..36ba99515 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx @@ -2,9 +2,9 @@ import { useCallback, useContext } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; import { workflowIdState } from '@/workflow/states/workflowIdState'; import { EMPTY_TRIGGER_STEP_ID } from '@/workflow/workflow-diagram/constants/EmptyTriggerStepId'; import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; @@ -27,7 +27,7 @@ export const WorkflowDiagramCanvasEditableEffect = () => { const { openWorkflowTriggerTypeInCommandMenu, openWorkflowEditStepInCommandMenu, - } = useCommandMenu(); + } = useWorkflowCommandMenu(); const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect.tsx index f29a8c9be..1e75b864e 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect.tsx @@ -1,4 +1,4 @@ -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; import { workflowIdState } from '@/workflow/states/workflowIdState'; import { useTriggerNodeSelection } from '@/workflow/workflow-diagram/hooks/useTriggerNodeSelection'; import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState'; @@ -16,7 +16,7 @@ import { useIcons } from 'twenty-ui'; export const WorkflowDiagramCanvasReadonlyEffect = () => { const { getIcon } = useIcons(); const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState); - const { openWorkflowViewStepInCommandMenu } = useCommandMenu(); + const { openWorkflowViewStepInCommandMenu } = useWorkflowCommandMenu(); const workflowId = useRecoilValue(workflowIdState); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect.tsx index a26e5c053..dc4124342 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect.tsx @@ -1,4 +1,4 @@ -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; import { useTabListStates } from '@/ui/layout/tab/hooks/internal/useTabListStates'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { workflowIdState } from '@/workflow/states/workflowIdState'; @@ -20,7 +20,7 @@ import { useIcons } from 'twenty-ui'; export const WorkflowRunDiagramCanvasEffect = () => { const { getIcon } = useIcons(); const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState); - const { openWorkflowViewRunStepInCommandMenu } = useCommandMenu(); + const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu(); const workflowId = useRecoilValue(workflowIdState); @@ -68,7 +68,7 @@ export const WorkflowRunDiagramCanvasEffect = () => { } if (isDefined(workflowId)) { - openWorkflowViewRunStepInCommandMenu( + openWorkflowRunViewStepInCommandMenu( workflowId, selectedNodeData.name, getIcon(getWorkflowNodeIconKey(selectedNodeData)), @@ -80,7 +80,7 @@ export const WorkflowRunDiagramCanvasEffect = () => { workflowId, getIcon, goBackToFirstWorkflowRunRightDrawerTabIfNeeded, - openWorkflowViewRunStepInCommandMenu, + openWorkflowRunViewStepInCommandMenu, ], ); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useStartNodeCreation.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useStartNodeCreation.ts index 55e268bf7..2a0078354 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useStartNodeCreation.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useStartNodeCreation.ts @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; import { workflowIdState } from '@/workflow/states/workflowIdState'; import { workflowCreateStepFromParentStepIdState } from '@/workflow/workflow-steps/states/workflowCreateStepFromParentStepIdState'; import { isDefined } from 'twenty-shared'; @@ -10,7 +10,7 @@ export const useStartNodeCreation = () => { const setWorkflowCreateStepFromParentStepId = useSetRecoilState( workflowCreateStepFromParentStepIdState, ); - const { openWorkflowActionInCommandMenu } = useCommandMenu(); + const { openStepSelectInCommandMenu } = useWorkflowCommandMenu(); const workflowId = useRecoilValue(workflowIdState); @@ -23,14 +23,14 @@ export const useStartNodeCreation = () => { setWorkflowCreateStepFromParentStepId(parentNodeId); if (isDefined(workflowId)) { - openWorkflowActionInCommandMenu(workflowId); + openStepSelectInCommandMenu(workflowId); return; } }, [ setWorkflowCreateStepFromParentStepId, workflowId, - openWorkflowActionInCommandMenu, + openStepSelectInCommandMenu, ], ); diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/RightDrawerWorkflowSelectTriggerTypeContent.tsx b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/RightDrawerWorkflowSelectTriggerTypeContent.tsx index 6e9978526..6ed810101 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/RightDrawerWorkflowSelectTriggerTypeContent.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/RightDrawerWorkflowSelectTriggerTypeContent.tsx @@ -1,4 +1,4 @@ -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { WorkflowTriggerType, @@ -26,7 +26,7 @@ export const RightDrawerWorkflowSelectTriggerTypeContent = ({ const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState); - const { openWorkflowEditStepInCommandMenu } = useCommandMenu(); + const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu(); const handleTriggerTypeClick = ({ type, diff --git a/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx b/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx index 1d20db2e4..029c3711c 100644 --- a/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx +++ b/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx @@ -2,12 +2,14 @@ import { PropsWithChildren, useEffect, useState } from 'react'; import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; +import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { ContextStoreTargetedRecordsRule, contextStoreTargetedRecordsRuleComponentState, } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; @@ -18,6 +20,7 @@ export type JestContextStoreSetterMocks = { contextStoreFilters?: RecordFilter[]; contextStoreCurrentObjectMetadataNameSingular?: string; contextStoreCurrentViewId?: string; + contextStoreCurrentViewType?: ContextStoreViewType; }; type JestContextStoreSetterProps = @@ -31,6 +34,7 @@ export const JestContextStoreSetter = ({ contextStoreNumberOfSelectedRecords = 0, contextStoreCurrentObjectMetadataNameSingular = 'company', contextStoreFilters = [], + contextStoreCurrentViewType, children, }: JestContextStoreSetterProps) => { const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2( @@ -54,6 +58,10 @@ export const JestContextStoreSetter = ({ contextStoreCurrentViewIdComponentState, ); + const setContextStoreCurrentViewType = useSetRecoilComponentStateV2( + contextStoreCurrentViewTypeComponentState, + ); + const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular: contextStoreCurrentObjectMetadataNameSingular, }); @@ -67,7 +75,7 @@ export const JestContextStoreSetter = ({ setContextStoreCurrentObjectMetadataItemId(objectMetadataItem.id); setContextStoreNumberOfSelectedRecords(contextStoreNumberOfSelectedRecords); setcontextStoreFiltersComponentState(contextStoreFilters); - + setContextStoreCurrentViewType(contextStoreCurrentViewType ?? null); setIsLoaded(true); }, [ setContextStoreTargetedRecordsRule, @@ -81,6 +89,8 @@ export const JestContextStoreSetter = ({ objectMetadataItem, setContextStoreCurrentViewId, contextStoreCurrentViewId, + setContextStoreCurrentViewType, + contextStoreCurrentViewType, ]); return isLoaded ? <>{children} : null; diff --git a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper.tsx b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper.tsx index 0169d51c2..a76476afd 100644 --- a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper.tsx +++ b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper.tsx @@ -28,6 +28,7 @@ export const getJestMetadataAndApolloMocksAndActionMenuWrapper = ({ onInitializeRecoilSnapshot, contextStoreTargetedRecordsRule, contextStoreCurrentViewId, + contextStoreCurrentViewType, contextStoreNumberOfSelectedRecords, contextStoreCurrentObjectMetadataNameSingular, contextStoreFilters, @@ -93,6 +94,7 @@ export const getJestMetadataAndApolloMocksAndActionMenuWrapper = ({ contextStoreCurrentObjectMetadataNameSingular={ contextStoreCurrentObjectMetadataNameSingular } + contextStoreCurrentViewType={contextStoreCurrentViewType} > {children}