From c15536f9f8f2f84eb0b905b69dc732322022fb84 Mon Sep 17 00:00:00 2001 From: Weiko Date: Wed, 11 Feb 2026 17:56:21 +0100 Subject: [PATCH] Fix page layout seeding for record page layouts (#17871) ## Context Fix broken record page layout seeding. This was not detected by the CI because it doesn't have the env variable yet. Following the same mechanism as labelIdentifier in object for circular dependency resolution --- ...d-stringify.constant.constant.spec.ts.snap | 1 + ...configuration-by-metadata-name.constant.ts | 5 ++ ...orkspace-migration-action-with-ids.util.ts | 45 +++++++++++++++++- ...space-migration-page-layout-action.type.ts | 4 +- ...eate-page-layout-action-handler.service.ts | 46 ++++++++++++++++--- ...date-page-layout-action-handler.service.ts | 44 +++++++++++++++--- ...b-id-in-create-page-layout-context.util.ts | 24 ++++++++++ 7 files changed, 154 insertions(+), 15 deletions(-) create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/utils/find-page-layout-tab-id-in-create-page-layout-context.util.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.constant.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.constant.spec.ts.snap index f188f56f83..4c168ad231 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.constant.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.constant.spec.ts.snap @@ -127,6 +127,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma "name", "type", "objectMetadataUniversalIdentifier", + "defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier", "deletedAt", ], "propertiesToStringify": [], diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts index 6ea3b26eb9..4a65381b45 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts @@ -228,6 +228,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = { toStringify: false, universalProperty: 'objectMetadataUniversalIdentifier', }, + defaultTabToFocusOnMobileAndSidePanelId: { + toStringify: false, + universalProperty: + 'defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier', + }, deletedAt: { toStringify: false, universalProperty: undefined }, }, pageLayoutWidget: { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts index a5384d8e52..8cbd5a102c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts @@ -3,6 +3,7 @@ import { assertUnreachable, isDefined } from 'twenty-shared/utils'; import { type UniversalCreateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action'; import { type UniversalCreateObjectAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action'; +import { type UniversalCreatePageLayoutAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout/types/workspace-migration-page-layout-action.type'; import { type WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration'; export type IdByUniversalIdentifierByMetadataName = { @@ -40,6 +41,25 @@ const buildFieldIdByUniversalIdentifier = ({ return fieldIdByUniversalIdentifier; }; +const buildTabIdByUniversalIdentifier = ({ + action, + pageLayoutTabIdByUniversalIdentifier, +}: { + action: UniversalCreatePageLayoutAction; + pageLayoutTabIdByUniversalIdentifier: Record; +}): Record | undefined => { + const tabIdByUniversalIdentifier = { + ...action.tabIdByUniversalIdentifier, + ...pageLayoutTabIdByUniversalIdentifier, + }; + + if (Object.keys(tabIdByUniversalIdentifier).length === 0) { + return undefined; + } + + return tabIdByUniversalIdentifier; +}; + export const enrichCreateWorkspaceMigrationActionsWithIds = ({ workspaceMigration, idByUniversalIdentifierByMetadataName, @@ -49,6 +69,8 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({ }): WorkspaceMigration => { const fieldMetadataIdByUniversalIdentifier = idByUniversalIdentifierByMetadataName.fieldMetadata; + const pageLayoutTabIdByUniversalIdentifier = + idByUniversalIdentifierByMetadataName.pageLayoutTab; const enrichedActions = workspaceMigration.actions.map((action) => { if (action.type !== 'create') { @@ -60,7 +82,8 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({ if ( !isDefined(idByUniversalIdentifier) && - !isDefined(fieldMetadataIdByUniversalIdentifier) + !isDefined(fieldMetadataIdByUniversalIdentifier) && + !isDefined(pageLayoutTabIdByUniversalIdentifier) ) { return action; } @@ -98,6 +121,25 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({ }), }; } + case 'pageLayout': { + const id = isDefined(idByUniversalIdentifier) + ? idByUniversalIdentifier[action.flatEntity.universalIdentifier] + : undefined; + const tabIdByUniversalIdentifier = isDefined( + pageLayoutTabIdByUniversalIdentifier, + ) + ? buildTabIdByUniversalIdentifier({ + action, + pageLayoutTabIdByUniversalIdentifier, + }) + : undefined; + + return { + ...action, + id, + tabIdByUniversalIdentifier, + }; + } case 'view': case 'viewField': case 'viewGroup': @@ -111,7 +153,6 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({ case 'roleTarget': case 'agent': case 'skill': - case 'pageLayout': case 'pageLayoutWidget': case 'pageLayoutTab': case 'commandMenuItem': diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout/types/workspace-migration-page-layout-action.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout/types/workspace-migration-page-layout-action.type.ts index 43575bd74a..75ccfcbfb5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout/types/workspace-migration-page-layout-action.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout/types/workspace-migration-page-layout-action.type.ts @@ -9,7 +9,9 @@ export type FlatCreatePageLayoutAction = BaseFlatCreateWorkspaceMigrationAction<'pageLayout'>; export type UniversalCreatePageLayoutAction = - BaseUniversalCreateWorkspaceMigrationAction<'pageLayout'>; + BaseUniversalCreateWorkspaceMigrationAction<'pageLayout'> & { + tabIdByUniversalIdentifier?: Record; + }; export type FlatUpdatePageLayoutAction = BaseFlatUpdateWorkspaceMigrationAction<'pageLayout'>; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/create-page-layout-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/create-page-layout-action-handler.service.ts index dc41109f53..d403c03c59 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/create-page-layout-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/create-page-layout-action-handler.service.ts @@ -1,15 +1,21 @@ import { Injectable } from '@nestjs/common'; +import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity'; import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; import { FlatCreatePageLayoutAction, UniversalCreatePageLayoutAction, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout/types/workspace-migration-page-layout-action.type'; +import { findPageLayoutTabIdInCreatePageLayoutContext } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/utils/find-page-layout-tab-id-in-create-page-layout-context.util'; import { WorkspaceMigrationActionRunnerArgs, WorkspaceMigrationActionRunnerContext, @@ -30,12 +36,40 @@ export class CreatePageLayoutActionHandlerService extends WorkspaceMigrationRunn flatApplication, workspaceId, }: WorkspaceMigrationActionRunnerArgs): Promise { - const { objectMetadataId, defaultTabToFocusOnMobileAndSidePanelId } = - resolveUniversalRelationIdentifiersToIds({ - flatEntityMaps: allFlatEntityMaps, - metadataName: action.metadataName, - universalForeignKeyValues: action.flatEntity, - }); + const { objectMetadataId } = resolveUniversalRelationIdentifiersToIds< + 'pageLayout', + 'objectMetadataUniversalIdentifier' + >({ + flatEntityMaps: allFlatEntityMaps, + metadataName: 'pageLayout', + universalForeignKeyValues: { + objectMetadataUniversalIdentifier: + action.flatEntity.objectMetadataUniversalIdentifier, + }, + }); + + let defaultTabToFocusOnMobileAndSidePanelId: string | null = null; + + const defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier = + action.flatEntity + .defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier; + + if (isDefined(defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier)) { + defaultTabToFocusOnMobileAndSidePanelId = + findPageLayoutTabIdInCreatePageLayoutContext({ + universalIdentifier: + defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier, + tabIdByUniversalIdentifier: action.tabIdByUniversalIdentifier, + flatPageLayoutTabMaps: allFlatEntityMaps.flatPageLayoutTabMaps, + }); + + if (!isDefined(defaultTabToFocusOnMobileAndSidePanelId)) { + throw new FlatEntityMapsException( + `Could not resolve defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier to defaultTabToFocusOnMobileAndSidePanelId: no pageLayoutTab found for universal identifier ${defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } return { ...action, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/update-page-layout-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/update-page-layout-action-handler.service.ts index 4bff4dd810..7bb5b1e41d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/update-page-layout-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/update-page-layout-action-handler.service.ts @@ -1,8 +1,16 @@ import { Injectable } from '@nestjs/common'; +import { isDefined } from 'twenty-shared/utils'; + import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; +import { type FlatEntityUpdate } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-update.type'; import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; +import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity'; import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-update-relation-identifiers-to-ids.util'; import { @@ -29,17 +37,41 @@ export class UpdatePageLayoutActionHandlerService extends WorkspaceMigrationRunn universalIdentifier: action.universalIdentifier, }); - const update = resolveUniversalUpdateRelationIdentifiersToIds({ - metadataName: 'pageLayout', - universalUpdate: action.update, - allFlatEntityMaps, - }); + const { + defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier, + ...restUpdate + } = action.update; + + const transpiledUpdate: FlatEntityUpdate<'pageLayout'> = + resolveUniversalUpdateRelationIdentifiersToIds({ + metadataName: 'pageLayout', + universalUpdate: restUpdate, + allFlatEntityMaps, + }); + + if (isDefined(defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier)) { + const flatPageLayoutTab = findFlatEntityByUniversalIdentifier({ + flatEntityMaps: allFlatEntityMaps.flatPageLayoutTabMaps, + universalIdentifier: + defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier, + }); + + if (!isDefined(flatPageLayoutTab)) { + throw new FlatEntityMapsException( + `Could not resolve defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier to defaultTabToFocusOnMobileAndSidePanelId: no pageLayoutTab found for universal identifier ${defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + transpiledUpdate.defaultTabToFocusOnMobileAndSidePanelId = + flatPageLayoutTab.id; + } return { type: 'update', metadataName: 'pageLayout', entityId: flatPageLayout.id, - update, + update: transpiledUpdate, }; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/utils/find-page-layout-tab-id-in-create-page-layout-context.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/utils/find-page-layout-tab-id-in-create-page-layout-context.util.ts new file mode 100644 index 0000000000..1e495d193e --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout/services/utils/find-page-layout-tab-id-in-create-page-layout-context.util.ts @@ -0,0 +1,24 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; + +export const findPageLayoutTabIdInCreatePageLayoutContext = ({ + universalIdentifier, + tabIdByUniversalIdentifier, + flatPageLayoutTabMaps, +}: { + universalIdentifier: string; + tabIdByUniversalIdentifier: Record | undefined; + flatPageLayoutTabMaps: AllFlatEntityMaps['flatPageLayoutTabMaps']; +}): string | null => { + const providedId = tabIdByUniversalIdentifier?.[universalIdentifier]; + + if (isDefined(providedId)) { + return providedId; + } + + const existingTab = + flatPageLayoutTabMaps.byUniversalIdentifier[universalIdentifier]; + + return existingTab?.id ?? null; +};