From 337b6a86ab40ce2e4cebfbd91677bab880be18b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:44:21 +0100 Subject: [PATCH] 251 create top bar chips inside the command menu (#9809) Closes #https://github.com/twentyhq/core-team-issues/issues/251 https://github.com/user-attachments/assets/065c97fe-1daf-4b48-9d57-6bbb96d24ede --- .../hooks/useOpenCalendarEventRightDrawer.ts | 6 +- .../hooks/useOpenCopilotRightDrawer.ts | 6 +- .../useOpenEmailThreadRightDrawer.test.ts | 5 + .../hooks/useOpenEmailThreadRightDrawer.ts | 6 +- .../components/CommandMenuContextChip.tsx | 63 +++++ .../CommandMenuContextRecordChip.tsx | 56 ++-- .../CommandMenuContextRecordChipAvatars.tsx | 18 +- .../components/CommandMenuTopBar.tsx | 14 + .../CommandMenuContextChip.stories.tsx | 53 ++++ .../CommandMenuContextRecordChip.stories.tsx | 261 ++++++++++++++++++ .../command-menu/hooks/useCommandMenu.ts | 10 + .../states/commandMenuPageTitle.ts | 10 + .../right-drawer/hooks/useRightDrawer.ts | 14 +- .../WorkflowDiagramCanvasEditableEffect.tsx | 20 +- .../WorkflowDiagramCanvasReadonlyEffect.tsx | 13 +- .../WorkflowDiagramStepNodeBase.tsx | 23 +- .../hooks/useStartNodeCreation.ts | 6 +- .../utils/getWorkflowNodeIcon.ts | 56 ++++ .../hooks/__tests__/useCreateStep.test.ts | 2 +- .../workflow-steps/hooks/useCreateStep.ts | 13 +- ...DrawerWorkflowSelectTriggerTypeContent.tsx | 10 +- 21 files changed, 582 insertions(+), 83 deletions(-) create mode 100644 packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChip.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenuContextChip.stories.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenuContextRecordChip.stories.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/states/commandMenuPageTitle.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowNodeIcon.ts diff --git a/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer.ts b/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer.ts index b10743f35..4575eb875 100644 --- a/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer.ts +++ b/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer.ts @@ -5,6 +5,7 @@ import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { IconCalendarEvent } from 'twenty-ui'; export const useOpenCalendarEventRightDrawer = () => { const { openRightDrawer } = useRightDrawer(); @@ -13,7 +14,10 @@ export const useOpenCalendarEventRightDrawer = () => { const openCalendarEventRightDrawer = (calendarEventId: string) => { setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); - openRightDrawer(RightDrawerPages.ViewCalendarEvent); + openRightDrawer(RightDrawerPages.ViewCalendarEvent, { + title: 'Calendar Event', + Icon: IconCalendarEvent, + }); setViewableRecordId(calendarEventId); }; diff --git a/packages/twenty-front/src/modules/activities/copilot/right-drawer/hooks/useOpenCopilotRightDrawer.ts b/packages/twenty-front/src/modules/activities/copilot/right-drawer/hooks/useOpenCopilotRightDrawer.ts index 536945162..8b3e8b4cb 100644 --- a/packages/twenty-front/src/modules/activities/copilot/right-drawer/hooks/useOpenCopilotRightDrawer.ts +++ b/packages/twenty-front/src/modules/activities/copilot/right-drawer/hooks/useOpenCopilotRightDrawer.ts @@ -2,6 +2,7 @@ import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { IconSparkles } from 'twenty-ui'; export const useOpenCopilotRightDrawer = () => { const { openRightDrawer } = useRightDrawer(); @@ -9,6 +10,9 @@ export const useOpenCopilotRightDrawer = () => { return () => { setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); - openRightDrawer(RightDrawerPages.Copilot); + openRightDrawer(RightDrawerPages.Copilot, { + title: 'Copilot', + Icon: IconSparkles, + }); }; }; diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useOpenEmailThreadRightDrawer.test.ts b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useOpenEmailThreadRightDrawer.test.ts index 8660d7617..25c21ee10 100644 --- a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useOpenEmailThreadRightDrawer.test.ts +++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useOpenEmailThreadRightDrawer.test.ts @@ -4,6 +4,7 @@ import { act } from 'react-dom/test-utils'; import { useOpenEmailThreadRightDrawer } from '@/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer'; import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; +import { IconMail } from 'twenty-ui'; const mockOpenRightDrawer = jest.fn(); const mockSetHotkeyScope = jest.fn(); @@ -31,5 +32,9 @@ test('useOpenEmailThreadRightDrawer opens the email thread right drawer', () => ); expect(mockOpenRightDrawer).toHaveBeenCalledWith( RightDrawerPages.ViewEmailThread, + { + title: 'Email Thread', + Icon: IconMail, + }, ); }); diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer.ts b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer.ts index e718d97d2..1e18a20d4 100644 --- a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer.ts +++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer.ts @@ -2,6 +2,7 @@ import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { IconMail } from 'twenty-ui'; export const useOpenEmailThreadRightDrawer = () => { const { openRightDrawer } = useRightDrawer(); @@ -9,6 +10,9 @@ export const useOpenEmailThreadRightDrawer = () => { return () => { setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); - openRightDrawer(RightDrawerPages.ViewEmailThread); + openRightDrawer(RightDrawerPages.ViewEmailThread, { + title: 'Email Thread', + Icon: IconMail, + }); }; }; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChip.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChip.tsx new file mode 100644 index 000000000..119ed77ec --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChip.tsx @@ -0,0 +1,63 @@ +import styled from '@emotion/styled'; + +const StyledChip = styled.div` + align-items: center; + background: ${({ theme }) => theme.background.transparent.light}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-radius: ${({ theme }) => theme.border.radius.md}; + box-sizing: border-box; + display: flex; + gap: ${({ theme }) => theme.spacing(1)}; + height: ${({ theme }) => theme.spacing(8)}; + padding: 0 ${({ theme }) => theme.spacing(2)}; + font-size: ${({ theme }) => theme.font.size.sm}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + line-height: ${({ theme }) => theme.text.lineHeight.lg}; + color: ${({ theme }) => theme.font.color.primary}; +`; + +const StyledIconsContainer = styled.div` + display: flex; +`; + +const StyledIconWrapper = styled.div<{ withIconBackground?: boolean }>` + background: ${({ theme, withIconBackground }) => + withIconBackground ? theme.background.primary : 'unset'}; + border-radius: ${({ theme }) => theme.border.radius.sm}; + padding: ${({ theme }) => theme.spacing(0.5)}; + border: 1px solid + ${({ theme, withIconBackground }) => + withIconBackground ? theme.border.color.medium : 'transparent'}; + &:not(:first-of-type) { + margin-left: -${({ theme }) => theme.spacing(1)}; + } + display: flex; + align-items: center; + justify-content: center; +`; + +export const CommandMenuContextChip = ({ + Icons, + text, + withIconBackground, +}: { + Icons: React.ReactNode[]; + text?: string; + withIconBackground?: boolean; +}) => { + return ( + + + {Icons.map((Icon, index) => ( + + {Icon} + + ))} + + {text} + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChip.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChip.tsx index 0335346d7..75f22319a 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChip.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChip.tsx @@ -1,30 +1,10 @@ +import { CommandMenuContextChip } from '@/command-menu/components/CommandMenuContextChip'; import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars'; import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier'; -import styled from '@emotion/styled'; import { capitalize } from 'twenty-shared'; -const StyledChip = styled.div` - align-items: center; - background: ${({ theme }) => theme.background.transparent.light}; - border: 1px solid ${({ theme }) => theme.border.color.medium}; - border-radius: ${({ theme }) => theme.border.radius.md}; - box-sizing: border-box; - display: flex; - gap: ${({ theme }) => theme.spacing(1)}; - height: ${({ theme }) => theme.spacing(8)}; - padding: 0 ${({ theme }) => theme.spacing(2)}; - font-size: ${({ theme }) => theme.font.size.sm}; - font-weight: ${({ theme }) => theme.font.weight.medium}; - line-height: ${({ theme }) => theme.text.lineHeight.lg}; - color: ${({ theme }) => theme.font.color.primary}; -`; - -const StyledAvatarContainer = styled.div` - display: flex; -`; - export const CommandMenuContextRecordChip = ({ objectMetadataItemId, }: { @@ -43,21 +23,25 @@ export const CommandMenuContextRecordChip = ({ return null; } + const Avatars = records.map((record) => ( + + )); + + const text = + totalCount === 1 + ? getObjectRecordIdentifier({ objectMetadataItem, record: records[0] }) + .name + : `${totalCount} ${capitalize(objectMetadataItem.namePlural)}`; + return ( - - - {records.map((record) => ( - - ))} - - {totalCount === 1 - ? getObjectRecordIdentifier({ objectMetadataItem, record: records[0] }) - .name - : `${totalCount} ${capitalize(objectMetadataItem.namePlural)}`} - + ); }; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChipAvatars.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChipAvatars.tsx index a83ab135a..51792b4cc 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChipAvatars.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChipAvatars.tsx @@ -3,22 +3,8 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useRecordChipData } from '@/object-record/hooks/useRecordChipData'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; import { Avatar } from 'twenty-ui'; -const StyledAvatarWrapper = styled.div` - background-color: ${({ theme }) => theme.background.primary}; - border-radius: ${({ theme }) => theme.border.radius.sm}; - padding: ${({ theme }) => theme.spacing(0.5)}; - border: 1px solid ${({ theme }) => theme.border.color.medium}; - &:not(:first-of-type) { - margin-left: -${({ theme }) => theme.spacing(1)}; - } - display: flex; - align-items: center; - justify-content: center; -`; - export const CommandMenuContextRecordChipAvatars = ({ objectMetadataItem, record, @@ -38,7 +24,7 @@ export const CommandMenuContextRecordChipAvatars = ({ const theme = useTheme(); return ( - + <> {Icon ? ( ) : ( @@ -50,6 +36,6 @@ export const CommandMenuContextRecordChipAvatars = ({ size="sm" /> )} - + ); }; 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 f7052151f..2d61d13a3 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx @@ -1,12 +1,15 @@ +import { CommandMenuContextChip } from '@/command-menu/components/CommandMenuContextChip'; import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip'; import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages'; import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight'; import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; +import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useRecoilState, useRecoilValue } from 'recoil'; import { IconX, LightIconButton, isDefined, useIsMobile } from 'twenty-ui'; @@ -82,6 +85,10 @@ export const CommandMenuTopBar = () => { const commandMenuPage = useRecoilValue(commandMenuPageState); + const { title, Icon } = useRecoilValue(commandMenuPageInfoState); + + const theme = useTheme(); + return ( @@ -90,6 +97,13 @@ export const CommandMenuTopBar = () => { objectMetadataItemId={contextStoreCurrentObjectMetadataId} /> )} + {isDefined(Icon) && ( + ]} + text={title} + /> + )} + {commandMenuPage === CommandMenuPages.Root && ( = { + title: 'Modules/CommandMenu/CommandMenuContextChip', + component: CommandMenuContextChip, + decorators: [ComponentDecorator], +}; + +export default meta; +type Story = StoryObj; + +export const SingleIcon: Story = { + args: { + Icons: [], + text: 'Person', + }, +}; + +export const MultipleIcons: Story = { + args: { + Icons: [, ], + text: 'Person & Company', + }, +}; + +export const WithIconBackground: Story = { + args: { + Icons: [], + text: 'Person', + withIconBackground: true, + }, +}; + +export const MultipleIconsWithIconBackground: Story = { + args: { + Icons: [, ], + text: 'Person & Company', + withIconBackground: true, + }, +}; + +export const IconsOnly: Story = { + args: { + Icons: [, ], + }, +}; 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 new file mode 100644 index 000000000..2a02b9190 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenuContextRecordChip.stories.tsx @@ -0,0 +1,261 @@ +import { gql } from '@apollo/client'; +import { Decorator, Meta, StoryObj } from '@storybook/react'; + +import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip'; +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'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { ComponentDecorator } from 'twenty-ui'; +import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper'; +import { getCompaniesMock } from '~/testing/mock-data/companies'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; + +const FIND_MANY_COMPANIES = gql` + query FindManyCompanies( + $filter: CompanyFilterInput + $orderBy: [CompanyOrderByInput] + $lastCursor: String + $limit: Int + ) { + companies( + filter: $filter + orderBy: $orderBy + first: $limit + after: $lastCursor + ) { + edges { + node { + __typename + accountOwnerId + address { + addressStreet1 + addressStreet2 + addressCity + addressState + addressCountry + addressPostcode + addressLat + addressLng + } + annualRecurringRevenue { + amountMicros + currencyCode + } + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + domainName { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + employees + id + idealCustomerProfile + introVideo { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + linkedinLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + name + position + tagline + updatedAt + visaSponsorship + workPolicy + xLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + totalCount + } + } +`; + +const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'company', +); + +const companiesMock = getCompaniesMock(); + +const companyMock = companiesMock[0]; + +const chipGeneratorPerObjectPerField: Record< + string, + Record RecordChipData> +> = { + company: { + name: (record: ObjectRecord): RecordChipData => ({ + recordId: record.id, + name: record.name as string, + avatarUrl: '', + avatarType: 'rounded', + isLabelIdentifier: true, + objectNameSingular: 'company', + }), + }, +}; + +const identifierChipGeneratorPerObject: Record< + string, + (record: ObjectRecord) => RecordChipData +> = { + company: chipGeneratorPerObjectPerField.company.name, +}; + +const ChipGeneratorsDecorator: Decorator = (Story) => ( + + + +); + +const createContextStoreWrapper = ({ + companies, + componentInstanceId, +}: { + companies: typeof companiesMock; + componentInstanceId: string; +}) => { + return getJestMetadataAndApolloMocksAndActionMenuWrapper({ + apolloMocks: [ + { + request: { + query: FIND_MANY_COMPANIES, + variables: { + filter: { + id: { in: companies.map((company) => company.id) }, + deletedAt: { is: 'NOT_NULL' }, + }, + orderBy: [{ position: 'AscNullsFirst' }], + limit: 3, + }, + }, + result: { + data: { + companies: { + edges: companies.slice(0, 3).map((company, index) => ({ + node: company, + cursor: `cursor-${index + 1}`, + })), + pageInfo: { + hasNextPage: companies.length > 3, + hasPreviousPage: false, + startCursor: 'cursor-1', + endCursor: + companies.length > 0 + ? `cursor-${Math.min(companies.length, 3)}` + : null, + }, + totalCount: companies.length, + }, + }, + }, + }, + ], + componentInstanceId, + contextStoreCurrentObjectMetadataNameSingular: + companyMockObjectMetadataItem?.nameSingular, + contextStoreTargetedRecordsRule: { + mode: 'selection', + selectedRecordIds: companies.map((company) => company.id), + }, + contextStoreNumberOfSelectedRecords: companies.length, + onInitializeRecoilSnapshot: (snapshot) => { + for (const company of companies) { + snapshot.set(recordStoreFamilyState(company.id), company); + } + }, + }); +}; + +const ContextStoreDecorator: Decorator = (Story) => { + const ContextStoreWrapper = createContextStoreWrapper({ + companies: [companyMock], + componentInstanceId: '1', + }); + + return ( + + + + ); +}; + +const meta: Meta = { + title: 'Modules/CommandMenu/CommandMenuContextRecordChip', + component: CommandMenuContextRecordChip, + decorators: [ + ContextStoreDecorator, + ChipGeneratorsDecorator, + ComponentDecorator, + ], + args: { + objectMetadataItemId: companyMockObjectMetadataItem?.id, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithTwoCompanies: Story = { + decorators: [ + (Story) => { + const twoCompaniesMock = companiesMock.slice(0, 2); + const TwoCompaniesWrapper = createContextStoreWrapper({ + companies: twoCompaniesMock, + componentInstanceId: '2', + }); + + return ( + + + + ); + }, + ], +}; + +export const WithTenCompanies: Story = { + decorators: [ + (Story) => { + const tenCompaniesMock = companiesMock.slice(0, 10); + const TenCompaniesWrapper = createContextStoreWrapper({ + companies: tenCompaniesMock, + componentInstanceId: '3', + }); + + return ( + + + + ); + }, + ], +}; 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 9afb5d1f7..a37a2b66a 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts @@ -9,6 +9,7 @@ import { isDefined } from '~/utils/isDefined'; import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; +import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; @@ -213,6 +214,10 @@ export const useCommandMenu = () => { set(viewableRecordIdState, null); set(commandMenuPageState, CommandMenuPages.Root); + set(commandMenuPageInfoState, { + title: undefined, + Icon: undefined, + }); set(isCommandMenuOpenedState, false); resetSelectedItem(); goBackToPreviousHotkeyScope(); @@ -278,6 +283,11 @@ export const useCommandMenu = () => { }), null, ); + + set(commandMenuPageInfoState, { + title: undefined, + Icon: undefined, + }); }; }, []); diff --git a/packages/twenty-front/src/modules/command-menu/states/commandMenuPageTitle.ts b/packages/twenty-front/src/modules/command-menu/states/commandMenuPageTitle.ts new file mode 100644 index 000000000..986421873 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/states/commandMenuPageTitle.ts @@ -0,0 +1,10 @@ +import { createState } from '@ui/utilities/state/utils/createState'; +import { IconComponent } from 'twenty-ui'; + +export const commandMenuPageInfoState = createState<{ + title: string | undefined; + Icon: IconComponent | undefined; +}>({ + key: 'command-menu/commandMenuPageInfoState', + defaultValue: { title: undefined, Icon: undefined }, +}); diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts index 52a604e71..ebcc3efe2 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts @@ -5,9 +5,11 @@ import { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/righ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; +import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle'; import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent'; import { mapRightDrawerPageToCommandMenuPage } from '@/ui/layout/right-drawer/utils/mapRightDrawerPageToCommandMenuPage'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { IconComponent } from 'twenty-ui'; import { FeatureFlagKey } from '~/generated/graphql'; import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState'; import { rightDrawerPageState } from '../states/rightDrawerPageState'; @@ -27,12 +29,22 @@ export const useRightDrawer = () => { const openRightDrawer = useRecoilCallback( ({ set }) => - (rightDrawerPage: RightDrawerPages) => { + ( + rightDrawerPage: RightDrawerPages, + commandMenuPageInfo?: { + title?: string; + Icon?: IconComponent; + }, + ) => { if (isCommandMenuV2Enabled) { const commandMenuPage = mapRightDrawerPageToCommandMenuPage(rightDrawerPage); set(commandMenuPageState, commandMenuPage); + set(commandMenuPageInfoState, { + title: commandMenuPageInfo?.title, + Icon: commandMenuPageInfo?.Icon, + }); openCommandMenu(); return; } 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 f1844f78d..ebb581ad5 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 @@ -8,11 +8,15 @@ import { EMPTY_TRIGGER_STEP_ID } from '@/workflow/workflow-diagram/constants/Emp import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation'; import { useTriggerNodeSelection } from '@/workflow/workflow-diagram/hooks/useTriggerNodeSelection'; import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState'; -import { WorkflowDiagramNode } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { + WorkflowDiagramNode, + WorkflowDiagramStepNodeData, +} from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { getWorkflowNodeIcon } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIcon'; import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react'; import { useCallback } from 'react'; import { useSetRecoilState } from 'recoil'; -import { isDefined } from 'twenty-ui'; +import { IconBolt, isDefined } from 'twenty-ui'; export const WorkflowDiagramCanvasEditableEffect = () => { const { startNodeCreation } = useStartNodeCreation(); @@ -37,7 +41,10 @@ export const WorkflowDiagramCanvasEditableEffect = () => { const isEmptyTriggerNode = selectedNode.type === EMPTY_TRIGGER_STEP_ID; if (isEmptyTriggerNode) { - openRightDrawer(RightDrawerPages.WorkflowStepSelectTriggerType); + openRightDrawer(RightDrawerPages.WorkflowStepSelectTriggerType, { + title: 'Trigger Type', + Icon: IconBolt, + }); return; } @@ -53,9 +60,14 @@ export const WorkflowDiagramCanvasEditableEffect = () => { return; } + const selectedNodeData = selectedNode.data as WorkflowDiagramStepNodeData; + setWorkflowSelectedNode(selectedNode.id); setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); - openRightDrawer(RightDrawerPages.WorkflowStepEdit); + openRightDrawer(RightDrawerPages.WorkflowStepEdit, { + title: selectedNodeData.name, + Icon: getWorkflowNodeIcon(selectedNodeData), + }); }, [ setWorkflowSelectedNode, 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 71cdd8e33..0f2a11c30 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 @@ -5,7 +5,11 @@ import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPage import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useTriggerNodeSelection } from '@/workflow/workflow-diagram/hooks/useTriggerNodeSelection'; import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState'; -import { WorkflowDiagramNode } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { + WorkflowDiagramNode, + WorkflowDiagramStepNodeData, +} from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { getWorkflowNodeIcon } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIcon'; import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react'; import { useCallback } from 'react'; import { useSetRecoilState } from 'recoil'; @@ -30,7 +34,12 @@ export const WorkflowDiagramCanvasReadonlyEffect = () => { setWorkflowSelectedNode(selectedNode.id); setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); - openRightDrawer(RightDrawerPages.WorkflowStepView); + + const selectedNodeData = selectedNode.data as WorkflowDiagramStepNodeData; + openRightDrawer(RightDrawerPages.WorkflowStepView, { + title: selectedNodeData.name, + Icon: getWorkflowNodeIcon(selectedNodeData), + }); }, [ setWorkflowSelectedNode, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx index 50f9e65eb..b754defb7 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase.tsx @@ -1,15 +1,9 @@ import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; import { WorkflowDiagramBaseStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseStepNode'; import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram'; +import { getWorkflowNodeIcon } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIcon'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { - IconAddressBook, - IconCode, - IconHandMove, - IconMail, - IconPlaylistAdd, -} from 'twenty-ui'; const StyledStepNodeLabelIconContainer = styled.div` align-items: center; @@ -29,6 +23,8 @@ export const WorkflowDiagramStepNodeBase = ({ }) => { const theme = useTheme(); + const Icon = getWorkflowNodeIcon(data); + const renderStepIcon = () => { switch (data.nodeType) { case 'trigger': { @@ -36,7 +32,7 @@ export const WorkflowDiagramStepNodeBase = ({ case 'DATABASE_EVENT': { return ( - @@ -46,7 +42,7 @@ export const WorkflowDiagramStepNodeBase = ({ case 'MANUAL': { return ( - @@ -62,17 +58,14 @@ export const WorkflowDiagramStepNodeBase = ({ case 'CODE': { return ( - + ); } case 'SEND_EMAIL': { return ( - + ); } @@ -81,7 +74,7 @@ export const WorkflowDiagramStepNodeBase = ({ case 'DELETE_RECORD': { return ( - { const { openRightDrawer } = useRightDrawer(); @@ -22,7 +23,10 @@ export const useStartNodeCreation = () => { setWorkflowCreateStepFromParentStepId(parentNodeId); setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); - openRightDrawer(RightDrawerPages.WorkflowStepSelectAction); + openRightDrawer(RightDrawerPages.WorkflowStepSelectAction, { + title: 'Select Action', + Icon: IconSettingsAutomation, + }); }, [openRightDrawer, setWorkflowCreateStepFromParentStepId, setHotkeyScope], ); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowNodeIcon.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowNodeIcon.ts new file mode 100644 index 000000000..feefe37ae --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowNodeIcon.ts @@ -0,0 +1,56 @@ +import { + WorkflowActionType, + WorkflowTriggerType, +} from '@/workflow/types/Workflow'; +import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; +import { + IconAddressBook, + IconCode, + IconHandMove, + IconMail, + IconPlaylistAdd, +} from 'twenty-ui'; + +export const getWorkflowNodeIcon = ( + data: + | { + nodeType: 'trigger'; + triggerType: WorkflowTriggerType; + } + | { + nodeType: 'action'; + actionType: WorkflowActionType; + }, +) => { + switch (data.nodeType) { + case 'trigger': { + switch (data.triggerType) { + case 'DATABASE_EVENT': { + return IconPlaylistAdd; + } + case 'MANUAL': { + return IconHandMove; + } + } + + return assertUnreachable(data.triggerType); + } + case 'action': { + switch (data.actionType) { + case 'CODE': { + return IconCode; + } + case 'SEND_EMAIL': { + return IconMail; + } + case 'CREATE_RECORD': + case 'UPDATE_RECORD': + case 'DELETE_RECORD': { + return IconAddressBook; + } + } + + return assertUnreachable(data.actionType); + } + } +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useCreateStep.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useCreateStep.test.ts index 243083014..751f65799 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useCreateStep.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useCreateStep.test.ts @@ -5,7 +5,7 @@ import { useCreateStep } from '../useCreateStep'; const mockOpenRightDrawer = jest.fn(); const mockCreateDraftFromWorkflowVersion = jest.fn().mockResolvedValue('457'); const mockCreateWorkflowVersionStep = jest.fn().mockResolvedValue({ - data: { createWorkflowVersionStep: { id: '1' } }, + data: { createWorkflowVersionStep: { id: '1', type: 'CODE' } }, }); jest.mock('recoil', () => ({ diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateStep.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateStep.ts index 89a4f76a1..8d782dff8 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateStep.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateStep.ts @@ -7,6 +7,7 @@ import { WorkflowWithCurrentVersion, } from '@/workflow/types/Workflow'; import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState'; +import { getWorkflowNodeIcon } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIcon'; import { useCreateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useCreateWorkflowVersionStep'; import { workflowCreateStepFromParentStepIdState } from '@/workflow/workflow-steps/states/workflowCreateStepFromParentStepIdState'; import { useRecoilValue, useSetRecoilState } from 'recoil'; @@ -17,7 +18,6 @@ export const useCreateStep = ({ }: { workflow: WorkflowWithCurrentVersion; }) => { - const { openRightDrawer } = useRightDrawer(); const { createWorkflowVersionStep } = useCreateWorkflowVersionStep(); const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState); const setWorkflowLastCreatedStepId = useSetRecoilState( @@ -30,6 +30,8 @@ export const useCreateStep = ({ const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion(); + const { openRightDrawer } = useRightDrawer(); + const createStep = async (newStepType: WorkflowStepType) => { if (!isDefined(workflowCreateStepFromParentStepId)) { throw new Error('Select a step to create a new step from first.'); @@ -50,7 +52,14 @@ export const useCreateStep = ({ setWorkflowSelectedNode(createdStep.id); setWorkflowLastCreatedStepId(createdStep.id); - openRightDrawer(RightDrawerPages.WorkflowStepEdit); + + openRightDrawer(RightDrawerPages.WorkflowStepEdit, { + title: createdStep.name, + Icon: getWorkflowNodeIcon({ + nodeType: 'action', + actionType: createdStep.type as WorkflowStepType, + }), + }); }; return { 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 e5819a7bc..66cf20907 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 @@ -63,7 +63,10 @@ export const RightDrawerWorkflowSelectTriggerTypeContent = ({ setWorkflowSelectedNode(TRIGGER_STEP_ID); - openRightDrawer(RightDrawerPages.WorkflowStepEdit); + openRightDrawer(RightDrawerPages.WorkflowStepEdit, { + title: action.name, + Icon: action.icon, + }); }} /> ))} @@ -84,7 +87,10 @@ export const RightDrawerWorkflowSelectTriggerTypeContent = ({ setWorkflowSelectedNode(TRIGGER_STEP_ID); - openRightDrawer(RightDrawerPages.WorkflowStepEdit); + openRightDrawer(RightDrawerPages.WorkflowStepEdit, { + title: action.name, + Icon: action.icon, + }); }} /> ))}