Dynamic icon for command menu items (#19307)
Allow for dynamic icons in command menu items. For instance
`${objectMetadataItem.icon}` will resolve the object metadata item icon
This commit is contained in:
+1
-1
@@ -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(
|
||||
|
||||
+17
-8
@@ -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,
|
||||
}),
|
||||
|
||||
+2
-2
@@ -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,
|
||||
});
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+58
-30
@@ -2,7 +2,7 @@ import {
|
||||
CommandMenuContextApiPageType,
|
||||
type CommandMenuContextApi,
|
||||
} from '@/types';
|
||||
import { interpolateCommandMenuItemLabel } from '../interpolateCommandMenuItemLabel';
|
||||
import { interpolateCommandMenuItemTemplate } from '../interpolateCommandMenuItemTemplate';
|
||||
|
||||
const buildContext = (
|
||||
overrides: Partial<CommandMenuContextApi> = {},
|
||||
@@ -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,
|
||||
}),
|
||||
+1
-1
@@ -50,7 +50,7 @@ const resolveTemplateExpression = ({
|
||||
: stringValue;
|
||||
};
|
||||
|
||||
export const interpolateCommandMenuItemLabel = ({
|
||||
export const interpolateCommandMenuItemTemplate = ({
|
||||
label,
|
||||
context,
|
||||
}: {
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user