Create command menu items for workflows with manual trigger (#18746)

- Automatically create and sync command menu items for all workflows
with a manual trigger
- Refactor `useCommandMenuItemsFromBackend`
- Prefill a _Quick Lead_ workflow command menu item during workspace
setup and dev seeding
- Add a ready prop to `HeadlessEngineCommandWrapperEffect` to prevent
premature execution when async data hasn't loaded yet

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Raphaël Bosi
2026-03-22 23:52:43 +01:00
committed by GitHub
parent d16b94bde6
commit c107d804d2
29 changed files with 1277 additions and 860 deletions
@@ -2,6 +2,8 @@ import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { CommandMenuItemModule } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
@@ -43,6 +45,8 @@ import { WorkflowVersionValidationWorkspaceService } from 'src/modules/workflow/
WorkspaceManyOrAllFlatEntityMapsCacheModule,
ObjectMetadataModule,
CodeStepBuildModule,
CommandMenuItemModule,
FeatureFlagModule,
],
providers: [
WorkflowCreateOnePreQueryHook,
@@ -1,5 +1,7 @@
import { Module } from '@nestjs/common';
import { CommandMenuItemModule } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { WorkflowQueryHookModule } from 'src/modules/workflow/common/query-hooks/workflow-query-hook.module';
@@ -10,6 +12,8 @@ import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/work
WorkflowQueryHookModule,
LogicFunctionModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
CommandMenuItemModule,
FeatureFlagModule,
],
providers: [WorkflowCommonWorkspaceService],
exports: [WorkflowCommonWorkspaceService],
@@ -1,7 +1,10 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { FeatureFlagKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { CommandMenuItemService } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.service';
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 FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
@@ -40,10 +43,14 @@ export type ObjectMetadataInfo = {
@Injectable()
export class WorkflowCommonWorkspaceService {
private readonly logger = new Logger(WorkflowCommonWorkspaceService.name);
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly logicFunctionFromSourceService: LogicFunctionFromSourceService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly commandMenuItemService: CommandMenuItemService,
private readonly featureFlagService: FeatureFlagService,
) {}
async getWorkflowVersionOrFail({
@@ -262,38 +269,100 @@ export class WorkflowCommonWorkspaceService {
{ shouldBypassPermissionChecks: true },
);
const workflow = await workflowRepository.findOne({
where: { id: workflowId },
withDeleted: true,
});
if (workflow?.statuses?.includes(WorkflowStatus.ACTIVE)) {
const newStatuses = [
...workflow.statuses.filter(
(status) => status !== WorkflowStatus.ACTIVE,
),
WorkflowStatus.DEACTIVATED,
];
await workflowRepository.update(workflowId, {
statuses: newStatuses,
});
}
const workflowVersions = await workflowVersionRepository.find({
where: {
workflowId,
},
where: { workflowId },
withDeleted: true,
});
for (const workflowVersion of workflowVersions) {
if (workflowVersion.status === WorkflowVersionStatus.ACTIVE) {
await workflowVersionRepository.update(workflowVersion.id, {
status: WorkflowVersionStatus.DEACTIVATED,
});
await this.cleanupCommandMenuItemForVersion(
workflowVersion.id,
workspaceId,
);
}
}
const workspaceDataSource =
await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource();
const queryRunner = workspaceDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const workflow = await workflowRepository.findOne(
{
where: { id: workflowId },
withDeleted: true,
},
queryRunner.manager,
);
if (workflow?.statuses?.includes(WorkflowStatus.ACTIVE)) {
const newStatuses = [
...workflow.statuses.filter(
(status) => status !== WorkflowStatus.ACTIVE,
),
WorkflowStatus.DEACTIVATED,
];
await workflowRepository.update(
workflowId,
{ statuses: newStatuses },
queryRunner.manager,
);
}
for (const workflowVersion of workflowVersions) {
if (workflowVersion.status === WorkflowVersionStatus.ACTIVE) {
await workflowVersionRepository.update(
workflowVersion.id,
{ status: WorkflowVersionStatus.DEACTIVATED },
queryRunner.manager,
);
}
}
await queryRunner.commitTransaction();
} catch (error) {
if (queryRunner.isTransactionActive) {
await queryRunner.rollbackTransaction();
}
throw error;
} finally {
await queryRunner.release();
}
}
private async cleanupCommandMenuItemForVersion(
workflowVersionId: string,
workspaceId: string,
) {
const isCommandMenuItemEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
workspaceId,
);
if (!isCommandMenuItemEnabled) {
return;
}
const existingCommandMenuItem =
await this.commandMenuItemService.findByWorkflowVersionId(
workflowVersionId,
workspaceId,
);
if (isDefined(existingCommandMenuItem)) {
await this.commandMenuItemService.delete(
existingCommandMenuItem.id,
workspaceId,
);
}
}
async handleLogicFunctionSubEntities({