Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit b13ea3c

Browse files
feat(types)!: ObjectGridSchema.defaultFilters and the flat gantt / map filter follow their @objectstack/spec rows by reference (objectui#6152, round 10) (#11993)
Part of #6152 (round 10). Round 11 (the list VIEW's per-view-type blocks) is still to come, so #6152 remains open. Clause-②: yes — it widens: the flat `ObjectGridSchema.defaultFilters` mirror, and through it an `object-view`'s `table.defaultFilters`, now ACCEPTS the `ViewFilterRule` array the `object-grid` row declares (the old `z.record(z.string(), z.any())` refused it). It narrows: the record form of `defaultFilters` is refused on both faces, and the flat `ObjectGanttSchema` / `ObjectMapSchema` `filter` refuse the AST tuple array they took as `any[]`. ## What changed Three flat members now take their `@objectstack/spec` 17.7.0 row's own member by reference, on the TypeScript interface (`packages/types/src/objectql.ts`) and on the zod mirror (`packages/types/src/zod/objectql.zod.ts`): | member | was | now | |---|---|---| | `ObjectGridSchema.defaultFilters` | `Record` (string keys, any values) / `z.record(z.string(), z.any())` | `ObjectGridProps['defaultFilters']` / `ObjectGridPropsSchema.shape.defaultFilters` | | `ObjectGanttSchema.filter` | `any[]` / `z.array(z.any())` | `ObjectGanttProps['filter']` / `ObjectGanttPropsSchema.shape.filter` | | `ObjectMapSchema.filter` | `any[]` / `z.array(z.any())` | `ObjectMapProps['filter']` / `ObjectMapPropsSchema.shape.filter` | Each is the `ViewFilterRule` array `[{ field, operator, value }, ...]`. The zod members are the rows' own objects (identity is pinned), so a record is refused with the protocol's own `ruleArrayFilterError` message, which computes the rule array from the author's keys. `defaultFilters` keeps its `@deprecated` tag: the row's own description still says "Prefer `filter`". **Doors.** The flat grid mirror is the source of the `object-view` `table` slot, so `table.defaultFilters` (objectui's own door) moves with it on the tolerant face, the strict face and `safeValidateSchema`. The authored `object-grid` node's `properties` bag is the row itself and already gave these verdicts (pinned as a control). The flat gantt / map mirrors are not authoring arms (objectui#10859): their authored nodes' bags are the rows, which already refused the tuple array, so no authored door moves there. **No reader changed.** `ObjectGrid` already lowers `defaultFilters` through the same `toFilterNode` sink as `filter` (objectui#4082), so the spec's shape was already honoured; this was a declaration gap, not an implementation gap. **Sentences this head would have made false, repaired** (each called the key `Record` typed): the `defaultFilters` comment in `ObjectGrid.tsx`'s load effect; the `toFilterNode` docblock in `packages/core/src/utils/filter-converter.ts`; the `mergeFilterNodes` comment in `packages/plugin-view/src/ObjectView.tsx` (outside the claim's listed surface: a one-comment repair, stated here as the round-9 ACCEPT treated its two doc sentences); and the "write this / not this" row in `content/docs/plugins/plugin-view.mdx` and `packages/plugin-view/README.md`, which taught `defaultFilters` as a record of field to value (equality-only). ## Measurement first **1. The spec's shape** (installed `@objectstack/spec` 17.7.0, `ui/component.zod.ts`): the `object-grid` row's `defaultFilters` is `z.array(ViewFilterRuleSchema, { error: ruleArrayFilterError(...) }).optional()`, the same wrapper chain as `filter`, described "Legacy base-filter fallback, read only when `filter` is absent ... Prefer `filter`" (narrowed, not retired, by the row's own note). The gantt and map rows' `filter` are the same rule-array member. The pin re-reads all three on every run. **2. The reader (H2 holds).** Measured through the registered `object-grid` renderer and a `ValueDataSource` that applies the `$filter` it receives: `defaultFilters: [{ field: 'status', operator: 'equals', value: 'open' }]` sends `[['status', 'equals', 'open']]` and draws the two open rows of three; the same array written as `filter` sends the same `$filter` and draws the same rows. Lit control: a `closed` rule moves both readings (one row). Controls: no key draws all three and sends no `$filter`; written both ways, `filter` wins. New pin: `packages/plugin-grid/src/__tests__/ObjectGrid.defaultFiltersRuleArray-6152.test.tsx`. **3. Corpus census, before any edit.** A TypeScript-AST census over the authored corpus (`examples/` with the schema catalog, `content/`, `skills/`, `docs/`, `apps/`, the root README and every package README; tests and CHANGELOGs excluded): 941 files. Controls: the extractor saw 53 `object-grid`, 46 `object-view`, 27 `object-gantt` and 22 `object-map` node literals, and 11 `filter` keys on typed nodes. | looked for | authored occurrences | |---|---| | a `defaultFilters` key on any node (flat, in a bag, or in an `object-view` `table`), either shape | **0** | | a `filter` on an `object-gantt` / `object-map` node, flat or in its bag, any shape | **0** | | prose naming `defaultFilters` | 9 lines in 2 documents (`plugin-view.mdx`, `plugin-view` README); 2 of them taught the record shape, corrected here | objectstack (read-only): no authored `defaultFilters` outside tests and the spec package; the spec ships its own migration (`object-grid-default-filters-rule-array`). **Test fixtures, classified separately.** Re-spelled to the rule array because a typed fixture became a `tsc` error: `ObjectGrid.filterContextTokens-10607` (2 cells), `ObjectView.tableSlotRelay-10976` (1, now a value distinct from `filter`'s so a swap cannot read as arrival), `ObjectMap.schemaAlignment` (1), `gantt-flat-config-declared-keys` (the `GOOD` value and the `_FilterIsDeclared` row); `object-gallery-filter-9309`'s source census updated for the two new spellings. Left as written: 18 record-valued cells in 5 untyped runtime tests (`gridDefaultFiltersLowering`, `ObjectGrid.malformedFilter-9050`, `ObjectView.canonicalTableKeys`, `ObjectView.filterSources`, `ObjectView.namedViewContextTokens-10506`). They pin the READ, which did not narrow (the objectui#10199 split, as round 8 left its AST fixtures); `gridDefaultFiltersLowering`'s two comments that called the record "the declared shape" now say "declared until this round". **H1** holds for authored nodes (zero) and is falsified in prose: two documentation rows taught the record. **H3** holds: the flat gantt / map `filter` readers are `ObjectGantt` and `ObjectMap` (each `useResolvedFilter(schema.filter, …)` onto `$filter`), plus `ObjectMap`'s legacy `filter.map` probe, which reads the value as `unknown`, warns, and was already outside `any[]` for a record. `ElementDataSourceGate` writes a merged AST to `filter` at runtime, and `ListView` writes its effective filter onto a gantt node from an `unknown`-typed value. `tsc` on all of them stays green; no renderer changed. **Not by reference, stated:** the tuple-array refusal is the row's element-level message (`expected object, received array` at `filter.0`), which does not name the rule array; only the record refusal carries the conversion. Changing that would mean a local copy of the member, so it is left to the spec. ## Tests On head `e2fe94285`, under `os-verify-lock`: - `pnpm exec vitest run --maxWorkers=2 packages/types/ packages/plugin-grid/ packages/plugin-gantt/ packages/plugin-map/ packages/plugin-view/ packages/core/ apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`: `Test Files 962 passed | 1 skipped (963)`, `Tests 17511 passed | 141 skipped (17652)`. - `pnpm exec vitest run --maxWorkers=2 scripts/__tests__/`: `Test Files 179 passed | 2 skipped (181)`, `Tests 5452 passed | 2 skipped (5454)`. - The lock held 26m07s for both and the doc gates' build closure (35/35 turbo tasks), on a shared box. - The two new pins alone: the `@object-ui/types` round-10 pin (`grid-default-filters-gantt-map-filter-round10-6152.test.ts`) 84 tests; the `plugin-grid` H2 pin 5 tests. **`type-check`** (script name echoed on every row) after `turbo run build --filter='@object-ui/console^...' --filter='@object-ui/example-schema-catalog^...'` (34/34 tasks): `types`, `core`, `app-shell`, `plugin-calendar`, `plugin-dashboard`, `plugin-designer`, `plugin-gantt`, `plugin-grid`, `plugin-kanban`, `plugin-list`, `plugin-map`, `plugin-tree`, `plugin-view`, `console`, `example-schema-catalog`: every package whose source names `ObjectGridSchema`, `ObjectGanttSchema`, `ObjectMapSchema` or `ObjectViewSchema` (the consumers, downstream of `@object-ui/types`). The first sweep was red only on the three typed fixtures above (`plugin-grid`, `plugin-map`, `plugin-view` test configs; every package's `src` compiled); green after the re-spelling, against a rebuilt `types` dist. **Gates**, all exit 0, on the same head after that build: - `pnpm check:doc-snippets` (784 of 784 blocks judged, 0 failed), `check:doc-examples`, `check:skill-examples`, `check:doc-types`, `check:readme-exports` - `check:spec-symbols`; `check:component-surface-parity` (report-only, no row for these members) - `node scripts/check-changeset-presence.mjs` (13 source files of 5 released packages, 1 changeset), `node scripts/check-changeset-no-major.mjs`, `check:changeset-claims`, `check:pending-changeset-literals` - `check:new-line-citations` (0 new), `check:control-bytes` - `node scripts/check-governed-queue-guard.mjs --test` over the 16 paths: NOT GOVERNED **ESLint** over the 13 changed TS files: 0 errors (warnings pre-existing `no-explicit-any` / react-hooks). ## Ablations (one-shot, through `ablation-replace.mjs`; each mutation proven on disk, each restore to the `HEAD` blob with an empty `git diff HEAD`) Predictions were written before the runs; every observed direction matched. - **A.** `defaultFilters` mirror back to `z.record(z.string(), z.any()).optional()`: the round-10 pin goes **27 red / 57 green**, exactly the predicted set (member identity; the grid `defaultFilters` verdict rows for the rule array, the empty array, the record and the empty record; on each of the three doors the rule-array, record and tuple rows and the same four verdict rows; the `object-view` mirror's slot). Green: the precondition, the gantt / map rows, the grid `filter` lit control, the door lit controls, the authored-bag control. - **B.** gantt mirror `filter` back to `z.array(z.any()).optional()`: **4 red** (the gantt / map identity test; the gantt tuple, AST and unknown-operator verdict rows); `gantt-flat-config-declared-keys` stays green (its refused value is a string). - **C.** a consumer literal in `plugin-grid/src/ObjectGrid.tsx`, typed `ObjectGridSchema`, with `defaultFilters: { status: 'open' }`: `plugin-grid` `tsc` **red** (`TS2353`, `status` not in the rule type); control leg with the rule array **green**. This proves the consumer reads the rebuilt `.d.ts`. - **D.** the same in `plugin-gantt/src/ObjectGantt.tsx` with `filter: [['status', '=', 'open']]`: **red** (`TS2322`); control leg **green**. - The first C/D attempt was a no-op: `ablation-replace` refused it because the replacement re-contained the anchor, ran nothing, and restored. Re-run with an anchor-altering replacement; the numbers above are the second attempt's. ## Acceptance notes - `packages/plugin-list/src/ListView.tsx`'s `buildEffectiveFilter` docblock still says `table.defaultFilters` is "declared `Record`". Fenced from this round (objectui#11860 is in flight on that file). Carrier: objectui#11860's PR, or the next PR that touches `ListView.tsx`. - `ObjectView.tsx`'s `gridSchema` memo comment says `ObjectGrid` "raw-assigns the legacy" `defaultFilters` slot. That has been false since objectui#4082 (it is lowered through the same sink), not made false here. Carrier: the next PR that touches that memo. - The gantt and map `Schema API` blocks in `content/docs/plugins/plugin-gantt.mdx` / `plugin-map.mdx` spell the bag's `filter` as an array of `any`. Not false (the row takes an array), only imprecise. Carrier: none. Implemented by the dispatched `os-dev` run (mode subagent) in session `https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw`, claim `6063790756`. --- _Generated by [Claude Code](https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d21bd08 commit b13ea3c

16 files changed

Lines changed: 542 additions & 47 deletions
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
'@object-ui/types': minor
3+
'@object-ui/core': patch
4+
'@object-ui/plugin-grid': patch
5+
'@object-ui/plugin-map': patch
6+
'@object-ui/plugin-view': patch
7+
---
8+
9+
feat(types)!: `ObjectGridSchema.defaultFilters` and the flat `ObjectGanttSchema` / `ObjectMapSchema` `filter` follow their `@objectstack/spec` rows (objectui#6152, round 10)
10+
11+
Clause-②: yes
12+
13+
`@objectstack/spec` has typed `ComponentPropsMap['object-grid'].defaultFilters` as the same
14+
`ViewFilterRule` array as `filter`, `[{ field, operator, value }, ...]`, since 17.6.0: the legacy
15+
fallback `ObjectGrid` reads only when `filter` is absent, refusing the MongoDB-style record, a bare
16+
string and the AST tuple array. The `object-gantt` and `object-map` rows type `filter` the same
17+
way. `@object-ui/types` now takes each row's own member by reference, on the TypeScript interface
18+
and on the zod mirror, with no alias window.
19+
20+
**Widened.** `ObjectGridSchema.defaultFilters` was `Record<string, any>` and
21+
`z.record(z.string(), z.any())`, so the zod mirror REFUSED the rule array the row declares. The
22+
flat grid mirror is the source of an `object-view`'s `table` slot, so
23+
`table: { defaultFilters: [{ field: 'status', operator: 'equals', value: 'open' }] }` now parses
24+
there, on the tolerant and the strict face and through `safeValidateSchema`.
25+
26+
**Narrowed (breaking).**
27+
28+
- The record form of `defaultFilters` is refused: on the interface (a compile error, in an
29+
`object-view`'s `table` too) and on the zod mirror, at `defaultFilters` (`table.defaultFilters`
30+
in an `object-view`), with the protocol's own message, which computes the rule array from the
31+
record's keys. Respell
32+
`defaultFilters: { status: 'open' }` as
33+
`defaultFilters: [{ field: 'status', operator: 'equals', value: 'open' }]`, or, better, move it
34+
to `filter`, which takes the same array and wins when both are written.
35+
- `ObjectGanttSchema.filter` and `ObjectMapSchema.filter` were `any[]` and `z.array(z.any())`, so
36+
`filter: [['status', '=', 'open']]` type-checked and parsed. Both are the row's rule array now;
37+
respell the tuple as `[{ field: 'status', operator: 'equals', value: 'open' }]`. These two flat
38+
types describe the node as the renderers read it: an authored `object-gantt` / `object-map`
39+
node's `properties` bag is the row itself, which refused the tuple array already.
40+
41+
What did not move: the renderers' reads. `ObjectGrid` lowers `defaultFilters` through the same
42+
`toFilterNode` sink as `filter`, so a rule array there sends the same `$filter` and draws the same
43+
rows as the same array written as `filter`; the sink still lowers a record or an AST that reaches
44+
the slot at runtime, and `ObjectGantt` / `ObjectMap` still forward an AST a host composes. The
45+
`@object-ui/core`, `@object-ui/plugin-grid` and `@object-ui/plugin-view` entries are comment
46+
repairs to sentences that called the key `Record<string, any>`. `@object-ui/plugin-grid`,
47+
`@object-ui/plugin-view` and `@object-ui/plugin-map` also carry typed test fixtures re-spelled to
48+
the rule array, and `@object-ui/plugin-grid` a pin of the above through the real renderer.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ its legacy half is retired — see the note above.)
171171
| --- | --- |
172172
| `pagination: { pageSize, pageSizeOptions? }` | `pageSize: number` |
173173
| `selection: { type: 'single' \| 'multiple' \| 'none' }` | `selectable: boolean \| 'single' \| 'multiple'` |
174-
| `filter: [{ field, operator, value }, …]` (same shape as a named view's `filter`) | `defaultFilters: Record<field, value>` (equality-only) |
174+
| `filter: [{ field, operator, value }, …]` (same shape as a named view's `filter`) | `defaultFilters: [{ field, operator, value }, …]` — the same rule array; TypeScript and the validator refuse the record form `{ field: value }` (objectui#6152 round 10) |
175175

176176
Before objectui#5102, `pagination` / `selection` / `filter` / `sort` had **no
177177
read point at all** in this file: an author who wrote the canonical shape

‎packages/core/src/utils/filter-converter.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,8 +1397,12 @@ function viewFilterRuleToNode(rule: ViewFilterRuleLike): FilterNode {
13971397
*
13981398
* The third is the one that kept getting lost. Renderers tested `source.length
13991399
* > 0` before using it, which is `undefined > 0` for an object — so a
1400-
* `table.defaultFilters` (declared `Record<string, any>`) was DROPPED and the
1401-
* view returned every record. Silently: no error, just a wider answer.
1400+
* `table.defaultFilters` (declared `Record<string, any>` until objectui#6152
1401+
* round 10; the protocol's `ViewFilterRule` array since, and the record no
1402+
* longer validates there) was DROPPED and the view returned every record.
1403+
* Silently: no error, just a wider answer. That narrowing moved what an author
1404+
* may write, not this sink: the object arm below still lowers a record whatever
1405+
* caller hands it one.
14021406
*
14031407
* The FIRST never worked at all (objectui#3431). The array branch returned
14041408
* every array VERBATIM, so a saved view's `ViewFilterRule[]` travelled to

‎packages/plugin-grid/src/ObjectGrid.tsx‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2629,13 +2629,14 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
26292629
// already go through `toFilterNode`/`mergeFilterNodes`.
26302630
//
26312631
// Byte-copying is refused on the wire for BOTH shapes this slot
2632-
// carries. `defaultFilters` is declared `Record<string, any>`
2633-
// (the MongoDB-style shape) and `isFilterAST` is false for a plain
2634-
// object; `plugin-view` also forwards an active named view's
2635-
// `ViewFilterRule[]` into this slot (`ObjectView.tsx`, the
2636-
// `gridSchema` memo), and `isFilterAST` is false for an array of
2637-
// rule objects too. Either one answers `400 INVALID_FILTER`
2638-
// (measured against a real backend in objectui#3431).
2632+
// carries. `defaultFilters` was declared `Record<string, any>`
2633+
// (the MongoDB-style shape) until objectui#6152 round 10 — it is the
2634+
// row's `ViewFilterRule` array since, the shape `filter` takes — and
2635+
// `isFilterAST` is false for a plain object; `plugin-view` also
2636+
// forwards an active named view's `ViewFilterRule[]` into this slot
2637+
// (`ObjectView.tsx`, the `gridSchema` memo), and `isFilterAST` is
2638+
// false for an array of rule objects too. Either one answers `400
2639+
// INVALID_FILTER` (measured against a real backend in objectui#3431).
26392640
//
26402641
// `toFilterNode` handles both without new logic: objects route
26412642
// through `convertFiltersToAST`, rule arrays lower element-wise,
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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+
* objectui#6152 round 10 — `defaultFilters` takes the `object-grid` row's
9+
* `ViewFilterRule` array, and the real `ObjectGrid` honours it exactly as it honours
10+
* the same array written as `filter`.
11+
*
12+
* `@object-ui/types` now declares `ObjectGridSchema.defaultFilters` as the row's own
13+
* member (it was `Record<string, any>`). No reader changed: `ObjectGrid` lowers the
14+
* legacy slot through the same `toFilterNode` sink as the canonical one, so a rule
15+
* array there already reached the wire as AST. This file measures that through the
16+
* registered renderer and a `ValueDataSource` that APPLIES the `$filter` it receives,
17+
* so "the same rows" is a reading of what the grid draws, not only of what it sends.
18+
*
19+
* The fixtures are typed `ObjectGridSchema` with no cast: the rule array is what the
20+
* declaration takes.
21+
*/
22+
import { describe, it, expect, vi } from 'vitest';
23+
import { render, screen, waitFor } from '@testing-library/react';
24+
import '@testing-library/jest-dom';
25+
import React from 'react';
26+
import type { ObjectGridSchema } from '@object-ui/types';
27+
import { ValueDataSource } from '@object-ui/core';
28+
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
29+
// Registers `object-grid`.
30+
import '../index';
31+
32+
const ROWS = [
33+
{ id: '1', name: 'Acme', status: 'open' },
34+
{ id: '2', name: 'Beta', status: 'closed' },
35+
{ id: '3', name: 'Cyan', status: 'open' },
36+
];
37+
const NAMES = ROWS.map((r) => r.name);
38+
39+
type Rules = NonNullable<ObjectGridSchema['defaultFilters']>;
40+
const OPEN: Rules = [{ field: 'status', operator: 'equals', value: 'open' }];
41+
const CLOSED: Rules = [{ field: 'status', operator: 'equals', value: 'closed' }];
42+
43+
/** An adapter whose `find` answers through a `ValueDataSource`, so `$filter` is applied. */
44+
function makeAdapter() {
45+
const rows = new ValueDataSource({ items: ROWS, idField: 'id' });
46+
return {
47+
find: vi.fn((resource: string, params: Record<string, unknown>) => rows.find(resource, params as never)),
48+
findOne: vi.fn(),
49+
create: vi.fn(),
50+
update: vi.fn(),
51+
delete: vi.fn(),
52+
getObjectSchema: vi.fn().mockResolvedValue({
53+
name: 'account',
54+
fields: { id: { type: 'text' }, name: { type: 'text' }, status: { type: 'text' } },
55+
}),
56+
};
57+
}
58+
59+
/** Render the node; return the `$filter` its first query sent and the names it drew. */
60+
async function draw(node: ObjectGridSchema): Promise<{ $filter: unknown; drawn: string[] }> {
61+
const adapter = makeAdapter();
62+
const { unmount } = render(
63+
<SchemaRendererProvider dataSource={adapter as never}>
64+
<SchemaRenderer schema={node} />
65+
</SchemaRendererProvider>,
66+
);
67+
await waitFor(() => expect(adapter.find).toHaveBeenCalled());
68+
const params = (adapter.find.mock.calls[0] as [string, Record<string, unknown>])[1];
69+
const answer = await adapter.find.mock.results[0].value;
70+
const expected = (answer as { data: Array<{ name: string }> }).data.map((r) => r.name);
71+
// Wait until the grid has drawn what the source answered, then read the DOM.
72+
if (expected.length > 0) await screen.findByText(expected[0]);
73+
const drawn = NAMES.filter((name) => screen.queryByText(name) !== null);
74+
unmount();
75+
return { $filter: params.$filter, drawn };
76+
}
77+
78+
const node = (extra: Partial<ObjectGridSchema>): ObjectGridSchema => ({
79+
type: 'object-grid',
80+
objectName: 'account',
81+
columns: [{ field: 'name' }],
82+
...extra,
83+
});
84+
85+
describe('objectui#6152 round 10 — a `defaultFilters` rule array draws what the same `filter` draws', () => {
86+
it('CONTROL: with neither key, every row is drawn and no `$filter` goes out', async () => {
87+
const { $filter, drawn } = await draw(node({}));
88+
expect($filter).toBeUndefined();
89+
expect(drawn).toEqual(['Acme', 'Beta', 'Cyan']);
90+
});
91+
92+
it('`defaultFilters: [rule]` sends the lowered AST and draws the matching rows', async () => {
93+
const { $filter, drawn } = await draw(node({ defaultFilters: OPEN }));
94+
expect($filter).toEqual([['status', 'equals', 'open']]);
95+
expect(drawn).toEqual(['Acme', 'Cyan']);
96+
});
97+
98+
it('the same array written as `filter` sends the same `$filter` and draws the same rows', async () => {
99+
const legacy = await draw(node({ defaultFilters: OPEN }));
100+
const canonical = await draw(node({ filter: OPEN }));
101+
expect(legacy).toEqual(canonical);
102+
});
103+
104+
it('LIT CONTROL: a different rule array moves both readings, so the equality above can fail', async () => {
105+
const { $filter, drawn } = await draw(node({ defaultFilters: CLOSED }));
106+
expect($filter).toEqual([['status', 'equals', 'closed']]);
107+
expect(drawn).toEqual(['Beta']);
108+
});
109+
110+
it('CONTROL: written both ways, `filter` still wins', async () => {
111+
const { $filter, drawn } = await draw(node({ filter: OPEN, defaultFilters: CLOSED }));
112+
expect($filter).toEqual([['status', 'equals', 'open']]);
113+
expect(drawn).toEqual(['Acme', 'Cyan']);
114+
});
115+
});

‎packages/plugin-grid/src/__tests__/ObjectGrid.filterContextTokens-10607.test.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ describe('object-grid — the node’s own filter reaches the query resolved (ob
122122

123123
it('resolves the deprecated `defaultFilters` alias too, when `filter` is absent', async () => {
124124
const adapter = makeAdapter();
125-
render(ui(adapter, gridNode({ defaultFilters: { owner: '{current_user_id}' } })));
125+
// objectui#6152 round 10: `defaultFilters` is the row's rule array too (it was a
126+
// `Record`, the `{ owner: '{current_user_id}' }` this cell used to write).
127+
render(ui(adapter, gridNode({ defaultFilters: MINE })));
126128
const sent = JSON.stringify(await queriedFilter(adapter.find));
127129
expect(sent).toContain(USER);
128130
expect(sent).not.toContain('{current_user_id}');
@@ -156,7 +158,7 @@ describe('object-grid — the node’s own filter reaches the query resolved (ob
156158

157159
it('re-queries with the new id when the user changes and only `defaultFilters` carries the token', async () => {
158160
const adapter = makeAdapter();
159-
const node = gridNode({ defaultFilters: { owner: '{current_user_id}' } });
161+
const node = gridNode({ defaultFilters: MINE });
160162
const { rerender } = render(ui(adapter, node));
161163
expect(JSON.stringify(await queriedFilter(adapter.find, 0))).toContain(USER);
162164
await settle();

‎packages/plugin-grid/src/__tests__/gridDefaultFiltersLowering.test.tsx‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828
* is the `400 INVALID_FILTER` measured against a real backend in
2929
* objectui#3431:
3030
*
31-
* - the declared `Record<string, any>` (MongoDB-style) — a plain OBJECT
32-
* where the wire expects an AST node;
31+
* - the `Record<string, any>` (MongoDB-style) the key was declared as until
32+
* objectui#6152 round 10 — a plain OBJECT where the wire expects an AST
33+
* node (the key is the row's `ViewFilterRule` array since, and the record
34+
* no longer validates, but the sink still lowers one that reaches the slot);
3335
* - a `ViewFilterRule[]` — an array of rule OBJECTS, which `plugin-view`
3436
* forwards into this slot for an active named view (`ObjectView.tsx`, the
3537
* `gridSchema` memo: `defaultFilters: viewFilter || …`).
@@ -129,10 +131,11 @@ async function findParamsFor(schema: Record<string, unknown>) {
129131
const BASE = { type: 'object-grid', objectName: 'account', columns: [{ field: 'name' }] };
130132

131133
describe('object-grid — legacy `defaultFilters` is lowered before the wire (objectui#4082)', () => {
132-
it('lowers the declared `Record<string, any>` shape to AST', async () => {
133-
// The shape the key is actually DECLARED as
134-
// (`packages/types/src/objectql.ts:682`). `isFilterAST` is false for a
135-
// plain object, so this is the case that used to guarantee a 400.
134+
it('lowers the legacy `Record<string, any>` shape to AST', async () => {
135+
// The shape the key was DECLARED as until objectui#6152 round 10
136+
// (`ObjectGridSchema.defaultFilters` in `@object-ui/types`; the row's rule
137+
// array since). `isFilterAST` is false for a plain object, so this is the
138+
// case that used to guarantee a 400.
136139
const params = await findParamsFor({ ...BASE, defaultFilters: { status: 'active' } });
137140
expect(params.$filter).toEqual(['status', '=', 'active']);
138141
});

‎packages/plugin-map/src/ObjectMap.schemaAlignment.test.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ describe('ObjectMapSchema declares what the renderer reads (type pins)', () => {
9696
objectName: 'stores',
9797
staticData: [{ id: '1' }],
9898
data: { provider: 'value', items: [{ id: '1' }] },
99-
filter: [['status', '=', 'open']],
99+
// objectui#6152 round 10: `filter` is the `object-map` row's `ViewFilterRule` array
100+
// (it was `any[]`; this cell carried the AST tuple array the row refuses).
101+
filter: [{ field: 'status', operator: 'equals', value: 'open' }],
100102
sort: [{ field: 'name', order: 'desc' }],
101103
map: {
102104
latitudeField: 'lat',

‎packages/plugin-view/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ just no longer the one to reach for:
246246
| --- | --- |
247247
| `pagination: { pageSize, pageSizeOptions? }` | `pageSize: number` |
248248
| `selection: { type: 'single' \| 'multiple' \| 'none' }` | `selectable: boolean \| 'single' \| 'multiple'` |
249-
| `filter: [{ field, operator, value }, …]` (same shape as a named view's `filter`) | `defaultFilters: Record<field, value>` (equality-only) |
249+
| `filter: [{ field, operator, value }, …]` (same shape as a named view's `filter`) | `defaultFilters: [{ field, operator, value }, …]` — the same rule array; TypeScript and the validator refuse the record form `{ field: value }` (objectui#6152 round 10) |
250250

251251
**Precedence when a key is written both ways** — `table: { pagination: {
252252
pageSize: 10 }, pageSize: 50 }`, say — the canonical spelling wins. That is

‎packages/plugin-view/src/ObjectView.tsx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,9 +1392,10 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
13921392
if (!objectSchemaReady) return;
13931393

13941394
try {
1395-
// `mergeFilterNodes` rescues an OBJECT source: `table.defaultFilters` is
1396-
// declared `Record<string, any>`, and the `baseFilter.length > 0` test
1397-
// this replaced read false for it — so a view's default filter was
1395+
// `mergeFilterNodes` rescues an OBJECT source: `table.defaultFilters` was
1396+
// declared `Record<string, any>` (until objectui#6152 round 10; the
1397+
// grid row's `ViewFilterRule` array since), and the `baseFilter.length
1398+
// > 0` test this replaced read false for it — so a view's default filter was
13981399
// dropped and every record came back. The grid path (ObjectGrid) always
13991400
// assigned it directly and was unaffected, so the same view filtered
14001401
// correctly as a grid and returned everything as a calendar/kanban/

0 commit comments

Comments
 (0)