Skip to content

CMG-1057 | Restart Locale Mapper Add Language dropdown#1117

Merged
vikrantraut-cstk merged 3 commits into
devfrom
bugfix/cmg-1057-1058-locale-fix
Jul 21, 2026
Merged

CMG-1057 | Restart Locale Mapper Add Language dropdown#1117
vikrantraut-cstk merged 3 commits into
devfrom
bugfix/cmg-1057-1058-locale-fix

Conversation

@chetan-contentstack

Copy link
Copy Markdown

🔗 Jira Ticket

CMG-1057


📋 PR Type

  • ✨ Feature
  • 🐛 Bug Fix
  • 🔥 Hotfix
  • ♻️ Refactor
  • 🧹 Chore / Dependency Update
  • 📝 Documentation

📝 Description

What changed?

  • Add Language button no longer locks after a single addition on delta restart. filledMappingCount had a stale sourceLocaleLabels.has(k) filter meant to defend against handleSelectedSourceLocale's old fallback-key path (source-first pick writing under source.label.toLowerCase()). That path has been gone for a while — source-first pick now no-ops in selectedMappings until CS is also picked — but the filter kept firing for legitimate CS locale keys whose codes match a source locale code (e.g. CS fr mapped to source fr, or jaja), dropping them from the count and freezing the button at 1.
  • Add Language stays enabled while any source is still unmapped, regardless of whether earlier rows are fully filled. The old hasIncompleteRow gate forced a strict "fill the current row before adding another", which meant the button toggled off after every Add and only came back when the row was fully mapped. Users can now spawn multiple empty rows and fill them in any order.
  • Row count is capped at sourceLocales.length. Prevents hammering Add and spawning empty rows that can never be filled — for 3 sources you get at most 3 rows on screen, whether they're mapped yet or not. Deleting a row frees up a slot again.

Why?

On delta-restart with sources like ['en', 'fr', 'ja'], a user could map the first additional locale, and Add Language would then stay disabled forever — trapping the user with 1 unmapped source they couldn't reach. Root cause was the stale filter above. While there, the "fully-fill-before-next-Add" behaviour was tightening the UX for no benefit; loosening it and adding a hard row cap gave a cleaner mapping flow.


🧩 Affected Areas

  • api — Node.js backend
  • ui — React frontend
  • upload-api — Upload API server
  • docker / docker-compose
  • CI / GitHub Actions workflows
  • Environment variables / config
  • Other:

Single file: ui/src/components/DestinationStack/Actions/LoadLanguageMapper.tsx (Add Language disabled predicate only).


🧪 How to Test

Prerequisite: a delta-migration project that finished iteration 1 with ≥ 3 source locales (e.g. Sitecore/Contentful project with source ['en', 'fr', 'ja'] and only en mapped as master).

  1. Restart migration on the project so it enters iteration 2, then land on Step 2 · Configure Destination Stack.
  2. Confirm the master row (en-us (master) ↔ en) renders with its saved source populated, and Add Language is enabled.
  3. Click Add Language → new empty row appears. Verify Add Language stays enabled (does not disable while the row is empty).
  4. Pick a CS locale whose code matches a source (e.g. CS fr → source fr). Add Language still enabled.
  5. Click Add Language again for the last unmapped source. Two empty rows now on screen. Row cap = 3 (master + 2), so Add Language should now be disabled.
  6. Fill in the last row (CS ja → source ja). All sources mapped → button remains disabled (correct — no more sources to add).
  7. Delete one non-master row: Add Language re-enables. Restore it: disables again.

Expected result:

  • Master row auto-populates on restart.
  • Add Language is enabled whenever at least one source is unmapped AND row count < total sources.
  • Row count never exceeds sourceLocales.length.
  • No CS-locale/source-code collision (frfr, jaja) locks the button.

📸 Screenshots / Recordings

Before After
Add Language disables permanently after the first addition when CS locale code matches a source code; 1 source stays unmappable. Add Language enabled while unmapped sources remain; row count capped at sourceLocales.length; users can spawn multiple empty rows and fill in any order.

🔗 Related PRs / Dependencies


