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/10859-dashboard-node-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ Migration:
- a selector `[data-obj-type="metric"]` / `[data-obj-type="metric-card"]` → `[data-obj-type="plugin-dashboard:metric"]` / `[data-obj-type="plugin-dashboard:metric-card"]`.

**Clause-②: yes** — registrations leave the runtime (narrowing), released as `minor` with this banner.

⚠️ **Dated note, 2026-10-02 — `metric-card` leaves the widget vocabulary — objectui#11483.** At this change, "The dashboard WIDGET vocabulary is unchanged" held, and that vocabulary named `metric-card`. Now the widget `type` vocabulary (`DashboardWidgetTypeSchema` / `DashboardWidgetTypeName`) names no component type. The `metric-card` slot entry is still authored as before when it carries its `value`; without one it is refused. `.changeset/11483-metric-card-needs-value.md` states what ships. The rest of this entry is kept as the reading of this change.
2 changes: 2 additions & 0 deletions .changeset/11070-dashboard-keys-round11.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ The strict authoring face accepts the `layout` that the editable dashboard grid'
`options.description` is not changed by this release. `@objectstack/spec` does not declare it on the widget's `options`, so the strict face keeps refusing an authored one (objectui#11228 ruling C), and its status is an open decision on objectui#11070. Comments in `@object-ui/plugin-dashboard` that called it declared now say so; that package's runtime behaviour is unchanged.

⚠️ **Dated note, 2026-10-02 — the slot arm declares `metric-card`'s registered inputs — objectui#11467.** "The widget arm, the component node's other keys and the registered-input record objectui#11022 added", under "What does not move" above, held when this change landed. Later in this same release, objectui#11467 moved all three. The widget arm's `component` member now takes the slot's component arm first, then `BaseSchema`. The component node declares `title`, `value`, `icon`, `trend` and `trendValue` as members on both faces, so each value is judged by its member. The registered-input record and the strict walker's step that admitted it are retired. `.changeset/11467-metric-card-arm-inputs.md` states what ships; the text above is kept as the reading of this change.

⚠️ **Dated note, 2026-10-02 — the widget arm's `type` vocabulary moves — objectui#11483.** At this change, "What does not move" above held for the widget arm, whose `type` vocabulary named `metric-card`; now that vocabulary names no component type, and the slot reads a `metric-card` through its component arm alone. `.changeset/11483-metric-card-needs-value.md` states what ships. The rest of this entry is kept as the reading of this change.
2 changes: 2 additions & 0 deletions .changeset/11467-metric-card-arm-inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ A `metric-card` in a dashboard's widget slot is checked against the props `Metri
**Fix:** write `trend` as one of the three, give the card a `value`, and keep `title`, `icon` and `trendValue` as strings (`title` may also be a per-locale map).

