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 <[email protected]>
Co-authored-by: prastoin <[email protected]>
This commit is contained in:
Baptiste Devessier
2026-03-25 16:43:54 +00:00
committed by GitHub
co-authored by Copilot prastoin
parent 4d6c8db205
commit cc247c2e8e
3 changed files with 142 additions and 76 deletions
@@ -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<void> {
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<string>();
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<string>();
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<void> {
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(
{
@@ -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<ObjectMetadataEnt
flatApplication: resolvedOwnerFlatApplication,
});
const flatDefaultViewFieldsToCreate =
await this.computeFlatViewFieldsToCreate({
flatApplication: workspaceCustomFlatApplication,
objectFlatFieldMetadatas: flatFieldMetadataToCreateOnObject,
labelIdentifierFieldMetadataUniversalIdentifier:
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
viewUniversalIdentifier: flatDefaultViewToCreate.universalIdentifier,
});
const flatDefaultViewFieldsToCreate = computeFlatViewFieldsToCreate({
flatApplication: workspaceCustomFlatApplication,
objectFlatFieldMetadatas: flatFieldMetadataToCreateOnObject,
labelIdentifierFieldMetadataUniversalIdentifier:
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
viewUniversalIdentifier: flatDefaultViewToCreate.universalIdentifier,
});
let flatRecordPageFieldsViewToCreate:
| (UniversalFlatView & { id: string })
@@ -449,15 +448,14 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
flatApplication: resolvedOwnerFlatApplication,
});
flatRecordPageFieldsViewFieldsToCreate =
await this.computeFlatViewFieldsToCreate({
flatApplication: workspaceCustomFlatApplication,
objectFlatFieldMetadatas: flatFieldMetadataToCreateOnObject,
labelIdentifierFieldMetadataUniversalIdentifier:
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
viewUniversalIdentifier:
flatRecordPageFieldsViewToCreate.universalIdentifier,
});
flatRecordPageFieldsViewFieldsToCreate = computeFlatViewFieldsToCreate({
flatApplication: workspaceCustomFlatApplication,
objectFlatFieldMetadatas: flatFieldMetadataToCreateOnObject,
labelIdentifierFieldMetadataUniversalIdentifier:
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
viewUniversalIdentifier:
flatRecordPageFieldsViewToCreate.universalIdentifier,
});
flatDefaultRecordPageLayoutsToCreate =
this.computeFlatDefaultRecordPageLayoutToCreate({
@@ -664,25 +662,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
});
}
private async computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier,
}: {
flatApplication: FlatApplication;
objectFlatFieldMetadatas: UniversalFlatFieldMetadata[];
viewUniversalIdentifier: string;
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
}) {
return computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier,
});
}
private async computeFlatNavigationMenuItemToCreate({
objectMetadata,
workspaceId,
@@ -29,6 +29,21 @@ export const computeFlatViewFieldsToCreate = ({
field.universalIdentifier ===
labelIdentifierFieldMetadataUniversalIdentifier),
)
.sort((a, b) => {
const aIsLabelIdentifierFieldMetadata =
a.universalIdentifier ===
labelIdentifierFieldMetadataUniversalIdentifier;
const bIsLabelIdentifierFieldMetadata =
b.universalIdentifier ===
labelIdentifierFieldMetadataUniversalIdentifier;
if (aIsLabelIdentifierFieldMetadata && !bIsLabelIdentifierFieldMetadata)
return -1;
if (!aIsLabelIdentifierFieldMetadata && bIsLabelIdentifierFieldMetadata)
return 1;
return 0;
})
.map<UniversalFlatViewField>((field, index) => ({
fieldMetadataUniversalIdentifier: field.universalIdentifier,
viewUniversalIdentifier,