Refactor navigation commands to use NAVIGATION engine key with payload (#19303)

- Adds a payload JSON column to `CommandMenuItem` and introduces a
unified `NAVIGATION` engine component key that replaces all individual
GO_TO_* keys
- Navigation commands now use the payload to determine their target
(either an objectMetadataItemId or a path), making navigation commands
dynamic and eliminating the need for a hardcoded engine key per object
- Includes a 1.21 upgrade command (refactor-navigation-commands) that
migrates existing GO_TO_* items to NAVIGATION items with the appropriate
payload, and applies a CHECK constraint enforcing payload coherence



https://github.com/user-attachments/assets/4d305ba2-ae0b-4556-bb0e-e9d899777350


TODO: In a second PR, create the sync between object metadata items and
the navigation command menu items
- Object metadata item created or enabled -> Create navigation command
- Object metadata item deleted or disabled -> Delete associated
navigation command

In another PR:
- Allow `label`, `shortLabel` and `icon` to resolve the
`navigateToObjectMetadataItem` dynamically in their interpolation
instead of being hardcoded in the command menu item
- Make the icon dynamic in the command menu items as the label so that
we can resolve ${navigateToObjectMetadataItem.icon} at runTime -> This
way we won't need to keep update the command menu item icon when we
update the objectMetadataItem icon
This commit is contained in:
Raphaël Bosi
2026-04-07 17:00:04 +02:00
committed by GitHub
parent 5e9792009f
commit d341d0d624
41 changed files with 780 additions and 172 deletions
@@ -254,105 +254,6 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
engineComponentKey: EngineComponentKey.HIDE_DELETED_RECORDS,
hotKeys: null,
},
goToPeople: {
universalIdentifier: 'dfe5fef8-d42c-40f0-941f-8e3b5eb01daa',
label: 'Go to People',
icon: 'IconUser',
isPinned: false,
position: 23,
shortLabel: 'People',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: 'targetObjectReadPermissions.person',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.GO_TO_PEOPLE,
hotKeys: ['G', 'P'],
},
goToCompanies: {
universalIdentifier: '196e4eec-bfdd-48a6-bcbb-6707ef11951a',
label: 'Go to Companies',
icon: 'IconBuildingSkyscraper',
isPinned: false,
position: 24,
shortLabel: 'Companies',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: 'targetObjectReadPermissions.company',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.GO_TO_COMPANIES,
hotKeys: ['G', 'C'],
},
goToDashboards: {
universalIdentifier: '11dc07d1-21b2-4f86-af8c-6a664c02f00c',
label: 'Go to Dashboards',
icon: 'IconLayoutDashboard',
isPinned: false,
position: 25,
shortLabel: 'Dashboards',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: 'targetObjectReadPermissions.dashboard',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.GO_TO_DASHBOARDS,
hotKeys: ['G', 'D'],
},
goToOpportunities: {
universalIdentifier: 'f04f5e00-a208-422f-acf2-ff189769510d',
label: 'Go to Opportunities',
icon: 'IconTargetArrow',
isPinned: false,
position: 26,
shortLabel: 'Opportunities',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression:
'targetObjectReadPermissions.opportunity',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.GO_TO_OPPORTUNITIES,
hotKeys: ['G', 'O'],
},
goToSettings: {
universalIdentifier: 'ef9aba44-0068-453e-930a-f8c182af18ee',
label: 'Go to Settings',
icon: 'IconSettings',
isPinned: false,
position: 27,
shortLabel: 'Settings',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.GO_TO_SETTINGS,
hotKeys: ['G', 'S'],
},
goToTasks: {
universalIdentifier: 'e8e3bd0b-5ce9-4577-bb61-588c0b1ad063',
label: 'Go to Tasks',
icon: 'IconCheckbox',
isPinned: false,
position: 28,
shortLabel: 'Tasks',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: 'targetObjectReadPermissions.task',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.GO_TO_TASKS,
hotKeys: ['G', 'T'],
},
goToNotes: {
universalIdentifier: '08e0f0cc-ac2d-46cd-9bca-39a409b9addf',
label: 'Go to Notes',
icon: 'IconCheckbox',
isPinned: false,
position: 29,
shortLabel: 'Notes',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: 'targetObjectReadPermissions.note',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.GO_TO_NOTES,
hotKeys: ['G', 'N'],
},
editRecordPageLayout: {
universalIdentifier: 'd9794c67-1799-424f-8871-5ea771dd4a6d',
label: 'Edit Layout',
@@ -432,20 +333,6 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
engineComponentKey: EngineComponentKey.DUPLICATE_DASHBOARD,
hotKeys: null,
},
goToWorkflows: {
universalIdentifier: '4fa778f9-7931-4d18-b895-929e1ef9c31f',
label: 'Go to Workflows',
icon: 'IconSettingsAutomation',
isPinned: false,
position: 41,
shortLabel: 'Workflows',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: 'targetObjectReadPermissions.workflow',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.GO_TO_WORKFLOWS,
hotKeys: ['G', 'W'],
},
activateWorkflow: {
universalIdentifier: '44f19c85-0fd0-482f-a14e-da513c60b1b3',
label: 'Activate Workflow',
@@ -606,21 +493,6 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
engineComponentKey: EngineComponentKey.DUPLICATE_WORKFLOW,
hotKeys: null,
},
goToRuns: {
universalIdentifier: '1ba959da-ff49-4c1f-a517-2b78ee200508',
label: 'Go to Runs',
icon: 'IconHistoryToggle',
isPinned: false,
position: 52,
shortLabel: 'See Runs',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression:
'not hasAnySoftDeleteFilterOnView and objectMetadataItem.nameSingular != "workflowRun" and objectMetadataItem.nameSingular != "workflowVersion" and objectMetadataItem.nameSingular != "workflow"',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.GO_TO_RUNS,
hotKeys: null,
},
seeVersionWorkflowRun: {
universalIdentifier: 'cc3a065c-c89e-40ac-9449-4272c55b1bb8',
label: 'See Version',
@@ -816,4 +688,19 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
engineComponentKey: EngineComponentKey.COMPOSE_EMAIL,
hotKeys: null,
},
goToSettings: {
universalIdentifier: 'ef9aba44-0068-453e-930a-f8c182af18ee',
label: 'Go to Settings',
icon: 'IconSettings',
isPinned: false,
position: 27,
shortLabel: 'Settings',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.NAVIGATION,
hotKeys: ['G', 'S'],
payload: { path: '/settings/profile' },
},
} as const;
@@ -1,6 +1,9 @@
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type FlatCommandMenuItemMaps } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item-maps.type';
import { buildNavigationFlatCommandMenuItem } 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 { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
@@ -46,5 +49,39 @@ export const buildStandardFlatCommandMenuItemMaps = ({
});
}
const activeObjects = Object.values(
flatObjectMetadataMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((flatObject) => flatObject.isActive)
.sort(seedCompareObjectMetadataForNavigationPosition);
const maxStandardPosition = Object.values(
flatCommandMenuItemMaps.byUniversalIdentifier,
).reduce(
(max, item) => (isDefined(item) ? Math.max(max, item.position) : max),
-1,
);
let nextPosition = maxStandardPosition + 1;
for (const flatObject of activeObjects) {
const position = nextPosition++;
const navigationItem = buildNavigationFlatCommandMenuItem({
objectMetadata: flatObject,
commandMenuItemId: v4(),
applicationId: twentyStandardApplicationId,
workspaceId,
position,
now,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: navigationItem,
flatEntityMapsToMutate: flatCommandMenuItemMaps,
});
}
return flatCommandMenuItemMaps;
};
@@ -65,6 +65,10 @@ export const createStandardCommandMenuItemFlatMetadata = ({
frontComponentId: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: definition.engineComponentKey,
payload:
'payload' in definition && isDefined(definition.payload)
? { ...definition.payload }
: null,
hotKeys: definition.hotKeys ? [...definition.hotKeys] : null,
workflowVersionId: null,
availabilityObjectMetadataId: resolvedObjectMetadataId,
@@ -1,9 +1,11 @@
import { Injectable } from '@nestjs/common';
import { type QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-update-relation-identifiers-to-ids.util';
import {
FlatUpdateCommandMenuItemAction,
@@ -56,7 +58,7 @@ export class UpdateCommandMenuItemActionHandlerService extends WorkspaceMigratio
await commandMenuItemRepository.update(
{ id: entityId, workspaceId },
update,
update as QueryDeepPartialEntity<CommandMenuItemEntity>,
);
}