Skip to content

Commit 244d516

Browse files
feat(plugin-grid,plugin-list,i18n): a grouped grid over a data source with no group header query refuses grouping; the Partial marker is retired (objectui#10881) (#10896)
Fixes #10881 Clause-②: yes Maintainer ruling F (`5862401217`), executed as ruled. A grouped grid over a data source that declares no `queryGroupHeaders` refuses grouping loudly, naming that member, and issues no row query; `ListView` makes the same refusal before it mounts such a grid; the `Partial` marker is retired everywhere. Rows handed in WHOLE still group in the browser, exactly (`useGroupedData` unchanged for that case). Builds on PR objectui#10878 (option K, merged). Patch round 2 adds the seat's decision B (contract review `5863494567`, ③ flag 1): rows a host hands in while DECLARING them one page of more are a window, not whole rows, and a grouped grid refuses them too, with a sentence of its own. ## What changed | Item | Where | What it does now | | --- | --- | --- | | (1) `ObjectGrid`, no header query | `ObjectGrid.tsx`, `groupingNeedsHeaderQuery` | `!hasInlineData` and `objectName` and a `dataSource` whose `queryGroupHeaders` is not a function and a grouping entry the grid would group by (same entries as `serverGroupedFetch`, less the readability gate). The load effect returns before Step 2 (no `find`; the object definition is still read, so a KNOWN masked-only grouping key flips the refusal off and the flat fetch then goes out). Render: the load-error panel's own shape (`role="alert"`, heading `grid.errorLoading`, sentence `grid.grouping.needsHeaderQuery`), `data-testid="grid-grouping-needs-header-query"`, placed AFTER the spinner branch, so while the definition read is in flight the grid shows it is loading rather than an error it may withdraw. | | (2) `ListView`, no header query | `ListView.tsx`, `groupingNeedsHeaderQuery` | grid view, a grouping entry with a named `field` (`collectGroupingFieldRefs`), rows NOT inline (`schema.data` neither an array nor a `value` provider), `objectName`, a `dataSource` without `queryGroupHeaders`. The fetch effect stands down (and clears held rows), a `DataErrorState` (`list.loadErrorTitle` plus the grid's own sentence, no Retry) replaces the grid, which is never mounted, and no record-count bar. The toolbar stays, so removing the grouping lifts it. A toolbar search does not lift it. | | (3) marker retired | `ObjectGrid.tsx`, `GroupRow.tsx`, ten packs, tests, docs | the three `grid.grouping.partial*` keys (defaults map and all ten packs), the `groupingIsPartial` computation and notice, `GroupRow`'s `partialLabel` / `partialTitle` props, `groupedPartialDisclosure-7189.test.tsx`; the "Two cases still group in the browser" section rewritten in `plugin-grid.mdx` and the plugin-grid README. | | (4) dated notes | `.changeset/7189-grouped-grid-partial-disclosure.md`, `.changeset/7189-server-side-grid-grouping.md` | PR objectui#10878 already added the note review ③ asked for; this PR's change falsified that note's last sentences, so a second dated note, 2026-09-28, names them. `7189-server-side-grid-grouping.md` gets one too, for its two sentences this PR falsified. | | (5) a host-declared window (round 2, B) | `ObjectGrid.tsx`, `groupingNeedsWholeRows` | rows handed in (`hasInlineData`) with the external-pagination props declared (`manualPagination`, `onPageChange`, a numeric `rowCount`) and `rowCount` above the rows handed (counted on the rows as handed, not on the `data` state that mirrors them a render later, so whole rows never flash it), and a grouping entry the grid would group by. Refused in the same panel idiom (`role="alert"`, `grid.errorLoading`) with its own sentence, `grid.grouping.needsWholeRows`: grouping needs every record, so hand the rows in whole or let the grid fetch them from a source that implements `queryGroupHeaders`. `data-testid="grid-grouping-needs-whole-rows"`. The header-query sentence would be wrong there: the host owns the fetch, and its source may declare the member (pinned). No `rowCount`, or one not above the rows handed, is whole rows and groups as before (pinned). | Two new keys, `grid.grouping.needsHeaderQuery` and `grid.grouping.needsWholeRows`, in `GRID_DEFAULT_TRANSLATIONS` and all ten packs (the first also in `LIST_DEFAULT_TRANSLATIONS`); `queryGroupHeaders` stays untranslated inside each sentence. New changeset `.changeset/10881-grouping-needs-header-query.md`: `minor` for `@object-ui/plugin-grid`, `@object-ui/plugin-list`, `@object-ui/i18n`, with the breaking semantics (including the host-declared window) and the migration (implement `queryGroupHeaders` and let the grid fetch, or hand the rows in whole). `@object-ui/types` and `@object-ui/react` are touched in doc comments only (the `DataSource.queryGroupHeaders` JSDoc sentence that said such a grid "keeps grouping the rows it fetched", and `NonGridRowCeilingNote`'s JSDoc citing the retired key); neither package's behaviour or keys move, so neither is named in the changeset. The presence gate is satisfied by the one changeset. Out of scope as ruled and untouched: `ListView` over the objectstack adapter while a toolbar search is active still hands a grouped grid its window (objectstack#20358); `ValueDataSource` gains no `queryGroupHeaders`. ## The PM's assumptions, measured 1. **Census.** Readers of `groupingIsPartial` / `grid.grouping.partial*` / `partialLabel` / `partialTitle` / the two marker test ids across `packages`, `apps`, `content`, `examples`, `e2e`, `scripts`, JSON: `ObjectGrid.tsx` (defaults, computation, two render sites), `GroupRow.tsx`, the ten packs (key rows, plus a `rowCeilingNote` comment in each pack that cited `grid.grouping.partialNotice`), `nonGridRowCeiling.tsx` (JSDoc citation), `useGroupedData.ts` (doc sentence), `serverGrouping-7189.test.tsx`, `groupedPartialDisclosure-7189.test.tsx`, and two test headers citing the retired file (`gridArrayArmOrderby-8973`, `gridGroupingMembers-8071`). `apps`, `content`, `examples`, `e2e`: none. After this PR the same `git grep` over the tree (CHANGELOGs and `.changeset/` excluded) hits only the new i18n pin, which is the control that the grep can hit. 2. **Whole versus window in `ListView`.** Decidable from what `ListView` knows: its fetch effect returns early, before any `find`, when `schema.data` is an array or a `value` provider (searched rows are filtered in memory, still the whole matching set); every other shape reaches `dataSource.find` with `$top: effectivePageSize` and hands that window down as `data`. The refusal keys on exactly that split, so truly whole rows never refuse (pinned, and the pin goes red when the `value` exclusion is ablated, below). **Does `ListView` ever take the round-2 path?** No: it hands a grid host paging only when `paginate && serverTotal != null`, and `paginate` is false whenever a grouping is set. Measured on the one shape in which it hands a GROUPED grid a window (a source that answers the header query, a toolbar search active): the grid is handed the rows and no `manualPagination` / `rowCount`, against a lit control where the same source ungrouped is handed `manualPagination: true` and `rowCount: 186` (pinned). 3. **The error panels.** The grid's existing load-error idiom, the i18n path through `GRID_DEFAULT_TRANSLATIONS` / `LIST_DEFAULT_TRANSLATIONS` (byte-identical to `en`, which `defaults-maps-mirror-en-pack` enforces, green) and the ten packs; no row query, proven by the `find` call count against a lit control where the same find-only source, ungrouped, is queried. 4. **PR objectui#10278** (head `eab4c8e52`). Its hunks in `ObjectGrid.tsx`: the spec import, the `DEFAULT_*` constants, the `groupedPageSize` / `serverPageSize` / `fetchWindow` state, the `$top` / `$skip` params, the load effect's dependency array, `pageSize`, `groupingPartialWindowFull`, the group pager's select. Lines of its hunks this PR had to touch: **the load effect's dependency array** (it renames `serverPageSize` to `fetchWindow` there; this PR appends `groupingNeedsHeaderQuery`; the line already differs from its base since PR objectui#10878) and **`groupingPartialWindowFull`** (it re-points it to `fetchWindow`; this PR deletes the whole marker computation, as ruled). Round 2 touched none of its hunks: the new predicate sits below `groupingNeedsHeaderQuery`, and the moved panel sits after the spinner branch. `groupedPagination.test.tsx` is untouched; in `ObjectGrid.pageSizeNonPositive-9853.test.tsx` only `groupedSchema` changed, which none of its hunks holds. For its merge of `main`: its docs sentence "A grouped view separately fetches a larger batch of rows to group" no longer describes the grid (a server-grouped grid strips `$top` / `$skip`, a refused one fetches nothing, inline rows fetch nothing), and its grouped `fetchWindow` branch is reached only where a grouped grid still fetches a flat window. 5. **Docs.** The section lived in `content/docs/plugins/plugin-grid.mdx` and `packages/plugin-grid/README.md` (identical text); rewritten there as ruled, and in round 2 its whole-rows paragraph names the host-declared window and its refusal. Also brought true: the `grouping` row in `content/docs/api/schema-reference.md`, a new paragraph in the plugin-list README "Grouping Records" section, and the plugin-list README's sort paragraph (its "the grouped view, which holds every row it groups" was already false for a server-grouped view and names no remaining case). 6. **Changeset.** As above: three packages, not five, with the reason; round 2 corrected its whole-rows sentence to rows handed in WHOLE and added the host-declared window to the breaking semantics and the migration. ## Pins (new files) and their red on base - `packages/plugin-grid/src/__tests__/groupingNeedsHeaderQuery-10881.test.tsx` (12): - a grouped grid over a find-only source renders the refusal naming `queryGroupHeaders` and `find` is never called; CONTROL the same source ungrouped is queried once and draws; CONTROL the same grouping over a source declaring the member is not refused; rows handed in whole (a `value` provider, and a `data` prop) group as 86/61/31/7/1 with no refusal and no query; - round 2: 100 rows handed with `rowCount: 186` render the whole-rows refusal, draw no group and query nothing; the same window over a source that DOES declare `queryGroupHeaders` is refused the same way and the header query is never asked; CONTROL 186 rows with `rowCount: 186` group exactly as 86/61/31/7/1; CONTROL 100 rows with no `rowCount` group as handed (86, 14), unrefused; - round 2: a masked-only grouping key shows `Loading grid…`, never the refusal, while the definition read is in flight, then the flat rows; - re-homed from the retired file: under 768px a grouped grid draws its groups and tables, not cards; CONTROL the same rows ungrouped at that width are cards with no table. - `packages/plugin-list/src/__tests__/ListView.groupingNeedsHeaderQuery-10881.test.tsx` (4): the refusal naming `queryGroupHeaders`, no grid mounted, no `find`, no record-count bar; CONTROL ungrouped is queried and handed its window WITH host paging; CONTROL rows handed in whole reach the grouped grid whole, no refusal, no `find`; round 2: a grouped grid handed a searched window is handed no host paging. - `packages/i18n/src/__tests__/groupingPartialRetired-10881.test.ts` (21): no pack carries a `grid.grouping.partial*` key; CONTROL the same walk finds `grid.grouping.needsHeaderQuery`, naming `queryGroupHeaders`, in all ten. Red on base, round 1: the three files run with the behaviour-carrying sources (`ObjectGrid.tsx`, `GroupRow.tsx`, `ListView.tsx`, ten packs) checked out at `b2683a2c0`, restored from `HEAD` after (hash-compared, `git diff HEAD` empty): `Tests 22 failed | 7 passed (29)`; the 22 are both refusal pins and the 20 locale rows, the 7 are lit controls and whole-rows pins F keeps. Red on base, round 2: the two behaviour files run with `ObjectGrid.tsx`, `ListView.tsx` and the ten packs at `638a8250c` (the round-1 head), restored the same way: `Tests 3 failed | 13 passed (16)`; the 3 are the two window refusals and the spinner-not-refusal pin; the 13 are the round-1 pins, the round-2 controls, the card-view pair and the `ListView` measurement, which pin behaviour kept. Green on head: grid pin file `Tests 12 passed (12)`, `ListView` pin file `Tests 4 passed (4)`. ## Ablations (`ablation-replace.mjs`, each restored to the `HEAD` blob with `git diff HEAD` empty) - A1 `ObjectGrid` header refusal without `!hasInlineData`: both whole-rows pins red (`2 failed | 3 passed`). - A2 `ObjectGrid` load-effect guard deleted: the refusal pin red on the `find` count (`1 failed | 4 passed`); the panel alone does not satisfy it. - A3 `ListView` refusal without the `value` exclusion: the whole-rows control red (`1 failed | 2 passed`). Declared: the first attempt was an anchor miss (a quoting slip left the newlines literal) and the second was refused by the tool because the replacement text was a suffix of the anchor; neither wrote anything. The third, with a marker in the replacement, landed; it was re-run at `638a8250c` with the same result. - A4 `ListView` fetch-effect stand-down disabled: the refusal pin red on the `find` count (`1 failed | 2 passed`). - R2-A1 the whole-rows guard neutralized (`groupingNeedsWholeRows` forced false), at `156eb5103`: both window pins red (`2 failed | 10 passed`). - R2-A2 its strict "above" loosened to "at least": the `rowCount`-equal control red (`1 failed | 11 passed`), so the control can fail. - R2-A3 the card view's `!isGrouped` exclusion removed: the re-homed grouped-mobile pin red (`1 failed | 11 passed`). ## Existing tests re-decided (none deleted blindly) - `groupedPartialDisclosure-7189.test.tsx` — retired as ruled. Its marker and notice cases pin what F retires. Its "does not mark rows handed to it inline" case is re-decided into the new whole-rows pins; its "reads the total a HOST supplies" case is re-decided into the round-2 window refusal (the same render, now refused); its card-view case is re-homed as a behavioural pin with a lit control; its ungrouped control asserted only the marker's absence beside a fetch other files pin. - `serverGrouping-7189.test.tsx` — the two marker-absence assertions dropped (nothing left to be absent: a phantom check); its CONTROL, which pinned page grouping plus two markers over a find-only source, now pins the refusal and no row query; header text says why. - `ListView.groupedGridOwnsFetch-7189.test.tsx` — its CONTROL, which pinned `ListView` handing a find-only source's window to the grid, now pins that no grid is mounted. - `groupingProjection-7179`, `gridGroupingMembers-8071`, `badgeHexCrossSurface`, `maskedColumnSurfaces-10583` (plugin-grid): each grouped over a find-only double, which F refuses; the doubles now answer `queryGroupHeaders`, and the projection / header label is read off the server-grouped path (its row query carries the grid's own `$select` / `$expand`). - `expandFls-7215` (plugin-grid): its PIN 7 groups only by an unreadable key; the double declares the member so the grid still fetches its flat window, the projection under test. - `ObjectGrid.pageSizeNonPositive-9853`: its grouped cases hand their seven rows in whole; the page of groups is the same on either path. - `ListView.groupingProjection-7179`, `ListView.expandFls-7215`, `ListView.speculativeFls-7216`: the doubles declare the member, so `ListView` still fetches the window those pins read (the rows a searched grouped view is handed, and what the chip counts and export read). - Two test headers citing the retired file re-pointed. ## Local verification - At `156eb5103` (the round-2 code; the later `94df46014` only types one test callback, and that file was re-run there: `Tests 4 passed (4)`): - `pnpm exec vitest run packages/plugin-grid/`: `Test Files 164 passed (164)`, `Tests 1535 passed (1535)`. - `pnpm exec vitest run packages/plugin-list/ packages/i18n/`: `Test Files 178 passed (178)`, `Tests 2380 passed (2380)`. - `pnpm exec vitest run scripts/__tests__/`: `Test Files 177 passed | 2 skipped (179)`. - `defaults-maps-mirror-en-pack` (app-shell): `Tests 15 passed (15)`. - `type-check` for plugin-grid, plugin-list and i18n after building their dependency closure: all `Done`; plugin-list re-run at `94df46014`. - At `94df46014`: `check:i18n-keys`, `check:i18n-drift`, `check:i18n-dead-keys`, `check:control-bytes`, `check:new-line-citations` (0 new), `check:changeset-claims` (report), `check:test-path-roots`, `check:vi-mock-specifiers`, `changeset:check`, `check-changeset-presence`: exit 0. - Round 1, unchanged by round 2 and not re-run: `packages/plugin-view/` with the app-shell `widget-dom-leak-sweep` and `ObjectView.rowColorRelay-7218`, `packages/react/src/utils`, `packages/types/` (`Test Files 318 passed (318)` at `dfd9bb187`), and the `types` / `react` type-checks. - Lint, narrowed and declared: `eslint --no-inline-config --format json` over the changed `.ts` / `.tsx` files that exist at `HEAD` (the population is `git diff --name-only --diff-filter=d`, not a guess): 0 errors; per-file warning counts equal to the previous head for every modified file (round 2: 14 files against `638a8250c`); the new test files carry `no-explicit-any` warnings in the idiom of their neighbours. The config enables no type-aware linting (no `parserOptions.project`), so this diff cannot move a verdict in an untouched file. NOT MEASURED locally, declared to CI: `check:readme-exports` (prerequisite: every package's `dist`; neither touched README gained or lost a fenced block, and no export name moved), `check:eager-locale-catalogues` (prerequisite: a console build; this PR moves no import), the repo-wide `pnpm lint`. ## Acceptance notes - A host that hands a grouped grid rows AND declares them one page of more is refused (item 5). This was an open question in round 1; the seat decided B on the contract review's measurement (`5863494567`, ③ flag 1: 100 of 186 rows with `rowCount: 186` drew two groups, 86 and 14, three units missing, with nothing on screen saying so, a render the retired disclosure pin had marked). No in-repo host takes this path: `ListView` hands a grouped grid no host paging (item 2, pinned). - Over a source that DOES declare `queryGroupHeaders`, a grouping whose every key the principal may not read is not asked of the server and still groups a fetched page (since PR objectui#10878); unchanged here. - A grouping whose only key is a masked type: over a source without the member, the grid now shows its spinner until the object definition lands, then the flat grid (pinned). Over rows a host handed as a declared window, there is no read to wait behind, so the whole-rows refusal shows until the definition lands and then withdraws; no in-repo producer. - The first five commits carry a `Co-Authored-By` trailer copied from the harness reminder; the round-2 commits carry the dispatch contract's model-free pair. History not rewritten (no force-push). The session that wrote this: `https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk`. --- _Generated by [Claude Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 775e079 commit 244d516

39 files changed

Lines changed: 1110 additions & 772 deletions
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
'@object-ui/plugin-grid': minor
3+
'@object-ui/plugin-list': minor
4+
'@object-ui/i18n': minor
5+
---
6+
7+
A grouped grid over a data source that declares no `queryGroupHeaders` now
8+
**refuses grouping** instead of grouping a page of rows (objectui#10881,
9+
maintainer ruling F). Grouping is a property of the query (ruling A on
10+
objectui#7189): a data source that cannot answer the group header query cannot
11+
group, so the grid names the member it is missing rather than drawing counts it
12+
cannot know.
13+
14+
- **`@object-ui/plugin-grid`** — a grouped `object-grid` that fetches its own
15+
rows over such a source renders an error panel naming `queryGroupHeaders`
16+
and issues no row query. It used to fetch one window and group it in the
17+
browser, so every group count was a page slice and a group whose rows all
18+
fell past the window was missing. Rows handed in WHOLE — a
19+
`data: { provider: 'value', items }` block, or a `data` prop from a parent
20+
view that declares no larger `rowCount` — are still grouped in the browser,
21+
exactly. A host that hands rows in while declaring them one page of more
22+
(the external-pagination props: `manualPagination`, `onPageChange` and a
23+
`rowCount` above the rows it handed) is now refused as well, with a sentence
24+
of its own: grouping needs every record. It used to have that page grouped as
25+
if it were whole.
26+
- **`@object-ui/plugin-list`** — `ListView` makes the same refusal, with the
27+
same sentence, in place of a grouped grid over such a source, and fetches no
28+
window for it. It used to fetch one window and hand it to the grid as
29+
`data`, which the grid grouped as if the rows were whole. Rows handed to
30+
`ListView` in whole are still handed to the grid.
31+
- **`@object-ui/i18n`** — two new keys, `grid.grouping.needsHeaderQuery` and
32+
`grid.grouping.needsWholeRows`, in all ten locale packs.
33+
34+
The `Partial` marker described by the pending
35+
`7189-grouped-grid-partial-disclosure` entry is retired everywhere before any
36+
release carried it: its three `grid.grouping.partial*` strings, `GroupRow`'s
37+
`partialLabel` / `partialTitle` props and the notice above the group list are
38+
gone.
39+
40+
**Breaking semantics (declared `minor` per this repo's version policy):** a
41+
grouped grid that fetches its own rows over a data source without
42+
`queryGroupHeaders` — `ApiDataSource`, `ValueDataSource`, or a host adapter —
43+
grouped the page it fetched in the last release, and now renders an error panel
44+
instead. So does a grouped grid view in `ListView` over such a source. So does
45+
a grouped grid handed rows by a host that declares them one page of more
46+
(`manualPagination`, `onPageChange` and a `rowCount` above the rows handed),
47+
whatever its data source. To keep grouping, either:
48+
49+
- implement `queryGroupHeaders` on the data source (the ObjectStack adapter
50+
does), and let the grid fetch its own rows, or
51+
- hand the rows in whole (`data: { provider: 'value', items }`, or a `data`
52+
prop with no `rowCount` above it).
53+
54+
Not changed here: while a toolbar search is active, `ListView` over a data
55+
source that answers the header query still hands a grouped grid its window,
56+
whose group counts are the window's, because the header query carries no
57+
search (objectstack#20358).

‎.changeset/7189-grouped-grid-partial-disclosure.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,15 @@ described above never render there. On a data source without
6767
as described above, and the marker and the notice are kept there. The rest of
6868
this entry is kept as the reading of this change; the
6969
`7189-server-side-grid-grouping` entry states what a grouped grid does now.
70+
71+
⚠️ **Dated note, 2026-09-28 — the `Partial` marker is retired before release —
72+
objectui#10881.** Later in this same release the marker and the notice described
73+
above were retired, with the three `grid.grouping.partial*` strings and
74+
`GroupRow`'s `partialLabel` / `partialTitle` props that carried them (maintainer
75+
ruling F). A grouped grid that fetches its own rows over a data source that
76+
declares no `queryGroupHeaders` no longer groups the page it fetched: it
77+
refuses grouping with an error naming that member. So the last sentences of the
78+
note above — that on such a source the grid still groups the page it fetched,
79+
and that the marker and the notice are kept there — no longer describe the
80+
grid. The rest of this entry is kept as the reading of this change; the
81+
`10881-grouping-needs-header-query` entry states what ships.

‎.changeset/7189-server-side-grid-grouping.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,15 @@ This supersedes the last paragraph of the pending
6666
`7189-grouped-grid-partial-disclosure` changeset, which said server-side
6767
grouping was not built: it now is, and the `Partial` marker described there
6868
remains only for the data sources that cannot answer the header query.
69+
70+
⚠️ **Dated note, 2026-09-28 — a source with no header query now refuses
71+
grouping — objectui#10881.** Later in this same release two sentences above
72+
stopped describing the grid: that a source with no header query still groups
73+
the page it fetched and still marks it partial, and that the `Partial` marker
74+
remains for the data sources that cannot answer the header query. Maintainer
75+
ruling F retired both. Over such a source a grouped grid that fetches its own
76+
rows, and a `ListView` hosting a grouped grid, refuse grouping with an error
77+
naming `queryGroupHeaders`, and the marker is gone everywhere. Rows handed in
78+
whole are still grouped in the browser. The rest of this entry is kept as the
79+
reading of this change; the `10881-grouping-needs-header-query` entry states
80+
what ships.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -781,7 +781,7 @@ A data grid that auto-fetches from an ObjectQL object definition. Includes searc
781781
| `operations` | `object` | Enabled CRUD operations. |
782782
| `rowActions` / `bulkActions` | `string[]` | Action identifiers for rows and batch selection. `bulkActions` is the spec-aligned key; `batchActions` is a legacy alias that takes precedence when both are set. |
783783
| `editable` | `boolean` | Enable inline cell editing. |
784-
| `grouping` | `GroupingConfig` | Row grouping configuration. **Server-side**: the set of groups, every group count and every per-group aggregation come from the group header query (`dataSource.queryGroupHeaders`), and each group's rows are paged by the server. Rows handed in whole are grouped in the browser (exact); over a data source with no header query the grid groups the page it fetched and marks the counts partial. |
784+
| `grouping` | `GroupingConfig` | Row grouping configuration. **Server-side**: the set of groups, every group count and every per-group aggregation come from the group header query (`dataSource.queryGroupHeaders`), and each group's rows are paged by the server. Rows handed in whole are grouped in the browser (exact); over a data source with no header query, a grid that fetches its own rows refuses grouping with an error naming `queryGroupHeaders`. |
785785
| `frozenColumns` | `number` | Number of columns frozen on scroll. |
786786
| `navigation` | `ViewNavigationConfig` | SPA navigation configuration. |
787787

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

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -362,23 +362,30 @@ filter AND the group's key, `limit` / `offset` per group). So:
362362
- A `lookup` / `master_detail` / `user` grouping key is the referenced record's
363363
id on the wire; the grid labels it from the referenced record.
364364

365-
When a `ListView` hosts the grid (the console's list views), it hands a grouped
366-
grid its own fetch and the view's effective filter, rather than a window of
367-
rows.
368-
369-
Two cases still group **in the browser**:
370-
371-
- **Rows handed in whole** (`data: { provider: 'value', items }`, or a host's
372-
whole result set): nothing was withheld, so grouping them there is exact.
373-
- **A data source that declares no `queryGroupHeaders`**: the grid can only
374-
group the page it fetched. Every count is then a page slice and a group whose
375-
records all fall past the page is absent, so the grid says so where the
376-
numbers are — a short `Partial` marker beside every group count and a line
377-
above the group list (*"Grouped over the first 100 of 186 records…"*). With
378-
counts from the server the marker never appears.
365+
When a `ListView` hosts the grid (the console's list views) over a data source
366+
that answers the group header query, it hands a grouped grid its own fetch and
367+
the view's effective filter, rather than a window of rows.
368+
369+
**Rows handed in whole** (`data: { provider: 'value', items }`, or a host's
370+
whole result set) are grouped where they are, in the browser: nothing was
371+
withheld, so the grouping is exact. The grid takes rows a host hands it to be
372+
the whole set — unless that host declares them one page of more
373+
(`manualPagination`, `onPageChange` and a `rowCount` above the rows it
374+
handed). A grouped grid refuses such a window with an error saying grouping
375+
needs every record; hand the rows in whole, or let the grid fetch them.
376+
377+
**Everything else needs the group header query** (objectui#10881). Over a data
378+
source that declares no `queryGroupHeaders`, a grouped grid that fetches its
379+
own rows does not group a page of them — every count would be a page slice,
380+
and a group whose records all fall past the page would be missing. It shows an
381+
error naming `queryGroupHeaders` instead, and asks for no rows. A `ListView`
382+
makes the same refusal before it mounts such a grid. To group, implement
383+
`queryGroupHeaders` on the data source, or hand the rows in whole.
379384

380385
A toolbar **search** has no counterpart on the group header query, so a
381-
`ListView` keeps grouping its own window while a search term is active.
386+
`ListView` over a data source that answers it keeps grouping its own window
387+
while a search term is active: those group counts are the window's, not the
388+
query's (objectstack#20358).
382389

383390
### ObjectQL Integration
384391

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
* objectui#10881 — the grouped grid's `Partial` marker is retired from every
11+
* pack, and the refusal that replaced it is in every pack.
12+
*
13+
* Maintainer ruling F: a grouped grid over a data source that declares no
14+
* `queryGroupHeaders` refuses grouping loudly, naming that member, instead of
15+
* grouping the page it fetched and marking the counts partial — so the
16+
* marker's three `grid.grouping.partial*` rows go from all ten packs, and one
17+
* `grid.grouping.needsHeaderQuery` row arrives in all ten.
18+
*
19+
* The absence half is only a measurement if the walker below can see that
20+
* namespace at all, so the presence half is its control: the SAME walk over
21+
* the SAME packs must find the new key in every one of them. A walker that
22+
* never reached `grid.grouping` would pass the first assertion and fail the
23+
* second.
24+
*/
25+
import { describe, it, expect } from 'vitest';
26+
import { builtInLocales } from '../locales';
27+
28+
/** Every leaf key of a pack, dotted, with its value. */
29+
function leaves(node: unknown, prefix = ''): Array<[string, unknown]> {
30+
if (node === null || typeof node !== 'object' || Array.isArray(node)) return [[prefix, node]];
31+
return Object.entries(node as Record<string, unknown>).flatMap(([key, value]) =>
32+
leaves(value, prefix ? `${prefix}.${key}` : key),
33+
);
34+
}
35+
36+
const PACKS = Object.entries(builtInLocales);
37+
38+
describe('the grouped grid marker is retired from every locale pack (objectui#10881)', () => {
39+
it('reads all ten packs', () => {
40+
expect(PACKS.map(([code]) => code).sort()).toEqual(['ar', 'de', 'en', 'es', 'fr', 'ja', 'ko', 'pt', 'ru', 'zh']);
41+
});
42+
43+
it.each(PACKS)('%s carries no grid.grouping.partial* key', (_code, pack) => {
44+
const partial = leaves(pack).map(([key]) => key).filter((key) => key.startsWith('grid.grouping.partial'));
45+
expect(partial).toEqual([]);
46+
});
47+
48+
// CONTROL — the same walk reaches `grid.grouping`: the refusal is there, and
49+
// names the member it refuses on in every language.
50+
it.each(PACKS)('%s carries grid.grouping.needsHeaderQuery, naming queryGroupHeaders', (_code, pack) => {
51+
const value = new Map(leaves(pack)).get('grid.grouping.needsHeaderQuery');
52+
expect(typeof value).toBe('string');
53+
expect(value as string).toContain('queryGroupHeaders');
54+
});
55+
});

‎packages/i18n/src/locales/ar.ts‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,12 @@ const ar = {
139139
printDialogHint: "يفتح مربع حوار الطباعة في المتصفح (ليس تصديرًا إلى PDF)",
140140
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
141141
// there are two conditions: a reported `total` states the fact with BOTH
142-
// numbers, a missing one cannot name how many. Same split as
143-
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
144-
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
145-
// `i18n-locales` chunk, not `framework`. ⛔ That ceiling's headroom is
146-
// deliberately NOT restated here — it moves on every re-baseline, and the
147-
// figure that was here went stale. `pnpm check:eager-closure` prints it.
142+
// numbers, a missing one cannot name how many. Kept terse deliberately —
143+
// this copy is eagerly loaded, and since objectui#7399 these bytes are
144+
// budgeted by the `i18n-locales` chunk, not `framework`. ⛔ That ceiling's
145+
// headroom is deliberately NOT restated here — it moves on every
146+
// re-baseline, and the figure that was here went stale.
147+
// `pnpm check:eager-closure` prints it.
148148
rowCeilingNote: "يتم عرض أول {{shown}} من أصل {{total}} سجل. ضيّق عامل التصفية.",
149149
rowCeilingNoteUnknownTotal: "يتم عرض أول {{shown}} سجل. ضيّق عامل التصفية.",
150150
},
@@ -409,13 +409,15 @@ const ar = {
409409
yes: "نعم",
410410
no: "لا",
411411
systemFields: "النظام",
412-
// objectui#7189 — partial-grouping disclosure for the grouped grid.
412+
// objectui#10881 — the grouped grid's two refusals: over a data source
413+
// that cannot answer the group header query, and over rows a host
414+
// handed in while declaring them one page of more. `queryGroupHeaders`
415+
// is the member's name and stays untranslated.
413416
grouping: {
414-
partialBadge: "جزئي",
415-
partialNotice:
416-
"تم التجميع على أول {{loaded}} سجل من أصل {{total}}. أعداد المجموعات تخص الصفحة المحمَّلة فقط، وأي مجموعة تقع كل سجلاتها خارج الصفوف المحمَّلة لا تظهر هنا.",
417-
partialNoticeUnknownTotal:
418-
"تم التجميع على {{loaded}} سجل محمَّل. قد تتطابق سجلات أخرى مع هذا العرض، لذا قد تكون أعداد المجموعات جزئية وقد لا تظهر إحدى المجموعات هنا.",
417+
needsHeaderQuery:
418+
"هذا العرض مُجمَّع، لكن مصدر بياناته لا يطبّق queryGroupHeaders، لذا لا يمكن عدّ المجموعات. أزِل التجميع لعرض السجلات.",
419+
needsWholeRows:
420+
"يتطلب التجميع جميع السجلات، لكن هذه الشبكة تلقّت صفحة واحدة منها فقط، لذا لا يمكن عدّ المجموعات. مرّر جميع السجلات، أو دع الشبكة تجلبها من مصدر بيانات يطبّق queryGroupHeaders.",
419421
},
420422
// objectui#4024 — column-footer aggregate prefixes, keyed by the spec's
421423
// `ColumnSummary` vocabulary. `pattern` owns the label/value join so this

‎packages/i18n/src/locales/de.ts‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,12 @@ const de = {
128128
printDialogHint: "Öffnet den Druckdialog Ihres Browsers (kein PDF-Export)",
129129
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
130130
// there are two conditions: a reported `total` states the fact with BOTH
131-
// numbers, a missing one cannot name how many. Same split as
132-
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
133-
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
134-
// `i18n-locales` chunk, not `framework`. ⛔ That ceiling's headroom is
135-
// deliberately NOT restated here — it moves on every re-baseline, and the
136-
// figure that was here went stale. `pnpm check:eager-closure` prints it.
131+
// numbers, a missing one cannot name how many. Kept terse deliberately —
132+
// this copy is eagerly loaded, and since objectui#7399 these bytes are
133+
// budgeted by the `i18n-locales` chunk, not `framework`. ⛔ That ceiling's
134+
// headroom is deliberately NOT restated here — it moves on every
135+
// re-baseline, and the figure that was here went stale.
136+
// `pnpm check:eager-closure` prints it.
137137
rowCeilingNote: "Erste {{shown}} von {{total}} Datensätzen. Filter eingrenzen.",
138138
rowCeilingNoteUnknownTotal: "Erste {{shown}} Datensätze. Filter eingrenzen.",
139139
},
@@ -398,13 +398,15 @@ const de = {
398398
yes: "Ja",
399399
no: "Nein",
400400
systemFields: "System",
401-
// objectui#7189 — partial-grouping disclosure for the grouped grid.
401+
// objectui#10881 — the grouped grid's two refusals: over a data source
402+
// that cannot answer the group header query, and over rows a host
403+
// handed in while declaring them one page of more. `queryGroupHeaders`
404+
// is the member's name and stays untranslated.
402405
grouping: {
403-
partialBadge: "Teilweise",
404-
partialNotice:
405-
"Gruppiert über die ersten {{loaded}} von {{total}} Datensätzen. Gruppenanzahlen gelten nur für die geladene Seite; eine Gruppe, deren Datensätze alle jenseits der geladenen Zeilen liegen, fehlt hier.",
406-
partialNoticeUnknownTotal:
407-
"Gruppiert über die {{loaded}} geladenen Datensätze. Möglicherweise passen weitere Datensätze zu dieser Ansicht, daher können Gruppenanzahlen unvollständig sein und eine Gruppe kann hier fehlen.",
406+
needsHeaderQuery:
407+
"Diese Ansicht ist gruppiert, aber ihre Datenquelle implementiert queryGroupHeaders nicht, daher können die Gruppen nicht gezählt werden. Entfernen Sie die Gruppierung, um die Datensätze anzuzeigen.",
408+
needsWholeRows:
409+
"Die Gruppierung benötigt alle Datensätze, aber dieses Raster hat nur eine Seite davon erhalten, daher können die Gruppen nicht gezählt werden. Übergeben Sie alle Datensätze oder lassen Sie das Raster sie aus einer Datenquelle laden, die queryGroupHeaders implementiert.",
408410
},
409411
// objectui#4024 — column-footer aggregate prefixes, keyed by the spec's
410412
// `ColumnSummary` vocabulary. `pattern` owns the label/value join so this

0 commit comments

Comments
 (0)