Skip to content

Commit cd4eb52

Browse files
committed
fix(types): declare the three props-half slots on ListViewRuntimeProps
Corrects this branch's own first reading. I had concluded that nothing needed adding to `ListViewRuntimeProps` because `onAddRecord` / `onBulkAction` / `onPageSizeChange` are read off the PROPS bag and `ListViewProps` (`@object-ui/plugin-list`) already declares all three as React props. ⛔ That measured only ONE of the two supply paths into that bag. `SchemaRenderer` spreads every non-metadata top-level key of the node into the props it creates the component with, so a host that builds the NODE in TypeScript reaches the same read — and that path was typed only by `BaseSchema`'s passthrough index signature. Declaring the refusal arms without declaring the slots would have narrowed a live programmatic channel from `unknown` to `undefined`, which is wider than this card asks for and is not what `'runtime-slot'` means. It is the same repair objectui#9344's slice made on `ObjectGallerySchema` for the same spread, and it means a published interface WIDENS here as well as narrowing — the changeset says so. `ObjectView.relayRungCensus-7559.test.ts` follows from that: the three entered its derived population with this declaration, and its own failure text says the answer is owed by the change that added them. Three `host-runtime` absences, the existing kind whose validator requires exactly `ListViewRuntimeProps` membership — no new absence kind, no new assertion. ⚠️ FILE SURFACE: that census file is outside this claim's declared surface (`packages/types/src/`, `scripts/check-handler-key-read-sites.mjs`, one changeset). It is reported rather than hidden; see the pull request body. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSd9P5u2Mf4p8g4n4SD4Fx
1 parent 2658321 commit cd4eb52

5 files changed

Lines changed: 105 additions & 25 deletions

File tree

‎.changeset/7804-list-view-handler-slots-declared.md‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,20 @@ precedence (`ListViewAuthored`), so:
4545
still the function types `ListViewRuntimeProps` declares, still supplied on
4646
the node by hosts such as `@object-ui/app-shell`'s `ObjectView`.
4747
- `ListViewSchema['onAddRecord']`, `['onBulkAction']` and `['onPageSizeChange']`
48-
narrow from `unknown` (they were only ever reachable through the passthrough
49-
index signature) to `undefined`. That is the intended narrowing: all three are
50-
React props, declared by name on `ListViewProps` in `@object-ui/plugin-list`,
51-
and a host passes them to `<ListView>` rather than authoring them on the node.
52-
- Nothing is ADDED to `ListViewRuntimeProps`, so no published interface widens.
48+
are now DECLARED on `ListViewRuntimeProps` with the signatures `ListViewProps`
49+
in `@object-ui/plugin-list` already carried, where before they were typed only
50+
by `BaseSchema`'s passthrough index signature — `unknown`, a declaration
51+
nobody wrote and nobody can read. `ListView` reads all three off its props
52+
bag, and a host fills that bag either by passing the React prop or by putting
53+
the key on the node, where `SchemaRenderer` spreads it in; this declaration is
54+
the second path's contract. Same repair as the `ObjectGallerySchema` pair one
55+
slice earlier, for the same spread.
56+
57+
**A published interface therefore WIDENS as well as narrowing.** Three members
58+
are added to `ListViewRuntimeProps`. Nothing that compiled before stops
59+
compiling — `unknown` accepted any host handler and the declared signatures
60+
accept the ones `ListViewProps` already held hosts to — but the surface is
61+
larger, and it is stated here rather than left to be discovered.
5362

5463
**Migration.** Nothing in the corpus has to change: no authored `'list-view'`
5564
document in this repository, its examples or its docs writes any of the five —

