From 16e145b036af00cd2d9ccdd436be0b70f2deebdd Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Thu, 9 Apr 2026 15:59:04 +0200 Subject: [PATCH] Fix moving a widget to another tab (#19450) https://github.com/user-attachments/assets/aac81e79-7f2f-4a34-bf68-c76061086821 --------- Co-authored-by: Weiko --- .../src/metadata/generated/schema.graphql | 1 + .../src/metadata/generated/schema.ts | 2 +- .../src/metadata/generated/types.ts | 3 + .../src/generated-metadata/graphql.ts | 1 + ...s-and-fields-widget-view-fields.command.ts | 3 +- ...-universal-flat-page-layout-widget.util.ts | 2 +- ...ompare-and-stringify.constant.spec.ts.snap | 6 +- ...configuration-by-metadata-name.constant.ts | 3 +- .../split-entities-by-reset-strategy.util.ts | 1 + ...out-widget-editable-properties.constant.ts | 1 + ...-flat-page-layout-widget-to-create.util.ts | 1 + ...-entity-to-flat-page-layout-widget.util.ts | 14 + ...t-overrides-to-universal-overrides.util.ts | 54 +++ ...e-layout-widget-to-update-or-throw.util.ts | 30 +- ...-page-layout-with-tabs-and-widgets.util.ts | 9 +- .../inputs/update-page-layout-widget.input.ts | 5 + .../entities/page-layout-widget.entity.ts | 2 + .../services/page-layout-widget.service.ts | 8 +- .../services/page-layout-reset.service.ts | 8 +- .../services/page-layout-update.service.ts | 309 ++++++++++++------ ...d-page-layout-widget-flat-metadata.util.ts | 3 +- ...ized-relation-by-metadata-name.constant.ts | 1 + ...ge-layout-widget-action-handler.service.ts | 10 + ...ge-layout-widget-action-handler.service.ts | 31 +- ...es-to-page-layout-widget-overrides.util.ts | 39 +++ 25 files changed, 415 insertions(+), 132 deletions(-) create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-overrides-to-universal-overrides.util.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/utils/from-universal-overrides-to-page-layout-widget-overrides.util.ts diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 77430131f06..85c4adb0c18 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -4025,6 +4025,7 @@ input CreatePageLayoutWidgetInput { } input UpdatePageLayoutWidgetInput { + pageLayoutTabId: UUID title: String type: WidgetType objectMetadataId: UUID diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 618dce935b4..84ebe1aca0f 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -6549,7 +6549,7 @@ export interface GridPositionInput {row: Scalars['Float'],column: Scalars['Float export interface CreatePageLayoutWidgetInput {pageLayoutTabId: Scalars['UUID'],title: Scalars['String'],type: WidgetType,objectMetadataId?: (Scalars['UUID'] | null),gridPosition: GridPositionInput,position?: (Scalars['JSON'] | null),configuration: Scalars['JSON']} -export interface UpdatePageLayoutWidgetInput {title?: (Scalars['String'] | null),type?: (WidgetType | null),objectMetadataId?: (Scalars['UUID'] | null),gridPosition?: (GridPositionInput | null),position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null)} +export interface UpdatePageLayoutWidgetInput {pageLayoutTabId?: (Scalars['UUID'] | null),title?: (Scalars['String'] | null),type?: (WidgetType | null),objectMetadataId?: (Scalars['UUID'] | null),gridPosition?: (GridPositionInput | null),position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null)} export interface CreateLogicFunctionFromSourceInput {id?: (Scalars['UUID'] | null),universalIdentifier?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),toolInputSchema?: (Scalars['JSON'] | null),isTool?: (Scalars['Boolean'] | null),source?: (Scalars['JSON'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null)} diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index b445d14b2a7..a262b4737de 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -10207,6 +10207,9 @@ export default { ] }, "UpdatePageLayoutWidgetInput": { + "pageLayoutTabId": [ + 3 + ], "title": [ 1 ], diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 90ce28ccdca..b86e180ac68 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -5493,6 +5493,7 @@ export type UpdatePageLayoutWidgetInput = { configuration?: InputMaybe; gridPosition?: InputMaybe; objectMetadataId?: InputMaybe; + pageLayoutTabId?: InputMaybe; position?: InputMaybe; title?: InputMaybe; type?: InputMaybe; diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-22/1-22-workspace-command-1780000001000-backfill-page-layouts-and-fields-widget-view-fields.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-22/1-22-workspace-command-1780000001000-backfill-page-layouts-and-fields-widget-view-fields.command.ts index c6901264f1c..114d4f2b72c 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-22/1-22-workspace-command-1780000001000-backfill-page-layouts-and-fields-widget-view-fields.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-22/1-22-workspace-command-1780000001000-backfill-page-layouts-and-fields-widget-view-fields.command.ts @@ -12,10 +12,10 @@ import { v4 } from 'uuid'; 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 { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type'; @@ -779,6 +779,7 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO deletedAt: null, conditionalDisplay: null, overrides: null, + universalOverrides: null, }; widgetsToCreate.push(widget); diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util.ts index 7c3ae6c60cb..2b4c8f72752 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util.ts @@ -36,6 +36,6 @@ export const fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget = ({ createdAt: now, updatedAt: now, deletedAt: null, - overrides: null, + universalOverrides: null, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap index 3cb86716385..4390d9e6504 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should match snapshot 1`] = ` { @@ -195,14 +195,14 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma "deletedAt", "conditionalDisplay", "isActive", - "overrides", + "universalOverrides", ], "propertiesToStringify": [ "gridPosition", "position", "universalConfiguration", "conditionalDisplay", - "overrides", + "universalOverrides", ], }, "permissionFlag": { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts index f7016ba100e..bb73fa994cb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts @@ -921,6 +921,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = { toCompare: false, toStringify: false, universalProperty: 'pageLayoutTabUniversalIdentifier', + isOverridable: true, }, conditionalDisplay: { toCompare: true, @@ -937,7 +938,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = { overrides: { toCompare: true, toStringify: true, - universalProperty: undefined, + universalProperty: 'universalOverrides', }, }, pageLayoutTab: { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/split-entities-by-reset-strategy.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/split-entities-by-reset-strategy.util.ts index 0b0b013c2ef..16ee1038ed5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/split-entities-by-reset-strategy.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/split-entities-by-reset-strategy.util.ts @@ -36,6 +36,7 @@ export const splitEntitiesByResetStrategy = < ...entity, isActive: true as const, overrides: null, + ...('universalOverrides' in entity ? { universalOverrides: null } : {}), updatedAt: now, }); } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/constants/flat-page-layout-widget-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/constants/flat-page-layout-widget-editable-properties.constant.ts index 9556e907077..23ddf79f395 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/constants/flat-page-layout-widget-editable-properties.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/constants/flat-page-layout-widget-editable-properties.constant.ts @@ -8,4 +8,5 @@ export const FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES = [ 'position', 'configuration', 'conditionalDisplay', + 'pageLayoutTabId', ] as const satisfies MetadataEntityPropertyName<'pageLayoutWidget'>[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util.ts index c94d2bd08c5..70a1d1c751c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util.ts @@ -75,6 +75,7 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({ applicationUniversalIdentifier: flatApplication.universalIdentifier, conditionalDisplay: null, overrides: null, + universalOverrides: null, universalConfiguration: fromPageLayoutWidgetConfigurationToUniversalConfiguration({ configuration: createPageLayoutWidgetInput.configuration, 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 db8f9cac224..82e059ecc34 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 @@ -7,6 +7,7 @@ import { import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util'; import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type'; import { fromPageLayoutWidgetConfigurationToUniversalConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util'; +import { fromPageLayoutWidgetOverridesToUniversalOverrides } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-overrides-to-universal-overrides.util'; import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; type FromPageLayoutWidgetEntityToFlatPageLayoutWidgetArgs = @@ -81,6 +82,18 @@ export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = ({ viewUniversalIdentifierById, }); + const pageLayoutTabUniversalIdentifierById = Object.fromEntries( + pageLayoutTabIdToUniversalIdentifierMap.entries(), + ); + + const universalOverrides = isDefined(pageLayoutWidgetEntity.overrides) + ? fromPageLayoutWidgetOverridesToUniversalOverrides({ + overrides: pageLayoutWidgetEntity.overrides, + pageLayoutTabUniversalIdentifierById, + shouldThrowOnMissingIdentifier: false, + }) + : null; + return { ...pageLayoutWidgetEntityWithoutRelations, createdAt: pageLayoutWidgetEntity.createdAt.toISOString(), @@ -93,5 +106,6 @@ export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = ({ pageLayoutTabUniversalIdentifier, objectMetadataUniversalIdentifier, universalConfiguration: configurationWithUniversalIdentifiers, + universalOverrides, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-overrides-to-universal-overrides.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-overrides-to-universal-overrides.util.ts new file mode 100644 index 00000000000..efba976d612 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-overrides-to-universal-overrides.util.ts @@ -0,0 +1,54 @@ +import { type FormatRecordSerializedRelationProperties } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; +import { type PageLayoutWidgetOverrides } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity'; + +type UniversalPageLayoutWidgetOverrides = + FormatRecordSerializedRelationProperties; + +export const fromPageLayoutWidgetOverridesToUniversalOverrides = ({ + overrides, + pageLayoutTabUniversalIdentifierById, + shouldThrowOnMissingIdentifier = true, +}: { + overrides: PageLayoutWidgetOverrides; + pageLayoutTabUniversalIdentifierById: Partial>; + shouldThrowOnMissingIdentifier?: boolean; +}): UniversalPageLayoutWidgetOverrides => { + const { pageLayoutTabId, ...scalarOverrides } = overrides; + + if (!isDefined(pageLayoutTabId)) { + return { + ...scalarOverrides, + ...(pageLayoutTabId === null + ? { pageLayoutTabUniversalIdentifier: null } + : {}), + }; + } + + const pageLayoutTabUniversalIdentifier = + pageLayoutTabUniversalIdentifierById[pageLayoutTabId]; + + if (!isDefined(pageLayoutTabUniversalIdentifier)) { + if (shouldThrowOnMissingIdentifier) { + throw new FlatEntityMapsException( + `PageLayoutTab universal identifier not found for id: ${pageLayoutTabId}`, + FlatEntityMapsExceptionCode.RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND, + ); + } + + return { + ...scalarOverrides, + pageLayoutTabUniversalIdentifier: null, + }; + } + + return { + ...scalarOverrides, + pageLayoutTabUniversalIdentifier, + }; +}; 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 cda2317e3b2..f167853e6a7 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 @@ -11,6 +11,7 @@ import { FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES } from 'src/engine/metadata 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 { fromPageLayoutWidgetConfigurationToUniversalConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util'; +import { fromPageLayoutWidgetOverridesToUniversalOverrides } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-overrides-to-universal-overrides.util'; import { type UpdatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input'; import { PageLayoutWidgetException, @@ -35,6 +36,7 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro flatFrontComponentMaps, flatViewFieldGroupMaps, flatViewMaps, + flatPageLayoutTabMaps, callerApplicationUniversalIdentifier, workspaceCustomApplicationUniversalIdentifier, }: { @@ -49,6 +51,7 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro | 'flatFrontComponentMaps' | 'flatViewFieldGroupMaps' | 'flatViewMaps' + | 'flatPageLayoutTabMaps' >): FlatPageLayoutWidget => { const { id: pageLayoutWidgetToUpdateId } = extractAndSanitizeObjectStringFields(rawUpdatePageLayoutWidgetInput, [ @@ -103,7 +106,21 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro update: updatedEditableProperties, }), overrides, - }; + } as FlatPageLayoutWidget; + + if (updatedEditableProperties.pageLayoutTabId !== undefined) { + const { pageLayoutTabUniversalIdentifier } = + resolveEntityRelationUniversalIdentifiers({ + metadataName: 'pageLayoutWidget', + foreignKeyValues: { + pageLayoutTabId: flatPageLayoutWidgetToUpdate.pageLayoutTabId, + }, + flatEntityMaps: { flatPageLayoutTabMaps }, + }); + + flatPageLayoutWidgetToUpdate.pageLayoutTabUniversalIdentifier = + pageLayoutTabUniversalIdentifier; + } if (updatedEditableProperties.objectMetadataId !== undefined) { const { objectMetadataUniversalIdentifier } = @@ -133,5 +150,16 @@ export const fromUpdatePageLayoutWidgetInputToFlatPageLayoutWidgetToUpdateOrThro }); } + if (isDefined(overrides)) { + flatPageLayoutWidgetToUpdate.universalOverrides = + fromPageLayoutWidgetOverridesToUniversalOverrides({ + overrides, + pageLayoutTabUniversalIdentifierById: + flatPageLayoutTabMaps.universalIdentifierById, + }); + } else { + flatPageLayoutWidgetToUpdate.universalOverrides = null; + } + return flatPageLayoutWidgetToUpdate; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/reconstruct-flat-page-layout-with-tabs-and-widgets.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/reconstruct-flat-page-layout-with-tabs-and-widgets.util.ts index 2aeb7bea76c..c914e0df6df 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/reconstruct-flat-page-layout-with-tabs-and-widgets.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/reconstruct-flat-page-layout-with-tabs-and-widgets.util.ts @@ -5,6 +5,7 @@ import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-la 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 { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type'; +import { resolveOverridableEntityProperty } from 'src/engine/metadata-modules/utils/resolve-overridable-entity-property.util'; export type FlatPageLayoutTabWithWidgets = FlatPageLayoutTab & { widgets: FlatPageLayoutWidget[]; @@ -14,6 +15,9 @@ export type FlatPageLayoutWithTabsAndWidgets = FlatPageLayout & { tabs: FlatPageLayoutTabWithWidgets[]; }; +const getResolvedPageLayoutTabId = (widget: FlatPageLayoutWidget): string => + resolveOverridableEntityProperty(widget, 'pageLayoutTabId'); + export const reconstructFlatPageLayoutWithTabsAndWidgets = ({ layout, flatPageLayoutTabMaps, @@ -33,7 +37,10 @@ export const reconstructFlatPageLayoutWithTabsAndWidgets = ({ flatPageLayoutWidgetMaps.byUniversalIdentifier, ) .filter(isDefined) - .filter((widget) => widget.pageLayoutTabId === tab.id && widget.isActive); + .filter( + (widget) => + getResolvedPageLayoutTabId(widget) === tab.id && widget.isActive, + ); return { ...tab, diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input.ts index b0e2470fa49..66745aba10a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input.ts @@ -22,6 +22,11 @@ import { AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/pa @InputType() export class UpdatePageLayoutWidgetInput { + @Field(() => UUIDScalarType, { nullable: true }) + @IsUUID() + @IsOptional() + pageLayoutTabId?: string; + @Field({ nullable: true }) @IsString() @IsOptional() diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity.ts index eac08efbf79..1b912550988 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity.ts @@ -4,6 +4,7 @@ import { PageLayoutWidgetConditionalDisplay, PageLayoutWidgetPosition, type GridPosition, + type SerializedRelation, } from 'twenty-shared/types'; import { Column, @@ -30,6 +31,7 @@ export type PageLayoutWidgetOverrides = { title?: string; position?: PageLayoutWidgetPosition | null; conditionalDisplay?: PageLayoutWidgetConditionalDisplay | null; + pageLayoutTabId?: SerializedRelation; }; @Entity({ name: 'pageLayoutWidget', schema: 'core' }) 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 27f6444e4fa..d0864c7cb41 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 @@ -18,6 +18,7 @@ import { import { CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input'; import { UpdatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input'; import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto'; +import { resolveOverridableEntityProperty } from 'src/engine/metadata-modules/utils/resolve-overridable-entity-property.util'; import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type'; import { PageLayoutWidgetException, @@ -162,8 +163,8 @@ export class PageLayoutWidgetService { .filter(isDefined) .filter( (widget) => - widget.pageLayoutTabId === pageLayoutTabId && - !isDefined(widget.deletedAt), + resolveOverridableEntityProperty(widget, 'pageLayoutTabId') === + pageLayoutTabId && !isDefined(widget.deletedAt), ) .sort( (widgetA, widgetB) => @@ -317,6 +318,7 @@ export class PageLayoutWidgetService { flatFrontComponentMaps: existingFlatFrontComponentMaps, flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps, flatViewMaps: existingFlatViewMaps, + flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps, }, { workspaceCustomFlatApplication }, ] = await Promise.all([ @@ -329,6 +331,7 @@ export class PageLayoutWidgetService { 'flatFrontComponentMaps', 'flatViewFieldGroupMaps', 'flatViewMaps', + 'flatPageLayoutTabMaps', ], }, ), @@ -368,6 +371,7 @@ export class PageLayoutWidgetService { flatFrontComponentMaps: existingFlatFrontComponentMaps, flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps, flatViewMaps: existingFlatViewMaps, + flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps, callerApplicationUniversalIdentifier: workspaceCustomFlatApplication.universalIdentifier, workspaceCustomApplicationUniversalIdentifier: diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-reset.service.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-reset.service.ts index d544fe6da95..9ae3b999432 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-reset.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout-reset.service.ts @@ -116,6 +116,7 @@ export class PageLayoutResetService { const widgetToUpdate: FlatPageLayoutWidget = { ...widget, overrides: null, + universalOverrides: null, updatedAt: now, }; @@ -547,15 +548,10 @@ export class PageLayoutResetService { now, }); - const viewFieldsToReset = fieldsToReset.map((field) => ({ - ...field, - universalOverrides: null, - })); - return { viewFieldGroupsToUpdate: groupsToReset, viewFieldGroupsToDelete: groupsToDelete, - viewFieldsToUpdate: viewFieldsToReset, + viewFieldsToUpdate: fieldsToReset, viewFieldsToDelete: fieldsToDelete, }; } 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 a78d58be98d..1b581244a21 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 @@ -10,6 +10,7 @@ import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util'; import { splitEntitiesByRemovalStrategy } from 'src/engine/metadata-modules/flat-entity/utils/split-entities-by-removal-strategy.util'; import { FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout-tab/constants/flat-page-layout-tab-editable-properties.constant'; import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type'; @@ -18,6 +19,7 @@ import { FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES } from 'src/engine/metadata import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type'; import { buildFlatPageLayoutWidgetCommonProperties } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/build-flat-page-layout-widget-common-properties.util'; import { fromPageLayoutWidgetConfigurationToUniversalConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util'; +import { fromPageLayoutWidgetOverridesToUniversalOverrides } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-overrides-to-universal-overrides.util'; import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type'; import { reconstructFlatPageLayoutWithTabsAndWidgets } from 'src/engine/metadata-modules/flat-page-layout/utils/reconstruct-flat-page-layout-with-tabs-and-widgets.util'; import { UpdatePageLayoutTabWithWidgetsInput } from 'src/engine/metadata-modules/page-layout-tab/dtos/inputs/update-page-layout-tab-with-widgets.input'; @@ -468,11 +470,16 @@ export class PageLayoutUpdateService { const allWidgetsToUpdate: FlatPageLayoutWidget[] = []; const allWidgetsToDelete: FlatPageLayoutWidget[] = []; + const widgetIdsAcrossAllTabs = new Set( + tabs.flatMap((tab) => tab.widgets.map((widget) => widget.id)), + ); + for (const tabInput of tabs) { const { widgetsToCreate, widgetsToUpdate, widgetsToDelete } = this.computeWidgetOperationsForTab({ tabId: tabInput.id, widgets: tabInput.widgets, + widgetIdsAcrossAllTabs, flatPageLayoutWidgetMaps, flatPageLayoutTabMaps, flatObjectMetadataMaps, @@ -500,6 +507,7 @@ export class PageLayoutUpdateService { private computeWidgetOperationsForTab({ tabId, widgets, + widgetIdsAcrossAllTabs, flatPageLayoutWidgetMaps, flatPageLayoutTabMaps, flatObjectMetadataMaps, @@ -513,6 +521,7 @@ export class PageLayoutUpdateService { }: { tabId: string; widgets: UpdatePageLayoutWidgetWithIdInput[]; + widgetIdsAcrossAllTabs: Set; workspaceId: string; workspaceCustomApplicationId: string; workspaceCustomApplicationUniversalIdentifier: string; @@ -538,11 +547,15 @@ export class PageLayoutUpdateService { }); } - const existingWidgets = Object.values( - flatPageLayoutWidgetMaps.byUniversalIdentifier, - ) - .filter(isDefined) - .filter((widget) => widget.pageLayoutTabId === tabId); + const widgetIdsInCurrentTabInput = new Set( + widgets.map((widget) => widget.id), + ); + + const existingWidgets = this.findWidgetsInTabOrMovingToTab({ + tabId, + widgetIdsInCurrentTabInput, + flatPageLayoutWidgetMaps, + }); const resolvedExistingWidgets = existingWidgets.map( resolveFlatEntityOverridableProperties, @@ -587,6 +600,7 @@ export class PageLayoutUpdateService { workspaceCustomApplicationUniversalIdentifier, conditionalDisplay: null, overrides: null, + universalOverrides: null, isActive: true, universalConfiguration: fromPageLayoutWidgetConfigurationToUniversalConfiguration({ @@ -604,120 +618,45 @@ export class PageLayoutUpdateService { ); const widgetsToUpdate: FlatPageLayoutWidget[] = entitiesToUpdate.map( - (widgetInput) => { - const existingWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityId: widgetInput.id, - flatEntityMaps: flatPageLayoutWidgetMaps, - }); - - const shouldOverride = isCallerOverridingEntity({ - callerApplicationUniversalIdentifier: - workspaceCustomApplicationUniversalIdentifier, - entityApplicationUniversalIdentifier: - existingWidget.applicationUniversalIdentifier, - workspaceCustomApplicationUniversalIdentifier, - }); - - const commonProperties = buildFlatPageLayoutWidgetCommonProperties({ + (widgetInput) => + this.buildUpdatedFlatPageLayoutWidget({ widgetInput, + flatPageLayoutWidgetMaps, flatPageLayoutTabMaps, flatObjectMetadataMaps, - }); - - const updatedConfiguration = widgetInput.configuration ?? null; - - const { overrides, updatedEditableProperties } = - sanitizeOverridableEntityInput({ - metadataName: 'pageLayoutWidget', - existingFlatEntity: existingWidget, - updatedEditableProperties: { - ...commonProperties, - configuration: updatedConfiguration, - }, - shouldOverride, - }); - - return { - ...existingWidget, - ...updatedEditableProperties, - overrides, - updatedAt: now.toISOString(), - ...(isDefined(updatedConfiguration) && { - universalConfiguration: - fromPageLayoutWidgetConfigurationToUniversalConfiguration({ - configuration: updatedConfiguration, - fieldMetadataUniversalIdentifierById: - flatFieldMetadataMaps.universalIdentifierById, - frontComponentUniversalIdentifierById: - flatFrontComponentMaps.universalIdentifierById, - viewFieldGroupUniversalIdentifierById: - flatViewFieldGroupMaps.universalIdentifierById, - viewUniversalIdentifierById: - flatViewMaps.universalIdentifierById, - }), - }), - }; - }, + flatFieldMetadataMaps, + flatFrontComponentMaps, + flatViewFieldGroupMaps, + flatViewMaps, + workspaceCustomApplicationUniversalIdentifier, + now, + }), ); const widgetsToRestoreAndUpdate: FlatPageLayoutWidget[] = - entitiesToRestoreAndUpdate.map((widgetInput) => { - const existingWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityId: widgetInput.id, - flatEntityMaps: flatPageLayoutWidgetMaps, - }); - - const shouldOverride = isCallerOverridingEntity({ - callerApplicationUniversalIdentifier: - workspaceCustomApplicationUniversalIdentifier, - entityApplicationUniversalIdentifier: - existingWidget.applicationUniversalIdentifier, - workspaceCustomApplicationUniversalIdentifier, - }); - - const commonProperties = buildFlatPageLayoutWidgetCommonProperties({ + entitiesToRestoreAndUpdate.map((widgetInput) => ({ + ...this.buildUpdatedFlatPageLayoutWidget({ widgetInput, + flatPageLayoutWidgetMaps, flatPageLayoutTabMaps, flatObjectMetadataMaps, - }); + flatFieldMetadataMaps, + flatFrontComponentMaps, + flatViewFieldGroupMaps, + flatViewMaps, + workspaceCustomApplicationUniversalIdentifier, + now, + }), + isActive: true, + })); - const restoredConfiguration = widgetInput.configuration ?? null; - - const { overrides, updatedEditableProperties } = - sanitizeOverridableEntityInput({ - metadataName: 'pageLayoutWidget', - existingFlatEntity: existingWidget, - updatedEditableProperties: { - ...commonProperties, - configuration: restoredConfiguration, - }, - shouldOverride, - }); - - return { - ...existingWidget, - ...updatedEditableProperties, - overrides, - isActive: true, - updatedAt: now.toISOString(), - ...(isDefined(restoredConfiguration) && { - universalConfiguration: - fromPageLayoutWidgetConfigurationToUniversalConfiguration({ - configuration: restoredConfiguration, - fieldMetadataUniversalIdentifierById: - flatFieldMetadataMaps.universalIdentifierById, - frontComponentUniversalIdentifierById: - flatFrontComponentMaps.universalIdentifierById, - viewFieldGroupUniversalIdentifierById: - flatViewFieldGroupMaps.universalIdentifierById, - viewUniversalIdentifierById: - flatViewMaps.universalIdentifierById, - }), - }), - }; + const widgetIdsToRemoveExcludingMovedToOtherTabs = + this.excludeWidgetsMovedToOtherTabs({ + idsToRemove, + widgetIdsAcrossAllTabs, }); - const widgetsToRemove = idsToRemove + const widgetsToRemove = widgetIdsToRemoveExcludingMovedToOtherTabs .map((widgetId) => findFlatEntityByIdInFlatEntityMaps({ flatEntityId: widgetId, @@ -743,6 +682,162 @@ export class PageLayoutUpdateService { }; } + private buildUpdatedFlatPageLayoutWidget({ + widgetInput, + flatPageLayoutWidgetMaps, + flatPageLayoutTabMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatFrontComponentMaps, + flatViewFieldGroupMaps, + flatViewMaps, + workspaceCustomApplicationUniversalIdentifier, + now, + }: { + widgetInput: UpdatePageLayoutWidgetWithIdInput; + workspaceCustomApplicationUniversalIdentifier: string; + now: Date; + } & Pick< + AllFlatEntityMaps, + | 'flatObjectMetadataMaps' + | 'flatFieldMetadataMaps' + | 'flatFrontComponentMaps' + | 'flatViewFieldGroupMaps' + | 'flatViewMaps' + | 'flatPageLayoutTabMaps' + | 'flatPageLayoutWidgetMaps' + >): FlatPageLayoutWidget { + const existingWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: widgetInput.id, + flatEntityMaps: flatPageLayoutWidgetMaps, + }); + + const shouldOverride = isCallerOverridingEntity({ + callerApplicationUniversalIdentifier: + workspaceCustomApplicationUniversalIdentifier, + entityApplicationUniversalIdentifier: + existingWidget.applicationUniversalIdentifier, + workspaceCustomApplicationUniversalIdentifier, + }); + + const configuration = widgetInput.configuration ?? null; + + const editableProperties: Partial = { + title: widgetInput.title, + type: widgetInput.type, + objectMetadataId: widgetInput.objectMetadataId ?? null, + gridPosition: widgetInput.gridPosition, + position: widgetInput.position ?? null, + configuration, + pageLayoutTabId: widgetInput.pageLayoutTabId, + }; + + if (widgetInput.conditionalDisplay !== undefined) { + editableProperties.conditionalDisplay = + widgetInput.conditionalDisplay ?? null; + } + + const { overrides, updatedEditableProperties } = + sanitizeOverridableEntityInput({ + metadataName: 'pageLayoutWidget', + existingFlatEntity: existingWidget, + updatedEditableProperties: editableProperties, + shouldOverride, + }); + + const updatedWidget: FlatPageLayoutWidget = { + ...existingWidget, + ...updatedEditableProperties, + overrides, + updatedAt: now.toISOString(), + }; + + if (updatedEditableProperties.pageLayoutTabId !== undefined) { + const { pageLayoutTabUniversalIdentifier } = + resolveEntityRelationUniversalIdentifiers({ + metadataName: 'pageLayoutWidget', + foreignKeyValues: { + pageLayoutTabId: updatedWidget.pageLayoutTabId, + }, + flatEntityMaps: { flatPageLayoutTabMaps }, + }); + + updatedWidget.pageLayoutTabUniversalIdentifier = + pageLayoutTabUniversalIdentifier; + } + + if (updatedEditableProperties.objectMetadataId !== undefined) { + const { objectMetadataUniversalIdentifier } = + resolveEntityRelationUniversalIdentifiers({ + metadataName: 'pageLayoutWidget', + foreignKeyValues: { + objectMetadataId: updatedWidget.objectMetadataId, + }, + flatEntityMaps: { flatObjectMetadataMaps }, + }); + + updatedWidget.objectMetadataUniversalIdentifier = + objectMetadataUniversalIdentifier; + } + + if (isDefined(overrides)) { + updatedWidget.universalOverrides = + fromPageLayoutWidgetOverridesToUniversalOverrides({ + overrides, + pageLayoutTabUniversalIdentifierById: + flatPageLayoutTabMaps.universalIdentifierById, + }); + } else { + updatedWidget.universalOverrides = null; + } + + if (isDefined(configuration)) { + updatedWidget.universalConfiguration = + fromPageLayoutWidgetConfigurationToUniversalConfiguration({ + configuration, + fieldMetadataUniversalIdentifierById: + flatFieldMetadataMaps.universalIdentifierById, + frontComponentUniversalIdentifierById: + flatFrontComponentMaps.universalIdentifierById, + viewFieldGroupUniversalIdentifierById: + flatViewFieldGroupMaps.universalIdentifierById, + viewUniversalIdentifierById: flatViewMaps.universalIdentifierById, + }); + } + + return updatedWidget; + } + + private findWidgetsInTabOrMovingToTab({ + tabId, + widgetIdsInCurrentTabInput, + flatPageLayoutWidgetMaps, + }: { + tabId: string; + widgetIdsInCurrentTabInput: Set; + flatPageLayoutWidgetMaps: AllFlatEntityMaps['flatPageLayoutWidgetMaps']; + }): FlatPageLayoutWidget[] { + return Object.values(flatPageLayoutWidgetMaps.byUniversalIdentifier) + .filter(isDefined) + .filter( + (widget) => + widget.pageLayoutTabId === tabId || + widgetIdsInCurrentTabInput.has(widget.id), + ); + } + + private excludeWidgetsMovedToOtherTabs({ + idsToRemove, + widgetIdsAcrossAllTabs, + }: { + idsToRemove: string[]; + widgetIdsAcrossAllTabs: Set; + }): string[] { + return idsToRemove.filter( + (widgetId) => !widgetIdsAcrossAllTabs.has(widgetId), + ); + } + private validateChartFieldReferences({ widgetInput, flatFieldMetadataMaps, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/create-standard-page-layout-widget-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/create-standard-page-layout-widget-flat-metadata.util.ts index 78348963448..8a1a153899e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/create-standard-page-layout-widget-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/create-standard-page-layout-widget-flat-metadata.util.ts @@ -1,7 +1,7 @@ import { + type GridPosition, type PageLayoutWidgetConditionalDisplay, type PageLayoutWidgetPosition, - type GridPosition, } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; @@ -110,5 +110,6 @@ export const createStandardPageLayoutWidgetFlatMetadata = ({ deletedAt: null, conditionalDisplay: conditionalDisplay ?? null, overrides: null, + universalOverrides: null, }; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts index cd977044103..3107aa43623 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts @@ -34,6 +34,7 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = { pageLayoutTab: {}, pageLayoutWidget: { configuration: 'configuration', + overrides: 'overrides', }, commandMenuItem: {}, navigationMenuItem: {}, 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 c6a563a0f38..df123d58d58 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 @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; @@ -11,6 +12,7 @@ import { UniversalCreatePageLayoutWidgetAction, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-widget/types/workspace-migration-page-layout-widget-action.type'; import { fromUniversalConfigurationToFlatPageLayoutWidgetConfiguration } from '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'; +import { fromUniversalOverridesToPageLayoutWidgetOverrides } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/utils/from-universal-overrides-to-page-layout-widget-overrides.util'; import { WorkspaceMigrationActionRunnerArgs, WorkspaceMigrationActionRunnerContext, @@ -47,6 +49,13 @@ export class CreatePageLayoutWidgetActionHandlerService extends WorkspaceMigrati flatViewFieldGroupMaps: allFlatEntityMaps.flatViewFieldGroupMaps, }); + const overrides = isDefined(action.flatEntity.universalOverrides) + ? fromUniversalOverridesToPageLayoutWidgetOverrides({ + universalOverrides: action.flatEntity.universalOverrides, + flatPageLayoutTabMaps: allFlatEntityMaps.flatPageLayoutTabMaps, + }) + : null; + const emptyUniversalForeignKeyAggregators = getUniversalFlatEntityEmptyForeignKeyAggregators({ metadataName: 'pageLayoutWidget', @@ -57,6 +66,7 @@ export class CreatePageLayoutWidgetActionHandlerService extends WorkspaceMigrati flatEntity: { ...action.flatEntity, configuration, + overrides, pageLayoutTabId, objectMetadataId, applicationId: flatApplication.id, 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 b34ffc94fb6..6c3c844f219 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 @@ -10,6 +10,7 @@ import { UniversalUpdatePageLayoutWidgetAction, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-widget/types/workspace-migration-page-layout-widget-action.type'; import { fromUniversalConfigurationToFlatPageLayoutWidgetConfiguration } from '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'; +import { fromUniversalOverridesToPageLayoutWidgetOverrides } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/utils/from-universal-overrides-to-page-layout-widget-overrides.util'; import { WorkspaceMigrationActionRunnerArgs, WorkspaceMigrationActionRunnerContext, @@ -34,14 +35,17 @@ export class UpdatePageLayoutWidgetActionHandlerService extends WorkspaceMigrati universalIdentifier: action.universalIdentifier, }); - const { universalConfiguration, ...updateWithResolvedForeignKeys } = - resolveUniversalUpdateRelationIdentifiersToIds({ - metadataName: 'pageLayoutWidget', - universalUpdate: action.update, - allFlatEntityMaps, - }); + const { + universalConfiguration, + universalOverrides, + ...updateWithResolvedForeignKeys + } = resolveUniversalUpdateRelationIdentifiersToIds({ + metadataName: 'pageLayoutWidget', + universalUpdate: action.update, + allFlatEntityMaps, + }); - const update = + const updateWithConfiguration = universalConfiguration === undefined ? updateWithResolvedForeignKeys : { @@ -58,6 +62,19 @@ export class UpdatePageLayoutWidgetActionHandlerService extends WorkspaceMigrati }), }; + const update = + universalOverrides === undefined + ? updateWithConfiguration + : universalOverrides === null + ? { ...updateWithConfiguration, overrides: null } + : { + ...updateWithConfiguration, + overrides: fromUniversalOverridesToPageLayoutWidgetOverrides({ + universalOverrides, + flatPageLayoutTabMaps: allFlatEntityMaps.flatPageLayoutTabMaps, + }), + }; + return { type: 'update', metadataName: 'pageLayoutWidget', diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/utils/from-universal-overrides-to-page-layout-widget-overrides.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/utils/from-universal-overrides-to-page-layout-widget-overrides.util.ts new file mode 100644 index 00000000000..648899b0088 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/page-layout-widget/services/utils/from-universal-overrides-to-page-layout-widget-overrides.util.ts @@ -0,0 +1,39 @@ +import { type FormatRecordSerializedRelationProperties } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; +import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type'; +import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type'; +import { type PageLayoutWidgetOverrides } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity'; + +type UniversalPageLayoutWidgetOverrides = + FormatRecordSerializedRelationProperties; + +export const fromUniversalOverridesToPageLayoutWidgetOverrides = ({ + universalOverrides, + flatPageLayoutTabMaps, +}: { + universalOverrides: UniversalPageLayoutWidgetOverrides; + flatPageLayoutTabMaps: FlatPageLayoutTabMaps; +}): PageLayoutWidgetOverrides => { + const { pageLayoutTabUniversalIdentifier, ...scalarOverrides } = + universalOverrides; + + if (!isDefined(pageLayoutTabUniversalIdentifier)) { + return { + ...scalarOverrides, + }; + } + + const flatPageLayoutTab = + findFlatEntityByUniversalIdentifierOrThrow({ + flatEntityMaps: flatPageLayoutTabMaps, + universalIdentifier: pageLayoutTabUniversalIdentifier, + }); + + return { + ...scalarOverrides, + pageLayoutTabId: + flatPageLayoutTab.id as PageLayoutWidgetOverrides['pageLayoutTabId'], + }; +};