From e86116aa571ed6ff3bc8b55aa0291bef6468b36f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Thu, 6 Mar 2025 17:09:40 +0100 Subject: [PATCH] 491 save the page component instance id for side panel navigation (#10700) Closes https://github.com/twentyhq/core-team-issues/issues/491 This PR: - Duplicates the right drawer pages for the command menu and replace all the states used in these pages by component states (The right drawer pages will be deleted when we deprecate the command menu v1) - Wraps those pages into a component instance provider - We store the component instance id upon navigation to restore the states when we navigate back to a page The only pages which are not updated for now are the pages related to the workflow objects, this will be done in another PR. In another PR, to improve the navigation experience I will replace the icons and titles of the chips by the label identifier and the avatar if the page is a record page. https://github.com/user-attachments/assets/a76d3345-01f3-4db9-8a55-331cca8b87e0 --------- Co-authored-by: Lucas Bordeau --- .../RecordShowRightDrawerActionMenu.tsx | 13 +- .../calendar/components/CalendarEventRow.tsx | 15 +- .../emails/components/EmailThreadPreview.tsx | 20 +- .../components/EventCardCalendarEvent.tsx | 7 +- .../message/components/EventCardMessage.tsx | 9 +- .../CommandMenuContextChipGroups.tsx | 12 +- ....tsx => CommandMenuContextRecordsChip.tsx} | 2 +- .../components/CommandMenuRouter.tsx | 34 +- .../components/CommandMenuTopBar.tsx | 17 +- .../ResetContextToSelectionCommandButton.tsx | 4 +- .../__stories__/CommandMenu.stories.tsx | 1 + .../CommandMenuContextRecordChip.stories.tsx | 8 +- .../constants/CommandMenuPagesConfig.tsx | 12 +- .../hooks/__tests__/useCommandMenu.test.tsx | 19 +- .../{useCommandMenu.ts => useCommandMenu.tsx} | 138 +++++- .../CommandMenuCalendarEventPage.tsx | 37 ++ ...dMenuMessageThreadIntermediaryMessages.tsx | 44 ++ .../CommandMenuMessageThreadPage.tsx | 169 +++++++ .../useEmailThreadInCommandMenu.test.tsx | 434 ++++++++++++++++++ .../hooks/useEmailThreadInCommandMenu.ts | 188 ++++++++ .../states/messageThreadComponentState.ts | 10 + .../components/CommandMenuRecordPage.tsx | 92 ++++ ...sNewViewableRecordLoadingComponentState.ts | 9 + .../states/viewableRecordIdComponentState.ts | 10 + ...iewableRecordNameSingularComponentState.ts | 10 + .../states/commandMenuNavigationStackState.ts | 1 + ...geTitle.ts => commandMenuPageInfoState.ts} | 11 +- ...ommandMenuPageComponentInstanceContext.tsx | 6 + .../states/viewableRecordNameSingularState.ts | 1 + .../hooks/useOpenRecordTableCellV2.ts | 2 +- 30 files changed, 1255 insertions(+), 80 deletions(-) rename packages/twenty-front/src/modules/command-menu/components/{CommandMenuContextRecordChip.tsx => CommandMenuContextRecordsChip.tsx} (96%) rename packages/twenty-front/src/modules/command-menu/hooks/{useCommandMenu.ts => useCommandMenu.tsx} (80%) create mode 100644 packages/twenty-front/src/modules/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/pages/message-thread/components/CommandMenuMessageThreadIntermediaryMessages.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/__tests__/useEmailThreadInCommandMenu.test.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/useEmailThreadInCommandMenu.ts create mode 100644 packages/twenty-front/src/modules/command-menu/pages/message-thread/states/messageThreadComponentState.ts create mode 100644 packages/twenty-front/src/modules/command-menu/pages/record-page/components/CommandMenuRecordPage.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/pages/record-page/states/isNewViewableRecordLoadingComponentState.ts create mode 100644 packages/twenty-front/src/modules/command-menu/pages/record-page/states/viewableRecordIdComponentState.ts create mode 100644 packages/twenty-front/src/modules/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState.ts rename packages/twenty-front/src/modules/command-menu/states/{commandMenuPageTitle.ts => commandMenuPageInfoState.ts} (59%) create mode 100644 packages/twenty-front/src/modules/command-menu/states/contexts/CommandMenuPageComponentInstanceContext.tsx diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenu.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenu.tsx index 5fea11be1..c1fb24a2a 100644 --- a/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenu.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenu.tsx @@ -5,8 +5,10 @@ import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMen import { RightDrawerActionMenuDropdown } from '@/action-menu/components/RightDrawerActionMenuDropdown'; import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; import { contextStoreCurrentObjectMetadataItemComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemComponentState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { isDefined } from 'twenty-shared'; import { FeatureFlagKey } from '~/generated-metadata/graphql'; export const RecordShowRightDrawerActionMenu = () => { @@ -14,6 +16,10 @@ export const RecordShowRightDrawerActionMenu = () => { contextStoreCurrentObjectMetadataItemComponentState, ); + const contextStoreTargetedRecordsRule = useRecoilComponentValueV2( + contextStoreTargetedRecordsRuleComponentState, + ); + const isWorkflowEnabled = useIsFeatureEnabled( FeatureFlagKey.IsWorkflowEnabled, ); @@ -24,7 +30,12 @@ export const RecordShowRightDrawerActionMenu = () => { - + + {isDefined(contextStoreTargetedRecordsRule) && + contextStoreTargetedRecordsRule.mode === 'selection' && + contextStoreTargetedRecordsRule.selectedRecordIds.length > 0 && ( + + )} {isWorkflowEnabled && ( 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 11cb67c02..9d986988c 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx @@ -11,6 +11,8 @@ 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 { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { isDefined } from 'twenty-shared'; import { Avatar, @@ -22,6 +24,7 @@ import { } from 'twenty-ui'; import { CalendarChannelVisibility, + FeatureFlagKey, TimelineCalendarEvent, } from '~/generated-metadata/graphql'; @@ -115,6 +118,10 @@ export const CalendarEventRow = ({ const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const { displayCurrentEventCursor = false } = useContext(CalendarContext); const { openCalendarEventRightDrawer } = useOpenCalendarEventRightDrawer(); + const { openCalendarEventInCommandMenu } = useCommandMenu(); + const isCommandMenuV2Enabled = useIsFeatureEnabled( + FeatureFlagKey.IsCommandMenuV2Enabled, + ); const startsAt = getCalendarEventStartDate(calendarEvent); const endsAt = getCalendarEventEndDate(calendarEvent); @@ -137,7 +144,13 @@ export const CalendarEventRow = ({ showTitle={showTitle} onClick={ showTitle - ? () => openCalendarEventRightDrawer(calendarEvent.id) + ? () => { + if (isCommandMenuV2Enabled) { + openCalendarEventInCommandMenu(calendarEvent.id); + } else { + openCalendarEventRightDrawer(calendarEvent.id); + } + } : undefined } > 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 a95cfb57c..aa7184f22 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx @@ -6,8 +6,14 @@ import { ActivityRow } from '@/activities/components/ActivityRow'; import { EmailThreadNotShared } from '@/activities/emails/components/EmailThreadNotShared'; import { useEmailThread } from '@/activities/emails/hooks/useEmailThread'; import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/states/lastViewableEmailThreadIdState'; +import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; -import { MessageChannelVisibility, TimelineThread } from '~/generated/graphql'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { + FeatureFlagKey, + MessageChannelVisibility, + TimelineThread, +} from '~/generated/graphql'; import { formatToHumanReadableDate } from '~/utils/date-utils'; const StyledHeading = styled.div<{ unread: boolean }>` @@ -72,6 +78,10 @@ type EmailThreadPreviewProps = { export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => { const { openEmailThread } = useEmailThread(); + const { openEmailThreadInCommandMenu } = useCommandMenu(); + const isCommandMenuV2Enabled = useIsFeatureEnabled( + FeatureFlagKey.IsCommandMenuV2Enabled, + ); const visibility = thread.visibility; @@ -111,12 +121,18 @@ export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => { emailThreadIdWhenEmailThreadWasClosed !== thread.id); if (canOpen) { - openEmailThread(thread.id); + if (isCommandMenuV2Enabled) { + openEmailThreadInCommandMenu(thread.id); + } else { + openEmailThread(thread.id); + } } }, [ + isCommandMenuV2Enabled, isSameEventThanRightDrawerClose, openEmailThread, + openEmailThreadInCommandMenu, thread.id, thread.visibility, ], diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/EventCardCalendarEvent.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/EventCardCalendarEvent.tsx index a58c05a4c..62176e412 100644 --- a/packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/EventCardCalendarEvent.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/EventCardCalendarEvent.tsx @@ -1,7 +1,6 @@ import styled from '@emotion/styled'; import { isUndefined } from '@sniptt/guards'; -import { useOpenCalendarEventRightDrawer } from '@/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer'; import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; @@ -107,8 +106,6 @@ export const EventCardCalendarEvent = ({ }, }); - const { openCalendarEventRightDrawer } = useOpenCalendarEventRightDrawer(); - const { timeZone } = useContext(UserContext); if (isDefined(error)) { @@ -152,9 +149,7 @@ export const EventCardCalendarEvent = ({ : null; return ( - openCalendarEventRightDrawer(calendarEvent.id)} - > + {startsAtMonth} diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventCardMessage.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventCardMessage.tsx index 9f7e9061f..7d11a133d 100644 --- a/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventCardMessage.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventCardMessage.tsx @@ -2,7 +2,6 @@ import styled from '@emotion/styled'; import { isUndefined } from '@sniptt/guards'; import { OverflowingTextWithTooltip } from 'twenty-ui'; -import { useEmailThread } from '@/activities/emails/hooks/useEmailThread'; import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage'; import { EventCardMessageNotShared } from '@/activities/timeline-activities/rows/message/components/EventCardMessageNotShared'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -80,8 +79,6 @@ export const EventCardMessage = ({ }, }); - const { openEmailThread } = useEmailThread(); - if (isDefined(error)) { const shouldHideMessageContent = error.graphQLErrors.some( (e) => e.extensions?.code === 'FORBIDDEN', @@ -120,11 +117,7 @@ export const EventCardMessage = ({ - openEmailThread(message.messageThreadId)} - > - {message.text} - + {message.text} ); diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChipGroups.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChipGroups.tsx index a261cf81f..1739bb606 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChipGroups.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChipGroups.tsx @@ -1,6 +1,7 @@ import { COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/command-menu/constants/CommandMenuContextChipGroupsDropdownId'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { isDefined } from 'twenty-shared'; import { MenuItem } from 'twenty-ui'; @@ -14,6 +15,8 @@ export const CommandMenuContextChipGroups = ({ }: { contextChips: CommandMenuContextChipProps[]; }) => { + const { closeDropdown } = useDropdownV2(); + if (contextChips.length === 0) { return null; } @@ -34,6 +37,7 @@ export const CommandMenuContextChipGroups = ({ } const firstChips = contextChips.slice(0, -1); + const firstThreeChips = firstChips.slice(0, 3); const lastChip = contextChips.at(-1); return ( @@ -42,8 +46,9 @@ export const CommandMenuContextChipGroups = ({ chip.Icons?.[0])} + Icons={firstThreeChips.map((chip) => chip.Icons?.[0])} onClick={() => {}} + text={`${firstChips.length}`} /> } dropdownComponents={ @@ -52,7 +57,10 @@ export const CommandMenuContextChipGroups = ({ { + closeDropdown(COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID); + chip.onClick?.(); + }} /> ))} diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChip.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordsChip.tsx similarity index 96% rename from packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChip.tsx rename to packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordsChip.tsx index a5802c551..928787036 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChip.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordsChip.tsx @@ -4,7 +4,7 @@ import { getSelectedRecordsContextText } from '@/command-menu/utils/getRecordCon import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; -export const CommandMenuContextRecordChip = ({ +export const CommandMenuContextRecordsChip = ({ objectMetadataItemId, instanceId, }: { diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx index 570e2f34e..ae08b3956 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx @@ -1,7 +1,9 @@ import { CommandMenuContainer } from '@/command-menu/components/CommandMenuContainer'; import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar'; import { COMMAND_MENU_PAGES_CONFIG } from '@/command-menu/constants/CommandMenuPagesConfig'; +import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; +import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { motion } from 'framer-motion'; @@ -16,6 +18,8 @@ const StyledCommandMenuContent = styled.div` export const CommandMenuRouter = () => { const commandMenuPage = useRecoilValue(commandMenuPageState); + const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState); + const commandMenuPageComponent = isDefined(commandMenuPage) ? ( COMMAND_MENU_PAGES_CONFIG.get(commandMenuPage) ) : ( @@ -26,20 +30,24 @@ export const CommandMenuRouter = () => { return ( - - - - - {commandMenuPageComponent} - + + + + + {commandMenuPageComponent} + + ); }; 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 714351c49..90507e145 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx @@ -128,16 +128,21 @@ export const CommandMenuTopBar = () => { (page) => page.page !== CommandMenuPages.Root, ); - return filteredCommandMenuNavigationStack.map((page, index) => ({ - Icons: [], - text: page.pageTitle, - onClick: - index === filteredCommandMenuNavigationStack.length - 1 + return filteredCommandMenuNavigationStack.map((page, index) => { + const isLastChip = + index === filteredCommandMenuNavigationStack.length - 1; + + return { + page, + Icons: [], + text: page.pageTitle, + onClick: isLastChip ? undefined : () => { navigateCommandMenuHistory(index); }, - })); + }; + }); }, [ commandMenuNavigationStack, navigateCommandMenuHistory, diff --git a/packages/twenty-front/src/modules/command-menu/components/ResetContextToSelectionCommandButton.tsx b/packages/twenty-front/src/modules/command-menu/components/ResetContextToSelectionCommandButton.tsx index 5992786b3..2f6ae8621 100644 --- a/packages/twenty-front/src/modules/command-menu/components/ResetContextToSelectionCommandButton.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/ResetContextToSelectionCommandButton.tsx @@ -1,4 +1,4 @@ -import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip'; +import { CommandMenuContextRecordsChip } from '@/command-menu/components/CommandMenuContextRecordsChip'; import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; import { RESET_CONTEXT_TO_SELECTION } from '@/command-menu/constants/ResetContextToSelection'; import { useResetPreviousCommandMenuContext } from '@/command-menu/hooks/useResetPreviousCommandMenuContext'; @@ -46,7 +46,7 @@ export const ResetContextToSelectionCommandButton = () => { Icon={IconArrowBackUp} label={t`Reset to`} RightComponent={ - diff --git a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx index 88e88cbad..a37b4bb52 100644 --- a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx @@ -100,6 +100,7 @@ const meta: Meta = { page: CommandMenuPages.Root, pageTitle: 'Command Menu', pageIcon: IconDotsVertical, + pageId: '1', }, ]); diff --git a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenuContextRecordChip.stories.tsx b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenuContextRecordChip.stories.tsx index 5f4429f48..1e08fc800 100644 --- a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenuContextRecordChip.stories.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenuContextRecordChip.stories.tsx @@ -1,7 +1,7 @@ import { gql } from '@apollo/client'; import { Decorator, Meta, StoryObj } from '@storybook/react'; -import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip'; +import { CommandMenuContextRecordsChip } from '@/command-menu/components/CommandMenuContextRecordsChip'; import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext'; import { RecordChipData } from '@/object-record/record-field/types/RecordChipData'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; @@ -207,9 +207,9 @@ const ContextStoreDecorator: Decorator = (Story) => { ); }; -const meta: Meta = { +const meta: Meta = { title: 'Modules/CommandMenu/CommandMenuContextRecordChip', - component: CommandMenuContextRecordChip, + component: CommandMenuContextRecordsChip, decorators: [ ContextStoreDecorator, ChipGeneratorsDecorator, @@ -221,7 +221,7 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = {}; diff --git a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx index 8426c0a33..89c739aaa 100644 --- a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx +++ b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx @@ -1,10 +1,10 @@ -import { RightDrawerCalendarEvent } from '@/activities/calendar/right-drawer/components/RightDrawerCalendarEvent'; import { RightDrawerAIChat } from '@/activities/copilot/right-drawer/components/RightDrawerAIChat'; -import { RightDrawerEmailThread } from '@/activities/emails/right-drawer/components/RightDrawerEmailThread'; import { CommandMenu } from '@/command-menu/components/CommandMenu'; +import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage'; import { CommandMenuSearchRecordsPage } from '@/command-menu/pages/components/CommandMenuSearchRecordsPage'; +import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage'; +import { CommandMenuRecordPage } from '@/command-menu/pages/record-page/components/CommandMenuRecordPage'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; -import { RightDrawerRecord } from '@/object-record/record-right-drawer/components/RightDrawerRecord'; import { RightDrawerWorkflowEditStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowEditStep'; import { RightDrawerWorkflowRunViewStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowRunViewStep'; import { RightDrawerWorkflowViewStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowViewStep'; @@ -16,9 +16,9 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map< React.ReactNode >([ [CommandMenuPages.Root, ], - [CommandMenuPages.ViewRecord, ], - [CommandMenuPages.ViewEmailThread, ], - [CommandMenuPages.ViewCalendarEvent, ], + [CommandMenuPages.ViewRecord, ], + [CommandMenuPages.ViewEmailThread, ], + [CommandMenuPages.ViewCalendarEvent, ], [CommandMenuPages.Copilot, ], [ CommandMenuPages.WorkflowStepSelectTriggerType, 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 2f76b12c9..f3db8942b 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 @@ -5,8 +5,8 @@ import { RecoilRoot, useRecoilValue } from 'recoil'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; +import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; -import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; import { IconList, IconSearch } from 'twenty-ui'; @@ -91,6 +91,7 @@ describe('useCommandMenu', () => { expect(result.current.commandMenuPageInfo).toEqual({ title: undefined, Icon: undefined, + instanceId: '', }); act(() => { @@ -98,6 +99,7 @@ describe('useCommandMenu', () => { page: CommandMenuPages.SearchRecords, pageTitle: 'Search', pageIcon: IconSearch, + pageId: '1', }); }); @@ -106,12 +108,14 @@ describe('useCommandMenu', () => { page: CommandMenuPages.SearchRecords, pageTitle: 'Search', pageIcon: IconSearch, + pageId: '1', }, ]); expect(result.current.commandMenuPage).toBe(CommandMenuPages.SearchRecords); expect(result.current.commandMenuPageInfo).toEqual({ title: 'Search', Icon: IconSearch, + instanceId: '1', }); act(() => { @@ -119,6 +123,7 @@ describe('useCommandMenu', () => { page: CommandMenuPages.ViewRecord, pageTitle: 'Company', pageIcon: IconList, + pageId: '2', }); }); @@ -127,17 +132,20 @@ describe('useCommandMenu', () => { page: CommandMenuPages.SearchRecords, pageTitle: 'Search', pageIcon: IconSearch, + pageId: '1', }, { page: CommandMenuPages.ViewRecord, pageTitle: 'Company', pageIcon: IconList, + pageId: '2', }, ]); expect(result.current.commandMenuPage).toBe(CommandMenuPages.ViewRecord); expect(result.current.commandMenuPageInfo).toEqual({ title: 'Company', Icon: IconList, + instanceId: '2', }); }); @@ -149,6 +157,7 @@ describe('useCommandMenu', () => { page: CommandMenuPages.SearchRecords, pageTitle: 'Search', pageIcon: IconSearch, + pageId: '1', }); }); @@ -157,6 +166,7 @@ describe('useCommandMenu', () => { page: CommandMenuPages.ViewRecord, pageTitle: 'Company', pageIcon: IconList, + pageId: '2', }); }); @@ -165,11 +175,13 @@ describe('useCommandMenu', () => { page: CommandMenuPages.SearchRecords, pageTitle: 'Search', pageIcon: IconSearch, + pageId: '1', }, { page: CommandMenuPages.ViewRecord, pageTitle: 'Company', pageIcon: IconList, + pageId: '2', }, ]); @@ -182,12 +194,14 @@ describe('useCommandMenu', () => { page: CommandMenuPages.SearchRecords, pageTitle: 'Search', pageIcon: IconSearch, + pageId: '1', }, ]); expect(result.current.commandMenuPage).toBe(CommandMenuPages.SearchRecords); expect(result.current.commandMenuPageInfo).toEqual({ title: 'Search', Icon: IconSearch, + instanceId: '1', }); act(() => { @@ -199,6 +213,7 @@ describe('useCommandMenu', () => { expect(result.current.commandMenuPage).toBe(CommandMenuPages.Root); expect(result.current.commandMenuPageInfo).toEqual({ title: undefined, + instanceId: '', Icon: undefined, }); expect(result.current.isCommandMenuOpened).toBe(false); @@ -212,6 +227,7 @@ describe('useCommandMenu', () => { page: CommandMenuPages.SearchRecords, pageTitle: 'Search', pageIcon: IconSearch, + pageId: '1', }); }); @@ -223,6 +239,7 @@ describe('useCommandMenu', () => { expect(result.current.commandMenuPageInfo).toEqual({ title: 'Search', Icon: IconSearch, + instanceId: '1', }); }); }); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.tsx similarity index 80% rename from packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts rename to packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.tsx index 85fc80c2d..6b7d9a2c2 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.tsx @@ -5,19 +5,25 @@ import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objec import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; -import { IconDotsVertical, IconSearch, useIcons } from 'twenty-ui'; +import { + IconCalendarEvent, + IconComponent, + IconDotsVertical, + IconMail, + IconSearch, + useIcons, +} from 'twenty-ui'; import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId'; import { COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/command-menu/constants/CommandMenuContextChipGroupsDropdownId'; import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId'; import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates'; import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates'; -import { - CommandMenuNavigationStackItem, - commandMenuNavigationStackState, -} from '@/command-menu/states/commandMenuNavigationStackState'; +import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState'; +import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState'; +import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState'; +import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; -import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle'; import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState'; import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; @@ -29,17 +35,23 @@ import { contextStoreFiltersComponentState } from '@/context-store/states/contex import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType'; -import { RIGHT_DRAWER_RECORD_INSTANCE_ID } from '@/object-record/record-right-drawer/constants/RightDrawerRecordInstanceId'; import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; -import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2'; import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent'; import { isDragSelectionStartEnabledState } from '@/ui/utilities/drag-select/states/internal/isDragSelectionStartEnabledState'; import { t } from '@lingui/core/macro'; import { useCallback } from 'react'; import { capitalize, isDefined } from 'twenty-shared'; +import { v4 } from 'uuid'; import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState'; +export type CommandMenuNavigationStackItem = { + page: CommandMenuPages; + pageTitle: string; + pageIcon: IconComponent; + pageId?: string; +}; + export const useCommandMenu = () => { const { resetSelectedItem } = useSelectableList('command-menu-list'); const { @@ -76,6 +88,7 @@ export const useCommandMenu = () => { set(commandMenuPageInfoState, { title: undefined, Icon: undefined, + instanceId: '', }); set(isCommandMenuOpenedState, false); set(commandMenuSearchState, ''); @@ -138,15 +151,21 @@ export const useCommandMenu = () => { page, pageTitle, pageIcon, + pageId, resetNavigationStack = false, }: CommandMenuNavigationStackItem & { resetNavigationStack?: boolean; }) => { + if (!pageId) { + pageId = v4(); + } + openCommandMenu(); set(commandMenuPageState, page); set(commandMenuPageInfoState, { title: pageTitle, Icon: pageIcon, + instanceId: pageId, }); const isCommandMenuClosing = snapshot @@ -157,16 +176,24 @@ export const useCommandMenu = () => { ? [] : snapshot.getLoadable(commandMenuNavigationStackState).getValue(); - const itemIsAlreadyInStack = currentNavigationStack.some( - (item) => item.page === page, - ); - - if (resetNavigationStack || itemIsAlreadyInStack) { - set(commandMenuNavigationStackState, [{ page, pageTitle, pageIcon }]); + if (resetNavigationStack) { + set(commandMenuNavigationStackState, [ + { + page, + pageTitle, + pageIcon, + pageId, + }, + ]); } else { set(commandMenuNavigationStackState, [ ...currentNavigationStack, - { page, pageTitle, pageIcon }, + { + page, + pageTitle, + pageIcon, + pageId, + }, ]); } }; @@ -221,6 +248,7 @@ export const useCommandMenu = () => { set(commandMenuPageInfoState, { title: lastNavigationStackItem.pageTitle, Icon: lastNavigationStackItem.pageIcon, + instanceId: lastNavigationStackItem.pageId, }); set(commandMenuNavigationStackState, newNavigationStack); @@ -252,6 +280,7 @@ export const useCommandMenu = () => { set(commandMenuPageInfoState, { title: newNavigationStackItem?.pageTitle, Icon: newNavigationStackItem?.pageIcon, + instanceId: newNavigationStackItem?.pageId, }); set(hasUserSelectedCommandState, false); @@ -269,7 +298,20 @@ export const useCommandMenu = () => { objectNameSingular: string; isNewRecord?: boolean; }) => { - set(viewableRecordNameSingularState, objectNameSingular); + const pageComponentInstanceId = v4(); + + set( + viewableRecordNameSingularComponentState.atomFamily({ + instanceId: pageComponentInstanceId, + }), + objectNameSingular, + ); + set( + viewableRecordIdComponentState.atomFamily({ + instanceId: pageComponentInstanceId, + }), + recordId, + ); set(viewableRecordIdState, recordId); const objectMetadataItem = snapshot @@ -289,14 +331,14 @@ export const useCommandMenu = () => { set( contextStoreCurrentObjectMetadataItemComponentState.atomFamily({ - instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID, + instanceId: pageComponentInstanceId, }), objectMetadataItem, ); set( contextStoreTargetedRecordsRuleComponentState.atomFamily({ - instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID, + instanceId: pageComponentInstanceId, }), { mode: 'selection', @@ -306,21 +348,21 @@ export const useCommandMenu = () => { set( contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ - instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID, + instanceId: pageComponentInstanceId, }), 1, ); set( contextStoreCurrentViewTypeComponentState.atomFamily({ - instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID, + instanceId: pageComponentInstanceId, }), ContextStoreViewType.ShowPage, ); set( contextStoreCurrentViewIdComponentState.atomFamily({ - instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID, + instanceId: pageComponentInstanceId, }), snapshot .getLoadable( @@ -343,8 +385,8 @@ export const useCommandMenu = () => { ? t`New ${capitalizedObjectNameSingular}` : capitalizedObjectNameSingular, pageIcon: Icon, - // TODO: remove this once we can store the navigation stack page states - resetNavigationStack: true, + pageId: pageComponentInstanceId, + resetNavigationStack: false, }); }; }, @@ -356,9 +398,56 @@ export const useCommandMenu = () => { page: CommandMenuPages.SearchRecords, pageTitle: 'Search', pageIcon: IconSearch, + pageId: v4(), }); }; + const openCalendarEventInCommandMenu = useRecoilCallback( + ({ set }) => { + return (calendarEventId: string) => { + const pageComponentInstanceId = v4(); + + set( + viewableRecordIdComponentState.atomFamily({ + instanceId: pageComponentInstanceId, + }), + calendarEventId, + ); + + navigateCommandMenu({ + page: CommandMenuPages.ViewCalendarEvent, + pageTitle: 'Calendar Event', + pageIcon: IconCalendarEvent, + pageId: pageComponentInstanceId, + }); + }; + }, + [navigateCommandMenu], + ); + + const openEmailThreadInCommandMenu = useRecoilCallback( + ({ set }) => { + return (emailThreadId: string) => { + const pageComponentInstanceId = v4(); + + set( + viewableRecordIdComponentState.atomFamily({ + instanceId: pageComponentInstanceId, + }), + emailThreadId, + ); + + navigateCommandMenu({ + page: CommandMenuPages.ViewEmailThread, + pageTitle: 'Email Thread', + pageIcon: IconMail, + pageId: pageComponentInstanceId, + }); + }; + }, + [navigateCommandMenu], + ); + const setGlobalCommandMenuContext = useRecoilCallback( ({ set }) => { return () => { @@ -401,6 +490,7 @@ export const useCommandMenu = () => { set(commandMenuPageInfoState, { title: undefined, Icon: undefined, + instanceId: '', }); set(hasUserSelectedCommandState, false); @@ -420,5 +510,7 @@ export const useCommandMenu = () => { openRecordInCommandMenu, toggleCommandMenu, setGlobalCommandMenuContext, + openCalendarEventInCommandMenu, + openEmailThreadInCommandMenu, }; }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage.tsx b/packages/twenty-front/src/modules/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage.tsx new file mode 100644 index 000000000..e63155544 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage.tsx @@ -0,0 +1,37 @@ +import { CalendarEventDetails } from '@/activities/calendar/components/CalendarEventDetails'; +import { CalendarEventDetailsEffect } from '@/activities/calendar/components/CalendarEventDetailsEffect'; +import { FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE } from '@/activities/calendar/graphql/operation-signatures/FindOneCalendarEventOperationSignature'; +import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent'; +import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState'; +import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; +import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect'; +import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; +import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +export const CommandMenuCalendarEventPage = () => { + const { upsertRecords } = useUpsertRecordsInStore(); + const viewableRecordId = useRecoilComponentValueV2( + viewableRecordIdComponentState, + ); + + const { record: calendarEvent } = useFindOneRecord({ + objectNameSingular: + FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE.objectNameSingular, + objectRecordId: viewableRecordId ?? '', + recordGqlFields: FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE.fields, + onCompleted: (record) => upsertRecords([record]), + }); + + if (!calendarEvent) { + return null; + } + + return ( + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/message-thread/components/CommandMenuMessageThreadIntermediaryMessages.tsx b/packages/twenty-front/src/modules/command-menu/pages/message-thread/components/CommandMenuMessageThreadIntermediaryMessages.tsx new file mode 100644 index 000000000..806d8a938 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/message-thread/components/CommandMenuMessageThreadIntermediaryMessages.tsx @@ -0,0 +1,44 @@ +import styled from '@emotion/styled'; +import { useState } from 'react'; +import { Button, IconArrowsVertical } from 'twenty-ui'; + +import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage'; +import { EmailThreadMessageWithSender } from '@/activities/emails/types/EmailThreadMessageWithSender'; + +const StyledButtonContainer = styled.div` + border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; + padding: 16px 24px; +`; + +export const CommandMenuMessageThreadIntermediaryMessages = ({ + messages, +}: { + messages: EmailThreadMessageWithSender[]; +}) => { + const [areMessagesOpen, setAreMessagesOpen] = useState(false); + + if (messages.length === 0) { + return null; + } + + return areMessagesOpen ? ( + messages.map((message) => ( + + )) + ) : ( + +