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
@@ -5,6 +5,7 @@ import {
WorkspaceMigrationV2ExceptionCode,
} from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { type QueryRunner } from 'typeorm';
import { FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -358,12 +359,17 @@ export class WorkspaceMigrationValidateBuildAndRunService {
public async validateBuildAndRunWorkspaceMigrationFromTo(
args: WorkspaceMigrationOrchestratorBuildArgs & {
idByUniversalIdentifierByMetadataName?: IdByUniversalIdentifierByMetadataName;
queryRunner?: QueryRunner;
},
): Promise<
| WorkspaceMigrationOrchestratorFailedResult
| WorkspaceMigrationOrchestratorSuccessfulResult
> {
const { idByUniversalIdentifierByMetadataName, ...buildArgs } = args;
const {
idByUniversalIdentifierByMetadataName,
queryRunner: externalQueryRunner,
...buildArgs
} = args;
const validateAndBuildResult =
await this.workspaceMigrationBuildOrchestratorService
@@ -396,6 +402,7 @@ export class WorkspaceMigrationValidateBuildAndRunService {
{
workspaceId: args.workspaceId,
workspaceMigration,
queryRunner: externalQueryRunner,
},
);
@@ -416,7 +423,10 @@ export class WorkspaceMigrationValidateBuildAndRunService {
workspaceId,
isSystemBuild = false,
applicationUniversalIdentifier,
}: ValidateBuildAndRunWorkspaceMigrationFromMatriceArgs): Promise<
queryRunner,
}: ValidateBuildAndRunWorkspaceMigrationFromMatriceArgs & {
queryRunner?: QueryRunner;
}): Promise<
| WorkspaceMigrationOrchestratorFailedResult
| WorkspaceMigrationOrchestratorSuccessfulResult
> {
@@ -443,6 +453,7 @@ export class WorkspaceMigrationValidateBuildAndRunService {
dependencyAllFlatEntityMaps,
additionalCacheDataMaps,
idByUniversalIdentifierByMetadataName,
queryRunner,
});
}
}
@@ -3,7 +3,7 @@ import { InjectDataSource } from '@nestjs/typeorm';
import { type AllMetadataName } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { DataSource } from 'typeorm';
import { DataSource, type QueryRunner } from 'typeorm';
import { LoggerService } from 'src/engine/core-modules/logger/logger.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
@@ -154,9 +154,11 @@ export class WorkspaceMigrationRunnerService {
run = async ({
workspaceMigration: { actions, applicationUniversalIdentifier },
workspaceId,
queryRunner: externalQueryRunner,
}: {
workspaceMigration: WorkspaceMigration;
workspaceId: string;
queryRunner?: QueryRunner;
}): Promise<{
allFlatEntityMaps: AllFlatEntityMaps;
metadataEvents: MetadataEvent[];
@@ -164,7 +166,10 @@ export class WorkspaceMigrationRunnerService {
this.logger.time('Runner', 'Total execution');
this.logger.time('Runner', 'Initial cache retrieval');
const queryRunner = this.coreDataSource.createQueryRunner();
const queryRunner =
externalQueryRunner ?? this.coreDataSource.createQueryRunner();
const isTransactionAlreadyActive = queryRunner.isTransactionActive;
const actionMetadataNames = [
...new Set(actions.flatMap((action) => action.metadataName)),
];
@@ -214,10 +219,12 @@ export class WorkspaceMigrationRunnerService {
this.logger.time('Runner', 'Transaction execution');
try {
if (!isTransactionAlreadyActive) {
await queryRunner.connect();
await queryRunner.startTransaction();
}
try {
const allMetadataEvents: MetadataEvent[] = [];
for (const action of actions) {
@@ -243,7 +250,9 @@ export class WorkspaceMigrationRunnerService {
allMetadataEvents.push(...metadataEvents);
}
await queryRunner.commitTransaction();
if (!isTransactionAlreadyActive) {
await queryRunner.commitTransaction();
}
this.logger.timeEnd('Runner', 'Transaction execution');
@@ -256,10 +265,12 @@ export class WorkspaceMigrationRunnerService {
return { allFlatEntityMaps, metadataEvents: allMetadataEvents };
} catch (error) {
if (queryRunner.isTransactionActive) {
await queryRunner.rollbackTransaction().catch((error) =>
if (!isTransactionAlreadyActive && queryRunner.isTransactionActive) {
await queryRunner.rollbackTransaction().catch((rollbackError) =>
// oxlint-disable-next-line no-console
console.trace(`Failed to rollback transaction: ${error.message}`),
console.trace(
`Failed to rollback transaction: ${rollbackError.message}`,
),
);
}
@@ -288,7 +299,9 @@ export class WorkspaceMigrationRunnerService {
code: WorkspaceMigrationRunnerExceptionCode.INTERNAL_SERVER_ERROR,
});
} finally {
await queryRunner.release();
if (!isTransactionAlreadyActive) {
await queryRunner.release();
}
}
};
}