Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-dashboard,i18n): the widget config panel reads AND writes an inline-locale-map label (#5301) - #5427
Merged
os-support-ai merged 1 commit intoAug 20, 2026
Conversation
…an inline-locale-map label (#5301) `WidgetConfigPanel` carried a private `resolveLabel` reading `defaultValue || key` — the key-reference form `@objectstack/spec` retired at 17.0.0-rc.6 (objectstack#5055). The inline per-locale map `I18nLabelSchema` admits has neither limb, so it resolved to `''`. That value seeds the editable draft, so a map-titled widget opened with an empty Title field and the next save wrote `''` over the author's map. Reading now goes through `pickLocalized(value, language)`. Writing replaces only the active locale's entry: an untouched label round-trips the stored object itself, an edited one merges into the entry that was displayed, and `onFieldChange` forwards the merged map for the same reason. `@object-ui/i18n` gains `setLocalized`, the write-side inverse of `pickLocalized`, co-located with it because the entry written must be the entry read — pinned as a property in `setLocalized.test.ts`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-support-ai
marked this pull request as ready for review
August 20, 2026 17:05
Uh oh!
There was an error while loading. Please reload this page.
os-support-ai
deleted the
claude/issue-5301-widget-config-panel-locale-map
branch
August 20, 2026 17:06
This was referenced Aug 20, 2026
os-sales pushed a commit
that referenced
this pull request
Aug 21, 2026
…-valued widget titles (#5428) Both dashboard authoring surfaces guarded their widget-title input by making a map-valued title read-only, justified by "nothing can reach this path from stored metadata yet - I18nLabel was plain string through rc.5". The pinned @objectstack/spec is 17.0.0, whose I18nLabelSchema admits the map, and the same file family documented that widening sixty lines away. The branch was denying authors an edit rather than protecting an unreachable path, and its deferral pointed at objectui#4163, closed as completed on 2026-08-15. Adopts @object-ui/i18n's setLocalized (objectui#5301's ruling, shipped by PR #5427) at both surfaces: a save replaces only the active locale's entry and carries every other locale across untouched. Plain-string titles are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#5301
WidgetConfigPanelcarried a privateresolveLabeldocumented as resolving anI18nLabelwhile readingdefaultValue || key— the key-reference form@objectstack/specretired at 17.0.0-rc.6 (objectstack#5055). The inlineper-locale map
I18nLabelSchemaactually admits has neither limb, so{ en: 'Revenue', zh: '收入' }resolved to''. Fourth private copy of thatresolver; objectui#4032 swept the other three out of
DashboardRenderer,MetricWidgetandMetricCard.Not a display bug: the resolved value seeds the editable draft, so a map-titled
widget opened with an empty Title field and the next save wrote
''over theauthor's map — on the ordinary path (open widget, change anything, save).
The measurement the dispatch asked for FIRST
Triage flagged an unchecked assumption: nobody had verified what the dashboard
save path persists for other object-valued config keys. Measured — the panel
already round-trips untouched keys; it does not rebuild the config from the
draft. No diff mechanism is needed and the card is the small shape. The chain,
end to end:
useConfigDraft(source)seedsuseState({ ...source })— a shallow copy ofthe whole config object, not a projection over the schema's field list.
updateFieldreturns{ ...prev, [field]: value }— only the named key moves.sanitizeDraftForType(draft)spreads the draft and deletes a fixed deny-list(legacy analytics keys, retired action keys,
dimensionsfor metric-likes).DashboardWithConfig.handleWidgetSaveforwards that object verbatim.Object-valued keys therefore already survive —
dimensions/valuesare arrayscarried across untouched, pinned by the existing
sanitizeDraftForTypetests.The normalization of
title/descriptionwas the single lossy step in thepath, which is exactly where the fix lands.
The fix (maintainer ruling, 2026-08-20)
pickLocalized(value, language), matching every sibling surfacepost-objectui#4032.
label round-trips the stored object itself (asserted with
toBe, nottoEqual: an equal-but-rebuilt object is a weaker guarantee, since a rebuildwould add an entry for the active locale to a map that never carried one). An
edited label merges into the entry that was displayed.
onFieldChangetoo. The live-update callback is the panel's other way outand a data-loss path of its own:
DashboardWithConfigwrites it intoliveSchema, which is where the nextconfigprop is derived from. Forwardingthe bare editor string dropped the map before a save ever ran — open, type,
close without saving, re-open, and the other locales were already gone.
@object-ui/i18ngainssetLocalized(value, language, next), the write-sideinverse of
pickLocalized, co-located in the same file because the entrywritten must be the entry read. It follows
pickLocalized's first three limbs —exact tag, base language, region-qualified sibling — and deliberately stops
there:
default/en/ first-value are DISPLAY fallbacks handing back anotherlocale's string, so writing to one would let an author editing in
froverwriteEnglish. With no entry for the active locale the edit adds one. The pairing
pickLocalized(setLocalized(map, lang, s), lang) === sis pinned as a propertyover 11 cases.
languageis a dependency of the normalization memo and has to be: it is thelocale the draft was seeded from, and the write-back compares the draft against
pickLocalized(stored, language)to tell untouched from edited. Omitting itwould let a mid-session language switch compare an old-locale draft against the
new locale — every untouched label reading as edited, merged into the wrong entry.
Verification (tree
369e6a073)Run from the repo root. Vitest resolves
@object-ui/i18ntopackages/i18n/srcvia the root config's alias, so no build artifact sits between the edit and the
thing under test on any test leg. (The type-check leg is different —
tschasno
pathsentry for@object-ui/i18nand resolves it todist, so thedependency closure was built first.)
vitest run packages/plugin-dashboard packages/i18npnpm --filter @object-ui/plugin-dashboard --filter @object-ui/i18n type-checkpnpm --filter ... lintany/ fast-refresh warnings)check:control-bytescheck:self-importcheck:phantom-depscheck:i18n-keys/check:i18n-driftcheck-changeset-{presence,no-major,fixed}New coverage: 29 cases in
packages/i18n/src/__tests__/setLocalized.test.ts,21 in
packages/plugin-dashboard/src/__tests__/WidgetConfigPanel.inlineLocaleMap.test.tsx.Reverse verification — two legs, predicted before running
Leg A — read half reverted to the retired-form resolver. Predicted 6 red
(3 display cases, 2 untouched-round-trip cases, the
decase's displayassertion). Observed 7. The extra one is honest and worth recording: "writes
an emptied field into the active locale" types
''into a field the ablatedread had already rendered as
'', so React dispatches no change event, the panelnever goes dirty, and the Save button — which only renders when dirty — is never
found. It fails for a mechanical reason rather than the assertion, which is why
the prediction missed it. Assertion messages on the other six were the defect
verbatim:
expected '' to be '收入', andexpected { en: 'Revenue', zh: '' } to be { en: 'Revenue', zh: '收入' }— thedraft seeded empty, then merged back as an empty entry.
Leg B — write-back removed from the save path, read intact. Predicted 6 red,
every save-payload case, with display and
onFieldChangestaying green.Observed exactly 6, messages showing the data-loss shape directly:
expected '收入' to be { en: 'Revenue', zh: '收入' },expected 'Umsatz' to deeply equal { en: 'Revenue', zh: '收入', …(1) }.Both legs were restored with
git checkout HEAD -- ...from the committed fixand the marker's absence verified before re-running green.
Generated by Claude Code