fix(server): make all view children reparentable across a workspace migration sync (#22600)

## Summary

Uniformizes the workspace migration engine so **every** view child
entity — `viewField`, `viewFieldGroup`, `viewGroup`, `viewFilter`,
`viewSort`, `viewFilterGroup` — can be reparented from one view to
another within a single manifest sync, including when the previous
parent view is deleted in the same sync.

### Context

When an app manifest deletes a view and reparents its children onto
another view in the same sync (e.g. replacing a custom `FIELDS_WIDGET`
view with a standard one), the sync failed with a builder validation
error `View field to update parent view not found`. Root causes:

1. `viewField`, `viewFieldGroup` and `viewGroup` had `viewId.toCompare:
false`, so the diff never detected the parent-view change and never
emitted a reparent update (the already-reparentable siblings
`viewFilter`/`viewSort`/`viewFilterGroup` had `toCompare: true`).
2. `validateFlatViewFieldGroupUpdate` resolved the *old* parent view (it
ignored the update patch), inconsistent with the other view-child
validators.
3. Once the builder no longer errors, the runner would fail silently:
`view.delete` ran **before** the child reparent updates, and `viewId` is
`onDelete: CASCADE`, so the old view's deletion cascade-deleted the
children before they could be reparented (silent data loss, since
`repository.update` on a missing row is a no-op).

### Changes

-
**`all-entity-properties-configuration-by-metadata-name.constant.ts`**:
set `viewId.toCompare: true` for `viewField`, `viewFieldGroup`,
`viewGroup`. Because `viewId` maps to `universalProperty:
'viewUniversalIdentifier'`, the diff compares **only**
`viewUniversalIdentifier` (never the raw FK). Snapshot updated
accordingly.
- **`flat-view-field-group-validator.service.ts`**: merge
`flatEntityUpdate` and resolve the **new** parent view, matching the
`viewField`/`viewGroup`/`viewSort` validators.
- **`compute-ordered-migration-actions.util.ts`**: move `view.delete` to
run **after** all view-child create/update actions so a child can be
reparented off a view that is being deleted in the same sync. Child
`delete → create → update` order is preserved (needed for `viewField`'s
partial-unique `(fieldMetadataId, viewId)`).
- **New integration test**
`successful-manifest-reparent-view-children.integration-spec.ts`
covering reparenting of every view child (a) between two persisting
views and (b) when the source view is deleted in the same sync.

## Test plan

- [x] `nx typecheck twenty-server`
- [x] oxlint + oxfmt on changed files
- [x] Unit snapshot regenerated:
`all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec`
- [x] New integration test passes (both scenarios)
- [x] Verified the delete-source scenario **fails** on the old action
ordering (children cascade-deleted, `Received length: 0`) and **passes**
after the reorder — confirming it's a genuine regression guard

Made with [Cursor](https://cursor.com)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22600?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Paul Rastoin
2026-07-07 10:52:34 +02:00
committed by GitHub
parent 1d3f6176b2
commit 5dc9d7ab36
5 changed files with 376 additions and 5 deletions
@@ -362,6 +362,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
"aggregateOperation",
"viewFieldGroupUniversalIdentifier",
"deletedAt",
"viewUniversalIdentifier",
"isActive",
"universalOverrides",
],
@@ -376,6 +377,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
"isVisible",
"isActive",
"deletedAt",
"viewUniversalIdentifier",
"overrides",
],
"propertiesToStringify": [
@@ -414,6 +416,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
"fieldValue",
"position",
"deletedAt",
"viewUniversalIdentifier",
],
"propertiesToStringify": [],
},
@@ -477,7 +477,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
toStringify: false,
},
viewId: {
toCompare: false,
toCompare: true,
universalProperty: 'viewUniversalIdentifier',
toStringify: false,
},
@@ -544,7 +544,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
universalProperty: 'fieldMetadataUniversalIdentifier',
},
viewId: {
toCompare: false,
toCompare: true,
toStringify: false,
universalProperty: 'viewUniversalIdentifier',
},
@@ -592,7 +592,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
universalProperty: undefined,
},
viewId: {
toCompare: false,
toCompare: true,
toStringify: false,
universalProperty: 'viewUniversalIdentifier',
},