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/11022-strict-widget-slot-registered-inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ The strict authoring face accepts a correctly authored `metric-card` in a dashbo
- **What does not move.** The tolerant face (`AnyComponentSchema`, `DashboardComponentSchema` and every other mirror): its accept set, output and inferred types are unchanged, and the slot arm's shape and catchall are what they were. The legacy `{ id, component, layout }` widget envelope is untouched, and its `component` is still judged as a plain `BaseSchema` node on both faces. `metric-card` is still not a root-level arm of `AnyComponentSchema`, so a root `{ type: 'metric-card' }` document is refused at `type` on both faces, as before.

No migration: a document that parsed on either face still parses there.

⚠️ **Dated note, 2026-10-01 — the slot arm declares `layout` — objectui#11070.** "the slot arm's shape and catchall are what they were" and "a document that parsed on either face still parses there" above held when this change landed. Later in this same release, round 11 of objectui#11070 added `layout` to the slot arm's shape, by reference to the spec's widget `layout`. So the tolerant face now refuses a malformed `layout` on a `metric-card` in the widget slot. `.changeset/11070-dashboard-keys-round11.md` states what ships; the text above is kept as the reading of this change.
15 changes: 15 additions & 0 deletions .changeset/11070-dashboard-keys-round11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@object-ui/types': minor
---

