Skip to content

Commit 582edef

Browse files
feat(types)!: showFilters is retired on object-grid, refused by name with the list view's userActions.filter named (objectui#11068) (#11306)
Part of #11068 Clause-②: yes The `showFilters` half of the card, as triage's retriage answer (comment 5896596687) ruled it: **RETIRE, with a named tombstone that points at the list view's `userActions.filter`**. `keyboardNavigation` is not in this PR. It waits until objectstack-ai/objectstack#20694's row is installable (PR objectstack-ai/objectstack#20882 is not in `@objectstack/spec` 17.5.0), so the card stays open. ## What changed - **`@object-ui/types`, zod twin** (`ObjectGridSchema` in `zod/objectql.zod.ts`): `showFilters: z.boolean().optional()` is now `retirementTombstone(OBJECT_GRID_SHOW_FILTERS_RETIRED)`, next to the `rowSpecActions` / `bulkSpecActions` / `name` / `placeholder` tombstones. The one string goes into both `.describe()` and the parse message. It names the card, says the grid has no filter UI, sends the author to a `list-view` and its `userActions.filter`, and names `filter` for narrowing the grid's own rows. - **`@object-ui/types`, TypeScript twin** (`ObjectGridSchema` in `objectql.ts`): `showFilters?: boolean` (the deprecated "legacy filters toggle") is now `showFilters?: never` with a RETIRED docblock. It is not deleted, because the key would then fall back to `BaseSchema`'s index signature and type-check again. That is the same convention `rowSpecActions` uses. - **Untouched, on purpose:** `ObjectViewSchema.showFilters` (both faces), which `ObjectView` reads. The list view's `showFilters` (zod `ListViewSchema`), which `ListView` folds onto `userActions.filter`. `NamedListView.showFilters`, already `?: never` since objectui#7924. `DashboardConfig.showFilters`. The `object-view` `table` slot's existing by-name refusal. - **Producers that stopped authoring it:** the root `README.md` `object-grid` example, and the objectui#9729 byte-ruler corpus in `ObjectGrid.operatorsInert-9729.test.tsx`. That corpus's "filter surface on" document carried `showFilters: true`, which never drew anything. It now measures with the grid's search box (`searchableFields`), and the test is renamed to match. - **Docs:** `schema-reference.md` gets a retired callout under `ObjectGridSchema` and an updated history sentence. `plugin-grid.mdx` gets a paragraph next to the four earlier retirements. - **Pins:** the new `packages/types/src/__tests__/object-grid-show-filters-retired-11068.test.ts` covers both authoring faces, the metadata channel, member-not-deletion, the upstream half and the TS twin. `ObjectGrid.declaredKeys-11068.test.tsx` adds `showFilters` (true and false) to the byte ruler. Comment-only count updates (four to five) in `object-view-slot-key-lists.test.ts` and the two slot docblocks. - **Changesets:** the new `.changeset/11068-object-grid-show-filters.md` is `@object-ui/types` `minor`, with a BREAKING (authoring) line. The pending `.changeset/11068-grid-declared-keys.md` (from PR objectui#11130, not yet released) said "`showFilters` and `keyboardNavigation` are still declared and still not read". This PR makes that false, so it now names only `keyboardNavigation`. ## Dispatch assumptions, measured 1. **Red first: holds.** At base `1563d3e10b`, the new pin read **5 failed / 13 passed**. `{ type: 'object-grid', objectName, showFilters: true }` parsed green on `safeValidateSchema` and on `StrictAnyComponentSchema` ("showFilters: true parsed green: expected true to be false"). The 13 that passed are the controls. The renderer half is a byte ruler: the same grid drawn with and without `showFilters` (true and false) gives identical bytes, and the lit control `description` moves them. `packages/plugin-grid/src` is not edited, so the ruler reads base behaviour. 2. **Which declarations belong to the grid: holds.** `objectql.ts` declares `showFilters` on three interfaces. These are `ObjectGridSchema` (retired here), `ObjectViewSchema` (its own key, read by `ObjectView` as the fallback behind `userActions.filter`) and `NamedListView` (already a tombstone). The zod side declares it on `ObjectGridSchema` (retired), `ObjectViewSchema`, `ListViewSchema`, `DashboardConfigSchema`, and in the table-slot withheld set. 3. **`object-view` table slot: holds, and nothing changes there.** The slot is `ObjectGridSchema.omit(type, objectName).extend(OBJECT_VIEW_TABLE_WITHHELD)`. The withheld set already refused `showFilters` by name (objectui#10976), and `.extend` overrides the grid's tombstone, so the slot's message is byte-for-byte the same. No live read goes through the slot. `ObjectView` reads only the node-level `schema.showFilters`, and `OBJECT_VIEW_TABLE_RELAY_KEYS` does not carry it. 4. **Spec row: holds, and the premise stands.** On the installed `@objectstack/spec` 17.5.0, `ComponentPropsMap['object-grid']` is `ObjectGridPropsSchema`, which does not declare `showFilters`. It refuses `{ objectName, showFilters: true }` with `unrecognized_keys`, while `{ objectName }` alone parses green. `ListViewSchema.userActions` declares `filter`. The pin re-derives both facts. 5. **Producers and docs: partly falsified.** The `schema-reference.md` table row that names `showFilters` (the `showSearch` / `showFilters` / `showCreate` row) is in the **`ObjectViewSchema`** table, not the grid's, so it stays. The `ObjectGridSchema` table never listed the key. The card said its only producer was the doc example, which PR objectui#11130 removed. Two more in-repo producers turned up: the root `README.md` `object-grid` example and the 9729 ruler corpus. Both now omit the key. The `plugin-view.mdx` and `packages/plugin-view/README.md` mentions are about the object view's own key and the table slot, and stay. ## Tests (head `b87cbca042`, after merging `main` at `1ccb5ba7de`, which brought in PR objectui#11292) - `pnpm --filter @object-ui/types build && pnpm --filter @object-ui/types type-check && pnpm --filter @object-ui/plugin-grid type-check && pnpm --filter @object-ui/plugin-view type-check` gives `VERDICT command-exit 0`. The types type-check runs three legs: `tsc --noEmit`, `tsconfig.examples.json` and `tsconfig.test.json`. - `pnpm exec vitest run packages/types/ examples/schema-catalog/` gives `Test Files 338 passed (338)`, `Tests 9836 passed (9836)`. - `pnpm exec vitest run packages/plugin-grid/` plus the ten other suites that `git grep -l showFilters` finds (app-shell ×3, core ×3, plugin-list ×1, plugin-view ×3) gives `Test Files 187 passed (187)`, `Tests 1959 passed (1959)`. - Before the merge, the build ran over the `@object-ui/plugin-view` dependency closure (`turbo run build --filter='@object-ui/plugin-view^...' --concurrency=2`: 15/15 tasks). **Ablations** ran on committed code (`181b9b615a`) through objectstack's `scripts/ablation-replace.mjs`, in WRAP mode. Each anchor went from x1 to x0, the blob changed, and the restore was proven (blob equals HEAD, `git diff HEAD` empty). The tests read `src`, so there is no `dist` leg. - (A) In `objectql.ts`, `showFilters?: never` changed to `boolean`. `tsc -p tsconfig.test.json` then exits 2 with `TS2578 Unused '@ts-expect-error'` in the new pin, plus `TS2322` in the `zod-mirror-parity` type ratchet. - (B) The zod tombstone changed to `z.boolean().optional()`. The new pin then reads `5 failed / 13 passed`, the same red as at base. **Repo checks** (at `b87cbca042`): - `check-control-bytes`: OK. - `new-cross-file-line-citations`: `VERDICT … 0 new citation(s)`. - `check-changeset-presence`: "6 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)". - `changeset:check`: no `major`. - `check:doc-types`, `check:doc-fences`, `check:doc-example-ids`, `docs:check-links`, `check:doc-example-readers` and `check:test-path-roots`: all OK. - `check:component-surface-parity`: report-only, and the report is identical before and after (no `object-grid` row either way). - `check:changeset-claims`: report-only. The one falsified pending sentence is repaired as described above. - `check-governed-queue-guard --test` on the 11 paths: NOT GOVERNED. **NOT MEASURED:** - `check:doc-snippets` and `check:doc-examples` exit 2 ("THE GATE COULD NOT RUN"): about 20 packages they import are unbuilt. This diff adds or changes no `ts` / `tsx` fence line (counted: 0), and the only fenced edit is one line removed from a `json` fence in `README.md`. These are left to CI. - `pnpm lint` is CI's to run. - Type-check of the other `ObjectGridSchema` importers (core, plugin-calendar, plugin-dashboard, plugin-designer, plugin-gantt, plugin-kanban, plugin-list, plugin-map, plugin-tree, console) is left to CI. A grep finds no typed writer of `showFilters` on an `ObjectGridSchema` value. Every other writer targets an `object-view`, a `list-view` or a dashboard. ## Acceptance notes - The `object-view` table slot refuses `table.showFilters` with "`ObjectGrid` has no read of it". An author who wrote it probably meant the object view's own `showFilters`, which `ObjectView` reads, and the message does not name it. This is polish to the wording of a refusal that is already loud. Carrier: none. Left as is, because the dispatch rules out widening or reworking that slot. - The `operators` tombstone's comment in `objectql.zod.ts` still describes the objectui#9729 reading as "with the filter surface off AND on". That is a historical description of a measurement taken when the corpus carried `showFilters`, and it is left as written. - `keyboardNavigation` is still declared on `ObjectGridSchema` and still unread. It is the card's remaining half. Session: `https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ` --- _Generated by [Claude Code](https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9cfe997 commit 582edef

