Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/11514-dashboard-slot-entry-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
40 changes: 40 additions & 0 deletions .changeset/11525-inline-metric-retired.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions content/docs/guide/dashboard-filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion content/docs/plugins/plugin-dashboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
7 changes: 5 additions & 2 deletions packages/plugin-dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"] },
Expand Down Expand Up @@ -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
Expand Down
30 changes: 12 additions & 18 deletions packages/plugin-dashboard/src/DashboardGridLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -372,22 +372,15 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
// 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 {
Expand Down Expand Up @@ -585,7 +578,6 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
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
Expand Down Expand Up @@ -623,10 +615,12 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
// 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
Expand Down
58 changes: 35 additions & 23 deletions packages/plugin-dashboard/src/DashboardRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,22 @@ const FILTERABLE_COMPONENT_TYPES: ReadonlySet<string> = new Set<FilterableCompon
* `object-chart` and `object-data-table` are the `@object-ui/types` node
* schemas, by reference. `object-metric` has no TypeScript declaration there
* (the spec's `ComponentPropsMap` row is its only published one, and it types
* the `properties` bag); the node this renderer builds carries `filter` flat,
* and `ObjectMetricBlock` hands that key to the widget's `filter` prop, so it is
* the `properties` bag); the node carries `filter` flat, and
* `ObjectMetricBlock` hands that key to the widget's `filter` prop, so it is
* typed by that prop.
*
* Since objectui#11525 this renderer builds no `object-metric` node: the
* dataset-less `provider: 'object'` metric draws the retired-format
* placeholder. The member stays because an `object-metric` node still reaches
* the merge from the author: a widget's legacy `component` envelope holding
* one is handed through as written, and the filter bar scopes it today.
* Dropping the member would stop that silently, which the ruling did not ask
* for.
*
* The value is not host state: the renderer wrote it from the widget's own
* spec-declared `filter` (or the provider's), and the dashboard's filter-bar
* values arrive separately, as the `scopedFilter` merged into it.
* spec-declared `filter` (or the provider's), or the author wrote it on the
* envelope's node, and the dashboard's filter-bar values arrive separately, as
* the `scopedFilter` merged into it.
*/
type FilterableComponentSchema =
| ObjectChartSchema
Expand Down Expand Up @@ -841,6 +850,23 @@ const DashboardRendererInner = forwardRef<HTMLDivElement, DashboardRendererProps
// and never routed the widget, so four spec chart types fell through
// to a red error box (#2943).
if (dispatch.family === 'metric') {
// provider: 'object' — RETIRED for the single-value family
// (objectui#11525, maintainer ruling C), with the same
// placeholder object the pivot arm below returns
// (objectui#10528), imported rather than restated. A metric
// binds a semantic-layer `dataset` (ADR-0021), which both
// validator faces require on every widget; a stored
// dataset-less widget whose `options.data` (or widget-level
// `data`) is `{ provider: 'object', … }` is stale metadata. This
// branch used to build a flat `object-metric` node carrying an
// ObjectQL-dialect `filter` that no node type declares, so the
// tile drew a number through a second filter dialect beside the
// dataset path. It now draws the visible rebind prompt instead.
// A typeless widget resolves to `metric` (objectui#11514), so it
// answers here too. The chart and table arms keep their
// `provider: 'object'` branches. `DashboardGridLayout`'s metric
// arm answers the same way.
if (isObjectProvider(widgetData)) return LEGACY_RETIRED_WIDGET_SCHEMA;
// objectui#4032 — the KPI card's heading comes from the SAME
// convention channel every other widget's header uses
// (`{ns}.dashboards.{dash}.widgets.{id}.title`), not from the
Expand All @@ -861,29 +887,12 @@ const DashboardRendererInner = forwardRef<HTMLDivElement, DashboardRendererProps
// its own key, `…widgets.{id}.subCaption`, because it is a
// different authored field (`options.description`) from the
// shared header's `widget.description`. Assigned AFTER the
// `...options` spread in both branches below: the spread is
// `...options` spread in the branch below: the spread is
// what carries the raw authored `options.description` through,
// and this is the resolved value that replaces it. When nothing
// translates it, `tWidgetSubCaption` hands back exactly what the
// spread would have — so an untranslated dashboard is byte-identical.
const subCaption = tWidgetSubCaption(widget);
// provider: 'object' — ObjectMetricWidget aggregates server-side.
if (isObjectProvider(widgetData)) {
const providerAgg = widgetData.aggregate;
return {
type: 'object-metric',
...options,
objectName: widgetData.object,
label,
description: subCaption,
aggregate: providerAgg ? {
field: providerAgg.field,
function: providerAgg.function,
groupBy: providerAgg.groupBy,
} : undefined,
filter: widgetData.filter || widget.filter,
};
}
// Static value: an inline `options.value`, else the first row's
// measure from an inline data array.
const rows = Array.isArray(widgetData) ? widgetData : widgetData?.items || [];
Expand Down Expand Up @@ -1028,7 +1037,10 @@ const DashboardRendererInner = forwardRef<HTMLDivElement, DashboardRendererProps
? { ...widget, filter: mergeFilters(widget.filter, scopedFilter) }
: widget;
// A `metric` widget renders its own card chrome ONLY in the inline
// (object-metric) path. A dataset-bound metric uses DatasetWidget, which
// path: the `plugin-dashboard:metric` card, or, for a retired
// `provider: 'object'` metric (objectui#11525), the placeholder, which
// draws its own border, as the legacy metric shape's placeholder always
// has here. A dataset-bound metric uses DatasetWidget, which
// renders just the value — so it must take the shared Card wrapper to get
// a title + border like the kpi/gauge widgets (otherwise it shows as bare
// text with no title, inconsistent with its neighbours).
Expand Down
9 changes: 5 additions & 4 deletions packages/plugin-dashboard/src/ObjectMetricWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,11 @@ export interface ObjectMetricWidgetProps {
* `groupBy` is the contract's own union — BY REFERENCE through
* `ObjectChartSchema['aggregate']`, which holds `ChartAggregate` from
* `@objectstack/spec/ui` by reference in turn, never a local near-copy of it
* (`check:spec-symbols`). It is the same authored key both dashboard relays
* compose for the `object-metric` and the `object-chart` node out of one
* provider block, so a second spelling here could only be a way for the two
* to disagree.
* (`check:spec-symbols`). It is the same authored key the `object-chart` node
* carries, so a second spelling here could only be a way for the two to
* disagree. (Both dashboard relays used to compose this node and the chart's
* out of one `provider: 'object'` block; since objectui#11525 a dataset-less
* metric draws the retired-format placeholder, and this node is authored.)
*
* It used to say `string`, which was a claim about the AUTHOR that nothing
* upstream backed: the value crosses two `any` seams on its way in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@
* still-live authoring surface (the #4600 measurement's do-not-conflate note);
* dataset widgets and static-data widgets are live too. None of them may acquire
* the placeholder — on either surface.
*
* ⚠️ Dated note, 2026-10-03 — objectui#11525. When this header was written the
* nested `provider: 'object'` config was live for every family. The
* single-value family's is now retired: a dataset-less provider metric draws
* this placeholder through each surface's metric arm, not through the shared
* detector (`inlineObjectMetricRetired-11525.test.tsx`). The provider control
* below is a `bar`, whose family stays live, so it holds as written. The rest
* of this header is kept as the reading of objectui#4612.
*/

import { describe, it, expect, afterEach } from 'vitest';
Expand Down
Loading
Loading