Pagelayout backfill command standard app (#19380)

## Context
Due to the chosen strategy for "Reset to default" feature for page
layouts. Those overridable entities need to be associated to the
Standard app to work properly (there is no "Default" state for custom
entities). Until we find a better implementation, I'm changing the
backfill command to reflect that
This commit is contained in:
Weiko
2026-04-07 14:49:35 +02:00
committed by GitHub
parent 96a242eb7d
commit 6e23ca35e6
5 changed files with 180 additions and 1765 deletions
@@ -128,7 +128,7 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
return;
}
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
const { twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
@@ -140,13 +140,12 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
await this.backfillCustomObjectPageLayouts({
workspaceId,
workspaceCustomFlatApplication,
twentyStandardFlatApplication,
});
await this.backfillFieldWidgets({
workspaceId,
twentyStandardFlatApplication,
workspaceCustomFlatApplication,
});
await this.featureFlagService.enableFeatureFlags(
@@ -403,10 +402,10 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
private async backfillCustomObjectPageLayouts({
workspaceId,
workspaceCustomFlatApplication,
twentyStandardFlatApplication,
}: {
workspaceId: string;
workspaceCustomFlatApplication: FlatApplication;
twentyStandardFlatApplication: FlatApplication;
}): Promise<void> {
const {
flatObjectMetadataMaps,
@@ -464,7 +463,7 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
for (const customObject of customObjectsWithoutPageLayout) {
const flatRecordPageFieldsView = computeFlatRecordPageFieldsViewToCreate({
objectMetadata: customObject,
flatApplication: workspaceCustomFlatApplication,
flatApplication: twentyStandardFlatApplication,
});
const objectFieldMetadatas = Object.values(
@@ -476,7 +475,7 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
const viewFields = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: objectFieldMetadatas,
viewUniversalIdentifier: flatRecordPageFieldsView.universalIdentifier,
flatApplication: workspaceCustomFlatApplication,
flatApplication: twentyStandardFlatApplication,
labelIdentifierFieldMetadataUniversalIdentifier:
customObject.labelIdentifierFieldMetadataUniversalIdentifier,
excludeLabelIdentifier: true,
@@ -485,7 +484,7 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
const { pageLayouts, pageLayoutTabs, pageLayoutWidgets } =
computeFlatDefaultRecordPageLayoutToCreate({
objectMetadata: customObject,
flatApplication: workspaceCustomFlatApplication,
flatApplication: twentyStandardFlatApplication,
recordPageFieldsView: flatRecordPageFieldsView,
workspaceId,
});
@@ -529,7 +528,7 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
twentyStandardFlatApplication.universalIdentifier,
},
);
@@ -550,11 +549,9 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
private async backfillFieldWidgets({
workspaceId,
twentyStandardFlatApplication,
workspaceCustomFlatApplication,
}: {
workspaceId: string;
twentyStandardFlatApplication: FlatApplication;
workspaceCustomFlatApplication: FlatApplication;
}): Promise<void> {
const {
flatObjectMetadataMaps,
@@ -675,8 +672,7 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
const now = new Date().toISOString();
const standardWidgetsToCreate: FlatPageLayoutWidget[] = [];
const customWidgetsToCreate: FlatPageLayoutWidget[] = [];
const widgetsToCreate: FlatPageLayoutWidget[] = [];
const processedMorphIds = new Set<string>();
for (const object of objectById.values()) {
@@ -692,14 +688,6 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
continue;
}
const flatApplication: FlatApplication = object.isCustom
? workspaceCustomFlatApplication
: twentyStandardFlatApplication;
const targetList: FlatPageLayoutWidget[] = object.isCustom
? customWidgetsToCreate
: standardWidgetsToCreate;
const fields = fieldsByObjectId.get(object.id) ?? [];
let nextWidgetIndex = homeTab.widgetCount;
@@ -758,8 +746,9 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
const widget: FlatPageLayoutWidget = {
id: v4(),
universalIdentifier: v4(),
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
applicationId: twentyStandardFlatApplication.id,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
workspaceId,
pageLayoutTabId: homeTab.id,
pageLayoutTabUniversalIdentifier: homeTab.universalIdentifier,
@@ -790,16 +779,13 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
overrides: null,
};
targetList.push(widget);
widgetsToCreate.push(widget);
existingFieldWidgetFieldIds.add(field.id);
nextWidgetIndex++;
}
}
const totalWidgets =
standardWidgetsToCreate.length + customWidgetsToCreate.length;
if (totalWidgets === 0) {
if (widgetsToCreate.length === 0) {
this.logger.log(
`All FIELD widgets already exist for workspace ${workspaceId}, skipping`,
);
@@ -808,65 +794,36 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
}
this.logger.log(
`Found ${totalWidgets} FIELD widget(s) to create for workspace ${workspaceId} (${standardWidgetsToCreate.length} standard, ${customWidgetsToCreate.length} custom)`,
`Found ${widgetsToCreate.length} FIELD widget(s) to create for workspace ${workspaceId}`,
);
if (standardWidgetsToCreate.length > 0) {
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
flatEntityToCreate: standardWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
flatEntityToCreate: widgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (result.status === 'fail') {
this.logger.error(
`Failed to create standard FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to create standard FIELD widgets for workspace ${workspaceId}`,
);
}
}
if (customWidgetsToCreate.length > 0) {
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
flatEntityToCreate: customWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (result.status === 'fail') {
this.logger.error(
`Failed to create custom FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to create custom FIELD widgets for workspace ${workspaceId}`,
);
}
if (result.status === 'fail') {
this.logger.error(
`Failed to create FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to create FIELD widgets for workspace ${workspaceId}`,
);
}
this.logger.log(
`Successfully created ${totalWidgets} FIELD widget(s) for workspace ${workspaceId}`,
`Successfully created ${widgetsToCreate.length} FIELD widget(s) for workspace ${workspaceId}`,
);
}
}
@@ -49,7 +49,6 @@ import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
import { UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
@Injectable()
@@ -386,7 +385,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
workspaceId: string;
ownerFlatApplication?: FlatApplication;
}): Promise<FlatObjectMetadata> {
const { workspaceCustomFlatApplication } =
const { workspaceCustomFlatApplication, twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
@@ -428,50 +427,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
viewUniversalIdentifier: flatDefaultViewToCreate.universalIdentifier,
});
let flatRecordPageFieldsViewToCreate:
| (UniversalFlatView & { id: string })
| null = null;
let flatRecordPageFieldsViewFieldsToCreate: UniversalFlatViewField[] = [];
let flatDefaultRecordPageLayoutsToCreate: {
pageLayouts: FlatPageLayout[];
pageLayoutTabs: FlatPageLayoutTab[];
pageLayoutWidgets: FlatPageLayoutWidget[];
} = {
pageLayouts: [],
pageLayoutTabs: [],
pageLayoutWidgets: [],
};
if (
const isRecordPageLayoutEditingEnabled =
existingFeatureFlagsMap[
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED
] ??
false
) {
flatRecordPageFieldsViewToCreate =
this.computeFlatRecordPageFieldsViewToCreate({
objectMetadata: flatObjectMetadataToCreate,
flatApplication: resolvedOwnerFlatApplication,
});
flatRecordPageFieldsViewFieldsToCreate = computeFlatViewFieldsToCreate({
flatApplication: workspaceCustomFlatApplication,
objectFlatFieldMetadatas: flatFieldMetadataToCreateOnObject,
labelIdentifierFieldMetadataUniversalIdentifier:
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
viewUniversalIdentifier:
flatRecordPageFieldsViewToCreate.universalIdentifier,
excludeLabelIdentifier: true,
});
flatDefaultRecordPageLayoutsToCreate =
this.computeFlatDefaultRecordPageLayoutToCreate({
objectMetadata: flatObjectMetadataToCreate,
flatApplication: resolvedOwnerFlatApplication,
recordPageFieldsView: flatRecordPageFieldsViewToCreate,
workspaceId,
});
}
] ?? false;
const flatNavigationMenuItemToCreate =
await this.computeFlatNavigationMenuItemToCreate({
@@ -492,20 +451,12 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
flatEntityToUpdate: [],
},
view: {
flatEntityToCreate: [
flatDefaultViewToCreate,
...(isDefined(flatRecordPageFieldsViewToCreate)
? [flatRecordPageFieldsViewToCreate]
: []),
],
flatEntityToCreate: [flatDefaultViewToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate: [
...flatDefaultViewFieldsToCreate,
...flatRecordPageFieldsViewFieldsToCreate,
],
flatEntityToCreate: flatDefaultViewFieldsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
@@ -522,27 +473,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayout: {
flatEntityToCreate: [
...flatDefaultRecordPageLayoutsToCreate.pageLayouts,
],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutTab: {
flatEntityToCreate: [
...flatDefaultRecordPageLayoutsToCreate.pageLayoutTabs,
],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutWidget: {
flatEntityToCreate: [
...flatDefaultRecordPageLayoutsToCreate.pageLayoutWidgets,
],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
...(isDefined(flatNavigationMenuItemToCreate)
? {
navigationMenuItem: {
@@ -567,6 +497,79 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
);
}
if (isRecordPageLayoutEditingEnabled) {
const flatRecordPageFieldsViewToCreate =
this.computeFlatRecordPageFieldsViewToCreate({
objectMetadata: flatObjectMetadataToCreate,
flatApplication: twentyStandardFlatApplication,
});
const flatRecordPageFieldsViewFieldsToCreate =
computeFlatViewFieldsToCreate({
flatApplication: twentyStandardFlatApplication,
objectFlatFieldMetadatas: flatFieldMetadataToCreateOnObject,
labelIdentifierFieldMetadataUniversalIdentifier:
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
viewUniversalIdentifier:
flatRecordPageFieldsViewToCreate.universalIdentifier,
excludeLabelIdentifier: true,
});
const flatDefaultRecordPageLayoutsToCreate =
this.computeFlatDefaultRecordPageLayoutToCreate({
objectMetadata: flatObjectMetadataToCreate,
flatApplication: twentyStandardFlatApplication,
recordPageFieldsView: flatRecordPageFieldsViewToCreate,
workspaceId,
});
const pageLayoutMigrationResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
view: {
flatEntityToCreate: [flatRecordPageFieldsViewToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate: flatRecordPageFieldsViewFieldsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayout: {
flatEntityToCreate:
flatDefaultRecordPageLayoutsToCreate.pageLayouts,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutTab: {
flatEntityToCreate:
flatDefaultRecordPageLayoutsToCreate.pageLayoutTabs,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutWidget: {
flatEntityToCreate:
flatDefaultRecordPageLayoutsToCreate.pageLayoutWidgets,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (pageLayoutMigrationResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
pageLayoutMigrationResult,
'Multiple validation errors occurred while creating page layouts for object',
);
}
}
const { flatObjectMetadataMaps: recomputedFlatObjectMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
@@ -11,13 +11,13 @@ export const seedPageLayoutWidgets = async ({
schemaName,
workspaceId,
objectMetadataItems,
workspaceCustomApplicationId,
applicationId,
}: {
dataSource: DataSource;
schemaName: string;
workspaceId: string;
objectMetadataItems: ObjectMetadataEntity[];
workspaceCustomApplicationId: string;
applicationId: string;
}) => {
const widgetSeeds = getPageLayoutWidgetDataSeeds(
workspaceId,
@@ -38,7 +38,7 @@ export const seedPageLayoutWidgets = async ({
position: widget.position,
configuration: widget.configuration,
universalIdentifier: v4(),
applicationId: workspaceCustomApplicationId,
applicationId,
overrides: widget.overrides ?? null,
};
});
@@ -129,10 +129,10 @@ export class DevSeederService {
this.coreDataSource,
'core',
workspaceId,
workspaceCustomFlatApplication.id,
twentyStandardFlatApplication.id,
);
await seedPageLayoutTabs({
applicationId: workspaceCustomFlatApplication.id,
applicationId: twentyStandardFlatApplication.id,
workspaceId,
dataSource: this.coreDataSource,
schemaName: 'core',
@@ -150,7 +150,7 @@ export class DevSeederService {
schemaName: 'core',
workspaceId,
objectMetadataItems,
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
applicationId: twentyStandardFlatApplication.id,
});
const relatedPageLayoutCacheKeysToInvalidate = [