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:
+1
@@ -10,4 +10,5 @@ export const FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES = [
|
||||
'availabilityType',
|
||||
'availabilityObjectMetadataId',
|
||||
'engineComponentKey',
|
||||
'pageLayoutId',
|
||||
] as const satisfies MetadataEntityPropertyName<'commandMenuItem'>[];
|
||||
|
||||
+2
@@ -7,6 +7,7 @@ import { WorkspaceFlatCommandMenuItemMapCacheService } from 'src/engine/metadata
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
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';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -15,6 +16,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
|
||||
ApplicationEntity,
|
||||
ObjectMetadataEntity,
|
||||
FrontComponentEntity,
|
||||
PageLayoutEntity,
|
||||
]),
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
],
|
||||
|
||||
+38
-22
@@ -12,6 +12,7 @@ import { fromCommandMenuItemEntityToFlatCommandMenuItem } from 'src/engine/metad
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
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 { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
|
||||
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
|
||||
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
|
||||
@@ -28,33 +29,45 @@ export class WorkspaceFlatCommandMenuItemMapCacheService extends WorkspaceCacheP
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectRepository(FrontComponentEntity)
|
||||
private readonly frontComponentRepository: Repository<FrontComponentEntity>,
|
||||
@InjectRepository(PageLayoutEntity)
|
||||
private readonly pageLayoutRepository: Repository<PageLayoutEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(workspaceId: string): Promise<FlatCommandMenuItemMaps> {
|
||||
const [commandMenuItems, applications, objectMetadatas, frontComponents] =
|
||||
await Promise.all([
|
||||
this.commandMenuItemRepository.find({
|
||||
where: { workspaceId },
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.applicationRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.objectMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.frontComponentRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
]);
|
||||
const [
|
||||
commandMenuItems,
|
||||
applications,
|
||||
objectMetadatas,
|
||||
frontComponents,
|
||||
pageLayouts,
|
||||
] = await Promise.all([
|
||||
this.commandMenuItemRepository.find({
|
||||
where: { workspaceId },
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.applicationRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.objectMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.frontComponentRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.pageLayoutRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const applicationIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(applications);
|
||||
@@ -62,6 +75,8 @@ export class WorkspaceFlatCommandMenuItemMapCacheService extends WorkspaceCacheP
|
||||
createIdToUniversalIdentifierMap(objectMetadatas);
|
||||
const frontComponentIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(frontComponents);
|
||||
const pageLayoutIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(pageLayouts);
|
||||
|
||||
const flatCommandMenuItemMaps = createEmptyFlatEntityMaps();
|
||||
|
||||
@@ -72,6 +87,7 @@ export class WorkspaceFlatCommandMenuItemMapCacheService extends WorkspaceCacheP
|
||||
applicationIdToUniversalIdentifierMap,
|
||||
objectMetadataIdToUniversalIdentifierMap,
|
||||
frontComponentIdToUniversalIdentifierMap,
|
||||
pageLayoutIdToUniversalIdentifierMap,
|
||||
});
|
||||
|
||||
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
|
||||
|
||||
+2
@@ -65,6 +65,8 @@ export const buildNavigationFlatCommandMenuItem = ({
|
||||
workflowVersionId: null,
|
||||
availabilityObjectMetadataId: null,
|
||||
availabilityObjectMetadataUniversalIdentifier: null,
|
||||
pageLayoutId: null,
|
||||
pageLayoutUniversalIdentifier: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
+19
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+11
-2
@@ -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,
|
||||
|
||||
+1
@@ -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),
|
||||
|
||||
+16
-1
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user