Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion examples/app-showcase/src/ui/pages/my-work.page.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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}']],
},
},
],
Expand Down
85 changes: 85 additions & 0 deletions examples/app-showcase/test/my-work-visibility.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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();
Expand DownExpand Up@@ -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}'],
]);
});
});
Loading