From cc247c2e8eaaa0a02e4aa33066437816f4f1f1fd Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Wed, 25 Mar 2026 17:43:54 +0100 Subject: [PATCH] Fix record page layout upgrade commands (#18962) - Fix issue with name that must be first in the view field list - Improve dry run and logs of backfill page layouts command --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: prastoin --- .../1-20-backfill-page-layouts.command.ts | 148 +++++++++++++----- .../object-metadata.service.ts | 55 ++----- ...compute-flat-view-fields-to-create.util.ts | 15 ++ 3 files changed, 142 insertions(+), 76 deletions(-) diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.command.ts index edc692a6a92..2c860da6bc3 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.command.ts @@ -68,19 +68,46 @@ export class BackfillPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigra return; } - if (isDryRun) { - this.logger.log( - `[DRY RUN] Would create RECORD_PAGE page layouts and enable IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED for workspace ${workspaceId}`, - ); - - return; - } - const { twentyStandardFlatApplication, workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { workspaceId }, ); + await this.backfillStandardObjectPageLayouts({ + workspaceId, + twentyStandardFlatApplication, + isDryRun, + }); + + await this.backfillCustomObjectPageLayouts({ + workspaceId, + workspaceCustomFlatApplication, + isDryRun, + }); + + if (!isDryRun) { + await this.featureFlagService.enableFeatureFlags( + [FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED], + workspaceId, + ); + } + + this.logger.log( + isDryRun + ? `[DRY RUN] Would create page layouts and enable IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED for workspace ${workspaceId}` + : `Successfully created page layouts and enabled IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED for workspace ${workspaceId}`, + ); + } + + private async backfillStandardObjectPageLayouts({ + workspaceId, + twentyStandardFlatApplication, + isDryRun, + }: { + workspaceId: string; + twentyStandardFlatApplication: FlatApplication; + isDryRun: boolean; + }): Promise { const { allFlatEntityMaps: standardAllFlatEntityMaps } = computeTwentyStandardApplicationAllFlatEntityMaps({ shouldIncludeRecordPageLayouts: true, @@ -89,6 +116,22 @@ export class BackfillPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigra twentyStandardApplicationId: twentyStandardFlatApplication.id, }); + const { + flatPageLayoutMaps: existingFlatPageLayoutMaps, + flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps, + flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps, + flatViewMaps: existingFlatViewMaps, + flatViewFieldMaps: existingFlatViewFieldMaps, + flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps, + } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatPageLayoutMaps', + 'flatPageLayoutTabMaps', + 'flatPageLayoutWidgetMaps', + 'flatViewMaps', + 'flatViewFieldMaps', + 'flatViewFieldGroupMaps', + ]); + const recordPageLayoutUniversalIdentifiers = new Set(); const pageLayoutsToCreate = Object.values( @@ -104,6 +147,16 @@ export class BackfillPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigra pageLayout.universalIdentifier, ); + if ( + isDefined( + existingFlatPageLayoutMaps.byUniversalIdentifier[ + pageLayout.universalIdentifier + ], + ) + ) { + return false; + } + return true; }); @@ -124,6 +177,16 @@ export class BackfillPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigra tabUniversalIdentifiers.add(tab.universalIdentifier); + if ( + isDefined( + existingFlatPageLayoutTabMaps.byUniversalIdentifier[ + tab.universalIdentifier + ], + ) + ) { + return false; + } + return true; }); @@ -131,20 +194,18 @@ export class BackfillPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigra standardAllFlatEntityMaps.flatPageLayoutWidgetMaps.byUniversalIdentifier, ) .filter(isDefined) - .filter((widget) => - tabUniversalIdentifiers.has(widget.pageLayoutTabUniversalIdentifier), + .filter( + (widget) => + tabUniversalIdentifiers.has( + widget.pageLayoutTabUniversalIdentifier, + ) && + !isDefined( + existingFlatPageLayoutWidgetMaps.byUniversalIdentifier[ + widget.universalIdentifier + ], + ), ); - const { - flatViewMaps: existingFlatViewMaps, - flatViewFieldMaps: existingFlatViewFieldMaps, - flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps, - } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'flatViewMaps', - 'flatViewFieldMaps', - 'flatViewFieldGroupMaps', - ]); - const viewUniversalIdentifiers = new Set(); const viewsToCreate = Object.values( @@ -201,6 +262,20 @@ export class BackfillPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigra ), ); + this.logger.log( + `${isDryRun ? '[DRY RUN] ' : ''}Found standard entities to create for workspace ${workspaceId}: ` + + `${pageLayoutsToCreate.length} page layout(s), ` + + `${pageLayoutTabsToCreate.length} tab(s), ` + + `${pageLayoutWidgetsToCreate.length} widget(s), ` + + `${viewsToCreate.length} view(s), ` + + `${viewFieldsToCreate.length} view field(s), ` + + `${viewFieldGroupsToCreate.length} view field group(s)`, + ); + + if (isDryRun) { + return; + } + const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { @@ -250,28 +325,16 @@ export class BackfillPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigra `Failed to create standard page layouts for workspace ${workspaceId}`, ); } - - await this.backfillCustomObjectPageLayouts({ - workspaceId, - workspaceCustomFlatApplication, - }); - - await this.featureFlagService.enableFeatureFlags( - [FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED], - workspaceId, - ); - - this.logger.log( - `Successfully created page layouts and enabled IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED for workspace ${workspaceId}`, - ); } private async backfillCustomObjectPageLayouts({ workspaceId, workspaceCustomFlatApplication, + isDryRun, }: { workspaceId: string; workspaceCustomFlatApplication: FlatApplication; + isDryRun: boolean; }): Promise { const { flatObjectMetadataMaps, @@ -316,10 +379,6 @@ export class BackfillPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigra return; } - this.logger.log( - `Creating page layouts for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}`, - ); - const allCustomPageLayoutsToCreate: FlatPageLayout[] = []; const allCustomPageLayoutTabsToCreate: FlatPageLayoutTab[] = []; const allCustomPageLayoutWidgetsToCreate: FlatPageLayoutWidget[] = []; @@ -361,6 +420,19 @@ export class BackfillPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigra allCustomViewFieldsToCreate.push(...viewFields); } + this.logger.log( + `${isDryRun ? '[DRY RUN] ' : ''}Found custom entities to create for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}: ` + + `${allCustomPageLayoutsToCreate.length} page layout(s), ` + + `${allCustomPageLayoutTabsToCreate.length} tab(s), ` + + `${allCustomPageLayoutWidgetsToCreate.length} widget(s), ` + + `${allCustomViewsToCreate.length} view(s), ` + + `${allCustomViewFieldsToCreate.length} view field(s)`, + ); + + if (isDryRun) { + return; + } + const customValidateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 341cc471213..b483edd44b2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -4,9 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { FeatureFlagKey, + ViewKey, ViewOpenRecordIn, ViewType, - ViewKey, ViewVisibility, } from 'twenty-shared/types'; import { fromArrayToUniqueKeyRecord, isDefined } from 'twenty-shared/utils'; @@ -24,7 +24,6 @@ import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type'; -import { NavigationMenuItemType } from 'src/engine/metadata-modules/navigation-menu-item/enums/navigation-menu-item-type.enum'; import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util'; import { fromDeleteObjectInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util'; @@ -32,6 +31,7 @@ import { fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities } from import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type'; import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type'; import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type'; +import { NavigationMenuItemType } from 'src/engine/metadata-modules/navigation-menu-item/enums/navigation-menu-item-type.enum'; import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input'; import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; @@ -414,14 +414,13 @@ export class ObjectMetadataService extends TypeOrmQueryService { + const aIsLabelIdentifierFieldMetadata = + a.universalIdentifier === + labelIdentifierFieldMetadataUniversalIdentifier; + const bIsLabelIdentifierFieldMetadata = + b.universalIdentifier === + labelIdentifierFieldMetadataUniversalIdentifier; + + if (aIsLabelIdentifierFieldMetadata && !bIsLabelIdentifierFieldMetadata) + return -1; + if (!aIsLabelIdentifierFieldMetadata && bIsLabelIdentifierFieldMetadata) + return 1; + + return 0; + }) .map((field, index) => ({ fieldMetadataUniversalIdentifier: field.universalIdentifier, viewUniversalIdentifier,