The internal `declareRegisteredInputs` side table (objectui#11022) and the strict walker's branch for it are removed. Every name it recorded is now a member of the arm. Neither was exported from the package.

⚠️ **Dated note, 2026-10-02 — a `metric-card` with no `value` is refused, whatever else it carries — objectui#11483.** At this change, a card with no `value` was refused directly in `widgets[]` only when it carried `icon`, `trend` or `trendValue`, and the widget arm still named `metric-card` as a widget type. A card carrying only keys the widget arm also accepts (`title`, `description`, `id`, `layout`, a `dataset`, `options`) parsed there as a widget. Now `metric-card` is no widget type, so both faces refuse every card with no `value` in `widgets[]`. `.changeset/11483-metric-card-needs-value.md` states what ships. The rest of this entry is kept as the reading of this change.
14 changes: 14 additions & 0 deletions .changeset/11483-metric-card-needs-value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@object-ui/types': minor
---

A `metric-card` in a dashboard's widget slot parses only with its `value` (objectui#11483). This narrows a published accept set on both validator faces, and it narrows the widget `type` vocabulary the TypeScript face exports. `minor`, per this repository's version alignment; the narrowing is the breaking part.

`DashboardWidgetTypeSchema` is the widget arm's `type` vocabulary. It named the component type `metric-card`, taken from `DASHBOARD_COMPONENT_WIDGET_TYPES`. So `{ type: 'metric-card', title: 'Revenue' }` in `widgets[]` parsed as a widget, because it carries only keys both arms accept. It parsed on `AnyComponentSchema` / `DashboardComponentSchema` and on `StrictAnyComponentSchema`. The card's required `value` (objectui#11467) governed only a card that also carried `icon`, `trend` or `trendValue`. The dashboard then drew the card with an empty figure.

- **What changed.** `DashboardWidgetTypeSchema` is now the spec's families plus `list` and `custom`, so the slot reads a `metric-card` through its component arm alone. The TypeScript twin, `DashboardWidgetTypeName`, drops `DashboardComponentWidgetType` the same way. `DASHBOARD_COMPONENT_WIDGET_TYPES` still lists `metric-card`: it is the component arm's `type`, and only that.
- **What now refuses that did not.** Directly in `widgets[]`, both faces refuse a `metric-card` with no `value`. That holds whatever else the card carries: `title`, `description`, `id` and `layout`, a `dataset` with `values`, or its figure under `options` (`options: { value }`), which the tolerant face accepted. On its own, `DashboardWidgetSchema` (zod) refuses `type: 'metric-card'` at `type`. On the TypeScript face, `'metric-card'` is no longer a `DashboardWidgetTypeName`, so assigning it to that type is a compile error.
- **Why no widget key stands in for `value`.** This was measured through the real `DashboardRenderer` and `DashboardGridLayout`. A `dataset` makes the dashboard draw `DatasetWidget` in the card's place, by a rule that does not read the `type`; the card is never mounted. `options` is spread onto the node as literal props: a second spelling of `value`, not a binding. No other widget key reached the figure.
- **What does not move.** On the TypeScript face, `DashboardWidgetSchema['type']` still includes `'metric-card'`. That interface is also the read type of every `widgets[]` entry, and the component arm stays assignable to it. So a `metric-card` literal with no `value` still compiles directly in `widgets[]`, while the validator refuses it on both faces. A `metric-card` in a widget's legacy `component` envelope is judged as before: there, the tolerant face's `BaseSchema` fallback still accepts a card that fails the arm. Every `metric-card` in the schema catalog, the READMEs and the docs fences carries its `value`. The one docs example that did not, under "TypeScript Support" in `content/docs/plugins/plugin-dashboard.mdx`, is rewritten.

**Fix:** give the card its `value`. For a figure queried from a dataset, write a `metric` widget (`{ type: 'metric', dataset, values: [measure] }`), which the dashboard draws as the same dataset tile.
2 changes: 2 additions & 0 deletions .changeset/dashboard-widget-type-closed-enum.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ Close the dashboard widget `type` vocabulary, and admit `metric-card` as objectu
It is now the CLOSED `DashboardWidgetTypeName` / `DashboardWidgetTypeSchema`: the spec's own `ChartTypeSchema` families **by reference**, plus two named, closed objectui extension sets — `DASHBOARD_WIDGET_TYPE_EXTENSIONS` (`list`, `custom`: objectui-only widget families) and `DASHBOARD_COMPONENT_WIDGET_TYPES` (`metric-card`: an objectui SDUI **component** type the widget slot holds directly, per the maintainer ruling of 2026-08-14 — objectui's own component enum, explicitly not the spec widget enum).

Three drifts the closure surfaced and this change fixes: the dashboard designer's palette offered `grid`, which is not a widget family in either contract and was refused at publish; the metadata-admin widget inspector and the designer both wrote an unvalidated `string` from their select boxes; and a `@object-ui/types` fixture pinned `bar-chart`, a `plugin-charts` component type, on a dataset-bound widget that could never render as one.

⚠️ **Dated note, 2026-10-02 — `metric-card` is no widget type — objectui#11483.** At this change, `DashboardWidgetTypeName` / `DashboardWidgetTypeSchema` were the spec's families plus two objectui sets, `DASHBOARD_WIDGET_TYPE_EXTENSIONS` and `DASHBOARD_COMPONENT_WIDGET_TYPES`; now they are the spec's families plus `DASHBOARD_WIDGET_TYPE_EXTENSIONS` only. A `metric-card` in the widget slot is read by the slot's component arm alone, which requires its `value`. `DASHBOARD_COMPONENT_WIDGET_TYPES` still lists `metric-card` as that arm's `type`, and `DashboardWidgetSchema['type']` on the TypeScript face still reads it. `.changeset/11483-metric-card-needs-value.md` states what ships. The rest of this entry is kept as the reading of this change.
27 changes: 24 additions & 3 deletions content/docs/plugins/plugin-dashboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ outside a dashboard's `widgets[]` renders nothing but the renderer's
}
```

A card with no `value` is refused by `@object-ui/types/zod`, on the tolerant
face and the strict one. `metric-card` is not a widget type, so a card is never
read through widget keys alone, and no widget key stands in for `value`: a
`dataset` makes the dashboard draw its dataset tile in the card's place, and
`options` is not a binding (objectui#11483). For a figure queried from a
dataset, write a `metric` widget with `dataset` and `values`.

## Usage

### Auto-registration (Side-effect Import)
Expand Down Expand Up @@ -446,19 +453,33 @@ preview or a design surface (objectui#11295).
## TypeScript Support

```ts
import type { DashboardComponentSchema, DashboardWidgetSchema } from '@object-ui/types';
import type {
DashboardComponentSchema,
DashboardWidgetSchema,
DashboardWidgetSlotComponentSchema,
} from '@object-ui/types';

