Skip to content

plugin-dashboard: WidgetConfigPanel's private resolveLabel reads only the RETIRED label form — an inline-locale-map title/description opens the panel EMPTY and can be saved over #5301

Description

@os-support-ai

Found while implementing #5264 (PR migrating ObjectMetricWidgetProps to string | I18nLabel). Not fixed there — different member surface, and the fix needs a decision this card records rather than guesses.

What

packages/plugin-dashboard/src/WidgetConfigPanel.tsx carries a private label resolver:

/** Resolve an I18nLabel (string or {key, defaultValue}) to a plain string. */functionresolveLabel(v: unknown): string{if(v===undefined||v===null)return'';if(typeofv==='string')returnv;if(typeofv==='object'){constobj=vasRecord<string,any>;returnobj.defaultValue||obj.key||'';}returnString(v);}

Its doc comment claims it resolves an I18nLabel. It does the opposite: { key, defaultValue } is the key-reference form @objectstack/spec RETIRED at 17.0.0-rc.6 (objectstack#5055), and the inline per-locale map that I18nLabelSchema actually admits has neither limb. Measured against this tree:

inputresolveLabel returns
{ en: 'Revenue', 'zh-CN': ... } — the spec's vocabulary""
{ key: 'a.b.c', defaultValue: 'Revenue' } — retired"Revenue"
'Revenue'"Revenue"

It is the same private-resolver class objectui#4032 swept out of DashboardRenderer, MetricWidget and MetricCard; this fourth copy was not in that pass. It is also the mirror of #5134 — a declared I18nLabel surface whose only read site cannot read the form the spec admits.

Why it is not merely cosmetic

The resolved value is not display-only — it seeds the editable draft:

constnormalizedConfig=React.useMemo(()=>({
...config,title: typeofconfig.title==='object' ? resolveLabel(config.title) : config.title,description: typeofconfig.description==='object' ? resolveLabel(config.description) : config.description,}),[config]);const{ draft, isDirty, updateField, discard }=useConfigDraft(normalizedConfig, ...);

So for a widget whose stored title is an inline locale map, opening the config panel shows an EMPTY Title field, and the draft that a save writes back carries '' where the author's map used to be. The panel is the surface Studio authors dashboards through, so the destructive path is the ordinary one — open the widget, change anything else, save.

Why it was not folded into #5264

That PR's fix shape is binding and scoped to ObjectMetricWidgetProps plus the fixture sweep. This is a different member surface (DashboardWidget config, not the widget's own props), and — unlike the four members there — the fix is not mechanical: resolveLabel is a module-level function with no locale in scope, so making it locale-aware means deciding which locale an authoring panel should display, and what a save should then write back. Those are not the same question:

  • Reading: pickLocalized(value, language) matches every sibling surface.
  • Writing: collapsing a multi-locale map to the one string the editor showed silently discards the other locales. Preserving them needs the panel to edit a map, or to write back only the active locale's entry.

The second half is a real design decision (arguably a Studio authoring-UX question, cf. #4163's "give Studio a way to author one"), which is why this is filed rather than guessed.

Repro

resolveLabel({en: 'Revenue','zh-CN': '收入'})// => "" (expected "Revenue" / "收入")

Note the pure-function repro is the honest one here: the panel path reaches it through normalizedConfig, so a component-level test would also be asserting useConfigDraft's behaviour.

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions