From bc0ec6b1043287169442142ac8e75957097888d4 Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:42:30 +0200 Subject: [PATCH] Maintain INDEX view system side effects on deactivated views (#23590) # Introduction Follow-up on https://github.com/twentyhq/twenty/pull/23585#discussion_r3683701472. Two INDEX view side-effect handlers bailed out when the view had `isActive: false`. This drops those gates. # Why `isActive: false` on a view has exactly one writer: the delete path, when `isCallerOverridingEntity` is true (`from-delete-view-input-to-flat-view-or-throw.util.ts`, `view.service.ts`). It is not in `FLAT_VIEW_EDITABLE_PROPERTIES`, so nothing else sets it. So the flag means "the workspace deleted an engine-owned view, and since the engine owns the row we deactivate instead of hard-deleting". It is a workspace override of a row we still own, not a signal the row is gone (that is `deletedAt`). Which makes it precisely the state where the engine must keep maintaining its own rows: the row still exists, still belongs to the engine, and is expected to be consistent whenever the override is lifted. Skipping the side effect instead left the view permanently incomplete, with no repair path. The gates were inherited from the candidate-view scan removed in the same commit as these handlers were introduced (`compute-flat-view-fields-from-fields-widgets.util.ts`, #23081). There, `!view.isActive` filtered which of many views were candidates. Transplanted into handlers that resolve *the* one deterministic engine-owned INDEX view identifier, the same predicate stops meaning "is this a candidate" and starts meaning "silently skip the system side effect". # Changes - `fieldIndexViewFieldOnCreate`: create the INDEX view field even when the view is deactivated. - `objectIndexViewLabelIdentifierOnUpdate`: reconcile the label identifier view field even when the view is deactivated. `deletedAt` gates are unchanged in both. The `should noop when the object has no active INDEX view` spec case is inverted accordingly. # Follow-up Both handlers also drop inactive view fields when computing positions, while `FlatViewFieldValidatorService` builds its `otherFlatViewFields` with no `isActive` filter. An inactive view field below all active ones would make a handler emit a label identifier position the validator then rejects. Unreachable today (view fields are only ever soft-deleted, never deactivated), so left out of this PR. --- ...-create-side-effect-handler.service.spec.ts | 18 ++++++++++++++++-- ...ld-on-create-side-effect-handler.service.ts | 1 - ...er-on-update-side-effect-handler.service.ts | 1 - 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/__tests__/field-index-view-field-on-create-side-effect-handler.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/__tests__/field-index-view-field-on-create-side-effect-handler.service.spec.ts index 8eef9d3bc9..1f789bf79c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/__tests__/field-index-view-field-on-create-side-effect-handler.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/__tests__/field-index-view-field-on-create-side-effect-handler.service.spec.ts @@ -585,7 +585,7 @@ describe('FieldIndexViewFieldOnCreateSideEffectHandlerService', () => { expect(result.status).toBe('noop'); }); - it('should noop when the object has no active INDEX view', () => { + it('should emit a view field on a deactivated INDEX view', () => { const result = handler.buildSideEffects( buildArgs({ triggerFieldMetadata: PRIORITY_FIELD, @@ -602,7 +602,21 @@ describe('FieldIndexViewFieldOnCreateSideEffectHandlerService', () => { }), ); - expect(result.status).toBe('noop'); + expect(result.status).toBe('success'); + + if (result.status !== 'success') { + throw new Error('expected success'); + } + + const viewFields = Object.values( + result.operations.viewField?.flatEntityToCreate ?? {}, + ); + + expect(viewFields).toHaveLength(1); + expect(viewFields[0].viewUniversalIdentifier).toBe( + DERIVED_INDEX_VIEW_UNIVERSAL_IDENTIFIER, + ); + expect(viewFields[0].isVisible).toBe(true); }); }); diff --git a/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-index-view-field-on-create-side-effect-handler.service.ts b/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-index-view-field-on-create-side-effect-handler.service.ts index 6893afae35..99ab348062 100644 --- a/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-index-view-field-on-create-side-effect-handler.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-index-view-field-on-create-side-effect-handler.service.ts @@ -183,7 +183,6 @@ export class FieldIndexViewFieldOnCreateSideEffectHandlerService extends Metadat if ( !isDefined(existingIndexFlatView) || - !existingIndexFlatView.isActive || isDefined(existingIndexFlatView.deletedAt) ) { return undefined; diff --git a/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-index-view-label-identifier-on-update-side-effect-handler.service.ts b/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-index-view-label-identifier-on-update-side-effect-handler.service.ts index 23a70ebb2e..e578e50b5c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-index-view-label-identifier-on-update-side-effect-handler.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-index-view-label-identifier-on-update-side-effect-handler.service.ts @@ -80,7 +80,6 @@ export class ObjectIndexViewLabelIdentifierOnUpdateSideEffectHandlerService exte if ( !isDefined(indexFlatView) || indexFlatView.isSystemSideEffect !== true || - !indexFlatView.isActive || isDefined(indexFlatView.deletedAt) ) { return { status: 'noop' };