diff --git a/examples/app-showcase/src/ui/pages/my-work.page.ts b/examples/app-showcase/src/ui/pages/my-work.page.ts index ce5453a7a8..e6e1143b37 100644 --- a/examples/app-showcase/src/ui/pages/my-work.page.ts +++ b/examples/app-showcase/src/ui/pages/my-work.page.ts @@ -44,12 +44,19 @@ export const MyWorkPage = definePage({ }, { type: 'element:divider', properties: {} }, // Personal work queue — records owned by the signed-in user. + // `filter`, SINGULAR: that is the key `object-grid` both publishes and + // reads (objectui `plugin-grid/src/index.tsx` declares + // `{ name: 'filter', … }`; `ObjectGrid.tsx` reads `schema.filter` and + // lowers it through `toFilterNode` to `$filter`). The plural spelling + // this line used to carry has ZERO read points in the renderer, so it + // was accepted and then dropped — the queue silently listed every row + // (objectstack#7750). { type: 'object-grid', properties: { objectName: 'showcase_task', columns: ['title', 'project', 'status', 'priority', 'due_date'], - filters: [['owner_id', '=', '{current_user_id}']], + filter: [['owner_id', '=', '{current_user_id}']], }, }, ], diff --git a/examples/app-showcase/test/my-work-visibility.test.ts b/examples/app-showcase/test/my-work-visibility.test.ts index 5f7e268bfb..558f2f80a6 100644 --- a/examples/app-showcase/test/my-work-visibility.test.ts +++ b/examples/app-showcase/test/my-work-visibility.test.ts @@ -67,6 +67,37 @@ const leadershipCard = () => (c) => c.type === 'page:card' && c.properties?.title === 'Leadership View', ); +const workQueueGrid = () => allComponents().find((c) => c.type === 'object-grid'); + +/** + * The two non-canonical spellings this grid must not carry — and the DIFFERENT + * reason each one is absent. They share an assertion; they must never share a + * justification, because only one of them is dead: + * + * - **`filters` is DEAD.** Zero read points in the renderer on any ref, so it + * is accepted at authoring time and dropped before the wire. That is the + * #7750 defect itself. + * - **`defaultFilters` is ALIVE.** `ObjectGrid.tsx` reads it and lowers it to + * `params.$filter` — the legacy path `object-grid` still honors. It is + * pinned absent because this page authors the CURRENT key, NOT because the + * legacy one is inert. + * + * That distinction is the whole point of the card, so getting it wrong here + * would reproduce the defect one level up: a future author debugging a filter + * bug must not read this test as licence to treat `defaultFilters` as a no-op, + * nor reach for it as a workaround. + */ +const NON_CANONICAL_FILTER_SPELLINGS: ReadonlyArray<{ key: string; why: string }> = [ + { + key: 'filters', + why: 'has no reader in `object-grid` — it is accepted at authoring time and then dropped before the wire (#7750)', + }, + { + key: 'defaultFilters', + why: 'is the LEGACY key `object-grid` still reads and lowers to `$filter` — it works, but it is not the key this page declares', + }, +]; + describe('My Work — admin-only card gating (ADR-0089 component-level visibleWhen)', () => { it('carries the predicate at the COMPONENT level, not inside `properties`', () => { const card = leadershipCard(); @@ -106,3 +137,57 @@ describe('My Work — admin-only card gating (ADR-0089 component-level visibleWh expect(evalPredicate(source, { current_user: { email: 'analyst@objectos.ai' } })).toBe(false); }); }); + +/** + * The personal work queue's filter key, in the ONE spelling `object-grid` + * publishes and reads (objectstack#7750). + * + * This page authored the plural `filters:`. objectui's renderer reads only + * `schema.filter` (`plugin-grid/src/ObjectGrid.tsx`, lowered through + * `toFilterNode`) and the legacy `schema.defaultFilters`; `schema.filters` has + * zero read points on any ref. So the declared personal scope was accepted at + * authoring time and then dropped before the wire — no `$filter` parameter at + * all, and the "my work" queue listed every row. + * + * ⚠️ Not an authorization bypass: the unfiltered read is still RLS-constrained, + * so the caller only ever saw rows they may see. The defect is that a DECLARED + * personal-scope filter silently never applied. + * + * What these assertions can and cannot prove, stated honestly: + * - they pin the AUTHORED key against the spelling objectui declares, which is + * the whole of what is checkable inside this repo; + * - they do NOT prove `$filter` reaches the wire. That is objectui's own pin + * (`gridFilterInputSpelling.test.tsx`, objectui#4041 / `9154d9e`), and + * driving it end-to-end here needs the vendored `packages/console/dist` + * rebuilt at the current pin — tracked as objectstack#7752. + * + * The sibling `object-metric` tiles on this same page already spell `filter` + * singular, which is why the grid was the only site that drifted. + */ +describe('My Work — the personal queue declares its filter in the key object-grid reads', () => { + it('authors `filter` (singular) on the work-queue grid', () => { + const grid = workQueueGrid(); + expect(grid, 'the personal work queue `object-grid` must exist').toBeTruthy(); + expect( + grid!.properties, + '`object-grid` publishes and reads `filter` — the singular key it lowers to `$filter`', + ).toHaveProperty('filter'); + }); + + it('authors neither non-canonical spelling: `filters`, which is dropped, nor the legacy `defaultFilters`', () => { + // Each key fails for its OWN reason, so each carries its own message — + // `filters` because nothing reads it, `defaultFilters` because it is the + // superseded spelling rather than a dead one. + for (const { key, why } of NON_CANONICAL_FILTER_SPELLINGS) { + expect(workQueueGrid()!.properties, `\`${key}\` ${why}`).not.toHaveProperty(key); + } + }); + + it('still scopes the queue to the signed-in user', () => { + // The spelling is only half the fix; the predicate is what makes the page + // personal at all. `{current_user_id}` is the page-level identity token. + expect(workQueueGrid()!.properties!.filter).toEqual([ + ['owner_id', '=', '{current_user_id}'], + ]); + }); +});