Skip to content

Commit d92b2a1

Browse files
fix(plugin-dashboard): retire the metric sub-caption reads at both ends (objectui#11389) (#11957)
Fixes #11389 Clause-②: no objectui#11070 remains open. This PR is the objectui reader half of ruling C on objectui#11389 (record `5942430353`), after objectstack's half landed as `99e1912afc` and shipped in `@objectstack/spec` 17.7.0, which objectui `main` installs. > **C: retire the metric sub-caption at both ends, objectstack first.** > A widget keeps one authored description, `widget.description`, still rendered as the card-header subtitle. ## What changes - **`@object-ui/plugin-dashboard`.** No metric tile draws a sub-caption any more, on either surface (`DashboardRenderer`, `DashboardGridLayout`), dataset-bound or stored inline. - `DatasetWidget` loses its `subCaption` prop, its read of the options bag's `description`, and the caption row. - The resolver module `widgetSubCaption.ts` (`useWidgetSubCaption`, both limbs) is deleted, and both dispatch sites stop passing the prop. - Both surfaces' inline metric arms spread `...options` onto the metric node, and `MetricWidget` draws the node's `description` as its caption. So the spread alone kept drawing `options.description`. Both arms now spread `withoutRetiredSubCaption(options)`, one helper in `widgetDispatch.ts`, so the two surfaces cannot disagree. `DashboardGridLayout`'s arm was not on the dispatch's list; it is the same read through the same spread. - `widget.description` still draws as the card-header subtitle on `DashboardRenderer`, translated through the widget's `description` bundle key. - **`@object-ui/i18n`.** `useObjectLabel()` no longer returns `widgetSubCaption` (limb 2's resolver). - **`@object-ui/sdui-parser`.** `CONSUMED_WIDGET_OPTION_KEYS` drops `description`; it is now exactly the five keys the spec declares. - **Docs.** `plugin-dashboard.mdx` (the options table row and the translation-key list), the plugin-dashboard README's `localized` section, and `dashboard-filters.md`, whose sentence "the renderer still draws a stored widget that carries them" this change made false for `options.description`. ## Exports removed or narrowed - `@object-ui/i18n`: the `widgetSubCaption` member of `useObjectLabel()`'s return value. Nothing in this repo read it. - `@object-ui/sdui-parser`: `CONSUMED_WIDGET_OPTION_KEYS` loses `'description'`. The export stays. - `@object-ui/plugin-dashboard`: nothing on the package entry. `useWidgetSubCaption`, `SubCaptionWidget` and `DatasetWidget`'s `subCaption` prop were internal. The new `withoutRetiredSubCaption` is internal too. The changeset `.changeset/11389-retire-metric-subcaption.md` declares the three packages `minor` with the BREAKING note and lists the same removals. ## The restart probe, run first on the install surface The hold comment (`5947079886`) ordered it before any edit: - **Installed spec.** `@objectstack/spec` resolves to 17.7.0 from `packages/types`, `packages/i18n`, `packages/plugin-dashboard` and `packages/sdui-parser`. - **Refused by name.** `TranslationDataSchema` and `TranslationBundleSchema` refuse a bundle carrying `dashboards.D.widgets.W.subCaption`: `invalid_type` at that path, with a message naming the key as removed in 17.7.0. - **Lit control.** The same bundle with `widgets.W.title` parses on both schemas. - **Consumed sets.** The installed `@objectstack/sdui-parser@17.7.0` (pulled in by `@objectstack/lint`) has five keys and no `description`. objectui's own `CONSUMED_WIDGET_OPTION_KEYS` still had `description` before this PR. ## The dispatch's mechanism assumptions, measured **H1 (read sites): confirmed, plus one.** Re-measured on `19a7348` (`main` had moved past the seat's `f1781be`). The read sites: - `DatasetWidget`'s metric branch; - `useWidgetSubCaption`'s limb 1; - `DashboardRenderer`'s inline metric arm (`description: subCaption` after the spread); - the two dataset dispatch sites (`subCaption={tWidgetSubCaption(datasetWidget) ?? null}`); - **the extra one:** `DashboardGridLayout`'s inline metric arm, whose bare `...options` spread carried the key into `MetricWidget`. **H2 (limb 2 has no live feed on 17.7.0): holds.** - **Producers measured: zero.** No non-test `subCaption` bundle entry exists anywhere in objectui. On objectstack `origin/main` (`f4bed583`) only the tombstone, the conversion, the migration entries and prose remain. - **The server cannot serve one.** A 17.7.0 server refuses a stored `subCaption` at load: the conversion `translation-widget-sub-caption-removed` is `retiredFromLoadPath`, and the schema tombstone does the refusing. - **Doors that still take the key without the schema.** There are three: - a host's `I18nProvider` `config.resources`; - a host's `loadLanguage`: `transformSpecTranslations` copies `dashboards` through verbatim; - a pre-17.7 server serving a stored entry. - **Why the open doors are not a feed.** Each would carry only a key the installed spec refuses by name, and nothing writes one. Keeping a client reader for them would be consumer-side tolerance for off-spec metadata (AGENTS.md #0.1). So this PR takes the full route: limb 1, limb 2, the hook, the prop chain and the `useObjectLabel` member. **H3 (the parser half): narrows.** The accepted set goes from 6 keys to 5. For an authored `options.description` on a dataset-bound widget: - **Before:** no diagnostic. Ablation A below reads the old set as `expected [] to have a length of 1 but got +0`. - **After:** exactly one diagnostic, severity `warning`, code `unconsumed-widget-option`. It is a warning, never an error, and `suppressWarnings` still applies. ## Pins: reversed, not deleted - **`DashboardRenderer.metricSubCaption.test.tsx` (inline metric arm, both surfaces).** - An authored string, or a per-locale map, draws nothing. - A bundle `subCaption` draws nothing, while the same entry's `title` translates (the lit control). - On `kpi`, `widget.description` draws as the header subtitle and `options.description` beside it does not. - A dashboard with no `name` draws nothing either. - **`DashboardSurfaces.datasetSubCaption.test.tsx` (dataset tile, both surfaces).** - R1 and R2: an authored string or map grows no caption node. - R3: a bundle `subCaption` grows none, with or without an authored value. - L1 (lit, on every case): the value is drawn. - L2: the translated `widget.description` header subtitle is drawn. - **`DatasetWidget.subCaption.test.tsx` (direct mount).** Every authored form renders the no-caption markup byte for byte. - **`DashboardRenderer.servedLocalized-11295.test.tsx` and app-shell `DashboardView.servedLocalized-11295.test.tsx`.** The four live texts keep their pins. The retired sub-caption stays in the fixtures (on the served widget and in the bundle) and is pinned absent. - **`dashboard-widget-options.test.ts`.** A new H3 block, with a control: a widget-level `description` and a declared option draw nothing. - **`dashboard-widget-options-census.test.ts`.** - Leg 1: no undeclared key is accepted. - Leg 2: the read set is the declared set, and `description` is refused by name. - Leg 3 (reversed): no `plugin-dashboard` source reads `options.description` or resolves a `subCaption`, with a lit `options.limit` control on the same comment-stripped scan. The resolver module is gone and no surface calls its hook. - Leg 4: the tripwire list loses `widgetSubCaption.ts` and `useObjectLabel.ts`. - **`useObjectLabel-identity-5564.test.tsx`.** The member-count pin goes from 28 to 27 and now names `widgetSubCaption` as absent. ## Reverse verification All three ablations ran from the committed `344f8cea`. Each mutation went through `ablation-replace.mjs`, with the anchor count and the blob change verified on disk, and each restore was proven: blob equal to `HEAD` and `git diff HEAD` empty. The tests resolve `@object-ui/*` to `src/` through the root vitest aliases and import their subjects relatively, so no `dist/` leg applies. - **A: put `description` back into `CONSUMED_WIDGET_OPTION_KEYS`.** Red as predicted: 4 failed and 27 passed. The failures are the two H3 cases, census leg 1 and census leg 2's "every accepted key has a read site". The H3 control stayed green. - **B: make `withoutRetiredSubCaption` return `options` unchanged.** Red as predicted: 18 failed and 19 passed. Every failure is an inline-arm pin: `metricSubCaption` on both surfaces, and both `servedLocalized` files. The two dataset-tile files stayed green, so the pins discriminate by arm. - **C: re-inject a string-only `options.description` caption into `DatasetWidget`'s metric branch.** Red as predicted: 11 failed and 29 passed. - **Red:** census legs 2 and 3; R1 and R3 (authored beside a bundle entry) on both surfaces; L2; and the direct-mount string, overlay and repro cases. - **Green:** the per-locale map cases, because the injected read was string-only, and the inline-arm file. That is the direction expected. - **First attempt:** it was refused by the tool, because the replacement contained the anchor. It held the lock 0s and ran no test, and the restore was proven. It was re-run with a non-repeating anchor. ## Tests and gates The head is `0916a26d`. The two commits after `344f8cea` touch only `packages/i18n`: the count pin, and one restored space in `useObjectLabel.ts`. **Tests (`pnpm exec vitest run` from the repo root):** - **`packages/plugin-dashboard/`:** 174 files, 1725 passed and 6 skipped, at `344f8cea`. The baseline at `19a7348` was 195 files and 2030 passed across plugin-dashboard and sdui-parser together. - **Part two, at `b8b7d515`:** 117 files, 2304 passed and 13 skipped. It covered: - `packages/sdui-parser/` and `packages/i18n/`; - the app-shell `servedLocalized` test; - the outside tests that read the changed files or symbols: `unconsumed-widget-option-claim-6186`, `one-authority-per-exported-name-6273`, `markdown-test-inputs`, `dollar-dialect-alias-census`, `check-doc-component-types`, `check-control-bytes`, `check-i18n-call-site-keys`, `check-i18n-dead-keys`, the schema-catalog dashboard gallery render, the console and cli `unfulfilled-chart-stubs-8760` tests, and the types `dashboard-widget-slot-component-arm-7952` and `strict-face-read-keys-11070` tests. - The first part-two run, at `344f8cea`, was red in exactly one test: the i18n member-count pin, 28 against 27. `b8b7d515` updates it. - **`packages/i18n/` and `packages/sdui-parser/` again, at `0916a26d`:** 103 files, 1617 passed and 13 skipped. **Type-check:** - `pnpm --filter … run type-check` for `@object-ui/plugin-dashboard`, `@object-ui/sdui-parser` and `@object-ui/i18n` is green at `344f8cea`, with the script name echoed for each. - `@object-ui/i18n` is green again at `0916a26d`, and `@object-ui/app-shell` is green at `344f8cea`. - `tsconfig.test.json --listFilesOnly` lists the 4 changed plugin-dashboard test files and the 2 changed sdui-parser test files. **Gates:** - **Green at `0916a26d`:** `check-changeset-presence` (1 changeset, 4 released packages touched), `check:changeset-claims`, `check:new-line-citations` (0 new), `check:control-bytes`, `check:i18n-keys`. - **Green at `b8b7d515`:** `check:pending-changeset-literals`, `check:i18n-drift`, `check:i18n-dead-keys`, `check:doc-fences`, `check:doc-types`, `check:doc-snippets` (784 of 784 blocks judged, 0 failed), `check:doc-examples`, `check:readme-exports`, `check:skill-examples`. - **How the build-dependent gates got a build.** The last four first answered PRECONDITION NOT MET (unbuilt `dist/`). They ran after the scoped build `check-doc-snippet-types.mjs --build-filter` names (35 tasks). - **Lint.** `pnpm lint` is CI's. ESLint over the 14 changed TS/TSX files gives 0 errors and 34 warnings. One warning sits on a changed line: the `DatasetWidget` signature, rewritten to drop `subCaption`; its `widget: any` predates this branch. This is a targeted run, not a proven narrowing. - **Governed surface.** `check-governed-queue-guard.mjs --test` answers NOT GOVERNED for all 18 paths. ## Acceptance notes - **A producer outside both repos is not measured.** objectui#7293, the card that taught `DatasetWidget` to draw `options.description`, was filed from `objectstack-ai/duly` (duly#109), which asked for a caption under a KPI value. The ruling's producer count covered objectui and objectstack. Whether duly authors `options.description` today is NOT MEASURED: this session could not read that repository (REST 403, and repository access was not granted). If it does, its tiles lose that caption silently on render, and the authoring validator now warns `unconsumed-widget-option`. The ruling already names the way back: a declared widget-level key outside `options`. - **The grid surface draws no header subtitle.** `DashboardGridLayout`'s card header draws the title only, never `widget.description`; `DashboardRenderer` draws both. This predates this PR and is dormant: no in-repo surface mounts `DashboardGridLayout`. Not filed. - **Stale objectstack comments.** Two example comments in objectstack ("plus the description sub-caption", in the app-crm pipeline and app-todo task dashboards) go stale with this PR. The holder's pointer (`5946940099`) says objectstack seat post #6017 carries them for the `.objectui-sha` bump. The implementing session is `https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw`. --- _Generated by [Claude Code](https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f0496bd commit d92b2a1

19 files changed

Lines changed: 473 additions & 893 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@object-ui/plugin-dashboard': minor
3+
'@object-ui/i18n': minor
4+
'@object-ui/sdui-parser': minor
5+
---
6+
7+
The metric sub-caption is retired on the reader side (objectui#11389, ruling C, the objectui half after `@objectstack/spec` 17.7.0). **BREAKING** for any dashboard that drew a caption under a metric's value.
8+
9+
A metric tile used to draw a sub-caption under its value from the widget's `options.description`, translated by a client bundle entry at `dashboards.NAME.widgets.ID.subCaption`. The spec never declared that options key, and its only writer was the server's `translateDashboard` overlay. `@objectstack/spec` 17.7.0 removed the overlay and refuses a `subCaption` translation entry by name. objectui now stops reading both:
10+
11+
- **`@object-ui/plugin-dashboard`.** No metric tile draws a sub-caption, on either dashboard surface (`DashboardRenderer`, `DashboardGridLayout`), whether the tile is dataset-bound or a stored inline metric. An authored `options.description` (a string or a per-locale map) draws nothing, and neither does a bundle `subCaption` entry. A widget's one authored description, `widget.description`, still draws as the card-header subtitle on `DashboardRenderer`, translated through the widget's `description` bundle key. Nothing on the package entry is removed: the sub-caption resolver module and `DatasetWidget`'s `subCaption` prop were internal.
12+
- **`@object-ui/i18n`.** `useObjectLabel()` no longer returns `widgetSubCaption`. This removes a member of a published hook's return value: a caller that destructured it no longer compiles, and has nothing to call instead, because the key it read is refused by the spec.
13+
- **`@object-ui/sdui-parser`.** `CONSUMED_WIDGET_OPTION_KEYS` drops `'description'` and is now exactly the five keys the spec declares (`dateGranularity`, `limit`, `sortBy`, `sortOrder`, `stageOrder`). So `validateTree` reports an authored `options.description` on a dataset-bound dashboard widget as an `unconsumed-widget-option` **warning**, where it used to report nothing. It is a warning, not an error, and the widget's `suppressWarnings` escape hatch still applies.
14+
15+
**Clause-②: no (narrowing).** One export member is removed (`useObjectLabel().widgetSubCaption`) and one exported constant loses a member (`CONSUMED_WIDGET_OPTION_KEYS`). Nothing is added and no accepted input widens.
16+
17+
If a caption under a metric's value is wanted again, it returns as a declared widget-level key outside `options`, not as `options.description` (ruling C).

‎content/docs/guide/dashboard-filters.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@ into it as `runtimeFilter`.
7878
> `gauge`, `solid-gauge`, `kpi`, `bullet`, or a widget with no `type`) whose
7979
> `options.data` is a `{ "provider": "object", … }` query shows the
8080
> retired-format prompt below instead of its number (objectui#11525), as a
81-
> `pivot` widget with that query already did.
81+
> `pivot` widget with that query already did. And one key no longer draws on
82+
> any widget: `options.description`, the metric sub-caption, is retired at both
83+
> ends (objectui#11389), so a stored one draws nothing. A widget's one authored
84+
> description is `widget.description`, the card-header subtitle.
8285
8386
> **Retired: the top-level inline analytics shape.** `object` +
8487
> `categoryField` / `valueField` / `aggregate` on the widget itself (and the

‎content/docs/plugins/plugin-dashboard.mdx‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,14 @@ required), the renderers read exactly:
367367
| `sortBy` / `sortOrder` | orders by a projected dimension or measure |
368368
| `limit` | caps the row count |
369369
| `stageOrder` | explicit stage order for `funnel` / `pyramid` |
370-
| `description` | metric-card sub-caption; the `widgets.{id}.subCaption` translation channel writes this key |
370+
371+
These are the five keys the spec declares, and nothing else is read.
372+
`options.description` is not among them: it was the metric-card sub-caption,
373+
retired at both ends (objectui#11389). `@objectstack/spec` no longer writes
374+
it from a translation, and objectui no longer draws it. A widget has one
375+
authored description, `widget.description`, drawn as the card-header subtitle.
376+
If a caption under a metric's value returns, it returns as a declared
377+
widget-level key, not as an `options` key.
371378

372379
Every other `options` key on a dataset-bound widget **reaches no renderer**.
373380
The authoring validator (`validateTree` in `@object-ui/sdui-parser`) reports
@@ -518,8 +525,10 @@ which lists the records for either form and draws no report.
518525
## Translations — a served dashboard is translated once
519526

520527
Widget texts have a client translation channel: bundle keys under
521-
`dashboards.<name>.widgets.<id>` (`title`, `description`, `subCaption`), plus
522-
`dashboards.<name>.label` and `.description`. A document read from
528+
`dashboards.<name>.widgets.<id>` (`title`, `description`), plus
529+
`dashboards.<name>.label` and `.description`. There is no `subCaption` key:
530+
the translation schema in `@objectstack/spec` refuses it by name in a bundle,
531+
and the renderer reads no such key (objectui#11389). A document read from
523532
ObjectStack's `/meta` route does not need it. The server has already applied
524533
those keys for the request's locale, and it keeps a published edit over them:
525534
an explicit override beats the packaged default.

‎packages/app-shell/src/views/DashboardView.servedLocalized-11295.test.tsx‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
*
2727
* Directions, written before the run: the edited cells RED before the change
2828
* (the bundle answered), GREEN after; the unedited cells GREEN on both sides.
29+
*
30+
* The widget's retired sub-caption (objectui#11389, ruling C) stays in the
31+
* fixtures — an `options.description` on the served widget and the bundle's
32+
* `subCaption` entries — and every case pins that none of it draws on the
33+
* console page.
2934
*/
3035

3136
import * as React from 'react';
@@ -63,6 +68,7 @@ const BUNDLE = {
6368
system_overview: {
6469
label: 'System Overview',
6570
description: 'Platform health at a glance',
71+
// `subCaption` is refused by the installed spec; kept to pin that it draws nothing.
6672
widgets: { widget_total_users: { title: 'Total Users', description: 'Active accounts', subCaption: 'Across all organizations' } },
6773
},
6874
},
@@ -86,15 +92,17 @@ interface Texts {
8692
description: string;
8793
title: string;
8894
widgetDescription: string;
89-
subCaption: string;
9095
}
9196

97+
/** The retired sub-caption, authored and bundled: none of it may draw. */
98+
const RETIRED_SUB_CAPTION = 'All orgs (edited-11295)';
99+
const RETIRED_TEXTS = [RETIRED_SUB_CAPTION, 'Across all organizations', '覆盖所有组织'];
100+
92101
const EDITED: Texts = {
93102
label: 'Operations board (edited-11295)',
94103
description: 'What the ops team watches (edited-11295)',
95104
title: 'Total Users (edited-11295)',
96105
widgetDescription: 'Accounts (edited-11295)',
97-
subCaption: 'All orgs (edited-11295)',
98106
};
99107

100108
const SERVED_UNEDITED: Record<'en' | 'zh-CN', Texts> = {
@@ -103,14 +111,12 @@ const SERVED_UNEDITED: Record<'en' | 'zh-CN', Texts> = {
103111
description: 'Platform health at a glance',
104112
title: 'Total Users',
105113
widgetDescription: 'Active accounts',
106-
subCaption: 'Across all organizations',
107114
},
108115
'zh-CN': {
109116
label: '系统概览',
110117
description: '平台健康一览',
111118
title: '用户总数',
112119
widgetDescription: '活跃账户',
113-
subCaption: '覆盖所有组织',
114120
},
115121
};
116122

@@ -126,7 +132,7 @@ function served(texts: Texts) {
126132
type: 'kpi',
127133
title: texts.title,
128134
description: texts.widgetDescription,
129-
options: { value: 42, description: texts.subCaption },
135+
options: { value: 42, description: RETIRED_SUB_CAPTION },
130136
},
131137
],
132138
};
@@ -167,6 +173,7 @@ describe('DashboardView — a served dashboard is drawn as served (objectui#1129
167173
expect(h1.textContent).toBe(EDITED.label);
168174
for (const text of Object.values(EDITED)) expect(screen.getAllByText(text).length).toBeGreaterThan(0);
169175
for (const text of Object.values(SERVED_UNEDITED[language])) expect(screen.queryByText(text)).toBeNull();
176+
for (const text of RETIRED_TEXTS) expect(screen.queryByText(text)).toBeNull();
170177
});
171178

172179
it.each(['en', 'zh-CN'] as const)('%s: an unedited dashboard shows the translation the server put in', async (language) => {
@@ -175,5 +182,6 @@ describe('DashboardView — a served dashboard is drawn as served (objectui#1129
175182

176183
expect(h1.textContent).toBe(texts.label);
177184
for (const text of Object.values(texts)) expect(screen.getAllByText(text).length).toBeGreaterThan(0);
185+
for (const text of RETIRED_TEXTS) expect(screen.queryByText(text)).toBeNull();
178186
});
179187
});

‎packages/i18n/src/__tests__/useObjectLabel-identity-5564.test.tsx‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,14 @@ describe('useObjectLabel identity (objectui#5564)', () => {
170170
// by objectui#7219 (ruled 2026-09-02), taking the count from 27 to 26.
171171
// objectui#11344 added `actionOutcome` (an action's per-outcome success
172172
// copy), taking it back to 27. objectui#11696 added `objectPluralLabel`
173-
// (the name of an object's list), taking it to 28. A new resolver must land
173+
// (the name of an object's list), taking it to 28. objectui#11389 retired
174+
// `widgetSubCaption` with the metric sub-caption (ruling C), taking it to
175+
// 27. A new resolver must land
174176
// on both paths at once, because there is only one path — and a retired one
175177
// leaves both at once for the same reason, which is what the equality above
176178
// measures and this count anchors to an absolute.
177-
expect(Object.keys(unbound.seen[0])).toHaveLength(28);
179+
expect(Object.keys(unbound.seen[0])).toHaveLength(27);
180+
expect(Object.keys(unbound.seen[0])).not.toContain('widgetSubCaption');
178181
expect(typeof unbound.seen[0].objectLabel).toBe('function');
179182
expect(unbound.seen[0].objectLabel({ name: 'lead', label: 'Lead' })).toBe('Lead');
180183
});

‎packages/i18n/src/useObjectLabel.ts‎

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -463,41 +463,18 @@ export function useObjectLabel() {
463463
* Resolve translated widget description within a dashboard.
464464
* Convention: `{ns}.dashboards.{dashboardName}.widgets.{widgetId}.description`.
465465
* Returns undefined when neither metadata nor translation provides one.
466+
*
467+
* This is a widget's ONE translated description. The sibling sub-caption
468+
* key, `widgets.{widgetId}.subCaption`, is retired with its resolver
469+
* (objectui#11389, ruling C): `@objectstack/spec` 17.7.0 refuses it by
470+
* name in a translation bundle, and nothing here reads it.
466471
*/
467472
widgetDescription: (dashboardName: string, widgetId: string, fallback?: string) => {
468473
const fb = fallback ?? '';
469474
const resolved = resolve(dashboardSuffixes(dashboardName, `widgets.${widgetId}.description`), fb);
470475
return resolved || undefined;
471476
},
472477

473-
/**
474-
* Resolve a translated metric-widget SUB-CAPTION within a dashboard.
475-
* Convention: `{ns}.dashboards.{dashboardName}.widgets.{widgetId}.subCaption`.
476-
* Returns undefined when neither metadata nor translation provides one.
477-
*
478-
* Deliberately its OWN key, not a second reader of `widgets.{id}.description`.
479-
* The KPI card renders two authored strings from two different fields — the
480-
* shared card header's `widget.description`, and the sub-caption under the
481-
* value, which is authored as `widget.options.description` — and the
482-
* objectstack#5428 item-4 ruling (2026-08-06) settled that they get two
483-
* keys, not one: 「两个作者字段两个 key」. Collapsing them would make one
484-
* translation entry silently retarget the other field on any widget type
485-
* that renders both at once (`kpi`, `gauge`, `bullet` — every metric-family
486-
* type except the self-contained `metric`).
487-
*
488-
* `subCaption` is the member objectstack#8056 added to the widget
489-
* translation node for exactly this, shipped in `@objectstack/spec@17.0.0`.
490-
* The server-side resolver reads the SAME key and overlays it onto
491-
* `options.description` (`translateDashboard`), so a document served
492-
* through `/meta` and a document translated here land on the same string —
493-
* this is the client half of one convention, not a second dialect.
494-
*/
495-
widgetSubCaption: (dashboardName: string, widgetId: string, fallback?: string) => {
496-
const fb = fallback ?? '';
497-
const resolved = resolve(dashboardSuffixes(dashboardName, `widgets.${widgetId}.subCaption`), fb);
498-
return resolved || undefined;
499-
},
500-
501478
/**
502479
* Resolve translated page label, falling back to pageDef.label.
503480
* Convention: `{ns}.pages.{pageName}.label`.

‎packages/plugin-dashboard/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -834,7 +834,7 @@ keeps a published edit over that catalog: an explicit override beats the
834834
packaged default. Pass `localized={true}` when your document came from such a
835835
read. The renderer then draws these texts as given:
836836

837-
- the widget `title`, `description` and sub-caption (`options.description`);
837+
- the widget `title` and `description`;
838838
- its own header `label` and `description`.
839839

840840
An inline per-locale map is still collapsed to the active language. The client

‎packages/plugin-dashboard/src/DashboardGridLayout.tsx‎

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ import {
1717
resolveWidgetType,
1818
toDashboardNodeType,
1919
unsupportedWidgetSchema,
20+
withoutRetiredSubCaption,
2021
type DashboardWidgetSlotEntry,
2122
} from './widgetDispatch';
2223
import { LEGACY_RETIRED_WIDGET_SCHEMA, isLegacyRetiredWidget, isRetiredEnvelopeNode } from './legacyRetiredWidget';
2324
import { DatasetWidget } from './DatasetWidget';
2425
import type { DashboardChartRenderSchema } from './chartRenderHandoff';
25-
import { useWidgetSubCaption } from './widgetSubCaption';
2626
import { useDashboardAutoRefresh } from './useDashboardAutoRefresh';
2727

2828
/** Bridges editMode transitions to the ObjectUI DnD system when a DndProvider is present. */
@@ -175,22 +175,6 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
175175
composeSeriesLabel(t, fieldLabel, objectName, yField, aggFn),
176176
[t, fieldLabel],
177177
);
178-
/**
179-
* The metric tile's sub-caption resolver — objectui#8889.
180-
*
181-
* This surface routes a dataset-bound widget to `DatasetWidget` exactly as
182-
* `DashboardRenderer` does (objectui#4614), so it owes that component the
183-
* same resolved sub-caption. It is the SAME hook the sibling calls, not a
184-
* second copy of the composition: the field's invariant is that its two
185-
* channels "can never disagree", and two independent resolvers are precisely
186-
* how they would.
187-
*
188-
* `schema.name` is the dashboard name every convention key on this surface is
189-
* built from (`BaseSchema.name`, which `DashboardComponentSchema` extends).
190-
* Absent it the hook degrades to the authored value alone — the same silent
191-
* degradation the sibling's title/description lookups perform.
192-
*/
193-
const tWidgetSubCaption = useWidgetSubCaption(schema.name);
194178
// The refresh indicator, the manual handler and the auto-refresh timer come
195179
// from the one implementation this component shares with `DashboardRenderer`
196180
// (objectui#8820), which is also the only place `refreshIntervalSeconds` is
@@ -415,7 +399,10 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
415399
// Its declared type is `DashboardMetricNodeSchema`, the
416400
// `CustomNodeRegistry` entry `./widgetDispatch` adds (objectui#11466).
417401
type: DASHBOARD_NODE_TYPES.metric,
418-
...options,
402+
// The card draws no sub-caption from `options` (objectui#11389, ruling
403+
// C): the spread drops the retired `description` key, the same way
404+
// `DashboardRenderer`'s metric arm does.
405+
...withoutRetiredSubCaption(options),
419406
label,
420407
value: options.value ?? rows[0]?.[valueField] ?? '—',
421408
};
@@ -711,13 +698,6 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
711698
? <DatasetWidget
712699
widget={datasetWidget}
713700
dataSource={dataSource}
714-
/* objectui#8889 — dispatch site 2 of 2, and the half that
715-
objectui#4614 exists to stop anyone from forgetting: the
716-
sibling passing this alone would fix one surface and leave
717-
this one silently unchanged. `?? null` says "a surface
718-
resolved it, to nothing", which is NOT the same as the
719-
prop being absent — see the prop's docblock. */
720-
subCaption={tWidgetSubCaption(datasetWidget) ?? null}
721701
/>
722702
: <SchemaRenderer schema={componentSchema} />}
723703
</div>

0 commit comments

Comments
 (0)