Skip to content

Commit 2e818d0

Browse files
fix(plugin-dashboard,plugin-designer): the 22 slot-entry and chart-producer reads compile without BaseSchema's index signature (objectui#11598) (#11599)
Fixes #11598 Clause-②: no The plugin-dashboard and plugin-designer half that the removal of `BaseSchema`'s index signature (objectui#8347) needs first. The 22 source reads that compiled only through that signature now compile without it, and the signature is still present. Two commits, one per class, so they read apart: `921e56cc` (N1) and `d6da79f6` (N2). The parent objectui#8347 stays open; this PR only clears its `Blocked-by:`. ## N1: the `chart` producers' render keys (seat ruling B) - New `packages/plugin-dashboard/src/chartRenderHandoff.ts` declares `DashboardChartRenderSchema`: `ChartSchema` plus `colors?: string[]` and `isAnimationActive?: boolean`. Its header follows plugin-timeline's `renderHandoff.ts` (the objectui#6356 ruling, Q1 = A). It is not exported from the package index (the built `dist/index.d.ts` names it 0 times) and is not added to `@object-ui/types`. - `DashboardRenderer` and `DashboardGridLayout` build the static-data `chart` node as a const annotated with the hand-off. That annotation excess-checks the literal against it. The node is then returned to `SchemaRenderer` with no cast. Nothing drawn changes. - **H3 holds; the consumer side needs nothing.** `ChartRenderer` (`packages/plugin-charts/src/ChartRenderer.tsx`) declares both keys on its own props type (`ChartRendererProps.schema`: `colors?: string[]`, `isAnimationActive?: boolean`) and reads `schema.colors` and `schema.isAnimationActive` with no cast. The registered `chart` component receives the node as `schema`, typed by that props type, not by `ChartSchema`. The new test pins the hand-off's two key types as assignable to `React.ComponentProps` of `ChartRenderer`'s `schema`. ## N2: widget keys read off the slot entry (seat ruling A, premised) - **Premise checked first, and it holds** (evidence below): no authored document carries a widget-arm key on a `metric-card` entry. - Both surfaces now decide the slot-component passthrough before reading any widget key: `if (isSlotComponentEntry(widget)) return toDashboardNodeType({ ...widget })`. Below that line `widget` is the widget arm, so `options`, `chartConfig`, `filter`, `compareTo`, `searchable` and `pagination` are read off the arm that declares them. - The either-arm `dataset` fork is gone. `DashboardRenderer` reads `dataset` off `widgetArm` (the entry narrowed by `isSlotComponentEntry`). `DashboardGridLayout`'s twin fork was a cast, `(widget as { dataset?: unknown })`. The P1 census could not see a cast, so this is a 23rd read; it now narrows the same way. - `entryComponent` reads `component` on the widget arm only. Its cross-arm read is gone. - `DashboardWithConfig` seeds the config panel's `dataset`, `dimensions`, `values` and `colorVariant` from the widget arm. Before, the first three were read through `as any`. - `DashboardEditor` (plugin-designer) narrows with a local twin of the guard, because it cannot import plugin-dashboard (the same reason its `measureRefusal` is a twin). Both read the one closed list, `DASHBOARD_COMPONENT_WIDGET_TYPES`. The Color Variant select is offered for a widget only (H2, below). - Also narrowed: the filter-bar scoping in `DashboardRenderer` (`buildWidgetScopedFilter` reads `filterBindings` through a `Pick`), and the `subCaption` read at both dataset forks. - **What changes at runtime.** Only a `metric-card` entry that carries a widget key changes. The strict face refuses such an entry and the tolerant face accepts it (pinned). That card now draws itself. Before, it drew one of three other things: the dataset tile (on both surfaces), its own keys with `options` spread over them, or the node in its `component` envelope. Every document the strict face accepts draws as before. ## Measurements ### H1: the 22 sites, before and after The instrument: the four tsc programs (`tsconfig.json` and `tsconfig.test.json` of each package), one-off, trap-restored. It deletes the docblock and signature lines of `BaseSchema` in `packages/types/src/base.ts` and rebuilds `@object-ui/types`; the dist keeps one signature, `ComponentRendererProps`'. Restore is proven by the blob hash equal to HEAD's and an empty `git diff HEAD`. H1 holds: `origin/main` is still the census's `94985a9`, and BASE reproduces the census exactly. No site was added or removed. | Site (file · key) | BASE `94985a9`, signature removed | HEAD `d6da79f6`, signature removed | |---|---|---| | DashboardRenderer · `dataset` | TS2339 | none | | DashboardRenderer · `options` | TS2339 | none | | DashboardRenderer · `chartConfig` | TS2339 | none | | DashboardRenderer · `filter` (object-chart node) | TS2339 | none | | DashboardRenderer · `compareTo` | TS2339 | none | | DashboardRenderer · `colors` on the `chart` literal (N1; `isAnimationActive` is its second excess key, which tsc does not name) | TS2353 | none | | DashboardRenderer · `filter` (object-data-table node) | TS2339 | none | | DashboardRenderer · `searchable` | TS2339 | none | | DashboardRenderer · `pagination` | TS2339 | none | | DashboardRenderer · `filter` (dataset-widget merge) | TS2339 | none | | DashboardGridLayout · `options` | TS2339 | none | | DashboardGridLayout · `chartConfig` | TS2339 | none | | DashboardGridLayout · `colors` on the `chart` literal (N1) | TS2353 | none | | DashboardGridLayout · `filter` | TS2339 | none | | DashboardGridLayout · `searchable` | TS2339 | none | | DashboardGridLayout · `pagination` | TS2339 | none | | DashboardWithConfig · `colorVariant` | TS2339 | none | | widgetDispatch `entryComponent` · `component` | TS2339 | none | | DashboardEditor · `values` | TS2339 | none | | DashboardEditor · `dimensions` | TS2339 | none | | DashboardEditor · `dimensions` | TS2339 | none | | DashboardEditor · `colorVariant` | TS2339 | none | Program totals with the signature removed, BASE then HEAD: - plugin-dashboard src: 18, then 0. - plugin-dashboard test: 24, then 6. - plugin-designer src: 4, then 0. - plugin-designer test: 4, then 0. The 6 left are objectui#8347's adjudicated probe flips (`bogusKey` ×2, `zzUndeclared7353` ×4), the same 6 at BASE. With the signature present, all four programs exit 0 at both commits. **The N1 hand-off really checks the literal.** In a one-off probe with the signature removed, a planted `bogusProbeKey11598` on each `chart` literal fails with TS2353 "does not exist in type 'DashboardChartRenderSchema'", in `DashboardGridLayout` and in `DashboardRenderer`. The probe was trap-restored and hash-verified. ### N2 premise census (at BASE) - **Population**: `git grep -l -F metric-card 94985a9 -- .`, 45 files. Each file is read with `git show` at the same commit, so the file list and the contents come from the same source. - **Detector**: every object literal whose `type` is `'metric-card'`, found two ways. AST: the TypeScript compiler over TS, TSX and JSON. SCAN: the balanced brace region around each occurrence, which covers md and mdx. Each literal's keys are compared with the widget-arm-only key set (`dataset`, `options`, `chartConfig`, `filter`, `component`, `colorVariant`, `values`, `dimensions`, `compareTo`, `searchable`, `pagination`, `filterBindings` and the rest of the spec row). - **Result**: 51 AST and 88 SCAN literals. **2 carry a widget-arm key**, both in `packages/types/src/__tests__/metric-card-needs-value-11483.test.ts`. They are refusal probes: a card with `dataset` and `values`, and a card with `options`, each with no `value` and refused on every face. They are not authored documents. - **Per corpus, 0 hits**: - `content/docs/**`: 2 files, 9 literals. - `examples/**`: 3 schema-catalog dashboards, 11 literals. - Package READMEs: 2 files, 16 literals. - `skills/**`: no file in the population. - app-shell seeds and presets: none. app-shell's only file in the population is a test that uses the `plugin-dashboard:metric-card` node key. - **Lit controls**: - Planted: an md file with a fenced dashboard whose card carries `dataset`, and a TS file whose card carries `options` and `component`. Both are flagged. - Real corpus: the same detector pointed at `type: 'bar'` sees the known widget-arm `dataset` entry in `content/docs/plugins/plugin-dashboard.mdx` (`{ "id": "w1", "type": "bar", "dataset": "invoices", … }`) and the schema catalog's `filtered-dashboard-dataset-widgets.json` entry. ### The two-way `dataset` pin, and reverse validation `packages/plugin-dashboard/src/__tests__/slotEntryWidgetArmReads-11598.test.tsx` tests on both surfaces. It identifies `DatasetWidget` by its `dataset-loading` skeleton under a query that never settles. - A widget-arm entry with `dataset` draws `DatasetWidget`, and the query is called with `{ dimensions: ['status'], measures: ['count'] }`. - A `metric-card` carrying `dataset` does not reach it: the card's `$24k` is drawn, no skeleton appears, and no query is sent. - The CONTROL is a strict-valid card, which draws as before. - The file also pins the `options` and `component` reads, `entryComponent`, and the face verdicts: each moved card is strict-refused at `widgets.0` and tolerant-accepted. The designer test is `packages/plugin-designer/src/__tests__/DashboardEditor.slotComponentArm-11598.test.tsx`. **Reverse validation, run from the committed state.** The five N2 source files were put back to their bytes at `921e56cc`. On disk, the `N2 A` markers went from 10 to 0 and the either-arm comment came back. With the new pins unchanged, the run was `Tests 8 failed | 10 passed (18)`: - every card case failed on both surfaces: `dataset-loading` rendered in the card's place, `OPTIONS-FIGURE` drawn, and the envelope drawn; - the `entryComponent` unit failed; - the designer `metric-card` case failed; - the positives, controls and face checks stayed green. The direction was red, as expected. The restore is proven by 5 blob hashes equal to HEAD and `git diff HEAD` at 0 bytes. My first attempt stopped before running any test: its marker count was wrong, because N1 had already put the issue number into two of the files. It was re-run with the `N2 A` marker. ### H2: `DashboardEditor` - **`values` and `dimensions`** are read by `measureRefusal`, the door probe behind the type picker. For a strict-valid `metric-card` they were already absent, so the probe is unchanged and nothing is visible. - **`colorVariant`** is the Color Variant select. Here the seat's "invisible" expectation does not hold. The select was offered for every `metric-card` entry, and a pick wrote `colorVariant` onto the card. The strict face refuses that key on the card, and `MetricCard` has no such prop, so the value is not drawn (app-shell's `widget-dom-leak-sweep` test notes it lands as a `colorvariant` DOM attribute). After this PR, a `metric-card` entry is not offered the select. Title, width and height stay, and a widget is offered the select as before. So one control is gone for strict-valid cards, and its only effect was storing a key that publish refuses. ## Gates (all at `d6da79f6`, each exit captured before any pipe, heavy runs under the shared verify lock, `TURBO_FORCE=true` on every turbo run) - `turbo run build` for plugin-dashboard and plugin-designer: exit 0. "Tasks: 19 successful, 19 total", "Cached: 0 cached". - Build of the doc-gate closure (`--build-filter`): exit 0, "Tasks: 35 successful, 35 total". - `pnpm --filter @object-ui/plugin-dashboard type-check`: exit 0. - `pnpm --filter @object-ui/plugin-designer type-check`: exit 0. - `pnpm --filter @object-ui/types type-check`: exit 0. - `pnpm exec vitest run --maxWorkers=2 packages/plugin-dashboard/`: exit 0, "Test Files 168 passed (168)", "Tests 1669 passed | 6 skipped (1675)". - `pnpm exec vitest run --maxWorkers=2 packages/plugin-designer/`: exit 0, "Test Files 41 passed (41)", "Tests 305 passed (305)". - `check:doc-snippets`: exit 0, "777 of 777 block(s) judged, 0 failed". - `check:doc-examples`: exit 0, "35 compile, 87 fail, 87 of those declared in the ledger". - `check:doc-types`: exit 0. - `check:readme-exports`: exit 0. - The first runs of `check:doc-snippets`, `check:doc-examples` and `check:readme-exports` stopped on PRECONDITION NOT MET (packages unbuilt). They were not red, and were re-run after the closure build. - `check:changeset-claims`: exit 0. - `check:pending-changeset-literals`: exit 0. - `check:control-bytes`: exit 0. - `changeset:check`: exit 0. - `check-changeset-presence`: exit 0, "9 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)". - `type-check:coverage`: exit 0. - `check:new-line-citations`: exit 0, "0 new citation(s)". - ESLint, narrowed to the 9 touched TS/TSX files: 0 errors and 23 warnings, all pre-existing rules on lines this PR did not add. The count of 9 is read from the `--format json` output. The config does not enable type-aware linting, so this diff cannot move a verdict on an untouched file. The repo-wide `pnpm lint` is left to CI. - Consumer `type-check` (app-shell, console): NOT MEASURED. No exported declaration changed: the hand-off is not reachable from `dist/index.d.ts`, and the props of `DashboardRenderer`, `DashboardGridLayout`, `DashboardWithConfig` and `DashboardEditor` are untouched. ## Acceptance notes - **File surface, declared.** This PR also rewrites one paragraph in `packages/plugin-dashboard/README.md` and the same paragraph in `content/docs/plugins/plugin-dashboard.mdx`. Both said "a `dataset` makes the dashboard draw its dataset tile in the card's place", which this change makes false (AGENTS.md #2). It also repairs four cross-file line citations inside the `DashboardGridLayout.tsx` comments it touched, by content (AGENTS.md #11). - **Prose made false in `packages/types`, not edited, because the claim excludes `packages/types`.** Each is a present-tense account of the either-arm draw: - `complex.ts`, in the `DASHBOARD_COMPONENT_WIDGET_TYPES` docblock: "a `dataset` draws `DatasetWidget` in the card's place by a rule that ignores the `type`"; - `zod/complex.zod.ts`, the same sentence, plus "The `options` bag is spread onto the node as literal props"; - `__tests__/metric-card-needs-value-11483.test.ts`, in its header and two inline comments. The first two ship in `@object-ui/types`' dist JSDoc. Proposed carrier: the PR that executes objectui#8347, which already edits complex.ts's `widgets` notes. Or the seat may direct otherwise (see the report). - Observation, not reproduced: `DashboardEditor`'s type picker still offers widget types for a `metric-card` entry. A pick would write a widget `type` onto the card's keys. - `isLegacyRetiredWidget` still reads `dataset`, `data`, `options` and `object` through its own named shape. After the reorder, it only ever receives widget-arm entries on both surfaces. --- _Generated by [Claude Code](https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e8c0b96 commit 2e818d0

