[COMMAND MENU ITEMS] Add engine component key (#18554)

## PR Description

In the process of migrating all the existing commands to the backend, we
stumbled across a couple of problems that made us reconsider the full
migration. This PR introduces a way for command menu items to bypass
front components and to directly reference a frontend component from
twenty front.

It:
- Introduces a `engineFrontComponentKey` field on `CommandMenuItem` as
an alternative to `frontComponentId` and `workflowVersionId`, allowing
command menu items to reference frontend components by key directly
rather than requiring a FrontComponent entity
- Updates the DB constraint to allow exactly one of `workflowVersionId`,
`frontComponentId`, or `engineFrontComponentKey`

### All standard command menu items from the frontend which use
`standardFrontComponentKey`

These are all commands that execute a GraphQL query or a mutation.
Two mains concerned have been raised that made us go with this
(temporary) architecture instead:
- If those commands are part of the standard application, they can only
alter objects from that application and not custom objects.
- We would need to implement a way to trigger optimistic rendering from
the front components, which might take some time to implement.

List:
- Create new record
- Delete (single record)
- Delete records (multiple)
- Restore record
- Restore records (multiple)
- Permanently destroy record
- Permanently destroy records (multiple)
- Add to favorites
- Remove from favorites
- Merge records
- Duplicate Dashboard
- Save Dashboard
- Save Page Layout
- Activate Workflow
- Deactivate Workflow
- Discard Draft (workflow)
- Test Workflow
- Tidy up workflow
- Duplicate Workflow
- Stop (workflow run)
- Use as draft (workflow version)

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Raphaël Bosi
2026-03-12 13:14:45 +01:00
committed by GitHub
parent 78473a606a
commit e8f8189167
55 changed files with 857 additions and 1152 deletions
@@ -61,6 +61,7 @@ export const fromCommandMenuItemEntityToFlatCommandMenuItem = ({
id: commandMenuItemEntity.id,
workflowVersionId: commandMenuItemEntity.workflowVersionId,
frontComponentId: commandMenuItemEntity.frontComponentId,
engineComponentKey: commandMenuItemEntity.engineComponentKey,
label: commandMenuItemEntity.label,
icon: commandMenuItemEntity.icon,
shortLabel: commandMenuItemEntity.shortLabel,
@@ -7,7 +7,7 @@ import {
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/entities/command-menu-item.entity';
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';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
@@ -32,10 +32,19 @@ export const fromCreateCommandMenuItemInputToFlatCommandMenuItemToCreate = ({
const hasFrontComponentId = isDefined(
createCommandMenuItemInput.frontComponentId,
);
const hasEngineComponentKey = isDefined(
createCommandMenuItemInput.engineComponentKey,
);
if (hasWorkflowVersionId === hasFrontComponentId) {
const sourceCount = [
hasWorkflowVersionId,
hasFrontComponentId,
hasEngineComponentKey,
].filter(Boolean).length;
if (sourceCount !== 1) {
throw new CommandMenuItemException(
'Exactly one of workflowVersionId or frontComponentId is required',
'Exactly one of workflowVersionId, frontComponentId or engineComponentKey is required',
CommandMenuItemExceptionCode.WORKFLOW_OR_FRONT_COMPONENT_REQUIRED,
);
}
@@ -62,6 +71,7 @@ export const fromCreateCommandMenuItemInputToFlatCommandMenuItemToCreate = ({
workflowVersionId: createCommandMenuItemInput.workflowVersionId ?? null,
frontComponentId: createCommandMenuItemInput.frontComponentId ?? null,
frontComponentUniversalIdentifier,
engineComponentKey: createCommandMenuItemInput.engineComponentKey ?? null,
label: createCommandMenuItemInput.label,
icon: createCommandMenuItemInput.icon ?? null,
shortLabel: createCommandMenuItemInput.shortLabel ?? null,
@@ -7,6 +7,7 @@ export const fromFlatCommandMenuItemToCommandMenuItemDto = (
id: flatCommandMenuItem.id,
workflowVersionId: flatCommandMenuItem.workflowVersionId ?? undefined,
frontComponentId: flatCommandMenuItem.frontComponentId ?? undefined,
engineComponentKey: flatCommandMenuItem.engineComponentKey ?? undefined,
label: flatCommandMenuItem.label,
icon: flatCommandMenuItem.icon ?? undefined,
shortLabel: flatCommandMenuItem.shortLabel ?? undefined,