Link command menu items to specific page layout (#19706)

- Add a `pageLayoutId` foreign key to `CommandMenuItem`, allowing
command menu items to be scoped to a specific page layout instead of
being globally available
- Filter command menu items by the current page layout on the frontend.
Items with a `pageLayoutId` only appear when viewing that layout, while
items without one remain globally visible
- Create an effect to track the current page layout ID
- Include a seed example: a "Show Notification" command pinned to the
Star history standalone page layout

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Raphaël Bosi
2026-04-16 14:07:36 +02:00
committed by GitHub
parent 7af82fb6a4
commit 2b5b8a8b13
39 changed files with 342 additions and 51 deletions
@@ -97,11 +97,19 @@ export class CommandMenuItemService {
input: CreateCommandMenuItemInput,
workspaceId: string,
): Promise<CommandMenuItemDTO> {
const { flatObjectMetadataMaps, flatFrontComponentMaps } =
const {
flatObjectMetadataMaps,
flatFrontComponentMaps,
flatPageLayoutMaps,
} =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFrontComponentMaps'],
flatMapsKeys: [
'flatObjectMetadataMaps',
'flatFrontComponentMaps',
'flatPageLayoutMaps',
],
},
);
@@ -117,6 +125,7 @@ export class CommandMenuItemService {
flatApplication: workspaceCustomFlatApplication,
flatObjectMetadataMaps,
flatFrontComponentMaps,
flatPageLayoutMaps,
});
const validateAndBuildResult =
@@ -171,19 +180,24 @@ export class CommandMenuItemService {
const {
flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
} =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatCommandMenuItemMaps', 'flatObjectMetadataMaps'],
},
);
flatPageLayoutMaps: existingFlatPageLayoutMaps,
} = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatCommandMenuItemMaps',
'flatObjectMetadataMaps',
'flatPageLayoutMaps',
],
},
);
const flatCommandMenuItemToUpdate =
fromUpdateCommandMenuItemInputToFlatCommandMenuItemToUpdateOrThrow({
flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps,
updateCommandMenuItemInput: input,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
flatPageLayoutMaps: existingFlatPageLayoutMaps,
});
const validateAndBuildResult =
@@ -91,6 +91,11 @@ export class CommandMenuItemDTO {
@Field(() => UUIDScalarType, { nullable: true })
availabilityObjectMetadataId?: string;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
pageLayoutId?: string;
@HideField()
workspaceId: string;
@@ -83,4 +83,9 @@ export class CreateCommandMenuItemInput {
@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
payload?: CommandMenuItemPayload;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
pageLayoutId?: string;
}
@@ -65,4 +65,9 @@ export class UpdateCommandMenuItemInput {
@IsOptional()
@Field(() => [String], { nullable: true })
hotKeys?: string[];
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
pageLayoutId?: string;
}
@@ -16,6 +16,7 @@ import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/com
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
@Entity({ name: 'commandMenuItem', schema: 'core' })
@@ -30,6 +31,10 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
@Index('IDX_COMMAND_MENU_ITEM_AVAILABILITY_OBJECT_METADATA_ID', [
'availabilityObjectMetadataId',
])
@Index('IDX_COMMAND_MENU_ITEM_PAGE_LAYOUT_ID_WORKSPACE_ID', [
'pageLayoutId',
'workspaceId',
])
@Check(
'CHK_CMD_MENU_ITEM_ENGINE_KEY_COHERENCE',
`("engineComponentKey" = 'TRIGGER_WORKFLOW_VERSION' AND "workflowVersionId" IS NOT NULL AND "frontComponentId" IS NULL AND "payload" IS NULL) OR ("engineComponentKey" = 'FRONT_COMPONENT_RENDERER' AND "frontComponentId" IS NOT NULL AND "workflowVersionId" IS NULL AND "payload" IS NULL) OR ("engineComponentKey" = 'NAVIGATION' AND "payload" IS NOT NULL AND "workflowVersionId" IS NULL AND "frontComponentId" IS NULL) OR ("engineComponentKey" NOT IN ('TRIGGER_WORKFLOW_VERSION', 'FRONT_COMPONENT_RENDERER', 'NAVIGATION') AND "workflowVersionId" IS NULL AND "frontComponentId" IS NULL AND "payload" IS NULL)`,
@@ -99,6 +104,16 @@ export class CommandMenuItemEntity
@JoinColumn({ name: 'availabilityObjectMetadataId' })
availabilityObjectMetadata: Relation<ObjectMetadataEntity> | null;
@Column({ nullable: true, type: 'uuid' })
pageLayoutId: string | null;
@ManyToOne(() => PageLayoutEntity, {
onDelete: 'CASCADE',
nullable: true,
})
@JoinColumn({ name: 'pageLayoutId' })
pageLayout: Relation<PageLayoutEntity> | null;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;