Skip to content

PROD-2431/2435/2442: remap Linked Content selections (checkbox/dropdown/grid) during sync - #207

Merged
5PK merged 3 commits into
mainfrom
PROD-2431
Aug 26, 2026
Merged

PROD-2431/2435/2442: remap Linked Content selections (checkbox/dropdown/grid) during sync#207
5PK merged 3 commits into
mainfrom
PROD-2431

Conversation

@5PK

@5PK 5PK commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes PROD-2431, PROD-2435, and PROD-2442 — three related bugs where ContentFieldMapper fails 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>_ValueField field. The main field's own sortids was already being remapped correctly, but the sibling _ValueField is 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 _ValueField suffix 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 companion linkedContentId, HotAndColdNumbersSection uses LinkedContentValue). The real, authoritative name is the field's own model setting, settings.LinkeContentDropdownValueFieldmapContentFields() now reads that directly off a model field added to ContentFieldMappingContext, 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() checks isListReferenceField() (referencename + fulllist:true) before isContentReferenceField(), 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 populated sortids (a custom sort order, via the model's SortIDFieldName setting, the grid analogue of LinkeContentDropdownValueField). That sortids — the field's own primary value, not just a companion — never reached the remap logic at all, and its SortIDFieldName companion column had the same "no fixed naming convention" problem as PROD-2435, uncovered by that fix since it only reads LinkeContentDropdownValueField.

Verified this against the real server code the CLI's Sync SDK reads from (ContentRepoItemBuilder.cs / classic ContentRepoManagement-GetContentRepoItem.cs) and confirmed live by executing the mapper against a real reproduction:

sharedGridSorted.sortids AFTER mapping: 13088,13087,13086,13085,13084   ← should be 14088,14087,14086,14085,14084
sharedGridSorted_SortField AFTER mapping: 13088,13087,13086,13085,13084 ← should be 14088,14087,14086,14085,14084

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 (LinkeContentDropdownValueField or, as of PROD-2442, SortIDFieldName), remap its comma-separated ID string through the same content-item reference mapper used for sortids. Shared split/map/join logic lives in mapContentIdListString().
  • isListReferenceField() now also requires an empty/absent sortids — a populated one falls through to the existing mapContentReferenceField() remap path instead of short-circuiting as an inert list-by-name reference.
  • getContentDropdownValueFieldNames() falls back to SortIDFieldName when LinkeContentDropdownValueField isn'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 _ValueField companion remap alongside sortids, an arbitrarily-named/case-mismatched companion, self-pointing and sentinel settings, no-op/back-compat cases, a grid field's sortids now getting remapped instead of short-circuiting (with a no-regression case for the common blank-sortids grid field), and the SortIDFieldName companion remap.
  • Full suite: npm test — 105 suites / 1914 tests passing, no regressions.
  • npm run build — compiles clean.

🤖 Generated with Claude Code

5PK added 2 commits August 26, 2026 12:56
… 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).
@5PK

5PK commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

Update: generalized the fix to also cover PROD-2435

The original commit fixed _ValueField-suffix-named companion fields (PROD-2431's LoyaltyLevelsTable case). Investigating the linked ticket PROD-2435 found the companion field has no fixed naming conventionPlayslipSection/WinningNumbersSection name theirs linkedContentId, HotAndColdNumbersSection uses LinkedContentValue. A sweep of the target instance's schema confirmed only 4 of 14 dropdown fields actually use the _ValueField suffix; the rest are arbitrarily named.

This commit replaces the suffix heuristic with a schema-driven lookup: each Content-typed field's model settings already record LinkeContentDropdownValueField (Agility's own spelling), naming its real companion field. mapContentFields() now reads that directly via a new model field on ContentFieldMappingContext, and content-batch-processor.ts passes the already in-scope sourceModel through at the one call site.

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 LinkedContentDropdown companion-field bug across PlayslipSection, WinningNumbersSection, and HotAndColdNumbersSection.

Handles two edge cases found in the schema sweep:

  • Self-pointing settings (e.g. GameBanner's field names itself as its own companion) — skipped to avoid double-processing.
  • Sentinel values that aren't real field names (e.g. PostsListing's setting is the literal string "CREATENEW") — tolerated as a lookup miss rather than throwing.

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

@5PK
5PK requested a review from jules-exel August 26, 2026 19:38
…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>
@5PK 5PK changed the title PROD-2431: remap checkbox/multi-select Linked Content _ValueField IDs during sync PROD-2431/2435/2442: remap Linked Content selections (checkbox/dropdown/grid) during sync Aug 26, 2026
@5PK
5PK merged commit c43c2b7 into main Aug 26, 2026
3 checks passed
@5PK
5PK deleted the PROD-2431 branch August 26, 2026 20:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants