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
11 changes: 11 additions & 0 deletions .changeset/object-chart-drilldown-declared-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@object-ui/plugin-charts': minor
---

`<ObjectChart>` 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 `<ObjectChart>` 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.
34 changes: 33 additions & 1 deletion packages/plugin-charts/src/ObjectChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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." },
]
});
32 changes: 32 additions & 0 deletions packages/plugin-charts/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
});
Loading