Refactor: Unify command menu item execution (#18908)

- Makes engineComponentKey non-nullable on CommandMenuItem. Adds two new
engine component keys: TRIGGER_WORKFLOW_VERSION and
FRONT_COMPONENT_RENDERER to cover the former standalone cases.
- Unifies the previously separated engine, front component and workflow
runners into a single `CommandRunner` component with a unified
`useMountCommand` hook that handles all command types.
This commit is contained in:
Raphaël Bosi
2026-03-24 16:59:52 +01:00
committed by GitHub
parent 27c0ca975f
commit 49afe5dbc4
103 changed files with 1296 additions and 1131 deletions
@@ -37,9 +37,9 @@ export class CommandMenuItemDTO {
frontComponent?: FrontComponentDTO | null;
@IsEnum(EngineComponentKey)
@IsOptional()
@Field(() => EngineComponentKey, { nullable: true })
engineComponentKey?: EngineComponentKey;
@IsNotEmpty()
@Field(() => EngineComponentKey)
engineComponentKey: EngineComponentKey;
@IsString()
@IsNotEmpty()
@@ -27,9 +27,9 @@ export class CreateCommandMenuItemInput {
frontComponentId?: string;
@IsEnum(EngineComponentKey)
@IsOptional()
@Field(() => EngineComponentKey, { nullable: true })
engineComponentKey?: EngineComponentKey;
@IsNotEmpty()
@Field(() => EngineComponentKey)
engineComponentKey: EngineComponentKey;
@IsString()
@IsNotEmpty()
@@ -30,8 +30,8 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
'availabilityObjectMetadataId',
])
@Check(
'CHK_CMD_MENU_ITEM_WF_OR_FC_OR_ENGINE_KEY',
'("workflowVersionId" IS NOT NULL AND "frontComponentId" IS NULL AND "engineComponentKey" IS NULL) OR ("workflowVersionId" IS NULL AND "frontComponentId" IS NOT NULL AND "engineComponentKey" IS NULL) OR ("workflowVersionId" IS NULL AND "frontComponentId" IS NULL AND "engineComponentKey" IS NOT NULL)',
'CHK_CMD_MENU_ITEM_ENGINE_KEY_COHERENCE',
`("engineComponentKey" = 'TRIGGER_WORKFLOW_VERSION' AND "workflowVersionId" IS NOT NULL AND "frontComponentId" IS NULL) OR ("engineComponentKey" = 'FRONT_COMPONENT_RENDERER' AND "frontComponentId" IS NOT NULL AND "workflowVersionId" IS NULL) OR ("engineComponentKey" NOT IN ('TRIGGER_WORKFLOW_VERSION', 'FRONT_COMPONENT_RENDERER') AND "workflowVersionId" IS NULL AND "frontComponentId" IS NULL)`,
)
export class CommandMenuItemEntity
extends SyncableEntity
@@ -53,12 +53,8 @@ export class CommandMenuItemEntity
@JoinColumn({ name: 'frontComponentId' })
frontComponent: Relation<FrontComponentEntity> | null;
@Column({
type: 'enum',
enum: Object.values(EngineComponentKey),
nullable: true,
})
engineComponentKey: EngineComponentKey | null;
@Column({ type: 'varchar', nullable: false })
engineComponentKey: EngineComponentKey;
@Column({ nullable: false })
label: string;
@@ -58,6 +58,8 @@ export enum EngineComponentKey {
SEARCH_RECORDS_FALLBACK = 'SEARCH_RECORDS_FALLBACK',
ASK_AI = 'ASK_AI',
VIEW_PREVIOUS_AI_CHATS = 'VIEW_PREVIOUS_AI_CHATS',
TRIGGER_WORKFLOW_VERSION = 'TRIGGER_WORKFLOW_VERSION',
FRONT_COMPONENT_RENDERER = 'FRONT_COMPONENT_RENDERER',
}
registerEnumType(EngineComponentKey, {
@@ -1,11 +1,6 @@
import { isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import {
CommandMenuItemException,
CommandMenuItemExceptionCode,
} from 'src/engine/metadata-modules/command-menu-item/command-menu-item.exception';
import { type CreateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/create-command-menu-item.input';
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
@@ -26,29 +21,6 @@ export const fromCreateCommandMenuItemInputToFlatCommandMenuItemToCreate = ({
AllFlatEntityMaps,
'flatObjectMetadataMaps' | 'flatFrontComponentMaps'
>): FlatCommandMenuItem => {
const hasWorkflowVersionId = isDefined(
createCommandMenuItemInput.workflowVersionId,
);
const hasFrontComponentId = isDefined(
createCommandMenuItemInput.frontComponentId,
);
const hasEngineComponentKey = isDefined(
createCommandMenuItemInput.engineComponentKey,
);
const sourceCount = [
hasWorkflowVersionId,
hasFrontComponentId,
hasEngineComponentKey,
].filter(Boolean).length;
if (sourceCount !== 1) {
throw new CommandMenuItemException(
'Exactly one of workflowVersionId, frontComponentId or engineComponentKey is required',
CommandMenuItemExceptionCode.WORKFLOW_OR_FRONT_COMPONENT_REQUIRED,
);
}
const id = uuidv4();
const now = new Date().toISOString();
@@ -71,7 +43,7 @@ export const fromCreateCommandMenuItemInputToFlatCommandMenuItemToCreate = ({
workflowVersionId: createCommandMenuItemInput.workflowVersionId ?? null,
frontComponentId: createCommandMenuItemInput.frontComponentId ?? null,
frontComponentUniversalIdentifier,
engineComponentKey: createCommandMenuItemInput.engineComponentKey ?? null,
engineComponentKey: createCommandMenuItemInput.engineComponentKey,
label: createCommandMenuItemInput.label,
icon: createCommandMenuItemInput.icon ?? null,
shortLabel: createCommandMenuItemInput.shortLabel ?? null,
@@ -7,7 +7,7 @@ export const fromFlatCommandMenuItemToCommandMenuItemDto = (
id: flatCommandMenuItem.id,
workflowVersionId: flatCommandMenuItem.workflowVersionId ?? undefined,
frontComponentId: flatCommandMenuItem.frontComponentId ?? undefined,
engineComponentKey: flatCommandMenuItem.engineComponentKey ?? undefined,
engineComponentKey: flatCommandMenuItem.engineComponentKey,
label: flatCommandMenuItem.label,
icon: flatCommandMenuItem.icon ?? undefined,
shortLabel: flatCommandMenuItem.shortLabel ?? undefined,