Fix Quick Lead command menu item not appearing (#19635)

- Refactored prefillWorkflowCommandMenuItems and
prefillFrontComponentCommandMenuItems to use
validateBuildAndRunWorkspaceMigration instead of raw TypeORM
createQueryBuilder inserts
- This ensures the flat entity cache is properly updated when seeding
command menu items, fixing the Quick Lead item not appearing after
workspace creation
- Moved command menu item prefill calls outside the transaction since
they now go through the migration pipeline
This commit is contained in:
Raphaël Bosi
2026-04-14 11:30:31 +02:00
committed by GitHub
parent 3e699c4458
commit eb13378760
6 changed files with 223 additions and 113 deletions
@@ -1,60 +1,82 @@
import { type EntityManager } from 'typeorm';
import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared/utils';
import { type ApplicationService } from 'src/engine/core-modules/application/application.service';
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import { type WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import {
getSeedFrontComponentCommandMenuItemDefinitions,
getSeedFrontComponentIds,
} from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-front-component-definitions.util';
import { type WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
export const prefillFrontComponentCommandMenuItems = async (
entityManager: EntityManager,
workspaceId: string,
) => {
export const prefillFrontComponentCommandMenuItems = async ({
workspaceId,
applicationService,
flatEntityMapsCacheService,
workspaceMigrationValidateBuildAndRunService,
}: {
workspaceId: string;
applicationService: ApplicationService;
flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService;
workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService;
}): Promise<void> => {
const { helloWorldId } = getSeedFrontComponentIds(workspaceId);
const frontComponentRow = await entityManager
.createQueryBuilder()
.select('fc.applicationId', 'applicationId')
.from('core.frontComponent', 'fc')
.where('fc.id = :id', { id: helloWorldId })
.andWhere('fc.workspaceId = :workspaceId', { workspaceId })
.getRawOne();
const { flatCommandMenuItemMaps, flatFrontComponentMaps } =
await flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps({
workspaceId,
flatMapsKeys: ['flatCommandMenuItemMaps', 'flatFrontComponentMaps'],
});
if (!frontComponentRow) {
const frontComponent = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: helloWorldId,
flatEntityMaps: flatFrontComponentMaps,
});
if (!isDefined(frontComponent)) {
return;
}
const { workspaceCustomFlatApplication } =
await applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const definitions =
getSeedFrontComponentCommandMenuItemDefinitions(workspaceId);
await entityManager
.createQueryBuilder()
.insert()
.into('core.commandMenuItem', [
'workspaceId',
'universalIdentifier',
'applicationId',
'workflowVersionId',
'frontComponentId',
'engineComponentKey',
'label',
'icon',
'shortLabel',
'position',
'isPinned',
'availabilityType',
'conditionalAvailabilityExpression',
'availabilityObjectMetadataId',
'hotKeys',
])
.values(
definitions.map((definition) => ({
workspaceId,
const now = new Date().toISOString();
const flatCommandMenuItemsToCreate: FlatCommandMenuItem[] = definitions
.filter(
(definition) =>
!isDefined(
flatCommandMenuItemMaps.byUniversalIdentifier[
definition.universalIdentifier
],
),
)
.map((definition) => {
const definitionFrontComponent = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: definition.frontComponentId,
flatEntityMaps: flatFrontComponentMaps,
});
return {
id: v4(),
universalIdentifier: definition.universalIdentifier,
applicationId: frontComponentRow.applicationId,
applicationId: frontComponent.applicationId,
applicationUniversalIdentifier:
frontComponent.applicationUniversalIdentifier,
workspaceId,
workflowVersionId: null,
frontComponentId: definition.frontComponentId,
frontComponentUniversalIdentifier:
definitionFrontComponent?.universalIdentifier ?? null,
engineComponentKey: EngineComponentKey.FRONT_COMPONENT_RENDERER,
label: definition.label,
icon: definition.icon,
@@ -64,9 +86,37 @@ export const prefillFrontComponentCommandMenuItems = async (
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
availabilityObjectMetadataId: null,
availabilityObjectMetadataUniversalIdentifier: null,
payload: null,
hotKeys: null,
})),
)
.orIgnore()
.execute();
createdAt: now,
updatedAt: now,
};
});
if (flatCommandMenuItemsToCreate.length === 0) {
return;
}
const result =
await workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
flatEntityToCreate: flatCommandMenuItemsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (result.status === 'fail') {
throw new Error(
`Failed to create front component command menu items for workspace ${workspaceId}: ${JSON.stringify(result, null, 2)}`,
);
}
};
@@ -1,70 +1,96 @@
import { type EntityManager } from 'typeorm';
import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared/utils';
import { type ApplicationService } from 'src/engine/core-modules/application/application.service';
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import { type WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { QUICK_LEAD_WORKFLOW_VERSION_ID } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
import { type WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
const QUICK_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER =
'a1b2c3d4-e5f6-7890-abcd-1234567890ab';
'5b389a80-345f-42b5-83fa-2e6b6ad95f01';
export const prefillWorkflowCommandMenuItems = async (
entityManager: EntityManager,
workspaceId: string,
) => {
const applicationRow = await entityManager
.createQueryBuilder()
.select('app.id')
.from('core.application', 'app')
.where('app.universalIdentifier = :universalIdentifier', {
universalIdentifier: TWENTY_STANDARD_APPLICATION.universalIdentifier,
})
.andWhere('app.workspaceId = :workspaceId', { workspaceId })
.getRawOne();
export const prefillWorkflowCommandMenuItems = async ({
workspaceId,
applicationService,
flatEntityMapsCacheService,
workspaceMigrationValidateBuildAndRunService,
}: {
workspaceId: string;
applicationService: ApplicationService;
flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService;
workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService;
}): Promise<void> => {
const { workspaceCustomFlatApplication } =
await applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
if (!applicationRow) {
const { flatCommandMenuItemMaps } =
await flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps({
workspaceId,
flatMapsKeys: ['flatCommandMenuItemMaps'],
});
const alreadyExists = isDefined(
flatCommandMenuItemMaps.byUniversalIdentifier[
QUICK_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER
],
);
if (alreadyExists) {
return;
}
await entityManager
.createQueryBuilder()
.insert()
.into('core.commandMenuItem', [
'workspaceId',
'universalIdentifier',
'applicationId',
'workflowVersionId',
'frontComponentId',
'engineComponentKey',
'label',
'icon',
'shortLabel',
'position',
'isPinned',
'availabilityType',
'conditionalAvailabilityExpression',
'availabilityObjectMetadataId',
'hotKeys',
])
.values([
const now = new Date().toISOString();
const quickLeadFlatCommandMenuItem: FlatCommandMenuItem = {
id: v4(),
universalIdentifier: QUICK_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER,
applicationId: workspaceCustomFlatApplication.id,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
workspaceId,
workflowVersionId: QUICK_LEAD_WORKFLOW_VERSION_ID,
frontComponentId: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION,
label: 'Quick Lead',
icon: 'IconUserPlus',
shortLabel: 'Quick Lead',
position: 100,
isPinned: false,
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
availabilityObjectMetadataId: null,
availabilityObjectMetadataUniversalIdentifier: null,
payload: null,
hotKeys: null,
createdAt: now,
updatedAt: now,
};
const result =
await workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
flatEntityToCreate: [quickLeadFlatCommandMenuItem],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
universalIdentifier: QUICK_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER,
applicationId: applicationRow.app_id,
workflowVersionId: QUICK_LEAD_WORKFLOW_VERSION_ID,
frontComponentId: null,
engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION,
label: 'Quick Lead',
icon: 'IconUserPlus',
shortLabel: 'Quick Lead',
position: 100,
isPinned: false,
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: null,
availabilityObjectMetadataId: null,
hotKeys: null,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
])
.orIgnore()
.execute();
);
if (result.status === 'fail') {
throw new Error(
`Failed to create Quick Lead command menu item for workspace ${workspaceId}: ${JSON.stringify(result, null, 2)}`,
);
}
};