The strict authoring face accepts the `layout` that the editable dashboard grid's Save Layout writes onto a `metric-card` in a dashboard's widget slot (objectui#11070, round 11). This widens a published accept set, and it narrows the tolerant face on one corner, stated below.

`DashboardGridLayout`'s Save Layout (`mergeLayoutIntoSchema`) writes `layout: { x, y, w, h }` onto every entry of `widgets[]`, a `metric-card` component node included, and the grid places each entry by it. The widget slot's component-node arm declared no `layout`. The tolerant face kept it through `BaseSchema`'s passthrough, and `StrictAnyComponentSchema` refused it as `unrecognized_keys: ['layout']`, so a dashboard the grid had saved failed on the strict face.

- **What changed.** `DashboardWidgetSlotComponentSchema` declares `layout` on both faces, by reference to `@objectstack/spec`'s widget `layout`, the member the widget arm already carries: the four numbers `x`, `y`, `w` and `h`, and no other key. On the TypeScript face the member is typed `SpecDashboardWidget['layout']`, where it used to be the index signature's `any`.
- **What now refuses that did not.** A malformed `layout` on a component node in the widget slot (a coordinate that is not a number, a missing coordinate, or a key besides the four) was kept unjudged by the tolerant face (`AnyComponentSchema`, `DashboardComponentSchema`). It is now refused there, as it already was on the widget arm. On the TypeScript face such a literal is a compile error. `scripts/measure-strict-authoring-face.mjs`, run over the schema catalog, the docs fences and the apps' authored documents at this change's base and on this change, found no document that writes `layout` on a component node, so no corpus verdict moved.
- **What does not move.** The widget arm, the component node's other keys and the registered-input record objectui#11022 added.

**Fix:** write `layout` as the four numbers Save Layout writes, or leave it out.

`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.
2 changes: 2 additions & 0 deletions .changeset/11348-dashboard-widget-reads.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ compiles with the signature present.
The drill-down drawer's `pageSize` is settled in `@object-ui/types` instead, by
a declaration on `ObjectDataTableSchema` with its own changeset; the drawer's
literal is unchanged.

⚠️ **Dated note, 2026-10-01 — the component arm now declares `layout` — objectui#11070.** "the component arm has no spec row and declares none of them" above held when this change landed. Later in this same release, round 11 of objectui#11070 declared `layout` on the component arm, by reference to the spec's widget `layout`, because Save Layout writes it onto every `widgets[]` entry. `title` and `colorVariant` are still declared on the widget arm only. `.changeset/11070-dashboard-keys-round11.md` states what ships; the text above is kept as the reading of this change.
2 changes: 1 addition & 1 deletion content/docs/api/schema-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1055,7 +1055,7 @@ A widget-based dashboard with configurable grid layout and auto-refresh.
|----------|------|-------------|
| `columns` | `number` | Number of grid columns. |
| `gap` | `number` | Gap between widgets (Tailwind spacing scale). |
| `widgets` | `(DashboardWidgetSlotComponentSchema \| DashboardWidgetSchema)[]` | **Required.** Each entry is a widget or a component node. A widget (`DashboardWidgetSchema`) names itself with `id`, `title` and `description`, sizes itself with `layout: { x, y, w, h }`, and holds its content either as a family named in `type` bound to a `dataset` (with that family's settings under `options`), or as a registered component node in `component`; its full key set is the spec's `DashboardWidget` plus objectui's own. A component node (`DashboardWidgetSlotComponentSchema`) sits in the slot directly: its `type` is a member of the closed `DASHBOARD_COMPONENT_WIDGET_TYPES` set, such as `metric-card`, and its other keys are that component's own props. |
| `widgets` | `(DashboardWidgetSlotComponentSchema \| DashboardWidgetSchema)[]` | **Required.** Each entry is a widget or a component node. A widget (`DashboardWidgetSchema`) names itself with `id`, `title` and `description`, sizes itself with `layout: { x, y, w, h }`, and holds its content either as a family named in `type` bound to a `dataset` (with that family's settings under `options`), or as a registered component node in `component`; its full key set is the spec's `DashboardWidget` plus objectui's own. A component node (`DashboardWidgetSlotComponentSchema`) sits in the slot directly: its `type` is a member of the closed `DASHBOARD_COMPONENT_WIDGET_TYPES` set, such as `metric-card`, its other keys are that component's own props, and it is placed by the same `layout` a widget is, which the editable grid's Save Layout writes onto every entry. |
| `refreshIntervalSeconds` | `number` | Auto-refresh interval in **seconds** — the renderer multiplies by 1000. Renamed from `refreshInterval`, which this table documented as milliseconds and which it never was (objectui#7783). |

A widget's size is its `layout`: `w` and `h` are the grid columns and rows it spans, and `x` and `y` are its position on the editable `dashboard-grid`. `layout` takes all four numbers or is left out. `colSpan`, `rowSpan` and `body` are **not** widget keys: `DashboardWidgetSchema` is strict (objectui#6002) and refuses all three by name. The size is `layout.w` / `layout.h`, and the content is `type` + `dataset` (with `options`) or `component`.
Expand Down
15 changes: 11 additions & 4 deletions content/docs/plugins/plugin-dashboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -399,9 +399,9 @@ const dashboard: DashboardComponentSchema = {

An entry of `widgets[]` is either a widget (`DashboardWidgetSchema`) or a
component node placed directly in the slot (`DashboardWidgetSlotComponentSchema`).
The widget keys (`layout`, `title`, `colorVariant`, `filter`, …) are declared on
the widget arm, which takes them from the spec's `DashboardWidget` row, and on
no member of the component arm. To read one off `widgets[]`, read it through
The widget keys (`title`, `colorVariant`, `filter`, …) are declared on the widget
arm, which takes them from the spec's `DashboardWidget` row, and on no member of
the component arm. To read one off `widgets[]`, read it through
`DashboardWidgetSchema`. The component arm is assignable to that type, so the
annotation is checked rather than asserted, and the key gets its declared type
instead of the `any` the component arm's passthrough supplies:
Expand All @@ -412,9 +412,16 @@ import type { DashboardComponentSchema, DashboardWidgetSchema } from '@object-ui
declare const dashboard: DashboardComponentSchema;

const widgets: DashboardWidgetSchema[] = dashboard.widgets;
const widestSpan = Math.max(0, ...widgets.map((w) => w.layout?.w ?? 0));
const accents = widgets.map((w) => w.colorVariant ?? 'default');
```

`layout` is the exception: both arms declare it, the component arm by reference
to the spec's widget `layout`, because the editable grid's Save Layout writes it
onto every entry, a `metric-card` node included. It reads with the spec's type
off any `widgets[]` entry, the strict authoring face accepts it on a component
node, and a malformed one (not four numbers, or a key besides `x`, `y`, `w` and
`h`) is refused on either arm.

## Type-aware list/table widget cells

`type: 'table'` widgets bound to an `objectName` infer the renderer for
Expand Down
16 changes: 12 additions & 4 deletions packages/plugin-dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ The authored shape is typed by `@object-ui/types`:
| --- | --- |
| `DashboardComponentSchema` | the whole `type: 'dashboard'` node — `columns`, `gap`, `widgets`, `header`, `globalFilters`, `dateRange`, `refreshIntervalSeconds`, … |
| `DashboardWidgetSchema` | one entry of `widgets[]` — the spec's `DashboardWidget` keys, plus objectui's own (`component`, `layout`, `options`, …) |
| `DashboardWidgetSlotComponentSchema` | the other kind of `widgets[]` entry — a component node placed directly in the slot, `type` one of the closed component set (`metric-card`); every other key is that component's own prop |
| `DashboardWidgetSlotComponentSchema` | the other kind of `widgets[]` entry — a component node placed directly in the slot, `type` one of the closed component set (`metric-card`); every other key is that component's own prop, except the spec's widget `layout`, which places the node |
| `DashboardWidgetLayout` | a widget's `{ x, y, w, h }` grid box |

```typescript
Expand Down Expand Up @@ -575,8 +575,8 @@ never as widget keys.

### Reading a widget key off `widgets[]`

The widget keys (`layout`, `title`, `colorVariant`, `filter`, …) are declared
on the widget arm, `DashboardWidgetSchema`, which takes them from the spec's
The widget keys (`title`, `colorVariant`, `filter`, …) are declared on the
widget arm, `DashboardWidgetSchema`, which takes them from the spec's
`DashboardWidget` row. The component arm declares none of them. Read straight
off a `widgets[]` entry, such a key is typed `any`, supplied by the component
arm's passthrough. Read it through `DashboardWidgetSchema` instead, which is how
Expand All @@ -590,9 +590,17 @@ import type { DashboardComponentSchema, DashboardWidgetSchema } from '@object-ui
declare const dashboard: DashboardComponentSchema;

const widgets: DashboardWidgetSchema[] = dashboard.widgets;
const widestSpan = Math.max(0, ...widgets.map((w) => w.layout?.w ?? 0));
const accents = widgets.map((w) => w.colorVariant ?? 'default');
```

`layout` is the one widget key both arms declare. The component arm takes the
spec's widget `layout` by reference, because Save Layout writes it onto every
entry of `widgets[]`, a `metric-card` node included (see
[DashboardGridLayout](#dashboardgridlayout--persisting-drag--resize-edits)). So
`layout` reads with the spec's type straight off a `widgets[]` entry, and the
strict authoring face accepts it on a component node. A malformed one (not
four numbers, or a key besides `x`, `y`, `w` and `h`) is refused on either arm.

## Customization

All components support Tailwind CSS classes:
Expand Down
15 changes: 8 additions & 7 deletions packages/plugin-dashboard/src/DashboardGridLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,14 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
//
// `w` is annotated `DashboardWidgetSchema`, as `buildDefaultLayouts` above
// already is (objectui#11348). A `widgets[]` entry is either arm of a union,
// and `layout` is declared on the widget arm only: the spec's
// `DashboardWidget` row declares it, and the component arm
// (`DashboardWidgetSlotComponentSchema`) has no spec row and declares none of
// the widget keys. That arm is assignable to `DashboardWidgetSchema` (pinned
// by `@object-ui/types`' `dashboard-widget-slot-component-arm-7952.test.ts`),
// so the annotation is checked by the compiler rather than asserted, and the
// read no longer rides `BaseSchema`'s index signature on the other arm.
// and both arms declare `layout` as the spec's `DashboardWidget` member: the
// widget arm through the spec row, the component arm
// (`DashboardWidgetSlotComponentSchema`) by reference to it, because
// `mergeLayoutIntoSchema` below writes it onto every entry, a component node
// included (objectui#11070 round 11). That arm is assignable to
// `DashboardWidgetSchema` (pinned by `@object-ui/types`'
// `dashboard-widget-slot-component-arm-7952.test.ts`), so the annotation is
// checked by the compiler rather than asserted.
const widgetsSignature = React.useMemo(
() => JSON.stringify(schema.widgets?.map((w: DashboardWidgetSchema, i: number) => ({
i: w.id || `widget-${i}`,
Expand Down
9 changes: 5 additions & 4 deletions packages/plugin-dashboard/src/DashboardRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -323,10 +323,11 @@ const DashboardRendererInner = forwardRef<HTMLDivElement, DashboardRendererProps
const inferredColumns = (() => {
if (schema.columns != null) return schema.columns;
// Typed `DashboardWidgetSchema[]`, the type `renderWidget` below already
// takes (objectui#11348): `layout` is declared on the widget arm of the
// `widgets[]` union only — the spec's `DashboardWidget` row declares it —
// and the component arm (`DashboardWidgetSlotComponentSchema`) is
// assignable to that arm, so this is a checked widening, not a cast.
// takes (objectui#11348). Both arms of the `widgets[]` union declare
// `layout` as the spec's `DashboardWidget` member — the component arm
// (`DashboardWidgetSlotComponentSchema`) by reference since objectui#11070
// round 11 — and that arm is assignable to the widget arm, so this is a
// checked widening, not a cast.
const widgets: DashboardWidgetSchema[] = schema.widgets ?? [];
let maxSpan = 0;
for (const w of widgets) {
Expand Down
14 changes: 8 additions & 6 deletions packages/plugin-dashboard/src/DashboardWithConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,13 @@ export function DashboardWithConfig({
// field change. This prevents useConfigDraft from resetting the draft.
const selectedWidgetConfig = React.useMemo(() => {
if (!selectedWidgetId || !liveSchema.widgets) return null;
// Read through `DashboardWidgetSchema` (objectui#11348). `title`,
// `colorVariant` and `layout` below are declared on the widget arm of the
// `widgets[]` union only — the spec's `DashboardWidget` row declares all
// three — and the component arm (`DashboardWidgetSlotComponentSchema`) is
// assignable to that arm (pinned by `@object-ui/types`'
// Read through `DashboardWidgetSchema` (objectui#11348). `title` and
// `colorVariant` below are declared on the widget arm of the `widgets[]`
// union only — the spec's `DashboardWidget` row declares both — and
// `layout` on both arms (the component arm takes the spec's member by
// reference, objectui#11070 round 11). The component arm
// (`DashboardWidgetSlotComponentSchema`) is assignable to the widget arm
// (pinned by `@object-ui/types`'
// `dashboard-widget-slot-component-arm-7952.test.ts`), so the annotation is
// checked by the compiler rather than asserted.
const widgets: DashboardWidgetSchema[] = liveSchema.widgets;
Expand Down Expand Up @@ -161,7 +163,7 @@ export function DashboardWithConfig({
return {
...prev,
// `DashboardWidgetSchema`, for the reason `selectedWidgetConfig`
// states: `title` and `layout` are widget-arm keys (objectui#11348).
// states: `title` is a widget-arm key (objectui#11348).
widgets: prev.widgets.map((w: DashboardWidgetSchema) => {
if ((w.id || w.title) !== selectedWidgetId) return w;
if (field === 'layoutW') {
Expand Down
23 changes: 17 additions & 6 deletions packages/plugin-dashboard/src/DatasetWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1071,12 +1071,23 @@ export function DatasetWidget({ widget, dataSource, subCaption }: { widget: any;
// declared the key stays byte-identical.
const accentClass = metricAccentTextClass(widget?.colorVariant);
// ── The declared sub-caption (objectui#7293) ───────────────────────────
// `options.description` is the metric tile's SUB-CAPTION slot. It is
// declared end to end and reached nothing: it has its own translation key
// (`{ns}.dashboards.{dash}.widgets.{id}.subCaption`, objectui#4032 item 4 /
// objectstack#8056), the server's `translateDashboard` OVERLAYS that
// translation onto this very key, and `DashboardRenderer.tWidgetSubCaption`
// resolves it — but only onto the two INLINE arms of `getComponentSchema`.
// `options.description` is the metric tile's SUB-CAPTION slot. Before
// #7293 it was wired end to end and reached nothing here: it has its own
// translation key (`{ns}.dashboards.{dash}.widgets.{id}.subCaption`,
// objectui#4032 item 4 / objectstack#8056), the server's
// `translateDashboard` OVERLAYS that translation onto this very key, and
// `DashboardRenderer.tWidgetSubCaption` resolved it — but only onto the two
// INLINE arms of `getComponentSchema`.
//
// ⚠️ Wired, not DECLARED (objectui#11070 round 11). `@objectstack/spec`'s
// `DashboardWidgetOptionsSchema` has no `description` member; its open bag
// admits the key without judging it. `translateDashboard` writes it on the
// served path (objectstack's `check:widget-option-census` ledgers it as an
// undeclared resolver output). objectui's strict authoring face refuses an
// authored one with the inline-dialect keys (objectui#11228 ruling C).
// Which way it goes — a spec declaration, a declared home for the overlay,
// or retiring both ends — is an open decision on objectui#11070; this read
// stays meanwhile, because its writer is live.
// `dataset` is REQUIRED on `DashboardWidgetSchema` (verified against the
// published @objectstack/spec@17.4.0: required keys are id/dataset/values),
// so every spec-legal widget renders HERE instead, and every author who
Expand Down
Loading
Loading