diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 13467de80b..bc3602150a 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1617,6 +1617,7 @@ export type FieldRichTextConfiguration = { export type FieldsConfiguration = { __typename?: 'FieldsConfiguration'; configurationType: WidgetConfigurationType; + newFieldDefaultConfiguration?: Maybe; viewId?: Maybe; }; @@ -3243,6 +3244,12 @@ export type NavigationMenuItem = { viewId?: Maybe; }; +export type NewFieldDefaultConfiguration = { + __typename?: 'NewFieldDefaultConfiguration'; + isVisible: Scalars['Boolean']; + viewFieldGroupId?: Maybe; +}; + export type NotesConfiguration = { __typename?: 'NotesConfiguration'; configurationType: WidgetConfigurationType; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts index 501a6985d2..b66673e47c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts @@ -7,6 +7,7 @@ import { type FindOneOptions, type Repository } from 'typeorm'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; import { type DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input'; import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; @@ -24,10 +25,12 @@ import { fromCreateFieldInputToFlatFieldMetadatasToCreate } from 'src/engine/met import { fromDeleteFieldInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util'; 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 { 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'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; +import { UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type'; @Injectable() export class FieldMetadataService extends TypeOrmQueryService { @@ -333,9 +336,19 @@ export class FieldMetadataService extends TypeOrmQueryService ({ + objectMetadataUniversalIdentifier: + flatFieldMetadata.objectMetadataUniversalIdentifier, + fieldMetadataUniversalIdentifier: + flatFieldMetadata.universalIdentifier, + })), + flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps, + flatViewFieldMaps: existingFlatViewFieldMaps, + flatViewMaps: existingFlatViewMaps, + flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps, + applicationUniversalIdentifier: + resolvedOwnerFlatApplication.universalIdentifier, + }); + } + const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { @@ -386,6 +423,11 @@ export class FieldMetadataService extends TypeOrmQueryService; export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({ @@ -26,6 +29,7 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({ flatPageLayoutTabMaps, flatObjectMetadataMaps, flatFieldMetadataMaps, + flatViewFieldGroupMaps, }: FromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreateArgs): FlatPageLayoutWidget => { const { pageLayoutTabId, ...createPageLayoutWidgetInput } = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( @@ -76,6 +80,8 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({ configuration: createPageLayoutWidgetInput.configuration, fieldMetadataUniversalIdentifierById: flatFieldMetadataMaps.universalIdentifierById, + viewFieldGroupUniversalIdentifierById: + flatViewFieldGroupMaps.universalIdentifierById, shouldThrowOnMissingIdentifier: true, }), }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util.ts index 2eeedc361b..9d35406947 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util.ts @@ -42,10 +42,12 @@ const getFieldMetadataUniversalIdentifier = ({ export const fromPageLayoutWidgetConfigurationToUniversalConfiguration = ({ configuration, fieldMetadataUniversalIdentifierById, + viewFieldGroupUniversalIdentifierById = {}, shouldThrowOnMissingIdentifier = false, }: { configuration: PageLayoutWidgetConfiguration; fieldMetadataUniversalIdentifierById: Partial>; + viewFieldGroupUniversalIdentifierById?: Partial>; shouldThrowOnMissingIdentifier?: boolean; }): UniversalPageLayoutWidgetConfiguration => { switch (configuration.configurationType) { @@ -200,9 +202,43 @@ export const fromPageLayoutWidgetConfigurationToUniversalConfiguration = ({ }; } + case WidgetConfigurationType.FIELDS: { + const { newFieldDefaultConfiguration, ...rest } = configuration; + + if (!isDefined(newFieldDefaultConfiguration)) { + return configuration; + } + + let viewFieldGroupUniversalIdentifier: string | null = null; + + if (isDefined(newFieldDefaultConfiguration.viewFieldGroupId)) { + viewFieldGroupUniversalIdentifier = + viewFieldGroupUniversalIdentifierById[ + newFieldDefaultConfiguration.viewFieldGroupId + ] ?? null; + + if ( + !isDefined(viewFieldGroupUniversalIdentifier) && + shouldThrowOnMissingIdentifier + ) { + throw new FlatEntityMapsException( + `View field group universal identifier not found for id: ${newFieldDefaultConfiguration.viewFieldGroupId}`, + FlatEntityMapsExceptionCode.RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND, + ); + } + } + + return { + ...rest, + newFieldDefaultConfiguration: { + isVisible: newFieldDefaultConfiguration.isVisible, + viewFieldGroupId: viewFieldGroupUniversalIdentifier, + }, + }; + } + case WidgetConfigurationType.VIEW: case WidgetConfigurationType.FIELD: - case WidgetConfigurationType.FIELDS: case WidgetConfigurationType.TIMELINE: case WidgetConfigurationType.TASKS: case WidgetConfigurationType.NOTES: diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util.ts index 7836497aa6..0ddb217b56 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util.ts @@ -12,6 +12,7 @@ import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/type type FromPageLayoutWidgetEntityToFlatPageLayoutWidgetArgs = FromEntityToFlatEntityArgs<'pageLayoutWidget'> & { fieldMetadataUniversalIdentifierById: Partial>; + viewFieldGroupUniversalIdentifierById?: Partial>; }; export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = ({ @@ -20,6 +21,7 @@ export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = ({ pageLayoutTabIdToUniversalIdentifierMap, objectMetadataIdToUniversalIdentifierMap, fieldMetadataUniversalIdentifierById, + viewFieldGroupUniversalIdentifierById, }: FromPageLayoutWidgetEntityToFlatPageLayoutWidgetArgs): FlatPageLayoutWidget => { const pageLayoutWidgetEntityWithoutRelations = removePropertiesFromRecord( pageLayoutWidgetEntity, @@ -70,6 +72,7 @@ export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = ({ fromPageLayoutWidgetConfigurationToUniversalConfiguration({ configuration: pageLayoutWidgetEntityWithoutRelations.configuration, fieldMetadataUniversalIdentifierById, + viewFieldGroupUniversalIdentifierById, }); return { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-update-page-layout-widget-input-to-flat-page-layout-widget-to-update-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-update-page-layout-widget-input-to-flat-page-layout-widget-to-update-or-throw.util.ts index e35d98435e..63cd368623 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-update-page-layout-widget-input-to-flat-page-layout-widget-to-update-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-update-page-layout-widget-input-to-flat-page-layout-widget-to-update-or-throw.util.ts @@ -30,12 +30,15 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro flatPageLayoutWidgetMaps, flatObjectMetadataMaps, flatFieldMetadataMaps, + flatViewFieldGroupMaps, }: { updatePageLayoutWidgetInput: UpdatePageLayoutWidgetInputWithId; flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps; } & Pick< AllFlatEntityMaps, - 'flatObjectMetadataMaps' | 'flatFieldMetadataMaps' + | 'flatObjectMetadataMaps' + | 'flatFieldMetadataMaps' + | 'flatViewFieldGroupMaps' >): FlatPageLayoutWidget => { const { id: pageLayoutWidgetToUpdateId } = extractAndSanitizeObjectStringFields(rawUpdatePageLayoutWidgetInput, [ @@ -97,6 +100,8 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro configuration: flatPageLayoutWidgetToUpdate.configuration, fieldMetadataUniversalIdentifierById: flatFieldMetadataMaps.universalIdentifierById, + viewFieldGroupUniversalIdentifierById: + flatViewFieldGroupMaps.universalIdentifierById, }); } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/__tests__/compute-flat-view-fields-from-fields-widgets.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/__tests__/compute-flat-view-fields-from-fields-widgets.util.spec.ts new file mode 100644 index 0000000000..6b7e01cf39 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/__tests__/compute-flat-view-fields-from-fields-widgets.util.spec.ts @@ -0,0 +1,743 @@ +import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant'; +import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type'; +import { computeFlatViewFieldsFromFieldsWidgets } from 'src/engine/metadata-modules/flat-view-field/utils/compute-flat-view-fields-from-fields-widgets.util'; +import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type'; +import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type'; +import { type FlatViewMaps } from 'src/engine/metadata-modules/flat-view/types/flat-view-maps.type'; +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'; + +const APPLICATION_UNIVERSAL_IDENTIFIER = 'app-uid-1'; +const VIEW_ID = 'view-db-id-1'; +const VIEW_UNIVERSAL_IDENTIFIER = 'view-uid-1'; +const VIEW_FIELD_GROUP_ID = 'vfg-db-id-1'; +const VIEW_FIELD_GROUP_UNIVERSAL_IDENTIFIER = 'vfg-uid-1'; +const OBJECT_METADATA_UNIVERSAL_IDENTIFIER = 'obj-uid-1'; + +const buildEmptyFlatEntityMaps = () => ({ + byUniversalIdentifier: {}, + universalIdentifierById: {}, + universalIdentifiersByApplicationId: {}, +}); + +const buildFlatViewMaps = ( + entries: { id: string; universalIdentifier: string }[] = [], +): FlatViewMaps => + ({ + byUniversalIdentifier: Object.fromEntries( + entries.map((entry) => [ + entry.universalIdentifier, + { universalIdentifier: entry.universalIdentifier, id: entry.id }, + ]), + ), + universalIdentifierById: Object.fromEntries( + entries.map((entry) => [entry.id, entry.universalIdentifier]), + ), + universalIdentifiersByApplicationId: {}, + }) as unknown as FlatViewMaps; + +const buildFlatViewFieldGroupMaps = ( + entries: { id: string; universalIdentifier: string }[] = [], +): FlatViewFieldGroupMaps => + ({ + byUniversalIdentifier: Object.fromEntries( + entries.map((entry) => [ + entry.universalIdentifier, + { universalIdentifier: entry.universalIdentifier, id: entry.id }, + ]), + ), + universalIdentifierById: Object.fromEntries( + entries.map((entry) => [entry.id, entry.universalIdentifier]), + ), + universalIdentifiersByApplicationId: {}, + }) as unknown as FlatViewFieldGroupMaps; + +const buildFlatViewFieldMaps = ( + entries: { + universalIdentifier: string; + viewId: string; + viewFieldGroupId: string | null; + position: number; + deletedAt: string | null; + }[] = [], +): FlatViewFieldMaps => + ({ + byUniversalIdentifier: Object.fromEntries( + entries.map((entry) => [entry.universalIdentifier, entry]), + ), + universalIdentifierById: {}, + universalIdentifiersByApplicationId: {}, + }) as unknown as FlatViewFieldMaps; + +const buildFieldsWidget = ({ + widgetUniversalIdentifier = 'widget-uid-1', + objectMetadataUniversalIdentifier = OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + viewId = VIEW_ID, + isVisible = true, + viewFieldGroupId = null as string | null, + deletedAt = null as string | null, +}: { + widgetUniversalIdentifier?: string; + objectMetadataUniversalIdentifier?: string; + viewId?: string | null; + isVisible?: boolean; + viewFieldGroupId?: string | null; + deletedAt?: string | null; +} = {}) => ({ + universalIdentifier: widgetUniversalIdentifier, + objectMetadataUniversalIdentifier, + type: WidgetType.FIELDS, + deletedAt, + configuration: { + configurationType: WidgetConfigurationType.FIELDS, + viewId, + newFieldDefaultConfiguration: { + isVisible, + viewFieldGroupId, + }, + }, + universalConfiguration: null, +}); + +const buildFlatPageLayoutWidgetMaps = ( + widgets: ReturnType[], +): FlatPageLayoutWidgetMaps => + ({ + byUniversalIdentifier: Object.fromEntries( + widgets.map((widget) => [widget.universalIdentifier, widget]), + ), + universalIdentifierById: {}, + universalIdentifiersByApplicationId: {}, + }) as unknown as FlatPageLayoutWidgetMaps; + +describe('computeFlatViewFieldsFromFieldsWidgets', () => { + describe('when no matching widgets exist', () => { + it('should return empty array when widget maps are empty', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + ], + flatPageLayoutWidgetMaps: + buildEmptyFlatEntityMaps() as unknown as FlatPageLayoutWidgetMaps, + flatViewFieldMaps: buildFlatViewFieldMaps(), + flatViewMaps: buildFlatViewMaps(), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result).toEqual([]); + }); + + it('should return empty array when fieldsToCreate is empty', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget(), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps(), + flatViewMaps: buildFlatViewMaps([ + { id: VIEW_ID, universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER }, + ]), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result).toEqual([]); + }); + + it('should skip widgets for a different object', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: 'other-obj-uid', + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget(), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps(), + flatViewMaps: buildFlatViewMaps([ + { id: VIEW_ID, universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER }, + ]), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result).toEqual([]); + }); + + it('should skip deleted widgets', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget({ deletedAt: '2024-01-01T00:00:00.000Z' }), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps(), + flatViewMaps: buildFlatViewMaps([ + { id: VIEW_ID, universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER }, + ]), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result).toEqual([]); + }); + + it('should skip widgets without viewId', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget({ viewId: null }), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps(), + flatViewMaps: buildFlatViewMaps(), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result).toEqual([]); + }); + + it('should skip widgets whose viewId cannot be resolved', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget({ viewId: 'non-existent-view-id' }), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps(), + flatViewMaps: buildFlatViewMaps(), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result).toEqual([]); + }); + }); + + describe('when creating view fields for a single field', () => { + it('should create a view field with correct properties', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget({ isVisible: true }), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps(), + flatViewMaps: buildFlatViewMaps([ + { id: VIEW_ID, universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER }, + ]), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + viewUniversalIdentifier: VIEW_UNIVERSAL_IDENTIFIER, + viewFieldGroupUniversalIdentifier: null, + isVisible: true, + size: DEFAULT_VIEW_FIELD_SIZE, + position: 0, + aggregateOperation: null, + deletedAt: null, + }); + expect(result[0].universalIdentifier).toBeDefined(); + expect(result[0].createdAt).toBeDefined(); + expect(result[0].updatedAt).toBeDefined(); + }); + + it('should respect isVisible: false from configuration', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget({ isVisible: false }), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps(), + flatViewMaps: buildFlatViewMaps([ + { id: VIEW_ID, universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER }, + ]), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result).toHaveLength(1); + expect(result[0].isVisible).toBe(false); + }); + }); + + describe('position computation', () => { + it('should start at position 0 when no existing view fields', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget(), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps(), + flatViewMaps: buildFlatViewMaps([ + { id: VIEW_ID, universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER }, + ]), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result[0].position).toBe(0); + }); + + it('should append after the last existing view field position', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget(), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps([ + { + universalIdentifier: 'existing-vf-1', + viewId: VIEW_ID, + viewFieldGroupId: null, + position: 3, + deletedAt: null, + }, + { + universalIdentifier: 'existing-vf-2', + viewId: VIEW_ID, + viewFieldGroupId: null, + position: 7, + deletedAt: null, + }, + ]), + flatViewMaps: buildFlatViewMaps([ + { id: VIEW_ID, universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER }, + ]), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result[0].position).toBe(8); + }); + + it('should assign sequential positions for multiple fields in the same batch', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-2', + }, + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-3', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget(), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps([ + { + universalIdentifier: 'existing-vf-1', + viewId: VIEW_ID, + viewFieldGroupId: null, + position: 2, + deletedAt: null, + }, + ]), + flatViewMaps: buildFlatViewMaps([ + { id: VIEW_ID, universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER }, + ]), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result).toHaveLength(3); + expect(result[0].position).toBe(3); + expect(result[1].position).toBe(4); + expect(result[2].position).toBe(5); + }); + + it('should ignore deleted view fields when computing position', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget(), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps([ + { + universalIdentifier: 'existing-vf-1', + viewId: VIEW_ID, + viewFieldGroupId: null, + position: 5, + deletedAt: null, + }, + { + universalIdentifier: 'deleted-vf', + viewId: VIEW_ID, + viewFieldGroupId: null, + position: 99, + deletedAt: '2024-01-01T00:00:00.000Z', + }, + ]), + flatViewMaps: buildFlatViewMaps([ + { id: VIEW_ID, universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER }, + ]), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result[0].position).toBe(6); + }); + + it('should ignore view fields from a different view', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget(), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps([ + { + universalIdentifier: 'other-view-vf', + viewId: 'other-view-db-id', + viewFieldGroupId: null, + position: 50, + deletedAt: null, + }, + ]), + flatViewMaps: buildFlatViewMaps([ + { id: VIEW_ID, universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER }, + ]), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result[0].position).toBe(0); + }); + }); + + describe('view field group handling', () => { + it('should resolve viewFieldGroupUniversalIdentifier when viewFieldGroupId is set', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget({ viewFieldGroupId: VIEW_FIELD_GROUP_ID }), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps(), + flatViewMaps: buildFlatViewMaps([ + { id: VIEW_ID, universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER }, + ]), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps([ + { + id: VIEW_FIELD_GROUP_ID, + universalIdentifier: VIEW_FIELD_GROUP_UNIVERSAL_IDENTIFIER, + }, + ]), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result).toHaveLength(1); + expect(result[0].viewFieldGroupUniversalIdentifier).toBe( + VIEW_FIELD_GROUP_UNIVERSAL_IDENTIFIER, + ); + }); + + it('should set viewFieldGroupUniversalIdentifier to null when viewFieldGroupId is null', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget({ viewFieldGroupId: null }), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps(), + flatViewMaps: buildFlatViewMaps([ + { id: VIEW_ID, universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER }, + ]), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result[0].viewFieldGroupUniversalIdentifier).toBeNull(); + }); + + it('should compute position only from view fields in the same group', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget({ viewFieldGroupId: VIEW_FIELD_GROUP_ID }), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps([ + { + universalIdentifier: 'vf-in-group', + viewId: VIEW_ID, + viewFieldGroupId: VIEW_FIELD_GROUP_ID, + position: 2, + deletedAt: null, + }, + { + universalIdentifier: 'vf-no-group', + viewId: VIEW_ID, + viewFieldGroupId: null, + position: 99, + deletedAt: null, + }, + { + universalIdentifier: 'vf-other-group', + viewId: VIEW_ID, + viewFieldGroupId: 'other-group-id', + position: 50, + deletedAt: null, + }, + ]), + flatViewMaps: buildFlatViewMaps([ + { id: VIEW_ID, universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER }, + ]), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps([ + { + id: VIEW_FIELD_GROUP_ID, + universalIdentifier: VIEW_FIELD_GROUP_UNIVERSAL_IDENTIFIER, + }, + ]), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result[0].position).toBe(3); + }); + + it('should compute position only from ungrouped view fields when viewFieldGroupId is null', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget({ viewFieldGroupId: null }), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps([ + { + universalIdentifier: 'vf-no-group', + viewId: VIEW_ID, + viewFieldGroupId: null, + position: 1, + deletedAt: null, + }, + { + universalIdentifier: 'vf-in-group', + viewId: VIEW_ID, + viewFieldGroupId: VIEW_FIELD_GROUP_ID, + position: 99, + deletedAt: null, + }, + ]), + flatViewMaps: buildFlatViewMaps([ + { id: VIEW_ID, universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER }, + ]), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result[0].position).toBe(2); + }); + }); + + describe('multiple widgets for the same object', () => { + it('should create view fields for each matching widget', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget({ + widgetUniversalIdentifier: 'widget-uid-1', + viewId: 'view-db-id-A', + }), + buildFieldsWidget({ + widgetUniversalIdentifier: 'widget-uid-2', + viewId: 'view-db-id-B', + }), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps(), + flatViewMaps: buildFlatViewMaps([ + { id: 'view-db-id-A', universalIdentifier: 'view-uid-A' }, + { id: 'view-db-id-B', universalIdentifier: 'view-uid-B' }, + ]), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result).toHaveLength(2); + expect(result[0].viewUniversalIdentifier).toBe('view-uid-A'); + expect(result[1].viewUniversalIdentifier).toBe('view-uid-B'); + }); + }); + + describe('multiple objects', () => { + it('should create view fields for fields across different objects', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: 'obj-uid-1', + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + { + objectMetadataUniversalIdentifier: 'obj-uid-2', + fieldMetadataUniversalIdentifier: 'field-uid-2', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget({ + widgetUniversalIdentifier: 'widget-uid-1', + objectMetadataUniversalIdentifier: 'obj-uid-1', + viewId: 'view-db-id-A', + }), + buildFieldsWidget({ + widgetUniversalIdentifier: 'widget-uid-2', + objectMetadataUniversalIdentifier: 'obj-uid-2', + viewId: 'view-db-id-B', + }), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps(), + flatViewMaps: buildFlatViewMaps([ + { id: 'view-db-id-A', universalIdentifier: 'view-uid-A' }, + { id: 'view-db-id-B', universalIdentifier: 'view-uid-B' }, + ]), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + expect(result).toHaveLength(2); + + const viewFieldForObj1 = result.find( + (viewField) => + viewField.fieldMetadataUniversalIdentifier === 'field-uid-1', + ); + const viewFieldForObj2 = result.find( + (viewField) => + viewField.fieldMetadataUniversalIdentifier === 'field-uid-2', + ); + + expect(viewFieldForObj1?.viewUniversalIdentifier).toBe('view-uid-A'); + expect(viewFieldForObj2?.viewUniversalIdentifier).toBe('view-uid-B'); + }); + }); + + describe('unique universal identifiers', () => { + it('should generate unique universalIdentifier for each created view field', () => { + const result = computeFlatViewFieldsFromFieldsWidgets({ + fieldsToCreate: [ + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-1', + }, + { + objectMetadataUniversalIdentifier: + OBJECT_METADATA_UNIVERSAL_IDENTIFIER, + fieldMetadataUniversalIdentifier: 'field-uid-2', + }, + ], + flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ + buildFieldsWidget(), + ]), + flatViewFieldMaps: buildFlatViewFieldMaps(), + flatViewMaps: buildFlatViewMaps([ + { id: VIEW_ID, universalIdentifier: VIEW_UNIVERSAL_IDENTIFIER }, + ]), + flatViewFieldGroupMaps: buildFlatViewFieldGroupMaps(), + applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, + }); + + const universalIdentifiers = result.map( + (viewField) => viewField.universalIdentifier, + ); + + expect(new Set(universalIdentifiers).size).toBe( + universalIdentifiers.length, + ); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/compute-flat-view-fields-from-fields-widgets.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/compute-flat-view-fields-from-fields-widgets.util.ts new file mode 100644 index 0000000000..aa51289978 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/compute-flat-view-fields-from-fields-widgets.util.ts @@ -0,0 +1,183 @@ +import { isDefined } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; + +import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type'; +import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type'; +import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant'; +import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type'; +import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type'; +import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type'; +import { type FlatViewMaps } from 'src/engine/metadata-modules/flat-view/types/flat-view-maps.type'; +import { type FieldsConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/fields-configuration.dto'; +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 { type AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type'; +import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type'; + +type FieldToCreateInfo = { + objectMetadataUniversalIdentifier: string; + fieldMetadataUniversalIdentifier: string; +}; + +const isFieldsWidgetConfiguration = ( + configuration: AllPageLayoutWidgetConfiguration, +): configuration is FieldsConfigurationDTO => { + return ( + isDefined(configuration) && + configuration.configurationType === WidgetConfigurationType.FIELDS + ); +}; + +const getMatchingFieldsWidgets = ({ + objectMetadataUniversalIdentifier, + flatPageLayoutWidgetMaps, +}: { + objectMetadataUniversalIdentifier: string; + flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps; +}): FlatPageLayoutWidget[] => + Object.values(flatPageLayoutWidgetMaps.byUniversalIdentifier) + .filter(isDefined) + .filter( + (widget) => + !isDefined(widget.deletedAt) && + widget.type === WidgetType.FIELDS && + widget.objectMetadataUniversalIdentifier === + objectMetadataUniversalIdentifier && + isFieldsWidgetConfiguration(widget.configuration) && + isDefined(widget.configuration.viewId) && + isDefined(widget.configuration.newFieldDefaultConfiguration), + ); + +const computeNextPosition = ({ + viewId, + viewFieldGroupId, + flatViewFieldMaps, +}: { + viewId: string; + viewFieldGroupId: string | null; + flatViewFieldMaps: FlatViewFieldMaps; +}): number => { + const existingViewFields = Object.values( + flatViewFieldMaps.byUniversalIdentifier, + ) + .filter(isDefined) + .filter( + (viewField: FlatViewField) => + !isDefined(viewField.deletedAt) && viewField.viewId === viewId, + ) + .filter( + (viewField: FlatViewField) => + viewField.viewFieldGroupId === viewFieldGroupId, + ); + + if (existingViewFields.length === 0) { + return 0; + } + + const maxPosition = Math.max( + ...existingViewFields.map((viewField) => viewField.position), + ); + + return maxPosition + 1; +}; + +export const computeFlatViewFieldsFromFieldsWidgets = ({ + fieldsToCreate, + flatPageLayoutWidgetMaps, + flatViewFieldMaps, + flatViewMaps, + flatViewFieldGroupMaps, + applicationUniversalIdentifier, +}: { + fieldsToCreate: FieldToCreateInfo[]; + flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps; + flatViewFieldMaps: FlatViewFieldMaps; + flatViewMaps: FlatViewMaps; + flatViewFieldGroupMaps: FlatViewFieldGroupMaps; + applicationUniversalIdentifier: string; +}): UniversalFlatViewField[] => { + const flatViewFieldsToCreate: UniversalFlatViewField[] = []; + const now = new Date().toISOString(); + + const objectMetadataUniversalIdentifiers = [ + ...new Set( + fieldsToCreate.map((field) => field.objectMetadataUniversalIdentifier), + ), + ]; + + const nextPositionByKey = new Map(); + + for (const objectMetadataUniversalIdentifier of objectMetadataUniversalIdentifiers) { + const matchingWidgets = getMatchingFieldsWidgets({ + objectMetadataUniversalIdentifier, + flatPageLayoutWidgetMaps, + }); + + const fieldsForObject = fieldsToCreate.filter( + (field) => + field.objectMetadataUniversalIdentifier === + objectMetadataUniversalIdentifier, + ); + + for (const widget of matchingWidgets) { + if (!isFieldsWidgetConfiguration(widget.configuration)) { + continue; + } + + const configuration = widget.configuration; + + const viewId = configuration.viewId!; + const { isVisible, viewFieldGroupId } = + configuration.newFieldDefaultConfiguration!; + + const viewUniversalIdentifier = + flatViewMaps.universalIdentifierById[viewId] ?? null; + + if (!isDefined(viewUniversalIdentifier)) { + continue; + } + + const viewFieldGroupUniversalIdentifier = isDefined(viewFieldGroupId) + ? (flatViewFieldGroupMaps.universalIdentifierById[viewFieldGroupId] ?? + null) + : null; + + const positionKey = `${viewId}:${viewFieldGroupId ?? 'null'}`; + + if (!nextPositionByKey.has(positionKey)) { + nextPositionByKey.set( + positionKey, + computeNextPosition({ + viewId, + viewFieldGroupId, + flatViewFieldMaps, + }), + ); + } + + for (const field of fieldsForObject) { + const position = nextPositionByKey.get(positionKey)!; + + nextPositionByKey.set(positionKey, position + 1); + + flatViewFieldsToCreate.push({ + universalIdentifier: v4(), + applicationUniversalIdentifier, + fieldMetadataUniversalIdentifier: + field.fieldMetadataUniversalIdentifier, + viewUniversalIdentifier, + viewFieldGroupUniversalIdentifier, + isVisible, + size: DEFAULT_VIEW_FIELD_SIZE, + position, + aggregateOperation: null, + createdAt: now, + updatedAt: now, + deletedAt: null, + }); + } + } + } + + return flatViewFieldsToCreate; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 0b1598c841..3499d92051 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -2,14 +2,14 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; -import { fromArrayToUniqueKeyRecord, isDefined } from 'twenty-shared/utils'; -import { FindManyOptions, FindOneOptions, Repository } from 'typeorm'; -import { v4 as uuidv4, v4 } from 'uuid'; import { ViewOpenRecordIn, ViewType, ViewVisibility, } from 'twenty-shared/types'; +import { fromArrayToUniqueKeyRecord, isDefined } from 'twenty-shared/utils'; +import { FindManyOptions, FindOneOptions, Repository } from 'typeorm'; +import { v4 as uuidv4, v4 } from 'uuid'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; @@ -443,7 +443,10 @@ export class ObjectMetadataService extends TypeOrmQueryService Boolean) + @IsBoolean() + isVisible: boolean; + + @Field(() => String, { nullable: true }) + @IsOptional() + @IsUUID() + viewFieldGroupId: string | null; +} + @ObjectType('FieldsConfiguration') export class FieldsConfigurationDTO implements PageLayoutWidgetConfigurationBase @@ -18,4 +38,10 @@ export class FieldsConfigurationDTO @IsOptional() @IsUUID() viewId: string | null; + + @Field(() => NewFieldDefaultConfigurationDTO, { nullable: true }) + @IsOptional() + @ValidateNested() + @Type(() => NewFieldDefaultConfigurationDTO) + newFieldDefaultConfiguration: NewFieldDefaultConfigurationDTO | null; } diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service.ts index 9dd2c09ab0..5fa90bed71 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service.ts @@ -243,6 +243,7 @@ export class PageLayoutWidgetService { flatPageLayoutTabMaps, flatObjectMetadataMaps, flatFieldMetadataMaps, + flatViewFieldGroupMaps, } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -251,6 +252,7 @@ export class PageLayoutWidgetService { 'flatPageLayoutTabMaps', 'flatObjectMetadataMaps', 'flatFieldMetadataMaps', + 'flatViewFieldGroupMaps', ], }, ); @@ -263,6 +265,7 @@ export class PageLayoutWidgetService { flatPageLayoutTabMaps, flatObjectMetadataMaps, flatFieldMetadataMaps, + flatViewFieldGroupMaps, }); if (isDefined(createInput.configuration)) { @@ -321,13 +324,17 @@ export class PageLayoutWidgetService { const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps, flatFieldMetadataMaps: existingFlatFieldMetadataMaps, - } = - await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( - { - workspaceId, - flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], - }, - ); + flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps, + } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: [ + 'flatObjectMetadataMaps', + 'flatFieldMetadataMaps', + 'flatViewFieldGroupMaps', + ], + }, + ); const isConfigurationBeingUpdated = Object.prototype.hasOwnProperty.call( updateData, @@ -357,6 +364,7 @@ export class PageLayoutWidgetService { flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps, flatObjectMetadataMaps: existingFlatObjectMetadataMaps, flatFieldMetadataMaps: existingFlatFieldMetadataMaps, + flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps, }); const shouldValidateChartFields = diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-duplication.service.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-duplication.service.ts index 22e77d1bb2..64e94ffe0e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-duplication.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-duplication.service.ts @@ -52,6 +52,7 @@ export class PageLayoutDuplicationService { flatPageLayoutWidgetMaps, flatObjectMetadataMaps, flatFieldMetadataMaps, + flatViewFieldGroupMaps, } = await this.getPageLayoutFlatEntityMaps(workspaceId); const originalFlatLayout = this.findOriginalLayoutOrThrow( @@ -114,6 +115,7 @@ export class PageLayoutDuplicationService { flatPageLayoutTabMaps: optimisticFlatPageLayoutTabMaps, flatObjectMetadataMaps, flatFieldMetadataMaps, + flatViewFieldGroupMaps, }); const validateAndBuildResult = @@ -181,6 +183,7 @@ export class PageLayoutDuplicationService { 'flatPageLayoutWidgetMaps', 'flatObjectMetadataMaps', 'flatFieldMetadataMaps', + 'flatViewFieldGroupMaps', ], }, ); @@ -290,6 +293,7 @@ export class PageLayoutDuplicationService { flatPageLayoutTabMaps, flatObjectMetadataMaps, flatFieldMetadataMaps, + flatViewFieldGroupMaps, }: { originalTabsWithWidgets: { tab: FlatPageLayoutTab; @@ -301,6 +305,7 @@ export class PageLayoutDuplicationService { flatPageLayoutTabMaps: AllFlatEntityMaps['flatPageLayoutTabMaps']; flatObjectMetadataMaps: AllFlatEntityMaps['flatObjectMetadataMaps']; flatFieldMetadataMaps: AllFlatEntityMaps['flatFieldMetadataMaps']; + flatViewFieldGroupMaps: AllFlatEntityMaps['flatViewFieldGroupMaps']; }): FlatPageLayoutWidget[] { return originalTabsWithWidgets.flatMap(({ tab, widgets }) => { const newTabId = originalTabIdToNewTabIdMap.get(tab.id)!; @@ -320,6 +325,7 @@ export class PageLayoutDuplicationService { flatPageLayoutTabMaps, flatObjectMetadataMaps, flatFieldMetadataMaps, + flatViewFieldGroupMaps, }), ); }); diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts index fa583964f9..fe88fe43d3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-update.service.ts @@ -116,11 +116,19 @@ export class PageLayoutUpdateService { workspaceCustomFlatApplication.universalIdentifier, }); - const { flatObjectMetadataMaps, flatFieldMetadataMaps } = + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatViewFieldGroupMaps, + } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, - flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], + flatMapsKeys: [ + 'flatObjectMetadataMaps', + 'flatFieldMetadataMaps', + 'flatViewFieldGroupMaps', + ], }, ); @@ -144,6 +152,7 @@ export class PageLayoutUpdateService { workspaceCustomApplicationUniversalIdentifier: workspaceCustomFlatApplication.universalIdentifier, flatFieldMetadataMaps, + flatViewFieldGroupMaps, }); const validateAndBuildResult = @@ -355,6 +364,7 @@ export class PageLayoutUpdateService { workspaceCustomApplicationId, workspaceCustomApplicationUniversalIdentifier, flatFieldMetadataMaps, + flatViewFieldGroupMaps, }: { tabs: UpdatePageLayoutTabWithWidgetsInput[]; workspaceId: string; @@ -364,6 +374,7 @@ export class PageLayoutUpdateService { AllFlatEntityMaps, | 'flatObjectMetadataMaps' | 'flatFieldMetadataMaps' + | 'flatViewFieldGroupMaps' | 'flatPageLayoutTabMaps' | 'flatPageLayoutWidgetMaps' >): { @@ -386,6 +397,7 @@ export class PageLayoutUpdateService { workspaceCustomApplicationId, workspaceCustomApplicationUniversalIdentifier, flatFieldMetadataMaps, + flatViewFieldGroupMaps, }); allWidgetsToCreate.push(...widgetsToCreate); @@ -409,6 +421,7 @@ export class PageLayoutUpdateService { workspaceCustomApplicationId, workspaceCustomApplicationUniversalIdentifier, flatFieldMetadataMaps, + flatViewFieldGroupMaps, }: { tabId: string; widgets: UpdatePageLayoutWidgetWithIdInput[]; @@ -419,6 +432,7 @@ export class PageLayoutUpdateService { AllFlatEntityMaps, | 'flatObjectMetadataMaps' | 'flatFieldMetadataMaps' + | 'flatViewFieldGroupMaps' | 'flatPageLayoutTabMaps' | 'flatPageLayoutWidgetMaps' >): { @@ -487,6 +501,8 @@ export class PageLayoutUpdateService { configuration: widgetInput.configuration, fieldMetadataUniversalIdentifierById: flatFieldMetadataMaps.universalIdentifierById, + viewFieldGroupUniversalIdentifierById: + flatViewFieldGroupMaps.universalIdentifierById, shouldThrowOnMissingIdentifier: true, }), }; @@ -532,6 +548,8 @@ export class PageLayoutUpdateService { configuration: updatedConfiguration, fieldMetadataUniversalIdentifierById: flatFieldMetadataMaps.universalIdentifierById, + viewFieldGroupUniversalIdentifierById: + flatViewFieldGroupMaps.universalIdentifierById, }), }), }; @@ -578,6 +596,8 @@ export class PageLayoutUpdateService { configuration: restoredConfiguration, fieldMetadataUniversalIdentifierById: flatFieldMetadataMaps.universalIdentifierById, + viewFieldGroupUniversalIdentifierById: + flatViewFieldGroupMaps.universalIdentifierById, }), }), }; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts index db7b2c0c70..1bb9d1cbc4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts @@ -101,6 +101,11 @@ export const seedFeatureFlags = async ({ workspaceId: workspaceId, value: true, }, + { + key: FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED, + workspaceId: workspaceId, + value: true, + }, ]) .execute(); }; diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/build-standard-flat-page-layout-widget-metadata-maps.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/build-standard-flat-page-layout-widget-metadata-maps.util.ts index 0a9a813d54..f2e5930056 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/build-standard-flat-page-layout-widget-metadata-maps.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/build-standard-flat-page-layout-widget-metadata-maps.util.ts @@ -128,34 +128,71 @@ const buildFieldsWidgetConfiguration = ({ configuration: { configurationType: WidgetConfigurationType.FIELDS, viewId: null, + newFieldDefaultConfiguration: { + isVisible: true, + viewFieldGroupId: null, + }, }, universalConfiguration: { configurationType: WidgetConfigurationType.FIELDS, viewId: null, + newFieldDefaultConfiguration: { + isVisible: true, + viewFieldGroupId: null, + }, }, }; } const views = standardObjectMetadataRelatedEntityIds[objectName] - .views as Record; + .views as Record< + string, + { + id: string; + viewFieldGroups?: Record; + } + >; const viewId = views[recordPageFieldsViewName]?.id ?? null; // @ts-expect-error ignore const viewDefinition = STANDARD_OBJECTS[objectName].views?.[ recordPageFieldsViewName - ] as { universalIdentifier: string } | undefined; + ] as + | { + universalIdentifier: string; + viewFieldGroups?: Record; + } + | undefined; const viewUniversalIdentifier = viewDefinition?.universalIdentifier ?? null; + const otherViewFieldGroupId = + views[recordPageFieldsViewName]?.viewFieldGroups?.other?.id ?? null; + + const otherViewFieldGroupUniversalIdentifier = + viewDefinition?.viewFieldGroups?.other?.universalIdentifier ?? null; + + const newFieldDefaultConfiguration = { + isVisible: true, + viewFieldGroupId: otherViewFieldGroupId, + }; + + const universalNewFieldDefaultConfiguration = { + isVisible: true, + viewFieldGroupId: otherViewFieldGroupUniversalIdentifier, + }; + return { configuration: { configurationType: WidgetConfigurationType.FIELDS, viewId, + newFieldDefaultConfiguration, }, universalConfiguration: { configurationType: WidgetConfigurationType.FIELDS, viewId: viewUniversalIdentifier, + newFieldDefaultConfiguration: universalNewFieldDefaultConfiguration, }, }; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/create-page-layout-widget-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/create-page-layout-widget-action-handler.service.ts index cfd765186d..0a8edb372d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/create-page-layout-widget-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/create-page-layout-widget-action-handler.service.ts @@ -43,6 +43,7 @@ export class CreatePageLayoutWidgetActionHandlerService extends WorkspaceMigrati universalConfiguration: action.flatEntity.universalConfiguration, flatFieldMetadataMaps: allFlatEntityMaps.flatFieldMetadataMaps, flatViewMaps: allFlatEntityMaps.flatViewMaps, + flatViewFieldGroupMaps: allFlatEntityMaps.flatViewFieldGroupMaps, }); const emptyUniversalForeignKeyAggregators = diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/update-page-layout-widget-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/update-page-layout-widget-action-handler.service.ts index 288a4a751b..c52201fb74 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/update-page-layout-widget-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/update-page-layout-widget-action-handler.service.ts @@ -51,6 +51,8 @@ export class UpdatePageLayoutWidgetActionHandlerService extends WorkspaceMigrati universalConfiguration, flatFieldMetadataMaps: allFlatEntityMaps.flatFieldMetadataMaps, flatViewMaps: allFlatEntityMaps.flatViewMaps, + flatViewFieldGroupMaps: + allFlatEntityMaps.flatViewFieldGroupMaps, }), }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/utils/from-universal-configuration-to-flat-page-layout-widget-configuration.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/utils/from-universal-configuration-to-flat-page-layout-widget-configuration.util.ts index 3be33337cf..599359832c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/utils/from-universal-configuration-to-flat-page-layout-widget-configuration.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/utils/from-universal-configuration-to-flat-page-layout-widget-configuration.util.ts @@ -42,10 +42,12 @@ export const fromUniversalConfigurationToFlatPageLayoutWidgetConfiguration = ({ universalConfiguration, flatFieldMetadataMaps, flatViewMaps, + flatViewFieldGroupMaps, }: { universalConfiguration: FlatPageLayoutWidget['universalConfiguration']; flatFieldMetadataMaps: MetadataFlatEntityMaps<'fieldMetadata'>; flatViewMaps: MetadataFlatEntityMaps<'view'>; + flatViewFieldGroupMaps: MetadataFlatEntityMaps<'viewFieldGroup'>; }): FlatPageLayoutWidget['configuration'] => { switch (universalConfiguration.configurationType) { case WidgetConfigurationType.AGGREGATE_CHART: { @@ -198,26 +200,61 @@ export const fromUniversalConfigurationToFlatPageLayoutWidgetConfiguration = ({ } case WidgetConfigurationType.FIELDS: { - const { viewId: viewUniversalIdentifier, ...rest } = - universalConfiguration; + const { + viewId: viewUniversalIdentifier, + newFieldDefaultConfiguration: universalNewFieldDefaultConfiguration, + ...rest + } = universalConfiguration; - if (!isDefined(viewUniversalIdentifier)) { - return { ...rest, viewId: null }; + let viewId: string | null = null; + + if (isDefined(viewUniversalIdentifier)) { + const flatView = findFlatEntityByUniversalIdentifier({ + flatEntityMaps: flatViewMaps, + universalIdentifier: viewUniversalIdentifier, + }); + + if (!isDefined(flatView)) { + throw new FlatEntityMapsException( + `View not found for universal identifier: ${viewUniversalIdentifier}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + viewId = flatView.id; } - const flatView = findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatViewMaps, - universalIdentifier: viewUniversalIdentifier, - }); + let newFieldDefaultConfiguration: + | { isVisible: boolean; viewFieldGroupId: string | null } + | null + | undefined = universalNewFieldDefaultConfiguration; - if (!isDefined(flatView)) { - throw new FlatEntityMapsException( - `View not found for universal identifier: ${viewUniversalIdentifier}`, - FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, - ); + if ( + isDefined(universalNewFieldDefaultConfiguration) && + isDefined(universalNewFieldDefaultConfiguration.viewFieldGroupId) + ) { + const viewFieldGroupUniversalIdentifier = + universalNewFieldDefaultConfiguration.viewFieldGroupId; + + const flatViewFieldGroup = findFlatEntityByUniversalIdentifier({ + flatEntityMaps: flatViewFieldGroupMaps, + universalIdentifier: viewFieldGroupUniversalIdentifier, + }); + + if (!isDefined(flatViewFieldGroup)) { + throw new FlatEntityMapsException( + `View field group not found for universal identifier: ${viewFieldGroupUniversalIdentifier}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + newFieldDefaultConfiguration = { + isVisible: universalNewFieldDefaultConfiguration.isVisible, + viewFieldGroupId: flatViewFieldGroup.id, + }; } - return { ...rest, viewId: flatView.id }; + return { ...rest, viewId, newFieldDefaultConfiguration }; } case WidgetConfigurationType.VIEW: