From c1e4756f9cdb2f758508d587972777f5e0392041 Mon Sep 17 00:00:00 2001 From: Weiko Date: Thu, 2 Apr 2026 16:50:50 +0200 Subject: [PATCH] Add is active to overridable entities and deactivation logic for page layouts (#19200) - Replace soft-deletion (deletedAt) with isActive boolean for overridable entities (tabs, widgets, viewFieldGroups, viewFields). Standard entities are deactivated (isActive: false) when removed from update payloads, while custom entities are hard-deleted. - When a viewFieldGroup is deactivated/deleted, its viewFields are reassigned to the next section by position (or null if none remain). - Add isActive: true filters to viewFieldGroup and viewField API queries so deactivated entities are excluded from responses. Next: - fields-widget-upsert.service.ts should be refactored a bit - Add restore logic --- ...s-and-fields-widget-view-fields.command.ts | 1 + ...727625-addIsActiveToOverridableEntities.ts | 50 +++++ ...-to-universal-flat-page-layout-tab.util.ts | 1 + ...-universal-flat-page-layout-widget.util.ts | 1 + ...to-universal-flat-view-field-group.util.ts | 1 + ...ifest-to-universal-flat-view-field.util.ts | 1 + .../engine/dataloaders/dataloader.service.ts | 17 +- ...ompare-and-stringify.constant.spec.ts.snap | 4 + ...configuration-by-metadata-name.constant.ts | 24 +++ ...split-entities-by-removal-strategy.util.ts | 38 ++++ ...fter-flat-object-identifier-update.util.ts | 1 + ...-to-flat-page-layout-tab-to-create.util.ts | 1 + ...tab-entity-to-flat-page-layout-tab.util.ts | 1 + ...-flat-page-layout-widget-to-create.util.ts | 1 + ...-page-layout-with-tabs-and-widgets.util.ts | 9 +- ...to-flat-view-field-group-to-create.util.ts | 1 + ...ew-fields-from-fields-widgets.util.spec.ts | 39 ++-- ...at-view-fields-from-fields-widgets.util.ts | 7 +- ...input-to-flat-view-field-to-create.util.ts | 1 + ...fault-record-page-layout-to-create.util.ts | 2 + ...compute-flat-view-fields-to-create.util.ts | 1 + .../services/page-layout-update.service.ts | 124 ++++++------ .../services/fields-widget-upsert.service.ts | 180 ++++-------------- .../services/view-field-group.service.ts | 1 + .../view-field/services/view-field.service.ts | 2 + ...t-view-fields-and-groups-to-create.util.ts | 4 + ...dard-page-layout-tab-flat-metadata.util.ts | 1 + ...d-page-layout-widget-flat-metadata.util.ts | 1 + ...ard-view-field-group-flat-metadata.util.ts | 1 + ...-standard-view-field-flat-metadata.util.ts | 1 + .../types/overridable-entity.ts | 3 + ...estore-via-update.integration-spec.ts.snap | 60 +++--- ...ayout-with-tabs-update.integration-spec.ts | 9 - .../compute-diff-between-objects.test.ts | 75 +++++++- .../src/utils/compute-diff-between-objects.ts | 14 +- 35 files changed, 392 insertions(+), 286 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1774966727625-addIsActiveToOverridableEntities.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/split-entities-by-removal-strategy.util.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-backfill-page-layouts-and-fields-widget-view-fields.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-backfill-page-layouts-and-fields-widget-view-fields.command.ts index 98fcbd680a..04f753a224 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-backfill-page-layouts-and-fields-widget-view-fields.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-backfill-page-layouts-and-fields-widget-view-fields.command.ts @@ -782,6 +782,7 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO }, objectMetadataId: object.id, objectMetadataUniversalIdentifier: object.universalIdentifier, + isActive: true, createdAt: now, updatedAt: now, deletedAt: null, diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1774966727625-addIsActiveToOverridableEntities.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1774966727625-addIsActiveToOverridableEntities.ts new file mode 100644 index 0000000000..ba75edd24d --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1774966727625-addIsActiveToOverridableEntities.ts @@ -0,0 +1,50 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddIsActiveToOverridableEntities1774966727625 + implements MigrationInterface +{ + name = 'AddIsActiveToOverridableEntities1774966727625'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."viewFieldGroup" ADD "isActive" boolean NOT NULL DEFAULT true`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewField" ADD "isActive" boolean NOT NULL DEFAULT true`, + ); + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutTab" ADD "isActive" boolean NOT NULL DEFAULT true`, + ); + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutWidget" ADD "isActive" boolean NOT NULL DEFAULT true`, + ); + + await queryRunner.query( + `UPDATE "core"."viewFieldGroup" SET "isActive" = false WHERE "deletedAt" IS NOT NULL`, + ); + await queryRunner.query( + `UPDATE "core"."viewField" SET "isActive" = false WHERE "deletedAt" IS NOT NULL`, + ); + await queryRunner.query( + `UPDATE "core"."pageLayoutTab" SET "isActive" = false WHERE "deletedAt" IS NOT NULL`, + ); + await queryRunner.query( + `UPDATE "core"."pageLayoutWidget" SET "isActive" = false WHERE "deletedAt" IS NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutWidget" DROP COLUMN "isActive"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."pageLayoutTab" DROP COLUMN "isActive"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewField" DROP COLUMN "isActive"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewFieldGroup" DROP COLUMN "isActive"`, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-page-layout-tab-manifest-to-universal-flat-page-layout-tab.util.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-page-layout-tab-manifest-to-universal-flat-page-layout-tab.util.ts index ac44117116..fa367b91aa 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-page-layout-tab-manifest-to-universal-flat-page-layout-tab.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-page-layout-tab-manifest-to-universal-flat-page-layout-tab.util.ts @@ -23,6 +23,7 @@ export const fromPageLayoutTabManifestToUniversalFlatPageLayoutTab = ({ icon: pageLayoutTabManifest.icon ?? null, layoutMode: pageLayoutTabManifest.layoutMode ?? PageLayoutTabLayoutMode.GRID, + isActive: true, widgetUniversalIdentifiers: [], createdAt: now, updatedAt: now, 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 4cbae4d0f3..7c3ae6c60c 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 @@ -19,6 +19,7 @@ export const fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget = ({ applicationUniversalIdentifier, pageLayoutTabUniversalIdentifier, title: pageLayoutWidgetManifest.title, + isActive: true, type: pageLayoutWidgetManifest.type as WidgetType, objectMetadataUniversalIdentifier: pageLayoutWidgetManifest.objectUniversalIdentifier ?? null, diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-view-field-group-manifest-to-universal-flat-view-field-group.util.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-view-field-group-manifest-to-universal-flat-view-field-group.util.ts index 61c4f80b5f..68e5b9f495 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-view-field-group-manifest-to-universal-flat-view-field-group.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-view-field-group-manifest-to-universal-flat-view-field-group.util.ts @@ -20,6 +20,7 @@ export const fromViewFieldGroupManifestToUniversalFlatViewFieldGroup = ({ name: viewFieldGroupManifest.name ?? '', position: viewFieldGroupManifest.position, isVisible: viewFieldGroupManifest.isVisible ?? true, + isActive: true, overrides: null, viewFieldUniversalIdentifiers: [], createdAt: now, diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-view-field-manifest-to-universal-flat-view-field.util.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-view-field-manifest-to-universal-flat-view-field.util.ts index 317f3a5edb..836a5ab299 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-view-field-manifest-to-universal-flat-view-field.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-view-field-manifest-to-universal-flat-view-field.util.ts @@ -22,6 +22,7 @@ export const fromViewFieldManifestToUniversalFlatViewField = ({ viewFieldGroupUniversalIdentifier: viewFieldManifest.viewFieldGroupUniversalIdentifier ?? null, isVisible: viewFieldManifest.isVisible ?? true, + isActive: true, size: viewFieldManifest.size ?? 0, position: viewFieldManifest.position, aggregateOperation: viewFieldManifest.aggregateOperation ?? null, diff --git a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts index 79a1399aec..a265a25ee2 100644 --- a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts +++ b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts @@ -480,7 +480,11 @@ export class DataloaderService { flatEntityIds: flatView.viewFieldGroupIds, flatEntityMaps: flatViewFieldGroupMaps, }) - .filter((flatViewFieldGroup) => flatViewFieldGroup.deletedAt === null) + .filter( + (flatViewFieldGroup) => + flatViewFieldGroup.deletedAt === null && + flatViewFieldGroup.isActive, + ) .map(fromFlatViewFieldGroupToViewFieldGroupDto); }); }); @@ -509,7 +513,11 @@ export class DataloaderService { for (const flatViewField of Object.values( flatViewFieldMaps.byUniversalIdentifier, )) { - if (!isDefined(flatViewField) || flatViewField.deletedAt !== null) { + if ( + !isDefined(flatViewField) || + flatViewField.deletedAt !== null || + !flatViewField.isActive + ) { continue; } @@ -575,7 +583,10 @@ export class DataloaderService { flatEntityIds: flatView.viewFieldIds, flatEntityMaps: flatViewFieldMaps, }) - .filter((flatViewField) => flatViewField.deletedAt === null) + .filter( + (flatViewField) => + flatViewField.deletedAt === null && flatViewField.isActive, + ) .map(fromFlatViewFieldToViewFieldDto); }); }); 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 44cb4c0eb5..e83586d2d0 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 @@ -174,6 +174,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma "position", "deletedAt", "icon", + "isActive", "overrides", ], "propertiesToStringify": [ @@ -190,6 +191,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma "universalConfiguration", "deletedAt", "conditionalDisplay", + "isActive", "overrides", ], "propertiesToStringify": [ @@ -299,6 +301,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma "aggregateOperation", "viewFieldGroupUniversalIdentifier", "deletedAt", + "isActive", "universalOverrides", ], "propertiesToStringify": [ @@ -310,6 +313,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma "name", "position", "isVisible", + "isActive", "deletedAt", "overrides", ], 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 727c71bb6d..ef1df2e129 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 @@ -373,6 +373,12 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = { toCompare: true, isOverridable: true, }, + isActive: { + toCompare: true, + toStringify: false, + universalProperty: undefined, + isOverridable: false, + }, deletedAt: { toStringify: false, universalProperty: undefined, @@ -455,6 +461,12 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = { toStringify: false, universalProperty: 'viewUniversalIdentifier', }, + isActive: { + toCompare: true, + toStringify: false, + universalProperty: undefined, + isOverridable: false, + }, overrides: { toCompare: true, toStringify: true, @@ -916,6 +928,12 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = { universalProperty: undefined, isOverridable: true, }, + isActive: { + toCompare: true, + toStringify: false, + universalProperty: undefined, + isOverridable: false, + }, overrides: { toCompare: true, toStringify: true, @@ -966,6 +984,12 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = { toStringify: false, universalProperty: undefined, }, + isActive: { + toCompare: true, + toStringify: false, + universalProperty: undefined, + isOverridable: false, + }, overrides: { toCompare: true, toStringify: true, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/split-entities-by-removal-strategy.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/split-entities-by-removal-strategy.util.ts new file mode 100644 index 0000000000..33602a54b9 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/split-entities-by-removal-strategy.util.ts @@ -0,0 +1,38 @@ +type EntityWithApplicationIdentifier = { + applicationUniversalIdentifier: string; +}; + +export const splitEntitiesByRemovalStrategy = < + T extends EntityWithApplicationIdentifier, +>({ + entitiesToRemove, + workspaceCustomApplicationUniversalIdentifier, + now, +}: { + entitiesToRemove: T[]; + workspaceCustomApplicationUniversalIdentifier: string; + now: string; +}): { + toHardDelete: T[]; + toDeactivate: (T & { isActive: false; updatedAt: string })[]; +} => { + const toHardDelete: T[] = []; + const toDeactivate: (T & { isActive: false; updatedAt: string })[] = []; + + for (const entity of entitiesToRemove) { + if ( + entity.applicationUniversalIdentifier === + workspaceCustomApplicationUniversalIdentifier + ) { + toHardDelete.push(entity); + } else { + toDeactivate.push({ + ...entity, + isActive: false as const, + updatedAt: now, + }); + } + } + + return { toHardDelete, toDeactivate }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-view-field-identifier-after-flat-object-identifier-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-view-field-identifier-after-flat-object-identifier-update.util.ts index 45027dde1e..61b76de792 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-view-field-identifier-after-flat-object-identifier-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-view-field-identifier-after-flat-object-identifier-update.util.ts @@ -70,6 +70,7 @@ export const recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate = ({ updatedLabelIdentifierFieldMetadata.universalIdentifier, position: lowestViewFieldPosition - 1, isVisible: true, + isActive: true, size: DEFAULT_VIEW_FIELD_SIZE, viewId: flatView.id, viewUniversalIdentifier: flatView.universalIdentifier, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-create-page-layout-tab-input-to-flat-page-layout-tab-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-create-page-layout-tab-input-to-flat-page-layout-tab-to-create.util.ts index 0f5f88ecf9..c78ebefd94 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-create-page-layout-tab-input-to-flat-page-layout-tab-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/from-create-page-layout-tab-input-to-flat-page-layout-tab-to-create.util.ts @@ -44,6 +44,7 @@ export const fromCreatePageLayoutTabInputToFlatPageLayoutTabToCreate = ({ id: pageLayoutTabId, title: createPageLayoutTabInput.title, position: createPageLayoutTabInput.position ?? 0, + isActive: true, pageLayoutId: createPageLayoutTabInput.pageLayoutId, pageLayoutUniversalIdentifier, workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/transform-page-layout-tab-entity-to-flat-page-layout-tab.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/transform-page-layout-tab-entity-to-flat-page-layout-tab.util.ts index 15ddd62706..673a0d1622 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/transform-page-layout-tab-entity-to-flat-page-layout-tab.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/transform-page-layout-tab-entity-to-flat-page-layout-tab.util.ts @@ -41,6 +41,7 @@ export const transformPageLayoutTabEntityToFlatPageLayoutTab = ({ id: pageLayoutTabEntity.id, title: pageLayoutTabEntity.title, position: pageLayoutTabEntity.position, + isActive: pageLayoutTabEntity.isActive, pageLayoutId: pageLayoutTabEntity.pageLayoutId, workspaceId: pageLayoutTabEntity.workspaceId, universalIdentifier: pageLayoutTabEntity.universalIdentifier, 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 fccfa2ba70..c94d2bd08c 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 @@ -64,6 +64,7 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({ return { id: pageLayoutWidgetId, ...commonProperties, + isActive: true, workspaceId, createdAt, updatedAt: createdAt, 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 d799495a0e..2aeb7bea76 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 @@ -25,9 +25,7 @@ export const reconstructFlatPageLayoutWithTabsAndWidgets = ({ }): FlatPageLayoutWithTabsAndWidgets => { const tabs = Object.values(flatPageLayoutTabMaps.byUniversalIdentifier) .filter(isDefined) - .filter( - (tab) => tab.pageLayoutId === layout.id && !isDefined(tab.deletedAt), - ) + .filter((tab) => tab.pageLayoutId === layout.id && tab.isActive) .sort((a, b) => (a.position ?? 0) - (b.position ?? 0)); const tabsWithWidgets: FlatPageLayoutTabWithWidgets[] = tabs.map((tab) => { @@ -35,10 +33,7 @@ export const reconstructFlatPageLayoutWithTabsAndWidgets = ({ flatPageLayoutWidgetMaps.byUniversalIdentifier, ) .filter(isDefined) - .filter( - (widget) => - widget.pageLayoutTabId === tab.id && !isDefined(widget.deletedAt), - ); + .filter((widget) => widget.pageLayoutTabId === tab.id && widget.isActive); return { ...tab, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-create-view-field-group-input-to-flat-view-field-group-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-create-view-field-group-input-to-flat-view-field-group-to-create.util.ts index 188311bcb8..fcb54698c1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-create-view-field-group-input-to-flat-view-field-group-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-create-view-field-group-input-to-flat-view-field-group-to-create.util.ts @@ -46,6 +46,7 @@ export const fromCreateViewFieldGroupInputToFlatViewFieldGroupToCreate = ({ universalIdentifier: createViewFieldGroupInput.universalIdentifier ?? v4(), position: createViewFieldGroupInput.position ?? 0, isVisible: createViewFieldGroupInput.isVisible ?? true, + isActive: true, overrides: null, viewFieldUniversalIdentifiers: [], applicationUniversalIdentifier: flatApplication.universalIdentifier, 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 index 3f5a2db5e8..174271d552 100644 --- 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 @@ -53,6 +53,7 @@ const buildFlatViewFieldGroupMaps = ( id: entry.id, viewId: entry.viewId ?? VIEW_ID, position: entry.position ?? 0, + isActive: true, deletedAt: null, }, ]), @@ -69,7 +70,7 @@ const buildFlatViewFieldMaps = ( viewId: string; viewFieldGroupId: string | null; position: number; - deletedAt: string | null; + isActive: boolean; }[] = [], ): FlatViewFieldMaps => ({ @@ -85,18 +86,18 @@ const buildFieldsWidget = ({ objectMetadataUniversalIdentifier = OBJECT_METADATA_UNIVERSAL_IDENTIFIER, viewId = VIEW_ID, isVisible = true, - deletedAt = null as string | null, + isActive = true, }: { widgetUniversalIdentifier?: string; objectMetadataUniversalIdentifier?: string; viewId?: string | null; isVisible?: boolean; - deletedAt?: string | null; + isActive?: boolean; } = {}) => ({ universalIdentifier: widgetUniversalIdentifier, objectMetadataUniversalIdentifier, type: WidgetType.FIELDS, - deletedAt, + isActive, configuration: { configurationType: WidgetConfigurationType.FIELDS, viewId, @@ -178,7 +179,7 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => { expect(result).toEqual([]); }); - it('should skip deleted widgets', () => { + it('should skip inactive widgets', () => { const result = computeFlatViewFieldsFromFieldsWidgets({ fieldsToCreate: [ { @@ -188,7 +189,7 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => { }, ], flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([ - buildFieldsWidget({ deletedAt: '2024-01-01T00:00:00.000Z' }), + buildFieldsWidget({ isActive: false }), ]), flatViewFieldMaps: buildFlatViewFieldMaps(), flatViewMaps: buildFlatViewMaps([ @@ -349,14 +350,14 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => { viewId: VIEW_ID, viewFieldGroupId: null, position: 3, - deletedAt: null, + isActive: true, }, { universalIdentifier: 'existing-vf-2', viewId: VIEW_ID, viewFieldGroupId: null, position: 7, - deletedAt: null, + isActive: true, }, ]), flatViewMaps: buildFlatViewMaps([ @@ -397,7 +398,7 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => { viewId: VIEW_ID, viewFieldGroupId: null, position: 2, - deletedAt: null, + isActive: true, }, ]), flatViewMaps: buildFlatViewMaps([ @@ -413,7 +414,7 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => { expect(result[2].position).toBe(5); }); - it('should ignore deleted view fields when computing position', () => { + it('should ignore inactive view fields when computing position', () => { const result = computeFlatViewFieldsFromFieldsWidgets({ fieldsToCreate: [ { @@ -431,14 +432,14 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => { viewId: VIEW_ID, viewFieldGroupId: null, position: 5, - deletedAt: null, + isActive: true, }, { - universalIdentifier: 'deleted-vf', + universalIdentifier: 'inactive-vf', viewId: VIEW_ID, viewFieldGroupId: null, position: 99, - deletedAt: '2024-01-01T00:00:00.000Z', + isActive: false, }, ]), flatViewMaps: buildFlatViewMaps([ @@ -469,7 +470,7 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => { viewId: 'other-view-db-id', viewFieldGroupId: null, position: 50, - deletedAt: null, + isActive: true, }, ]), flatViewMaps: buildFlatViewMaps([ @@ -556,21 +557,21 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => { viewId: VIEW_ID, viewFieldGroupId: VIEW_FIELD_GROUP_ID, position: 2, - deletedAt: null, + isActive: true, }, { universalIdentifier: 'vf-no-group', viewId: VIEW_ID, viewFieldGroupId: null, position: 99, - deletedAt: null, + isActive: true, }, { universalIdentifier: 'vf-other-group', viewId: VIEW_ID, viewFieldGroupId: 'other-group-id', position: 50, - deletedAt: null, + isActive: true, }, ]), flatViewMaps: buildFlatViewMaps([ @@ -606,14 +607,14 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => { viewId: VIEW_ID, viewFieldGroupId: null, position: 1, - deletedAt: null, + isActive: true, }, { universalIdentifier: 'vf-in-group', viewId: VIEW_ID, viewFieldGroupId: VIEW_FIELD_GROUP_ID, position: 99, - deletedAt: null, + isActive: true, }, ]), flatViewMaps: buildFlatViewMaps([ 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 index 32416b7173..b74b9220a2 100644 --- 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 @@ -39,7 +39,7 @@ const getMatchingFieldsWidgets = ({ .filter(isDefined) .filter( (widget) => - !isDefined(widget.deletedAt) && + widget.isActive && widget.type === WidgetType.FIELDS && widget.objectMetadataUniversalIdentifier === objectMetadataUniversalIdentifier && @@ -59,7 +59,7 @@ const findLastViewFieldGroupId = ({ flatViewFieldGroupMaps.byUniversalIdentifier, ) .filter(isDefined) - .filter((group) => !isDefined(group.deletedAt) && group.viewId === viewId); + .filter((group) => group.isActive && group.viewId === viewId); if (groupsForView.length === 0) { return null; @@ -87,7 +87,7 @@ const computeNextPosition = ({ .filter(isDefined) .filter( (viewField: FlatViewField) => - !isDefined(viewField.deletedAt) && viewField.viewId === viewId, + viewField.isActive && viewField.viewId === viewId, ) .filter( (viewField: FlatViewField) => @@ -199,6 +199,7 @@ export const computeFlatViewFieldsFromFieldsWidgets = ({ size: DEFAULT_VIEW_FIELD_SIZE, position, aggregateOperation: null, + isActive: true, universalOverrides: null, createdAt: now, updatedAt: now, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-create-view-field-input-to-flat-view-field-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-create-view-field-input-to-flat-view-field-to-create.util.ts index e86c2183ee..a524ce7ef4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-create-view-field-input-to-flat-view-field-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-create-view-field-input-to-flat-view-field-to-create.util.ts @@ -66,6 +66,7 @@ export const fromCreateViewFieldInputToFlatViewFieldToCreate = ({ size: createViewFieldInput.size ?? DEFAULT_VIEW_FIELD_SIZE, position: createViewFieldInput.position ?? 0, aggregateOperation: createViewFieldInput.aggregateOperation ?? null, + isActive: true, universalOverrides: null, viewFieldGroupUniversalIdentifier, applicationUniversalIdentifier: flatApplication.universalIdentifier, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util.ts index b7d8b8d35b..17c5f982dc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util.ts @@ -65,6 +65,7 @@ export const computeFlatDefaultRecordPageLayoutToCreate = ({ pageLayoutUniversalIdentifier, widgetIds: [widgetId], widgetUniversalIdentifiers: [widgetUniversalIdentifier], + isActive: true, createdAt: now, updatedAt: now, deletedAt: null, @@ -117,6 +118,7 @@ export const computeFlatDefaultRecordPageLayoutToCreate = ({ universalConfiguration, objectMetadataId: objectMetadata.id, objectMetadataUniversalIdentifier: objectMetadata.universalIdentifier, + isActive: true, createdAt: now, updatedAt: now, deletedAt: null, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util.ts index cf05bcb568..e4d883d36f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util.ts @@ -64,6 +64,7 @@ export const computeFlatViewFieldsToCreate = ({ size: DEFAULT_VIEW_FIELD_SIZE, position: index, aggregateOperation: null, + isActive: true, universalOverrides: null, applicationUniversalIdentifier: flatApplication.universalIdentifier, })); 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 a940e5a8fb..7bf38fc4f2 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 { 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'; import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type'; @@ -167,9 +168,11 @@ export class PageLayoutUpdateService { flatViewMaps, }); - const orphanedViewIds = this.collectOrphanedViewIdsFromDeletedWidgets({ + const orphanedViewIds = this.collectOrphanedViewIdsFromRemovedWidgets({ widgetsToUpdate, + widgetsToDelete, tabsToUpdate, + tabsToDelete, flatPageLayoutWidgetMaps, }); @@ -278,7 +281,7 @@ export class PageLayoutUpdateService { toCreate: entitiesToCreate, toUpdate: entitiesToUpdate, toRestoreAndUpdate: entitiesToRestoreAndUpdate, - idsToDelete, + idsToRemove, } = computeDiffBetweenObjects< FlatPageLayoutTab, UpdatePageLayoutTabWithWidgetsInput @@ -286,6 +289,7 @@ export class PageLayoutUpdateService { existingObjects: existingTabs, receivedObjects: tabs, propertiesToCompare: FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES, + isEntityIncluded: (entity) => entity.isActive, }); const now = new Date(); @@ -313,6 +317,7 @@ export class PageLayoutUpdateService { icon: null, layoutMode: tabInput.layoutMode ?? PageLayoutTabLayoutMode.GRID, overrides: null, + isActive: true, }; }, ); @@ -346,38 +351,34 @@ export class PageLayoutUpdateService { title: tabInput.title, position: tabInput.position, layoutMode: tabInput.layoutMode ?? existingTab.layoutMode, - deletedAt: null, + isActive: true, updatedAt: now.toISOString(), }; }); - const tabsToDelete: FlatPageLayoutTab[] = idsToDelete - .map((tabId) => { - const existingTab = findFlatEntityByIdInFlatEntityMaps({ + const tabsToRemove = idsToRemove + .map((tabId) => + findFlatEntityByIdInFlatEntityMaps({ flatEntityId: tabId, flatEntityMaps: flatPageLayoutTabMaps, - }); - - if (!isDefined(existingTab)) { - return null; - } - - return { - ...existingTab, - deletedAt: now.toISOString(), - updatedAt: now.toISOString(), - }; - }) + }), + ) .filter(isDefined); + const { toHardDelete, toDeactivate } = splitEntitiesByRemovalStrategy({ + entitiesToRemove: tabsToRemove, + workspaceCustomApplicationUniversalIdentifier, + now: now.toISOString(), + }); + return { tabsToCreate, tabsToUpdate: [ ...tabsToUpdate, ...tabsToRestoreAndUpdate, - ...tabsToDelete, + ...toDeactivate, ], - tabsToDelete: [], + tabsToDelete: toHardDelete, }; } @@ -414,9 +415,10 @@ export class PageLayoutUpdateService { } { const allWidgetsToCreate: FlatPageLayoutWidget[] = []; const allWidgetsToUpdate: FlatPageLayoutWidget[] = []; + const allWidgetsToDelete: FlatPageLayoutWidget[] = []; for (const tabInput of tabs) { - const { widgetsToCreate, widgetsToUpdate } = + const { widgetsToCreate, widgetsToUpdate, widgetsToDelete } = this.computeWidgetOperationsForTab({ tabId: tabInput.id, widgets: tabInput.widgets, @@ -434,12 +436,13 @@ export class PageLayoutUpdateService { allWidgetsToCreate.push(...widgetsToCreate); allWidgetsToUpdate.push(...widgetsToUpdate); + allWidgetsToDelete.push(...widgetsToDelete); } return { widgetsToCreate: allWidgetsToCreate, widgetsToUpdate: allWidgetsToUpdate, - widgetsToDelete: [], + widgetsToDelete: allWidgetsToDelete, }; } @@ -474,6 +477,7 @@ export class PageLayoutUpdateService { >): { widgetsToCreate: FlatPageLayoutWidget[]; widgetsToUpdate: FlatPageLayoutWidget[]; + widgetsToDelete: FlatPageLayoutWidget[]; } { for (const widgetInput of widgets) { this.validateChartFieldReferences({ @@ -493,7 +497,7 @@ export class PageLayoutUpdateService { toCreate: entitiesToCreate, toUpdate: entitiesToUpdate, toRestoreAndUpdate: entitiesToRestoreAndUpdate, - idsToDelete, + idsToRemove, } = computeDiffBetweenObjects< FlatPageLayoutWidget, UpdatePageLayoutWidgetWithIdInput @@ -501,6 +505,7 @@ export class PageLayoutUpdateService { existingObjects: existingWidgets, receivedObjects: widgets, propertiesToCompare: FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES, + isEntityIncluded: (entity) => entity.isActive, }); const now = new Date(); @@ -527,6 +532,7 @@ export class PageLayoutUpdateService { workspaceCustomApplicationUniversalIdentifier, conditionalDisplay: null, overrides: null, + isActive: true, universalConfiguration: fromPageLayoutWidgetConfigurationToUniversalConfiguration({ configuration: widgetInput.configuration, @@ -595,7 +601,7 @@ export class PageLayoutUpdateService { flatObjectMetadataMaps, }), configuration: restoredConfiguration, - deletedAt: null, + isActive: true, updatedAt: now.toISOString(), ...(isDefined(restoredConfiguration) && { universalConfiguration: @@ -614,32 +620,29 @@ export class PageLayoutUpdateService { }; }); - const widgetsToDelete: FlatPageLayoutWidget[] = idsToDelete - .map((widgetId) => { - const existingWidget = findFlatEntityByIdInFlatEntityMaps({ + const widgetsToRemove = idsToRemove + .map((widgetId) => + findFlatEntityByIdInFlatEntityMaps({ flatEntityId: widgetId, flatEntityMaps: flatPageLayoutWidgetMaps, - }); - - if (!isDefined(existingWidget)) { - return null; - } - - return { - ...existingWidget, - deletedAt: now.toISOString(), - updatedAt: now.toISOString(), - }; - }) + }), + ) .filter(isDefined); + const { toHardDelete, toDeactivate } = splitEntitiesByRemovalStrategy({ + entitiesToRemove: widgetsToRemove, + workspaceCustomApplicationUniversalIdentifier, + now: now.toISOString(), + }); + return { widgetsToCreate, widgetsToUpdate: [ ...widgetsToUpdate, ...widgetsToRestoreAndUpdate, - ...widgetsToDelete, + ...toDeactivate, ], + widgetsToDelete: toHardDelete, }; } @@ -665,24 +668,37 @@ export class PageLayoutUpdateService { }); } - private collectOrphanedViewIdsFromDeletedWidgets({ + private collectOrphanedViewIdsFromRemovedWidgets({ widgetsToUpdate, + widgetsToDelete, tabsToUpdate, + tabsToDelete, flatPageLayoutWidgetMaps, }: { widgetsToUpdate: FlatPageLayoutWidget[]; + widgetsToDelete: FlatPageLayoutWidget[]; tabsToUpdate: FlatPageLayoutTab[]; + tabsToDelete: FlatPageLayoutTab[]; flatPageLayoutWidgetMaps: Pick< AllFlatEntityMaps, 'flatPageLayoutWidgetMaps' >['flatPageLayoutWidgetMaps']; }): string[] { const viewIdsToDelete = new Set(); - const directlyDeletedWidgetIds = new Set(); + const directlyRemovedWidgetIds = new Set(); + + for (const widget of widgetsToDelete) { + directlyRemovedWidgetIds.add(widget.id); + const viewId = this.getViewIdFromFieldsWidget(widget); + + if (isDefined(viewId)) { + viewIdsToDelete.add(viewId); + } + } for (const widget of widgetsToUpdate) { - if (isDefined(widget.deletedAt)) { - directlyDeletedWidgetIds.add(widget.id); + if (!widget.isActive) { + directlyRemovedWidgetIds.add(widget.id); const viewId = this.getViewIdFromFieldsWidget(widget); if (isDefined(viewId)) { @@ -691,21 +707,17 @@ export class PageLayoutUpdateService { } } - const deletedTabIds = new Set( - tabsToUpdate - .filter((tab) => isDefined(tab.deletedAt)) - .map((tab) => tab.id), - ); + const removedTabIds = new Set([ + ...tabsToUpdate.filter((tab) => !tab.isActive).map((tab) => tab.id), + ...tabsToDelete.map((tab) => tab.id), + ]); const allExistingWidgets = Object.values( flatPageLayoutWidgetMaps.byUniversalIdentifier, ).filter(isDefined); for (const widget of allExistingWidgets) { - if ( - !isDefined(widget.deletedAt) && - deletedTabIds.has(widget.pageLayoutTabId) - ) { + if (widget.isActive && removedTabIds.has(widget.pageLayoutTabId)) { const viewId = this.getViewIdFromFieldsWidget(widget); if (isDefined(viewId)) { @@ -716,9 +728,9 @@ export class PageLayoutUpdateService { for (const widget of allExistingWidgets) { if ( - !isDefined(widget.deletedAt) && - !directlyDeletedWidgetIds.has(widget.id) && - !deletedTabIds.has(widget.pageLayoutTabId) + widget.isActive && + !directlyRemovedWidgetIds.has(widget.id) && + !removedTabIds.has(widget.pageLayoutTabId) ) { const viewId = this.getViewIdFromFieldsWidget(widget); diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field-group/services/fields-widget-upsert.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-field-group/services/fields-widget-upsert.service.ts index de5adc3c7a..1b3081ed8c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field-group/services/fields-widget-upsert.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field-group/services/fields-widget-upsert.service.ts @@ -14,6 +14,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadat import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-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 { splitEntitiesByRemovalStrategy } from 'src/engine/metadata-modules/flat-entity/utils/split-entities-by-removal-strategy.util'; import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util'; 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'; @@ -141,17 +142,13 @@ export class FieldsWidgetUpsertService { flatViewFieldGroupMaps.byUniversalIdentifier, ) .filter(isDefined) - .filter( - (group) => !isDefined(group.deletedAt) && group.viewId === viewId, - ); + .filter((group) => group.isActive && group.viewId === viewId); const existingViewFields = Object.values( flatViewFieldMaps.byUniversalIdentifier, ) .filter(isDefined) - .filter( - (field) => !isDefined(field.deletedAt) && field.viewId === viewId, - ); + .filter((field) => field.isActive && field.viewId === viewId); if (hasGroups) { await this.upsertFieldsWidgetWithGroups({ @@ -228,7 +225,7 @@ export class FieldsWidgetUpsertService { const groupsToCreate: FlatViewFieldGroup[] = []; const groupsToUpdate: FlatViewFieldGroup[] = []; - const groupsToDelete: FlatViewFieldGroup[] = []; + const groupsToDeactivate: FlatViewFieldGroup[] = []; for (const inputGroup of inputGroups) { const existingGroup = existingGroups.find((g) => g.id === inputGroup.id); @@ -277,7 +274,7 @@ export class FieldsWidgetUpsertService { for (const existingGroup of existingGroups) { if (!inputGroupIds.has(existingGroup.id)) { - groupsToDelete.push(existingGroup); + groupsToDeactivate.push(existingGroup); } } @@ -465,6 +462,7 @@ export class FieldsWidgetUpsertService { aggregateOperation: null, overrides: null, universalOverrides: null, + isActive: true, createdAt: now, updatedAt: now, deletedAt: null, @@ -472,15 +470,15 @@ export class FieldsWidgetUpsertService { } } - const fieldsWithStaleGroupOverrides = - this.buildFieldUpdatesForStaleGroupOverrides({ - existingViewFields, - groupsToDelete, - alreadyUpdatedFieldIds: new Set( - viewFieldsToUpdate.map((field) => field.id), - ), - now, - }); + const { + toHardDelete: customGroupsToDelete, + toDeactivate: deactivatedGroupUpdates, + } = splitEntitiesByRemovalStrategy({ + entitiesToRemove: groupsToDeactivate, + workspaceCustomApplicationUniversalIdentifier: + applicationUniversalIdentifier, + now, + }); const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( @@ -488,16 +486,16 @@ export class FieldsWidgetUpsertService { allFlatEntityOperationByMetadataName: { viewFieldGroup: { flatEntityToCreate: groupsToCreate, - flatEntityToDelete: groupsToDelete, - flatEntityToUpdate: groupsToUpdate, + flatEntityToDelete: customGroupsToDelete, + flatEntityToUpdate: [ + ...groupsToUpdate, + ...deactivatedGroupUpdates, + ], }, viewField: { flatEntityToCreate: viewFieldsToCreate, flatEntityToDelete: [], - flatEntityToUpdate: [ - ...viewFieldsToUpdate, - ...fieldsWithStaleGroupOverrides, - ], + flatEntityToUpdate: viewFieldsToUpdate, }, }, workspaceId, @@ -539,7 +537,7 @@ export class FieldsWidgetUpsertService { }): Promise { const now = new Date().toISOString(); - const groupsToDelete: FlatViewFieldGroup[] = [...existingGroups]; + const groupsToDeactivate: FlatViewFieldGroup[] = [...existingGroups]; const viewFieldsToUpdate = existingViewFields.flatMap((existingField) => { const inputField = inputFields.find( @@ -670,21 +668,22 @@ export class FieldsWidgetUpsertService { aggregateOperation: null, overrides: null, universalOverrides: null, + isActive: true, createdAt: now, updatedAt: now, deletedAt: null, }; }); - const fieldsWithStaleGroupOverrides = - this.buildFieldUpdatesForStaleGroupOverrides({ - existingViewFields, - groupsToDelete, - alreadyUpdatedFieldIds: new Set( - viewFieldsToUpdate.map((field) => field.id), - ), - now: new Date().toISOString(), - }); + const { + toHardDelete: customGroupsToDelete, + toDeactivate: deactivatedGroupUpdates, + } = splitEntitiesByRemovalStrategy({ + entitiesToRemove: groupsToDeactivate, + workspaceCustomApplicationUniversalIdentifier: + applicationUniversalIdentifier, + now, + }); const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( @@ -692,16 +691,13 @@ export class FieldsWidgetUpsertService { allFlatEntityOperationByMetadataName: { viewFieldGroup: { flatEntityToCreate: [], - flatEntityToDelete: groupsToDelete, - flatEntityToUpdate: [], + flatEntityToDelete: customGroupsToDelete, + flatEntityToUpdate: deactivatedGroupUpdates, }, viewField: { flatEntityToCreate: viewFieldsToCreate, flatEntityToDelete: [], - flatEntityToUpdate: [ - ...viewFieldsToUpdate, - ...fieldsWithStaleGroupOverrides, - ], + flatEntityToUpdate: viewFieldsToUpdate, }, }, workspaceId, @@ -718,111 +714,6 @@ export class FieldsWidgetUpsertService { } } - private buildFieldUpdatesForStaleGroupOverrides({ - existingViewFields, - groupsToDelete, - alreadyUpdatedFieldIds, - now, - }: { - existingViewFields: FlatViewField[]; - groupsToDelete: FlatViewFieldGroup[]; - alreadyUpdatedFieldIds: Set; - now: string; - }): FlatViewField[] { - if (groupsToDelete.length === 0) { - return []; - } - - const deletedGroupIds = new Set(groupsToDelete.map((group) => group.id)); - - return existingViewFields - .filter((field) => { - if (alreadyUpdatedFieldIds.has(field.id)) { - return false; - } - - const overriddenGroupId = field.overrides?.viewFieldGroupId; - - const hasStaleOverride = - isDefined(overriddenGroupId) && - typeof overriddenGroupId === 'string' && - deletedGroupIds.has(overriddenGroupId); - - const hasStaleBase = - overriddenGroupId === undefined && - isDefined(field.viewFieldGroupId) && - deletedGroupIds.has(field.viewFieldGroupId); - - const hasStaleBaseHiddenByNullOverride = - overriddenGroupId === null && - isDefined(field.viewFieldGroupId) && - deletedGroupIds.has(field.viewFieldGroupId); - - return ( - hasStaleOverride || hasStaleBase || hasStaleBaseHiddenByNullOverride - ); - }) - .map((field) => { - const overriddenGroupId = field.overrides?.viewFieldGroupId; - const hasStaleOverride = - isDefined(overriddenGroupId) && - typeof overriddenGroupId === 'string' && - deletedGroupIds.has(overriddenGroupId); - - if (hasStaleOverride) { - const { viewFieldGroupId: _, ...remainingOverrides } = - field.overrides!; - - const cleanedOverrides = - Object.keys(remainingOverrides).length > 0 - ? (remainingOverrides as typeof field.overrides) - : null; - - const baseGroupIsAlsoStale = - isDefined(field.viewFieldGroupId) && - deletedGroupIds.has(field.viewFieldGroupId); - - return { - ...field, - ...(baseGroupIsAlsoStale - ? { - viewFieldGroupId: null, - viewFieldGroupUniversalIdentifier: null, - } - : {}), - overrides: cleanedOverrides, - universalOverrides: isDefined(cleanedOverrides) - ? fromViewFieldOverridesToUniversalOverrides({ - overrides: cleanedOverrides, - viewFieldGroupUniversalIdentifierById: {}, - }) - : null, - updatedAt: now, - }; - } - - if ( - overriddenGroupId === null && - isDefined(field.viewFieldGroupId) && - deletedGroupIds.has(field.viewFieldGroupId) - ) { - return { - ...field, - viewFieldGroupId: null, - viewFieldGroupUniversalIdentifier: null, - updatedAt: now, - }; - } - - return { - ...field, - viewFieldGroupId: null, - viewFieldGroupUniversalIdentifier: null, - updatedAt: now, - }; - }); - } - private buildGroupToCreate({ inputGroup, viewId, @@ -858,6 +749,7 @@ export class FieldsWidgetUpsertService { isVisible: inputGroup.isVisible, viewId, viewUniversalIdentifier, + isActive: true, overrides: null, createdAt: now, updatedAt: now, diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field-group/services/view-field-group.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-field-group/services/view-field-group.service.ts index 6c7b24da72..6c7f472682 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field-group/services/view-field-group.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field-group/services/view-field-group.service.ts @@ -347,6 +347,7 @@ export class ViewFieldGroupService { where: { workspaceId, viewId, + isActive: true, deletedAt: IsNull(), }, order: { position: 'ASC' }, diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field.service.ts index d60be54b42..8864779b1e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field.service.ts @@ -349,6 +349,7 @@ export class ViewFieldService { return this.viewFieldRepository.find({ where: { workspaceId, + isActive: true, deletedAt: IsNull(), }, order: { position: 'ASC' }, @@ -364,6 +365,7 @@ export class ViewFieldService { where: { workspaceId, viewId, + isActive: true, deletedAt: IsNull(), }, order: { position: 'ASC' }, diff --git a/packages/twenty-server/src/engine/metadata-modules/view/utils/compute-fields-widget-view-fields-and-groups-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/view/utils/compute-fields-widget-view-fields-and-groups-to-create.util.ts index f714b9f5b2..b721547559 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/utils/compute-fields-widget-view-fields-and-groups-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/utils/compute-fields-widget-view-fields-and-groups-to-create.util.ts @@ -62,6 +62,7 @@ export const computeFieldsWidgetViewFieldsAndGroupsToCreate = ({ name: 'General', position: 0, isVisible: true, + isActive: true, overrides: null, viewFieldUniversalIdentifiers: [], createdAt, @@ -83,6 +84,7 @@ export const computeFieldsWidgetViewFieldsAndGroupsToCreate = ({ deletedAt: null, universalIdentifier: v4(), isVisible, + isActive: true, size: DEFAULT_VIEW_FIELD_SIZE, position: index, aggregateOperation: null, @@ -101,6 +103,7 @@ export const computeFieldsWidgetViewFieldsAndGroupsToCreate = ({ name: 'Other', position: 1, isVisible: true, + isActive: true, overrides: null, viewFieldUniversalIdentifiers: [], createdAt, @@ -122,6 +125,7 @@ export const computeFieldsWidgetViewFieldsAndGroupsToCreate = ({ deletedAt: null, universalIdentifier: v4(), isVisible, + isActive: true, size: DEFAULT_VIEW_FIELD_SIZE, position: index, aggregateOperation: null, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-tab/create-standard-page-layout-tab-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-tab/create-standard-page-layout-tab-flat-metadata.util.ts index 3654b6556a..fa6bf6ee4d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-tab/create-standard-page-layout-tab-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/page-layout-tab/create-standard-page-layout-tab-flat-metadata.util.ts @@ -68,6 +68,7 @@ export const createStandardPageLayoutTabFlatMetadata = ({ pageLayoutUniversalIdentifier: layout.universalIdentifier, widgetIds, widgetUniversalIdentifiers, + isActive: true, createdAt: now, updatedAt: now, deletedAt: null, 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 f96e73468b..7834896344 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 @@ -104,6 +104,7 @@ export const createStandardPageLayoutWidgetFlatMetadata = ({ universalConfiguration, objectMetadataId, objectMetadataUniversalIdentifier, + isActive: true, createdAt: now, updatedAt: now, deletedAt: null, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view-field-group/create-standard-view-field-group-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view-field-group/create-standard-view-field-group-flat-metadata.util.ts index c8edfcec65..41062b30c1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view-field-group/create-standard-view-field-group-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view-field-group/create-standard-view-field-group-flat-metadata.util.ts @@ -60,6 +60,7 @@ export const createStandardViewFieldGroupFlatMetadata = < viewFieldUniversalIdentifiers: [], universalIdentifier: viewFieldGroupDefinition.universalIdentifier, applicationId: twentyStandardApplicationId, + isActive: true, applicationUniversalIdentifier: TWENTY_STANDARD_APPLICATION.universalIdentifier, workspaceId, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view-field/create-standard-view-field-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view-field/create-standard-view-field-flat-metadata.util.ts index 10c54e2e3b..cc85b419c0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view-field/create-standard-view-field-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view-field/create-standard-view-field-flat-metadata.util.ts @@ -120,6 +120,7 @@ export const createStandardViewFieldFlatMetadata = < isVisible, size, aggregateOperation, + isActive: true, overrides: null, universalOverrides: null, createdAt: now, diff --git a/packages/twenty-server/src/engine/workspace-manager/types/overridable-entity.ts b/packages/twenty-server/src/engine/workspace-manager/types/overridable-entity.ts index 767d98f1ad..b0ccdf2513 100644 --- a/packages/twenty-server/src/engine/workspace-manager/types/overridable-entity.ts +++ b/packages/twenty-server/src/engine/workspace-manager/types/overridable-entity.ts @@ -8,4 +8,7 @@ export abstract class OverridableEntity< > extends SyncableEntity { @Column({ type: 'jsonb', nullable: true }) overrides: JsonbProperty | null; + + @Column({ type: 'boolean', default: true }) + isActive: boolean; } diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-widget-restore-via-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-widget-restore-via-update.integration-spec.ts.snap index 1fe417ac64..f53e7e5802 100644 --- a/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-widget-restore-via-update.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/__snapshots__/successful-page-layout-widget-restore-via-update.integration-spec.ts.snap @@ -80,6 +80,36 @@ exports[`Page layout widget restore via bulk update should succeed should handle "title": "Test Tab For Widget Restore", "updatedAt": Any, "widgets": [ + { + "configuration": { + "aggregateFieldMetadataId": Any, + "aggregateOperation": "SUM", + "configurationType": "AGGREGATE_CHART", + "description": null, + "displayDataLabel": true, + "filter": null, + "firstDayOfTheWeek": 1, + "format": null, + "label": null, + "prefix": null, + "suffix": null, + "timezone": "UTC", + }, + "createdAt": Any, + "deletedAt": null, + "gridPosition": { + "column": 0, + "columnSpan": 1, + "row": 1, + "rowSpan": 1, + }, + "id": Any, + "objectMetadataId": Any, + "pageLayoutTabId": Any, + "title": "Restored Widget", + "type": "GRAPH", + "updatedAt": Any, + }, { "configuration": { "configurationType": "IFRAME", @@ -120,36 +150,6 @@ exports[`Page layout widget restore via bulk update should succeed should handle "type": "IFRAME", "updatedAt": Any, }, - { - "configuration": { - "aggregateFieldMetadataId": Any, - "aggregateOperation": "SUM", - "configurationType": "AGGREGATE_CHART", - "description": null, - "displayDataLabel": true, - "filter": null, - "firstDayOfTheWeek": 1, - "format": null, - "label": null, - "prefix": null, - "suffix": null, - "timezone": "UTC", - }, - "createdAt": Any, - "deletedAt": null, - "gridPosition": { - "column": 0, - "columnSpan": 1, - "row": 1, - "rowSpan": 1, - }, - "id": Any, - "objectMetadataId": Any, - "pageLayoutTabId": Any, - "title": "Restored Widget", - "type": "GRAPH", - "updatedAt": Any, - }, ], }, ], diff --git a/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-with-tabs-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-with-tabs-update.integration-spec.ts index 7a7ce6d1b6..2a9156da10 100644 --- a/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-with-tabs-update.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/page-layout/successful-page-layout-with-tabs-update.integration-spec.ts @@ -1,5 +1,4 @@ import { createOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/create-one-page-layout-tab.util'; -import { destroyOnePageLayoutTab } from 'test/integration/metadata/suites/page-layout-tab/utils/destroy-one-page-layout-tab.util'; import { fetchTestFieldMetadataIds } from 'test/integration/metadata/suites/page-layout-widget/utils/fetch-test-field-metadata-ids.util'; import { createOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/create-one-page-layout.util'; import { destroyOnePageLayout } from 'test/integration/metadata/suites/page-layout/utils/destroy-one-page-layout.util'; @@ -193,14 +192,6 @@ describe('Page layout with tabs update should succeed', () => { }); afterEach(async () => { - await destroyOnePageLayoutTab({ - expectToFail: false, - input: { id: testTabId1 }, - }); - await destroyOnePageLayoutTab({ - expectToFail: false, - input: { id: testTabId2 }, - }); await destroyOnePageLayout({ expectToFail: false, input: { id: testPageLayoutId }, diff --git a/packages/twenty-shared/src/utils/__tests__/compute-diff-between-objects.test.ts b/packages/twenty-shared/src/utils/__tests__/compute-diff-between-objects.test.ts index 15420c2583..733f772f9d 100644 --- a/packages/twenty-shared/src/utils/__tests__/compute-diff-between-objects.test.ts +++ b/packages/twenty-shared/src/utils/__tests__/compute-diff-between-objects.test.ts @@ -1,5 +1,14 @@ +import { isDefined } from '@/utils/validation'; + import { computeDiffBetweenObjects } from '../compute-diff-between-objects'; +const isEntityIncludedByDeletedAt = (entity: { + deletedAt: string | null; +}): boolean => !isDefined(entity.deletedAt); + +const isEntityIncludedByIsActive = (entity: { isActive: boolean }): boolean => + entity.isActive; + describe('computeDiffBetweenObjects', () => { it('should return the correct diff', () => { const existingObjects = [ @@ -15,13 +24,14 @@ describe('computeDiffBetweenObjects', () => { existingObjects, receivedObjects, propertiesToCompare: ['name'], + isEntityIncluded: isEntityIncludedByDeletedAt, }); expect(diff).toEqual({ toCreate: [{ id: '3', name: 'Object 3' }], toUpdate: [], toRestoreAndUpdate: [], - idsToDelete: ['2'], + idsToRemove: ['2'], }); }); @@ -33,13 +43,14 @@ describe('computeDiffBetweenObjects', () => { existingObjects, receivedObjects, propertiesToCompare: [], + isEntityIncluded: isEntityIncludedByDeletedAt, }); expect(diff).toEqual({ toCreate: [], toUpdate: [], toRestoreAndUpdate: [], - idsToDelete: [], + idsToRemove: [], }); }); @@ -51,13 +62,14 @@ describe('computeDiffBetweenObjects', () => { existingObjects, receivedObjects, propertiesToCompare: ['name'], + isEntityIncluded: isEntityIncludedByDeletedAt, }); expect(diff).toEqual({ toCreate: [{ id: '1', name: 'Object 1' }], toUpdate: [], toRestoreAndUpdate: [], - idsToDelete: [], + idsToRemove: [], }); }); @@ -69,13 +81,14 @@ describe('computeDiffBetweenObjects', () => { existingObjects, receivedObjects, propertiesToCompare: ['name'], + isEntityIncluded: isEntityIncludedByDeletedAt, }); expect(diff).toEqual({ toCreate: [], toUpdate: [], toRestoreAndUpdate: [], - idsToDelete: ['1'], + idsToRemove: ['1'], }); }); @@ -87,17 +100,18 @@ describe('computeDiffBetweenObjects', () => { existingObjects, receivedObjects, propertiesToCompare: ['name'], + isEntityIncluded: isEntityIncludedByDeletedAt, }); expect(diff).toEqual({ toCreate: [], toUpdate: [{ id: '1', name: 'Updated Object 1' }], toRestoreAndUpdate: [], - idsToDelete: [], + idsToRemove: [], }); }); - it('should restore and update deleted objects', () => { + it('should restore and update excluded objects when using deletedAt', () => { const existingObjects = [ { id: '1', name: 'Object 1', deletedAt: '2024-01-01' }, ]; @@ -107,17 +121,18 @@ describe('computeDiffBetweenObjects', () => { existingObjects, receivedObjects, propertiesToCompare: ['name'], + isEntityIncluded: isEntityIncludedByDeletedAt, }); expect(diff).toEqual({ toCreate: [], toUpdate: [], toRestoreAndUpdate: [{ id: '1', name: 'Restored Object 1' }], - idsToDelete: [], + idsToRemove: [], }); }); - it('should not include deleted objects in idsToDelete', () => { + it('should not include excluded objects in idsToRemove', () => { const existingObjects = [ { id: '1', name: 'Object 1', deletedAt: null }, { id: '2', name: 'Object 2', deletedAt: '2024-01-01' }, @@ -128,13 +143,55 @@ describe('computeDiffBetweenObjects', () => { existingObjects, receivedObjects, propertiesToCompare: ['name'], + isEntityIncluded: isEntityIncludedByDeletedAt, }); expect(diff).toEqual({ toCreate: [], toUpdate: [], toRestoreAndUpdate: [], - idsToDelete: ['1'], + idsToRemove: ['1'], + }); + }); + + it('should restore and update inactive objects when using isActive', () => { + const existingObjects = [{ id: '1', name: 'Object 1', isActive: false }]; + const receivedObjects = [{ id: '1', name: 'Restored Object 1' }]; + + const diff = computeDiffBetweenObjects({ + existingObjects, + receivedObjects, + propertiesToCompare: ['name'], + isEntityIncluded: isEntityIncludedByIsActive, + }); + + expect(diff).toEqual({ + toCreate: [], + toUpdate: [], + toRestoreAndUpdate: [{ id: '1', name: 'Restored Object 1' }], + idsToRemove: [], + }); + }); + + it('should not include inactive objects in idsToRemove when using isActive', () => { + const existingObjects = [ + { id: '1', name: 'Object 1', isActive: true }, + { id: '2', name: 'Object 2', isActive: false }, + ]; + const receivedObjects: { id: string; name: string }[] = []; + + const diff = computeDiffBetweenObjects({ + existingObjects, + receivedObjects, + propertiesToCompare: ['name'], + isEntityIncluded: isEntityIncludedByIsActive, + }); + + expect(diff).toEqual({ + toCreate: [], + toUpdate: [], + toRestoreAndUpdate: [], + idsToRemove: ['1'], }); }); }); diff --git a/packages/twenty-shared/src/utils/compute-diff-between-objects.ts b/packages/twenty-shared/src/utils/compute-diff-between-objects.ts index aef7c1f676..a81081038c 100644 --- a/packages/twenty-shared/src/utils/compute-diff-between-objects.ts +++ b/packages/twenty-shared/src/utils/compute-diff-between-objects.ts @@ -5,7 +5,7 @@ type Diff = { toCreate: T[]; toUpdate: T[]; toRestoreAndUpdate: T[]; - idsToDelete: string[]; + idsToRemove: string[]; }; const extractProperties = ( @@ -27,15 +27,17 @@ type ComputeDiffBetweenObjectsParams< existingObjects: T[]; receivedObjects: K[]; propertiesToCompare: (keyof K & keyof T)[]; + isEntityIncluded: (entity: NoInfer) => boolean; }; export const computeDiffBetweenObjects = < - T extends { id: string; deletedAt: string | null }, + T extends { id: string }, K extends { id: string }, >({ existingObjects, receivedObjects, propertiesToCompare, + isEntityIncluded, }: ComputeDiffBetweenObjectsParams): Diff => { const toCreate: K[] = []; const toUpdate: K[] = []; @@ -52,7 +54,7 @@ export const computeDiffBetweenObjects = < const existingEntity = existingEntitiesMap.get(receivedObject.id); if (isDefined(existingEntity)) { - if (isDefined(existingEntity.deletedAt)) { + if (!isEntityIncluded(existingEntity)) { toRestoreAndUpdate.push(receivedObject); } else { const comparableExistingEntity = extractProperties( @@ -74,8 +76,8 @@ export const computeDiffBetweenObjects = < } } - const idsToDelete = existingObjects - .filter((existingEntity) => !isDefined(existingEntity.deletedAt)) + const idsToRemove = existingObjects + .filter((existingEntity) => isEntityIncluded(existingEntity)) .filter((existingEntity) => !receivedEntitiesMap.has(existingEntity.id)) .map((entity) => entity.id); @@ -83,6 +85,6 @@ export const computeDiffBetweenObjects = < toCreate, toUpdate, toRestoreAndUpdate, - idsToDelete, + idsToRemove, }; };