[COMMAND MENU ITEMS] Add dynamic label and icon to command menu navigation items (#19452)
This commit is contained in:
+27
-4
@@ -15,10 +15,14 @@ import { addPayloadCheckConstraintToCommandMenuItem } from 'src/database/typeorm
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
|
||||
import { isObjectMetadataCommandMenuItemPayload } from 'src/engine/metadata-modules/command-menu-item/utils/is-object-metadata-command-menu-item-payload.util';
|
||||
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
|
||||
import {
|
||||
buildNavigationFlatCommandMenuItem,
|
||||
NAVIGATION_COMMAND_UUID_NAMESPACE,
|
||||
NAVIGATION_INTERPOLATED_ICON,
|
||||
NAVIGATION_INTERPOLATED_LABEL,
|
||||
NAVIGATION_INTERPOLATED_SHORT_LABEL,
|
||||
} from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util';
|
||||
import { seedCompareObjectMetadataForNavigationPosition } from 'src/engine/metadata-modules/flat-command-menu-item/utils/seed-compare-object-metadata-for-navigation-position.util';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
@@ -239,19 +243,38 @@ export class RefactorNavigationCommandsCommand extends ActiveOrSuspendedWorkspac
|
||||
});
|
||||
}
|
||||
|
||||
const staleNavigationItemsToUpdate = allCommandMenuItems
|
||||
.filter(
|
||||
(item) =>
|
||||
item.engineComponentKey === EngineComponentKey.NAVIGATION &&
|
||||
isObjectMetadataCommandMenuItemPayload(item.payload) &&
|
||||
(item.label !== NAVIGATION_INTERPOLATED_LABEL ||
|
||||
item.shortLabel !== NAVIGATION_INTERPOLATED_SHORT_LABEL ||
|
||||
item.icon !== NAVIGATION_INTERPOLATED_ICON),
|
||||
)
|
||||
.map((item) => ({
|
||||
...item,
|
||||
label: NAVIGATION_INTERPOLATED_LABEL,
|
||||
shortLabel: NAVIGATION_INTERPOLATED_SHORT_LABEL,
|
||||
icon: NAVIGATION_INTERPOLATED_ICON,
|
||||
updatedAt: now,
|
||||
}));
|
||||
|
||||
if (
|
||||
goToItemsToDelete.length === 0 &&
|
||||
flatCommandMenuItemsToCreate.length === 0
|
||||
flatCommandMenuItemsToCreate.length === 0 &&
|
||||
staleNavigationItemsToUpdate.length === 0
|
||||
) {
|
||||
this.logger.log(
|
||||
`All NAVIGATION commands already exist for workspace ${workspaceId}, skipping`,
|
||||
`All NAVIGATION commands already exist and use interpolation for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] Would create' : 'Creating'} ${flatCommandMenuItemsToCreate.length} NAVIGATION command(s) for workspace ${workspaceId}`,
|
||||
`${isDryRun ? '[DRY RUN] Would create' : 'Creating'} ${flatCommandMenuItemsToCreate.length} NAVIGATION command(s) ` +
|
||||
`and update ${staleNavigationItemsToUpdate.length} stale item(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
@@ -265,7 +288,7 @@ export class RefactorNavigationCommandsCommand extends ActiveOrSuspendedWorkspac
|
||||
commandMenuItem: {
|
||||
flatEntityToCreate: flatCommandMenuItemsToCreate,
|
||||
flatEntityToDelete: goToItemsToDelete,
|
||||
flatEntityToUpdate: [],
|
||||
flatEntityToUpdate: staleNavigationItemsToUpdate,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
|
||||
+2
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { I18nModule } from 'src/engine/core-modules/i18n/i18n.module';
|
||||
import { CommandMenuItemResolver } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.resolver';
|
||||
import { FrontComponentModule } from 'src/engine/metadata-modules/front-component/front-component.module';
|
||||
import { CommandMenuItemService } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.service';
|
||||
@@ -19,6 +20,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
FlatCommandMenuItemModule,
|
||||
FrontComponentModule,
|
||||
FeatureFlagModule,
|
||||
I18nModule,
|
||||
],
|
||||
providers: [
|
||||
CommandMenuItemService,
|
||||
|
||||
+57
-1
@@ -1,10 +1,19 @@
|
||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Parent, Query, ResolveField } from '@nestjs/graphql';
|
||||
import {
|
||||
Args,
|
||||
Context,
|
||||
Mutation,
|
||||
Parent,
|
||||
Query,
|
||||
ResolveField,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { type I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
@@ -30,6 +39,53 @@ export class CommandMenuItemResolver {
|
||||
private readonly frontComponentService: FrontComponentService,
|
||||
) {}
|
||||
|
||||
@ResolveField(() => String)
|
||||
async label(
|
||||
@Parent() commandMenuItem: CommandMenuItemDTO,
|
||||
@Context() context: { loaders: IDataloaders } & I18nContext,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<string> {
|
||||
return (
|
||||
(await this.commandMenuItemService.resolveNavigationField({
|
||||
commandMenuItem,
|
||||
fieldName: 'label',
|
||||
objectMetadataLoader: context.loaders.objectMetadataLoader,
|
||||
workspaceId: workspace.id,
|
||||
locale: context.req.locale,
|
||||
})) ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
@ResolveField(() => String, { nullable: true })
|
||||
async shortLabel(
|
||||
@Parent() commandMenuItem: CommandMenuItemDTO,
|
||||
@Context() context: { loaders: IDataloaders } & I18nContext,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<string | undefined> {
|
||||
return this.commandMenuItemService.resolveNavigationField({
|
||||
commandMenuItem,
|
||||
fieldName: 'shortLabel',
|
||||
objectMetadataLoader: context.loaders.objectMetadataLoader,
|
||||
workspaceId: workspace.id,
|
||||
locale: context.req.locale,
|
||||
});
|
||||
}
|
||||
|
||||
@ResolveField(() => String, { nullable: true })
|
||||
async icon(
|
||||
@Parent() commandMenuItem: CommandMenuItemDTO,
|
||||
@Context() context: { loaders: IDataloaders } & I18nContext,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<string | undefined> {
|
||||
return this.commandMenuItemService.resolveNavigationField({
|
||||
commandMenuItem,
|
||||
fieldName: 'icon',
|
||||
objectMetadataLoader: context.loaders.objectMetadataLoader,
|
||||
workspaceId: workspace.id,
|
||||
locale: context.req.locale,
|
||||
});
|
||||
}
|
||||
|
||||
@ResolveField(() => FrontComponentDTO, { nullable: true })
|
||||
async frontComponent(
|
||||
@Parent() commandMenuItem: CommandMenuItemDTO,
|
||||
|
||||
+65
@@ -1,8 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import type DataLoader from 'dataloader';
|
||||
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { type ObjectMetadataLoaderPayload } from 'src/engine/dataloaders/dataloader.service';
|
||||
import {
|
||||
CommandMenuItemException,
|
||||
CommandMenuItemExceptionCode,
|
||||
@@ -10,7 +14,11 @@ import {
|
||||
import { type CommandMenuItemDTO } from 'src/engine/metadata-modules/command-menu-item/dtos/command-menu-item.dto';
|
||||
import { type CreateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/create-command-menu-item.input';
|
||||
import { type UpdateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/update-command-menu-item.input';
|
||||
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
|
||||
import { isObjectMetadataCommandMenuItemPayload } from 'src/engine/metadata-modules/command-menu-item/utils/is-object-metadata-command-menu-item-payload.util';
|
||||
import { interpolateNavigationCommandMenuItemField } from 'src/engine/metadata-modules/command-menu-item/utils/interpolate-navigation-command-menu-item-field.util';
|
||||
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
|
||||
import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
import { fromCreateCommandMenuItemInputToFlatCommandMenuItemToCreate } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-create-command-menu-item-input-to-flat-command-menu-item-to-create.util';
|
||||
import { fromDeleteCommandMenuItemInputToFlatCommandMenuItemOrThrow } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-delete-command-menu-item-input-to-flat-command-menu-item-or-throw.util';
|
||||
import { fromFlatCommandMenuItemToCommandMenuItemDto } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-flat-command-menu-item-to-command-menu-item-dto.util';
|
||||
@@ -27,6 +35,7 @@ export class CommandMenuItemService {
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async findAll(workspaceId: string): Promise<CommandMenuItemDTO[]> {
|
||||
@@ -282,6 +291,62 @@ export class CommandMenuItemService {
|
||||
.sort((a, b) => a.position - b.position);
|
||||
}
|
||||
|
||||
async loadNavigationObjectMetadata({
|
||||
commandMenuItem,
|
||||
objectMetadataLoader,
|
||||
workspaceId,
|
||||
}: {
|
||||
commandMenuItem: CommandMenuItemDTO;
|
||||
objectMetadataLoader: DataLoader<
|
||||
ObjectMetadataLoaderPayload,
|
||||
ObjectMetadataDTO | null
|
||||
>;
|
||||
workspaceId: string;
|
||||
}): Promise<ObjectMetadataDTO | null> {
|
||||
if (
|
||||
commandMenuItem.engineComponentKey !== EngineComponentKey.NAVIGATION ||
|
||||
!isObjectMetadataCommandMenuItemPayload(commandMenuItem.payload)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return objectMetadataLoader.load({
|
||||
objectMetadataId: commandMenuItem.payload.objectMetadataItemId,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async resolveNavigationField({
|
||||
commandMenuItem,
|
||||
fieldName,
|
||||
objectMetadataLoader,
|
||||
workspaceId,
|
||||
locale,
|
||||
}: {
|
||||
commandMenuItem: CommandMenuItemDTO;
|
||||
fieldName: 'label' | 'shortLabel' | 'icon';
|
||||
objectMetadataLoader: DataLoader<
|
||||
ObjectMetadataLoaderPayload,
|
||||
ObjectMetadataDTO | null
|
||||
>;
|
||||
workspaceId: string;
|
||||
locale: keyof typeof APP_LOCALES | undefined;
|
||||
}): Promise<string | undefined> {
|
||||
const objectMetadata = await this.loadNavigationObjectMetadata({
|
||||
commandMenuItem,
|
||||
objectMetadataLoader,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return interpolateNavigationCommandMenuItemField({
|
||||
commandMenuItem,
|
||||
fieldName,
|
||||
objectMetadata,
|
||||
locale,
|
||||
i18nInstance: this.i18nService.getI18nInstance(locale ?? SOURCE_LOCALE),
|
||||
});
|
||||
}
|
||||
|
||||
async findByWorkflowVersionId(
|
||||
workflowVersionId: string,
|
||||
workspaceId: string,
|
||||
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
import { type I18n } from '@lingui/core';
|
||||
|
||||
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
|
||||
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
|
||||
import { interpolateNavigationCommandMenuItemField } from 'src/engine/metadata-modules/command-menu-item/utils/interpolate-navigation-command-menu-item-field.util';
|
||||
import {
|
||||
NAVIGATION_INTERPOLATED_ICON,
|
||||
NAVIGATION_INTERPOLATED_LABEL,
|
||||
NAVIGATION_INTERPOLATED_SHORT_LABEL,
|
||||
} from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util';
|
||||
import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
|
||||
const mockI18nInstance = {
|
||||
_: (messageId: string) => messageId,
|
||||
} as unknown as I18n;
|
||||
|
||||
const mockObjectMetadata = {
|
||||
id: 'obj-id-1',
|
||||
labelPlural: 'People',
|
||||
labelSingular: 'Person',
|
||||
description: 'A person',
|
||||
icon: 'IconUser',
|
||||
isCustom: false,
|
||||
standardOverrides: undefined,
|
||||
} as unknown as ObjectMetadataDTO;
|
||||
|
||||
const baseCommandMenuItem = {
|
||||
id: 'cmd-id-1',
|
||||
engineComponentKey: EngineComponentKey.NAVIGATION,
|
||||
label: NAVIGATION_INTERPOLATED_LABEL,
|
||||
shortLabel: NAVIGATION_INTERPOLATED_SHORT_LABEL,
|
||||
icon: NAVIGATION_INTERPOLATED_ICON,
|
||||
position: 1,
|
||||
isPinned: false,
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
payload: { objectMetadataItemId: 'obj-id-1' },
|
||||
workspaceId: 'ws-id-1',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
describe('interpolateNavigationCommandMenuItemField', () => {
|
||||
it('should resolve label template for NAVIGATION items', () => {
|
||||
const result = interpolateNavigationCommandMenuItemField({
|
||||
commandMenuItem: baseCommandMenuItem,
|
||||
fieldName: 'label',
|
||||
objectMetadata: mockObjectMetadata,
|
||||
locale: undefined,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result).toBe('Go to People');
|
||||
});
|
||||
|
||||
it('should resolve shortLabel template for NAVIGATION items', () => {
|
||||
const result = interpolateNavigationCommandMenuItemField({
|
||||
commandMenuItem: baseCommandMenuItem,
|
||||
fieldName: 'shortLabel',
|
||||
objectMetadata: mockObjectMetadata,
|
||||
locale: undefined,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result).toBe('People');
|
||||
});
|
||||
|
||||
it('should resolve icon template for NAVIGATION items', () => {
|
||||
const result = interpolateNavigationCommandMenuItemField({
|
||||
commandMenuItem: baseCommandMenuItem,
|
||||
fieldName: 'icon',
|
||||
objectMetadata: mockObjectMetadata,
|
||||
locale: undefined,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result).toBe('IconUser');
|
||||
});
|
||||
|
||||
it('should return raw label for non-NAVIGATION items', () => {
|
||||
const nonNavigationItem = {
|
||||
...baseCommandMenuItem,
|
||||
engineComponentKey: EngineComponentKey.CREATE_NEW_RECORD,
|
||||
payload: undefined,
|
||||
label: 'Create New Record',
|
||||
};
|
||||
|
||||
const result = interpolateNavigationCommandMenuItemField({
|
||||
commandMenuItem: nonNavigationItem,
|
||||
fieldName: 'label',
|
||||
objectMetadata: null,
|
||||
locale: undefined,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result).toBe('Create New Record');
|
||||
});
|
||||
|
||||
it('should return undefined when object metadata is null for a NAVIGATION item', () => {
|
||||
const result = interpolateNavigationCommandMenuItemField({
|
||||
commandMenuItem: baseCommandMenuItem,
|
||||
fieldName: 'label',
|
||||
objectMetadata: null,
|
||||
locale: undefined,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for undefined shortLabel', () => {
|
||||
const itemWithoutShortLabel = {
|
||||
...baseCommandMenuItem,
|
||||
shortLabel: undefined,
|
||||
};
|
||||
|
||||
const result = interpolateNavigationCommandMenuItemField({
|
||||
commandMenuItem: itemWithoutShortLabel,
|
||||
fieldName: 'shortLabel',
|
||||
objectMetadata: mockObjectMetadata,
|
||||
locale: undefined,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should resolve label for custom object metadata', () => {
|
||||
const customObjectMetadata = {
|
||||
...mockObjectMetadata,
|
||||
isCustom: true,
|
||||
labelPlural: 'Custom Objects',
|
||||
icon: 'IconCustom',
|
||||
} as unknown as ObjectMetadataDTO;
|
||||
|
||||
const result = interpolateNavigationCommandMenuItemField({
|
||||
commandMenuItem: baseCommandMenuItem,
|
||||
fieldName: 'label',
|
||||
objectMetadata: customObjectMetadata,
|
||||
locale: undefined,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result).toBe('Go to Custom Objects');
|
||||
});
|
||||
|
||||
it('should resolve icon for custom object metadata', () => {
|
||||
const customObjectMetadata = {
|
||||
...mockObjectMetadata,
|
||||
isCustom: true,
|
||||
icon: 'IconCustom',
|
||||
} as unknown as ObjectMetadataDTO;
|
||||
|
||||
const result = interpolateNavigationCommandMenuItemField({
|
||||
commandMenuItem: baseCommandMenuItem,
|
||||
fieldName: 'icon',
|
||||
objectMetadata: customObjectMetadata,
|
||||
locale: undefined,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result).toBe('IconCustom');
|
||||
});
|
||||
|
||||
it('should return raw value when payload has no objectMetadataItemId', () => {
|
||||
const itemWithPathPayload = {
|
||||
...baseCommandMenuItem,
|
||||
payload: { path: '/settings/profile' },
|
||||
};
|
||||
|
||||
const result = interpolateNavigationCommandMenuItemField({
|
||||
commandMenuItem: itemWithPathPayload,
|
||||
fieldName: 'label',
|
||||
objectMetadata: null,
|
||||
locale: undefined,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result).toBe(NAVIGATION_INTERPOLATED_LABEL);
|
||||
});
|
||||
|
||||
it('should return literal label as-is when it has no template variables', () => {
|
||||
const itemWithLiteralLabel = {
|
||||
...baseCommandMenuItem,
|
||||
label: 'Go to People',
|
||||
};
|
||||
|
||||
const result = interpolateNavigationCommandMenuItemField({
|
||||
commandMenuItem: itemWithLiteralLabel,
|
||||
fieldName: 'label',
|
||||
objectMetadata: mockObjectMetadata,
|
||||
locale: undefined,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result).toBe('Go to People');
|
||||
});
|
||||
});
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { type I18n } from '@lingui/core';
|
||||
import { type APP_LOCALES } from 'twenty-shared/translations';
|
||||
|
||||
import { type ObjectStandardOverridesDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-standard-overrides.dto';
|
||||
import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util';
|
||||
|
||||
export type NavigationInterpolationObjectMetadata = {
|
||||
labelPlural: string;
|
||||
labelSingular: string;
|
||||
description?: string | null;
|
||||
icon?: string | null;
|
||||
isCustom: boolean;
|
||||
standardOverrides?: ObjectStandardOverridesDTO | null;
|
||||
};
|
||||
|
||||
export const buildNavigationInterpolationContext = ({
|
||||
objectMetadata,
|
||||
locale,
|
||||
i18nInstance,
|
||||
}: {
|
||||
objectMetadata: NavigationInterpolationObjectMetadata;
|
||||
locale: keyof typeof APP_LOCALES | undefined;
|
||||
i18nInstance: I18n;
|
||||
}): Record<string, unknown> => {
|
||||
const overrideInput = {
|
||||
labelPlural: objectMetadata.labelPlural,
|
||||
labelSingular: objectMetadata.labelSingular,
|
||||
description: objectMetadata.description ?? undefined,
|
||||
icon: objectMetadata.icon ?? undefined,
|
||||
isCustom: objectMetadata.isCustom,
|
||||
standardOverrides: objectMetadata.standardOverrides ?? undefined,
|
||||
};
|
||||
|
||||
const resolvedLabelPlural = resolveObjectMetadataStandardOverride(
|
||||
overrideInput,
|
||||
'labelPlural',
|
||||
locale,
|
||||
i18nInstance,
|
||||
);
|
||||
|
||||
const resolvedIcon = resolveObjectMetadataStandardOverride(
|
||||
overrideInput,
|
||||
'icon',
|
||||
locale,
|
||||
i18nInstance,
|
||||
);
|
||||
|
||||
return {
|
||||
navigateToObjectMetadataItem: {
|
||||
labelPlural: resolvedLabelPlural,
|
||||
icon: resolvedIcon,
|
||||
},
|
||||
};
|
||||
};
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { type I18n } from '@lingui/core';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { type APP_LOCALES } from 'twenty-shared/translations';
|
||||
import {
|
||||
interpolateCommandMenuItemTemplate,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { type CommandMenuItemDTO } from 'src/engine/metadata-modules/command-menu-item/dtos/command-menu-item.dto';
|
||||
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
|
||||
import { buildNavigationInterpolationContext } from 'src/engine/metadata-modules/command-menu-item/utils/build-navigation-interpolation-context.util';
|
||||
import { isObjectMetadataCommandMenuItemPayload } from 'src/engine/metadata-modules/command-menu-item/utils/is-object-metadata-command-menu-item-payload.util';
|
||||
import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
|
||||
export const interpolateNavigationCommandMenuItemField = ({
|
||||
commandMenuItem,
|
||||
fieldName,
|
||||
objectMetadata,
|
||||
locale,
|
||||
i18nInstance,
|
||||
}: {
|
||||
commandMenuItem: CommandMenuItemDTO;
|
||||
fieldName: 'label' | 'shortLabel' | 'icon';
|
||||
objectMetadata: ObjectMetadataDTO | null;
|
||||
locale: keyof typeof APP_LOCALES | undefined;
|
||||
i18nInstance: I18n;
|
||||
}): string | undefined => {
|
||||
const rawValue = commandMenuItem[fieldName];
|
||||
|
||||
if (
|
||||
commandMenuItem.engineComponentKey !== EngineComponentKey.NAVIGATION ||
|
||||
!isObjectMetadataCommandMenuItemPayload(commandMenuItem.payload)
|
||||
) {
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
if (!isDefined(objectMetadata)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!isNonEmptyString(rawValue)) {
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
const context = buildNavigationInterpolationContext({
|
||||
objectMetadata,
|
||||
locale,
|
||||
i18nInstance,
|
||||
});
|
||||
|
||||
return (
|
||||
interpolateCommandMenuItemTemplate({
|
||||
label: rawValue,
|
||||
context,
|
||||
}) ?? rawValue
|
||||
);
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type CommandMenuItemPayload } from 'src/engine/metadata-modules/command-menu-item/dtos/command-menu-item-payload.union';
|
||||
import { type ObjectMetadataCommandMenuItemPayload } from 'src/engine/metadata-modules/command-menu-item/dtos/types/object-metadata-command-menu-item-payload.type';
|
||||
|
||||
export const isObjectMetadataCommandMenuItemPayload = (
|
||||
payload?: CommandMenuItemPayload | null,
|
||||
): payload is ObjectMetadataCommandMenuItemPayload =>
|
||||
isDefined(payload) && 'objectMetadataItemId' in payload;
|
||||
+11
-8
@@ -2,7 +2,12 @@ import { v5 } from 'uuid';
|
||||
|
||||
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
|
||||
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
|
||||
import { buildNavigationFlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util';
|
||||
import {
|
||||
buildNavigationFlatCommandMenuItem,
|
||||
NAVIGATION_INTERPOLATED_ICON,
|
||||
NAVIGATION_INTERPOLATED_LABEL,
|
||||
NAVIGATION_INTERPOLATED_SHORT_LABEL,
|
||||
} from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
|
||||
const NAVIGATION_COMMAND_UUID_NAMESPACE =
|
||||
@@ -11,8 +16,6 @@ const NAVIGATION_COMMAND_UUID_NAMESPACE =
|
||||
const baseObjectMetadata = {
|
||||
id: 'obj-id-1',
|
||||
universalIdentifier: 'obj-universal-1',
|
||||
labelPlural: 'People',
|
||||
icon: 'IconUser',
|
||||
nameSingular: 'person',
|
||||
shortcut: 'P',
|
||||
};
|
||||
@@ -38,17 +41,17 @@ describe('buildNavigationFlatCommandMenuItem', () => {
|
||||
expect(result.universalIdentifier).toBe(expectedUniversalIdentifier);
|
||||
});
|
||||
|
||||
it('should set label and shortLabel from objectMetadata', () => {
|
||||
it('should set label and shortLabel as interpolation templates', () => {
|
||||
const result = buildNavigationFlatCommandMenuItem(baseArgs);
|
||||
|
||||
expect(result.label).toBe('Go to People');
|
||||
expect(result.shortLabel).toBe('People');
|
||||
expect(result.label).toBe(NAVIGATION_INTERPOLATED_LABEL);
|
||||
expect(result.shortLabel).toBe(NAVIGATION_INTERPOLATED_SHORT_LABEL);
|
||||
});
|
||||
|
||||
it('should set icon from objectMetadata', () => {
|
||||
it('should set icon as interpolation template', () => {
|
||||
const result = buildNavigationFlatCommandMenuItem(baseArgs);
|
||||
|
||||
expect(result.icon).toBe('IconUser');
|
||||
expect(result.icon).toBe(NAVIGATION_INTERPOLATED_ICON);
|
||||
});
|
||||
|
||||
it('should set payload with objectMetadataItemId', () => {
|
||||
|
||||
+10
-5
@@ -9,6 +9,13 @@ import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty
|
||||
export const NAVIGATION_COMMAND_UUID_NAMESPACE =
|
||||
'b31830da-2ae0-48eb-a915-12fa4ab96dd3';
|
||||
|
||||
export const NAVIGATION_INTERPOLATED_LABEL =
|
||||
'Go to ${navigateToObjectMetadataItem.labelPlural}';
|
||||
export const NAVIGATION_INTERPOLATED_SHORT_LABEL =
|
||||
'${navigateToObjectMetadataItem.labelPlural}';
|
||||
export const NAVIGATION_INTERPOLATED_ICON =
|
||||
'${navigateToObjectMetadataItem.icon}';
|
||||
|
||||
export const buildNavigationFlatCommandMenuItem = ({
|
||||
objectMetadata,
|
||||
commandMenuItemId,
|
||||
@@ -20,8 +27,6 @@ export const buildNavigationFlatCommandMenuItem = ({
|
||||
objectMetadata: {
|
||||
id: string;
|
||||
universalIdentifier: string;
|
||||
labelPlural: string;
|
||||
icon: string | null;
|
||||
nameSingular: string;
|
||||
shortcut: string | null;
|
||||
};
|
||||
@@ -43,9 +48,9 @@ export const buildNavigationFlatCommandMenuItem = ({
|
||||
applicationUniversalIdentifier:
|
||||
TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
workspaceId,
|
||||
label: `Go to ${objectMetadata.labelPlural}`,
|
||||
shortLabel: objectMetadata.labelPlural,
|
||||
icon: objectMetadata.icon,
|
||||
label: NAVIGATION_INTERPOLATED_LABEL,
|
||||
shortLabel: NAVIGATION_INTERPOLATED_SHORT_LABEL,
|
||||
icon: NAVIGATION_INTERPOLATED_ICON,
|
||||
position,
|
||||
isPinned: false,
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
|
||||
+51
-2
@@ -1,14 +1,18 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
|
||||
|
||||
import { OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/object-metadata/constants/object-metadata-standard-overrides-properties.constant';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { NavigationMenuItemRecordIdentifierService } from 'src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-record-identifier.service';
|
||||
import { OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/object-metadata/constants/object-metadata-standard-overrides-properties.constant';
|
||||
import { type MetadataEventBatch } from 'src/engine/subscriptions/metadata-event/types/metadata-event-batch.type';
|
||||
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
|
||||
import { enrichCommandMenuItemEventWithResolvedNavigation } from 'src/engine/subscriptions/metadata-event/utils/enrich-command-menu-item-event-with-resolved-navigation.util';
|
||||
import { enrichFieldMetadataEventWithRelations } from 'src/engine/subscriptions/metadata-event/utils/enrich-field-metadata-event-with-relations.util';
|
||||
import { sanitizeOverridableEntityEventBatch } from 'src/engine/subscriptions/metadata-event/utils/sanitize-overridable-entity-event-batch.util';
|
||||
import { WorkspaceEventBroadcaster } from 'src/engine/subscriptions/workspace-event-broadcaster/workspace-event-broadcaster.service';
|
||||
import { enrichFieldMetadataEventWithRelations } from 'src/engine/subscriptions/metadata-event/utils/enrich-field-metadata-event-with-relations.util';
|
||||
|
||||
@Injectable()
|
||||
export class MetadataEventPublisher {
|
||||
@@ -16,6 +20,7 @@ export class MetadataEventPublisher {
|
||||
private readonly workspaceEventBroadcaster: WorkspaceEventBroadcaster,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly navigationMenuItemRecordIdentifierService: NavigationMenuItemRecordIdentifierService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async publish(metadataEventBatch: MetadataEventBatch): Promise<void> {
|
||||
@@ -50,6 +55,10 @@ export class MetadataEventPublisher {
|
||||
return this.enrichNavigationMenuItemEventsWithTargetRecordIdentifier(
|
||||
metadataEventBatch as MetadataEventBatch<'navigationMenuItem'>,
|
||||
);
|
||||
case 'commandMenuItem':
|
||||
return this.enrichCommandMenuItemEventsWithResolvedNavigation(
|
||||
metadataEventBatch as MetadataEventBatch<'commandMenuItem'>,
|
||||
);
|
||||
case 'objectMetadata':
|
||||
return this.resolveObjectMetadataStandardOverrides(
|
||||
metadataEventBatch as MetadataEventBatch<'objectMetadata'>,
|
||||
@@ -96,6 +105,46 @@ export class MetadataEventPublisher {
|
||||
return { ...metadataEventBatch, events: enrichedEvents };
|
||||
}
|
||||
|
||||
private async enrichCommandMenuItemEventsWithResolvedNavigation(
|
||||
metadataEventBatch: MetadataEventBatch<'commandMenuItem'>,
|
||||
): Promise<MetadataEventBatch<'commandMenuItem'>> {
|
||||
const { flatObjectMetadataMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId: metadataEventBatch.workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const i18nInstance = this.i18nService.getI18nInstance(SOURCE_LOCALE);
|
||||
|
||||
const enrichedEvents = metadataEventBatch.events.map((event) => {
|
||||
if (
|
||||
!('after' in event.properties) ||
|
||||
!isDefined(event.properties.after)
|
||||
) {
|
||||
return event;
|
||||
}
|
||||
|
||||
const enrichedAfter = enrichCommandMenuItemEventWithResolvedNavigation({
|
||||
record: event.properties.after as FlatCommandMenuItem,
|
||||
flatObjectMetadataMaps,
|
||||
locale: SOURCE_LOCALE,
|
||||
i18nInstance,
|
||||
});
|
||||
|
||||
return {
|
||||
...event,
|
||||
properties: {
|
||||
...event.properties,
|
||||
after: enrichedAfter,
|
||||
},
|
||||
} as typeof event;
|
||||
});
|
||||
|
||||
return { ...metadataEventBatch, events: enrichedEvents };
|
||||
}
|
||||
|
||||
private async enrichNavigationMenuItemEventsWithTargetRecordIdentifier(
|
||||
metadataEventBatch: MetadataEventBatch<'navigationMenuItem'>,
|
||||
): Promise<MetadataEventBatch<'navigationMenuItem'>> {
|
||||
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
import { type I18n } from '@lingui/core';
|
||||
import { SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
|
||||
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
|
||||
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import {
|
||||
NAVIGATION_INTERPOLATED_ICON,
|
||||
NAVIGATION_INTERPOLATED_LABEL,
|
||||
NAVIGATION_INTERPOLATED_SHORT_LABEL,
|
||||
} from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { enrichCommandMenuItemEventWithResolvedNavigation } from 'src/engine/subscriptions/metadata-event/utils/enrich-command-menu-item-event-with-resolved-navigation.util';
|
||||
|
||||
const mockI18nInstance = {
|
||||
_: (messageId: string) => messageId,
|
||||
} as unknown as I18n;
|
||||
|
||||
const OBJECT_METADATA_ID = 'obj-id-1';
|
||||
|
||||
const makeFlatObjectMetadata = (
|
||||
overrides?: Partial<FlatObjectMetadata>,
|
||||
): FlatObjectMetadata =>
|
||||
({
|
||||
id: OBJECT_METADATA_ID,
|
||||
universalIdentifier: 'obj-uid-1',
|
||||
workspaceId: 'ws-1',
|
||||
applicationId: 'app-1',
|
||||
labelPlural: 'People',
|
||||
labelSingular: 'Person',
|
||||
icon: 'IconUser',
|
||||
isCustom: false,
|
||||
standardOverrides: null,
|
||||
...overrides,
|
||||
}) as unknown as FlatObjectMetadata;
|
||||
|
||||
const makeFlatObjectMetadataMaps = (
|
||||
flatObjectMetadata: FlatObjectMetadata,
|
||||
): FlatEntityMaps<FlatObjectMetadata> => ({
|
||||
byUniversalIdentifier: {
|
||||
[flatObjectMetadata.universalIdentifier]: flatObjectMetadata,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
[flatObjectMetadata.id]: flatObjectMetadata.universalIdentifier,
|
||||
},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
});
|
||||
|
||||
const makeNavigationRecord = (
|
||||
overrides?: Partial<FlatCommandMenuItem>,
|
||||
): FlatCommandMenuItem =>
|
||||
({
|
||||
id: 'cmd-id-1',
|
||||
engineComponentKey: EngineComponentKey.NAVIGATION,
|
||||
label: NAVIGATION_INTERPOLATED_LABEL,
|
||||
shortLabel: NAVIGATION_INTERPOLATED_SHORT_LABEL,
|
||||
icon: NAVIGATION_INTERPOLATED_ICON,
|
||||
payload: { objectMetadataItemId: OBJECT_METADATA_ID },
|
||||
position: 1,
|
||||
isPinned: false,
|
||||
...overrides,
|
||||
}) as unknown as FlatCommandMenuItem;
|
||||
|
||||
describe('enrichCommandMenuItemEventWithResolvedNavigation', () => {
|
||||
it('should resolve label, shortLabel, and icon templates for NAVIGATION items', () => {
|
||||
const flatObjectMetadata = makeFlatObjectMetadata();
|
||||
const flatObjectMetadataMaps =
|
||||
makeFlatObjectMetadataMaps(flatObjectMetadata);
|
||||
|
||||
const record = makeNavigationRecord();
|
||||
|
||||
const result = enrichCommandMenuItemEventWithResolvedNavigation({
|
||||
record,
|
||||
flatObjectMetadataMaps,
|
||||
locale: SOURCE_LOCALE,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result.label).toBe('Go to People');
|
||||
expect(result.shortLabel).toBe('People');
|
||||
expect(result.icon).toBe('IconUser');
|
||||
});
|
||||
|
||||
it('should return record unchanged for non-NAVIGATION items', () => {
|
||||
const flatObjectMetadata = makeFlatObjectMetadata();
|
||||
const flatObjectMetadataMaps =
|
||||
makeFlatObjectMetadataMaps(flatObjectMetadata);
|
||||
|
||||
const record = makeNavigationRecord({
|
||||
engineComponentKey: EngineComponentKey.CREATE_NEW_RECORD,
|
||||
label: 'Create New Record',
|
||||
shortLabel: undefined,
|
||||
icon: 'IconPlus',
|
||||
payload: undefined,
|
||||
});
|
||||
|
||||
const result = enrichCommandMenuItemEventWithResolvedNavigation({
|
||||
record,
|
||||
flatObjectMetadataMaps,
|
||||
locale: SOURCE_LOCALE,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result).toBe(record);
|
||||
});
|
||||
|
||||
it('should return record unchanged when payload has no objectMetadataItemId', () => {
|
||||
const flatObjectMetadata = makeFlatObjectMetadata();
|
||||
const flatObjectMetadataMaps =
|
||||
makeFlatObjectMetadataMaps(flatObjectMetadata);
|
||||
|
||||
const record = makeNavigationRecord({
|
||||
payload: { path: '/settings' },
|
||||
});
|
||||
|
||||
const result = enrichCommandMenuItemEventWithResolvedNavigation({
|
||||
record,
|
||||
flatObjectMetadataMaps,
|
||||
locale: SOURCE_LOCALE,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result).toBe(record);
|
||||
});
|
||||
|
||||
it('should return record unchanged when object metadata is not found in maps', () => {
|
||||
const emptyMaps: FlatEntityMaps<FlatObjectMetadata> = {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
};
|
||||
|
||||
const record = makeNavigationRecord();
|
||||
|
||||
const result = enrichCommandMenuItemEventWithResolvedNavigation({
|
||||
record,
|
||||
flatObjectMetadataMaps: emptyMaps,
|
||||
locale: SOURCE_LOCALE,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result).toBe(record);
|
||||
});
|
||||
|
||||
it('should apply standard overrides when resolving templates', () => {
|
||||
const flatObjectMetadata = makeFlatObjectMetadata({
|
||||
labelPlural: 'People',
|
||||
icon: 'IconUser',
|
||||
standardOverrides: {
|
||||
labelPlural: 'Contacts',
|
||||
icon: 'IconContacts',
|
||||
} as unknown as FlatObjectMetadata['standardOverrides'],
|
||||
});
|
||||
const flatObjectMetadataMaps =
|
||||
makeFlatObjectMetadataMaps(flatObjectMetadata);
|
||||
|
||||
const record = makeNavigationRecord();
|
||||
|
||||
const result = enrichCommandMenuItemEventWithResolvedNavigation({
|
||||
record,
|
||||
flatObjectMetadataMaps,
|
||||
locale: SOURCE_LOCALE,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result.label).toBe('Go to Contacts');
|
||||
expect(result.shortLabel).toBe('Contacts');
|
||||
expect(result.icon).toBe('IconContacts');
|
||||
});
|
||||
|
||||
it('should use base values when standard overrides are null', () => {
|
||||
const flatObjectMetadata = makeFlatObjectMetadata({
|
||||
labelPlural: 'Companies',
|
||||
icon: 'IconBuilding',
|
||||
standardOverrides: null,
|
||||
});
|
||||
const flatObjectMetadataMaps =
|
||||
makeFlatObjectMetadataMaps(flatObjectMetadata);
|
||||
|
||||
const record = makeNavigationRecord();
|
||||
|
||||
const result = enrichCommandMenuItemEventWithResolvedNavigation({
|
||||
record,
|
||||
flatObjectMetadataMaps,
|
||||
locale: SOURCE_LOCALE,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result.label).toBe('Go to Companies');
|
||||
expect(result.shortLabel).toBe('Companies');
|
||||
expect(result.icon).toBe('IconBuilding');
|
||||
});
|
||||
|
||||
it('should return record unchanged when payload is null', () => {
|
||||
const flatObjectMetadata = makeFlatObjectMetadata();
|
||||
const flatObjectMetadataMaps =
|
||||
makeFlatObjectMetadataMaps(flatObjectMetadata);
|
||||
|
||||
const record = makeNavigationRecord({ payload: null });
|
||||
|
||||
const result = enrichCommandMenuItemEventWithResolvedNavigation({
|
||||
record,
|
||||
flatObjectMetadataMaps,
|
||||
locale: SOURCE_LOCALE,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result).toBe(record);
|
||||
});
|
||||
|
||||
it('should pass through already-resolved literal labels', () => {
|
||||
const flatObjectMetadata = makeFlatObjectMetadata();
|
||||
const flatObjectMetadataMaps =
|
||||
makeFlatObjectMetadataMaps(flatObjectMetadata);
|
||||
|
||||
const record = makeNavigationRecord({
|
||||
label: 'Go to People',
|
||||
shortLabel: 'People',
|
||||
icon: 'IconUser',
|
||||
});
|
||||
|
||||
const result = enrichCommandMenuItemEventWithResolvedNavigation({
|
||||
record,
|
||||
flatObjectMetadataMaps,
|
||||
locale: SOURCE_LOCALE,
|
||||
i18nInstance: mockI18nInstance,
|
||||
});
|
||||
|
||||
expect(result.label).toBe('Go to People');
|
||||
expect(result.shortLabel).toBe('People');
|
||||
expect(result.icon).toBe('IconUser');
|
||||
});
|
||||
});
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { type I18n } from '@lingui/core';
|
||||
import { type APP_LOCALES } from 'twenty-shared/translations';
|
||||
import {
|
||||
interpolateCommandMenuItemTemplate,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
|
||||
import { buildNavigationInterpolationContext } from 'src/engine/metadata-modules/command-menu-item/utils/build-navigation-interpolation-context.util';
|
||||
import { isObjectMetadataCommandMenuItemPayload } from 'src/engine/metadata-modules/command-menu-item/utils/is-object-metadata-command-menu-item-payload.util';
|
||||
import { FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
type EnrichCommandMenuItemEventArgs = {
|
||||
record: FlatCommandMenuItem;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
locale: keyof typeof APP_LOCALES | undefined;
|
||||
i18nInstance: I18n;
|
||||
};
|
||||
|
||||
export const enrichCommandMenuItemEventWithResolvedNavigation = ({
|
||||
record,
|
||||
flatObjectMetadataMaps,
|
||||
locale,
|
||||
i18nInstance,
|
||||
}: EnrichCommandMenuItemEventArgs): FlatCommandMenuItem => {
|
||||
if (record.engineComponentKey !== EngineComponentKey.NAVIGATION) {
|
||||
return record;
|
||||
}
|
||||
|
||||
const payload = record.payload;
|
||||
|
||||
if (!isObjectMetadataCommandMenuItemPayload(payload)) {
|
||||
return record;
|
||||
}
|
||||
|
||||
const objectMetadataItemId = payload.objectMetadataItemId;
|
||||
|
||||
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: objectMetadataItemId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(flatObjectMetadata)) {
|
||||
return record;
|
||||
}
|
||||
|
||||
const context = buildNavigationInterpolationContext({
|
||||
objectMetadata: flatObjectMetadata,
|
||||
locale,
|
||||
i18nInstance,
|
||||
});
|
||||
|
||||
const enriched = { ...record };
|
||||
|
||||
for (const field of ['label', 'shortLabel', 'icon'] as const) {
|
||||
const rawValue = record[field];
|
||||
|
||||
const resolvedValue = interpolateCommandMenuItemTemplate({
|
||||
label: rawValue,
|
||||
context,
|
||||
});
|
||||
|
||||
if (isDefined(resolvedValue)) {
|
||||
enriched[field] = resolvedValue;
|
||||
}
|
||||
}
|
||||
|
||||
return enriched;
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { ProcessNestedRelationsHelper } from 'src/engine/api/common/common-neste
|
||||
import { CommonSelectFieldsHelper } from 'src/engine/api/common/common-select-fields/common-select-fields-helper';
|
||||
import { CacheLockModule } from 'src/engine/core-modules/cache-lock/cache-lock.module';
|
||||
import { CacheStorageModule } from 'src/engine/core-modules/cache-storage/cache-storage.module';
|
||||
import { I18nModule } from 'src/engine/core-modules/i18n/i18n.module';
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
import { RedisClientModule } from 'src/engine/core-modules/redis-client/redis-client.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -32,6 +33,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
NavigationMenuItemModule,
|
||||
I18nModule,
|
||||
],
|
||||
providers: [
|
||||
SubscriptionService,
|
||||
|
||||
Reference in New Issue
Block a user