From 856f56f01901c1432fb4bfa7513ad8d222423923 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 03:53:23 +0000 Subject: [PATCH 1/3] fix(plugin-dashboard): a dataset-less provider-object metric widget draws the retired-format placeholder on both surfaces (objectui#11525) Maintainer ruling C on objectui#11525: both dashboard surfaces map a single-value widget (metric, gauge, solid-gauge, kpi, bullet, and a typeless widget, which resolves to metric) with no dataset and an options.data / widget-level data of { provider: 'object', ... } to the shared LEGACY_RETIRED_WIDGET_SCHEMA, as the pivot arms already do (objectui#10528). The flat object-metric producer is removed from both metric arms. The inline chart and table arms are untouched, and the filter broadcast keeps object-metric in its filterable set, because an author's component-envelope node still reaches the merge. legacyRetiredWidget.ts gets a dated correction for the metric family; the docs page, the filters guide and the README say what the stored form now draws. New pins in inlineObjectMetricRetired-11525.test.tsx. Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC Co-authored-by: Claude --- content/docs/guide/dashboard-filters.md | 5 + content/docs/plugins/plugin-dashboard.mdx | 6 +- packages/plugin-dashboard/README.md | 7 +- .../src/DashboardGridLayout.tsx | 30 +-- .../src/DashboardRenderer.tsx | 58 +++-- .../src/ObjectMetricWidget.tsx | 9 +- ...DashboardGridLayout.legacyRetired.test.tsx | 8 + .../inlineObjectMetricRetired-11525.test.tsx | 225 ++++++++++++++++++ .../src/legacyRetiredWidget.ts | 16 ++ 9 files changed, 316 insertions(+), 48 deletions(-) create mode 100644 packages/plugin-dashboard/src/__tests__/inlineObjectMetricRetired-11525.test.tsx diff --git a/content/docs/guide/dashboard-filters.md b/content/docs/guide/dashboard-filters.md index 03d995419c..571a778a09 100644 --- a/content/docs/guide/dashboard-filters.md +++ b/content/docs/guide/dashboard-filters.md @@ -74,6 +74,11 @@ into it as `runtimeFilter`. > `@objectstack/spec` requires `dataset` on every widget. The renderer still > draws a stored widget that carries them, but that path is renderer-internal > (ADR-0021), not something to author against: bind the widget to a dataset. +> One stored form no longer draws at all: a single-value widget (`metric`, +> `gauge`, `solid-gauge`, `kpi`, `bullet`, or a widget with no `type`) whose +> `options.data` is a `{ "provider": "object", … }` query shows the +> retired-format prompt below instead of its number (objectui#11525), as a +> `pivot` widget with that query already did. > **Retired: the top-level inline analytics shape.** `object` + > `categoryField` / `valueField` / `aggregate` on the widget itself (and the diff --git a/content/docs/plugins/plugin-dashboard.mdx b/content/docs/plugins/plugin-dashboard.mdx index 88e191bf76..7387ceac63 100644 --- a/content/docs/plugins/plugin-dashboard.mdx +++ b/content/docs/plugins/plugin-dashboard.mdx @@ -324,7 +324,11 @@ data format. Edit it to bind a dataset."* prompt instead of a chart. Inline widget data is not an authoring surface either: `options.data` (an array of rows or a `{ "provider": "object", … }` query) and `options.xField` / `options.yField` are refused by name by `@object-ui/types`' strict authoring -face (objectui#11228), and every widget binds a dataset. +face (objectui#11228), and every widget binds a dataset. A stored single-value +widget (`metric`, `gauge`, `solid-gauge`, `kpi`, `bullet`, or one with no +`type`) whose `options.data` is a `{ "provider": "object", … }` query shows the +same retired-format prompt instead of its number (objectui#11525), as a `pivot` +widget with that query already did. Binding rules, in precedence order: diff --git a/packages/plugin-dashboard/README.md b/packages/plugin-dashboard/README.md index ec427ecb90..d326805667 100644 --- a/packages/plugin-dashboard/README.md +++ b/packages/plugin-dashboard/README.md @@ -443,7 +443,10 @@ into each bound widget's inline query (`AND`-combined with the widget's own // Edit it to bind a dataset." instead of a chart. Inline widget data // (`options.data`, `options.xField` / `options.yField`) is not an // authoring surface either: the strict authoring face refuses those keys - // by name (objectui#11228). + // by name (objectui#11228). A stored single-value widget (`metric`, + // `gauge`, `solid-gauge`, `kpi`, `bullet`, or one with no `type`) whose + // `options.data` is a `{ "provider": "object", … }` query draws that same + // retired-format prompt instead of its number (objectui#11525). // // Default binding: the filter's own `field` (dateRange → created_at). { "id": "w1", "type": "bar", "dataset": "invoices", "dimensions": ["status"], "values": ["count"] }, @@ -511,7 +514,7 @@ Where the accent lands depends on the layout, not on the token: | Layout | Accent | | --- | --- | -| Card chrome (`MetricWidget`, inline `object-metric`) | the icon chip's background + foreground | +| Card chrome (`MetricWidget`, an `object-metric` block) | the icon chip's background + foreground | | Chrome-less (`MetricWidget variant: 'bare'`, and every dataset-bound `metric`) | the big number's text colour | Both read one shared table (`src/colorVariants.ts`), so the same declaration diff --git a/packages/plugin-dashboard/src/DashboardGridLayout.tsx b/packages/plugin-dashboard/src/DashboardGridLayout.tsx index e8e219eec9..1f972cb8b9 100644 --- a/packages/plugin-dashboard/src/DashboardGridLayout.tsx +++ b/packages/plugin-dashboard/src/DashboardGridLayout.tsx @@ -372,22 +372,15 @@ export const DashboardGridLayout: React.FC = ({ // Single-value families render as a metric card, not a chart (#2943). if (dispatch.family === 'metric') { const widgetData = (widget as any).data || options.data; + // provider: 'object' — RETIRED (objectui#11525, maintainer ruling C), with + // the same placeholder object this surface's pivot arm and + // `DashboardRenderer`'s metric arm return, imported rather than restated. + // A metric binds a `dataset` (ADR-0021); this branch used to build a flat + // `object-metric` node in a filter dialect no node type declares. A + // typeless widget resolves to `metric` (objectui#11514) and answers here + // too. The chart and table arms keep their `provider: 'object'` branches. + if (isObjectProvider(widgetData)) return LEGACY_RETIRED_WIDGET_SCHEMA; const label = widget.title || widgetType; - if (isObjectProvider(widgetData)) { - const providerAgg = widgetData.aggregate; - return { - type: 'object-metric', - ...options, - objectName: widgetData.object, - label, - aggregate: providerAgg ? { - field: providerAgg.field, - function: providerAgg.function, - groupBy: providerAgg.groupBy, - } : undefined, - filter: widgetData.filter || widget.filter, - }; - } const rows = Array.isArray(widgetData) ? widgetData : widgetData?.items || []; const valueField = options.yField || 'value'; return { @@ -585,7 +578,6 @@ export const DashboardGridLayout: React.FC = ({ const widgetId = widget.id || `widget-${index}`; // `getComponentSchema` builds a node for `SchemaRenderer` in every // branch, but not every branch's node is a declared type yet: the - // flat `object-metric` node has none (objectui#11525), the // `plugin-dashboard:metric` key is typed `string` // (`DASHBOARD_NODE_TYPES`, objectui#11466), and the static `pivot` // does not state `PivotTableSchema`'s required axes. So the @@ -623,10 +615,12 @@ export const DashboardGridLayout: React.FC = ({ // from a caller's fork (`legacyRetiredWidget.ts:97-102`). const datasetBound = !!(widget as { dataset?: unknown }).dataset; // A `metric` widget renders its own card chrome ONLY in the inline - // path. A dataset-bound metric uses DatasetWidget, which renders just + // path (the `plugin-dashboard:metric` card, or a retired + // `provider: 'object'` metric's placeholder, objectui#11525). A + // dataset-bound metric uses DatasetWidget, which renders just // the value — so it must take the shared Card wrapper to get a title // and border like its neighbours, instead of showing as bare text - // (`DashboardRenderer.tsx:777-782`, same rule, same reason). + // (`DashboardRenderer`'s `isSelfContained`, same rule, same reason). const isSelfContained = resolveWidgetType(widget) === 'metric' && !datasetBound; // `DashboardWidget.title` is the spec's `I18nLabel`: since // 17.0.0-rc.6 an author may inline a per-locale map diff --git a/packages/plugin-dashboard/src/DashboardRenderer.tsx b/packages/plugin-dashboard/src/DashboardRenderer.tsx index bc22f1333f..00fdb1e22a 100644 --- a/packages/plugin-dashboard/src/DashboardRenderer.tsx +++ b/packages/plugin-dashboard/src/DashboardRenderer.tsx @@ -165,13 +165,22 @@ const FILTERABLE_COMPONENT_TYPES: ReadonlySet = new Set [] as Array>); + +vi.mock('@object-ui/react', async () => { + const actual: any = await vi.importActual('@object-ui/react'); + const RealSchemaRenderer = actual.SchemaRenderer; + return { + ...actual, + // A listener, not a stub: the node is recorded and then rendered by the + // REAL renderer. Everything else is the real export by identity. + SchemaRenderer: (props: any) => { + if (props?.schema && typeof props.schema === 'object') received.push(props.schema); + return ; + }, + }; +}); + +import { SchemaRendererProvider } from '@object-ui/react'; +import { ComponentRegistry } from '@object-ui/core'; +import type { DashboardComponentSchema } from '@object-ui/types'; +// Side-effect imports at MODULE scope (AGENTS.md's flaky-test rule): +// `@object-ui/components` registers `text` / `data-table`, the package barrel +// registers `object-metric`, `object-data-table` and the metric cards. +import '@object-ui/components'; +import { DashboardRenderer, DashboardGridLayout } from '../index'; +import { LEGACY_RETIRED_WIDGET_SCHEMA } from '../legacyRetiredWidget'; +import { METRIC_LIKE_TYPES, DASHBOARD_NODE_TYPES } from '../widgetDispatch'; + +// The chart arm is a control here, read off the recorded node. A stub keeps the +// real `object-chart` (and its metadata probe) out of this file. +ComponentRegistry.register('object-chart', (() => null) as any, { + namespace: 'test', + label: 'object-chart stub', + category: 'plugin', +} as any); + +afterEach(() => { + cleanup(); + received.length = 0; +}); + +const PLACEHOLDER = 'This widget uses a retired data format. Edit it to bind a dataset.'; + +const PROVIDER = { provider: 'object', object: 'deal', aggregate: { field: 'amount', function: 'sum' } } as const; + +function makeAdapter() { + return { + find: vi.fn(async (_objectName: string, _params?: unknown) => ({ data: [{ id: 'd1', name: 'Acme', amount: 4242 }] })), + aggregate: vi.fn(async (_objectName: string, _params?: unknown) => [{ amount: 4242 }]), + queryDataset: vi.fn(async (_dataset: string, _selection?: unknown) => ({ rows: [{ revenue: 510000 }] })), + getObjectSchema: vi.fn(async (_objectName: string) => ({ + name: 'deal', + fields: { name: { type: 'text', label: 'Name' }, amount: { type: 'number', label: 'Amount' } }, + })), + }; +} + +type Adapter = ReturnType; + +const dash = (widgets: Record[], extra: Record = {}): DashboardComponentSchema => + ({ type: 'dashboard', widgets, ...extra }) as unknown as DashboardComponentSchema; + +const SURFACES = ['renderer', 'grid'] as const; +type Surface = (typeof SURFACES)[number]; + +function mount(surface: Surface, widgets: Record[], adapter: Adapter, extra: Record = {}) { + return render( + + {surface === 'renderer' ? ( + + ) : ( + + )} + , + ); +} + +const nodesOfType = (type: string) => received.filter((n) => n.type === type); + +/** The single-value family, read off the dispatch rather than restated. */ +const METRIC_FAMILY = [...METRIC_LIKE_TYPES]; + +describe.each(SURFACES)('%s surface — the retired path (objectui#11525)', (surface) => { + it('reads a non-empty single-value family (non-vacuity)', () => { + expect(METRIC_FAMILY).toContain('metric'); + expect(METRIC_FAMILY.length).toBeGreaterThan(1); + }); + + it.each([ + ...METRIC_FAMILY.map((type) => [`\`${type}\` with options.data`, { id: 'w1', type, title: 'Pipeline', options: { data: PROVIDER } }] as const), + ['`metric` with widget-level data', { id: 'w1', type: 'metric', title: 'Pipeline', data: PROVIDER }], + // objectui#11514: a typeless widget resolves to `metric`, so it answers too. + ['a typeless widget with options.data', { id: 'w1', title: 'Pipeline', options: { data: PROVIDER } }], + ])('%s draws the placeholder object and sends no query', async (_label, widget) => { + const adapter = makeAdapter(); + mount(surface, [widget], adapter); + + await waitFor(() => expect(received).toContain(LEGACY_RETIRED_WIDGET_SCHEMA)); + expect(screen.getByText(PLACEHOLDER)).toBeInTheDocument(); + // The node the branch used to build is gone, and so is its query. + expect(nodesOfType('object-metric')).toHaveLength(0); + expect(adapter.aggregate).not.toHaveBeenCalled(); + expect(adapter.find).not.toHaveBeenCalled(); + expect(adapter.queryDataset).not.toHaveBeenCalled(); + }); + + it('draws the same tile as the retired top-level `object` metric shape', () => { + // The chrome is the family's, not the placeholder's: a dataset-less + // `metric` is self-contained on both surfaces, so the retired provider tile + // and the retired legacy-shape tile are one tile. + const adapter = makeAdapter(); + const inline = mount(surface, [{ id: 'w1', type: 'metric', title: 'Pipeline', options: { data: PROVIDER } }], adapter).container.innerHTML; + cleanup(); + const legacy = mount(surface, [{ id: 'w1', type: 'metric', title: 'Pipeline', object: 'deal', aggregate: 'count' }], adapter).container.innerHTML; + expect(inline).toContain(PLACEHOLDER); + expect(inline).toBe(legacy); + }); +}); + +describe.each(SURFACES)('%s surface — controls (objectui#11525)', (surface) => { + it('the pivot arm answers a provider pivot with the same placeholder object', async () => { + const adapter = makeAdapter(); + mount(surface, [{ id: 'w1', type: 'pivot', title: 'Deals', options: { rowField: 'name', valueField: 'amount', data: PROVIDER } }], adapter); + await waitFor(() => expect(received).toContain(LEGACY_RETIRED_WIDGET_SCHEMA)); + expect(screen.getByText(PLACEHOLDER)).toBeInTheDocument(); + }); + + it('a dataset-bound metric still draws its number', async () => { + const adapter = makeAdapter(); + mount(surface, [{ id: 'w1', type: 'metric', title: 'Revenue', dataset: 'sales', values: ['revenue'] }], adapter); + expect(await screen.findByText('510000')).toBeInTheDocument(); + expect(adapter.queryDataset).toHaveBeenCalledWith('sales', expect.anything()); + expect(screen.queryByText(PLACEHOLDER)).not.toBeInTheDocument(); + }); + + it('a static metric still draws its inline value', async () => { + const adapter = makeAdapter(); + mount(surface, [{ id: 'w1', type: 'metric', title: 'Static', options: { value: '77' } }], adapter); + await waitFor(() => expect(nodesOfType(DASHBOARD_NODE_TYPES.metric).length).toBeGreaterThan(0)); + expect(screen.getByText('77')).toBeInTheDocument(); + expect(screen.queryByText(PLACEHOLDER)).not.toBeInTheDocument(); + }); + + it('the inline chart arm is untouched: a provider bar still builds an object-chart node', async () => { + const adapter = makeAdapter(); + mount(surface, [{ id: 'w1', type: 'bar', title: 'Deals', options: { data: PROVIDER } }], adapter); + await waitFor(() => expect(nodesOfType('object-chart').length).toBeGreaterThan(0)); + expect(nodesOfType('object-chart')[0].objectName).toBe('deal'); + expect(received).not.toContain(LEGACY_RETIRED_WIDGET_SCHEMA); + }); + + it('the inline table arm is untouched: a provider table still fetches its rows', async () => { + const adapter = makeAdapter(); + mount(surface, [{ id: 'w1', type: 'table', title: 'Deals', options: { data: PROVIDER } }], adapter); + await waitFor(() => expect(nodesOfType('object-data-table').length).toBeGreaterThan(0)); + await waitFor(() => expect(adapter.find).toHaveBeenCalled()); + expect(received).not.toContain(LEGACY_RETIRED_WIDGET_SCHEMA); + }); +}); + +describe('renderer surface — the filter broadcast still scopes an authored object-metric (objectui#11525)', () => { + it('a `component` envelope holding an object-metric node receives the filter bar value', async () => { + // Why `object-metric` stays a member of the broadcast's filterable set: the + // producer is gone, but an author's envelope node still reaches the merge. + const adapter = makeAdapter(); + mount( + 'renderer', + [{ id: 'w1', title: 'Envelope', component: { type: 'object-metric', objectName: 'deal', aggregate: { field: 'amount', function: 'sum' } } }], + adapter, + { globalFilters: [{ name: 'region', field: 'region', type: 'select', options: [{ value: 'EMEA', label: 'EMEA' }], defaultValue: 'EMEA' }] }, + ); + await waitFor(() => expect(nodesOfType('object-metric').length).toBeGreaterThan(0)); + const node = nodesOfType('object-metric')[nodesOfType('object-metric').length - 1]; + expect(node.filter).toEqual({ region: 'EMEA' }); + await waitFor(() => expect(adapter.aggregate).toHaveBeenCalled()); + expect(received).not.toContain(LEGACY_RETIRED_WIDGET_SCHEMA); + }); +}); diff --git a/packages/plugin-dashboard/src/legacyRetiredWidget.ts b/packages/plugin-dashboard/src/legacyRetiredWidget.ts index 0d66a8ea0c..d70ff3dc89 100644 --- a/packages/plugin-dashboard/src/legacyRetiredWidget.ts +++ b/packages/plugin-dashboard/src/legacyRetiredWidget.ts @@ -40,6 +40,22 @@ * feature, so the detector requires the absence of any widget-level data before * it looks at `object` at all — the same order `DashboardRenderer` has always * used. + * + * ⚠️ **Dated note, 2026-10-03 — the metric family's nested config is retired — + * objectui#11525.** When this paragraph was written the nested + * `provider: 'object'` config was live for every family; it no longer is for the + * single-value family (`metric`, `gauge`, `solid-gauge`, `kpi`, `bullet`, and a + * typeless widget, which resolves to `metric` since objectui#11514). By the + * maintainer's ruling C on objectui#11525, both surfaces' metric arms answer a + * dataset-less widget whose `options.data` (or widget-level `data`) is + * `{ provider: 'object', … }` with {@link LEGACY_RETIRED_WIDGET_SCHEMA}, as + * their pivot arms already did (objectui#10528). The chart and table families + * stay live. The retirement sits in those family arms, not in this detector: it + * says what a family may bind, not that a widget carries the retired top-level + * shape, so {@link isLegacyRetiredWidget} is unchanged and still steps aside + * for any widget-level data. The rest of this paragraph, and step 2's "live + * `provider: 'object'` nested config" below, are kept as the reading of + * objectui#4612. */ import type { DashboardWidgetSlotEntry } from './widgetDispatch'; From a1c7e645a94984951a4cb1d5b44ef3dc1b6a6bf4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 03:59:27 +0000 Subject: [PATCH 2/3] chore(plugin-dashboard): changeset for the retired inline provider-object metric, a dated note on 11514's entry, and a typed case table in the pin (objectui#11525) Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC Co-authored-by: Claude --- .../11514-dashboard-slot-entry-types.md | 2 + .changeset/11525-inline-metric-retired.md | 40 +++++++++++++++++++ .../inlineObjectMetricRetired-11525.test.tsx | 18 ++++++--- 3 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 .changeset/11525-inline-metric-retired.md diff --git a/.changeset/11514-dashboard-slot-entry-types.md b/.changeset/11514-dashboard-slot-entry-types.md index 675e51b40c..81562e32b6 100644 --- a/.changeset/11514-dashboard-slot-entry-types.md +++ b/.changeset/11514-dashboard-slot-entry-types.md @@ -11,3 +11,5 @@ - **The `object-chart` producers.** A series dispatch carries the family as a literal union whose members are all families `ObjectChartSchema.chartType` declares, so the node both surfaces build for a `provider: 'object'` series widget satisfies `ObjectChartSchema` with no cast. - **README.** "Reading a widget key off `widgets[]`" narrows an entry on `type` before reading a widget key, and a new section states what a widget with no `type`, or an unknown one, draws. - **What does not move.** Every widget that names a known family draws as before: the dispatch routes the same families, and each `object-chart` node carries the same `chartType` it carried. A legacy `component` envelope with no `type` is not the spec's widget and is not given its default; it draws under its card heading as before, and a number or `true` in its `component` draws the same text, now forwarded through `toRenderableSchema`. + +⚠️ **Dated note, 2026-10-03 — a dataset-less `provider: 'object'` metric is retired — objectui#11525.** At this change, "Every widget that names a known family draws as before" held for the single-value family's inline `provider: 'object'` widget, which drew its number through a flat `object-metric` node. Now a `metric`, `gauge`, `solid-gauge`, `kpi` or `bullet` widget, or a typeless one, with no `dataset` and an `options.data` (or widget-level `data`) of `{ provider: 'object', … }` draws the retired-format placeholder on both surfaces, by the maintainer's ruling C on objectui#11525. A typeless widget still draws exactly as the same widget with `type: 'metric'`, so it draws that placeholder too. `.changeset/11525-inline-metric-retired.md` states what ships. The rest of this entry is kept as the reading of this change. diff --git a/.changeset/11525-inline-metric-retired.md b/.changeset/11525-inline-metric-retired.md new file mode 100644 index 0000000000..b8a7e79cbd --- /dev/null +++ b/.changeset/11525-inline-metric-retired.md @@ -0,0 +1,40 @@ +--- +'@object-ui/plugin-dashboard': minor +--- + +**A dashboard metric widget bound inline to an object now shows the +retired-format prompt instead of its number (objectui#11525).** This follows +the maintainer's ruling C on objectui#11525. + +**What changes on screen.** The change covers a stored single-value widget: +`metric`, `gauge`, `solid-gauge`, `kpi`, `bullet`, or a widget with no `type`, +which draws as `metric`. If it has no `dataset` and its `options.data` (or +widget-level `data`) is `{ provider: 'object', object, aggregate }`, it is +affected: + +- **Before:** both `DashboardRenderer` and `DashboardGridLayout` aggregated the + object and drew the number in the tile. +- **Now:** the tile shows "This widget uses a retired data format. Edit it to + bind a dataset." and sends no query. This is the same placeholder a `pivot` + widget with that config already showed (objectui#10528). + +Breaking for stored dashboards that still carry this form; `minor` because +objectui never declares `major`. Under ADR-0021 a widget binds a +semantic-layer `dataset`, and both validator faces already refuse a widget +without one, so nothing authored against the current contract is affected. No +example, objectui authoring surface, cloud dashboard or AI Studio path was +measured emitting the form (the measurements are on objectui#11525). + +**The fix.** Rebind the widget to a dataset: set `dataset` and select its +`values` by name. A dataset-bound metric draws its number as before. + +**What does not move:** + +- The inline `provider: 'object'` chart and table widgets still draw. +- A static metric (`options.value` or an inline row array) still draws its value. +- The zod and TypeScript authoring faces are unchanged. +- An `object-metric` node an author places in a widget's legacy `component` + envelope still receives the dashboard filter bar's values. + +The flat `object-metric` node the two surfaces used to build carried an +ObjectQL-dialect `filter` that no node type declares; it is gone. diff --git a/packages/plugin-dashboard/src/__tests__/inlineObjectMetricRetired-11525.test.tsx b/packages/plugin-dashboard/src/__tests__/inlineObjectMetricRetired-11525.test.tsx index 1dba2a77e3..dcde33e819 100644 --- a/packages/plugin-dashboard/src/__tests__/inlineObjectMetricRetired-11525.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/inlineObjectMetricRetired-11525.test.tsx @@ -127,18 +127,24 @@ const nodesOfType = (type: string) => received.filter((n) => n.type === type); /** The single-value family, read off the dispatch rather than restated. */ const METRIC_FAMILY = [...METRIC_LIKE_TYPES]; +/** Each retired input, labelled. */ +const RETIRED_CASES: Array<[string, Record]> = [ + ...METRIC_FAMILY.map((type): [string, Record] => [ + `\`${type}\` with options.data`, + { id: 'w1', type, title: 'Pipeline', options: { data: PROVIDER } }, + ]), + ['`metric` with widget-level data', { id: 'w1', type: 'metric', title: 'Pipeline', data: PROVIDER }], + // objectui#11514: a typeless widget resolves to `metric`, so it answers too. + ['a typeless widget with options.data', { id: 'w1', title: 'Pipeline', options: { data: PROVIDER } }], +]; + describe.each(SURFACES)('%s surface — the retired path (objectui#11525)', (surface) => { it('reads a non-empty single-value family (non-vacuity)', () => { expect(METRIC_FAMILY).toContain('metric'); expect(METRIC_FAMILY.length).toBeGreaterThan(1); }); - it.each([ - ...METRIC_FAMILY.map((type) => [`\`${type}\` with options.data`, { id: 'w1', type, title: 'Pipeline', options: { data: PROVIDER } }] as const), - ['`metric` with widget-level data', { id: 'w1', type: 'metric', title: 'Pipeline', data: PROVIDER }], - // objectui#11514: a typeless widget resolves to `metric`, so it answers too. - ['a typeless widget with options.data', { id: 'w1', title: 'Pipeline', options: { data: PROVIDER } }], - ])('%s draws the placeholder object and sends no query', async (_label, widget) => { + it.each(RETIRED_CASES)('%s draws the placeholder object and sends no query', async (_label, widget) => { const adapter = makeAdapter(); mount(surface, [widget], adapter); From 29b4ab282b7e7ff20cccdeb8c176fe25bcd24e66 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 04:09:09 +0000 Subject: [PATCH 3/3] test(plugin-dashboard): the object-chart stub registers without casts (objectui#11525) Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC Co-authored-by: Claude --- .../src/__tests__/inlineObjectMetricRetired-11525.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-dashboard/src/__tests__/inlineObjectMetricRetired-11525.test.tsx b/packages/plugin-dashboard/src/__tests__/inlineObjectMetricRetired-11525.test.tsx index dcde33e819..50a40e47b4 100644 --- a/packages/plugin-dashboard/src/__tests__/inlineObjectMetricRetired-11525.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/inlineObjectMetricRetired-11525.test.tsx @@ -75,11 +75,11 @@ import { METRIC_LIKE_TYPES, DASHBOARD_NODE_TYPES } from '../widgetDispatch'; // The chart arm is a control here, read off the recorded node. A stub keeps the // real `object-chart` (and its metadata probe) out of this file. -ComponentRegistry.register('object-chart', (() => null) as any, { +ComponentRegistry.register('object-chart', () => null, { namespace: 'test', label: 'object-chart stub', category: 'plugin', -} as any); +}); afterEach(() => { cleanup();