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
14 changes: 14 additions & 0 deletions .changeset/11880-grid-filter-handoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@object-ui/plugin-view': patch
---

An `object-view` grid now exports the filter it shows, and returns to page 1 when that filter changes (objectui#11880, item 5).

`ObjectView` hands the grid it draws one filter slot, `filter`, carrying the whole chain: the active named view's filter, else `table.filter` unless it lowers to nothing (absent, `[]` or `{}`), else the deprecated `table.defaultFilters`. It no longer writes `defaultFilters` on that grid node, which `@objectstack/spec` retires on `object-grid` (objectstack#11509).

Two user-visible corrections, both on an `object-view` rendered by the registered renderer (a page node, the Studio's stored-view preview; a host that supplies `renderListView` already behaved this way):

- **The export carries the view's filter.** A named view's filter and an authored `table.defaultFilters` narrowed the rows on screen, but the server-streamed export (CSV, XLSX, JSON) was handed no filter and downloaded every record of the object. It now downloads the rows the view shows, as it already did for `table.filter`.
- **Changing the filter returns the grid to page 1.** When a named view's filter or `table.defaultFilters` changed while the grid stayed mounted, the next query kept the old page index, which could ask for a page the new result does not have. It now returns to page 1, as `table.filter` already did.

What did not change: which filter applies (each rung and each pair sends the same query and draws the same rows), and a refused filter still draws the grid's malformed-filter panel and queries nothing. Nothing is added to or removed from any published type, schema or export.
15 changes: 10 additions & 5 deletions content/docs/plugins/plugin-view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,16 @@ correctly, and silently did nothing. That is fixed — the legacy spellings on
the right are simply no longer the ones to reach for.

**When a key is written both ways, the canonical spelling wins** — that is
`ObjectGrid`'s own existing resolution (`schema.pagination?.pageSize ||
schema.pageSize`; `if (schema.selection?.type) … else if (schema.selectable
!== undefined)`; `schemaFilter !== undefined ? … : schema.defaultFilters`), and `ObjectView`
defers to it by forwarding both slots rather than re-resolving the pair
itself:
`ObjectGrid`'s own existing resolution for the paging pairs
(`schema.pagination?.pageSize || schema.pageSize`; `if
(schema.selection?.type) … else if (schema.selectable !== undefined)`), and
`ObjectView` defers to it by forwarding both slots rather than re-resolving
the pair itself. The filter pair is the exception: `ObjectView` resolves it
and hands the grid the winner in `filter` alone (objectui#11880). `table.filter`
wins unless it lowers to nothing (absent, `[]` or `{}`), and then
`table.defaultFilters` applies. Because the grid then has one filter slot,
its export carries the filter in force and a change to it returns the grid to
page 1, as `table.filter` always did:

```typescript
import type { ObjectViewSchema } from '@object-ui/types';
Expand Down
12 changes: 7 additions & 5 deletions packages/plugin-grid/src/ObjectGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2632,11 +2632,13 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
// carries. `defaultFilters` was declared `Record<string, any>`
// (the MongoDB-style shape) until objectui#6152 round 10 — it is the
// row's `ViewFilterRule` array since, the shape `filter` takes — and
// `isFilterAST` is false for a plain object; `plugin-view` also
// forwards an active named view's `ViewFilterRule[]` into this slot
// (`ObjectView.tsx`, the `gridSchema` memo), and `isFilterAST` is
// false for an array of rule objects too. Either one answers `400
// INVALID_FILTER` (measured against a real backend in objectui#3431).
// `isFilterAST` is false for a plain object; a rule array is the
// other shape (`plugin-view`'s `ObjectView` forwarded an active
// named view's `ViewFilterRule[]` into this slot until objectui#11880
// item 5, and hands its whole filter chain to `filter` since), and
// `isFilterAST` is false for an array of rule objects too. Either one
// answers `400 INVALID_FILTER` (measured against a real backend in
// objectui#3431).
//
// `toFilterNode` handles both without new logic: objects route
// through `convertFiltersToAST`, rule arrays lower element-wise,
Expand Down
15 changes: 10 additions & 5 deletions packages/plugin-view/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,11 +250,16 @@ just no longer the one to reach for:

**Precedence when a key is written both ways** — `table: { pagination: {
pageSize: 10 }, pageSize: 50 }`, say — the canonical spelling wins. That is
`ObjectGrid`'s own existing resolution (`schema.pagination?.pageSize ||
schema.pageSize`; `if (schema.selection?.type) … else if (schema.selectable
!== undefined)`; `schemaFilter !== undefined ? … : schema.defaultFilters`), and `ObjectView`
defers to it by forwarding both slots rather than re-resolving the pair
itself:
`ObjectGrid`'s own existing resolution for the paging pairs
(`schema.pagination?.pageSize || schema.pageSize`; `if
(schema.selection?.type) … else if (schema.selectable !== undefined)`), and
`ObjectView` defers to it by forwarding both slots rather than re-resolving
the pair itself. The filter pair is the exception: `ObjectView` resolves it
and hands the grid the winner in `filter` alone (objectui#11880). `table.filter`
wins unless it lowers to nothing (absent, `[]` or `{}`), and then
`table.defaultFilters` applies. Because the grid then has one filter slot,
its export carries the filter in force and a change to it returns the grid to
page 1, as `table.filter` always did:

```typescript
import type { ObjectViewSchema } from '@object-ui/types';
Expand Down
70 changes: 47 additions & 23 deletions packages/plugin-view/src/ObjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import {
convertSortToQueryParams,
recordDelete,
resolveFilterPlaceholders,
toFilterNodeSafely,
type FilterTokenScope,
} from '@object-ui/core';
import { SchemaRenderer as ImportedSchemaRenderer, useSettledSchema, notifyDataChanged, useDataInvalidation, useFilterScope } from '@object-ui/react';
Expand Down Expand Up @@ -410,6 +411,29 @@ interface AuthoredFilterSegments {
tableDefaults: ObjectGridSchema['defaultFilters'];
}

/**
* objectui#11880 item 5 — the `table` rung of the filter this component hands
* the grid it draws: `table.filter`, unless it lowers to nothing, else the
* deprecated `table.defaultFilters`.
*
* This is the choice `ObjectGrid` made itself while the two arrived in two
* slots (it read `defaultFilters` only when `filter` lowered to nothing), now
* made here so both reach it in `filter` alone — `ObjectGrid`'s export and
* page reset read that slot only. "Lowers to nothing" is the sink's own
* answer (`toFilterNodeSafely`: absent, `[]`, `{}`, a record of nulls), not a
* truthiness test, so an empty `table.filter` beside a `table.defaultFilters`
* keeps the grid's answer: the legacy rung applies.
*
* A REFUSED `table.filter` is kept, not skipped: it reaches `ObjectGrid`, which
* draws its malformed-filter panel, as it did before. Skipping it would let
* the legacy rung answer a query the author's canonical filter forbids.
*/
function gridTableFilterRung(segments: AuthoredFilterSegments): ObjectGridSchema['filter'] {
const canonical = toFilterNodeSafely(segments.table);
if (!canonical.ok || canonical.node !== undefined) return segments.table;
return segments.tableDefaults;
}

/**
* objectui#10506 — resolve every filter placeholder in the authored filter
* segments ONCE, through `@object-ui/core`'s shared `resolveFilterPlaceholders`,
Expand Down Expand Up @@ -2424,14 +2448,20 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
// on the floor. An author who wrote the canonical shape the type recommends
// got a compile-clean, semantically correct, RUNTIME-INERT view.
//
// ObjectGrid reads both spellings of the first three and resolves them
// ObjectGrid reads both spellings of the first two and resolves them
// canonical-first (`schema.pagination?.pageSize ?? schema.pageSize`;
// `if (schema.selection?.type) … else if (schema.selectable !== undefined)`;
// `schemaFilter !== undefined ? … : schema.defaultFilters`), so the fix is
// forwarding, not translation — and the precedence is not a free choice
// here: emitting both slots lets ObjectGrid's existing canonical-wins rule
// decide, which is the only answer that keeps the two layers saying the
// same thing.
// `if (schema.selection?.type) … else if (schema.selectable !== undefined)`),
// so for those the fix is forwarding, not translation: emitting both slots
// lets ObjectGrid's existing canonical-wins rule decide.
//
// objectui#11880 item 5: the filter pair is resolved HERE instead, and
// handed over in `filter` alone (see `gridTableFilterRung`). ObjectGrid does
// not read its two filter slots alike: its server export and its page reset
// read only `filter`, so a filter riding `defaultFilters` narrowed the rows
// on screen while the downloaded file held every record, and changing it in
// place kept the old page. `@objectstack/spec` also retires
// `object-grid.defaultFilters` (objectstack#11509, A-narrow), so this
// component no longer writes it at all.
//
// objectui#5861: the fourth pair is no longer a pair. `defaultSort` was
// RETIRED under ADR-0049 — `@objectstack/spec` refuses it by name on
Expand Down Expand Up @@ -2459,14 +2489,11 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
const gridSchema: ObjectGridSchema = useMemo(() => {
// The two segments ahead of the `table` one, resolved once.
//
// `viewFilter` keeps riding the LEGACY slot it rides today:
// `filter`/`defaultFilters` are not interchangeable downstream —
// ObjectGrid lowers the canonical slot through `toFilterNode` and
// raw-assigns the legacy one — so moving a named-view filter across would
// change the wire shape of a path objectui#5270 does not own. Sort has no
// such asymmetry to preserve: its canonical slot is the only one left
// (the legacy `defaultSort` is retired, objectui#5861), and it is the one
// that can hold more than a single key.
// objectui#11880 item 5: `viewFilter` rides the CANONICAL slot, `filter`,
// as the `table` rung does when no view filter is present. Both slots
// reach the wire through the same `toFilterNode` sink in ObjectGrid
// (objectui#4082), so the query is unchanged; what moves is that the
// export and the page reset, which read `filter` only, now see it.
//
// objectui#10506: all three filter segments are read RESOLVED (see
// `authoredFilters`), and held while their inputs are unchanged — ObjectGrid
Expand Down Expand Up @@ -2514,14 +2541,11 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
...gridOperations,
create: false, // Create is handled by the view's create button
},
defaultFilters: viewFilter || authoredFilters.tableDefaults,
// Canonical `table` keys, at last forwarded. `filter` carries the
// `table` segment ONLY: the view segment resolved above already occupies
// the legacy slot, and ObjectGrid prefers this slot over that one — so
// handing it `table.filter` while a named view is active would let the
// table default outrank the view, inverting the precedence the two
// untouched segments exist to express.
filter: viewFilter ? undefined : authoredFilters.table,
// objectui#11880 item 5 — the whole filter chain in ONE slot: the active
// view's filter (any truthy value, an empty array included, as before),
// else the `table` rung (`gridTableFilterRung`). No `defaultFilters`:
// ObjectGrid's export and page reset read `filter` only.
filter: viewFilter || gridTableFilterRung(authoredFilters),
// `sort` carries the WHOLE chain instead — view segments first, then the
// `table` one. Same precedence as `mergedSort` and the non-grid fetch
// express; what changes is only WHICH slot a view's sort arrives in, and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,21 @@
* PRECEDENCE, when an author writes both: the canonical key wins. Where
* `ObjectView` itself resolves the pair (the non-grid fetch, and the delegated
* `renderListView` schema) that choice is encoded in this file's `||` chains
* and pinned below. On the grid path `ObjectView` forwards BOTH slots and
* `ObjectGrid` arbitrates — it already resolves all four pairs canonical-first
* and pinned below. On the grid path `ObjectView` forwards BOTH slots of the
* pagination and selection pairs and `ObjectGrid` arbitrates canonical-first
* (`schema.pagination?.pageSize ?? schema.pageSize`; `if (schema.selection
* ?.type) … else if (schema.selectable !== undefined)`; `schemaFilter !==
* undefined ? … : schema.defaultFilters`; `schemaSort ?? (schema.defaultSort
* ? [schema.defaultSort] : undefined)`). Re-resolving the pair here instead
* would have made the two layers disagree, and synthesising a `pagination`
* object out of a legacy `pageSize` would flip `ObjectGrid`'s
* ?.type) … else if (schema.selectable !== undefined)`). Synthesising a
* `pagination` object out of a legacy `pageSize` would flip `ObjectGrid`'s
* `paginationEnabled` (`schema.pagination !== undefined ? true : …`) for every
* view that only ever wrote the legacy key.
*
* objectui#11880 item 5: the FILTER pair is the exception on the grid path.
* `ObjectView` resolves it itself and hands the winner in `filter` alone,
* never writing `defaultFilters`: `ObjectGrid`'s export and page reset read
* `filter` only, so a value in the legacy slot narrowed the rows but not the
* download. The precedence is unchanged (the canonical key wins unless it
* lowers to nothing), and `ObjectView.gridFilterHandoff-11880.test.tsx` reads
* it through the real grid.
*/

import { describe, it, expect, vi, beforeEach } from 'vitest';
Expand Down Expand Up @@ -204,9 +209,10 @@ describe('grid path: the legacy spellings still work', () => {
expect(forwardedGridSchema({ table: { selectable: true } as any }).selectable).toBe(true);
});

it('still forwards table.defaultFilters', () => {
it('still honours table.defaultFilters, handed over in `filter` (objectui#11880 item 5)', () => {
const grid = forwardedGridSchema({ table: { defaultFilters: { status: 'active' } } as any });
expect(grid.defaultFilters).toEqual({ status: 'active' });
expect(grid.filter).toEqual({ status: 'active' });
expect(grid.defaultFilters).toBeUndefined();
});

it('no longer forwards a retired table.defaultSort — and does not rescue it into `sort` (objectui#5861)', () => {
Expand All @@ -230,17 +236,20 @@ describe('grid path: the legacy spellings still work', () => {
});
expect(grid.pagination).toBeUndefined();
expect(grid.selection).toBeUndefined();
expect(grid.filter).toBeUndefined();
expect(grid.sort).toBeUndefined();
// objectui#11880 item 5 — the filter pair is resolved here, so the alias
// rides the only filter slot the node carries; nothing is synthesised.
expect(grid.filter).toEqual({ a: 1 });
expect(grid.defaultFilters).toBeUndefined();
});
});

describe('grid path: both spellings written — each rides its own slot', () => {
// ObjectView forwards both and ObjectGrid arbitrates canonical-first (see the
// file header for the four expressions). What is pinned here is ObjectView's
// half: the canonical value must arrive in the canonical slot, or the
// downstream rule has nothing to prefer.
it('puts the canonical value in the canonical slot and the alias in the legacy one', () => {
describe('grid path: both spellings written', () => {
// ObjectView forwards both slots of the pagination and selection pairs and
// ObjectGrid arbitrates canonical-first (see the file header). The filter
// pair is resolved here instead (objectui#11880 item 5): the canonical value
// arrives in `filter` and no `defaultFilters` is written.
it('puts each canonical value in its slot; the paging aliases ride theirs, the filter alias is not written', () => {
const grid = forwardedGridSchema({
table: {
pagination: { pageSize: 25 },
Expand All @@ -258,7 +267,7 @@ describe('grid path: both spellings written — each rides its own slot', () =>
expect(grid.selection).toEqual({ type: 'multiple' });
expect(grid.selectable).toBe(false);
expect(grid.filter).toEqual([['stage', '=', 'won']]);
expect(grid.defaultFilters).toEqual({ stage: 'lost' });
expect(grid.defaultFilters).toBeUndefined();
expect(grid.sort).toEqual([{ field: 'name', order: 'desc' }]);
// objectui#5861 — the sort pair has no legacy slot any more: the retired
// `defaultSort` is dropped, not forwarded beside the canonical value.
Expand All @@ -268,10 +277,9 @@ describe('grid path: both spellings written — each rides its own slot', () =>

describe('grid path: a named view still outranks the table segment', () => {
// The FILTER segments ahead of `table` (`listViews` entry, then `activeView`)
// are untouched by objectui#5102 — they keep riding the legacy slot. The
// canonical slot must therefore stay EMPTY while one of them is active:
// ObjectGrid prefers the canonical slot, so a `table.filter` forwarded
// unconditionally would outrank the view the user is looking at.
// ride `filter` since objectui#11880 item 5, the one filter slot the node
// carries, so the view's filter must be the value in it: a `table.filter`
// there would outrank the view the user is looking at.
//
// The SORT half moved in objectui#5270 — see the sort assertion below for
// why, and `ObjectView.namedViewSortArity.test.tsx` for the two consumers
Expand All @@ -293,8 +301,8 @@ describe('grid path: a named view still outranks the table segment', () => {
...namedView,
table: { filter: [['stage', '=', 'lost']] } as any,
} as any);
expect(grid.filter).toBeUndefined();
expect(grid.defaultFilters).toEqual([['stage', '=', 'won']]);
expect(grid.filter).toEqual([['stage', '=', 'won']]);
expect(grid.defaultFilters).toBeUndefined();
});

it('keeps the named view sort in force over a table.sort', () => {
Expand Down
Loading
Loading