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,
+ });
}}
/>
))}