diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/common/hooks/__tests__/useCommandMenuItemsFromBackend.test.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/common/hooks/__tests__/useCommandMenuItemsFromBackend.test.tsx index e7694420fe..fef21cfc1f 100644 --- a/packages/twenty-front/src/modules/command-menu-item/server-items/common/hooks/__tests__/useCommandMenuItemsFromBackend.test.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/server-items/common/hooks/__tests__/useCommandMenuItemsFromBackend.test.tsx @@ -24,7 +24,7 @@ jest.mock('twenty-shared/utils', () => { return { ...actual, - interpolateCommandMenuItemLabel: jest.fn( + interpolateCommandMenuItemTemplate: jest.fn( ({ label }: { label?: string | null }) => label ?? null, ), evaluateConditionalAvailabilityExpression: jest.fn( diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/common/hooks/useCommandMenuItemsFromBackend.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/common/hooks/useCommandMenuItemsFromBackend.tsx index 34e3e28311..3b4e850b90 100644 --- a/packages/twenty-front/src/modules/command-menu-item/server-items/common/hooks/useCommandMenuItemsFromBackend.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/server-items/common/hooks/useCommandMenuItemsFromBackend.tsx @@ -2,14 +2,14 @@ import { FrontComponentCommandMenuItem } from '@/command-menu-item/display/compo import { HeadlessCommandMenuItem } from '@/command-menu-item/display/components/HeadlessCommandMenuItem'; import { commandMenuItemsSelector } from '@/command-menu-item/server-items/common/states/commandMenuItemsSelector'; import { doesCommandMenuItemMatchObjectMetadataId } from '@/command-menu-item/server-items/common/utils/doesCommandMenuItemMatchObjectMetadataId'; -import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope'; import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig'; +import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope'; import { CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType'; import { type CommandMenuContextApi } from 'twenty-shared/types'; import { evaluateConditionalAvailabilityExpression, - interpolateCommandMenuItemLabel, + interpolateCommandMenuItemTemplate, isDefined, } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; @@ -47,17 +47,22 @@ const buildCommandMenuItemFromFrontComponent = ({ getIcon, commandMenuContextApi, }: BuildCommandMenuItemFromFrontComponentParams): CommandMenuItemConfig => { - const displayLabel = interpolateCommandMenuItemLabel({ + const displayLabel = interpolateCommandMenuItemTemplate({ label: item.label, context: commandMenuContextApi, }); - const displayShortLabel = interpolateCommandMenuItemLabel({ + const displayShortLabel = interpolateCommandMenuItemTemplate({ label: item.shortLabel, context: commandMenuContextApi, }); - const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON); + const interpolatedIcon = interpolateCommandMenuItemTemplate({ + label: item.icon, + context: commandMenuContextApi, + }); + + const Icon = getIcon(interpolatedIcon, COMMAND_MENU_DEFAULT_ICON); const isHeadless = item.frontComponent?.isHeadless === true; @@ -97,18 +102,22 @@ const buildCommandItemFromEngineKey = ({ getIcon, commandMenuContextApi, }: BuildCommandMenuItemFromStandardKeyParams): CommandMenuItemConfig => { - const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON); + const interpolatedIcon = interpolateCommandMenuItemTemplate({ + label: item.icon, + context: commandMenuContextApi, + }); + const Icon = getIcon(interpolatedIcon, COMMAND_MENU_DEFAULT_ICON); return { type, key: `command-menu-item-engine-${item.id}`, id: item.id, scope, - label: interpolateCommandMenuItemLabel({ + label: interpolateCommandMenuItemTemplate({ label: item.label, context: commandMenuContextApi, }), - shortLabel: interpolateCommandMenuItemLabel({ + shortLabel: interpolateCommandMenuItemTemplate({ label: item.shortLabel, context: commandMenuContextApi, }), diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/PinnedCommandMenuItemButtonsEditMode.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/PinnedCommandMenuItemButtonsEditMode.tsx index 9942c59152..06661556da 100644 --- a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/PinnedCommandMenuItemButtonsEditMode.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/PinnedCommandMenuItemButtonsEditMode.tsx @@ -15,7 +15,7 @@ import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants import { styled } from '@linaria/react'; import { motion } from 'framer-motion'; import { useContext, useMemo } from 'react'; -import { interpolateCommandMenuItemLabel } from 'twenty-shared/utils'; +import { interpolateCommandMenuItemTemplate } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; import { ThemeContext } from 'twenty-ui/theme-constants'; import { CommandMenuItemAvailabilityType } from '~/generated-metadata/graphql'; @@ -73,7 +73,7 @@ export const PinnedCommandMenuItemButtonsEditMode = () => { ); const interpolateLabel = (rawLabel: string | null | undefined) => - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: rawLabel, context: commandMenuContextApi, }); diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/SidePanelCommandMenuItemEditPage.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/SidePanelCommandMenuItemEditPage.tsx index 7bc6bce0f3..1eeed916df 100644 --- a/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/SidePanelCommandMenuItemEditPage.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/server-items/edit/components/SidePanelCommandMenuItemEditPage.tsx @@ -22,7 +22,7 @@ import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; import { CommandMenuContextApiPageType } from 'twenty-shared/types'; import { - interpolateCommandMenuItemLabel, + interpolateCommandMenuItemTemplate, isDefined, } from 'twenty-shared/utils'; import { @@ -119,7 +119,7 @@ export const SidePanelCommandMenuItemEditPage = () => { ); const getDisplayLabel = (item: CommandMenuItemFieldsFragment) => - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: item.label, context: commandMenuContextApi, }) ?? item.label; diff --git a/packages/twenty-shared/src/utils/command-menu-items/__tests__/interpolateCommandMenuItemLabel.test.ts b/packages/twenty-shared/src/utils/command-menu-items/__tests__/interpolateCommandMenuItemTemplate.test.ts similarity index 80% rename from packages/twenty-shared/src/utils/command-menu-items/__tests__/interpolateCommandMenuItemLabel.test.ts rename to packages/twenty-shared/src/utils/command-menu-items/__tests__/interpolateCommandMenuItemTemplate.test.ts index e6bb7afae8..161e829b86 100644 --- a/packages/twenty-shared/src/utils/command-menu-items/__tests__/interpolateCommandMenuItemLabel.test.ts +++ b/packages/twenty-shared/src/utils/command-menu-items/__tests__/interpolateCommandMenuItemTemplate.test.ts @@ -2,7 +2,7 @@ import { CommandMenuContextApiPageType, type CommandMenuContextApi, } from '@/types'; -import { interpolateCommandMenuItemLabel } from '../interpolateCommandMenuItemLabel'; +import { interpolateCommandMenuItemTemplate } from '../interpolateCommandMenuItemTemplate'; const buildContext = ( overrides: Partial = {}, @@ -33,7 +33,7 @@ const buildContext = ( ...overrides, }); -describe('interpolateCommandMenuItemLabel', () => { +describe('interpolateCommandMenuItemTemplate', () => { describe('sequential invocations (global regex lastIndex)', () => { it('should interpolate correctly when called multiple times in sequence', () => { const context = buildContext({ @@ -42,14 +42,14 @@ describe('interpolateCommandMenuItemLabel', () => { }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'First: ${numberOfSelectedRecords}', context, }), ).toBe('First: 2'); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'Second: ${objectMetadataItem.labelPlural}', context, }), @@ -62,7 +62,7 @@ describe('interpolateCommandMenuItemLabel', () => { const context = buildContext(); expect( - interpolateCommandMenuItemLabel({ label: 'Delete', context }), + interpolateCommandMenuItemTemplate({ label: 'Delete', context }), ).toBe('Delete'); }); @@ -70,7 +70,7 @@ describe('interpolateCommandMenuItemLabel', () => { const context = buildContext(); expect( - interpolateCommandMenuItemLabel({ label: null, context }), + interpolateCommandMenuItemTemplate({ label: null, context }), ).toBeNull(); }); @@ -78,14 +78,16 @@ describe('interpolateCommandMenuItemLabel', () => { const context = buildContext(); expect( - interpolateCommandMenuItemLabel({ label: undefined, context }), + interpolateCommandMenuItemTemplate({ label: undefined, context }), ).toBeNull(); }); it('should return an empty string for an empty label', () => { const context = buildContext(); - expect(interpolateCommandMenuItemLabel({ label: '', context })).toBe(''); + expect(interpolateCommandMenuItemTemplate({ label: '', context })).toBe( + '', + ); }); }); @@ -94,7 +96,7 @@ describe('interpolateCommandMenuItemLabel', () => { const context = buildContext({ numberOfSelectedRecords: 5 }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'Selected: ${numberOfSelectedRecords}', context, }), @@ -107,7 +109,7 @@ describe('interpolateCommandMenuItemLabel', () => { }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'Create new ${objectMetadataItem.labelSingular}', context, }), @@ -120,7 +122,7 @@ describe('interpolateCommandMenuItemLabel', () => { }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'Delete ${objectMetadataItem.labelPlural}', context, }), @@ -136,7 +138,7 @@ describe('interpolateCommandMenuItemLabel', () => { }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: '${numberOfSelectedRecords} ${objectMetadataItem.labelPlural} selected', context, @@ -155,7 +157,7 @@ describe('interpolateCommandMenuItemLabel', () => { }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'New ${objectMetadataItem.nameSingular}', context, }), @@ -170,7 +172,7 @@ describe('interpolateCommandMenuItemLabel', () => { }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'New ${objectMetadataItem.labelSingular}', context, }), @@ -181,7 +183,7 @@ describe('interpolateCommandMenuItemLabel', () => { const context = buildContext(); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'Create new ${objectMetadataItem.labelSingular}', context, }), @@ -194,7 +196,7 @@ describe('interpolateCommandMenuItemLabel', () => { const context = buildContext(); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'Label ${nonExistent.deep.path}', context, }), @@ -205,14 +207,14 @@ describe('interpolateCommandMenuItemLabel', () => { const context = buildContext(); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'Val: ${toString}', context, }), ).toBe('Val: '); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'Val: ${objectMetadataItem.hasOwnProperty}', context, }), @@ -225,7 +227,7 @@ describe('interpolateCommandMenuItemLabel', () => { const context = buildContext({ isInSidePanel: true }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'Side panel: ${isInSidePanel}', context, }), @@ -238,7 +240,7 @@ describe('interpolateCommandMenuItemLabel', () => { }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'Create new ${objectMetadataItem.labelSingular}', context, }), @@ -253,7 +255,7 @@ describe('interpolateCommandMenuItemLabel', () => { }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: '${capitalize(objectMetadataItem.labelSingular)} details', context, }), @@ -266,7 +268,7 @@ describe('interpolateCommandMenuItemLabel', () => { }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: '${capitalize(objectMetadataItem.labelPlural)} selected', context, }), @@ -279,7 +281,7 @@ describe('interpolateCommandMenuItemLabel', () => { }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: '${capitalize(objectMetadataItem.labelSingular)} details', context, }), @@ -294,7 +296,7 @@ describe('interpolateCommandMenuItemLabel', () => { }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'Create ${lowercase(objectMetadataItem.labelSingular)}', context, }), @@ -307,7 +309,7 @@ describe('interpolateCommandMenuItemLabel', () => { }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'Delete ${lowercase(objectMetadataItem.labelPlural)}', context, }), @@ -315,6 +317,32 @@ describe('interpolateCommandMenuItemLabel', () => { }); }); + describe('icon interpolation', () => { + it('should interpolate objectMetadataItem.icon', () => { + const context = buildContext({ + objectMetadataItem: { icon: 'IconBuilding' }, + }); + + expect( + interpolateCommandMenuItemTemplate({ + label: '${objectMetadataItem.icon}', + context, + }), + ).toBe('IconBuilding'); + }); + + it('should return static icon unchanged when no template variable present', () => { + const context = buildContext(); + + expect( + interpolateCommandMenuItemTemplate({ + label: 'IconTrash', + context, + }), + ).toBe('IconTrash'); + }); + }); + describe('objectMetadataLabel interpolation', () => { it('should resolve singular label with capitalize transform', () => { const context = buildContext({ @@ -322,7 +350,7 @@ describe('interpolateCommandMenuItemLabel', () => { }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'Delete ${capitalize(objectMetadataLabel)}', context, }), @@ -335,7 +363,7 @@ describe('interpolateCommandMenuItemLabel', () => { }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'Export ${capitalize(objectMetadataLabel)}', context, }), @@ -348,7 +376,7 @@ describe('interpolateCommandMenuItemLabel', () => { }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: '${objectMetadataLabel} selected', context, }), @@ -361,7 +389,7 @@ describe('interpolateCommandMenuItemLabel', () => { }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'Create ${lowercase(objectMetadataLabel)}', context, }), @@ -374,7 +402,7 @@ describe('interpolateCommandMenuItemLabel', () => { }); expect( - interpolateCommandMenuItemLabel({ + interpolateCommandMenuItemTemplate({ label: 'Delete ${capitalize(objectMetadataLabel)}', context, }), diff --git a/packages/twenty-shared/src/utils/command-menu-items/interpolateCommandMenuItemLabel.ts b/packages/twenty-shared/src/utils/command-menu-items/interpolateCommandMenuItemTemplate.ts similarity index 97% rename from packages/twenty-shared/src/utils/command-menu-items/interpolateCommandMenuItemLabel.ts rename to packages/twenty-shared/src/utils/command-menu-items/interpolateCommandMenuItemTemplate.ts index acdbbbcef1..21bf3cac73 100644 --- a/packages/twenty-shared/src/utils/command-menu-items/interpolateCommandMenuItemLabel.ts +++ b/packages/twenty-shared/src/utils/command-menu-items/interpolateCommandMenuItemTemplate.ts @@ -50,7 +50,7 @@ const resolveTemplateExpression = ({ : stringValue; }; -export const interpolateCommandMenuItemLabel = ({ +export const interpolateCommandMenuItemTemplate = ({ label, context, }: { diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index 53174539ab..59e7e4b2fb 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -25,7 +25,7 @@ export { assertUnreachable } from './assertUnreachable'; export { base64UrlEncode } from './base64UrlEncode'; export { conditionalAvailabilityParser } from './command-menu-items/conditionalAvailabilityParser'; export { evaluateConditionalAvailabilityExpression } from './command-menu-items/evaluateConditionalAvailabilityExpression'; -export { interpolateCommandMenuItemLabel } from './command-menu-items/interpolateCommandMenuItemLabel'; +export { interpolateCommandMenuItemTemplate } from './command-menu-items/interpolateCommandMenuItemTemplate'; export { resolveObjectMetadataLabel } from './command-menu-items/resolveObjectMetadataLabel'; export { safeGetNestedProperty } from './command-menu-items/safeGetNestedProperty'; export { computeDiffBetweenObjects } from './compute-diff-between-objects';