Fix fields widget new field visibility (#21111)

Fixes https://github.com/twentyhq/twenty/issues/21043

## Context
Newly created fields were never added to FIELDS widgets, regardless of
the "Set fields created in the future as visible" toggle. The widget's
newFieldDefaultVisibility was null on widgets that never explicitly set
it (it was never populated at creation), so the backend skipped them and
no view field was created.

## Implementation
Keep newFieldDefaultVisibility nullable with false (not visible) as the
behavior when not provided.
The FE now reflects that properly and shows "un-toggled" when it's null
(iso with BE behavior).

The fix also ensures the value is explicitly set to true wherever it
should be:
- Set newFieldDefaultVisibility: true at every FIELDS widget creation
path (backend default record-page layout, frontend
createDefaultFieldsWidget + useTemporaryFieldsConfiguration);
- Added a 2-9 workspace upgrade command that backfills true onto
existing standard FIELDS widgets where the value is null.
This commit is contained in:
Weiko
2026-06-03 17:29:57 +02:00
committed by GitHub
parent e64e5662e5
commit cc76b7bc50
13 changed files with 2666 additions and 325 deletions
@@ -2,14 +2,22 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
import { BackfillFieldsWidgetNewFieldDefaultVisibilityCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-workspace-command-1799000030000-backfill-fields-widget-new-field-default-visibility.command';
import { MigrateAiModelPreferencesCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-workspace-command-1799000000000-migrate-ai-model-preferences.command';
import { KeyValuePairEntity } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@Module({
imports: [
TypeOrmModule.forFeature([KeyValuePairEntity]),
WorkspaceCacheModule,
WorkspaceIteratorModule,
WorkspaceMigrationModule,
],
providers: [
MigrateAiModelPreferencesCommand,
BackfillFieldsWidgetNewFieldDefaultVisibilityCommand,
],
providers: [MigrateAiModelPreferencesCommand],
})
export class V2_9_UpgradeVersionCommandModule {}
@@ -0,0 +1,134 @@
import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { isFlatPageLayoutWidgetConfigurationOfType } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/is-flat-page-layout-widget-configuration-of-type.util';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@RegisteredWorkspaceCommand('2.9.0', 1799000030000)
@Command({
name: 'upgrade:2-9:backfill-fields-widget-new-field-default-visibility',
description:
'Backfill newFieldDefaultVisibility to true on FIELDS page layout widgets where it is null',
})
export class BackfillFieldsWidgetNewFieldDefaultVisibilityCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
constructor(
protected readonly workspaceIteratorService: WorkspaceIteratorService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
) {
super(workspaceIteratorService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
const { flatPageLayoutWidgetMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatPageLayoutWidgetMaps',
]);
const widgetsToBackfill = Object.values(
flatPageLayoutWidgetMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(
widget,
): widget is FlatPageLayoutWidget<WidgetConfigurationType.FIELDS> =>
isFlatPageLayoutWidgetConfigurationOfType(
widget,
WidgetConfigurationType.FIELDS,
) && !isDefined(widget.configuration.newFieldDefaultVisibility),
);
if (widgetsToBackfill.length === 0) {
this.logger.log(
`No FIELDS widgets to backfill in workspace ${workspaceId}`,
);
return;
}
if (isDryRun) {
this.logger.log(
`[DRY RUN] Would backfill ${widgetsToBackfill.length} FIELDS widget(s) in workspace ${workspaceId}`,
);
return;
}
const widgetsToBackfillByApplicationUniversalIdentifier = new Map<
string,
FlatPageLayoutWidget[]
>();
for (const widget of widgetsToBackfill) {
const updatedWidget: FlatPageLayoutWidget = {
...widget,
configuration: {
...widget.configuration,
newFieldDefaultVisibility: true,
},
universalConfiguration: isDefined(widget.universalConfiguration)
? {
...widget.universalConfiguration,
newFieldDefaultVisibility: true,
}
: widget.universalConfiguration,
};
const existingWidgets =
widgetsToBackfillByApplicationUniversalIdentifier.get(
widget.applicationUniversalIdentifier,
) ?? [];
widgetsToBackfillByApplicationUniversalIdentifier.set(
widget.applicationUniversalIdentifier,
[...existingWidgets, updatedWidget],
);
}
for (const [
applicationUniversalIdentifier,
updatedWidgets,
] of widgetsToBackfillByApplicationUniversalIdentifier) {
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: updatedWidgets,
},
},
workspaceId,
applicationUniversalIdentifier,
},
);
if (result.status === 'fail') {
this.logger.error(
`Failed to backfill FIELDS widgets for application ${applicationUniversalIdentifier} in workspace ${workspaceId}:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to backfill FIELDS widgets for workspace ${workspaceId}`,
);
}
this.logger.log(
`Backfilled ${updatedWidgets.length} FIELDS widget(s) for application ${applicationUniversalIdentifier} in workspace ${workspaceId}`,
);
}
}
}