11 files changed

Lines changed: 303 additions & 35 deletions

‎.changeset/11068-grid-declared-keys.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,5 @@ now authors only keys the grid reads.
3838
refused by name.
3939

4040
**Unchanged.** An `object-view`'s `table` slot still withholds `description` and `emptyState`.
41-
Write them on the `object-grid` node itself. `showFilters` and `keyboardNavigation` are still
42-
declared and still not read.
41+
Write them on the `object-grid` node itself. `keyboardNavigation` is still declared and still
42+
not read.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@object-ui/types': minor
3+
---
4+
5+
`showFilters` is retired on `object-grid` (objectui#11068).
6+
7+
**BREAKING (authoring).** An `object-grid` that carries `showFilters` now fails validation,
8+
and TypeScript code that writes it on an `ObjectGridSchema` no longer compiles. Delete the key.
9+
10+
**What was wrong.** `ObjectGridSchema` declared `showFilters`, but an `object-grid` has no filter
11+
UI and never read the key. An author who wrote `showFilters: true` on a grid got no type error,
12+
no validator refusal and no filter.
13+
14+
**Where filtering lives.** The filter builder is the `list-view` toolbar's. Author a `list-view`
15+
and switch the builder with its `userActions.filter`. To narrow the rows a grid fetches, write
16+
`filter` on the grid.
17+
18+
**How it is refused.** The key is `?: never` on the `ObjectGridSchema` interface and a named
19+
refusal on its Zod twin. On both authoring faces, the node face and the strict authoring face,
20+
the message names the key and `userActions.filter`. This matches the upstream protocol, whose
21+
`object-grid` row does not declare the key either.
22+
23+
**Unchanged.** An `object-view`'s own `showFilters` and a `list-view`'s are different keys, and
24+
both are still accepted. `ObjectView` reads its own, and `ListView` folds its own onto
25+
`userActions.filter`. An `object-view`'s `table` slot already refused `showFilters` and still
26+
does.

‎README.md‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,6 @@ published is not an error: the expression is returned as its own source text.
222222
{ "field": "created_at", "label": "Joined" }
223223
],
224224
"showSearch": true,
225-
"showFilters": true,
226225
"operations": { "create": true, "read": true, "update": true, "delete": true, "export": true }
227226
}
228227
```

‎content/docs/api/schema-reference.md‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -766,9 +766,9 @@ A data grid that auto-fetches from an ObjectQL object definition. Includes searc
766766
```
767767

