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
@@ -65,6 +65,8 @@ export const buildNavigationFlatCommandMenuItem = ({
workflowVersionId: null,
availabilityObjectMetadataId: null,
availabilityObjectMetadataUniversalIdentifier: null,
pageLayoutId: null,
pageLayoutUniversalIdentifier: null,
createdAt: now,
updatedAt: now,
};
@@ -12,6 +12,7 @@ export const fromCommandMenuItemEntityToFlatCommandMenuItem = ({
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
frontComponentIdToUniversalIdentifierMap,
pageLayoutIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'commandMenuItem'>): FlatCommandMenuItem => {
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
@@ -57,6 +58,22 @@ export const fromCommandMenuItemEntityToFlatCommandMenuItem = ({
}
}
let pageLayoutUniversalIdentifier: string | null = null;
if (isDefined(commandMenuItemEntity.pageLayoutId)) {
pageLayoutUniversalIdentifier =
pageLayoutIdToUniversalIdentifierMap.get(
commandMenuItemEntity.pageLayoutId,
) ?? null;
if (!isDefined(pageLayoutUniversalIdentifier)) {
throw new FlatEntityMapsException(
`PageLayout with id ${commandMenuItemEntity.pageLayoutId} not found for commandMenuItem ${commandMenuItemEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
return {
id: commandMenuItemEntity.id,
workflowVersionId: commandMenuItemEntity.workflowVersionId,
@@ -82,5 +99,7 @@ export const fromCommandMenuItemEntityToFlatCommandMenuItem = ({
commandMenuItemEntity.conditionalAvailabilityExpression,
availabilityObjectMetadataUniversalIdentifier,
frontComponentUniversalIdentifier,
pageLayoutId: commandMenuItemEntity.pageLayoutId,
pageLayoutUniversalIdentifier,
};
};
@@ -14,13 +14,14 @@ export const fromCreateCommandMenuItemInputToFlatCommandMenuItemToCreate = ({
flatApplication,
flatObjectMetadataMaps,
flatFrontComponentMaps,
flatPageLayoutMaps,
}: {
createCommandMenuItemInput: CreateCommandMenuItemInput;
workspaceId: string;
flatApplication: FlatApplication;
} & Pick<
AllFlatEntityMaps,
'flatObjectMetadataMaps' | 'flatFrontComponentMaps'
'flatObjectMetadataMaps' | 'flatFrontComponentMaps' | 'flatPageLayoutMaps'
>): FlatCommandMenuItem => {
const id = uuidv4();
const now = new Date().toISOString();
@@ -28,14 +29,20 @@ export const fromCreateCommandMenuItemInputToFlatCommandMenuItemToCreate = ({
const {
availabilityObjectMetadataUniversalIdentifier,
frontComponentUniversalIdentifier,
pageLayoutUniversalIdentifier,
} = resolveEntityRelationUniversalIdentifiers({
metadataName: 'commandMenuItem',
foreignKeyValues: {
availabilityObjectMetadataId:
createCommandMenuItemInput.availabilityObjectMetadataId,
frontComponentId: createCommandMenuItemInput.frontComponentId,
pageLayoutId: createCommandMenuItemInput.pageLayoutId,
},
flatEntityMaps: {
flatObjectMetadataMaps,
flatFrontComponentMaps,
flatPageLayoutMaps,
},
flatEntityMaps: { flatObjectMetadataMaps, flatFrontComponentMaps },
});
return {
@@ -64,6 +71,8 @@ export const fromCreateCommandMenuItemInputToFlatCommandMenuItemToCreate = ({
conditionalAvailabilityExpression:
createCommandMenuItemInput.conditionalAvailabilityExpression ?? null,
availabilityObjectMetadataUniversalIdentifier,
pageLayoutId: createCommandMenuItemInput.pageLayoutId ?? null,
pageLayoutUniversalIdentifier,
workspaceId,
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
@@ -20,6 +20,7 @@ export const fromFlatCommandMenuItemToCommandMenuItemDto = (
flatCommandMenuItem.conditionalAvailabilityExpression ?? undefined,
availabilityObjectMetadataId:
flatCommandMenuItem.availabilityObjectMetadataId ?? undefined,
pageLayoutId: flatCommandMenuItem.pageLayoutId ?? undefined,
workspaceId: flatCommandMenuItem.workspaceId,
applicationId: flatCommandMenuItem.applicationId ?? undefined,
createdAt: new Date(flatCommandMenuItem.createdAt),
@@ -18,12 +18,13 @@ export const fromUpdateCommandMenuItemInputToFlatCommandMenuItemToUpdateOrThrow
flatCommandMenuItemMaps,
updateCommandMenuItemInput,
flatObjectMetadataMaps,
flatPageLayoutMaps,
}: {
flatCommandMenuItemMaps: FlatCommandMenuItemMaps;
updateCommandMenuItemInput: UpdateCommandMenuItemInput;
} & Pick<
AllFlatEntityMaps,
'flatObjectMetadataMaps'
'flatObjectMetadataMaps' | 'flatPageLayoutMaps'
>): FlatCommandMenuItem => {
const existingFlatCommandMenuItem = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: updateCommandMenuItemInput.id,
@@ -63,5 +64,19 @@ export const fromUpdateCommandMenuItemInputToFlatCommandMenuItemToUpdateOrThrow
availabilityObjectMetadataUniversalIdentifier;
}
if (updates.pageLayoutId !== undefined) {
const { pageLayoutUniversalIdentifier } =
resolveEntityRelationUniversalIdentifiers({
metadataName: 'commandMenuItem',
foreignKeyValues: {
pageLayoutId: flatCommandMenuItemToUpdate.pageLayoutId,
},
flatEntityMaps: { flatPageLayoutMaps },
});
flatCommandMenuItemToUpdate.pageLayoutUniversalIdentifier =
pageLayoutUniversalIdentifier;
}
return flatCommandMenuItemToUpdate;
};