chore(page-layout): remove IS_RECORD_PAGE_LAYOUT_* feature flags (#20556)

## Summary

- Both \`IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED\` and
\`IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED\` are force-enabled on
every existing workspace by the 1.23.0 upgrade command
\`BackfillRecordPageLayoutsCommand\` and seeded enabled for new
workspaces via \`DEFAULT_FEATURE_FLAGS\` +
\`seed-feature-flags.util.ts\`. They are no longer load-bearing.
- Unwrap all \`if (flag) { … }\` conditionals to their enabled branch on
both server and front.
- Delete legacy fallback files that only the disabled branch reached:
\`PageLayoutRelationWidgetsSyncEffect\`,
\`usePageLayoutWithRelationWidgets\`,
\`reInjectDynamicRelationWidgetsFromDraft\`,
\`injectRelationWidgetsIntoLayout\`, \`isDynamicRelationWidget\` (and
their tests).
- Strip the two \`enableFeatureFlags\` calls from the 1.23 upgrade
command — the page-layout backfill data logic itself is kept intact
since old workspaces upgrading from < 1.23 still need it.
- No DB cleanup migration: stale \`featureFlag\` rows are left in place,
matching the precedent set by #20531 and #20460.

Net diff: 37 files, +106 / -1727.

## Test plan

- [x] \`npx nx typecheck twenty-shared twenty-server twenty-front\` —
all pass
- [x] \`npx nx lint:diff-with-main twenty-server twenty-front\` — all
pass
- [x] \`cd packages/twenty-front && npx jest page-layout\` — 1240 tests,
all pass
- [x] \`cd packages/twenty-server && npx jest
workspace-entity-manager.spec\` — pass
- [ ] Manual smoke: open a record page, verify tabs render and \"Edit
Layout\" command-menu action is available
- [ ] Manual smoke: Settings → Data model → object → Layout tab is
visible (and hidden for remote / Dashboard objects)
- [ ] Manual smoke: edit a tab title, save, reload — confirm persistence
This commit is contained in:
Charles Bochet
2026-05-14 08:12:00 +02:00
committed by GitHub
parent 6dd1e8a471
commit 04eb913551
37 changed files with 107 additions and 1705 deletions
@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { FeatureFlagKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type FindOneOptions, type Repository } from 'typeorm';
@@ -378,7 +377,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
flatViewFieldMaps: existingFlatViewFieldMaps,
flatViewMaps: existingFlatViewMaps,
flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps,
featureFlagsMap: existingFeatureFlagsMap,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
@@ -386,7 +384,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
'flatViewFieldMaps',
'flatViewMaps',
'flatViewFieldGroupMaps',
'featureFlagsMap',
]);
const allTranspiledTranspilationInputs: Awaited<
@@ -423,15 +420,8 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
{ flatFieldMetadatas: [], indexMetadatas: [] },
);
let flatViewFieldsToCreate: UniversalFlatViewField[] = [];
if (
existingFeatureFlagsMap[
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED
] ??
false
) {
flatViewFieldsToCreate = computeFlatViewFieldsFromFieldsWidgets({
const flatViewFieldsToCreate: UniversalFlatViewField[] =
computeFlatViewFieldsFromFieldsWidgets({
fieldsToCreate: flatFieldMetadatasToCreate.map((flatFieldMetadata) => ({
objectMetadataUniversalIdentifier:
flatFieldMetadata.objectMetadataUniversalIdentifier,
@@ -445,7 +435,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
applicationUniversalIdentifier:
resolvedOwnerFlatApplication.universalIdentifier,
});
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
@@ -3,7 +3,6 @@ import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import {
FeatureFlagKey,
ViewKey,
ViewOpenRecordIn,
ViewType,
@@ -475,13 +474,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
const resolvedOwnerFlatApplication =
ownerFlatApplication ?? workspaceCustomFlatApplication;
const {
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
featureFlagsMap: existingFeatureFlagsMap,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'featureFlagsMap',
]);
const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
]);
const {
flatObjectMetadataToCreate,
@@ -507,11 +503,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
viewUniversalIdentifier: flatDefaultViewToCreate.universalIdentifier,
});
const isRecordPageLayoutEditingEnabled =
existingFeatureFlagsMap[
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED
] ?? false;
const flatNavigationMenuItemToCreate =
await this.computeFlatNavigationMenuItemToCreate({
objectMetadata: flatObjectMetadataToCreate,
@@ -617,77 +608,75 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
);
}
if (isRecordPageLayoutEditingEnabled) {
const flatRecordPageFieldsViewToCreate =
this.computeFlatRecordPageFieldsViewToCreate({
objectMetadata: flatObjectMetadataToCreate,
flatApplication: twentyStandardFlatApplication,
});
const flatRecordPageFieldsViewToCreate =
this.computeFlatRecordPageFieldsViewToCreate({
objectMetadata: flatObjectMetadataToCreate,
flatApplication: twentyStandardFlatApplication,
});
const flatRecordPageFieldsViewFieldsToCreate =
computeFlatViewFieldsToCreate({
flatApplication: twentyStandardFlatApplication,
objectFlatFieldMetadatas: flatFieldMetadataToCreateOnObject,
labelIdentifierFieldMetadataUniversalIdentifier:
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
viewUniversalIdentifier:
flatRecordPageFieldsViewToCreate.universalIdentifier,
excludeLabelIdentifier: true,
});
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 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: [],
},
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,
},
);
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (pageLayoutMigrationResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
pageLayoutMigrationResult,
'Multiple validation errors occurred while creating page layouts for object',
);
}
if (pageLayoutMigrationResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
pageLayoutMigrationResult,
'Multiple validation errors occurred while creating page layouts for object',
);
}
const { flatObjectMetadataMaps: recomputedFlatObjectMetadataMaps } =
@@ -233,12 +233,10 @@ describe('WorkspaceEntityManager', () => {
IS_UNIQUE_INDEXES_ENABLED: false,
IS_JSON_FILTER_ENABLED: false,
IS_MARKETPLACE_SETTING_TAB_VISIBLE: false,
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED: false,
IS_PUBLIC_DOMAIN_ENABLED: false,
IS_EMAILING_DOMAIN_ENABLED: false,
IS_EMAIL_GROUP_ENABLED: false,
IS_JUNCTION_RELATIONS_ENABLED: false,
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: false,
IS_REST_METADATA_API_NEW_FORMAT_DIRECT: false,
},
userWorkspaceRoleMap: {},
@@ -50,16 +50,6 @@ export const seedFeatureFlags = async ({
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED,
workspaceId: workspaceId,
value: true,
},
])
.execute();
};
@@ -264,7 +264,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
shortLabel: 'Edit Layout',
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
conditionalAvailabilityExpression:
'pageType == "RECORD_PAGE" and featureFlags.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED and not isLayoutCustomizationModeEnabled and noneDefined(selectedRecords, "deletedAt") and objectPermissions.canUpdateObjectRecords and objectMetadataItem.nameSingular != "dashboard"',
'pageType == "RECORD_PAGE" and not isLayoutCustomizationModeEnabled and noneDefined(selectedRecords, "deletedAt") and objectPermissions.canUpdateObjectRecords and objectMetadataItem.nameSingular != "dashboard"',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.EDIT_RECORD_PAGE_LAYOUT,
@@ -1,7 +1,5 @@
import { FeatureFlagKey } from 'twenty-shared/types';
export const DEFAULT_FEATURE_FLAGS = [
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED,
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
FeatureFlagKey.IS_REST_METADATA_API_NEW_FORMAT_DIRECT,
] as const satisfies FeatureFlagKey[];