Bugfix(delta) | References, Entry Mapping, RTE, empty-state, CT count - #1136
Conversation
…le-migrated tracking Reference fields (Link/Array Entry) written during a locale-localize restart kept the source CMS entry id instead of the real Contentstack uid, since only the master-locale bulk import resolved references correctly. Threads entry uid-mapper data into the update config so entry-update-script.cjs can resolve them, mirroring the existing asset uid resolution. Also fixes a bug where finishing any locale marked ALL configured locales as migrated, causing not-yet-migrated locales to be silently skipped on later delta restarts. Now only locales actually processed in that run are recorded.
Was reading a literal .id property on the entryId map instead of the dynamic key, so the single-reference branch never took the array path when the mapper legitimately held an array of destination uids.
…le-migrated tracking Address review comments on #1128: - Move iteration 2+ recordMigratedLocales out of runCli into migration.service.ts, after utilsUpdateCli.updateEntryCli resolves. Recording pre-update meant a silent update-CLI failure would still flag the locales as migrated, permanently skipping them on later restarts — same class of bug this PR fixes. - Union in Object.keys(uid-mapper.entryByLocale) so brand-new-entries-only locales (no rows in updated-entries.json because they had no prior csEntryUid) are also recorded as migrated. - Extract flattenNestedUidMap into uid-mapper.utils.js and reuse it in both contentMapper.service and entry-update.utils. entry-update.utils.enrichConfigWithEntryMapping now handles the nested per-content-type shape (and the entryUid variant) instead of assuming a flat map. - entry-update-script.isReferenceArray: switch from .every() to .some(), and pass non-reference items through in resolveReferenceField. Producers legitimately emit mixed arrays (raw Contentful Link objects for unresolved refs, [undefined] from the single-ref fallback) — one such item no longer disables resolution for the whole field.
…action - Clear iter-1 logs and completion flag on new Start Migration; accept 'Entry Update Process Completed' as terminal on the delta path. - Move 'already migrated' placeholder outside the log map so it no longer repeats per line and doesn't hide iter-2 live logs. - extractEntries: use CT displayField with fallbacks instead of hard-coded title/name (fixes CMG-1102 empty Map Entry table).
…traversal Snyk SAST flagged fs.readFileSync on paths derived from projectId. Add explicit assertResolvedPathUnderBase checks so the sink is visibly sanitized against the database dir.
…sanitizer assertResolvedPathUnderBase is a custom helper Snyk SAST doesn't recognize. Apply path.basename inline to every user-derived path segment — the pattern Snyk accepts as a Path Traversal sanitizer.
Direct fs.readFileSync on a projectId-derived path kept flagging as Path Traversal even after path.basename sanitizers. Route the read through the existing getUidMapperDb model — same path resolution but no direct sink in this file.
runCli emits 'Migration Process Completed' but writes uid-mapper and 'No config file generated' lines after it, so a last-log-only check never fired on delta runs. Scan the whole logs array instead — the purge-on-start effect makes this safe from cross-iteration replays.
- CMG-1103 JSON RTE hyperlinks: plain -> type:'a', entry/asset -> type:'reference' with display-type:'link' so URLs survive in destination JSON RTE. - CMG-1104 entry-mapper selection persistence: locale-scoped server toggle, stale-fetch generation guard, unified data-load effect on both contentTypeUid and selectedLocale, tableRevision-based remount so Venus's Table picks up the fresh initialSelectedRowIds after each locale switch. - CMG-1097 asset mapper search-empty state: force .Table height when it contains an .EmptyState and center the .Table__centerWrapper so the illustration + heading + description render properly. - CMG-1105 map-entry count mismatch: getContentTypes now unions content types across all prior iterations (1..N-1) instead of only N-1, so types migrated in earlier iterations but absent from N-1 are still classified as 'old' on iteration N.
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
🔒 Security Scan Results
⏱️ SLA Breach Summary
ℹ️ Vulnerabilities Without Available Fixes (Informational Only)The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:
✅ BUILD PASSED - All security checks passed |
- CMG-1106: asset download falls back to fields.file.upload when .url is missing (newly-added, not-yet-CDN-processed Contentful assets). Same fallback in extractAssets.js; skip rows with neither. - CMG-1105: on iteration 2+, Map Entry Assets tab shows only assets already migrated in a prior iteration (has contentstackAssetUid). Brand-new assets upload automatically without a row to select.
🔒 Security Scan Results
⏱️ SLA Breach Summary
ℹ️ Vulnerabilities Without Available Fixes (Informational Only)The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:
✅ BUILD PASSED - All security checks passed |
umesh-more-cstk
left a comment
There was a problem hiding this comment.
Review summary
Six delta defects in one PR. I traced each fix through the real code paths (uid-mapper write order in runCli, removeEntriesFromDatabase config-key shape, Venus InfiniteScrollTable internals, createRefrence output shape) rather than reading the diff alone. Most of the reasoning is sound and several fixes are provably correct. Two changed code paths look broken in ways that will show up on real exports, and there are a few undisclosed behaviour changes worth a conscious decision before merge.
Blocking
api/src/services/contentful/jsonRTE.ts:399—parseEntryHyperlinkreadscontent-type-uidfromtarget.sys.contentType.sys.id, which does not exist on a Contentful export Link ({sys:{type:'Link',linkType:'Entry',id}}). It will always be'', and even when present it would be the camelCase Contentful id, not the snake_case CT uid this repo generates.parseBlockReference(lines 218-249) already solves exactly this by reading_content_type_uidout of the rte-references file.api/src/services/contentful.service.ts:675— the new.uploadfallback lets unprocessed Contentful assets reach the download path, but the code immediately below still dereferencesfields.file[locale].fileName(688-691) and.details.size(725, and again 759 with no optional chaining). Unprocessed assets have nodetails(that is why they have no.url), so the very case CMG-1106 targets throws, downloads twice, and is dropped with nofailedJSONentry.
Non-blocking (questions / nits)
- Locale bookkeeping now lives only inside
if (configFilePath)inmigration.service.tswhilerunClionly records for iteration ≤ 1 — iteration ≥ 2 runs whereremoveEntriesFromDatabasereturnsnullrecord nothing at all (see inline). getAssetMapping's newdisplayMappingfilter is a user-visible behaviour change with no ticket in the description.entryMapper.tsx: entries are now fetched only from the locale effect, so a project whose locale map can't be resolved shows a permanently empty Map Entry table; and each content-type click now fires two identical requests.MigrationLogViewer:'Migration Process Completed'is written byrunClibefore the delta update CLI runs, so adding'Entry Update Process Completed'as an OR doesn't stop the early completion flip on iteration 2+.- Reference resolution in
entry-update-script.cjsis top-level-only (group / modular-block nesting still keeps source uids). - Small doc/code mismatch in
flattenNestedUidMap, a truthiness check inupdateEntryStatus, and apickEntryTitleedge case — all inline.
Scope / process (out of diff, so noted here)
- The description claims dep bumps for
fast-uri/brace-expansionin root and each sub-project plus a rootpackage.jsonentry under Affected Areas. There is nopackage.jsonor lockfile in this diff at all (18 files, all source/test). Either the bumps were dropped or the description is stale — please reconcile, since the stated reason for keeping them here was to unblock the Snyk SLA check. - Conversely, four changes in the diff have no listed ticket: the asset
.uploadfallback (contentful.service.ts+extractAssets.js, code comment references CMG-1106 which isn't in the ticket list), thegetAssetMappingdelta display filter, theMigrationLogViewercompletion/purge rework, and themigrationCompleted: falsereset inpages/Migration/index.tsx. Each is defensible, but they widen the blast radius beyond the six linked tickets.
Verified good (worth calling out)
contentful.service.ts:439Array.isArray(entryId?.[id])is a genuine fix — the oldentryId?.idtested a literalidproperty, so the array branch was dead and already-array values got double-wrapped.parseAssetHyperlink's attrs (asset.uid/asset.filename/asset.content_type) match exactly whatsaveAssetwrites into the assets schema — unlike the pre-existingparseBlockAsset, which readsasset.fileNameand getsundefined.parseHyperlink→type:'a'+attrs.urlis the correct Contentstack JSON RTE anchor shape.- Deferring locale recording until after
updateEntryCliis right:updateEntryCli.service.ts:240-249really does swallow failures, so the old position could permanently skip a locale. writeUidMappingruns beforewritePerLocaleEntryUidMappinginrunCli(it assignsdata = {assets, entry}wholesale), so the newentryByLocaleread inmigration.service.tsisn't reading a clobbered key. Good ordering.setTableRevisionis correctly inside theseedSelectionbranch, so pagination/search don't remount the table; and I confirmed in the bundled Venus source thatgetSelectedRowis called withObject.keys(selectedRowIds)(seeded frominitialSelectedRowIds), so droppinginitialRowSelectedDatadoes not change the id set the app receives — only the unused second arg /maxSelectbookkeeping.extractAssets.js's newcontinueis inside afor…of, so it's valid; and locale codes flowing intomigrated_localesare consistently destination codes acrossextractLocalesFromUpdateConfig,entryByLocaleandmaster_localekeys.
Needs a human at a browser
The index.scss empty-state rules can't be verified statically. One thing to check: .asset-mapper-table .Table__body already carries flex: 0 0 auto !important (line ~858), so the new display:flex; flex-direction:column on the body won't let it grow into the forced calc(100vh - 22rem) table height — the illustration may be centred only because of min-height: 300px on .Table__centerWrapper. Please confirm at a couple of viewport heights / zoom levels.
- entry hyperlinks resolve content-type uid from rte-references instead of a field the Contentful export never populates; fall back to plain text when unresolvable. - asset download no longer crashes on .upload-only assets missing details/fileName; derived once, reused everywhere. - record delta locales even when no update-config was generated this iteration (iteration >= 2, nothing to localize). - entryMapper: safety-net fetch when locale resolution settles empty; drop now-duplicate direct fetchEntries calls. - MigrationLogViewer: terminal-message check is iteration-aware (delta requires the update-CLI's completion message, not the bulk import's). - updateEntryStatus returns 404 instead of a fake 200 when the locale filter matches zero rows. - entry-update-script: resolve references nested inside groups and modular blocks, not just top-level fields. - flattenNestedUidMap: per-key check instead of all-or-nothing, so mixed flat/nested uid maps normalize correctly. - extractEntries: hasAnyLocaleContent checks for meaningful content, not just key presence.
🔒 Security Scan Results
⏱️ SLA Breach Summary
ℹ️ Vulnerabilities Without Available Fixes (Informational Only)The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:
✅ BUILD PASSED - All security checks passed |
🔒 Security Scan Results
⏱️ SLA Breach Summary
ℹ️ Vulnerabilities Without Available Fixes (Informational Only)The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:
✅ BUILD PASSED - All security checks passed |
umesh-more-cstk
left a comment
There was a problem hiding this comment.
Reviewed all six fixes against the surrounding delta code paths and ran the API test suite locally (79 files, 725 tests — all passing). Overall a solid PR: the fixes are well-scoped, the intent comments are genuinely useful, and the new tests cover the resolver precedence and locale-extraction logic well.
Verified during review
- The
tableRevisionremount only bumps onseedSelectionfetches, so pagination/search don't reset the entry table. - The UI sends
localein theupdateEntryStatusbody (entryMapper.tsx:439), so the server-side scoping is actually exercised. __locale(destination dir name) andentryByLocalekeys agree, so the per-locale resolver lookups line up.- The union-of-prior-iterations CT fix is safe:
filterContentTypesByIterationonly consumesotherCmsUidfrom the prior list, so keeping the first-seen CT object per uid is harmless. - The
Array.isArray(entryId?.[id])typo fix incontentful.service.tsis correct.
Findings (details in the line comments)
- Medium — locale recording for iteration ≥ 2 is skipped entirely when no update config exists (
migration.service.ts), which can reproduce the "locale permanently skipped" bug this PR fixes via a different trigger. - Minor —
updateEntryStatus's locale guard silently no-ops on legacy rows without alanguagetag. - Nit —
flattenNestedUidMapdoc comment says "ANY" where the code doesevery. - Two informational notes on pre-existing behavior (early completion modal on delta runs; RTE
entry-uidnot remapped on the update-script path).
Description mismatch: the PR description claims dep bumps (fast-uri, brace-expansion) across root and sub-project manifests, but the diff contains no package.json changes — Snyk reports "No manifest changes detected in 8 projects". Presumably those landed on dev separately; please update the description so release notes aren't misled.
Generated by Claude Code
🔒 Security Scan Results
⏱️ SLA Breach Summary
ℹ️ Vulnerabilities Without Available Fixes (Informational Only)The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:
✅ BUILD PASSED - All security checks passed |
umesh-more-cstk
left a comment
There was a problem hiding this comment.
Review — 1 blocker, 4 questions, 1 nit
Reviewed at head 4fb87ff, base dev (85b1049) — 18 files, +1002/−163. Six delta-migration defects grouped into one PR. The core mechanisms hold up well: the union-of-prior-iterations fix for CMG-1095 is right, the locale plumbing is consistent (I traced entryByLocale → destination locale dir names in writePerLocaleEntryUidMapping, __locale/:: suffix → localeCode in removeEntriesFromDatabase, and migrated_locales → isFullMigrationForLocale — all destination codes, no source/destination mismatch), the updateEntryStatus locale scoping matches getEntryMapping's read-side filter exactly, and moving recordMigratedLocales after updateEntryCli resolves is the correct call. flattenNestedUidMap moving to uid-mapper.utils.ts with the per-key (rather than all-or-nothing) check is a genuine improvement over the version it replaces. Test coverage on the new helpers is good.
One blocker, on the UI completion path. The rest are questions where I want the author's read before calling them bugs.
Blocker
ui/src/components/LogScreen/MigrationLogViewer.tsx:214— on iteration ≥ 2 the completion detector requires the literal'Entry Update Process Completed', which onlyupdateEntryCli's success path writes. Two reachable paths never write it (update CLI skipped entirely when there's no config file and no asset updates; update CLI'scatch, which swallows the error).MigrationLogViewer.tsx:232is the only place in the UI that ever setsmigrationCompleted: true, so in both cases the user is stranded on the Execution Logs spinner after a fully successful run. Details and two suggested fixes inline.
Questions
api/src/services/contentMapper.service.ts:2031—if (foundEntry.length)correctly replaces an always-truthy check, butupdateEntryStatuslacks the previous-iteration fallback thatgetEntryMappinghas, so rows rendered from iteration N−1 now save with a hard 404 + "Failed to save entries" toast instead of the old silent 200.api/src/services/contentMapper.service.ts:2414— the new iteration-2+ Assets filter can empty the tab when the uid-mapperassetsmap hasn't been written yet, which is indistinguishable from "nothing to update" — awkward given CMG-1097 is about that very empty state.api/src/services/contentful/jsonRTE.ts:396—parseBlockquoteandparseHeading1–parseHeading6don't forwardlang/destination_stack_id, so entry/asset hyperlinks nested in a heading or blockquote still lose their URL. CMG-1103 is only partly fixed.api/src/utils/entry-update-script.cjs:83—.some()-basedisReferenceArrayshort-circuits the deep walk for mixed arrays, so nested references inside sibling block objects would be skipped. Looks unreachable with today's Contentful producer; raising it as a latent trap.
Nit
ui/src/components/ContentMapper/entryMapper.tsx:695— removing theinitialRowSelectedDataprop leaves the backing state write-only (declared L113, set L255/L398, never read).
Scope
Flagging two things about the diff-vs-description fit — no action needed on the code itself, but the description should match what actually ships.
-
The dependency bumps described aren't in this PR. The description says "Dep bumps to clear Snyk SLA (
fast-uri,brace-expansion) in root and each sub-project package.json", and Affected Areas checks "Other: rootpackage.json(dep override bumps)". Nopackage.jsonor lockfile appears in the diff againstdev. Either the bumps were dropped during a rebase or the description is stale — worth confirming, because the Reviewer Notes cite unblocking the Snyk SLA check as the reason for keeping them in this PR, and that reason no longer applies. (For what it's worth, splitting them out would have been the cleaner call anyway, as the notes acknowledge.) -
Several substantive changes aren't attributable to any of the six linked tickets. All within the checked Affected Areas, so the checkboxes are accurate, but "What changed?" doesn't mention them and QA won't know to test them:
contentful.service.tssaveAsset—file.uploadfallback and defensivefileName/details.size/contentTypehandling. The code comment cites CMG-1106, which isn't in the ticket list. Good change (it removes a realhttps:undefinedaxios call and apath.basename(undefined)throw), just untracked.upload-api/migration-contentful/libs/extractAssets.js— same.uploadfallback, plus assets with no downloadable source are now skipped entirely rather than emitted withassetPath: ''. Behaviour change to Map Entry's asset list.contentMapper.service.tsgetAssetMapping— the iteration-2+ filter (see question above).MigrationLogViewer.tsx— log purge on run start, single-pass terminal detection, and moving the "already migrated" placeholder out of the.map(that last one is a real fix — it previously rendered once per log line).ui/src/pages/Migration/index.tsx— explicitmigrationCompleted: falseon start.contentful.service.ts:439—Array?.isArray(entryId?.id)→Array.isArray(entryId?.[id]). The old expression tested a literalidkey and was always false, so this actually activates a previously-dead branch and stops double-wrapping already-array reference values. Correct, but a real behaviour change hidden in a one-line diff. Note theelsestill returns[entryId?.[id]], which yields[undefined]for an unmapped reference — the same[undefined]thatentry-update-script.cjs's newisReferenceArraycomment works around. Worth fixing at the source rather than tolerating downstream.
Adding a line per item to the description (and a ticket for CMG-1106) would keep the delta-migration audit trail intact.
Generated by Claude Code
- contentful.service.ts: fix cs_failed.json double-nested write path;
add retryFailedAsset() to re-download a single asset that failed
during the last migration run.
- extractAssets.js: no longer skip assets with no url/upload — emit
with hasSource:false so the UI can show a reason instead of hiding
them silently.
- contentMapper.service.ts: enrich getAssetMapping rows with
status ('ok'|'missing'|'failed') + errorMessage; new
retryAssetDownload service fn; ?status= filter param; aggregate
missingCount/failedCount for the UI banner.
- New route/controller: PUT /mapper/retryAsset/:projectId/:assetUid.
- assetMapper.tsx: Status column with 'No source'/'Failed' badges +
Retry button; broken-asset count banner; status filter dropdown.
- migration.service.ts: write a guaranteed terminal log line after the delta branch regardless of outcome — fixes UI stuck on Execution Logs forever when nothing was selected to update or updateEntryCli failed internally. - contentMapper.service.ts (updateEntryStatus): locale guard only enforced when the row has a language tag (legacy rows are no longer permanently untoggleable); add iteration N-1 fallback mirroring the read side, so restart-fallback rows can be saved without a 404. - contentMapper.service.ts (getAssetMapping): only apply the delta asset filter once uid-mapper data actually exists — avoids showing an empty tab during the restart race before this iteration's uid data is written. - jsonRTE.ts: thread lang/destination_stack_id through parseBlockquote and parseHeading1-6 so hyperlinks nested in a heading/blockquote resolve instead of degrading to plain text. - entry-update-script.cjs: resolveReferencesDeep recurses into every array element instead of delegating arrays to the shallow isReferenceArray path, fixing mixed arrays (bare ref + nested-ref object) where the nested one was left unresolved. Added unit tests. - entryMapper.tsx: remove dead initialRowSelectedData state, unused since the InfiniteScrollTable prop was dropped.
🔒 Security Scan Results
⏱️ SLA Breach Summary
ℹ️ Vulnerabilities Without Available Fixes (Informational Only)The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:
✅ BUILD PASSED - All security checks passed |
….json Fixes Snyk path-traversal finding in getAssetMapping by reusing the existing sanitizeStackId/assertResolvedPathUnderBase helpers. Also adds unit tests for getAssetMapping and retryFailedAsset to close the function-coverage CI gap.
🔒 Security Scan Results
⏱️ SLA Breach Summary
ℹ️ Vulnerabilities Without Available Fixes (Informational Only)The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:
✅ BUILD PASSED - All security checks passed |
Adds icons to status badges, an amber (not red) heads-up banner, header/ column overflow fixes so long labels and UIDs stop bleeding into neighboring rows/columns, and rebalanced column widths for Size and Contentstack UID.
🔒 Security Scan Results
⏱️ SLA Breach Summary
ℹ️ Vulnerabilities Without Available Fixes (Informational Only)The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:
✅ BUILD PASSED - All security checks passed |
- entry hyperlinks resolve content-type uid from rte-references instead of a field the Contentful export never populates; fall back to plain text when unresolvable. - asset download no longer crashes on .upload-only assets missing details/fileName; derived once, reused everywhere. - record delta locales even when no update-config was generated this iteration (iteration >= 2, nothing to localize). - entryMapper: safety-net fetch when locale resolution settles empty; drop now-duplicate direct fetchEntries calls. - MigrationLogViewer: terminal-message check is iteration-aware (delta requires the update-CLI's completion message, not the bulk import's). - updateEntryStatus returns 404 instead of a fake 200 when the locale filter matches zero rows. - entry-update-script: resolve references nested inside groups and modular blocks, not just top-level fields. - flattenNestedUidMap: per-key check instead of all-or-nothing, so mixed flat/nested uid maps normalize correctly. - extractEntries: hasAnyLocaleContent checks for meaningful content, not just key presence.
- migration.service.ts: write a guaranteed terminal log line after the delta branch regardless of outcome — fixes UI stuck on Execution Logs forever when nothing was selected to update or updateEntryCli failed internally. - contentMapper.service.ts (updateEntryStatus): locale guard only enforced when the row has a language tag (legacy rows are no longer permanently untoggleable); add iteration N-1 fallback mirroring the read side, so restart-fallback rows can be saved without a 404. - contentMapper.service.ts (getAssetMapping): only apply the delta asset filter once uid-mapper data actually exists — avoids showing an empty tab during the restart race before this iteration's uid data is written. - jsonRTE.ts: thread lang/destination_stack_id through parseBlockquote and parseHeading1-6 so hyperlinks nested in a heading/blockquote resolve instead of degrading to plain text. - entry-update-script.cjs: resolveReferencesDeep recurses into every array element instead of delegating arrays to the shallow isReferenceArray path, fixing mixed arrays (bare ref + nested-ref object) where the nested one was left unresolved. Added unit tests. - entryMapper.tsx: remove dead initialRowSelectedData state, unused since the InfiniteScrollTable prop was dropped.
🔗 Jira Tickets
Parent story: CMG-789 — Delta Migration | v1.0.0
📋 PR Type
📝 Description
What changed?
entry-update-script.cjsresolves Link(Entry) fields on the delta/localize path; locale-migrated tracking moved afterupdateEntryCliresolves so silent failures don't permanently flag locales as done.extractEntries.jsnow uses each content type'sdisplayFieldwith fallbacks (title → name → first localized string → sys.id) instead of hard-codedtitle/name, so entries with other display fields no longer disappear from Map Entry..Tableheight when it contains an.EmptyStateand center.Table__centerWrapperso the illustration + heading + description render properly.→ type:'a' attrs.url, entry/asset→ type:'reference'withdisplay-type:'link'. URLs now survive to destination.tableRevisionremount so Venus's Table re-readsinitialSelectedRowIds.getContentTypesnow unions content types from every prior iteration (1..N-1) instead of only N-1, so types migrated in earlier iterations but absent from N-1 are still classified as "old" on iteration N.fast-uri,brace-expansion) in root and each sub-project package.Why?
Six independent delta-migration defects reported against v1.0.0. All share the same delta code paths so grouping keeps the review context tight.
🧩 Affected Areas
api— Node.js backendui— React frontendupload-api— Upload API serverdocker/docker-composepackage.json(dep override bumps)🧪 How to Test
Fresh project on
bugfix/cmg-1101-ref-locale. Migrate 3 iterations using the bundled fixtures.contentful-export.json(3 CTs, 1 locale). Complete migration; references resolve in destination.asic.json(24 CTs incl.gender/moreInformation/tip/widthwhich useinternalTitle). On Map Entry, all 24 should appear with entries. Save selections on multiple locales and verify each persists across locale switches.asic-merged.json(27 CTs = iter 1's 3 + iter 2's 24). Map Entry should show 26 content types (one CT genuinely has 0 entries).disclaimer) → verify the link is preserved in the destination JSON RTE.cd api && npm testpasses.Expected result: All 26 content types visible on iter 3 Map Entry; references and JSON RTE hyperlinks intact in destination; per-locale entry selections persist; assets search empty-state layout correct.
📸 Screenshots / Recordings
🔗 Related PRs / Dependencies
✅ Author Checklist
bugfix/cmg-1101-ref-locale.env/example.envupdated — N/Anpm testinapi/)README.md/ docs updated — N/A👀 Reviewer Notes