feat: CommandMenuItem entity and FrontComponent support in command menu (#17555)

https://github.com/user-attachments/assets/6f172ffc-d43d-42fd-a26b-94f591fb767e
This commit is contained in:
Abdul Rahman
2026-01-30 22:05:06 +05:30
committed by GitHub
parent 6462ff6c5f
commit 51a2a90b0a
45 changed files with 899 additions and 34 deletions
@@ -0,0 +1,7 @@
export const FRONT_COMPONENT_SEEDS = {
DEMO_APP: 'DEMO_APP',
} as const;
export const COMMAND_MENU_ITEM_SEEDS = {
DEMO_FRONT_COMPONENT: 'DEMO_FRONT_COMPONENT',
} as const;
@@ -0,0 +1,76 @@
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import {
COMMAND_MENU_ITEM_SEEDS,
FRONT_COMPONENT_SEEDS,
} from 'src/engine/workspace-manager/dev-seeder/core/constants/front-component-and-command-menu-item-seeds.constant';
import { generateSeedId } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-seed-id.util';
type FrontComponentSeed = {
id: string;
name: string;
workspaceId: string;
universalIdentifier: string;
applicationId: string;
};
type CommandMenuItemSeed = {
id: string;
workspaceId: string;
universalIdentifier: string;
applicationId: string;
workflowVersionId: null;
frontComponentId: string;
label: string;
icon: string | null;
isPinned: boolean;
availabilityType: CommandMenuItemAvailabilityType;
availabilityObjectMetadataId: null;
};
type FrontComponentAndCommandMenuItemSeeds = {
frontComponents: FrontComponentSeed[];
commandMenuItems: CommandMenuItemSeed[];
};
export const getFrontComponentAndCommandMenuItemDataSeeds = (
workspaceId: string,
applicationId: string,
): FrontComponentAndCommandMenuItemSeeds => {
const frontComponentId = generateSeedId(
workspaceId,
FRONT_COMPONENT_SEEDS.DEMO_APP,
);
const frontComponents: FrontComponentSeed[] = [
{
id: frontComponentId,
name: 'Demo App',
workspaceId,
universalIdentifier: frontComponentId,
applicationId,
},
];
const commandMenuItemId = generateSeedId(
workspaceId,
COMMAND_MENU_ITEM_SEEDS.DEMO_FRONT_COMPONENT,
);
const commandMenuItems: CommandMenuItemSeed[] = [
{
id: commandMenuItemId,
workspaceId,
universalIdentifier: commandMenuItemId,
applicationId,
workflowVersionId: null,
frontComponentId,
label: 'Open Demo App',
icon: 'IconApps',
isPinned: false,
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
availabilityObjectMetadataId: null,
},
];
return { frontComponents, commandMenuItems };
};
@@ -91,6 +91,11 @@ export const seedFeatureFlags = async ({
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
workspaceId: workspaceId,
value: true,
},
])
.execute();
};
@@ -0,0 +1,80 @@
import { type DataSource } from 'typeorm';
import { getFrontComponentAndCommandMenuItemDataSeeds } from 'src/engine/workspace-manager/dev-seeder/core/utils/get-front-component-and-command-menu-item-data-seeds.util';
type SeedFrontComponentsAndCommandMenuItemsArgs = {
dataSource: DataSource;
schemaName: string;
workspaceId: string;
applicationId: string;
};
export const seedFrontComponentsAndCommandMenuItems = async ({
dataSource,
schemaName,
workspaceId,
applicationId,
}: SeedFrontComponentsAndCommandMenuItemsArgs) => {
const { frontComponents, commandMenuItems } =
getFrontComponentAndCommandMenuItemDataSeeds(workspaceId, applicationId);
if (frontComponents.length > 0) {
await dataSource
.createQueryBuilder()
.insert()
.into(`${schemaName}.frontComponent`, [
'id',
'name',
'workspaceId',
'universalIdentifier',
'applicationId',
])
.values(
frontComponents.map((row) => ({
id: row.id,
name: row.name,
workspaceId: row.workspaceId,
universalIdentifier: row.universalIdentifier,
applicationId: row.applicationId,
})),
)
.orIgnore()
.execute();
}
if (commandMenuItems.length > 0) {
await dataSource
.createQueryBuilder()
.insert()
.into(`${schemaName}.commandMenuItem`, [
'id',
'workspaceId',
'universalIdentifier',
'applicationId',
'workflowVersionId',
'frontComponentId',
'label',
'icon',
'isPinned',
'availabilityType',
'availabilityObjectMetadataId',
])
.values(
commandMenuItems.map((row) => ({
id: row.id,
workspaceId: row.workspaceId,
universalIdentifier: row.universalIdentifier,
applicationId: row.applicationId,
workflowVersionId: row.workflowVersionId,
frontComponentId: row.frontComponentId,
label: row.label,
icon: row.icon,
isPinned: row.isPinned,
availabilityType: row.availabilityType,
availabilityObjectMetadataId: row.availabilityObjectMetadataId,
})),
)
.orIgnore()
.execute();
}
};
@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { DataSource } from 'typeorm';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
@@ -17,6 +17,7 @@ import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/work
import { SeededWorkspacesIds } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
import { DevSeederPermissionsService } from 'src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service';
import { seedCoreSchema } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-core-schema.util';
import { seedFrontComponentsAndCommandMenuItems } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-front-components-and-command-menu-items.util';
import { seedPageLayoutTabs } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-tabs.util';
import { seedPageLayoutWidgets } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-widgets.util';
import { seedPageLayouts } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layouts.util';
@@ -145,6 +146,23 @@ export class DevSeederService {
featureFlags: featureFlagsMap,
});
await seedFrontComponentsAndCommandMenuItems({
dataSource: this.coreDataSource,
schemaName: 'core',
workspaceId,
applicationId: workspaceCustomFlatApplication.id,
});
const relatedCommandMenuItemAndFrontComponentCacheKeysToInvalidate = [
...getMetadataRelatedMetadataNames(ALL_METADATA_NAME.commandMenuItem),
...getMetadataRelatedMetadataNames(ALL_METADATA_NAME.frontComponent),
].map(getMetadataFlatEntityMapsKey);
await this.workspaceCacheService.invalidateAndRecompute(
workspaceId,
relatedCommandMenuItemAndFrontComponentCacheKeysToInvalidate,
);
await this.workspaceCacheStorageService.flush(workspaceId, undefined);
}
}
@@ -19,6 +19,7 @@ import { WorkspaceMigrationCommandMenuItemActionsBuilderService } from 'src/engi
import { WorkspaceMigrationFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service';
import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/workspace-migration-front-component-actions-builder.service';
import { WorkspaceMigrationIndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/index/workspace-migration-index-actions-builder.service';
import { WorkspaceMigrationLogicFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/workspace-migration-logic-function-actions-builder.service';
import { WorkspaceMigrationNavigationMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/workspace-migration-navigation-menu-item-actions-builder.service';
import { WorkspaceMigrationObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/workspace-migration-object-actions-builder.service';
import { WorkspaceMigrationPageLayoutTabActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-tab/workspace-migration-page-layout-tab-actions-builder.service';
@@ -28,7 +29,6 @@ import { WorkspaceMigrationRoleTargetActionsBuilderService } from 'src/engine/wo
import { WorkspaceMigrationRoleActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role/workspace-migration-role-actions-builder.service';
import { WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate-group/workspace-migration-row-level-permission-predicate-group-actions-builder.service';
import { WorkspaceMigrationRowLevelPermissionPredicateActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate/workspace-migration-row-level-permission-predicate-actions-builder.service';
import { WorkspaceMigrationLogicFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/workspace-migration-logic-function-actions-builder.service';
import { WorkspaceMigrationSkillActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/skill/workspace-migration-skill-actions-builder.service';
import { WorkspaceMigrationViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/workspace-migration-view-field-actions-builder.service';
import { WorkspaceMigrationViewFilterGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter-group/workspace-migration-view-filter-group-actions-builder.service';
@@ -721,6 +721,8 @@ export class WorkspaceMigrationBuildOrchestratorService {
dependencyOptimisticFlatEntityMaps: {
flatObjectMetadataMaps:
optimisticAllFlatEntityMaps.flatObjectMetadataMaps,
flatFrontComponentMaps:
optimisticAllFlatEntityMaps.flatFrontComponentMaps,
},
workspaceId,
},
@@ -37,6 +37,19 @@ export class FlatCommandMenuItemValidatorService {
});
}
const hasWorkflowVersionId = isDefined(
flatCommandMenuItem.workflowVersionId,
);
const hasFrontComponentId = isDefined(flatCommandMenuItem.frontComponentId);
if (hasWorkflowVersionId === hasFrontComponentId) {
validationResult.errors.push({
code: CommandMenuItemExceptionCode.WORKFLOW_OR_FRONT_COMPONENT_REQUIRED,
message: t`Exactly one of workflowVersionId or frontComponentId is required`,
userFriendlyMessage: msg`Exactly one of workflow version or front component is required`,
});
}
return validationResult;
}