diff --git a/.changeset/10932-pivot-drilldown-retired.md b/.changeset/10932-pivot-drilldown-retired.md new file mode 100644 index 0000000000..06b940ef62 --- /dev/null +++ b/.changeset/10932-pivot-drilldown-retired.md @@ -0,0 +1,40 @@ +--- +'@object-ui/types': minor +'@object-ui/plugin-dashboard': minor +--- + +`drillDown` is retired on the bare `pivot` node: author `object-pivot` to drill (objectui#10932) + +**Breaking for authored metadata, graded `minor` by this repo's convention (AGENTS.md: a +breaking change here is `minor`, never `major`):** a `pivot` node no longer accepts +`drillDown` on either published face. The TypeScript `PivotTableSchema.drillDown` is a +`?: never` tombstone, and the `pivot` arm of the zod mirror, the door `objectui validate` / +`safeValidateSchema` run, refuses the key by name with `invalid_type` at `drillDown`. The +refusal names the remedy. Migration: delete the key, or, for a pivot whose cells open the +records behind a value, author an `object-pivot` (`objectName` plus the same `rowField` / +`columnField` / `valueField`) and put the `drillDown` there. Every other `pivot` member is +accepted exactly as before. + +Nothing ever honoured the key on this node. `PivotTable` fired a drill only when its host +passed an `onDrillDown` handler. The one host that does is `ObjectPivotTable`, which renders +`object-pivot`, and the `pivot` registration passes none. So an authored `drillDown` +validated and clicking a cell did nothing, with no signal. A pivot over inline `data` also +names no object to list the drilled records from. No shipped or example document authors +the key on a `pivot` node. + +This supersedes what three pending entries say about the `pivot` arm's `drillDown`: the +objectui#10859 (batch 2) entry ("`drillDown` is the shared `DrillDownConfigSchema`"), and the +dated notes on the objectui#7352 and objectui#10685 entries, which read that key as the shared +mirror (a third referencing declaration, and `mode` accepted there). As of this change the +arm refuses the key. + +- `@object-ui/types`: `PivotTableSchema.drillDown` is `?: never`, and the zod `pivot` arm's + `drillDown` is a `retirementTombstone()`. The content-channel refusal on `body` / `children` + no longer lists `drillDown` among what a `pivot` renders. +- `@object-ui/plugin-dashboard`: `PivotTable` reads nothing off `schema` for its drill. + **Behaviour change for React callers:** its `onDrillDown` prop is now the only switch. A + caller that passes a handler gets interactive cells, headers and totals, and a + `schema.drillDown` no longer gates them (the type refuses that key anyway). A caller that + wants no drill passes no handler. `ObjectPivotTable` already passed its handler only when + its own `drillDown` was enabled, so `object-pivot` behaves exactly as before, and its + `schema.drillDown` keeps the `ObjectPivotDrillDownConfig` type. diff --git a/packages/plugin-dashboard/src/ObjectPivotTable.tsx b/packages/plugin-dashboard/src/ObjectPivotTable.tsx index 96c5028cca..edebf499ea 100644 --- a/packages/plugin-dashboard/src/ObjectPivotTable.tsx +++ b/packages/plugin-dashboard/src/ObjectPivotTable.tsx @@ -38,8 +38,23 @@ import type { ObjectPivotDrillDownConfig, PivotTableSchema } from '@object-ui/ty */ const EMPTY_ROWS = Object.freeze([]) as unknown as any[]; +/** + * `PivotTableSchema` minus `drillDown`, which is a retirement tombstone on the + * `pivot` node (objectui#10932). `object-pivot` declares its own `drillDown` + * below, and intersecting it with the tombstone would collapse that member to + * `never`: the one pivot that drills could not be authored with a drill. + * + * Key remapping, not `Omit`: `PivotTableSchema` extends `BaseSchema`, whose + * `[key: string]: any` widens `keyof` to all of `string`, so + * `Omit` would keep the index signature and drop + * every declared member. + */ +type PivotTableSchemaWithoutDrillDown = { + [K in keyof PivotTableSchema as K extends 'drillDown' ? never : K]: PivotTableSchema[K]; +}; + export interface ObjectPivotTableProps { - schema: PivotTableSchema & { + schema: PivotTableSchemaWithoutDrillDown & { objectName?: string; /** * RETIRED (objectui#7353, ADR-0049 remove arm, ruling 5809008870) — write @@ -96,11 +111,12 @@ export interface ObjectPivotTableProps { // 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. + * This block's drill-down shape, `ObjectPivotDrillDownConfig`: `mode` is + * refused by name (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. `PivotTableSchema` + * carries no drill at all: on a plain `pivot` node the key is a retirement + * tombstone (objectui#10932), and this block is where a pivot drill lives. */ drillDown?: ObjectPivotDrillDownConfig; }; @@ -354,9 +370,12 @@ export const ObjectPivotTable: React.FC = ({ schema, data ); } - // Delegate to PivotTable with resolved data + // Delegate to PivotTable with resolved data. `drillDown` is this block's and + // stays behind: `PivotTable` drills through `onDrillDown` below, and on the + // `pivot` node it renders the key is a retirement tombstone (objectui#10932). + const { drillDown: _objectPivotDrillDown, ...pivotSchema } = schema; const finalSchema: PivotTableSchema = { - ...schema, + ...pivotSchema, data: finalData, }; diff --git a/packages/plugin-dashboard/src/PivotTable.tsx b/packages/plugin-dashboard/src/PivotTable.tsx index f2fb79a267..6c346c5433 100644 --- a/packages/plugin-dashboard/src/PivotTable.tsx +++ b/packages/plugin-dashboard/src/PivotTable.tsx @@ -9,7 +9,7 @@ import React, { useMemo } from 'react'; import type { PivotTableSchema, PivotAggregation } from '@object-ui/types'; import { cn } from '@object-ui/components'; -import { isDrillEnabled, type DrillEvent } from '@object-ui/core'; +import type { DrillEvent } from '@object-ui/core'; import { useSafeTranslate } from '@object-ui/i18n'; import { WidgetEmptyState } from './WidgetEmptyState'; @@ -41,9 +41,17 @@ export interface PivotTableProps { */ sourceLabel?: string; /** - * Drill-down click handler. When provided **and** `schema.drillDown` is - * enabled, cells / row & column headers / totals become interactive. - * Receives the click context which is forwarded to the drill-down engine. + * Drill-down click handler, and the ONLY drill switch. When provided, cells / + * row & column headers / totals become interactive and each click hands its + * context here; the host decides what a drill opens. `ObjectPivotTable` + * passes it exactly when its `object-pivot` `drillDown` is enabled. + * + * Nothing is read off `schema` for this: `drillDown` on a `pivot` node is a + * retirement tombstone (objectui#10932), since this component used to drill + * only for a host that passed this handler and the `pivot` registration + * passes none. Not a `drillDown` prop either: `SchemaRenderer` spreads a + * node's keys as props, so a prop of that name would hand an authored + * `drillDown` straight back to this component. */ onDrillDown?: (event: DrillEvent) => void; } @@ -179,11 +187,11 @@ export const PivotTable: React.FC = ({ schema, className, rowLa showColumnTotals = false, format, columnColors, - drillDown, } = schema; const totalLabel = useTotalLabel(); - const drillEnabled = isDrillEnabled(drillDown) && typeof onDrillDown === 'function'; + // The host's handler is the switch (see `onDrillDown`); nothing on the node is. + const drillEnabled = typeof onDrillDown === 'function'; const fireDrill = (ev: DrillEvent) => { if (!drillEnabled) return; onDrillDown!({ diff --git a/packages/plugin-dashboard/src/__tests__/PivotTable.drill.test.tsx b/packages/plugin-dashboard/src/__tests__/PivotTable.drill.test.tsx index 8178556a30..fdedb3d1ab 100644 --- a/packages/plugin-dashboard/src/__tests__/PivotTable.drill.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/PivotTable.drill.test.tsx @@ -26,24 +26,34 @@ const baseSchema: any = { }; describe('PivotTable drill-down', () => { - it('does not render interactive cells when drillDown is omitted', () => { + it('does not render interactive cells without a host `onDrillDown`', () => { render(); // No element with role="button" expect(screen.queryAllByRole('button').length).toBe(0); }); - it('does not call onDrillDown when drillDown.enabled !== true', () => { - const onDrillDown = vi.fn(); - render(); + // objectui#10932: `drillDown` on a `pivot` node is a retirement tombstone, + // and `PivotTable` reads nothing off the node for its drill. The host's + // `onDrillDown` is the only switch (`ObjectPivotTable` passes it exactly when + // its `object-pivot` drill is enabled). Both directions are pinned: the node + // key can neither turn the drill on nor turn it off. + it('the node\'s `drillDown` cannot turn the drill on: no handler, no interactive cells (objectui#10932)', () => { + render(); expect(screen.queryAllByRole('button').length).toBe(0); - expect(onDrillDown).not.toHaveBeenCalled(); + }); + + it('the node\'s `drillDown` cannot turn the drill off: the host handler is the switch (objectui#10932)', () => { + const onDrillDown = vi.fn(); + render(); + fireEvent.click(screen.getByLabelText('Drill into stage=won, source=web')); + expect(onDrillDown).toHaveBeenCalledTimes(1); }); it('emits cell payload with rowKey/colKey/value/scope on click', () => { const onDrillDown = vi.fn(); render( , ); @@ -62,7 +72,7 @@ describe('PivotTable drill-down', () => { const onDrillDown = vi.fn(); render( , ); @@ -74,7 +84,7 @@ describe('PivotTable drill-down', () => { const onDrillDown = vi.fn(); render( , ); @@ -86,7 +96,7 @@ describe('PivotTable drill-down', () => { const onDrillDown = vi.fn(); render( , ); @@ -100,7 +110,7 @@ describe('PivotTable drill-down', () => { const onDrillDown = vi.fn(); render( { + it('renders the cross-tab through SchemaRenderer, with no interactive cell or header', () => { + render(); + // Lit control: the pivot itself rendered, cells and headers included. + expect(screen.getByText('won')).toBeTruthy(); + expect(screen.getByText('100')).toBeTruthy(); + expect(screen.queryAllByRole('button')).toHaveLength(0); + expect(screen.queryByLabelText('Drill into stage=won, source=web')).toBeNull(); + }); + + it('CONTROL — the query can see a drill affordance when a host passes `onDrillDown`', () => { + render(); + expect(screen.getByLabelText('Drill into stage=won, source=web')).toBeTruthy(); + expect(screen.queryAllByRole('button').length).toBeGreaterThan(0); + }); +}); 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 a5ba32a7aa..070a83c411 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 @@ -24,9 +24,10 @@ * ledger side (both `UnmirroredDeclared` rows gone, the new pair registered) * is pinned by the parity file's own ratchet. * - * ⚠️ `PivotTableSchema.drillDown` is NOT covered here: that declaration has no - * zod mirror at all, so it sits in no ledger. The mirror minted here is the home - * that key will use whenever the pivot pair is mirrored (a separate card). + * ⚠️ `PivotTableSchema.drillDown` is NOT covered here. The pivot pair gained a + * zod mirror in objectui#10859 (batch 2), which judged that key by the mirror + * minted here, and objectui#10932 then retired it on the `pivot` node, which + * nothing drills: `pivot-drilldown-retired-10932.test.ts` pins the refusal. * * ⚠️ Not the spec's `ChartDrillDownSchema`, deliberately: `@objectstack/spec/ui` * models the CHART-ONLY subset (`enabled` / `filter` / `title` / `target` / diff --git a/packages/types/src/__tests__/pivot-drilldown-retired-10932.test.ts b/packages/types/src/__tests__/pivot-drilldown-retired-10932.test.ts new file mode 100644 index 0000000000..a611e10976 --- /dev/null +++ b/packages/types/src/__tests__/pivot-drilldown-retired-10932.test.ts @@ -0,0 +1,145 @@ +/** + * 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. + */ + +/** + * Retirement pin — `drillDown` on the bare `pivot` node is REFUSED, on both + * faces (objectui#10932, triage grade: retire behind a measured premise). + * + * ## The failure this pin exists to prevent + * + * `PivotTableSchema` declared `drillDown` as the shared `DrillDownConfig`, and + * the zod `pivot` arm (objectui#10859, batch 2) accepted it, so `objectui + * validate` passed a `pivot` node carrying a drill. Nothing honoured it: + * `PivotTable` fired a drill only when its HOST passed an `onDrillDown` + * handler, and the one host that does is `ObjectPivotTable`, which renders + * `object-pivot`. The `pivot` registration hands the node to `PivotTable` bare. + * So an author (or an AI) wrote a drill, validation accepted it, and clicking a + * cell did nothing, with no signal: declared, not enforced. + * + * The premise measured before retiring: no shipped or example document, in + * this repository or in objectstack's, authors `drillDown` on a `pivot` node, + * and `DashboardGridLayout`'s static-data pivot never writes one of its own. + * The readings, with their positive controls, are on the pull request. + * + * ## What is NOT pinned here + * + * `object-pivot`'s `drillDown` (`ObjectPivotDrillDownConfig`) is untouched and + * still drills. It has no zod arm, so it is not a `safeParse` control; its + * pins live in `@object-ui/plugin-dashboard`: the prop type in + * `ObjectPivotTable.drillDownRefusal-10685.test.tsx` (the `live` accept + * control) and the drawer opening in `drillRefusal-10789.test.tsx`. + * + * ## Two instruments + * + * The `Expect` / `Equal` rows and the `@ts-expect-error` line are judged by + * `tsc -p tsconfig.test.json` (the third leg of this package's `type-check`); + * vitest strips types and judges the `describe` blocks. + */ + +import { describe, expect, it } from 'vitest'; +import { PivotTableSchema, StrictAnyComponentSchema, safeValidateSchema } from '../zod/index.zod.js'; +import type { PivotTableSchema as Ts_PivotTableSchema } from '../data-display'; + +/* ── The TypeScript face: `?: never` ─────────────────────────────────────── */ + +type Equal = + (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; +type Expect = T; + +/** + * `?: never` reads as `undefined`. A deletion would read as `any` (the + * `BaseSchema` index signature), and the old member as `DrillDownConfig | undefined`. + */ +export type assertionPivotDrillDownIsATombstone = Expect>; +/** Non-vacuity twin: a member the node does read keeps its real type. */ +export type assertionPivotTitleStaysLive = Expect>; + +const MINIMAL = { + type: 'pivot', + rowField: 'region', + columnField: 'quarter', + valueField: 'revenue', + data: [{ region: 'EU', quarter: 'Q1', revenue: 10 }], +} as const; + +const WITH_DRILL = { ...MINIMAL, drillDown: { enabled: true, target: 'drawer', title: 'Revenue records' } }; + +describe('`drillDown` on a `pivot` node is a tombstone on the TypeScript face (objectui#10932)', () => { + it('authoring it is a compile error; the same node without it compiles', () => { + const accepted: Ts_PivotTableSchema = { ...MINIMAL, data: [...MINIMAL.data] }; + const refused: Ts_PivotTableSchema = { + ...MINIMAL, + data: [...MINIMAL.data], + // @ts-expect-error `drillDown` is retired on `pivot` (objectui#10932) — author `object-pivot` to drill + drillDown: { enabled: true }, + }; + expect([accepted.type, refused.type]).toEqual(['pivot', 'pivot']); + }); +}); + +/* ── The zod face: `retirementTombstone()` ───────────────────────────────── */ + +describe('`drillDown` on a `pivot` node is refused by the zod arm (objectui#10932)', () => { + it('the arm refuses it at `drillDown`, `invalid_type` against `never`', () => { + const result = PivotTableSchema.safeParse(WITH_DRILL); + expect(result.success, 'an authored pivot drill was ACCEPTED — it validates and does nothing').toBe(false); + if (result.success) return; + const issue = result.error.issues.find((i) => i.path[0] === 'drillDown'); + expect(issue, JSON.stringify(result.error.issues)).toBeDefined(); + expect(issue?.code).toBe('invalid_type'); + expect(issue?.path).toEqual(['drillDown']); + expect((issue as { expected?: string } | undefined)?.expected).toBe('never'); + }); + + it.each([ + ['safeValidateSchema (the rendering face)', (doc: unknown) => safeValidateSchema(doc)], + ['StrictAnyComponentSchema (the authoring face)', (doc: unknown) => StrictAnyComponentSchema.safeParse(doc)], + ] as const)('%s refuses it at `drillDown` too', (_face, parse) => { + const result = parse(WITH_DRILL); + expect(result.success).toBe(false); + if (result.success) return; + const issue = result.error.issues.find((i) => i.path[0] === 'drillDown'); + expect(issue, JSON.stringify(result.error.issues)).toBeDefined(); + expect(issue?.code).toBe('invalid_type'); + expect(issue?.path).toEqual(['drillDown']); + }); + + it('the refusal names the remedy, `object-pivot`, in one string on both channels', () => { + const result = PivotTableSchema.safeParse(WITH_DRILL); + expect(result.success).toBe(false); + if (result.success) return; + const issue = result.error.issues.find((i) => i.path[0] === 'drillDown'); + expect(issue?.message).not.toContain('Invalid input: expected never'); + expect(issue?.message).toContain('`object-pivot`'); + // `retirementTombstone()`: the parse message and the `.describe()` metadata + // are the same string, so generated docs and the refusal cannot drift. + expect(issue?.message).toBe((PivotTableSchema.shape.drillDown as { description?: string }).description); + }); + + it('keeps `drillDown` DECLARED on the arm: a tombstone, not a deletion', () => { + // `BaseSchema` is `.passthrough()`: a deleted key would be kept unjudged on + // the rendering face, the silent no-op this card closes. + expect('drillDown' in PivotTableSchema.shape).toBe(true); + }); + + it('CONTROL — the same node without `drillDown` still parses, on the arm and on both faces', () => { + const arm = PivotTableSchema.safeParse(MINIMAL); + expect(arm.success, JSON.stringify(arm.success ? null : arm.error.issues)).toBe(true); + expect(safeValidateSchema(MINIMAL).success).toBe(true); + expect(StrictAnyComponentSchema.safeParse(MINIMAL).success).toBe(true); + }); + + it('the content-channel refusal no longer lists `drillDown` among what a `pivot` renders', () => { + const result = PivotTableSchema.safeParse({ ...MINIMAL, children: [{ type: 'text', content: 'x' }] }); + expect(result.success).toBe(false); + if (result.success) return; + const issue = result.error.issues.find((i) => i.path[0] === 'children'); + expect(issue?.message).toContain('`columnColors`'); // lit control: the list is there + expect(issue?.message).not.toContain('`drillDown`'); + }); +}); diff --git a/packages/types/src/__tests__/registered-type-arms-10859-b2.test.ts b/packages/types/src/__tests__/registered-type-arms-10859-b2.test.ts index 21aa8cdd74..84c216e3b2 100644 --- a/packages/types/src/__tests__/registered-type-arms-10859-b2.test.ts +++ b/packages/types/src/__tests__/registered-type-arms-10859-b2.test.ts @@ -188,6 +188,9 @@ describe('the registered types armed in batch 2 validate (objectui#10859)', () = }); it('accepts a fully populated `pivot` document on both faces', () => { + // Every member the arm still accepts. `drillDown` is not one of them since + // objectui#10932 retired it on this node; its refusal is pinned in + // `pivot-drilldown-retired-10932.test.ts`. const doc = { type: 'pivot', title: 'Revenue by region', @@ -200,7 +203,6 @@ describe('the registered types armed in batch 2 validate (objectui#10859)', () = showColumnTotals: false, format: '$,.2f', columnColors: { Q1: 'text-emerald-600' }, - drillDown: { enabled: true, target: 'drawer', title: 'Revenue records' }, }; expect(safeValidateSchema(doc).success).toBe(true); expect(StrictAnyComponentSchema.safeParse(doc).success).toBe(true); @@ -262,9 +264,13 @@ describe('the batch-2 arms are closed where their declaration is (objectui#10859 expect(issue.path).toEqual(['data']); }); - it('pivot: `drillDown` is judged by the shared drill-down mirror', () => { + it('pivot: `drillDown` is refused whole, not judged member by member (objectui#10932)', () => { + // Until objectui#10932 this arm judged the key by the shared drill-down + // mirror, so the refusal landed at `['drillDown', 'enabled']`. The key is a + // retirement tombstone now; `pivot-drilldown-retired-10932.test.ts` pins it. const issue = firstIssue(safeValidateSchema({ ...MINIMAL[0], drillDown: { enabled: 'yes' } })); - expect(issue.path).toEqual(['drillDown', 'enabled']); + expect(issue.code).toBe('invalid_type'); + expect(issue.path).toEqual(['drillDown']); }); it.each(['body', 'children'] as const)('pivot refuses the `%s` content channel by name (objectui#9256)', (key) => { diff --git a/packages/types/src/data-display.ts b/packages/types/src/data-display.ts index 881682acb0..4dfce4ac64 100644 --- a/packages/types/src/data-display.ts +++ b/packages/types/src/data-display.ts @@ -2510,8 +2510,9 @@ export interface ObjectMetricDrillDownConfig extends DrillDownConfig { * types against it (`ObjectPivotTable`'s `schema.drillDown`). `object-pivot` * has no zod mirror, so a stored JSON config is checked by no validator and * reaches the block unchanged. (`PivotTableSchema`, the plain `pivot` node, - * gained one in objectui#10859 batch 2; its `drillDown` is the shared - * `DrillDownConfig`, as declared, so it does not refuse `mode` either.) + * gained one in objectui#10859 batch 2, and there `drillDown` is refused whole: + * the key is a retirement tombstone on both faces since objectui#10932, because + * nothing drills a `pivot` node. This type is where a pivot drill is authored.) */ export interface ObjectPivotDrillDownConfig extends DrillDownConfig { /** @@ -2644,10 +2645,35 @@ export interface PivotTableSchema extends BaseSchema { */ columnColors?: Record; /** - * Optional drill-down configuration. When enabled, clicking a cell / - * row header / column header / total opens a filtered list view. + * ADR-0049 RETIREMENT TOMBSTONE — `drillDown` on the `pivot` node + * (objectui#10932, triage grade: retire, do NOT wire a drill). + * + * What was measured: declared here as the shared {@link DrillDownConfig}, + * accepted by the zod arm, and honoured by nothing. `PivotTable` fired a + * drill only when its HOST passed an `onDrillDown` handler, and the one host + * that does is `ObjectPivotTable` (`object-pivot`). The `pivot` registration + * hands the node to `PivotTable` bare, console's lazy stub loads that same + * registration, and `DashboardGridLayout`'s static-data pivot is rendered + * through it, so every `pivot` path left the key inert: clicking a cell did + * nothing, with no signal. A static-data pivot also has nothing to drill + * into: a drill lists the records behind a value, and a pivot over inline + * `data` names no object to query them from. + * + * `?: never` is this package's tombstone convention (see + * `DataTableSchema.toolbar`, {@link StaticTableColumn}), NOT a deletion: + * `BaseSchema`'s `[key: string]: any` would admit a deleted key as `any` + * again. The Zod twin refuses it loudly via `retirementTombstone()` + * (`zod/data-display.zod.ts`), naming the same remedy. + * + * RETIRED (objectui#10932, ADR-0049) — a `pivot` node draws a cross-tab of + * the rows you pass in and drills nowhere. For a pivot whose cells open the + * records behind a value, author an `object-pivot` (`objectName` + the same + * `rowField` / `columnField` / `valueField`), whose `drillDown` is + * {@link ObjectPivotDrillDownConfig}. + * + * @deprecated Not a member `pivot` reads — author `object-pivot` to drill. */ - drillDown?: DrillDownConfig; + drillDown?: never; /** * REFUSED BY NAME (objectui#9256, ADR-0049) — `pivot` reads NEITHER content * channel: no renderer read consumes `body` or `children` for this node. diff --git a/packages/types/src/zod/data-display.zod.ts b/packages/types/src/zod/data-display.zod.ts index 9b2e889b00..a17f27b71a 100644 --- a/packages/types/src/zod/data-display.zod.ts +++ b/packages/types/src/zod/data-display.zod.ts @@ -871,11 +871,11 @@ const CHART_Y_AXIS_IS_A_LIST_GUIDANCE = * (`../data-display.ts`), key for key (objectui#7352). * * Shared by the declarations that carry `drillDown`: `ChartSchema` below and - * `ObjectDataTableSchema` (`objectql.zod.ts`) reference it, and so does - * `PivotTableSchema` below since objectui#10859 (batch 2) mirrored the pivot - * pair — the home this docblock had named for that key while the pair had no - * mirror. Until this mirror existed neither declaring mirror had heard of the - * key, so under + * `ObjectDataTableSchema` (`objectql.zod.ts`) reference it. `PivotTableSchema` + * below referenced it from objectui#10859 (batch 2) until objectui#10932 + * retired the key on the `pivot` node, which nothing drills; `object-pivot`, + * the block that does drill, has no zod mirror. Until this mirror existed + * neither declaring mirror had heard of the key, so under * `BaseSchema`'s `.passthrough()` a `drillDown: { enabled: 'yes' }` parsed green * and reached a widget that reads `enabled` as truthy — `declared !== enforced`, * ledgered in `zod-mirror-parity.test.ts` (`UnmirroredDeclared`) by @@ -1033,9 +1033,22 @@ const PIVOT_NEITHER_CHANNEL = neitherContentChannelGuidance( 'pivot', 'its registration hands the node to `PivotTable`, which reads it as `PivotTableSchema` and destructures only the members below', 'a cross-tab of `data` — `rowField` down, `columnField` across, `valueField` aggregated by `aggregation` — ' - + 'with `title`, `showRowTotals`, `showColumnTotals`, `format`, `columnColors` and `drillDown`', + + 'with `title`, `showRowTotals`, `showColumnTotals`, `format` and `columnColors`', ); +/** + * objectui#10932: the refusal for `drillDown` on a `pivot` node, one string + * feeding the parse-time message and `.describe()` (`retirementTombstone`). + * Nothing drills a `pivot` node: `PivotTable` fires a drill only when its host + * passes `onDrillDown`, and the one host that does renders `object-pivot`. + */ +const PIVOT_DRILL_DOWN_RETIRED = + 'RETIRED (objectui#10932, ADR-0049) — nothing drills a `pivot` node: `PivotTable` fires a drill only when ' + + 'its host passes an `onDrillDown` handler, and the `pivot` registration passes none, so an authored ' + + '`drillDown` validated and did nothing. A pivot over inline `data` has no object to list records from. ' + + 'For a pivot whose cells open the records behind a value, author an `object-pivot` (`objectName` + the ' + + 'same `rowField` / `columnField` / `valueField`) and put `drillDown` there.'; + /** * Pivot Table Schema — mirrors `PivotTableSchema` in `../data-display.ts`, * member for member (objectui#10859, batch 2). @@ -1048,11 +1061,14 @@ const PIVOT_NEITHER_CHANNEL = neitherContentChannelGuidance( * `packages/cli/src/__tests__/registered-types-validate-ratchet-10859.test.ts`. * * Requiredness is the declaration's: `rowField`, `columnField`, `valueField` - * and `data` are required there, so they are required here. `drillDown` is the - * shared `DrillDownConfigSchema` above, the home its docblock names for this - * pair. Both content channels are refused by name, as the declaration's - * `?: never` members refuse them (objectui#9256). The pin is - * `../__tests__/registered-type-arms-10859-b2.test.ts`. + * and `data` are required there, so they are required here. `drillDown` is a + * retirement tombstone, as the declaration's `?: never` member is + * (objectui#10932): it rode this arm as the shared `DrillDownConfigSchema` from + * objectui#10859 until then, accepted and read by nothing. Both content + * channels are refused by name, as the declaration's `?: never` members refuse + * them (objectui#9256). The pins are + * `../__tests__/registered-type-arms-10859-b2.test.ts` and, for `drillDown`, + * `../__tests__/pivot-drilldown-retired-10932.test.ts`. */ export const PivotTableSchema = BaseSchema.extend({ type: z.literal('pivot'), @@ -1072,7 +1088,7 @@ export const PivotTableSchema = BaseSchema.extend({ .record(z.string(), z.string()) .optional() .describe('Mapping of column header values to Tailwind text-color classes'), - drillDown: DrillDownConfigSchema.optional().describe('Drill-down config (the shared `DrillDownConfig`). Inert on a `pivot` node: `PivotTable` drills only when its host passes `onDrillDown`, and the `pivot` registration passes none; `object-pivot` is the block whose clicks open the records behind a value'), + drillDown: retirementTombstone(PIVOT_DRILL_DOWN_RETIRED), body: retirementTombstone(PIVOT_NEITHER_CHANNEL), children: retirementTombstone(PIVOT_NEITHER_CHANNEL), });