Migrate field widgets to backend (#18808)

- Renamed FieldConfiguration's layout field to fieldDisplayMode as it
caused issues with the layout field of BarChartConfiguration
- Create relation Field widgets for standard objects

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Baptiste Devessier
2026-03-23 01:26:00 +01:00
committed by GitHub
parent d90d2e3151
commit be89ef30cd
38 changed files with 1388 additions and 790 deletions
@@ -2,9 +2,9 @@ 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';
import { FeatureFlagKey } from 'twenty-shared/types';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
@@ -19,6 +19,7 @@ import {
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-universal-identifier-in-universal-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { fromCreateFieldInputToFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util';
@@ -26,6 +27,7 @@ import { fromDeleteFieldInputToFlatFieldMetadatasToDelete } from 'src/engine/met
import { fromUpdateFieldInputToFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util';
import { throwOnFieldInputTranspilationsError } from 'src/engine/metadata-modules/flat-field-metadata/utils/throw-on-field-input-transpilations-error.util';
import { computeFlatViewFieldsFromFieldsWidgets } from 'src/engine/metadata-modules/flat-view-field/utils/compute-flat-view-fields-from-fields-widgets.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 { EMPTY_ORCHESTRATOR_FAILURE_REPORT } from 'src/engine/workspace-manager/workspace-migration/constant/empty-orchestrator-failure-report.constant';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
@@ -93,6 +95,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
flatIndexMaps: existingFlatIndexMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
@@ -100,6 +103,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
'flatObjectMetadataMaps',
'flatIndexMaps',
'flatFieldMetadataMaps',
'flatPageLayoutWidgetMaps',
],
},
);
@@ -122,6 +126,31 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
},
);
const deletedFieldIds = new Set(
flatFieldMetadatasToDelete
.map((f) => {
const resolved = findFlatEntityByUniversalIdentifier({
universalIdentifier: f.universalIdentifier,
flatEntityMaps: existingFlatFieldMetadataMaps,
});
return resolved?.id;
})
.filter(isDefined),
);
const flatPageLayoutWidgetsToDelete = Object.values(
existingFlatPageLayoutWidgetMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(widget) =>
!isDefined(widget.deletedAt) &&
widget.configuration?.configurationType ===
WidgetConfigurationType.FIELD &&
deletedFieldIds.has(widget.configuration.fieldMetadataId),
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
@@ -136,6 +165,15 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
flatEntityToDelete: flatIndexesToDelete,
flatEntityToUpdate: flatIndexesToUpdate,
},
...(flatPageLayoutWidgetsToDelete.length > 0
? {
pageLayoutWidget: {
flatEntityToCreate: [],
flatEntityToDelete: flatPageLayoutWidgetsToDelete,
flatEntityToUpdate: [],
},
}
: {}),
},
workspaceId,
isSystemBuild,
@@ -51,10 +51,8 @@ export class FlatPageLayoutWidgetTypeValidatorService {
msg`Widget type VIEW is not supported yet.`,
),
IFRAME: validateIframeFlatPageLayoutWidgetForCreation,
FIELD: rejectWidgetType(
WidgetType.FIELD,
'Widget type FIELD is not supported yet.',
msg`Widget type FIELD is not supported yet.`,
FIELD: validateSimpleRecordPageWidgetForCreation(
WidgetConfigurationType.FIELD,
),
FIELDS: validateFieldsFlatPageLayoutWidgetForCreation,
GRAPH: validateGraphFlatPageLayoutWidgetForCreation,
@@ -101,10 +99,8 @@ export class FlatPageLayoutWidgetTypeValidatorService {
msg`Widget type VIEW is not supported yet.`,
),
IFRAME: validateIframeFlatPageLayoutWidgetForUpdate,
FIELD: rejectWidgetType(
WidgetType.FIELD,
'Widget type FIELD is not supported yet.',
msg`Widget type FIELD is not supported yet.`,
FIELD: validateSimpleRecordPageWidgetForUpdate(
WidgetConfigurationType.FIELD,
),
FIELDS: () => [],
GRAPH: validateGraphFlatPageLayoutWidgetForUpdate,
@@ -321,8 +321,25 @@ export const fromPageLayoutWidgetConfigurationToUniversalConfiguration = ({
};
}
case WidgetConfigurationType.FIELD: {
const { fieldMetadataId, fieldDisplayMode, configurationType } =
configuration;
const fieldMetadataUniversalIdentifier =
getFieldMetadataUniversalIdentifier({
fieldMetadataId,
fieldMetadataUniversalIdentifierById,
shouldThrowOnMissingIdentifier,
});
return {
configurationType,
fieldMetadataId: fieldMetadataUniversalIdentifier ?? fieldMetadataId,
fieldDisplayMode,
};
}
case WidgetConfigurationType.VIEW:
case WidgetConfigurationType.FIELD:
case WidgetConfigurationType.TIMELINE:
case WidgetConfigurationType.TASKS:
case WidgetConfigurationType.NOTES:
@@ -1,8 +1,10 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { IsIn, IsNotEmpty } from 'class-validator';
import { IsEnum, IsIn, IsNotEmpty, IsString } from 'class-validator';
import { type FieldConfiguration } from 'twenty-shared/types';
import { FieldDisplayMode } from 'src/engine/metadata-modules/page-layout-widget/enums/field-display-mode.enum';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
@ObjectType('FieldConfiguration')
@@ -11,4 +13,14 @@ export class FieldConfigurationDTO implements FieldConfiguration {
@IsIn([WidgetConfigurationType.FIELD])
@IsNotEmpty()
configurationType: WidgetConfigurationType.FIELD;
@Field(() => String)
@IsString()
@IsNotEmpty()
fieldMetadataId: string;
@Field(() => FieldDisplayMode)
@IsEnum(FieldDisplayMode)
@IsNotEmpty()
fieldDisplayMode: FieldDisplayMode;
}
@@ -0,0 +1,12 @@
import { registerEnumType } from '@nestjs/graphql';
export enum FieldDisplayMode {
CARD = 'CARD',
FIELD = 'FIELD',
VIEW = 'VIEW',
}
registerEnumType(FieldDisplayMode, {
name: 'FieldDisplayMode',
description: 'Display mode for field configuration widgets',
});