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:
+15
-128
@@ -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;
|
||||
|
||||
+37
@@ -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;
|
||||
};
|
||||
|
||||
+4
@@ -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,
|
||||
|
||||
+4
-2
@@ -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>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user