Support template variables in command menu item labels (#18707)
- Adds template variable interpolation (`${...}`) to command menu item
labels and short labels, enabling dynamic text like `Create new
${capitalize(objectMetadataItem.labelSingular)}` instead of static
`Create new record`.
- Supports `capitalize` and `lowercase` transform functions within
template expressions.
This commit is contained in:
+1
-1
@@ -38,7 +38,7 @@ export const CommandListItem = ({
|
||||
id={action.key}
|
||||
Icon={action.Icon}
|
||||
label={getCommandMenuItemLabel(action.label)}
|
||||
description={getCommandMenuItemLabel(action.description ?? '')}
|
||||
description={getCommandMenuItemLabel(action.description)}
|
||||
to={to}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
hotKeys={action.hotKeys}
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import { type MenuItemAccent } from 'twenty-ui/navigation';
|
||||
|
||||
export type CommandMenuItemDisplayProps = {
|
||||
key: string;
|
||||
label: MessageDescriptor | string;
|
||||
label: Nullable<MessageDescriptor | string>;
|
||||
shortLabel?: Nullable<MessageDescriptor | string>;
|
||||
description?: MessageDescriptor | string;
|
||||
Icon: IconComponent;
|
||||
|
||||
+2
-4
@@ -42,9 +42,7 @@ export const Default: Story = {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
await canvas.findByText(
|
||||
getCommandMenuItemLabel(
|
||||
addToFavoritesCommandMenuItem?.shortLabel ?? '',
|
||||
),
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.shortLabel),
|
||||
),
|
||||
);
|
||||
expect(addToFavoritesMock).toHaveBeenCalled();
|
||||
@@ -59,7 +57,7 @@ export const WithLink: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const menuItem = await canvas.findByText(
|
||||
getCommandMenuItemLabel(goToPeopleCommandMenuItem?.shortLabel ?? ''),
|
||||
getCommandMenuItemLabel(goToPeopleCommandMenuItem?.shortLabel),
|
||||
);
|
||||
expect(menuItem).toBeVisible();
|
||||
expect(canvas.getByRole('link')).toHaveAttribute('href', '/objects/people');
|
||||
|
||||
+1
-3
@@ -60,9 +60,7 @@ export const Default: Story = {
|
||||
|
||||
expect(
|
||||
await canvas.findByText(
|
||||
getCommandMenuItemLabel(
|
||||
addToFavoritesCommandMenuItem?.shortLabel ?? '',
|
||||
),
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.shortLabel),
|
||||
),
|
||||
).toBeVisible();
|
||||
},
|
||||
|
||||
+3
-5
@@ -65,9 +65,7 @@ export const AsButton: Story = {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
await canvas.findByText(
|
||||
getCommandMenuItemLabel(
|
||||
addToFavoritesCommandMenuItem?.shortLabel ?? '',
|
||||
),
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.shortLabel),
|
||||
),
|
||||
);
|
||||
expect(addToFavoritesMock).toHaveBeenCalled();
|
||||
@@ -103,7 +101,7 @@ export const AsListItem: Story = {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
await canvas.findByText(
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.label ?? ''),
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.label),
|
||||
),
|
||||
);
|
||||
expect(addToFavoritesMock).toHaveBeenCalled();
|
||||
@@ -139,7 +137,7 @@ export const AsDropdownItem: Story = {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
await canvas.findByText(
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.label ?? ''),
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.label),
|
||||
),
|
||||
);
|
||||
expect(addToFavoritesMock).toHaveBeenCalled();
|
||||
|
||||
+2
-2
@@ -53,7 +53,7 @@ export const Default: Story = {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
await canvas.findByText(
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.label ?? ''),
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.label),
|
||||
),
|
||||
);
|
||||
expect(addToFavoritesMock).toHaveBeenCalled();
|
||||
@@ -68,7 +68,7 @@ export const WithLink: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const dropdownItem = await canvas.findByText(
|
||||
getCommandMenuItemLabel(goToPeopleCommandMenuItem?.label ?? ''),
|
||||
getCommandMenuItemLabel(goToPeopleCommandMenuItem?.label),
|
||||
);
|
||||
expect(dropdownItem).toBeVisible();
|
||||
},
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { CommandListItem } from '@/command-menu-item/display/components/CommandListItem';
|
||||
import { createMockCommandMenuItems } from '@/command-menu-item/mock/command-menu-items.mock';
|
||||
import { NoSelectionRecordCommandKeys } from '@/command-menu-item/record/no-selection/types/NoSelectionRecordCommandKeys';
|
||||
import { SingleRecordCommandKeys } from '@/command-menu-item/record/single-record/types/SingleRecordCommandKeys';
|
||||
import { createMockCommandMenuItems } from '@/command-menu-item/mock/command-menu-items.mock';
|
||||
import { getCommandMenuItemLabel } from '@/command-menu-item/utils/getCommandMenuItemLabel';
|
||||
import { SelectableListComponentInstanceContext } from '@/ui/layout/selectable-list/states/contexts/SelectableListComponentInstanceContext';
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
@@ -53,7 +53,7 @@ export const Default: Story = {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
await canvas.findByText(
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.label ?? ''),
|
||||
getCommandMenuItemLabel(addToFavoritesCommandMenuItem?.label),
|
||||
),
|
||||
);
|
||||
expect(addToFavoritesMock).toHaveBeenCalled();
|
||||
@@ -68,7 +68,7 @@ export const WithLink: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const listItem = await canvas.findByText(
|
||||
getCommandMenuItemLabel(goToPeopleCommandMenuItem?.label ?? ''),
|
||||
getCommandMenuItemLabel(goToPeopleCommandMenuItem?.label),
|
||||
);
|
||||
expect(listItem).toBeVisible();
|
||||
},
|
||||
|
||||
+2
-4
@@ -1,5 +1,5 @@
|
||||
import { Command } from '@/command-menu-item/display/components/Command';
|
||||
import { CommandConfigContext } from '@/command-menu-item/contexts/CommandConfigContext';
|
||||
import { Command } from '@/command-menu-item/display/components/Command';
|
||||
import { computeProgressText } from '@/command-menu-item/utils/computeProgressText';
|
||||
import { getCommandMenuItemLabel } from '@/command-menu-item/utils/getCommandMenuItemLabel';
|
||||
import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState';
|
||||
@@ -78,9 +78,7 @@ export const DeleteMultipleRecordsCommand = () => {
|
||||
|
||||
const originalLabel = getCommandMenuItemLabel(actionConfig.label);
|
||||
|
||||
const originalShortLabel = getCommandMenuItemLabel(
|
||||
actionConfig.shortLabel ?? '',
|
||||
);
|
||||
const originalShortLabel = getCommandMenuItemLabel(actionConfig.shortLabel);
|
||||
|
||||
const progressText = computeProgressText(progress);
|
||||
|
||||
|
||||
+1
-3
@@ -85,9 +85,7 @@ export const DestroyMultipleRecordsCommand = () => {
|
||||
|
||||
const originalLabel = getCommandMenuItemLabel(actionConfig.label);
|
||||
|
||||
const originalShortLabel = getCommandMenuItemLabel(
|
||||
actionConfig.shortLabel ?? '',
|
||||
);
|
||||
const originalShortLabel = getCommandMenuItemLabel(actionConfig.shortLabel);
|
||||
|
||||
const progressText = computeProgressText(progress);
|
||||
|
||||
|
||||
+1
-3
@@ -49,9 +49,7 @@ export const ExportMultipleRecordsCommand = () => {
|
||||
|
||||
const originalLabel = getCommandMenuItemLabel(actionConfig.label);
|
||||
|
||||
const originalShortLabel = getCommandMenuItemLabel(
|
||||
actionConfig.shortLabel ?? '',
|
||||
);
|
||||
const originalShortLabel = getCommandMenuItemLabel(actionConfig.shortLabel);
|
||||
|
||||
const progressText = computeProgressText(exportProgress);
|
||||
|
||||
|
||||
+20
-5
@@ -16,6 +16,7 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
|
||||
import { type CommandMenuContextApi } from 'twenty-shared/types';
|
||||
import {
|
||||
evaluateConditionalAvailabilityExpression,
|
||||
interpolateCommandMenuItemLabel,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { type IconComponent, useIcons } from 'twenty-ui/display';
|
||||
@@ -73,7 +74,15 @@ const buildCommandMenuItemFromFrontComponent = ({
|
||||
mountContext,
|
||||
commandMenuContextApi,
|
||||
}: BuildCommandMenuItemFromFrontComponentParams) => {
|
||||
const displayLabel = item.label;
|
||||
const displayLabel = interpolateCommandMenuItemLabel({
|
||||
label: item.label,
|
||||
context: commandMenuContextApi,
|
||||
});
|
||||
|
||||
const displayShortLabel = interpolateCommandMenuItemLabel({
|
||||
label: item.shortLabel,
|
||||
context: commandMenuContextApi,
|
||||
});
|
||||
|
||||
const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON);
|
||||
|
||||
@@ -85,7 +94,7 @@ const buildCommandMenuItemFromFrontComponent = ({
|
||||
} else {
|
||||
openFrontComponentInSidePanel({
|
||||
frontComponentId: item.frontComponentId,
|
||||
pageTitle: displayLabel,
|
||||
pageTitle: displayLabel ?? '',
|
||||
pageIcon: Icon,
|
||||
recordContext: isDefined(mountContext)
|
||||
? {
|
||||
@@ -102,7 +111,7 @@ const buildCommandMenuItemFromFrontComponent = ({
|
||||
key: `command-menu-item-front-component-${item.id}`,
|
||||
scope,
|
||||
label: displayLabel,
|
||||
shortLabel: item.shortLabel,
|
||||
shortLabel: displayShortLabel,
|
||||
position: item.position,
|
||||
isPinned,
|
||||
Icon,
|
||||
@@ -160,8 +169,14 @@ const buildCommandItemFromEngineKey = ({
|
||||
type,
|
||||
key: `command-menu-item-engine-${item.id}`,
|
||||
scope,
|
||||
label: item.label,
|
||||
shortLabel: item.shortLabel,
|
||||
label: interpolateCommandMenuItemLabel({
|
||||
label: item.label,
|
||||
context: commandMenuContextApi,
|
||||
}),
|
||||
shortLabel: interpolateCommandMenuItemLabel({
|
||||
label: item.shortLabel,
|
||||
context: commandMenuContextApi,
|
||||
}),
|
||||
position: item.position,
|
||||
isPinned,
|
||||
Icon,
|
||||
|
||||
@@ -14,7 +14,7 @@ export type CommandMenuItemConfig = {
|
||||
type: CommandMenuItemType;
|
||||
scope: CommandMenuItemScope;
|
||||
key: string;
|
||||
label: MessageDescriptor | string;
|
||||
label: Nullable<MessageDescriptor | string>;
|
||||
shortLabel?: Nullable<MessageDescriptor | string>;
|
||||
description?: MessageDescriptor | string;
|
||||
position: number;
|
||||
|
||||
+7
-1
@@ -1,8 +1,14 @@
|
||||
import { i18n, type MessageDescriptor } from '@lingui/core';
|
||||
import { isString } from '@sniptt/guards';
|
||||
import { type Nullable } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const getCommandMenuItemLabel = (
|
||||
label: string | MessageDescriptor,
|
||||
label: Nullable<string | MessageDescriptor>,
|
||||
): string => {
|
||||
if (!isDefined(label)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return isString(label) ? label : i18n._(label);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user