‎packages/app-shell/src/views/ObjectView.relayRungCensus-7559.test.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,14 @@ const ABSENCES: Record<string, Absence> = {
482482
// ── The runtime-only half of the intersection ─────────────────────────────
483483
onNavigate: { kind: 'host-runtime', reason: 'Host callback. This host wires record navigation through the `onRowClick` prop on the `<ListView>` element instead; a view record cannot carry a function.' },
484484
refreshTrigger: { kind: 'host-runtime', reason: 'Host refresh counter, supplied by the caller; not view metadata.' },
485+
// ⭐ objectui#7804 declared these three on `ListViewRuntimeProps`, so they
486+
// entered this census's population with that slice and owe an answer here.
487+
// All three are host callbacks `ListView` reads off its PROPS bag; this host
488+
// supplies none of them and a view RECORD cannot carry a function, so there is
489+
// no rung to add — the absence is the whole of their contract on this path.
490+
onAddRecord: { kind: 'host-runtime', reason: 'Host callback for the toolbar "+ New" affordance. This host routes record creation through its own action layer rather than through ListView, and a view record cannot carry a function.' },
491+
onBulkAction: { kind: 'host-runtime', reason: 'Host callback for non-delete bulk actions. This host wires bulk delete through the `onBulkDelete` prop on the `<ListView>` element and offers no other bulk action here; a view record cannot carry a function.' },
492+
onPageSizeChange: { kind: 'host-runtime', reason: 'Host callback for the pager\'s page-size select. This host lets ListView keep page size as session state rather than persisting it, the same posture as `onFilterChange` (objectui#4155); a view record cannot carry a function.' },
485493
};
486494

487495
// ---------------------------------------------------------------------------

‎packages/types/src/__tests__/list-view-handler-slots-7804.test.ts‎

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@
2828
* package, chained from the package's `type-check` script.
2929
*
3030
* ⛔ What this file does NOT assert: that the keys are unreachable. They are
31-
* RUNTIME SLOTS — `onNavigate` / `onDensityChange` reach `ListView` through the
32-
* node (`schema.onX`), the other three through React props declared on
33-
* `ListViewProps` in `@object-ui/plugin-list`. A host still supplies them; what
34-
* is refused is AUTHORING one as JSON, which could only ever hand a call site
35-
* a plain object where it expects a function.
31+
* RUNTIME SLOTS. `onNavigate` / `onDensityChange` reach `ListView` off the node
32+
* (`schema.onX`); the other three reach it off the PROPS bag, which a host fills
33+
* either by passing the React prop `ListViewProps` declares or by putting the
34+
* key on the NODE, where `SchemaRenderer` spreads it in. A host still supplies
35+
* all five; what is refused is AUTHORING one as JSON, which could only ever hand
36+
* a call site a plain object where it expects a function.
3637
*/
3738
import { describe, it, expect } from 'vitest';
3839
import { ListViewSchema as ListViewMirror } from '../zod/objectql.zod.js';
@@ -143,11 +144,36 @@ const TYPE_PINS = {
143144
objectName: 'accounts' satisfies ListViewSchema['objectName'],
144145
} as const;
145146

146-
/** The three PROPS-half keys are NOT authorable on the node type. */
147-
// @ts-expect-error `onAddRecord` is a React prop on `ListViewProps`, not a node key.
148-
const notOnTheNode: ListViewSchema['onAddRecord'] = () => undefined;
149-
void notOnTheNode;
147+
/**
148+
* The three PROPS-half slots stay CALLABLE on the node type too — the second
149+
* supply path (`SchemaRenderer` spreading a node key into the props bag) is what
150+
* this declaration contracts, and it is exactly what the refusal arm would have
151+
* killed without the precedence.
152+
*/
153+
const propsHalf = {
154+
onAddRecord: (() => undefined) satisfies NonNullable<ListViewSchema['onAddRecord']>,
155+
onBulkAction: ((action: string, records: unknown[]) => {
156+
void action;
157+
void records;
158+
}) satisfies NonNullable<ListViewSchema['onBulkAction']>,
159+
onPageSizeChange: ((size: number) => {
160+
void size;
161+
}) satisfies NonNullable<ListViewSchema['onPageSizeChange']>,
162+
} as const;
163+
void propsHalf;
150164

151-
/** And the runtime half's own key list is what the precedence is keyed on. */
152-
const runtimeKeys: Array<keyof ListViewRuntimeProps> = ['onNavigate', 'onDensityChange', 'refreshTrigger'];
165+
/**
166+
* ⛔ And the precedence is keyed on `ListViewRuntimeProps`' own member list, so
167+
* a sixth refusal arm added to the mirror without a matching declaration here
168+
* would go back to resolving as `undefined`. This is that list, stated where it
169+
* fails if a member leaves it.
170+
*/
171+
const runtimeKeys: Array<keyof ListViewRuntimeProps> = [
172+
'onNavigate',
173+
'onDensityChange',
174+
'refreshTrigger',
175+
'onAddRecord',
176+
'onBulkAction',
177+
'onPageSizeChange',
178+
];
153179
void runtimeKeys;