const widget: DashboardWidgetSchema = {
id: 'revenue',
title: 'Revenue',
type: 'metric-card',
type: 'metric',
dataset: 'invoices',
values: ['amount'],
layout: { x: 0, y: 0, w: 1, h: 1 },
};

const card: DashboardWidgetSlotComponentSchema = {
id: 'users',
type: 'metric-card',
title: 'Users',
value: '1,234',
layout: { x: 1, y: 0, w: 1, h: 1 },
};

const dashboard: DashboardComponentSchema = {
type: 'dashboard',
columns: 3,
widgets: [widget],
widgets: [widget, card],
};
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,11 @@ function auditWidget(widget: Widget, where: string): string[] {
// toward `z.string()`, which is precisely the regression objectui#4600
// closed: the catalog would keep its own floor instead of going quietly
// green with the contract.
const known = DashboardWidgetTypeSchema.safeParse(type);
if (!known.success) {
// A slot component type (`metric-card`) is no widget type since
// objectui#11483 — the slot reads it through its component arm alone — so
// the vocabulary is the widget types plus the slot's component set.
const known = COMPONENT_WIDGET_TYPES.has(type) || DashboardWidgetTypeSchema.safeParse(type).success;
if (!known) {
problems.push(`${where}: \`type: '${type}'\` is outside the closed widget vocabulary`);
}
}
Expand Down
8 changes: 8 additions & 0 deletions packages/plugin-dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,14 @@ outside a dashboard's `widgets[]` renders the "Unknown component type" panel.
`value` is the only required key. `title` and `description` take a plain string
or the spec's inline per-locale map (`I18nLabel`).

A card with no `value` is refused by `@object-ui/types/zod`, on the tolerant
face and the strict one. `metric-card` is not a widget type, so a card is never
read through widget keys alone, and no widget key stands in for `value`: a
`dataset` makes the dashboard draw its dataset tile in the card's place, and
`options` is not a binding (objectui#11483). For a figure queried from a
dataset, write a `metric` widget with `dataset` and `values` (see "TypeScript
Support" below).

#### Percent `format` patterns (`'0%'`, `'0.00%'`)

A numeral pattern ending in `%` is handed whole to `formatPercent`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,46 @@ describe('objectui#11070 round 11 — the slot\'s `layout`, as Save Layout write
expect([placed.layout?.w, misplaced.type]).toEqual([3, 'metric-card']);
});
});