15 files changed

Lines changed: 647 additions & 142 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@object-ui/plugin-dashboard': patch
3+
'@object-ui/plugin-designer': patch
4+
'@object-ui/types': patch
5+
---
6+
7+
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).
8+
9+
**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.
10+
11+
**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:
12+
13+
- a `metric-card` carrying `dataset` draws its card; it used to draw the dataset tile in the card's place, on both dashboard surfaces;
14+
- a `metric-card` carrying `options` draws its own keys; `options` used to be spread over them, so `options.value` replaced `value`;
15+
- a `metric-card` carrying a `component` draws its card; the envelope's node used to be drawn instead.
16+
17+
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.

‎content/docs/plugins/plugin-dashboard.mdx‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,11 @@ outside a dashboard's `widgets[]` renders nothing but the renderer's
9494

9595
A card with no `value` is refused by `@object-ui/types/zod`, on the tolerant
9696
face and the strict one. `metric-card` is not a widget type, so a card is never
97-
read through widget keys alone, and no widget key stands in for `value`: a
98-
`dataset` makes the dashboard draw its dataset tile in the card's place, and
99-
`options` is not a binding (objectui#11483). For a figure queried from a
97+
read through widget keys, and no widget key stands in for `value`: the
98+
dashboard reads a widget key (`dataset`, `options`, `component`,
99+
`colorVariant`, …) on a widget alone, so a card draws its own keys whatever
100+
else it carries, and the strict face refuses a widget key on it
101+
(objectui#11483, objectui#11598). For a figure queried from a
100102
dataset, write a `metric` widget with `dataset` and `values`.
101103

102104
## Usage

‎packages/plugin-dashboard/README.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,11 @@ or the spec's inline per-locale map (`I18nLabel`).
207207

208208
A card with no `value` is refused by `@object-ui/types/zod`, on the tolerant
209209
face and the strict one. `metric-card` is not a widget type, so a card is never
210-
read through widget keys alone, and no widget key stands in for `value`: a
211-
`dataset` makes the dashboard draw its dataset tile in the card's place, and
212-
`options` is not a binding (objectui#11483). For a figure queried from a
210+
read through widget keys, and no widget key stands in for `value`: the
211+
dashboard reads a widget key (`dataset`, `options`, `component`,
212+
`colorVariant`, …) on a widget alone, so a card draws its own keys whatever
213+
else it carries, and the strict face refuses a widget key on it
214+
(objectui#11483, objectui#11598). For a figure queried from a
213215
dataset, write a `metric` widget with `dataset` and `values` (see "TypeScript
214216
Support" below).
215217

‎packages/plugin-dashboard/src/DashboardGridLayout.tsx‎

Lines changed: 53 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from './widgetDispatch';
2222
import { LEGACY_RETIRED_WIDGET_SCHEMA, isLegacyRetiredWidget, isRetiredEnvelopeNode } from './legacyRetiredWidget';
2323
import { DatasetWidget } from './DatasetWidget';
24+
import type { DashboardChartRenderSchema } from './chartRenderHandoff';
2425
import { useWidgetSubCaption } from './widgetSubCaption';
2526
import { useDashboardAutoRefresh } from './useDashboardAutoRefresh';
2627

@@ -56,11 +57,11 @@ export interface DashboardGridLayoutProps {
5657
* `DatasetWidget` for ADR-0021 dataset-bound widgets (objectui#4614).
5758
*
5859
* Typed `unknown`, not `any`. The sibling declares `dataSource?: any`
59-
* (`DashboardRenderer.tsx:198`) for a reason that is explicitly historical —
60+
* (`DashboardRendererProps.dataSource`) for a reason that is explicitly historical —
6061
* "that is precisely what it resolved to before", via the index signature that
6162
* used to answer for it — and this is a NEW declaration with no prior
6263
* resolution to preserve. `unknown` is what the consumer itself declares
63-
* (`DatasetWidget.tsx:655`, `dataSource: unknown`), so the value is forwarded
64+
* (`DatasetWidget`'s own parameter, `dataSource: unknown`), so the value is forwarded
6465
* to exactly the type that receives it, and no call site is held to anything
6566
* new: every value is assignable to `unknown`. Narrowing it further to a real
6667
* adapter type is a separate change with its own consumer sweep, on both
@@ -73,10 +74,11 @@ export interface DashboardGridLayoutProps {
7374
* A dataset-bound widget arriving that way renders `DatasetWidget`'s own
7475
* no-capability diagnostic — a visible state, never a blank tile.
7576
*
76-
* `SchemaRenderer` forwards this as a React prop (its `...props` spread, last:
77-
* `SchemaRenderer.tsx:632`). It cannot be shadowed by the spec's per-element
78-
* `dataSource` BINDING, which is stripped from the schema before that spread
79-
* for exactly this reason (`SchemaRenderer.tsx:564-575`, objectstack#5576).
77+
* `SchemaRenderer` forwards this as a React prop (its `...props` spread,
78+
* last). It cannot be shadowed by the spec's per-element `dataSource`
79+
* BINDING, which is stripped from the schema before that spread for exactly
80+
* this reason (the `dataSource: _dataSource` strip in `SchemaRenderer`'s
81+
* destructure, objectstack#5576).
8082
*/
8183
dataSource?: unknown;
8284
/**
@@ -258,6 +260,14 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
258260
// that prop's (objectui#11466): each node is checked against its declared
259261
// type where it is built, and the render sites below take it with no cast.
260262
const getComponentSchema = React.useCallback((widget: DashboardWidgetSlotEntry): SchemaRendererProps['schema'] => {
263+
// The slot-component passthrough serves the slot's component arm alone and
264+
// takes the namespaced node key too (`toDashboardNodeType`, objectui#10859
265+
// batch 8; objectui#11514, Q2 A). Decided FIRST, as `DashboardRenderer`
266+
// decides it, and reading no widget key (objectui#11598, N2 A): below it
267+
// `widget` is the widget arm, so every widget key is read off the arm that
268+
// declares it.
269+
if (isSlotComponentEntry(widget)) return toDashboardNodeType({ ...widget });
270+
261271
// Same boundary as `DashboardRenderer`: the author's node keeps its
262272
// spelling except a `metric` / `metric-card` node key, which moves onto its
263273
// namespaced registration (`toDashboardNodeType`, objectui#10859 batch 8).
@@ -359,7 +369,11 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
359369

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

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

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

504-
// The slot-component passthrough serves the slot's component arm alone and
505-
// takes the namespaced node key too (`toDashboardNodeType`, objectui#10859
506-
// batch 8; objectui#11514, Q2 A). Any other entry here names no family:
507-
// stale metadata, drawn as the labelled placeholder, as `DashboardRenderer`
508-
// draws it.
509-
if (isSlotComponentEntry(widget)) {
510-
return toDashboardNodeType({
511-
...widget,
512-
...options
513-
});
514-
}
519+
// An entry here names no family and is no component node (the passthrough
520+
// above took those): stale metadata, drawn as the labelled placeholder, as
521+
// `DashboardRenderer` draws it.
515522
return unsupportedWidgetSchema(widgetType);
516523
}, [resolveSeriesLabel]);
517524

@@ -613,33 +620,31 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
613620
// ADR-0021 — a widget bound to a semantic-layer dataset renders
614621
// through the governed queryDataset path (DatasetWidget) instead of
615622
// the inline object-aggregate schema. Decided per widget AT THE
616-
// RENDER SITE, which is `DashboardRenderer`'s own mechanic
617-
// (`DashboardRenderer.tsx:524` for the predicate, `:849-851` for the
618-
// fork) rather than a second dispatch idiom invented here: this
619-
// surface had NO dataset path at all, so every current-shape widget
620-
// fell through `getComponentSchema` to the static-data branch and
621-
// drew `data: []` — a blank chart, an em-dash metric or an empty
622-
// table depending on the family, with no diagnostic (objectui#4614).
623-
// The cast names the ONE key it reads, rather than reaching for
624-
// `as any` the way the sibling does (`DashboardRenderer.tsx:524`):
625-
// the bundled DashboardWidget type gains `dataset` only after
626-
// objectui bumps @objectstack/spec, and `legacyRetiredWidget.ts`
627-
// already answers that same problem in this package by naming the
628-
// undeclared keys in a shape of its own — "what keeps `as any` out
629-
// of both call sites" (`legacyRetiredWidget.ts:64-80`). One key is
630-
// read here, so the shape is stated inline.
623+
// RENDER SITE, which is `DashboardRenderer`'s own mechanic rather
624+
// than a second dispatch idiom invented here: this surface had NO
625+
// dataset path at all, so every current-shape widget fell through
626+
// `getComponentSchema` to the static-data branch and drew
627+
// `data: []` — a blank chart, an em-dash metric or an empty table
628+
// depending on the family, with no diagnostic (objectui#4614).
629+
//
630+
// `dataset` is a widget key, read on the widget arm alone
631+
// (objectui#11598, N2 A), as `DashboardRenderer` reads it: a
632+
// component node in the slot (a `metric-card`) draws itself,
633+
// whatever else it carries. The read used to be a cast naming the
634+
// one key, so a `metric-card` the strict face refuses for its
635+
// `dataset` drew `DatasetWidget` in the card's place.
631636
//
632637
// Position relative to the objectui#4612 legacy sentinel (which
633638
// stays where it is, inside `getComponentSchema` BEFORE the dispatch
634639
// branches): the two conditions are MUTUALLY EXCLUSIVE by
635640
// construction, because `isLegacyRetiredWidget` returns false the
636-
// moment a widget carries `dataset` (`legacyRetiredWidget.ts:107`).
637-
// Neither can capture the other's widget, so their relative order
638-
// cannot change a verdict on either surface — the same arrangement
639-
// `DashboardRenderer` has carried since #4612, and the reason that
640-
// module states step 1 on its own terms instead of inheriting it
641-
// from a caller's fork (`legacyRetiredWidget.ts:97-102`).
642-
const datasetBound = !!(widget as { dataset?: unknown }).dataset;
641+
// moment a widget carries `dataset` (its step 1). Neither can capture
642+
// the other's widget, so their relative order cannot change a verdict
643+
// on either surface — the same arrangement `DashboardRenderer` has
644+
// carried since #4612, and the reason that module states step 1 on
645+
// its own terms instead of inheriting it from a caller's fork.
646+
const datasetWidget = !isSlotComponentEntry(widget) && widget.dataset ? widget : undefined;
647+
const datasetBound = datasetWidget !== undefined;
643648
// A `metric` widget renders its own card chrome ONLY in the inline
644649
// path (the `plugin-dashboard:metric` card, or a retired
645650
// `provider: 'object'` metric's placeholder, objectui#11525). A
@@ -688,35 +693,31 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
688693
<CardContent className="p-0 h-full">
689694
<div className={cn("h-full w-full overflow-auto p-4")}>
690695
{/*
691-
The fork itself, mirroring `DashboardRenderer.tsx:849-851`.
692-
`widget` is passed whole: DatasetWidget reads
696+
The fork itself, mirroring `DashboardRenderer`'s.
697+
The widget arm is passed whole: DatasetWidget reads
693698
`widget.filter` and forwards it to the query as
694699
`runtimeFilter`, so an authored per-widget filter still
695-
applies. The sibling wraps it as `effectiveWidget` only
696-
to merge in the dashboard FILTER BAR's scoped filter,
700+
applies. The sibling copies it as its `datasetWidget`
701+
only to merge in the dashboard FILTER BAR's scoped filter,
697702
which this surface does not have — there is no
698703
`scopedFilter` here to merge, so re-creating the wrapper
699704
would state a dependency that does not exist.
700705
701706
This is the ONLY fork: the self-contained branch above
702707
cannot be reached by a dataset-bound widget, since
703-
`isSelfContained` now requires `!datasetBound`. The
704-
sibling carries a second, identical fork inside its
705-
self-contained branch (`:820-822`) that is unreachable
706-
for that same reason; the reachable mechanics are what
707-
is mirrored here, not the stranded limb.
708+
`isSelfContained` now requires `!datasetBound`.
708709
*/}
709-
{datasetBound
710+
{datasetWidget
710711
? <DatasetWidget
711-
widget={widget}
712+
widget={datasetWidget}
712713
dataSource={dataSource}
713714
/* objectui#8889 — dispatch site 2 of 2, and the half that
714715
objectui#4614 exists to stop anyone from forgetting: the
715716
sibling passing this alone would fix one surface and leave
716717
this one silently unchanged. `?? null` says "a surface
717718
resolved it, to nothing", which is NOT the same as the
718719
prop being absent — see the prop's docblock. */
719-
subCaption={tWidgetSubCaption(widget) ?? null}
720+
subCaption={tWidgetSubCaption(datasetWidget) ?? null}
720721
/>
721722
: <SchemaRenderer schema={componentSchema} />}
722723
</div>

0 commit comments

Comments
 (0)