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:
Raphaël Bosi
2026-04-03 15:41:56 +02:00
committed by GitHub
parent 119014f86d
commit 8543576dae
7 changed files with 82 additions and 45 deletions
@@ -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,
}),
@@ -50,7 +50,7 @@ const resolveTemplateExpression = ({
: stringValue;
};
export const interpolateCommandMenuItemLabel = ({
export const interpolateCommandMenuItemTemplate = ({
label,
context,
}: {
+1 -1
View File
@@ -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';