Conversation
… during sync Checkbox/multi-select Linked Content fields store their actual selection as a comma-separated content-ID string in a sibling "<field>_ValueField" field (named by the model schema's LinkeContentDropdownValueField property). The main field object's own "sortids" is only display order and was already remapped correctly, but the sibling _ValueField is a bare string, not an object, so isContentReferenceField() never recognized it as a content reference — it passed through unchanged and shipped raw SOURCE-instance content IDs into the target on sync. This produced two symptoms on the target after sync: the field appearing empty/no-container-bound, and — once the container was re-selected — the source instance's raw IDs rendering as orphaned "checked" items alongside the real (unchecked) target options. Fix: after the existing per-field mapping pass, walk fields a second time for any object with a "sortids" key and remap its sibling "<field>_ValueField" comma-ID string through the same content-item mapper used for sortids. Extracted the shared split/map/join logic into mapContentIdListString() and reused it for both sortids and the new _ValueField pass.
…mpanion remap Supersedes the previous commit's _ValueField-suffix heuristic for finding a linked-content dropdown's companion selection field. Investigating PROD-2435 (LinkedContentDropdown values not remapped during sync) showed the companion field has no fixed naming convention: PlayslipSection/WinningNumbersSection name theirs "linkedContentId", HotAndColdNumbersSection uses "LinkedContentValue" — neither matches "<field>_ValueField". Sweeping the target instance's own ContentDefinitions.xmlSchema confirms only 4 of 14 dropdown fields actually follow that suffix; the rest are arbitrarily named. The real, authoritative name is already recorded on the model: each Content-typed field's settings.LinkeContentDropdownValueField (Agility's own spelling) names its companion. mapContentFields() now reads that directly off a new `model` field on ContentFieldMappingContext, instead of guessing a suffix, and content-batch-processor.ts passes the already in-scope sourceModel through at the one call site. Confirmed against the target Batches table (batch 240, item 1326): the payload already carried the bug baked in — "linkedDrawGameAsset": "1034" (main field, correctly remapped via the existing PROD-2341 single-item-selection path) alongside "linkedContentId": "11875" (the companion, still the raw SOURCE-instance id — 11875 doesn't exist on target, whose whole id range tops out at 1730). Two edge cases the schema-driven lookup has to tolerate: - Self-pointing settings (GameBanner's field names itself as its own companion) — skip re-processing, the ordinary field pass already covers it. - Sentinel values that aren't real field names (PostsListing's LinkeContentDropdownValueField is the literal string "CREATENEW") — tolerate the lookup miss rather than throwing. Also matches the companion field name case-insensitively, since schema casing and the payload's actual field-key casing can differ (observed: schema "LinkedContentValue" vs. a batch payload key "linkedContentValue"). Replaced the 4 old suffix-only tests with 8 covering: PROD-2431's checkbox/multi-select case via schema instead of suffix, PROD-2435's arbitrarily-named single-select case, case-insensitive matching, an unresolved-id warning, the two edge cases above, and two no-op/back- compat cases (no model in context; model with no matching setting).
Update: generalized the fix to also cover PROD-2435The original commit fixed This commit replaces the suffix heuristic with a schema-driven lookup: each Content-typed field's model settings already record This is a strict superset of the original fix — it covers PROD-2431's case too, just via the schema instead of a suffix guess — and additionally resolves PROD-2435's Handles two edge cases found in the schema sweep:
Testing: replaced the 4 old suffix-only tests with 8 covering both tickets' cases, the two edge cases above, case-insensitive field-name matching, and back-compat (no model / no matching setting in context). Full suite: 105 suites / 1911 tests passing. Build clean. Note: this is a forward-fix — it prevents the bug on future syncs but doesn't retroactively repair content items already corrupted on affected target instances; those still need a re-sync/re-push after this ships. 🤖 Generated with Claude Code |
…order isListReferenceField() matched any object with referencename+fulllist:true and returned it unchanged from mapSingleField() before mapContentReferenceField() ever ran - including a full-list "grid" Linked Content field that also carries a populated sortids (a custom sort order, via the model's SortIDFieldName setting - the grid analogue of LinkeContentDropdownValueField). That shipped raw SOURCE content IDs to the target on every sync for a grid field with a configured sort order. Fix: isListReferenceField() now also requires an empty/absent sortids, so a populated one falls through to the existing content-reference remap path instead of short-circuiting. getContentDropdownValueFieldNames() now also reads SortIDFieldName as a fallback, so the field's own SortIDFieldName companion column (structurally identical to the _ValueField companion PROD-2431/2435 already fixed) gets remapped too. Found and verified live on test instance a921a90f-us2 while building a full Linked Content permutation matrix (dropdown/checkbox/searchlistbox/shared grid/shared link/nested) to validate PROD-2431/2435's fix - confirmed those render styles work correctly; this grid case was the one gap. Added 3 new test cases: grid field with populated sortids gets remapped (previously silently untouched), a full-list field with empty sortids stays an inert list reference (no regression), and the SortIDFieldName companion gets remapped alongside it. Full suite: 105 suites / 1914 tests passing. npm run build compiles clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
Fixes PROD-2431, PROD-2435, and PROD-2442 — three related bugs where
ContentFieldMapperfails to remap a Linked Content field's actual selection during cross-instance sync, each shipping raw SOURCE-instance content IDs into the target. Same root class of bug in three different field shapes: a checkbox/multi-select field's companion value column, an arbitrarily-named dropdown companion column, and a full-list grid field's own sort order.PROD-2431 — checkbox/multi-select companion field not remapped
Checkbox/multi-select Linked Content fields store the actual selection as a comma-separated content-ID string in a sibling
<field>_ValueFieldfield. The main field's ownsortidswas already being remapped correctly, but the sibling_ValueFieldis a bare string, not an object —isContentReferenceField()only recognizes object-shaped values — so it fell through unmapped and shipped raw SOURCE content IDs into the target on every sync. Symptoms: field appears empty with no container bound, or shows the source instance's raw content IDs as orphaned "checked" items.PROD-2435 — companion field has no fixed naming convention
The
_ValueFieldsuffix heuristic from the PROD-2431 fix doesn't hold generally: sampling the target schema, only 4 of 14 dropdown fields actually use that suffix (e.g. PlayslipSection names its companionlinkedContentId, HotAndColdNumbersSection usesLinkedContentValue). The real, authoritative name is the field's own model setting,settings.LinkeContentDropdownValueField—mapContentFields()now reads that directly off amodelfield added toContentFieldMappingContext, instead of guessing a suffix. Matches case-insensitively (schema casing vs. payload key casing can differ) and tolerates a self-pointing setting or a sentinel value (e.g."CREATENEW") that names no real field.PROD-2442 — full-list grid field's own sortids never reached the remap
While live-testing every Linked Content render style (dropdown/checkbox/searchlistbox/shared grid/shared link/nested) against the PROD-2431/2435 fix on a throwaway test instance, found a third, more severe gap:
mapSingleField()checksisListReferenceField()(referencename+fulllist:true) beforeisContentReferenceField(), and returns the field unchanged immediately on a match. A full-list "grid" Linked Content field matches that shape too — but can also carry a populatedsortids(a custom sort order, via the model'sSortIDFieldNamesetting, the grid analogue ofLinkeContentDropdownValueField). Thatsortids— the field's own primary value, not just a companion — never reached the remap logic at all, and itsSortIDFieldNamecompanion column had the same "no fixed naming convention" problem as PROD-2435, uncovered by that fix since it only readsLinkeContentDropdownValueField.Verified this against the real server code the CLI's Sync SDK reads from (
ContentRepoItemBuilder.cs/ classicContentRepoManagement-GetContentRepoItem.cs) and confirmed live by executing the mapper against a real reproduction:Fix
In
src/lib/content/content-field-mapper.ts:mapContentFields()walks fields a second time after the main pass: for any field whose model setting names a companion column (LinkeContentDropdownValueFieldor, as of PROD-2442,SortIDFieldName), remap its comma-separated ID string through the same content-item reference mapper used forsortids. Shared split/map/join logic lives inmapContentIdListString().isListReferenceField()now also requires an empty/absentsortids— a populated one falls through to the existingmapContentReferenceField()remap path instead of short-circuiting as an inert list-by-name reference.getContentDropdownValueFieldNames()falls back toSortIDFieldNamewhenLinkeContentDropdownValueFieldisn't set, so a grid field's own companion column is covered by the same mechanism.Testing
src/lib/content/tests/content-field-mapper.test.ts: covers the_ValueFieldcompanion remap alongsidesortids, an arbitrarily-named/case-mismatched companion, self-pointing and sentinel settings, no-op/back-compat cases, a grid field'ssortidsnow getting remapped instead of short-circuiting (with a no-regression case for the common blank-sortidsgrid field), and theSortIDFieldNamecompanion remap.npm test— 105 suites / 1914 tests passing, no regressions.npm run build— compiles clean.🤖 Generated with Claude Code