Found while repairing objectui#9150 (PR objectui#9272). Named and filed rather than repaired
there: a different package, an authoring surface rather than a rendering one, and a WRITE
path — all outside that card's stated scope, which is the report renderer's own heading.
The defect
packages/app-shell/src/views/metadata-admin/inspectors/ReportDefaultInspector.tsx reads
the report chart's title into the Studio inspector through the same plain-string narrowing
objectui#9150 just removed from the renderer, and then commits over it with a shallow
merge:
const chartTitle = typeof chart.title === 'string' ? (chart.title as string) : '';
const commitChart = (patch) => { // param type: a RECORD of string to unknown
const next = { ...chart, ...patch }; // <- the shallow merge
onPatch({ chart: next.type ? next : undefined });
};
...
value={chartTitle}
onCommit={(v) => commitChart({ title: v || undefined })}
⚠️ The generic parameter in the snippet above is spelled out in WORDS, not in its
literal angle-bracket form. GitHub's body sanitizer deletes tag-shaped fragments on save
and fenced code blocks do not protect them, so written literally it would render as a bare
Record and the quote would be wrong in exactly the place it is being quoted for.
@objectstack/spec types ReportChartSchema.title as I18nLabel — a plain string OR an
inline locale map — so { "title": { "en": "Pricing", "zh-CN": "定价" } } is authored
surface the contract accepts. Against that stored value the two halves fail in opposite
directions and compound:
- The read half shows nothing. The map arm fails the narrowing,
chartTitle is '',
and Studio paints an EMPTY Title box over a report that has a title. The author is shown
the absence of the thing they authored.
- The write half destroys it.
commitChart({ title: v }) shallow-merges, so whatever
the author types lands as a plain string in place of the whole map — every other
locale gone. And because the box reads empty, committing it untouched sends
v || undefined, which DELETES the stored map outright.
So the damage does not need a mistake: the surface invites the author to retype a title it
told them was missing, and the retype is what silently drops every language they were not
looking at. Nothing on the path warns, and the loss is not visible in the panel that just
"changed" it.
Why this is not the same card
objectui#9150 is a READ site in @object-ui/plugin-report that drew no heading. This is a
read site AND a write site in @object-ui/app-shell that loses stored data. Fixing the
renderer does not touch it, and it fails in a strictly worse way: the renderer defect was
recoverable the moment the renderer was fixed, this one is not recoverable at all once the
other locales have been overwritten.
The repair already exists in this repo, as a pair
@object-ui/i18n ships both halves and its own doc comment describes exactly this failure:
pickLocalized(value, language) — the read half, the repo's one resolver for the union.
setLocalized(value, next, language) — the write half, documented as existing precisely
because "onChange(e.target.value) straight back over { en, zh-CN } destroys every
locale the author was not looking at, silently, on the first keystroke". It writes the
edit into the ACTIVE locale's entry and preserves every other one, and it deliberately
stops short of the display-only fallback limbs so an author editing in fr, shown the
en string, cannot overwrite English.
So the shape of the fix is settled and its two functions already exist; what is missing is
that this inspector calls neither.
Suggested acceptance (not a ruling)
- A report whose
chart.title is an inline locale map shows the ACTIVE locale's entry in
the inspector's Title box, not an empty one.
- Editing it writes back into that locale's entry and every other locale survives verbatim
— asserted on the committed patch, not just on the input.
- A plain-string title keeps behaving exactly as it does today (the lit control).
- Clearing the box does not delete sibling locales wholesale; decide and pin what clearing
one locale of a multi-locale title means.
- Worth checking in the same pass whether the neighbouring
subtitle / description
inspector inputs on this surface take the same shape, since they are the same union.
Reachability
Measured on origin/main at the time of filing, by reading the file: the narrowing, the
shallow merge and the two bindings quoted above are all live, and @object-ui/i18n is
already a dependency of @object-ui/app-shell.
Reported from the os-dev seat while landing PR objectui#9272 for objectui#9150. Generated
with Claude Code; session reference session_01UzHd6hDYatoDn17BuwKxnZ.
Generated by Claude Code
Found while repairing objectui#9150 (PR objectui#9272). Named and filed rather than repaired
there: a different package, an authoring surface rather than a rendering one, and a WRITE
path — all outside that card's stated scope, which is the report renderer's own heading.
The defect
packages/app-shell/src/views/metadata-admin/inspectors/ReportDefaultInspector.tsxreadsthe report chart's title into the Studio inspector through the same plain-string narrowing
objectui#9150 just removed from the renderer, and then commits over it with a shallow
merge:
literal angle-bracket form. GitHub's body sanitizer deletes tag-shaped fragments on save
and fenced code blocks do not protect them, so written literally it would render as a bare
Recordand the quote would be wrong in exactly the place it is being quoted for.@objectstack/spectypesReportChartSchema.titleasI18nLabel— a plain string OR aninline locale map — so
{ "title": { "en": "Pricing", "zh-CN": "定价" } }is authoredsurface the contract accepts. Against that stored value the two halves fail in opposite
directions and compound:
chartTitleis'',and Studio paints an EMPTY Title box over a report that has a title. The author is shown
the absence of the thing they authored.
commitChart({ title: v })shallow-merges, so whateverthe author types lands as a plain string in place of the whole map — every other
locale gone. And because the box reads empty, committing it untouched sends
v || undefined, which DELETES the stored map outright.So the damage does not need a mistake: the surface invites the author to retype a title it
told them was missing, and the retype is what silently drops every language they were not
looking at. Nothing on the path warns, and the loss is not visible in the panel that just
"changed" it.
Why this is not the same card
objectui#9150 is a READ site in
@object-ui/plugin-reportthat drew no heading. This is aread site AND a write site in
@object-ui/app-shellthat loses stored data. Fixing therenderer does not touch it, and it fails in a strictly worse way: the renderer defect was
recoverable the moment the renderer was fixed, this one is not recoverable at all once the
other locales have been overwritten.
The repair already exists in this repo, as a pair
@object-ui/i18nships both halves and its own doc comment describes exactly this failure:pickLocalized(value, language)— the read half, the repo's one resolver for the union.setLocalized(value, next, language)— the write half, documented as existing preciselybecause "
onChange(e.target.value)straight back over{ en, zh-CN }destroys everylocale the author was not looking at, silently, on the first keystroke". It writes the
edit into the ACTIVE locale's entry and preserves every other one, and it deliberately
stops short of the display-only fallback limbs so an author editing in
fr, shown theenstring, cannot overwrite English.So the shape of the fix is settled and its two functions already exist; what is missing is
that this inspector calls neither.
Suggested acceptance (not a ruling)
chart.titleis an inline locale map shows the ACTIVE locale's entry inthe inspector's Title box, not an empty one.
— asserted on the committed patch, not just on the input.
one locale of a multi-locale title means.
subtitle/descriptioninspector inputs on this surface take the same shape, since they are the same union.
Reachability
Measured on
origin/mainat the time of filing, by reading the file: the narrowing, theshallow merge and the two bindings quoted above are all live, and
@object-ui/i18nisalready a dependency of
@object-ui/app-shell.Reported from the os-dev seat while landing PR objectui#9272 for objectui#9150. Generated
with Claude Code; session reference
session_01UzHd6hDYatoDn17BuwKxnZ.Generated by Claude Code