768768
Every key in this example is one the grid reads (objectui#11068). It used to
769-
also author `showFilters`, `striped` and `operations.read`, which nothing reads,
770-
and `selection: { enabled, mode }` and `pagination.enabled`, which the validator
771-
refuses: selection is spelled `selection: { type: 'multiple' }` (`'none'`,
769+
also author `showFilters` (now retired on this node, below), `striped` and
770+
`operations.read`, which nothing reads, and `selection: { enabled, mode }` and
771+
`pagination.enabled`, which the validator refuses: selection is spelled `selection: { type: 'multiple' }` (`'none'`,
772772
`'single'` or `'multiple'`), and `pagination` declares no on switch — its
773773
presence enables paging.
774774

@@ -798,6 +798,14 @@ presence enables paging.
798798
> `@object-ui/types` refuse them by name. Write `rowActions`, `bulkActions`, `id`
799799
> or `label`, and `emptyState: { "message": … }` instead.
800800
801+
> **`showFilters` is retired on this node too (objectui#11068).** An `object-grid`
802+
> has no filter UI, and nothing read the key. The filter builder is the
803+
> `list-view` toolbar's: author a `list-view` node and switch it with
804+
> `"userActions": { "filter": true }`. To narrow the rows a grid fetches, write
805+
> `filter`. Both faces of `@object-ui/types` refuse an authored `showFilters` on an
806+
> `object-grid` by name. An [`object-view`](#objectviewschema)'s own `showFilters`,
807+
> below, is a different key and is unchanged.
808+
801809
> **`defaultSort` is retired (objectui#5861).** `ObjectGridSchema` used to accept a
802810
> legacy single-entry `defaultSort: { field, order }` beside `sort`. The installed
803811
> `@objectstack/spec` protocol refuses it by name (a retired-key tombstone), and no

‎content/docs/plugins/plugin-grid.mdx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ this node (objectui#11068): nothing ever read them, and both faces of
135135
`@object-ui/types` refuse them by name. Write `id` or `label`,
136136
`emptyState: { message }`, `rowActions` and `bulkActions` instead.
137137

138+
`showFilters` is **retired** on this node too (objectui#11068). The grid has no
139+
filter UI, and nothing read the key. The filter builder is the `list-view`
140+
toolbar's, switched by its `userActions.filter`; to narrow the rows a grid
141+
fetches, write `filter`. An `object-view`'s own `showFilters` is a different key
142+
and is unchanged.
143+
138144
### Column Definition
139145

140146
A column is either a **field name** (`"name"`) or a `ListColumn` object.

‎packages/plugin-grid/src/__tests__/ObjectGrid.declaredKeys-11068.test.tsx‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@
1010
* Every key `ObjectGridSchema` declares takes effect or is retired
1111
* (objectui#11068) — the RENDERER half. The declaration half (the four
1212
* tombstones and the `emptyState` mirror) is `packages/types`'
13-
* `object-grid-declared-keys-11068.test.ts`.
13+
* `object-grid-declared-keys-11068.test.ts`; the fifth tombstone, `showFilters`,
14+
* is `object-grid-show-filters-retired-11068.test.ts` there.
1415
*
1516
* 1. `description` draws one line of help text above the grid, resolved like
1617
* `label` — a locale map against the display locale.
1718
* 2. `emptyState` draws in place of an empty table — but not when a term in
1819
* the grid's own server-side search box emptied it, where the table and its
1920
* search box must stay.
20-
* 3. `name`, `placeholder`, `rowSpecActions` and `bulkSpecActions` — the four
21-
* keys the card retired — draw nothing. Asked with a RULER, the same
22-
* document drawn twice and compared as bytes, because `ObjectGridRenderer`
21+
* 3. `name`, `placeholder`, `rowSpecActions`, `bulkSpecActions` and
22+
* `showFilters` — the five keys the card retired — draw nothing. Asked
23+
* with a RULER, the same document drawn twice and compared as bytes,
24+
* because `ObjectGridRenderer`
2325
* hands a node's leftover keys on as props and a source grep cannot see
2426
* that channel (AGENTS.md, "a source grep's zero cannot answer this").
2527
*
@@ -172,7 +174,7 @@ describe('`emptyState` draws in place of an empty table (objectui#11068)', () =>
172174
});
173175
});
174176

175-
/* ── 3. The four retired keys draw nothing ────────────────────────────────── */
177+
/* ── 3. The five retired keys draw nothing ────────────────────────────────── */
176178

177179
/** Draw one document and return its markup, once the rows are on screen. */
178180
async function draw(schema: Record<string, unknown>): Promise<string> {
@@ -198,13 +200,17 @@ describe('the ruler', () => {
198200
});
199201
});
200202

201-
describe('the four keys objectui#11068 retired change nothing the grid draws', () => {
203+
describe('the five keys objectui#11068 retired change nothing the grid draws', () => {
202204
it.each([
203205
['name', 'all_contacts'],
204206
['placeholder', 'Nothing here yet'],
205207
['rowSpecActions', ['edit', 'delete']],
206208
['bulkSpecActions', ['delete']],
207-
])('`%s`', async (key, value) => {
209+
// Retired after triage's retriage answer: the grid has no filter UI, so
210+
// neither value draws anything — the filter builder is `list-view`'s.
211+
['showFilters', true],
212+
['showFilters', false],
213+
])('`%s` (%j)', async (key, value) => {
208214
const a = await draw(grid(ROWS));
209215
cleanup();
210216
const b = await draw(grid(ROWS, { [key]: value }));

‎packages/plugin-grid/src/__tests__/ObjectGrid.operatorsInert-9729.test.tsx‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@
2929
* draw identical bytes (the ruler is stable — no timestamps, no random ids), the
3030
* second is a LIT CONTROL on a key the renderer demonstrably DOES read, and only
3131
* then is the measurement taken. It is taken twice: once on a plain grid and
32-
* once with the filter surface on, since `operators` is a filtering word and a
33-
* grid without a filter affordance would be the wrong corpus to ask.
32+
* once with the grid's search box on (`searchableFields`), since `operators` is
33+
* a filtering word and a grid without its one query affordance would be the
34+
* wrong corpus to ask. That corpus also used to carry `showFilters: true`, which
35+
* never drew anything: an `object-grid` has no filter UI, and objectui#11068
36+
* retired the key on this node.
3437
*
3538
* ⚠️ The claim is bounded by the ruler: this is what the RENDERED output does
3639
* with the key, on these documents. ⛔ It is not a claim that no code anywhere
@@ -88,11 +91,11 @@ describe('an authored `operators`', () => {
8891
expect(b).toBe(a);
8992
});
9093

91-
it('changes nothing with the filter surface on either', async () => {
92-
const FILTERS = { ...BASE, showFilters: true, searchableFields: ['name'] };
93-
const a = await draw({ ...FILTERS });
94+
it('changes nothing with the search box on either', async () => {
95+
const SEARCH = { ...BASE, searchableFields: ['name'] };
96+
const a = await draw({ ...SEARCH });
9497
cleanup();
95-
const b = await draw({ ...FILTERS, operators: { name: ['equals', 'contains'] } });
98+
const b = await draw({ ...SEARCH, operators: { name: ['equals', 'contains'] } });
9699
expect(b).toBe(a);
97100
});
98101
});
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* `showFilters` is retired on `object-grid` (objectui#11068, triage's answer to
11+
* the card's retriage) — the DECLARATION half. The renderer half, which shows an
12+
* authored `showFilters` draws nothing, is `plugin-grid`'s
13+
* `ObjectGrid.declaredKeys-11068.test.tsx`.
14+
*
15+
* ## Why a tombstone, and why it points at the list view
16+
*
17+
* `ObjectGrid` has no filter UI. The one filter surface objectui draws is the
18+
* `list-view` toolbar's filter builder, switched by `userActions.filter`, so
19+
* honouring the key on the grid would have meant building a second filter
20+
* surface. The upstream protocol's `object-grid` row does not declare the key
21+
* either. Deleting the member would refuse nothing: the zod twin ends
22+
* `.passthrough()` and the interface inherits `BaseSchema`'s index signature.
23+
* So the key stays DECLARED and unwritable on both faces — `retirementTombstone()`
24+
* and `?: never` — as `rowSpecActions` and `defaultSort` are.
25+
*
26+
* ## The two authoring faces, and the controls beside each refusal
27+
*
28+
* The node face (`safeValidateSchema`) and the strict authoring face
29+
* (`StrictAnyComponentSchema`) are both asked. A red parse proves a by-name
30+
* refusal only beside a document that stays green on the same instrument: the
31+
* grid without the key, a list view's own `showFilters` and `userActions.filter`,
32+
* and an object view's own `showFilters` — which `ObjectView` still reads, so it
33+
* is not retired here.
34+
*/
35+
36+
import { describe, it, expect } from 'vitest';
37+
import type { z } from 'zod';
38+
import { ObjectGridPropsSchema as SpecObjectGridPropsSchema, ListViewSchema as SpecListViewSchema } from '@objectstack/spec/ui';
39+
import { ObjectGridSchema } from '../zod/objectql.zod.js';
40+
// Both faces through the barrel: objectui#8345 pins it as the only entry into
41+
// the strict face's module cycle, test files included.
42+
import { safeValidateSchema, StrictAnyComponentSchema } from '../zod/index.zod.js';
43+
import type {
44+
ListViewSchema as ListViewSchemaType,
45+
ObjectGridSchema as ObjectGridSchemaType,
46+
ObjectViewSchema as ObjectViewSchemaType,
47+
} from '../objectql.js';
48+
49+
/** A control key no surface in this package or the protocol declares. */
50+
const UNKNOWN_KEY = 'zzzNotAKeyAnySurfaceDeclares11068';
51+
52+
/** The minimum green `object-grid` document. */
53+
const GRID = { type: 'object-grid' as const, objectName: 'probe' };
54+
55+
type Issue = { code: string; path: PropertyKey[]; message: string; errors?: Issue[][] };
56+
57+
/** The two authoring faces, each as a `safeParse`. */
58+
const FACES = [
59+
['node face (safeValidateSchema)', (doc: unknown) => safeValidateSchema(doc)],
60+
['strict authoring face (StrictAnyComponentSchema)', (doc: unknown) => StrictAnyComponentSchema.safeParse(doc)],
61+
] as const;
62+
63+
type Parse = (doc: unknown) => z.ZodSafeParseResult<unknown>;
64+
65+
/** Every issue in the tree, union arms included, with its full path. */
66+
function flatten(issues: Issue[] | undefined, prefix: PropertyKey[] = []): Issue[] {
67+
const out: Issue[] = [];
68+
for (const issue of issues ?? []) {
69+
const path = [...prefix, ...issue.path];
70+
out.push({ ...issue, path });
71+
for (const arm of issue.errors ?? []) out.push(...flatten(arm, path));
72+
}
73+
return out;
74+
}
75+
76+
function issueAt(parse: Parse, doc: unknown, path: string): Issue | undefined {
77+
const result = parse(doc);
78+
if (result.success) return undefined;
79+
return flatten(result.error.issues as unknown as Issue[]).find((i) => i.path.map(String).join('.') === path);
80+
}
81+
82+
describe('an authored `showFilters` on `object-grid` is refused BY NAME on both authoring faces (objectui#11068)', () => {
83+
it.each(FACES)('%s refuses it at its own path, whatever the value', (_face, parse) => {
84+
for (const value of [true, false]) {
85+
const result = parse({ ...GRID, showFilters: value });
86+
expect(result.success, `showFilters: ${value} parsed green`).toBe(false);
87+
const issue = issueAt(parse, { ...GRID, showFilters: value }, 'showFilters');
88+
expect(issue, `no issue at showFilters for ${value}`).toBeDefined();
89+
// `retirementTombstone()` customises the MESSAGE only; the code is a bare `z.never()`'s.
90+
expect(issue?.code).toBe('invalid_type');
91+
}
92+
});
93+
94+
it.each(FACES)('%s names the card and the list view’s `userActions.filter`', (_face, parse) => {
95+
const message = String(issueAt(parse, { ...GRID, showFilters: true }, 'showFilters')?.message);
96+
expect(message).toContain('objectui#11068');
97+
expect(message).toContain('`userActions.filter`');
98+
expect(message).toContain('`list-view`');
99+
});
100+
101+
it.each(FACES)('LIT CONTROL — %s accepts the same grid without the key', (_face, parse) => {
102+
const result = parse(GRID);
103+
expect(result.success, result.success ? '' : JSON.stringify(result.error.issues)).toBe(true);
104+
});
105+
106+
it('LIT CONTROL — the node face still keeps an unrecognised key on the same document', () => {
107+
// Without this, the refusal above would be consistent with the whole object
108+
// having turned strict on the node face — a far larger change than the one made.
109+
expect(safeValidateSchema({ ...GRID, [UNKNOWN_KEY]: true }).success).toBe(true);
110+
});
111+
112+
it('writes the SAME string into the metadata channel as into the parse message', () => {
113+
const described = (ObjectGridSchema.shape.showFilters as { description?: string }).description;
114+
const issue = ObjectGridSchema.safeParse({ ...GRID, showFilters: true });
115+
expect(issue.success).toBe(false);
116+
const message = issue.success ? undefined : issue.error.issues.find((i) => i.path.join('.') === 'showFilters')?.message;
117+
expect(described).toBe(message);
118+
});
119+
120+
it('the tombstone is a MEMBER, not a deletion — that is what makes the refusal loud', () => {
121+
expect('showFilters' in ObjectGridSchema.shape).toBe(true);
122+
expect(UNKNOWN_KEY in ObjectGridSchema.shape).toBe(false);
123+
});
124+
});
125+
126+
describe('`showFilters` stays live where a renderer reads it (the controls the retirement must not touch)', () => {
127+
const LIST = { type: 'list-view' as const, objectName: 'probe', columns: ['name'] };
128+
129+
it.each(FACES)('%s accepts the list view’s `userActions.filter` — the spelling the refusal names', (_face, parse) => {
130+
const result = parse({ ...LIST, userActions: { filter: true } });
131+
expect(result.success, result.success ? '' : JSON.stringify(result.error.issues)).toBe(true);
132+
});
133+
134+
it.each(FACES)('%s accepts a list view’s own `showFilters`', (_face, parse) => {
135+
const result = parse({ ...LIST, showFilters: true });
136+
expect(result.success, result.success ? '' : JSON.stringify(result.error.issues)).toBe(true);
137+
});
138+
139+
it.each(FACES)('%s accepts an object view’s own `showFilters`', (_face, parse) => {
140+
const result = parse({ type: 'object-view', objectName: 'probe', showFilters: true });
141+
expect(result.success, result.success ? '' : JSON.stringify(result.error.issues)).toBe(true);
142+
});
143+
});
144+
145+
describe('the UPSTREAM half, re-derived from the installed pin', () => {
146+
it('upstream `object-grid` does not declare `showFilters`, and refuses it', () => {
147+
expect('showFilters' in SpecObjectGridPropsSchema.shape).toBe(false);
148+
expect(SpecObjectGridPropsSchema.safeParse({ objectName: 'probe' }).success).toBe(true);
149+
expect(SpecObjectGridPropsSchema.safeParse({ objectName: 'probe', showFilters: true }).success).toBe(false);
150+
});
151+
152+
it('upstream `list-view` declares the toggle the refusal names: `userActions.filter`', () => {
153+
const userActions = SpecListViewSchema.shape.userActions.unwrap();
154+
expect('filter' in userActions.shape).toBe(true);
155+
});
156+
});
157+
158+
/* ── The TypeScript twin ────────────────────────────────────────────────── */
159+
160+
/**
161+
* The `@ts-expect-error` line below IS the assertion: the member is `?: never`,
162+
* so the literal is a compile error. It fails this package's
163+
* `tsc -p tsconfig.test.json` leg as an UNUSED directive if the member is
164+
* deleted (the key would fall back to `BaseSchema`'s index signature) or
165+
* re-typed as a live value.
166+
*/
167+
export const authoredShowFiltersRefused: ObjectGridSchemaType = {
168+
...GRID,
169+
// @ts-expect-error — `showFilters` is RETIRED on `object-grid` (objectui#11068): a list view's `userActions.filter`.
170+
showFilters: true,
171+
};
172+
173+
/** LIT CONTROL: the spellings that stay live are declared, writable members. */
174+
export const listViewFilterToggleIsDeclared: ListViewSchemaType = {
175+
type: 'list-view',
176+
objectName: 'probe',
177+
columns: ['name'],
178+
userActions: { filter: true },
179+
};
180+
181+
export const objectViewShowFiltersIsDeclared: ObjectViewSchemaType = {
182+
type: 'object-view',
183+
objectName: 'probe',
184+
showFilters: true,
185+
};
186+
187+
describe('the TypeScript twin', () => {
188+
it('is compiled by this package’s type-check leg, which is where the bindings above are read', () => {
189+
expect([authoredShowFiltersRefused, listViewFilterToggleIsDeclared, objectViewShowFiltersIsDeclared]).toHaveLength(3);
190+
});
191+
});

‎packages/types/src/__tests__/object-view-slot-key-lists.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ const FORM_IDENTITY_KEYS = ['type', 'objectName', 'mode'] as const;
129129
*/
130130
const TABLE_WITHHELD_BY_REASON = {
131131
/**
132-
* `ObjectGrid` has no read of it, so nothing could draw it. Four of these are
132+
* `ObjectGrid` has no read of it, so nothing could draw it. Five of these are
133133
* retirement tombstones on `ObjectGridSchema` itself since objectui#11068
134-
* (`bulkSpecActions`, `name`, `placeholder`, `rowSpecActions`).
134+
* (`bulkSpecActions`, `name`, `placeholder`, `rowSpecActions`, `showFilters`).
135135
*/
136136
unread: ['bulkSpecActions', 'keyboardNavigation', 'name', 'placeholder', 'rowSpecActions', 'showFilters'],
137137
/**
@@ -157,7 +157,7 @@ const TABLE_WITHHELD_KEYS: readonly string[] = Object.values(TABLE_WITHHELD_BY_R
157157
/**
158158
* Retirement tombstones `ObjectGridSchema` declares itself (`?: never` on both
159159
* faces) that the slot keeps: they type nothing, and they carry the named
160-
* refusal and its guidance onto this face too. The four the grid retired in
160+
* refusal and its guidance onto this face too. The five the grid retired in
161161
* objectui#11068 are in the withheld set above instead, refused by the slot's
162162
* own message.
163163
*/

0 commit comments

Comments
 (0)