[COMMAND MENU ITEMS] Resolve object metadata label in dynamic command menu item label (#19211)
- Adds a resolveObjectMetadataLabel utility that returns the singular or
plural label from object metadata based on the number of selected
records (e.g., "person" vs "people").
- Exposes a pre-computed objectMetadataLabel string on
CommandMenuContextApi, making it available for label interpolation
(e.g., Delete ${capitalize(objectMetadataLabel)}).
This commit is contained in:
+10
-2
@@ -14,18 +14,18 @@ import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/r
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { recordStoreRecordsSelector } from '@/object-record/record-store/states/selectors/recordStoreRecordsSelector';
|
||||
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { useStore } from 'jotai';
|
||||
import {
|
||||
CommandMenuContextApiPageType,
|
||||
type CommandMenuContextApi,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined, resolveObjectMetadataLabel } from 'twenty-shared/utils';
|
||||
|
||||
export const useCommandMenuContextApi = (): CommandMenuContextApi => {
|
||||
const store = useStore();
|
||||
@@ -138,6 +138,13 @@ export const useCommandMenuContextApi = (): CommandMenuContextApi => {
|
||||
permissions.canUpdate;
|
||||
}
|
||||
|
||||
const objectMetadataLabel = isDefined(objectMetadataItem)
|
||||
? resolveObjectMetadataLabel({
|
||||
objectMetadataItem: objectMetadataItem,
|
||||
numberOfSelectedRecords: contextStoreNumberOfSelectedRecords,
|
||||
})
|
||||
: '';
|
||||
|
||||
return {
|
||||
pageType,
|
||||
isInSidePanel,
|
||||
@@ -152,5 +159,6 @@ export const useCommandMenuContextApi = (): CommandMenuContextApi => {
|
||||
targetObjectReadPermissions,
|
||||
targetObjectWritePermissions,
|
||||
objectMetadataItem: objectMetadataItem ?? {},
|
||||
objectMetadataLabel,
|
||||
};
|
||||
};
|
||||
|
||||
+1
@@ -38,6 +38,7 @@ const buildMockCommandMenuContextApi = (
|
||||
targetObjectReadPermissions: {},
|
||||
targetObjectWritePermissions: {},
|
||||
objectMetadataItem: {},
|
||||
objectMetadataLabel: '',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
||||
+3
@@ -25,6 +25,9 @@ export const targetObjectWritePermissions =
|
||||
export const objectMetadataItem =
|
||||
null as unknown as CommandMenuContextApi['objectMetadataItem'];
|
||||
|
||||
export const objectMetadataLabel =
|
||||
null as unknown as CommandMenuContextApi['objectMetadataLabel'];
|
||||
|
||||
export const isDefined = null as unknown as (value: unknown) => boolean;
|
||||
export const isNonEmptyString = null as unknown as (value: unknown) => boolean;
|
||||
export const includes = null as unknown as (
|
||||
|
||||
@@ -16,4 +16,5 @@ export type CommandMenuContextApi = {
|
||||
targetObjectReadPermissions: Record<string, boolean>;
|
||||
targetObjectWritePermissions: Record<string, boolean>;
|
||||
objectMetadataItem: Record<string, unknown>;
|
||||
objectMetadataLabel: string;
|
||||
};
|
||||
|
||||
+1
@@ -29,6 +29,7 @@ const buildContext = (
|
||||
targetObjectReadPermissions: {},
|
||||
targetObjectWritePermissions: {},
|
||||
objectMetadataItem: {},
|
||||
objectMetadataLabel: '',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
||||
+68
@@ -29,6 +29,7 @@ const buildContext = (
|
||||
targetObjectReadPermissions: {},
|
||||
targetObjectWritePermissions: {},
|
||||
objectMetadataItem: {},
|
||||
objectMetadataLabel: '',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
@@ -313,4 +314,71 @@ describe('interpolateCommandMenuItemLabel', () => {
|
||||
).toBe('Delete people');
|
||||
});
|
||||
});
|
||||
|
||||
describe('objectMetadataLabel interpolation', () => {
|
||||
it('should resolve singular label with capitalize transform', () => {
|
||||
const context = buildContext({
|
||||
objectMetadataLabel: 'person',
|
||||
});
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'Delete ${capitalize(objectMetadataLabel)}',
|
||||
context,
|
||||
}),
|
||||
).toBe('Delete Person');
|
||||
});
|
||||
|
||||
it('should resolve plural label with capitalize transform', () => {
|
||||
const context = buildContext({
|
||||
objectMetadataLabel: 'companies',
|
||||
});
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'Export ${capitalize(objectMetadataLabel)}',
|
||||
context,
|
||||
}),
|
||||
).toBe('Export Companies');
|
||||
});
|
||||
|
||||
it('should resolve objectMetadataLabel without transform', () => {
|
||||
const context = buildContext({
|
||||
objectMetadataLabel: 'people',
|
||||
});
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: '${objectMetadataLabel} selected',
|
||||
context,
|
||||
}),
|
||||
).toBe('people selected');
|
||||
});
|
||||
|
||||
it('should resolve objectMetadataLabel with lowercase transform', () => {
|
||||
const context = buildContext({
|
||||
objectMetadataLabel: 'Person',
|
||||
});
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'Create ${lowercase(objectMetadataLabel)}',
|
||||
context,
|
||||
}),
|
||||
).toBe('Create person');
|
||||
});
|
||||
|
||||
it('should return empty segment when objectMetadataLabel is empty', () => {
|
||||
const context = buildContext({
|
||||
objectMetadataLabel: '',
|
||||
});
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'Delete ${capitalize(objectMetadataLabel)}',
|
||||
context,
|
||||
}),
|
||||
).toBe('Delete ');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { resolveObjectMetadataLabel } from '../resolveObjectMetadataLabel';
|
||||
|
||||
describe('resolveObjectMetadataLabel', () => {
|
||||
const objectMetadataItem = {
|
||||
labelSingular: 'person',
|
||||
labelPlural: 'people',
|
||||
};
|
||||
|
||||
it('should return labelSingular when numberOfSelectedRecords is 1', () => {
|
||||
expect(
|
||||
resolveObjectMetadataLabel({
|
||||
objectMetadataItem,
|
||||
numberOfSelectedRecords: 1,
|
||||
}),
|
||||
).toBe('person');
|
||||
});
|
||||
|
||||
it('should return labelPlural when numberOfSelectedRecords is 0', () => {
|
||||
expect(
|
||||
resolveObjectMetadataLabel({
|
||||
objectMetadataItem,
|
||||
numberOfSelectedRecords: 0,
|
||||
}),
|
||||
).toBe('people');
|
||||
});
|
||||
|
||||
it('should return labelPlural when numberOfSelectedRecords is greater than 1', () => {
|
||||
expect(
|
||||
resolveObjectMetadataLabel({
|
||||
objectMetadataItem,
|
||||
numberOfSelectedRecords: 5,
|
||||
}),
|
||||
).toBe('people');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
export const resolveObjectMetadataLabel = ({
|
||||
objectMetadataItem,
|
||||
numberOfSelectedRecords,
|
||||
}: {
|
||||
objectMetadataItem: { labelSingular: string; labelPlural: string };
|
||||
numberOfSelectedRecords: number;
|
||||
}): string => {
|
||||
return numberOfSelectedRecords === 1
|
||||
? objectMetadataItem.labelSingular
|
||||
: objectMetadataItem.labelPlural;
|
||||
};
|
||||
@@ -26,6 +26,7 @@ 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 { resolveObjectMetadataLabel } from './command-menu-items/resolveObjectMetadataLabel';
|
||||
export { safeGetNestedProperty } from './command-menu-items/safeGetNestedProperty';
|
||||
export { computeDiffBetweenObjects } from './compute-diff-between-objects';
|
||||
export { isPlainDateAfter } from './date/isPlainDateAfter';
|
||||
|
||||
Reference in New Issue
Block a user