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) => ( + + )) + ) : ( + +