[BREAKING_CHANGE_DASHBOARDS][DASHBOARD_CACHE_FLUSH_REQUIRED] Refactor page layout widget configuration type (#16671)

# Introduction
In this pull-request we're refactoring the page layout widget
configuration entity to be containing its discriminated key simplifying
underlying code and maintainability
- Made the configuration and title non nullable
- Introduced a generic predicate for the `widgetConfigurationType`
- Upgraded command to remove `graphType` and insert new
`configurationType` to existing entries
- Migrated frontend to new type system

---------

Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
This commit is contained in:
Paul Rastoin
2025-12-29 15:46:59 +01:00
committed by GitHub
parent 2b9728230a
commit 4135a6473a
221 changed files with 3241 additions and 1947 deletions
@@ -6,10 +6,10 @@ import { UpdateCreatedByEnumCommand } from 'src/database/commands/upgrade-versio
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@Module({
imports: [
@@ -0,0 +1,288 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { BarChartLayout } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-layout.enum';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
enum DeprecatedGraphType {
VERTICAL_BAR = 'VERTICAL_BAR',
HORIZONTAL_BAR = 'HORIZONTAL_BAR',
PIE = 'PIE',
LINE = 'LINE',
AGGREGATE = 'AGGREGATE',
GAUGE = 'GAUGE',
}
const migrateConfiguration = (
widgetType: WidgetType,
configuration: Record<string, unknown>,
): Record<string, unknown> => {
const existingConfigurationType = configuration.configurationType as
| string
| undefined;
if (isDefined(existingConfigurationType)) {
return configuration;
}
switch (widgetType) {
case WidgetType.GRAPH:
return migrateGraphConfiguration(configuration);
case WidgetType.IFRAME:
return {
...configuration,
configurationType: WidgetConfigurationType.IFRAME,
};
case WidgetType.STANDALONE_RICH_TEXT:
return {
...configuration,
configurationType: WidgetConfigurationType.STANDALONE_RICH_TEXT,
};
case WidgetType.VIEW:
return {
...configuration,
configurationType: WidgetConfigurationType.VIEW,
};
case WidgetType.FIELDS:
return {
...configuration,
configurationType: WidgetConfigurationType.FIELDS,
};
case WidgetType.TIMELINE:
return {
...configuration,
configurationType: WidgetConfigurationType.TIMELINE,
};
case WidgetType.TASKS:
return {
...configuration,
configurationType: WidgetConfigurationType.TASKS,
};
case WidgetType.NOTES:
return {
...configuration,
configurationType: WidgetConfigurationType.NOTES,
};
case WidgetType.FILES:
return {
...configuration,
configurationType: WidgetConfigurationType.FILES,
};
case WidgetType.EMAILS:
return {
...configuration,
configurationType: WidgetConfigurationType.EMAILS,
};
case WidgetType.CALENDAR:
return {
...configuration,
configurationType: WidgetConfigurationType.CALENDAR,
};
case WidgetType.FIELD_RICH_TEXT:
return {
...configuration,
configurationType: WidgetConfigurationType.FIELD_RICH_TEXT,
};
case WidgetType.WORKFLOW:
return {
...configuration,
configurationType: WidgetConfigurationType.WORKFLOW,
};
case WidgetType.WORKFLOW_VERSION:
return {
...configuration,
configurationType: WidgetConfigurationType.WORKFLOW_VERSION,
};
case WidgetType.WORKFLOW_RUN:
return {
...configuration,
configurationType: WidgetConfigurationType.WORKFLOW_RUN,
};
case WidgetType.FIELD:
return {
...configuration,
configurationType: WidgetConfigurationType.FIELD,
};
default:
assertUnreachable(widgetType);
}
};
const migrateGraphConfiguration = (
configuration: Record<string, unknown>,
): Record<string, unknown> => {
const { graphType: _, ...restConfig } = configuration;
const graphType = configuration.graphType as DeprecatedGraphType | undefined;
if (!isDefined(graphType)) {
return configuration;
}
switch (graphType) {
case DeprecatedGraphType.VERTICAL_BAR:
return {
...restConfig,
configurationType: WidgetConfigurationType.BAR_CHART,
layout: BarChartLayout.VERTICAL,
};
case DeprecatedGraphType.HORIZONTAL_BAR:
return {
...restConfig,
configurationType: WidgetConfigurationType.BAR_CHART,
layout: BarChartLayout.HORIZONTAL,
};
case DeprecatedGraphType.PIE:
return {
...restConfig,
configurationType: WidgetConfigurationType.PIE_CHART,
};
case DeprecatedGraphType.LINE:
return {
...restConfig,
configurationType: WidgetConfigurationType.LINE_CHART,
};
case DeprecatedGraphType.AGGREGATE:
return {
...restConfig,
configurationType: WidgetConfigurationType.AGGREGATE_CHART,
};
case DeprecatedGraphType.GAUGE:
return {
...restConfig,
configurationType: WidgetConfigurationType.GAUGE_CHART,
};
default:
assertUnreachable(graphType);
}
};
const needsMigration = (
widgetType: WidgetType,
configuration: Record<string, unknown>,
): boolean => {
const graphType = configuration.graphType as DeprecatedGraphType | undefined;
const configurationType = configuration.configurationType as
| WidgetConfigurationType
| undefined;
if (isDefined(graphType)) {
return true;
}
if (
!isDefined(configurationType) &&
(widgetType === WidgetType.IFRAME ||
widgetType === WidgetType.STANDALONE_RICH_TEXT)
) {
return true;
}
return false;
};
@Command({
name: 'upgrade:1-15:migrate-page-layout-widget-configuration',
description:
'Migrate page layout widget configurations to use configurationType',
})
export class MigratePageLayoutWidgetConfigurationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectRepository(PageLayoutWidgetEntity)
private readonly pageLayoutWidgetRepository: Repository<PageLayoutWidgetEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Starting migration of page layout widget configurations for workspace ${workspaceId}`,
);
const widgets = await this.pageLayoutWidgetRepository.find({
where: { workspaceId },
select: ['id', 'type', 'configuration'],
});
const widgetsWithNullConfiguration = widgets.filter(
(widget) => !isDefined(widget.configuration),
);
for (const widget of widgetsWithNullConfiguration) {
this.logger.log(
`Widget ${widget.id} has a corrupted configuration (null). Skipping migration.`,
);
}
const widgetsToMigrate = widgets.filter(
(widget) =>
isDefined(widget.configuration) &&
needsMigration(
widget.type,
widget.configuration as unknown as Record<string, unknown>,
),
);
this.logger.log(
`Found ${widgetsToMigrate.length} widget(s) needing migration out of ${widgets.length} total`,
);
if (widgetsToMigrate.length === 0) {
return;
}
for (const widget of widgetsToMigrate) {
const migratedConfiguration = migrateConfiguration(
widget.type,
widget.configuration as unknown as Record<string, unknown>,
);
if (options.dryRun) {
this.logger.log(
`[DRY RUN] Would update widget ${widget.id} configuration from ${JSON.stringify(widget.configuration)} to ${JSON.stringify(migratedConfiguration)}`,
);
} else {
await this.pageLayoutWidgetRepository.update(
{ id: widget.id },
{ configuration: migratedConfiguration },
);
this.logger.log(`Updated widget ${widget.id} configuration`);
}
}
this.logger.log(
`${options.dryRun ? '[DRY RUN] Would have migrated' : 'Successfully migrated'} ${widgetsToMigrate.length} widget(s)`,
);
if (!options.dryRun && widgetsToMigrate.length > 0) {
this.logger.log(
`Invalidating and recomputing cache for workspace ${workspaceId}`,
);
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'flatPageLayoutWidgetMaps',
]);
this.logger.log(`Cache invalidated and recomputed successfully`);
}
}
}
@@ -2,12 +2,27 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FixNanPositionValuesInNotesCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-fix-nan-position-values-in-notes.command';
import { MigratePageLayoutWidgetConfigurationCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-migrate-page-layout-widget-configuration.command';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@Module({
imports: [TypeOrmModule.forFeature([WorkspaceEntity]), DataSourceModule],
providers: [FixNanPositionValuesInNotesCommand],
exports: [FixNanPositionValuesInNotesCommand],
imports: [
TypeOrmModule.forFeature([WorkspaceEntity, PageLayoutWidgetEntity]),
DataSourceModule,
WorkspaceSchemaManagerModule,
WorkspaceCacheModule,
],
providers: [
MigratePageLayoutWidgetConfigurationCommand,
FixNanPositionValuesInNotesCommand,
],
exports: [
MigratePageLayoutWidgetConfigurationCommand,
FixNanPositionValuesInNotesCommand,
],
})
export class V1_15_UpgradeVersionCommandModule {}
@@ -19,6 +19,7 @@ import { UpdateRoleTargetsUniqueConstraintMigrationCommand } from 'src/database/
import { DeleteRemovedAgentsCommand } from 'src/database/commands/upgrade-version-command/1-14/1-14-delete-removed-agents.command';
import { UpdateCreatedByEnumCommand } from 'src/database/commands/upgrade-version-command/1-14/1-14-update-created-by-enum.command';
import { FixNanPositionValuesInNotesCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-fix-nan-position-values-in-notes.command';
import { MigratePageLayoutWidgetConfigurationCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-migrate-page-layout-widget-configuration.command';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
@@ -52,6 +53,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly deleteRemovedAgentsCommand: DeleteRemovedAgentsCommand,
// 1.15 Commands
protected readonly migratePageLayoutWidgetConfigurationCommand: MigratePageLayoutWidgetConfigurationCommand,
protected readonly fixNanPositionValuesInNotesCommand: FixNanPositionValuesInNotesCommand,
) {
super(
@@ -80,6 +82,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
];
const commands_1150: VersionCommands = [
this.migratePageLayoutWidgetConfigurationCommand,
this.fixNanPositionValuesInNotesCommand,
];