‎packages/types/src/objectql.ts‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2644,6 +2644,42 @@ export interface ListViewRuntimeProps {
26442644
* Used by parent components (e.g. ObjectView) to signal that a mutation occurred.
26452645
*/
26462646
refreshTrigger?: number;
2647+
2648+
/**
2649+
* ⭐ The three slots below are declared here by objectui#7804, and the reason is
2650+
* the one objectui#9344's slice already measured on `ObjectGallerySchema`: a key
2651+
* that reaches the renderer through `SchemaRenderer`'s props spread is on the
2652+
* TypeScript face whether or not anyone declared it — `BaseSchema`'s index
2653+
* signature was typing all three `unknown`, which is a declaration nobody wrote
2654+
* and nobody can read.
2655+
*
2656+
* `ListView` reads them off its PROPS bag (`props.onAddRecord`, not
2657+
* `schema.onAddRecord`), and there are two supply paths into that bag, both
2658+
* live: a React host renders the component and passes the prop directly — the
2659+
* `ListViewProps` interface in `@object-ui/plugin-list` declares all three by
2660+
* name for exactly that — or a host builds the NODE in TypeScript and
2661+
* `SchemaRenderer` spreads every non-metadata top-level key into the props it
2662+
* creates the component with. This declaration is the second path's contract.
2663+
* Declaring it is what keeps the `'runtime-slot'` disposition on the matching
2664+
* zod arms true: JSON cannot author a function, so the mirror refuses the key
2665+
* by name, while a programmatic host goes on supplying one HERE.
2666+
*
2667+
* ⚠️ Signatures match `ListViewProps` deliberately, `any` included. A host that
2668+
* discovered the payload from the implementation annotated its own handler
2669+
* against that interface, and a narrower declaration here refuses such a host
2670+
* CONTRAVARIANTLY — the reading objectui#9341 took on
2671+
* `ObjectKanbanSchema.onCardClick` and the reason `ListViewProps.onRowClick`
2672+
* carries the same spelling.
2673+
*/
2674+
2675+
/** Called when the user asks for a new record (toolbar "+ New" and the empty-state CTA). */
2676+
onAddRecord?: () => void;
2677+
2678+
/** Called with a non-delete bulk action key and the currently selected rows. */
2679+
onBulkAction?: (action: string, records: any[]) => void;
2680+
2681+
/** Called when the user picks a different page size in the pager. */
2682+
onPageSizeChange?: (pageSize: number) => void;
26472683
}
26482684

26492685
/**

‎packages/types/src/zod/objectql.zod.ts‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,15 +1142,16 @@ export const ListViewSchema = BaseSchema
11421142
// `onNavigate` has no in-repo supplier on a `list-view` node, yet the
11431143
// channel is wired end to end and the read still fires.
11441144
// `onAddRecord` · `onBulkAction` · `onPageSizeChange` read off the PROPS
1145-
// bag (`props.onX`) and declared as React props on `ListViewProps`
1146-
// (`@object-ui/plugin-list`), the interface objectui#4528 wrote out by
1147-
// name. `StudioDesignSurface` supplies `onAddRecord` as a React prop;
1148-
// the other two have no in-repo supplier and are still read and still
1149-
// invoked.
1150-
//
1151-
// ⇒ nothing is ADDED to `ListViewRuntimeProps` by this slice: every one of
1152-
// the five already has a declared TypeScript home, and the three props-half
1153-
// keys never belonged on the node type at all.
1145+
// bag (`props.onX`). TWO supply paths reach that bag and both are live:
1146+
// a React host passes the prop to the component — `ListViewProps`
1147+
// (`@object-ui/plugin-list`) declares all three by name, and
1148+
// `StudioDesignSurface` supplies `onAddRecord` that way — or a host
1149+
// builds the NODE and `SchemaRenderer` spreads it in. The second path
1150+
// was typed only by `BaseSchema`'s index signature, so this slice
1151+
// declares the three on `ListViewRuntimeProps` as well, which is the
1152+
// same repair objectui#9344's slice made on `ObjectGallerySchema` for
1153+
// the same spread. `onBulkAction` / `onPageSizeChange` have no in-repo
1154+
// supplier and are still read and still invoked.
11541155
//
11551156
// ⚠️ Unlike the plain `interface X extends BaseSchema` arms this card
11561157
// drained before it, THIS arm feeds its own TypeScript face:

0 commit comments

Comments
 (0)