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
17 changes: 17 additions & 0 deletions .changeset/11598-dashboard-slot-entry-reads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@object-ui/plugin-dashboard': patch
'@object-ui/plugin-designer': patch
'@object-ui/types': patch
---

The dashboard surfaces read every `widgets[]` entry without leaning on `BaseSchema`'s index signature: a widget key is read on the widget arm alone, and the `chart` node is built as a private hand-off type (objectui#11598).

**N1, the `chart` producers.** `DashboardRenderer` and `DashboardGridLayout` build a `chart` node for a series widget bound to inline rows, and compose two render keys onto it: the dashboard palette (`colors`) and `isAnimationActive: false`, the deterministic first paint inside the grid (#2756). `ChartSchema` declares neither key, on either face, and the chart renderer reads both. Each producer now checks its literal against a hand-off type private to this package, `ChartSchema` plus those two keys, and hands the node on with no cast; the type is not exported, and the keys stay off `@object-ui/types`, because the strict authoring face refuses both on an authored `chart` node. Nothing drawn changes.

**N2, widget keys on the slot entry.** An entry of `widgets[]` is a widget or a component node placed in the slot (a `metric-card`), and only the widget declares the widget keys (`dataset`, `options`, `chartConfig`, `filter`, `component`, `colorVariant`, `values`, `dimensions`, …). Every read of one now narrows the entry to the widget first, on `DashboardRenderer`, `DashboardGridLayout`, `DashboardWithConfig` and `DashboardEditor`. What changes is confined to a `metric-card` entry that carries a widget key, which `@object-ui/types/zod`'s strict face refuses and only the tolerant face accepts:

- a `metric-card` carrying `dataset` draws its card; it used to draw the dataset tile in the card's place, on both dashboard surfaces;
- a `metric-card` carrying `options` draws its own keys; `options` used to be spread over them, so `options.value` replaced `value`;
- a `metric-card` carrying a `component` draws its card; the envelope's node used to be drawn instead.

A document the strict face accepts draws exactly as before. `@object-ui/types`' docblocks on `DASHBOARD_COMPONENT_WIDGET_TYPES` and the Zod widget vocabulary, which described the dataset tile in the card's place and the `options` spread as live, were corrected to match; no type in it changes. In `DashboardEditor`, a `metric-card` entry is no longer offered the Color Variant select: the card declares no `colorVariant`, `MetricCard` draws nothing from it, and a pick stored a key publish refuses. A widget is offered it as before.
8 changes: 5 additions & 3 deletions content/docs/plugins/plugin-dashboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,11 @@ 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
read through widget keys, and no widget key stands in for `value`: the
dashboard reads a widget key (`dataset`, `options`, `component`,
`colorVariant`, …) on a widget alone, so a card draws its own keys whatever
else it carries, and the strict face refuses a widget key on it
(objectui#11483, objectui#11598). For a figure queried from a
dataset, write a `metric` widget with `dataset` and `values`.

## Usage
Expand Down
8 changes: 5 additions & 3 deletions packages/plugin-dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,11 @@ 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
read through widget keys, and no widget key stands in for `value`: the
dashboard reads a widget key (`dataset`, `options`, `component`,
`colorVariant`, …) on a widget alone, so a card draws its own keys whatever
else it carries, and the strict face refuses a widget key on it
(objectui#11483, objectui#11598). For a figure queried from a
dataset, write a `metric` widget with `dataset` and `values` (see "TypeScript
Support" below).

Expand Down
105 changes: 53 additions & 52 deletions packages/plugin-dashboard/src/DashboardGridLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from './widgetDispatch';
import { LEGACY_RETIRED_WIDGET_SCHEMA, isLegacyRetiredWidget, isRetiredEnvelopeNode } from './legacyRetiredWidget';
import { DatasetWidget } from './DatasetWidget';
import type { DashboardChartRenderSchema } from './chartRenderHandoff';
import { useWidgetSubCaption } from './widgetSubCaption';
import { useDashboardAutoRefresh } from './useDashboardAutoRefresh';

Expand Down Expand Up @@ -56,11 +57,11 @@ export interface DashboardGridLayoutProps {
* `DatasetWidget` for ADR-0021 dataset-bound widgets (objectui#4614).
*
* Typed `unknown`, not `any`. The sibling declares `dataSource?: any`
* (`DashboardRenderer.tsx:198`) for a reason that is explicitly historical —
* (`DashboardRendererProps.dataSource`) for a reason that is explicitly historical —
* "that is precisely what it resolved to before", via the index signature that
* used to answer for it — and this is a NEW declaration with no prior
* resolution to preserve. `unknown` is what the consumer itself declares
* (`DatasetWidget.tsx:655`, `dataSource: unknown`), so the value is forwarded
* (`DatasetWidget`'s own parameter, `dataSource: unknown`), so the value is forwarded
* to exactly the type that receives it, and no call site is held to anything
* new: every value is assignable to `unknown`. Narrowing it further to a real
* adapter type is a separate change with its own consumer sweep, on both
Expand All @@ -73,10 +74,11 @@ export interface DashboardGridLayoutProps {
* A dataset-bound widget arriving that way renders `DatasetWidget`'s own
* no-capability diagnostic — a visible state, never a blank tile.
*
* `SchemaRenderer` forwards this as a React prop (its `...props` spread, last:
* `SchemaRenderer.tsx:632`). It cannot be shadowed by the spec's per-element
* `dataSource` BINDING, which is stripped from the schema before that spread
* for exactly this reason (`SchemaRenderer.tsx:564-575`, objectstack#5576).
* `SchemaRenderer` forwards this as a React prop (its `...props` spread,
* last). It cannot be shadowed by the spec's per-element `dataSource`
* BINDING, which is stripped from the schema before that spread for exactly
* this reason (the `dataSource: _dataSource` strip in `SchemaRenderer`'s
* destructure, objectstack#5576).
*/
dataSource?: unknown;
/**
Expand Down Expand Up @@ -258,6 +260,14 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
// that prop's (objectui#11466): each node is checked against its declared
// type where it is built, and the render sites below take it with no cast.
const getComponentSchema = React.useCallback((widget: DashboardWidgetSlotEntry): SchemaRendererProps['schema'] => {
// The slot-component passthrough serves the slot's component arm alone and
// takes the namespaced node key too (`toDashboardNodeType`, objectui#10859
// batch 8; objectui#11514, Q2 A). Decided FIRST, as `DashboardRenderer`
// decides it, and reading no widget key (objectui#11598, N2 A): below it
// `widget` is the widget arm, so every widget key is read off the arm that
// declares it.
if (isSlotComponentEntry(widget)) return toDashboardNodeType({ ...widget });

// Same boundary as `DashboardRenderer`: the author's node keeps its
// spelling except a `metric` / `metric-card` node key, which moves onto its
// namespaced registration (`toDashboardNodeType`, objectui#10859 batch 8).
Expand Down Expand Up @@ -359,7 +369,11 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({

const dataItems = Array.isArray(widgetData) ? widgetData : widgetData?.items || [];

return {
// Checked against the dashboard's chart hand-off, `ChartSchema` plus the
// two render keys this producer composes (`./chartRenderHandoff`,
// objectui#11598), as `DashboardRenderer` builds it; handed on with no
// cast.
const chartNode: DashboardChartRenderSchema = {
type: 'chart',
chartType: dispatch.chartType,
data: dataItems,
Expand All @@ -374,6 +388,7 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
className: "h-full",
...chartPresentation,
};
return chartNode;
}

// Single-value families render as a metric card, not a chart (#2943).
Expand Down Expand Up @@ -501,17 +516,9 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
return unsupportedWidgetSchema(widgetType);
}

// The slot-component passthrough serves the slot's component arm alone and
// takes the namespaced node key too (`toDashboardNodeType`, objectui#10859
// batch 8; objectui#11514, Q2 A). Any other entry here names no family:
// stale metadata, drawn as the labelled placeholder, as `DashboardRenderer`
// draws it.
if (isSlotComponentEntry(widget)) {
return toDashboardNodeType({
...widget,
...options
});
}
// An entry here names no family and is no component node (the passthrough
// above took those): stale metadata, drawn as the labelled placeholder, as
// `DashboardRenderer` draws it.
return unsupportedWidgetSchema(widgetType);
}, [resolveSeriesLabel]);

Expand Down Expand Up @@ -613,33 +620,31 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
// ADR-0021 — a widget bound to a semantic-layer dataset renders
// through the governed queryDataset path (DatasetWidget) instead of
// the inline object-aggregate schema. Decided per widget AT THE
// RENDER SITE, which is `DashboardRenderer`'s own mechanic
// (`DashboardRenderer.tsx:524` for the predicate, `:849-851` for the
// fork) rather than a second dispatch idiom invented here: this
// surface had NO dataset path at all, so every current-shape widget
// fell through `getComponentSchema` to the static-data branch and
// drew `data: []` — a blank chart, an em-dash metric or an empty
// table depending on the family, with no diagnostic (objectui#4614).
// The cast names the ONE key it reads, rather than reaching for
// `as any` the way the sibling does (`DashboardRenderer.tsx:524`):
// the bundled DashboardWidget type gains `dataset` only after
// objectui bumps @objectstack/spec, and `legacyRetiredWidget.ts`
// already answers that same problem in this package by naming the
// undeclared keys in a shape of its own — "what keeps `as any` out
// of both call sites" (`legacyRetiredWidget.ts:64-80`). One key is
// read here, so the shape is stated inline.
// RENDER SITE, which is `DashboardRenderer`'s own mechanic rather
// than a second dispatch idiom invented here: this surface had NO
// dataset path at all, so every current-shape widget fell through
// `getComponentSchema` to the static-data branch and drew
// `data: []` — a blank chart, an em-dash metric or an empty table
// depending on the family, with no diagnostic (objectui#4614).
//
// `dataset` is a widget key, read on the widget arm alone
// (objectui#11598, N2 A), as `DashboardRenderer` reads it: a
// component node in the slot (a `metric-card`) draws itself,
// whatever else it carries. The read used to be a cast naming the
// one key, so a `metric-card` the strict face refuses for its
// `dataset` drew `DatasetWidget` in the card's place.
//
// Position relative to the objectui#4612 legacy sentinel (which
// stays where it is, inside `getComponentSchema` BEFORE the dispatch
// branches): the two conditions are MUTUALLY EXCLUSIVE by
// construction, because `isLegacyRetiredWidget` returns false the
// moment a widget carries `dataset` (`legacyRetiredWidget.ts:107`).
// Neither can capture the other's widget, so their relative order
// cannot change a verdict on either surface — the same arrangement
// `DashboardRenderer` has carried since #4612, and the reason that
// module states step 1 on its own terms instead of inheriting it
// from a caller's fork (`legacyRetiredWidget.ts:97-102`).
const datasetBound = !!(widget as { dataset?: unknown }).dataset;
// moment a widget carries `dataset` (its step 1). Neither can capture
// the other's widget, so their relative order cannot change a verdict
// on either surface — the same arrangement `DashboardRenderer` has
// carried since #4612, and the reason that module states step 1 on
// its own terms instead of inheriting it from a caller's fork.
const datasetWidget = !isSlotComponentEntry(widget) && widget.dataset ? widget : undefined;
const datasetBound = datasetWidget !== undefined;
// A `metric` widget renders its own card chrome ONLY in the inline
// path (the `plugin-dashboard:metric` card, or a retired
// `provider: 'object'` metric's placeholder, objectui#11525). A
Expand Down Expand Up @@ -688,35 +693,31 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
<CardContent className="p-0 h-full">
<div className={cn("h-full w-full overflow-auto p-4")}>
{/*
The fork itself, mirroring `DashboardRenderer.tsx:849-851`.
`widget` is passed whole: DatasetWidget reads
The fork itself, mirroring `DashboardRenderer`'s.
The widget arm is passed whole: DatasetWidget reads
`widget.filter` and forwards it to the query as
`runtimeFilter`, so an authored per-widget filter still
applies. The sibling wraps it as `effectiveWidget` only
to merge in the dashboard FILTER BAR's scoped filter,
applies. The sibling copies it as its `datasetWidget`
only to merge in the dashboard FILTER BAR's scoped filter,
which this surface does not have — there is no
`scopedFilter` here to merge, so re-creating the wrapper
would state a dependency that does not exist.

This is the ONLY fork: the self-contained branch above
cannot be reached by a dataset-bound widget, since
`isSelfContained` now requires `!datasetBound`. The
sibling carries a second, identical fork inside its
self-contained branch (`:820-822`) that is unreachable
for that same reason; the reachable mechanics are what
is mirrored here, not the stranded limb.
`isSelfContained` now requires `!datasetBound`.
*/}
{datasetBound
{datasetWidget
? <DatasetWidget
widget={widget}
widget={datasetWidget}
dataSource={dataSource}
/* objectui#8889 — dispatch site 2 of 2, and the half that
objectui#4614 exists to stop anyone from forgetting: the
sibling passing this alone would fix one surface and leave
this one silently unchanged. `?? null` says "a surface
resolved it, to nothing", which is NOT the same as the
prop being absent — see the prop's docblock. */
subCaption={tWidgetSubCaption(widget) ?? null}
subCaption={tWidgetSubCaption(datasetWidget) ?? null}
/>
: <SchemaRenderer schema={componentSchema} />}
</div>
Expand Down
Loading
Loading