/**
* objectui#11483 — the registration's `required` and the schema agree IN THE SLOT.
*
* The arm declared `value` required since objectui#11467, and the walk above holds
* that member to the registration. The slot still disagreed: its widget arm's
* `type` vocabulary named `metric-card`, so a card carrying no card-only input
* parsed as a widget and the arm's requirement governed nothing. This walk reads
* every REQUIRED input off the live registration and drops it from an otherwise
* complete node, so it measures the slot's verdict, not the arm's member.
*/
describe('objectui#11483 — a widget-slot registration\'s required inputs are required in the slot', () => {
describe.each([...DASHBOARD_COMPONENT_WIDGET_TYPES])('`%s`', (type) => {
const required = (): Input[] =>
registeredInputs(type).filter((input) => (input as Input & { required?: boolean }).required);

it('the registration declares a required input — the walk below is not empty', () => {
expect(required().length).toBeGreaterThan(0);
});

it('CONTROL — the node carrying every required input parses in the slot on both faces', () => {
for (const face of [AnyComponentSchema, StrictAnyComponentSchema]) {
expect(face.safeParse(dashboard({ ...nodeOf(type) })).success).toBe(true);
}
});

it('a node missing any required input is refused in the slot on both faces, by the component arm at that input', () => {
for (const input of required()) {
const node: Record<string, unknown> = { ...nodeOf(type), title: 'Revenue' };
delete node[input.name];
for (const face of [AnyComponentSchema, StrictAnyComponentSchema]) {
const result = face.safeParse(dashboard(node));
expect(result.success, `\`${input.name}\` is required by the registration`).toBe(false);
const union = (result.success ? [] : (result.error.issues as unknown as Issue[])).find(
(issue) => issue.code === 'invalid_union' && issue.path.join('.') === 'widgets.0',
);
const componentArm = flatIssues(union?.errors?.[0] ?? []);
expect(componentArm.map((issue) => issue.path.join('.'))).toContain(input.name);
}
}
});
});
});
7 changes: 5 additions & 2 deletions packages/types/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,15 +219,18 @@ objectui#11467; the members are held to the live registration and to
by it, while the tolerant face also keeps `BaseSchema` for any other component
node. The node also declares one widget key, `layout`, the spec's widget
position, which the editable grid's Save Layout writes onto every entry; it is
judged by the spec's shape (objectui#11070):
judged by the spec's shape (objectui#11070). `metric-card` is not a widget
`type`, so the slot reads such a card through this node alone, and a card with
no `value` is refused on both faces (objectui#11483):

```typescript
import { StrictAnyComponentSchema } from '@object-ui/types/zod';

const card = (widget: object) => ({ type: 'dashboard', widgets: [widget] });

StrictAnyComponentSchema.safeParse(card({ type: 'metric-card', value: 42 })).success; // true
StrictAnyComponentSchema.safeParse(card({ type: 'metric-card', bogus: 1 })).success; // false — `bogus` is named
StrictAnyComponentSchema.safeParse(card({ type: 'metric-card', value: 42, bogus: 1 })).success; // false — `bogus` is named
StrictAnyComponentSchema.safeParse(card({ type: 'metric-card', title: 'Revenue' })).success; // false — no `value`
StrictAnyComponentSchema.safeParse(card({ type: 'metric-card', value: 42, layout: { x: 0, y: 0, w: 3, h: 2 } })).success; // true
StrictAnyComponentSchema.safeParse(card({ type: 'metric-card', value: 42, trend: 'sideways' })).success; // false — not a trend
```
Expand Down
Loading
Loading