From ea8ee30db8e8ef2faa106d879e689d43181c4f38 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 19:40:33 +0000 Subject: [PATCH 1/5] fix(types,plugin-dashboard): object-pivot refuses drillDown.mode on its prop type `object-pivot` takes its own drill shape, `ObjectPivotDrillDownConfig`: the shared `DrillDownConfig` with `mode?: never`. Every pivot click point is an aggregated bucket, so the block always drills through and never read `mode`. The `DrillDownConfig.mode` docblock now names only the block that reads the key, `object-data-table`. Pins, compile-time: the per-block shapes for object-pivot and object-chart in @object-ui/types, and ObjectPivotTable's schema.drillDown prop in @object-ui/plugin-dashboard. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN --- .../plugin-dashboard/src/ObjectPivotTable.tsx | 14 ++- ...PivotTable.drillDownRefusal-10685.test.tsx | 75 +++++++++++++ .../drill-down-per-block-10685.test.ts | 105 ++++++++++++++++++ packages/types/src/data-display.ts | 63 ++++++++--- packages/types/src/index.ts | 1 + 5 files changed, 242 insertions(+), 16 deletions(-) create mode 100644 packages/plugin-dashboard/src/__tests__/ObjectPivotTable.drillDownRefusal-10685.test.tsx create mode 100644 packages/types/src/__tests__/drill-down-per-block-10685.test.ts diff --git a/packages/plugin-dashboard/src/ObjectPivotTable.tsx b/packages/plugin-dashboard/src/ObjectPivotTable.tsx index b37d1993cc..eeba53e84a 100644 --- a/packages/plugin-dashboard/src/ObjectPivotTable.tsx +++ b/packages/plugin-dashboard/src/ObjectPivotTable.tsx @@ -14,7 +14,7 @@ import { Skeleton, cn } from '@object-ui/components'; import { PivotTable } from './PivotTable'; import { DrillDownDrawer } from './DrillDownDrawer'; import { resolveFilterPlaceholders } from './utils'; -import type { PivotTableSchema } from '@object-ui/types'; +import type { ObjectPivotDrillDownConfig, PivotTableSchema } from '@object-ui/types'; /** * Shared empty fallback for the resolved row list (objectui#4629). @@ -95,6 +95,14 @@ export interface ObjectPivotTableProps { // takes `unknown` and routes every shape through the repo's single filter // sink, so the drill is correct for both arms whatever this key is typed. filter?: any; + /** + * This block's drill-down shape, not the shared `DrillDownConfig` that + * `PivotTableSchema` carries: `mode` is refused by name + * (`ObjectPivotDrillDownConfig`, objectui#10685, applying objectui#9002's + * ruling B). Every click point on a pivot is an aggregated bucket, so it + * always drills through; there is no row for `mode` to open as a record. + */ + drillDown?: ObjectPivotDrillDownConfig; }; dataSource?: any; className?: string; @@ -302,7 +310,7 @@ export const ObjectPivotTable: React.FC = ({ schema, data const rowFieldLabel = schema.rowField ? fieldNameLabels[schema.rowField] : undefined; // --- Drill-down wiring --------------------------------------------------- - const drillDown = (schema as any).drillDown; + const drillDown = schema.drillDown; const handleDrillDown = isDrillEnabled(drillDown) ? (event: DrillEvent) => setDrillEvent(event) @@ -350,7 +358,7 @@ export const ObjectPivotTable: React.FC = ({ schema, data dataSource={dataSource} columns={drillDown?.columns} maxRows={drillDown?.maxRows} - report={(drillDown as any)?.report} + report={drillDown?.report} /> ); }; diff --git a/packages/plugin-dashboard/src/__tests__/ObjectPivotTable.drillDownRefusal-10685.test.tsx b/packages/plugin-dashboard/src/__tests__/ObjectPivotTable.drillDownRefusal-10685.test.tsx new file mode 100644 index 0000000000..e374986354 --- /dev/null +++ b/packages/plugin-dashboard/src/__tests__/ObjectPivotTable.drillDownRefusal-10685.test.tsx @@ -0,0 +1,75 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * `object-pivot` — `drillDown.mode` is refused on the prop type an author writes + * against (objectui#10685, applying objectui#9002's ruling B to the sibling + * blocks of `object-metric`). + * + * `ObjectPivotDrillDownConfig` in `@object-ui/types` declares the per-block + * shape, and its own pin lives beside it + * (`drill-down-per-block-10685.test.ts`). This file pins the other half: that + * the component this package PUBLISHES takes that shape on `schema.drillDown`, + * and not the shared `DrillDownConfig` that `PivotTableSchema` carries. The + * refusal is worth nothing if the widget's prop still admits the shared type, + * and that is a one-line regression in this package the `@object-ui/types` pin + * cannot see. + * + * Read through the package entry (`../index`), so the type under test is the + * one a consumer of `@object-ui/plugin-dashboard` reaches. + * + * ## These assertions are compile-time only + * + * Judged by `tsc -p tsconfig.test.json`, the second leg of this package's + * `type-check` script, which resolves `@object-ui/types` through its BUILT + * `.d.ts`. Vitest strips types; the `expect` only keeps this a collected suite. + * Runtime behaviour did not change, so nothing here asserts any. + */ + +import { describe, it, expect } from 'vitest'; +import type { DrillDownConfig } from '@object-ui/types'; +import { ObjectPivotTable } from '../index'; + +describe('object-pivot: drillDown.mode is refused on the prop type (objectui#10685)', () => { + it('is pinned at compile time', () => { + const base = { + type: 'pivot' as const, + objectName: 'deal', + rowField: 'stage', + columnField: 'source', + valueField: 'amount', + data: [] as Record[], + }; + + // @ts-expect-error `drillDown.mode` is refused on `object-pivot` (objectui#10685). + const withMode = ; + const shared: DrillDownConfig = { enabled: true, mode: 'record' }; + // @ts-expect-error a shared config that may carry `mode` is not the pivot's drill shape. + const handedAcross = ; + + // Accept control: the members this block reads still compile. + const live = ( + + ); + + expect([withMode, handedAcross, live]).toHaveLength(3); + }); +}); diff --git a/packages/types/src/__tests__/drill-down-per-block-10685.test.ts b/packages/types/src/__tests__/drill-down-per-block-10685.test.ts new file mode 100644 index 0000000000..bcc8e06345 --- /dev/null +++ b/packages/types/src/__tests__/drill-down-per-block-10685.test.ts @@ -0,0 +1,105 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Each drill block refuses, by name, the `DrillDownConfig` members it can + * never honour (objectui#10685, applying objectui#9002's ruling B, a per-block + * refusal, to the sibling blocks of `object-metric`, whose pin is + * `object-metric-drill-down-config-9002.test.ts`). + * + * One `describe` per block. Each refuses its members one per line, so each + * `@ts-expect-error` owns exactly one error, and each carries a control that + * compiles: the members the block DOES read. + * + * - `object-pivot`: `mode`. Every pivot click point is an aggregated bucket, + * so the pivot always drills through; there is no row to open as a record. + * - `object-chart`: `mode` and `report`. Its drill shape is the spec's + * `ChartDrillDown`, the chart subset that declares neither (bound by + * objectui#8885, whose pin holds that binding with `Equal`). A chart segment + * is an aggregated bucket too, and the chart's own drawer renders a record + * list, never a report. This half was already refused before this card, by + * omission from the spec type; it is pinned here so each block's refused + * set sits in one place. + * + * ## These assertions are compile-time only + * + * Judged by `tsc -p tsconfig.test.json`, the third leg of this package's + * `type-check` script. Vitest strips types, so the `expect` at the bottom only + * keeps each block a collected suite. A `@ts-expect-error` line whose error + * disappears is itself an error (TS2578), which is what lets each refusal fail. + */ + +import { describe, it, expect } from 'vitest'; +// The per-block shapes through the module the published +// `@object-ui/types/data-display` subpath serves; the chart through the barrel. +import type { DrillDownConfig, ObjectPivotDrillDownConfig } from '../data-display'; +import type { ObjectChartSchema } from '../index'; + +type Assert = T; +type Equal = (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 ? true : false; +type IsAny = 0 extends 1 & T ? true : false; + +describe('object-pivot refuses drillDown.mode (objectui#10685)', () => { + it('is pinned at compile time', () => { + // Keeps the refusals below from passing vacuously: an `any` shape would + // accept `mode` and leave each `@ts-expect-error` unused for the wrong reason. + type _PivotShapeIsReal = Assert, false>>; + + // @ts-expect-error `mode` is refused on `object-pivot` (objectui#10685). + const withMode: ObjectPivotDrillDownConfig = { enabled: true, mode: 'filter' }; + const shared: DrillDownConfig = { enabled: true, mode: 'record' }; + // @ts-expect-error a shared config that may carry `mode` is not a pivot drill config. + const handedAcross: ObjectPivotDrillDownConfig = shared; + + // Control: every member the pivot reads still compiles, `report` included. + const live: ObjectPivotDrillDownConfig = { + enabled: true, + filter: { stage: '${event.rowKey}' }, + title: '${event.rowLabel} × ${event.colLabel}', + target: 'navigate', + columns: ['name', 'amount'], + maxRows: 25, + report: { name: 'won_deals_by_owner' }, + }; + // Still a `DrillDownConfig`, so `computeDrillFilter` and friends take it. + const liveAsShared: DrillDownConfig = live; + + // Control: the shared type keeps `mode` for `object-data-table`. + const table: DrillDownConfig = { enabled: true, mode: 'record' }; + + expect([withMode, handedAcross, liveAsShared, table]).toHaveLength(4); + }); +}); + +describe('object-chart refuses drillDown.mode and drillDown.report (objectui#10685)', () => { + it('is pinned at compile time', () => { + type _ChartDrillIsReal = Assert>, false>>; + + const base = { type: 'object-chart', chartType: 'bar', objectName: 'opportunity' } as const; + + // @ts-expect-error `mode` is not a chart drill member: a segment always drills through. + const withMode: ObjectChartSchema = { ...base, drillDown: { enabled: true, mode: 'record' } }; + // @ts-expect-error `report` is not a chart drill member: the chart's drawer lists records. + const withReport: ObjectChartSchema = { ...base, drillDown: { enabled: true, report: { name: 'pipeline' } } }; + + // Control: every member the chart reads still compiles. + const live: ObjectChartSchema = { + ...base, + drillDown: { + enabled: true, + filter: { stage: '${event.category}' }, + title: '${event.category}', + target: 'navigate', + columns: ['name', 'amount'], + maxRows: 50, + }, + }; + + expect([withMode, withReport, live]).toHaveLength(3); + }); +}); diff --git a/packages/types/src/data-display.ts b/packages/types/src/data-display.ts index a8934d1c74..57be6df46a 100644 --- a/packages/types/src/data-display.ts +++ b/packages/types/src/data-display.ts @@ -2263,19 +2263,21 @@ export interface DrillDownConfig { /** Master switch. Set to true (or supply any other field) to enable. */ enabled?: boolean; /** - * Which drill interaction the widget performs: - * - * - `'filter'` (default) — **drill-through**: the click point is an - * aggregated bucket (pivot cell, chart segment, KPI). The drawer lists - * the underlying records filtered by the click context. Used by charts, - * pivot tables and metric cards. - * - `'record'` — **drill-to-record**: the click point already *is* a single - * record (a row in a table / list widget). The drawer shows that record's - * detail instead of a filtered list. This is the default for table / list - * widgets, mirroring Salesforce list-view row → record and Power BI's - * "see records" row interaction. - * - * When omitted the consuming widget picks the natural default for its type. + * Which drill interaction a row click performs. Read by `object-data-table` + * alone, on its row click (objectui#10685): + * + * - `'record'` (the default) — **drill-to-record**: the clicked row already + * *is* a single record, so the drawer shows that record's detail. This + * mirrors Salesforce list-view row → record and Power BI's "see records" + * row interaction. + * - `'filter'` — **drill-through**: the interaction of a click point that is + * an aggregated bucket, listing the records behind it filtered by the click + * context. A table row is not such a bucket, and `object-data-table` has no + * drill-through arm, so there this value turns the row drill off. + * + * A block whose every click point is an aggregate always drills through, so + * it has nothing for this key to choose and refuses it on its own drill shape + * ({@link ObjectPivotDrillDownConfig}, {@link ObjectMetricDrillDownConfig}). */ mode?: 'filter' | 'record'; /** @@ -2384,6 +2386,41 @@ export interface ObjectMetricDrillDownConfig extends DrillDownConfig { mode?: never; } +/** + * The `object-pivot` block's drill-down shape: {@link DrillDownConfig} with + * `mode` REFUSED BY NAME (objectui#10685, which applies objectui#9002's ruling + * B, a per-block refusal, to the sibling blocks of `object-metric`). + * + * Every click point on a pivot is an aggregated bucket: a cell, a row or + * column header, or a total, and never a single record. So a pivot always + * drills through to the records behind the clicked value, and `mode`, which + * chooses drill-to-record for a clicked ROW, has nothing to choose here. Every + * other member is read: `enabled` by `isDrillEnabled`, `filter` by + * `computeDrillFilter`, `title` by `resolveDrillTitle`, and `target`, + * `columns`, `maxRows` and `report` by the `DrillDownDrawer` the block opens. + * + * A `?: never` tombstone rather than an omission, for the reason + * {@link ObjectMetricDrillDownConfig} gives: it also refuses a value that is + * not a fresh literal, and it carries the reason into the error an author (or + * an AI) sees. + * + * ⚠️ This is a TypeScript declaration, so it refuses the key where an author + * types against it (`ObjectPivotTable`'s `schema.drillDown`). Neither + * `object-pivot` nor `PivotTableSchema` has a zod mirror, so a stored JSON + * config is checked by no validator and reaches the block unchanged. + */ +export interface ObjectPivotDrillDownConfig extends DrillDownConfig { + /** + * REFUSED BY NAME on `object-pivot` (objectui#10685). A pivot's click point + * is always an aggregated bucket, so it always drills through to the records + * behind it, and there is no row to open as a record. `mode` applies on + * `object-data-table`, whose row click reads it. + * + * @deprecated Not a member `object-pivot` reads. A pivot always lists the records behind the clicked value. + */ + mode?: never; +} + /** * Pivot table (cross-tabulation) component * diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index f719db2174..5c4ad149e3 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -252,6 +252,7 @@ export type { PivotTableSchema, DrillDownConfig, ObjectMetricDrillDownConfig, + ObjectPivotDrillDownConfig, TimelineEvent, TimelineScale, TimelineSchema, From 916bc17896144d883024d6d7efbb087ea7d18b52 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 19:46:31 +0000 Subject: [PATCH 2/5] docs(changeset): object-pivot drillDown.mode refusal; date the 7352 entry's mode reading Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN --- .changeset/10685-drilldown-per-block.md | 30 +++++++++++++++++++++ .changeset/7352-drill-down-config-mirror.md | 12 +++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 .changeset/10685-drilldown-per-block.md diff --git a/.changeset/10685-drilldown-per-block.md b/.changeset/10685-drilldown-per-block.md new file mode 100644 index 0000000000..25adf73fcd --- /dev/null +++ b/.changeset/10685-drilldown-per-block.md @@ -0,0 +1,30 @@ +--- +'@object-ui/types': patch +'@object-ui/plugin-dashboard': patch +--- + +`object-pivot` refuses `drillDown.mode` on its prop type (objectui#10685) + +Refused at the TypeScript door only. A stored JSON pivot config carrying the key is still +accepted and ignored at render, as it was before: neither `object-pivot` nor +`PivotTableSchema` has a zod mirror, so no validator in this repository reads the members +of a pivot's `drillDown`. + +The key never had a read site on a pivot. Every pivot click point is an aggregated bucket +(a cell, a row or column header, or a total), so the block always drills through to the +records behind the clicked value, and `mode`, which chooses drill-to-record for a clicked +row, had nothing to choose. It still type-checked on `ObjectPivotTable`'s +`schema.drillDown` and then did nothing, with no diagnostic. + +- `@object-ui/types` adds `ObjectPivotDrillDownConfig`, published on the root entry + `@object-ui/types` and on the `@object-ui/types/data-display` subpath: `DrillDownConfig` + with a `mode?: never` tombstone whose docblock names the block that does read the key, + `object-data-table`. The shared `DrillDownConfig` keeps `mode` for that block, and its + `mode` docblock now names only that block instead of charts, pivot tables and metric + cards, none of which read it. +- `@object-ui/plugin-dashboard` types `ObjectPivotTable`'s `schema.drillDown` with it, and + the component reads its drill config through that type instead of through `any`. + +TSX code that passes `mode` inside `ObjectPivotTable`'s `schema.drillDown` now fails to +compile, and so does a value typed as the shared `DrillDownConfig`. Delete the key: the +pivot always drilled through. Runtime behaviour is unchanged. diff --git a/.changeset/7352-drill-down-config-mirror.md b/.changeset/7352-drill-down-config-mirror.md index b3d40dd2e0..56f13e4de8 100644 --- a/.changeset/7352-drill-down-config-mirror.md +++ b/.changeset/7352-drill-down-config-mirror.md @@ -37,5 +37,13 @@ Accept-set change on the published validator, stated plainly: `DrillDownConfigSchema` is deliberately NOT `@objectstack/spec/ui`'s `ChartDrillDownSchema`: that object models the chart-only subset strictly and -refuses `mode` and `report` by name, both of which are live keys on the table / -pivot / metric widgets that share `DrillDownConfig`. +refuses `mode` and `report` by name, both of which were, at this change, keys +`DrillDownConfig` declared for the table / pivot / metric widgets that share it. + +⚠️ **Dated note, 2026-09-25 — `mode` is read by the table alone — objectui#10685.** +The blocks now take per-block drill shapes. `mode` is read only by +`object-data-table`, on its row click, and is refused by name on `object-pivot` +(`ObjectPivotDrillDownConfig`, objectui#10685) and on `object-metric` +(`ObjectMetricDrillDownConfig`, objectui#9002), whose click points are always +aggregates. `report` is read by `object-pivot` and `object-metric`, through the +drawer they open. The rest of this entry is kept as the reading of this change. From e9303d0536873614743c3ec47e131786a0972b9c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 20:27:43 +0000 Subject: [PATCH 3/5] fix(types): object-data-table refuses drillDown.filter, .maxRows, .report and target navigate on both doors `ObjectDataTableSchema.drillDown` takes the block's own shape, `ObjectDataTableDrillDownConfig`: the shared `DrillDownConfig` with `filter`, `maxRows` and `report` as `?: never` tombstones and `target` narrowed to 'drawer' | 'dialog'. The zod mirror's `drillDown` extends the shared mirror the same way, refusing each member by name, so `objectui validate` refuses a stored config carrying one. A row drills to the one record it already is; none of the four was ever read. The DrillDownConfig class docblock and the declared-keys pin comment no longer say every block honours every target arm. The 6576 anchor pins follow the new shape, the 7352 mirror pin's table leg runs over the values the table takes, and the 6576 changeset carries a dated note. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN --- .changeset/10685-drilldown-per-block.md | 53 +++++---- .changeset/6576-widget-schema-anchors.md | 10 +- .../ObjectDataTable.schemaAnchor-6576.test.ts | 9 +- .../drill-down-config-declared-keys.test.ts | 10 +- .../drill-down-config-mirror-7352.test.ts | 25 ++++- .../drill-down-per-block-10685.test.ts | 101 ++++++++++++++++-- .../widget-schema-anchors-6576.test.ts | 11 +- packages/types/src/data-display.ts | 79 +++++++++++++- packages/types/src/index.ts | 1 + packages/types/src/objectql.ts | 11 +- packages/types/src/zod/objectql.zod.ts | 42 +++++++- 11 files changed, 305 insertions(+), 47 deletions(-) diff --git a/.changeset/10685-drilldown-per-block.md b/.changeset/10685-drilldown-per-block.md index 25adf73fcd..9b1d5d50e7 100644 --- a/.changeset/10685-drilldown-per-block.md +++ b/.changeset/10685-drilldown-per-block.md @@ -3,28 +3,41 @@ '@object-ui/plugin-dashboard': patch --- -`object-pivot` refuses `drillDown.mode` on its prop type (objectui#10685) +`object-data-table` refuses `drillDown.filter`, `.maxRows`, `.report` and `target: 'navigate'`, and `object-pivot` refuses `drillDown.mode` (objectui#10685) -Refused at the TypeScript door only. A stored JSON pivot config carrying the key is still -accepted and ignored at render, as it was before: neither `object-pivot` nor -`PivotTableSchema` has a zod mirror, so no validator in this repository reads the members -of a pivot's `drillDown`. +**Stored JSON: `objectui validate` now refuses an `object-data-table` config that carries +`drillDown.filter`, `drillDown.maxRows`, `drillDown.report` or `drillDown.target: 'navigate'`.** +Each refusal names the key and the blocks that do read it. None of the four was ever read by +this block. Its row drills to the one record it already is, so a drilled list's filter, row +cap and report had nothing to act on, and `'navigate'` was drawn as a drawer. Before this +change the table's zod mirror took the shared `DrillDownConfigSchema` and accepted all four. +Delete the key; for the drill target, write `'drawer'` or `'dialog'`. `enabled`, `mode`, +`title` and `columns` are accepted exactly as before. -The key never had a read site on a pivot. Every pivot click point is an aggregated bucket -(a cell, a row or column header, or a total), so the block always drills through to the -records behind the clicked value, and `mode`, which chooses drill-to-record for a clicked -row, had nothing to choose. It still type-checked on `ObjectPivotTable`'s -`schema.drillDown` and then did nothing, with no diagnostic. +`object-pivot` is refused at the TypeScript door only. A stored JSON pivot config carrying +`mode` is still accepted and ignored at render, as it was before: neither `object-pivot` nor +`PivotTableSchema` has a zod mirror, so no validator in this repository reads the members of +a pivot's `drillDown`. `mode` never had a read site on a pivot. Every pivot click point is an +aggregated bucket (a cell, a row or column header, or a total), so the block always drills +through, and `mode`, which chooses drill-to-record for a clicked row, had nothing to choose. -- `@object-ui/types` adds `ObjectPivotDrillDownConfig`, published on the root entry - `@object-ui/types` and on the `@object-ui/types/data-display` subpath: `DrillDownConfig` - with a `mode?: never` tombstone whose docblock names the block that does read the key, - `object-data-table`. The shared `DrillDownConfig` keeps `mode` for that block, and its - `mode` docblock now names only that block instead of charts, pivot tables and metric +- `@object-ui/types` adds two per-block shapes beside `ObjectMetricDrillDownConfig`, both + published on the root entry `@object-ui/types` and on the `@object-ui/types/data-display` + subpath. Each tombstone's docblock names the blocks that do read the key. + - `ObjectDataTableDrillDownConfig`: `DrillDownConfig` with `filter?: never`, + `maxRows?: never` and `report?: never`, and `target?: 'drawer' | 'dialog'`. + `ObjectDataTableSchema.drillDown` is typed with it, and the `drillDown` member of its zod + mirror (`@object-ui/types/zod`) refuses the same members by name. + - `ObjectPivotDrillDownConfig`: `DrillDownConfig` with `mode?: never`. +- The shared `DrillDownConfig` keeps every member for the blocks that read them. Its `mode` + docblock now names only `object-data-table` instead of charts, pivot tables and metric cards, none of which read it. -- `@object-ui/plugin-dashboard` types `ObjectPivotTable`'s `schema.drillDown` with it, and - the component reads its drill config through that type instead of through `any`. +- `@object-ui/plugin-dashboard` types `ObjectPivotTable`'s `schema.drillDown` with + `ObjectPivotDrillDownConfig`, and the component reads its drill config through that type + instead of through `any`. `ObjectDataTable`'s prop is `ObjectDataTableSchema`, so it takes + the table's shape with it. -TSX code that passes `mode` inside `ObjectPivotTable`'s `schema.drillDown` now fails to -compile, and so does a value typed as the shared `DrillDownConfig`. Delete the key: the -pivot always drilled through. Runtime behaviour is unchanged. +TSX code now fails to compile in two places. The first is a refused member inside +`ObjectDataTableSchema.drillDown`, or inside `ObjectPivotTable`'s `schema.drillDown`. The +second is a value typed as the shared `DrillDownConfig` handed to either. Delete the key. +Runtime rendering is unchanged. diff --git a/.changeset/6576-widget-schema-anchors.md b/.changeset/6576-widget-schema-anchors.md index edd2fed16b..ba6e6cd87d 100644 --- a/.changeset/6576-widget-schema-anchors.md +++ b/.changeset/6576-widget-schema-anchors.md @@ -14,7 +14,15 @@ objectui#6914). BaseSchema`, beside the other `Object*Schema` declarations, with zod mirrors of the same names under `@object-ui/types/zod`. `ObjectDataTableSchema` declares the two keys the widget was reading behind casts — `drillDown` - (`DrillDownConfig`) and `onRowClick` — which no declaration carried before. + (`DrillDownConfig`, at this change) and `onRowClick` — which no declaration + carried before. + + ⚠️ **Dated note, 2026-09-25 — `drillDown` now takes the table's own shape — + objectui#10685.** `ObjectDataTableSchema.drillDown` is + `ObjectDataTableDrillDownConfig`, on both faces: `filter`, `maxRows` and + `report` are refused by name, and `target` is `'drawer'` or `'dialog'`. None of + the four was ever read by this block. The rest of this entry is kept as the + reading of this change. - `@object-ui/plugin-list`: the published `ObjectGalleryProps.schema` is `ObjectGallerySchema`. Its accept set WIDENS — every `BaseSchema` member is writable (`visibleWhen`, a real base member, was a compile error on the diff --git a/packages/plugin-dashboard/src/__tests__/ObjectDataTable.schemaAnchor-6576.test.ts b/packages/plugin-dashboard/src/__tests__/ObjectDataTable.schemaAnchor-6576.test.ts index 2dcc0c3aa6..18bbebc1c3 100644 --- a/packages/plugin-dashboard/src/__tests__/ObjectDataTable.schemaAnchor-6576.test.ts +++ b/packages/plugin-dashboard/src/__tests__/ObjectDataTable.schemaAnchor-6576.test.ts @@ -36,7 +36,7 @@ import { describe, it, expect } from 'vitest'; import type { ObjectDataTableProps } from '../ObjectDataTable'; -import type { BaseSchema, DrillDownConfig, ObjectDataTableSchema } from '@object-ui/types'; +import type { BaseSchema, ObjectDataTableDrillDownConfig, ObjectDataTableSchema } from '@object-ui/types'; type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; @@ -51,8 +51,11 @@ export type assertionSchemaExtendsBase = Expect<[Schema] extends [BaseSchema] ? * objectui#6914 — `Equal`, not `extends`: before the declaration both keys * resolved to `any` through the literal's index signature, and `any` satisfies * a one-way check. These were RED on the unmodified tree for exactly that reason. + * objectui#10685 re-pointed this row at the block's own drill shape: the shared + * `DrillDownConfig` with `filter` / `maxRows` / `report` refused and `target` + * narrowed, so the widget's prop refuses exactly what the schema refuses. */ -export type assertionDrillDownDeclared = Expect>; +export type assertionDrillDownDeclared = Expect>; /** * objectui#9799 widened the anchor's own declaration to an OPTIONAL second * parameter; this pin reads through `ObjectDataTableProps['schema']`, so it is @@ -91,7 +94,7 @@ describe('ObjectDataTableProps.schema — anchored to ObjectDataTableSchema (obj }; expect(node.drillDown?.mode).toBe('record'); - // @ts-expect-error — `enabled` is a boolean on DrillDownConfig; the index signature used to hide this. + // @ts-expect-error — `enabled` is a boolean on the drill shape; the index signature used to hide this. const wrongShape: Schema = { type: 'object-data-table', drillDown: { enabled: 'yes' } }; // @ts-expect-error — `onRowClick` is a function. const notAFunction: Schema = { type: 'object-data-table', onRowClick: 'toast' }; diff --git a/packages/types/src/__tests__/drill-down-config-declared-keys.test.ts b/packages/types/src/__tests__/drill-down-config-declared-keys.test.ts index de6e6f575e..c51dd2fa78 100644 --- a/packages/types/src/__tests__/drill-down-config-declared-keys.test.ts +++ b/packages/types/src/__tests__/drill-down-config-declared-keys.test.ts @@ -70,10 +70,12 @@ describe('DrillDownConfig declares no key that no renderer reads', () => { type _MaxRowsSurvives = Assert>; type _TargetSurvives = Assert>; - // `'navigate'` is delivered by every widget that shares this type now: - // `DrillDownDrawer.navigateOnly` for table / pivot / metric, and - // ObjectChart's own branch since objectui#3354. Narrowing the union would - // be a spec change, not a cleanup. + // `'navigate'` is delivered by every block whose drill shape admits it: + // `DrillDownDrawer.navigateOnly` for pivot / metric, and ObjectChart's own + // branch since objectui#3354. `object-data-table` drills to the one record + // its row already is, so its own shape narrows the arm away instead + // (`ObjectDataTableDrillDownConfig`, objectui#10685); the shared union stays + // whole. Narrowing it here would be a spec change, not a cleanup. type _TargetUnion = Assert< Equal, 'drawer' | 'dialog' | 'navigate'> >; diff --git a/packages/types/src/__tests__/drill-down-config-mirror-7352.test.ts b/packages/types/src/__tests__/drill-down-config-mirror-7352.test.ts index 0bfcd8debd..a5ba32a7aa 100644 --- a/packages/types/src/__tests__/drill-down-config-mirror-7352.test.ts +++ b/packages/types/src/__tests__/drill-down-config-mirror-7352.test.ts @@ -123,6 +123,19 @@ const ACCEPTED: Array<[string, DrillDownConfig]> = [ ['an inline report missing objectName, which the reference arm accepts', { report: { name: 'pipeline', columns: [] } }], ]; +/** + * objectui#10685 — the `object-data-table` mirror takes that block's OWN drill + * shape: the shared mirror with `filter`, `maxRows` and `report` refused by name + * and `target` narrowed to `'drawer'` / `'dialog'` + * (`drill-down-per-block-10685.test.ts` pins those refusals on both doors). So + * the table leg below runs over the ACCEPTED entries that carry none of them; + * the rest are that pin's refusals, not this ledger's acceptances. + */ +const TABLE_REFUSED_KEYS = ['filter', 'maxRows', 'report'] as const; +const TABLE_ACCEPTED = ACCEPTED.filter( + ([, value]) => !TABLE_REFUSED_KEYS.some((key) => key in value) && value.target !== 'navigate', +); + /** Each is a DECLARED key with a value outside its declared type. */ const REFUSED: Array<[string, unknown, string]> = [ ['enabled as a string', { enabled: 'yes' }, 'enabled'], @@ -166,16 +179,26 @@ describe('objectui#7352 — DrillDownConfigSchema is the zod mirror of DrillDown describe('objectui#7352 — both declaring mirrors read the key', () => { it('ChartSchema and ObjectDataTableSchema declare drillDown through the shared mirror', () => { + // The table's through its per-block extension of it (objectui#10685). expect(ChartSchema.shape.drillDown).toBeDefined(); expect(ObjectDataTableSchema.shape.drillDown).toBeDefined(); }); + it('the table leg still covers every value hosts synthesise for a table (objectui#10685)', () => { + // Non-vacuity for the filtered leg below: the blocks `DashboardRenderer`, + // `DrillDownDrawer` and `ObjectChart` write onto an `object-data-table`, and + // the drill tests' disabled and filter-mode blocks, all stay in it. + expect(TABLE_ACCEPTED.map(([label]) => label)).toEqual(expect.arrayContaining([ + 'the empty block', 'enabled', 'disabled', 'record mode', 'filter mode', 'record mode in a dialog', + ])); + }); + it.each(ACCEPTED)('a chart carrying %s validates', (_label, value) => { const r = safeValidateSchema(chart(value)); expect(r.success, r.success ? '' : JSON.stringify(r.error.issues, null, 2)).toBe(true); }); - it.each(ACCEPTED)('an object-data-table carrying %s validates', (_label, value) => { + it.each(TABLE_ACCEPTED)('an object-data-table carrying %s validates', (_label, value) => { const r = safeValidateSchema(table(value)); expect(r.success, r.success ? '' : JSON.stringify(r.error.issues, null, 2)).toBe(true); }); diff --git a/packages/types/src/__tests__/drill-down-per-block-10685.test.ts b/packages/types/src/__tests__/drill-down-per-block-10685.test.ts index bcc8e06345..8378a6ca6a 100644 --- a/packages/types/src/__tests__/drill-down-per-block-10685.test.ts +++ b/packages/types/src/__tests__/drill-down-per-block-10685.test.ts @@ -25,20 +25,30 @@ * list, never a report. This half was already refused before this card, by * omission from the spec type; it is pinned here so each block's refused * set sits in one place. + * - `object-data-table`: `filter`, `maxRows`, `report`, and `target: + * 'navigate'`. A row drills to the one record it already is, so a drilled + * list's filter, row cap and report have nothing to act on, and + * `'navigate'` (the object's list page) is the wrong destination. This is + * the one block with a zod door that reads its drill members + * (`safeValidateSchema`, which `objectui validate` runs), so it is pinned on + * both doors. * - * ## These assertions are compile-time only + * ## Two instruments * - * Judged by `tsc -p tsconfig.test.json`, the third leg of this package's - * `type-check` script. Vitest strips types, so the `expect` at the bottom only - * keeps each block a collected suite. A `@ts-expect-error` line whose error - * disappears is itself an error (TS2578), which is what lets each refusal fail. + * The `@ts-expect-error` lines are judged by `tsc -p tsconfig.test.json`, the + * third leg of this package's `type-check` script. Vitest strips types, so for + * those blocks the `expect` only keeps the block a collected suite. A + * `@ts-expect-error` line whose error disappears is itself an error (TS2578), + * which is what lets each refusal fail. The zod `describe` at the bottom is a + * RUNTIME pin, judged by vitest. */ import { describe, it, expect } from 'vitest'; // The per-block shapes through the module the published // `@object-ui/types/data-display` subpath serves; the chart through the barrel. -import type { DrillDownConfig, ObjectPivotDrillDownConfig } from '../data-display'; -import type { ObjectChartSchema } from '../index'; +import type { DrillDownConfig, ObjectDataTableDrillDownConfig, ObjectPivotDrillDownConfig } from '../data-display'; +import type { ObjectChartSchema, ObjectDataTableSchema } from '../index'; +import { safeValidateSchema } from '../zod/index.zod'; type Assert = T; type Equal = (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 ? true : false; @@ -103,3 +113,80 @@ describe('object-chart refuses drillDown.mode and drillDown.report (objectui#106 expect([withMode, withReport, live]).toHaveLength(3); }); }); + +describe('object-data-table refuses drillDown.filter, .maxRows, .report and target navigate (objectui#10685)', () => { + it('is pinned at compile time, on the per-block shape and on the schema the prop is anchored to', () => { + type _TableShapeIsReal = Assert, false>>; + // The anchored schema takes the per-block shape, not the shared one. + type _SchemaTakesTheTableShape = Assert< + Equal + >; + + // One refused member per line, so each directive owns exactly one error. + // @ts-expect-error `filter` is refused on `object-data-table` (objectui#10685). + const withFilter: ObjectDataTableDrillDownConfig = { enabled: true, filter: { stage: 'won' } }; + // @ts-expect-error `maxRows` is refused on `object-data-table` (objectui#10685). + const withMaxRows: ObjectDataTableDrillDownConfig = { enabled: true, maxRows: 25 }; + // @ts-expect-error `report` is refused on `object-data-table` (objectui#10685). + const withReport: ObjectDataTableDrillDownConfig = { enabled: true, report: { name: 'pipeline' } }; + // @ts-expect-error `target: 'navigate'` is refused on `object-data-table` (objectui#10685). + const withNavigate: ObjectDataTableDrillDownConfig = { enabled: true, target: 'navigate' }; + const shared: DrillDownConfig = { enabled: true, mode: 'record' }; + // @ts-expect-error a shared config that may carry any of them is not a table drill config. + const handedAcross: ObjectDataTableDrillDownConfig = shared; + + const base = { type: 'object-data-table', objectName: 'opportunity' } as const; + // @ts-expect-error the same refusal through the node an author writes. + const nodeWithFilter: ObjectDataTableSchema = { ...base, drillDown: { enabled: true, filter: { stage: 'won' } } }; + // @ts-expect-error the same refusal through the node an author writes. + const nodeWithNavigate: ObjectDataTableSchema = { ...base, drillDown: { enabled: true, target: 'navigate' } }; + + // Control: every member the block reads still compiles, `enabled` and `mode` first. + const live: ObjectDataTableDrillDownConfig = { + enabled: true, + mode: 'record', + title: 'Opportunity', + columns: ['name', 'amount'], + target: 'dialog', + }; + const liveNode: ObjectDataTableSchema = { ...base, drillDown: { enabled: false, mode: 'filter', target: 'drawer' } }; + // Still a `DrillDownConfig`, so `isDrillEnabled` takes it. + const liveAsShared: DrillDownConfig = live; + + expect([ + withFilter, withMaxRows, withReport, withNavigate, handedAcross, + nodeWithFilter, nodeWithNavigate, liveNode, liveAsShared, + ]).toHaveLength(9); + }); +}); + +describe('object-data-table: the zod door refuses the same members by name (objectui#10685)', () => { + const table = (drillDown: unknown) => ({ type: 'object-data-table', objectName: 'opportunity', drillDown }); + + it('accepts a config carrying every member the block reads', () => { + for (const drillDown of [ + {}, + { enabled: true, mode: 'record', title: 'Opportunity', columns: ['name', 'amount'], target: 'dialog' }, + { enabled: false, mode: 'filter', target: 'drawer' }, + ]) { + const r = safeValidateSchema(table(drillDown)); + expect(r.success, r.success ? '' : JSON.stringify(r.error.issues)).toBe(true); + } + }); + + it.each([ + ['filter', { filter: { stage: 'won' } }, 'invalid_type'], + ['maxRows', { maxRows: 25 }, 'invalid_type'], + ['report', { report: { name: 'pipeline' } }, 'invalid_type'], + ['target', { target: 'navigate' }, 'invalid_value'], + ] as const)('refuses drillDown.%s, and the issue names the key', (key, member, code) => { + const r = safeValidateSchema(table({ enabled: true, ...member })); + expect(r.success).toBe(false); + const issues = r.success ? [] : r.error.issues; + const mine = issues.filter((i) => i.path.join('.') === `drillDown.${key}`); + expect(mine, JSON.stringify(issues)).toHaveLength(1); + expect(mine[0].code).toBe(code); + // The named subject, not the wording: the message spells out the key it refuses. + expect(mine[0].message).toContain(`drillDown.${key}`); + }); +}); diff --git a/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts b/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts index 3c6e02853f..2d48b72a5d 100644 --- a/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts +++ b/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts @@ -65,7 +65,7 @@ import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import type { BaseSchema } from '../base.js'; -import type { DrillDownConfig } from '../data-display.js'; +import type { ObjectDataTableDrillDownConfig } from '../data-display.js'; import type { ObjectGallerySchema, ObjectDataTableSchema } from '../objectql.js'; import { ObjectGallerySchema as ObjectGalleryMirror, @@ -88,8 +88,13 @@ export type assertionGalleryExtendsBase = Expect>; export type assertionGalleryTypeIsRegistryKey = Expect>; export type assertionDataTableTypeIsRegistryKey = Expect>; -/** objectui#6914 — declared with the types measured at their prior homes. */ -export type assertionDrillDownDeclared = Expect>; +/** + * objectui#6914 — declared with the types measured at their prior homes. + * `drillDown` has since moved to this block's own shape, the shared + * `DrillDownConfig` with `filter` / `maxRows` / `report` refused and `target` + * narrowed (objectui#10685); `Equal` keeps it from drifting back to the shared type. + */ +export type assertionDrillDownDeclared = Expect>; /** * ⭐ STILL measured at its prior home, which is the point of leaving this pin * `Equal` rather than relaxing it: objectui#6576 declared `onRowClick` here with diff --git a/packages/types/src/data-display.ts b/packages/types/src/data-display.ts index 57be6df46a..5f2b1fb2c8 100644 --- a/packages/types/src/data-display.ts +++ b/packages/types/src/data-display.ts @@ -2246,9 +2246,15 @@ export type PivotAggregation = 'sum' | 'count' | 'avg' | 'min' | 'max'; * * ## Declared = delivered (objectui#3354) * - * Every key below is read by at least one of the five widgets that share this - * interface, and `target` is honoured by all of them. Two keys used to break - * that rule and were removed rather than left as authoring bait: + * Every key below is read by at least one block that takes this interface or a + * per-block shape derived from it. A block that can never honour a key refuses + * it by name on its own shape rather than accepting and dropping it + * ({@link ObjectMetricDrillDownConfig}, {@link ObjectPivotDrillDownConfig}, + * {@link ObjectDataTableDrillDownConfig}; objectui#9002, objectui#10685), so + * every `target` arm a block's shape admits is one that block honours: + * `object-data-table`'s shape admits `'drawer'` and `'dialog'` only. Two keys + * used to break that rule for every block and were removed rather than left as + * authoring bait: * * - `view?: string` — self-described as "reserved"; no renderer ever looked it * up, so the drawer rendered its inline `object-data-table` regardless. @@ -2421,6 +2427,73 @@ export interface ObjectPivotDrillDownConfig extends DrillDownConfig { mode?: never; } +/** + * The `object-data-table` block's drill-down shape: {@link DrillDownConfig} + * with `filter`, `maxRows` and `report` REFUSED BY NAME and `target` narrowed + * to its two in-place arms (objectui#10685, which applies objectui#9002's ruling + * B, a per-block refusal, to the sibling blocks of `object-metric`). + * + * A data table drills to the RECORD its clicked row already is: the row click + * opens that record in `RecordDetailDrawer`, which lists nothing. The three + * refused members each configure a drilled record LIST (the filter that scopes + * it, the cap on its rows, the report that replaces it), and this block has no + * list drill for them to apply to: `mode: 'filter'` turns its row drill off + * rather than drilling through. `target: 'navigate'` opens the object's full + * LIST page through the host's `openRecordList`, which is the wrong destination + * for one record, and the block used to draw it as a drawer instead. + * + * What the block reads: `enabled` by `isDrillEnabled`; `mode`, where `'record'` + * (the default) opens the row and `'filter'` turns the drill off; `title`, a + * non-template title that heads the record drawer; `columns`, the record + * drawer's field whitelist; and `target`, `'drawer'` or `'dialog'`. + * + * `?: never` tombstones for the reason {@link ObjectMetricDrillDownConfig} + * gives: they refuse a value that is not a fresh literal too, and they carry + * the reason into the error an author (or an AI) sees. + * + * Twin of the `drillDown` member of `ObjectDataTableSchema`'s zod mirror + * (`zod/objectql.zod.ts`), which refuses the same members by name, so a stored + * JSON config carrying one is refused at `objectui validate` as well. + */ +export interface ObjectDataTableDrillDownConfig extends DrillDownConfig { + /** + * REFUSED BY NAME on `object-data-table` (objectui#10685). A drill `filter` + * scopes a drilled record list, and this block drills to the one record its + * row already is. Drill filters apply on `object-chart` and `object-pivot`, + * which hand this member to `computeDrillFilter`. + * + * @deprecated Not a member `object-data-table` reads. A row opens the record it already is. + */ + filter?: never; + /** + * REFUSED BY NAME on `object-data-table` (objectui#10685). `maxRows` caps a + * drilled record list, and this block drills to one record. It applies on + * `object-chart`, `object-pivot` and `object-metric`, whose drill lists page + * by it. + * + * @deprecated Not a member `object-data-table` reads. A row opens the record it already is. + */ + maxRows?: never; + /** + * REFUSED BY NAME on `object-data-table` (objectui#10685). A drill `report` + * replaces a drilled record list, and this block drills to one record. It + * applies on `object-pivot` and `object-metric`, whose `DrillDownDrawer` + * renders it. + * + * @deprecated Not a member `object-data-table` reads. A row opens the record it already is. + */ + report?: never; + /** + * Where the record drill lands: `'drawer'` (the default) or `'dialog'`. + * + * `'navigate'` is REFUSED on `object-data-table` (objectui#10685): it opens + * the object's full list page through the host's `openRecordList`, and this + * block's row opens one record. `'navigate'` applies on `object-chart`, + * `object-pivot` and `object-metric`. + */ + target?: 'drawer' | 'dialog'; +} + /** * Pivot table (cross-tabulation) component * diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 5c4ad149e3..26145ddb77 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -253,6 +253,7 @@ export type { DrillDownConfig, ObjectMetricDrillDownConfig, ObjectPivotDrillDownConfig, + ObjectDataTableDrillDownConfig, TimelineEvent, TimelineScale, TimelineSchema, diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index 1dedf27680..abba6c8334 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -23,7 +23,7 @@ import type { BaseSchema } from './base.js'; // and read here by `ObjectKanbanSchema.columns` (objectui#8913) so the two // kanban faces judge a card the same way. Type-only: no runtime edge. import type { KanbanCard } from './complex.js'; -import type { DrillDownConfig } from './data-display.js'; +import type { DrillDownConfig, ObjectDataTableDrillDownConfig } from './data-display.js'; // `QueryParams` is the destination `ObjectGallerySchema.filter`'s own docblock // names — the value is forwarded verbatim into that slot — so the declaration // is an INDEXED ACCESS on it rather than a copy of its arms (objectui#9309). @@ -4767,8 +4767,15 @@ export interface ObjectDataTableSchema extends BaseSchema { /** * Drill-to-record: clicking a row opens that record in a detail drawer. * `DashboardRenderer` defaults object-backed table widgets to `{ enabled: true }`. + * + * This block's own drill shape, not the shared `DrillDownConfig` + * (objectui#10685): `filter`, `maxRows` and `report` are refused by name, + * because a row drills to the one record it already is and there is no + * drilled list for them to act on, and `target` is `'drawer'` or `'dialog'`, + * because `'navigate'` opens the object's list page. The zod mirror's + * `drillDown` refuses the same members. */ - drillDown?: DrillDownConfig; + drillDown?: ObjectDataTableDrillDownConfig; /** * Row click handler — a RUNTIME SLOT a React host supplies through this * interface, never through authored JSON (objectui#6124; the zod mirror diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index a1874841e7..585918e12f 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -2711,7 +2711,10 @@ export const ObjectGallerySchema = BaseSchema.extend({ * and left it unmirrored — minting the mirror was a new export outside * that ruling — so `UnmirroredDeclared` carried it, as it had carried * `ChartSchema.drillDown` since objectui#6058; both entries left the - * ledger with that mirror. + * ledger with that mirror. Since objectui#10685 it is this block's OWN + * drill shape, the twin of `ObjectDataTableDrillDownConfig`: the shared + * mirror extended so that `filter`, `maxRows` and `report` are refused by + * name and `target` takes `'drawer'` or `'dialog'` only. * - `dataProvider` is a RETIREMENT TOMBSTONE (objectui#7353), `?: never` on * the TS twin: refused by name, pointing at `objectName`. Deleting it * instead would leave an authored value KEPT unchecked, because @@ -2730,8 +2733,41 @@ export const ObjectDataTableSchema = BaseSchema.extend({ columns: z.array(z.any()).optional().describe('Column definitions (names or column objects)'), searchable: z.boolean().optional().describe('Forwarded to the rendered data-table'), pagination: z.boolean().optional().describe('Forwarded to the rendered data-table'), - drillDown: DrillDownConfigSchema.optional().describe( - 'Drill-to-record: clicking a row opens that record in a detail drawer (DashboardRenderer defaults object-backed table widgets to { enabled: true, mode: record })', + // objectui#10685 — this block's OWN drill shape, the twin of + // `ObjectDataTableDrillDownConfig` (`../data-display.ts`). A row drills to the + // one record it already is, so the three members that configure a drilled + // record LIST have nothing to act on, and `'navigate'` (the object's list + // page) is the wrong destination for one record. `retirementTombstone` is the + // repo's one declared-and-refused mechanism; here it refuses per block, not a + // retirement: the shared mirror keeps all three keys for the blocks that read + // them. `enabled`, `mode`, `title` and `columns` parse exactly as the shared + // mirror parses them. + drillDown: DrillDownConfigSchema.extend({ + filter: retirementTombstone( + 'REFUSED on `object-data-table` (objectui#10685) — `drillDown.filter` scopes a drilled record list, and ' + + 'this block drills to the one record its row already is, so there is no list to filter. A drill `filter` ' + + 'applies on `object-chart` and `object-pivot`. Delete the key.', + ), + maxRows: retirementTombstone( + 'REFUSED on `object-data-table` (objectui#10685) — `drillDown.maxRows` caps a drilled record list, and ' + + 'this block drills to the one record its row already is. It applies on `object-chart`, `object-pivot` ' + + 'and `object-metric`, whose drill lists page by it. Delete the key.', + ), + report: retirementTombstone( + 'REFUSED on `object-data-table` (objectui#10685) — `drillDown.report` replaces a drilled record list with ' + + 'a report, and this block drills to the one record its row already is. It applies on `object-pivot` and ' + + '`object-metric`, whose drill drawer renders it. Delete the key.', + ), + target: z.enum(['drawer', 'dialog'], { + error: '`drillDown.target` on `object-data-table` is `\'drawer\'` or `\'dialog\'` (objectui#10685). ' + + '`\'navigate\'` is refused here: it opens the object\'s full list page, and this block\'s row opens one ' + + 'record. `\'navigate\'` applies on `object-chart`, `object-pivot` and `object-metric`.', + }).optional().describe( + "Where the record drill lands: 'drawer' (default) or 'dialog'. 'navigate' is refused on this block (objectui#10685)", + ), + }).optional().describe( + 'Drill-to-record: clicking a row opens that record in a detail drawer (DashboardRenderer defaults object-backed table widgets to { enabled: true, mode: record }). ' + + 'This block\'s own drill shape: filter, maxRows and report are refused by name, and target is drawer or dialog (objectui#10685)', ), onRowClick: handlerKeyRefusal('onRowClick', 'runtime-slot', 'Row click handler (overrides drill-to-record)'), body: retirementTombstone( From a1217b5151bfa82f85b3fbe4447c15bf6cacf059 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 20:52:29 +0000 Subject: [PATCH 4/5] fix(types): drop the DrillDownConfig import objectql.ts no longer uses `ObjectDataTableSchema.drillDown` now names `ObjectDataTableDrillDownConfig`, so the shared type is referenced only from docblocks, and `object-ui/no-unused-imports` fails `@object-ui/types#lint` on it. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN --- packages/types/src/objectql.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index 02f275e650..22b133a3c0 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -23,7 +23,7 @@ import type { BaseSchema } from './base.js'; // and read here by `ObjectKanbanSchema.columns` (objectui#8913) so the two // kanban faces judge a card the same way. Type-only: no runtime edge. import type { KanbanCard } from './complex.js'; -import type { DrillDownConfig, ObjectDataTableDrillDownConfig } from './data-display.js'; +import type { ObjectDataTableDrillDownConfig } from './data-display.js'; // `QueryParams` is the destination `ObjectGallerySchema.filter`'s own docblock // names — the value is forwarded verbatim into that slot — so the declaration // is an INDEXED ACCESS on it rather than a copy of its arms (objectui#9309). From 64d43b6d67dee3aafbdb1c04faf65be1357a359b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 03:24:40 +0000 Subject: [PATCH 5/5] docs(changeset): grade the object-data-table zod-door refusal minor with a breaking banner The 10685 changeset narrows the published validator's accept set: `objectui validate` / `safeValidateSchema` refuse a stored `object-data-table` config carrying `drillDown.filter`, `.maxRows`, `.report` or `target: 'navigate'` that parsed green before. The repo's convention grades that `minor` with a breaking banner (as 6881, 6951, 6972, 7322, 7963, 8801 and 7352 do); `patch` was the grade for a TypeScript-door-only refusal, which no longer describes this change. `@object-ui/plugin-dashboard` stays `patch`: a React-prop narrowing with no runtime door. Every other sentence is kept. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN --- .changeset/10685-drilldown-per-block.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.changeset/10685-drilldown-per-block.md b/.changeset/10685-drilldown-per-block.md index 9b1d5d50e7..9e73e744e0 100644 --- a/.changeset/10685-drilldown-per-block.md +++ b/.changeset/10685-drilldown-per-block.md @@ -1,18 +1,23 @@ --- -'@object-ui/types': patch +'@object-ui/types': minor '@object-ui/plugin-dashboard': patch --- `object-data-table` refuses `drillDown.filter`, `.maxRows`, `.report` and `target: 'navigate'`, and `object-pivot` refuses `drillDown.mode` (objectui#10685) -**Stored JSON: `objectui validate` now refuses an `object-data-table` config that carries -`drillDown.filter`, `drillDown.maxRows`, `drillDown.report` or `drillDown.target: 'navigate'`.** -Each refusal names the key and the blocks that do read it. None of the four was ever read by -this block. Its row drills to the one record it already is, so a drilled list's filter, row -cap and report had nothing to act on, and `'navigate'` was drawn as a drawer. Before this -change the table's zod mirror took the shared `DrillDownConfigSchema` and accepted all four. -Delete the key; for the drill target, write `'drawer'` or `'dialog'`. `enabled`, `mode`, -`title` and `columns` are accepted exactly as before. +**Breaking for authored metadata, graded `minor` by this repo's convention (AGENTS.md: a +breaking change here is `minor`, never `major`):** `object-data-table` no longer accepts +`drillDown.filter`, `drillDown.maxRows`, `drillDown.report` or `drillDown.target: 'navigate'` +on either published face — the TypeScript `ObjectDataTableSchema.drillDown` and its zod +mirror, the door `objectui validate` / `safeValidateSchema` run — so a stored JSON table +config that parsed green before is refused by name now. Each refusal names the key and the +blocks that do read it. None of the four was ever read by this block. Migration: delete the +key; for the drill target write `'drawer'` or `'dialog'`. `enabled`, `mode`, `title` and +`columns` are accepted exactly as before. + +Its row drills to the one record it already is, so a drilled list's filter, row cap and +report had nothing to act on, and `'navigate'` was drawn as a drawer. Before this change the +table's zod mirror took the shared `DrillDownConfigSchema` and accepted all four. `object-pivot` is refused at the TypeScript door only. A stored JSON pivot config carrying `mode` is still accepted and ignored at render, as it was before: neither `object-pivot` nor