✅ Author Checklist

  • Branch follows naming convention: feature/, bugfix/, or hotfix/ + 5–30 lowercase chars
  • Jira ticket linked above
  • Self-reviewed the diff — no debug logs, commented-out code, or TODOs left in
  • .env / example.env updated if new environment variables were added (n/a)
  • No sensitive credentials or secrets committed
  • Existing tests pass locally (npm test) (run before requesting review)
  • New tests written (or not applicable — explain why) — n/a: pure UI-gate predicate change, covered by the manual test plan above; no pure logic extracted to a utility.
  • README.md / docs updated if behaviour changed (n/a — internal UI gating only)
  • Talisman pre-push scan passes (no secrets flagged) (will confirm on push)

👀 Reviewer Notes

  • The whole change is contained in a single IIFE inside the Button's disabled={...} prop — the non-restart branch above it is untouched.
  • Please sanity-check the rowCapReached gate: it uses sourceLocales?.length (hydrated state), same source of truth as the existing allSourcesMapped check, so both bounds move together. If Redux sourceLocale hasn't hydrated yet, sourcesNotReady already disables the button — the cap doesn't need its own guard.
  • The retired sourceLocaleLabels filter's only caller was filledMappingCount, and filledMappingCount itself is gone with hasIncompleteRow, so no dead code is left behind.

…es remain, cap rows at source count

The Add Language button on delta-restart iteration was misbehaving in two ways:

1. It stayed disabled after a single addition when the picked CS locale's
   code happened to match a source locale code (e.g. CS `fr` mapped to
   source `fr`, or `ja` → `ja`). `filledMappingCount` had a stale
   `sourceLocaleLabels.has(k)` filter meant to defend against a
   fallback-key path in `handleSelectedSourceLocale` that no longer
   exists (source-first pick now no-ops until CS is picked). The filter
   was dropping legitimate CS keys, freezing the count at 1, and locking
   the button after the first Add.
2. The `hasIncompleteRow` gate forced users to fully fill each row
   before Add Language re-enabled. Removed — users can now spawn
   multiple empty rows and fill them in any order.

