Skip to content

finding(app-shell): the report chart's Title inspector narrows chart.title to a plain string and shallow-merges over it, so editing it destroys every other locale #9274

Description

@os-tesla

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:

  1. 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.
  2. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpackage: app-shellpm:dispatchedpriority:p1

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions