From ff6ec24b7accfd74e74bc71b60d2316d7275ef2e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 10:50:14 +0000 Subject: [PATCH] feat(plugin-charts): declare `drillDown` as an ObjectChart registry input (framework#5022) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ObjectChart` has read `schema.drillDown` all along — it is what opens the drawer of underlying records when a segment is clicked — but the prop was declared in neither the protocol nor this registry entry. The manifest the SDUI save gate validates page JSX against is built verbatim from these `inputs`, so an author writing the drill got an `unknown-prop` diagnostic for a prop that works. framework#5022 declares the protocol half (`ChartDrillDownSchema`, published on the react-tier `` contract); this is the renderer half. The published input describes the six keys the spec declares — `enabled`, `filter`, `title`, `target` ('drawer' | 'dialog'), `columns`, `maxRows` — and deliberately NOT the wider `DrillDownConfig` this repo shares with the table / pivot / metric widgets. `ObjectChart` reads none of `mode` / `report` / `view` / `sort`, and does not implement `target: 'navigate'` (it falls through to the drawer). Advertising them in the designer palette would re-open the gap framework#5022 just closed, one layer down. Two of those keys are read by no renderer at all — objectui#3354. The untyped `(schema as any).drillDown` becomes a typed `DrillDownConfig` read. Narrowing it further to the spec's `ChartDrillDown` is a TODO on the version pin: `@objectstack/spec` is pinned at `^17.0.0-rc.2` here and the declaration lands in the next rc, so the import would not compile today — and re-declaring the shape locally is exactly the fork that would let the two drift. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 --- .../object-chart-drilldown-declared-input.md | 11 ++++++ packages/plugin-charts/src/ObjectChart.tsx | 34 ++++++++++++++++++- packages/plugin-charts/src/index.test.ts | 32 +++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 .changeset/object-chart-drilldown-declared-input.md diff --git a/.changeset/object-chart-drilldown-declared-input.md b/.changeset/object-chart-drilldown-declared-input.md new file mode 100644 index 0000000000..19b132420e --- /dev/null +++ b/.changeset/object-chart-drilldown-declared-input.md @@ -0,0 +1,11 @@ +--- +'@object-ui/plugin-charts': minor +--- + +`` declares `drillDown` as a registry input, so the SDUI save gate treats the segment drill as a contract prop instead of an unknown one (framework#5022). + +The component has read `schema.drillDown` all along — it is what opens the drawer of underlying records when you click a bar or a slice — but the prop was declared in neither the protocol nor this registry entry. The manifest the save gate validates page JSX against is built verbatim from these `inputs`, so an author who wrote the drill got an `unknown-prop` diagnostic for a prop that works. `@objectstack/spec` now declares the shape (`ChartDrillDownSchema`, published on the react-tier `` contract); this is the renderer half. + +The published input describes the six keys the spec declares — `enabled`, `filter`, `title`, `target` (`'drawer' | 'dialog'`), `columns`, `maxRows` — and deliberately not the wider `DrillDownConfig` this repo shares with the table / pivot / metric widgets: `ObjectChart` reads none of `mode` / `report` / `view` / `sort`, and does not implement `target: 'navigate'` (it renders the drawer instead — objectui#3354). + +The untyped `(schema as any).drillDown` read is now typed as `DrillDownConfig`. Narrowing it to the spec's `ChartDrillDown` is left as a TODO on the version pin: `@objectstack/spec` is pinned at `^17.0.0-rc.2` here and the declaration lands in the next rc, and re-declaring the shape locally would be the fork that lets the two drift. diff --git a/packages/plugin-charts/src/ObjectChart.tsx b/packages/plugin-charts/src/ObjectChart.tsx index c6dfcf1938..af8422251f 100644 --- a/packages/plugin-charts/src/ObjectChart.tsx +++ b/packages/plugin-charts/src/ObjectChart.tsx @@ -6,6 +6,7 @@ import { ComponentRegistry, extractRecords, computeDrillFilter, isDrillEnabled, import { Sheet, SheetContent, SheetHeader, SheetTitle, Dialog, DialogContent, DialogHeader, DialogTitle, RefreshIndicator, Button, ChartSkeleton } from '@object-ui/components'; import { AlertCircle, ArrowUpRight } from 'lucide-react'; import { useSafeFieldLabel, useSafeTranslate } from '@object-ui/i18n'; +import type { DrillDownConfig } from '@object-ui/types'; /** * Humanize a snake_case or kebab-case string into Title Case. @@ -599,7 +600,26 @@ export const ObjectChart = (props: any) => { // filtered by the click context (category → groupBy field). The drilled // table is rendered via SchemaRenderer + the registered "object-data-table" // component (provided by plugin-dashboard). - const drillDown = (schema as any).drillDown; + // + // This read used to be `(schema as any).drillDown`, and framework#5022 is the + // issue that untyped read produced: the block drove a real capability that + // the PROTOCOL declared nowhere, so the spec's own migration prose ended up + // prescribing a key no schema had. The protocol now declares it — + // `ChartDrillDownSchema` in `@objectstack/spec/ui`, published on this block's + // react contract — and the registry `inputs` below carry it, so the SDUI save + // gate treats it as a contract prop instead of an unknown one. + // + // TODO(framework#5022): narrow this to the spec type once the pin advances. + // `@objectstack/spec` is pinned at `^17.0.0-rc.2` here and the declaration + // lands in the NEXT rc, so importing `ChartDrillDown` today would not compile + // against the published package. Re-declaring the shape locally instead is + // exactly the fork that would let the two drift, so the read stays on + // `DrillDownConfig` — the renderer-side type five widgets already share — + // until the bump. Note the two are not the same set on purpose: the spec type + // is the CHART subset (`enabled`/`filter`/`title`/`target`/`columns`/ + // `maxRows`), while this one also carries the table/pivot/metric keys + // (`mode`, `report`, the `navigate` target) that this component does not read. + const drillDown = (schema as { drillDown?: DrillDownConfig }).drillDown; const groupByField = schema.aggregate?.groupBy || schema.xAxisKey; // Build a label→raw map from the resolved chart data. resolveGroupByLabels @@ -828,5 +848,17 @@ ComponentRegistry.register('object-chart', ObjectChart, { { name: 'data', type: 'array', label: 'Data', description: 'Optional static data' }, { name: 'filter', type: 'array', label: 'Filter' }, { name: 'aggregate', type: 'object', label: 'Aggregate', description: 'Aggregation config: { field, function, groupBy }' }, + // framework#5022. The manifest built from these `inputs` is what the + // SDUI save gate validates a page's JSX against, so an undeclared prop + // is reported as `unknown-prop` — which is what an author writing the + // segment drill got, for a prop this component has always read. The + // spec now declares the shape (`ChartDrillDownSchema`), and this entry + // is the half that makes the gate agree. + // + // Only the six keys the spec declares are honoured here; the wider + // renderer-side `DrillDownConfig` (mode / report / view / sort, and the + // `navigate` target) belongs to the table/pivot/metric widgets, and two + // of its keys are read by nothing at all — objectui#3354. + { name: 'drillDown', type: 'object', label: 'Drill-down', description: "Segment drill config: { enabled?, filter?, title?, target?: 'drawer' | 'dialog', columns?, maxRows? }. Present = on; {} is enough. Clicking a segment opens the underlying records filtered by the clicked category." }, ] }); diff --git a/packages/plugin-charts/src/index.test.ts b/packages/plugin-charts/src/index.test.ts index 62b76e6306..3cd38b48c4 100644 --- a/packages/plugin-charts/src/index.test.ts +++ b/packages/plugin-charts/src/index.test.ts @@ -136,3 +136,35 @@ describe('Plugin Charts', () => { }); }); }); + +// framework#5022 — the segment drill is a CONTRACT prop now, on both sides. +// +// The SDUI save gate validates a page's JSX against a manifest built from these +// registry `inputs` (`manifestFromConfigs` copies them verbatim), so a prop that +// is missing here is reported as `unknown-prop` — which is what an author +// writing `drillDown` got, for a prop `ObjectChart` has read all along. The spec +// half is `ChartDrillDownSchema`; this is the half that makes the gate agree. +describe('object-chart — drillDown is a declared input (framework#5022)', () => { + const inputs = () => ComponentRegistry.getConfig('object-chart')?.inputs ?? []; + + it('declares drillDown, so the save gate stops calling it unknown', () => { + const drill = inputs().find((i: any) => i.name === 'drillDown'); + expect(drill, 'the registry must publish the prop the renderer reads').toBeDefined(); + expect(drill?.type).toBe('object'); + }); + + it('describes the SIX keys the spec declares — not the wider renderer union', () => { + // objectui's own `DrillDownConfig` is shared with the table/pivot/metric + // widgets and carries `mode` / `report` / `view` / `sort` and a `navigate` + // target. ObjectChart reads none of them, and two are read by no renderer + // at all (objectui#3354), so advertising them here would re-open the gap + // framework#5022 closed — one layer down, in the designer palette. + const d = String(inputs().find((i: any) => i.name === 'drillDown')?.description ?? ''); + for (const key of ['enabled', 'filter', 'title', 'target', 'columns', 'maxRows']) { + expect(d, `the declared key ${key} must be described`).toContain(key); + } + for (const key of ['mode', 'report', 'navigate']) { + expect(d, `${key} is another widget's key — it must not be advertised here`).not.toContain(key); + } + }); +});