Added a hard row cap at `totalSources` so N sources → at most N rows on
screen (whether they're mapped yet or not), preventing a user from
hammering Add and stranding empty rows that no source can back.
@github-actions

Copy link
Copy Markdown

🔒 Security Scan Results

ℹ️ Note: Only vulnerabilities with available fixes (upgrades or patches) are counted toward thresholds.

Check Type Count (with fixes) Without fixes Threshold Result
🔴 Critical Severity 1 0 10 ✅ Passed
🟠 High Severity 7 384 25 ✅ Passed
🟡 Medium Severity 0 1 500 ✅ Passed
🔵 Low Severity 1 0 1000 ✅ Passed

⏱️ SLA Breach Summary

⚠️ Warning: The following vulnerabilities have exceeded their SLA thresholds (days since publication).

Severity Breaches (with fixes) Breaches (no fixes) SLA Threshold (with/no fixes) Status
🔴 Critical 1 0 15 / 30 days ❌ Failed
🟠 High 0 0 30 / 120 days ✅ Passed
🟡 Medium 0 0 90 / 365 days ✅ Passed
🔵 Low 0 0 180 / 365 days ✅ Passed

🔴 Critical Severity - SLA Breached Issues (with fixes)

Showing 1 issue(s) that have exceeded the 15-day SLA threshold:

  1. Resources Downloaded over Insecure Protocol
    • ID: SNYK-JS-ESBUILD-17750822
    • Package: esbuild@0.27.3
    • Published: 19 days ago (SLA: 15 days)
    • CVSS Score: 9.2

ℹ️ 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:

  • Critical without fixes: 0
  • High without fixes: 384
  • Medium without fixes: 1
  • Low without fixes: 0

❌ BUILD FAILED - Security checks failed

Please review and fix the security vulnerabilities before merging.

Unblocks the security-sca gate on the CMG-1057/1058 PR — its critical
SLA breach was SNYK-JS-ESBUILD-17750822 (esbuild@0.27.3, "Resources
downloaded over insecure protocol"), which reached its 15-day SLA
threshold while dev's scan hadn't re-run.

- upload-api: vitest ^4.0.18 → ^4.1.10 (also @vitest/coverage-v8), pulls
  in vitest ≥ 4.1.0 which patches CVE-2026-47429 (Vitest UI arbitrary
  file read/exec) and drops the vulnerable transitive esbuild
- ui: refresh package-lock.json so esbuild 0.27.3 → 0.27.7 across all
  @esbuild/* platform packages (via vite@^7.3.5, no manifest change)

No source-code changes; scope-limited to lockfile / dev-dep bumps.
@github-actions

Copy link
Copy Markdown

🔒 Security Scan Results

ℹ️ Note: Only vulnerabilities with available fixes (upgrades or patches) are counted toward thresholds.

Check Type Count (with fixes) Without fixes Threshold Result
🔴 Critical Severity 1 0 10 ✅ Passed
🟠 High Severity 7 384 25 ✅ Passed
🟡 Medium Severity 0 1 500 ✅ Passed
🔵 Low Severity 1 0 1000 ✅ Passed

⏱️ SLA Breach Summary

⚠️ Warning: The following vulnerabilities have exceeded their SLA thresholds (days since publication).

Severity Breaches (with fixes) Breaches (no fixes) SLA Threshold (with/no fixes) Status
🔴 Critical 1 0 15 / 30 days ❌ Failed
🟠 High 0 0 30 / 120 days ✅ Passed
🟡 Medium 0 0 90 / 365 days ✅ Passed
🔵 Low 0 0 180 / 365 days ✅ Passed

🔴 Critical Severity - SLA Breached Issues (with fixes)

Showing 1 issue(s) that have exceeded the 15-day SLA threshold:

  1. Resources Downloaded over Insecure Protocol
    • ID: SNYK-JS-ESBUILD-17750822
    • Package: esbuild@0.27.7
    • Published: 19 days ago (SLA: 15 days)
    • CVSS Score: 9.2

ℹ️ 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:

  • Critical without fixes: 0
  • High without fixes: 384
  • Medium without fixes: 1
  • Low without fixes: 0

❌ BUILD FAILED - Security checks failed

Please review and fix the security vulnerabilities before merging.

Prior bump (dcbf00b) moved ui's transitive esbuild from 0.27.3 → 0.27.7
by refreshing the lockfile, but SNYK-JS-ESBUILD-17750822 flags every
0.27.x. The Snyk fix lands in 0.28.x.

ui's `vite@^7.3.5` pins esbuild in the 0.27 range, so use an npm
override to force ^0.28.1 across every consumer of the package
(vite@7.3.5 → esbuild override; storybook's transitive esbuild dedupes
to the same). No manifest bump for vite — keeps the update surgical
and avoids the vite 7 → 8 major (vite 8 dropped esbuild entirely for
rolldown, but that's a bigger migration than a security CI unblock
warrants).
@github-actions

Copy link
Copy Markdown

🔒 Security Scan Results

ℹ️ Note: Only vulnerabilities with available fixes (upgrades or patches) are counted toward thresholds.

Check Type Count (with fixes) Without fixes Threshold Result
🔴 Critical Severity 0 0 10 ✅ Passed
🟠 High Severity 7 384 25 ✅ Passed
🟡 Medium Severity 0 1 500 ✅ Passed
🔵 Low Severity 0 0 1000 ✅ Passed

⏱️ SLA Breach Summary

✅ No SLA breaches detected. All vulnerabilities are within acceptable time thresholds.

Severity Breaches (with fixes) Breaches (no fixes) SLA Threshold (with/no fixes) Status
🔴 Critical 0 0 15 / 30 days ✅ Passed
🟠 High 0 0 30 / 120 days ✅ Passed
🟡 Medium 0 0 90 / 365 days ✅ Passed
🔵 Low 0 0 180 / 365 days ✅ Passed

ℹ️ 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:

  • Critical without fixes: 0
  • High without fixes: 384
  • Medium without fixes: 1
  • Low without fixes: 0

✅ BUILD PASSED - All security checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vikrantraut-cstk
vikrantraut-cstk merged commit cf6f8d5 into dev Jul 21, 2026
9 of 10 checks passed
@vikrantraut-cstk
vikrantraut-cstk deleted the bugfix/cmg-1057-1058-locale-fix branch July 21, 2026 08:55

@umesh-more-cstk umesh-more-cstk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — CMG-1057 Add Language dropdown

The isolated disabled-predicate change is a clean, correct fix. Retiring the sourceLocaleLabels/filledMappingCount filter is the right call — it genuinely mis-fired on legitimate CS keys whose code matched a source code (frfr, jaja). Cap-consistency holds: rowCapReached and allSourcesMapped both derive from sourceLocales, and the delete path clears selectedMappings→Redux so the button re-enables after a row delete.

Verdict: Comment (the code change alone I'd approve; my one blocking-class reservation is scope — see the dependency comments).

Blocking

  • Unrelated dependency/lockfile churn vs. stated scope — the PR says "single file / ui only" but also bumps esbuild (ui overrides) and vitest/@vitest/coverage-v8 in upload-api (not even checked in Affected Areas), with large lockfile churn in both packages. See inline comments on ui/package.json and upload-api/package.json.

Non-blocking

  • rowCap/allSourcesMapped drift edge case (inline, LoadLanguageMapper.tsx)
  • redundant optional chaining (inline)
  • Stale comment (nit): LoadLanguageMapper.tsx:419 still references filledMappingCount, which this PR deleted. The selectedLocaleKey guard it documents is still valid — just retarget the wording (e.g. "…would create an orphan mapping key and mis-count mapped sources"). Couldn't anchor inline since that line is outside the diff.

Comment thread ui/package.json
"form-data": ">=4.0.6",
"ws": ">=8.21.0"
"ws": ">=8.21.0",
"esbuild": "^0.28.1"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocker (scope): This esbuild override is unrelated to CMG-1057 (a locale-mapper UI gate). It's correctly placed in overrides (not runtime deps), but it silently moves the build toolchain 0.27.3 → 0.28.1 inside a bugfix PR and adds ~26 platform packages of lockfile churn.

The description says "Single file" / "Affected Areas: ui only".

Suggestion: split dependency bumps into a separate chore PR, or if a security pin must ride along, call it out in the description and check the "Dependency Update" box.

Comment thread upload-api/package.json
"ts-node": "^10.9.2",
"typescript": "^5.3.3",
"vitest": "^4.0.18"
"vitest": "^4.1.10"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocker (scope): vitest (and @vitest/coverage-v8 above) are bumped to ^4.1.10 here in upload-api — but upload-api isn't checked in the PR's Affected Areas, and this is unrelated to CMG-1057. Pulls in oxc/rolldown lockfile churn too.

Suggestion: move these into a separate dependency PR so the bugfix stays isolated and cleanly revertable.

const hasIncompleteRow = (cmsLocaleOptions?.length ?? 0) > filledMappingCount;
const mappedSources = new Set(
Object.values(savedMapping).filter(
(v): v is string => typeof v === 'string' && v?.length > 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: v is already narrowed to string by typeof v === 'string', so v?.length can be v.length. Harmless, but the ?. reads as if v could still be nullish here.

Comment on lines +916 to +918
const rowCapReached =
totalSources > 0 && (cmsLocaleOptions?.length ?? 0) >= totalSources;
return sourcesNotReady || allSourcesMapped || rowCapReached;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (medium): Both rowCapReached and allSourcesMapped assume every value in savedMapping corresponds to a source still present in sourceLocales. If a delta restart can carry a stale mapping for a source that no longer exists (source set shrank between iterations), this re-locks the button — the exact failure class this PR fixes, via a new vector:

Iter 1 mapped: { en-master_locale: en, de: de }   // 'de' later removed from source export
Iter 2 sourceLocales: [en, fr, ja]  → totalSources = 3
→ rebuilt = 2 rows (master, de);  mappedSources = {en, de}
→ user Adds a row (cap 3 reached), maps it to fr
→ mappedSources = {en, de, fr} → size 3 ≥ 3 → allSourcesMapped = true → disabled
→ 'ja' is now unreachable

Is localeMapping guaranteed to be reconciled against the current sourceLocales on restart? If a removed source can linger, harden by intersecting with the live source set:

const sourceSet = new Set(sourceLocales?.map((l) => l.value) ?? []);
const mappedSources = new Set(
  Object.values(savedMapping).filter(
    (v): v is string => typeof v === 'string' && v.length > 0 && sourceSet.has(v)
  )
);

If reconciliation is guaranteed upstream, no change needed — just confirm it.

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.

5 participants