From 13be2188cc6f1c6f707a95add200f5c48d2504cc Mon Sep 17 00:00:00 2001 From: martmull Date: Fri, 3 Jul 2026 09:36:18 +0200 Subject: [PATCH] Fix non-idempotent application sync for viewSorts (subFieldName undefined vs null) (#22505) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Successive application syncs (`yarn twenty dev --once`) kept reporting the same viewSorts as updated, even with no manifest changes. The manifest converter never set `subFieldName`, so the manifest-derived flat viewSort carried `undefined` where the flat viewSort computed from the database carried `null`. The comparator (microdiff) treats `null` vs `undefined` as a change, producing a phantom update action on every sync that never converges — the resulting update is a no-op on the database. Fixes twentyhq/core-team-issues#2629 ## Changes - **Converter**: `fromViewSortManifestToUniversalFlatViewSort` now sets `subFieldName: viewSortManifest.subFieldName ?? null`, matching how the sibling converters (e.g. view filters) handle optional compared properties. - **Type definition**: added optional `subFieldName?: string` to `ViewSortManifest` in `twenty-shared`, mirroring `ViewFilterManifest` — this also makes sorts on composite sub-fields (e.g. `amountMicros`) expressible in app manifests, which the entity already supports. - **Tests**: - Asserts `subFieldName` is `null` (not `undefined`) when omitted — the idempotency regression. - Asserts `subFieldName` is passed through when provided. ## Verification - All 12 application-manifest converter suites pass (47 tests). - Flat-entity comparison/constants suites pass (36 tests, 21 snapshots). - `subFieldName` was already part of the viewSort compare properties, so no comparator/constants changes needed. https://claude.ai/code/session_018FrD42MMQtu1UvDyiEZbSq ``<img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">`` --- ...est-to-universal-flat-view-sort.util.spec.ts | 17 +++++++++++++++++ ...manifest-to-universal-flat-view-sort.util.ts | 1 + .../src/application/viewManifestType.ts | 1 + 3 files changed, 19 insertions(+) diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/__tests__/from-view-sort-manifest-to-universal-flat-view-sort.util.spec.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/__tests__/from-view-sort-manifest-to-universal-flat-view-sort.util.spec.ts index 23e9a04343..485f9654dc 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/__tests__/from-view-sort-manifest-to-universal-flat-view-sort.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/__tests__/from-view-sort-manifest-to-universal-flat-view-sort.util.spec.ts @@ -29,6 +29,23 @@ describe('fromViewSortManifestToUniversalFlatViewSort', () => { expect(result.createdAt).toBe(now); expect(result.updatedAt).toBe(now); expect(result.deletedAt).toBeNull(); + expect(result.subFieldName).toBeNull(); + }); + + it('should pass through subFieldName when provided', () => { + const result = fromViewSortManifestToUniversalFlatViewSort({ + viewSortManifest: { + universalIdentifier: 'vsort-uuid-3', + fieldMetadataUniversalIdentifier: 'field-uuid-3', + direction: ViewSortDirection.ASC, + subFieldName: 'amountMicros', + }, + viewUniversalIdentifier, + applicationUniversalIdentifier, + now, + }); + + expect(result.subFieldName).toBe('amountMicros'); }); it('should convert a view sort manifest with DESC direction', () => { diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-view-sort-manifest-to-universal-flat-view-sort.util.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-view-sort-manifest-to-universal-flat-view-sort.util.ts index e34d56cf14..6f970ba9cf 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-view-sort-manifest-to-universal-flat-view-sort.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-view-sort-manifest-to-universal-flat-view-sort.util.ts @@ -20,6 +20,7 @@ export const fromViewSortManifestToUniversalFlatViewSort = ({ viewSortManifest.fieldMetadataUniversalIdentifier, viewUniversalIdentifier, direction: viewSortManifest.direction, + subFieldName: viewSortManifest.subFieldName ?? null, createdAt: now, updatedAt: now, deletedAt: null, diff --git a/packages/twenty-shared/src/application/viewManifestType.ts b/packages/twenty-shared/src/application/viewManifestType.ts index e788e8d5c7..834521579c 100644 --- a/packages/twenty-shared/src/application/viewManifestType.ts +++ b/packages/twenty-shared/src/application/viewManifestType.ts @@ -61,6 +61,7 @@ export type ViewFieldGroupManifest = SyncableEntityOptions & { export type ViewSortManifest = SyncableEntityOptions & { fieldMetadataUniversalIdentifier: string; direction: ViewSortDirection; + subFieldName?: string; }; export type ViewManifest = SyncableEntityOptions & {