Skip to content

Commit 62ba5ef

Browse files
fix(plugin-grid): order a grouped grid's groups by the level's orderBy (#25977)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9MGufFRaP71H4Vn7WRVFLTN)_ Fixes #25858 Clause-②: no Metadata-protocol: none ## What changes A grouped grid now orders a level's groups by the level's `grouping.fields[].orderBy` (the spec key landed in #25828), while the group label still comes from `field`. A list grouped by `period_label` with `orderBy: 'period_start', order: 'desc'` renders one header per label in descending `period_start` order, the undated group last. Measured on `origin/main` before the change, the key was dropped in three places, not one: 1. `ObjectGrid.tsx` (`serverGroupingKey`): the server grouping entries were rebuilt as field, order and collapsed only, so `orderBy` never left the grid. 2. `useServerGrouping.ts` (`groupingOf`): the header query was compiled from field names alone, so `compileListViewGroupQuery` never added the `min_FIELD` / `max_FIELD` node and the header rows never carried an order value. 3. `useGroupedData.ts` (`compareGroupEntries`): no `orderBy` handling on either sort path. The card named (3) and allowed (1) and (2) "if the header column is not forwarded"; it was not, so all three are fixed here. - **Server path.** `ObjectGrid` forwards `orderBy`; `useServerGroupHeaders` compiles each level with its `orderBy` and `order` (a level ordered by its own key still carries the name alone, so flipping its `order` re-asks nothing; a changed `orderBy` re-asks, since it changes the answer's columns). `useGroupedData` reads each level's order value off the column `listViewGroupOrderColumn(grouping, depth)` names, never a re-derived alias. - **Client path** (rows handed in whole). Each group's order value is the `min` (asc) or `max` (desc) of `orderBy` over its rows, folded once before the sort. - **One comparator for both.** `compareGroupEntries` orders by the order value first, in `order`; a `null` value sorts last in both directions; equal values fall back to the existing key comparison (declared option order, then label) in the same direction. A level without `orderBy`, or with `orderBy` naming the grouping field, is unchanged. Values are reduced with `@object-ui/core`'s `getSortValue` / `compareSortValues`; a numeric string (a driver's decimal) is read as the number it spells, so `3.10` does not sort after `3.9`. - **Read gate.** An `orderBy` the principal may not read (`groupingKeyReadable`, the gate the grouping key already passes) is not forwarded, and that level orders by its own key: the header row would otherwise carry the hidden field's min/max and the group order would disclose it. - **Type mirror (card item 4).** None to change: `UsableGroupingField` derives from `@objectstack/spec/ui`'s `GroupingConfig`, and the `@object-ui/types` zod faces take `GroupingConfigSchema` by reference, so `orderBy` was already on both. `scripts/sdui-manifest.record.json` is re-recorded because `check-sdui-manifest` hashes the `ui/packages` registry sources; `sdui.manifest.json` itself is byte-identical (106 components, sha256 1f6e1ccb925a…). ## Tests New `ui/packages/plugin-grid/src/__tests__/groupOrderBy-25858.test.tsx`, 18 tests: - the card's acceptance set (`Q4 2025`, `Sep 2026`, `Oct 2026`, `Q4 2026`, `Nov 2026`, `Q1 2027`, plus an undated period) on header rows produced by answering `compileListViewGroupQuery`'s own output, both directions, the undated group last in both; an outer level of a two-level grouping; counts unchanged; - the client path on the same rows, plus `min`-for-asc / `max`-for-desc over a group whose rows span values, and numeric-string values; - precedence over a declared option order, with ties falling back to it in the same direction; - through `ObjectGrid`: the header query carries `max_period_start` and the painted headers read in descending start; rows handed in order the same way; - controls: no `orderBy` (label order, option order, and a header query whose only aggregation is `count`), and `orderBy` naming the grouping field itself. Runs (code tree `b3b58b69`; the final head `19f3f573` adds only the sdui record file): - `pnpm exec vitest run packages/plugin-grid/src/__tests__/groupOrderBy-25858.test.tsx` (from `ui/`): Test Files 1 passed, Tests 18 passed. - `pnpm exec vitest run packages/plugin-grid/` (from `ui/`): Test Files 248 passed (248), Tests 2222 passed (2222). - `pnpm --filter @object-ui/plugin-grid run type-check` (`tsc --noEmit && tsc -p tsconfig.test.json`): exit 0. Ablations, each through `scripts/ablation-replace.mjs` (anchor hit 1, restored blob equal to HEAD, `git diff HEAD` empty), none left in the tree: - the order-value arm of `compareGroupEntries` disabled: 10 failed / 8 passed (every `orderBy` assertion red, every control green); - the numeric-string reading disabled: 1 failed (the numeric-string test); - `headerLevelOf` forwarding no `orderBy`: 1 failed (the `ObjectGrid` server-path test); - `ObjectGrid` forwarding no `orderBy`: 1 failed (the same test). ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `19f3f573` derives 47 families; all 47 were run and exited 0, and `--ran` with per-command exit codes answers "47 run, 0 NOT-MEASURED (a DERIVED zero)". `check-sdui-manifest` was red before the record was regenerated (`node scripts/gen-sdui-manifest-node.mjs`) and is green after. The type-check lanes and the CI-owned whole-tree families are left to this PR's checks. ## Acceptance notes - The client path is live only for rows handed in whole (inline rows or a `value` provider): a grid or `ListView` that fetches its own rows over a source with no header query refuses grouping (objectui#10881), so no projection change is needed for `orderBy` there; such hosts' rows already carry every field. - ⛔ Not this PR: the HotCRM `forecast.view.ts` edit. Its follow-up is to drop the outer `period_start` level once this lands. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Baqv5mDLFH63hvh9G4YGn5 --- _Generated by [Claude Code](https://claude.ai/code/session_01Baqv5mDLFH63hvh9G4YGn5)_ <!-- seat domain:ui#1: the claim on #25858 was reworded so the Closing-Target Claim Guard reads it; this edit re-runs that guard --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f0b33ee commit 62ba5ef

6 files changed

Lines changed: 520 additions & 22 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@object-ui/plugin-grid': patch
3+
---
4+
5+
fix(plugin-grid): a grouped grid orders a level's groups by its `grouping.fields[].orderBy`, the label still from `field`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A view grouped by `period_label` with `orderBy: 'period_start', order: 'desc'` now lists one header per label in descending `period_start` order, where it used to list the labels as text (`Nov 2026` before `Oct 2026` before `Q1 2027`).
11+
12+
- **Server-grouped grid.** The grid forwards the level's `orderBy` into the group header query, so the query carries the `min` (asc) / `max` (desc) column the spec compiles for it, and sorts the header rows by the column `listViewGroupOrderColumn` names. An `orderBy` field the user may not read is not forwarded: that level orders by its own key.
13+
- **Rows handed in whole.** Each group's order value is the `min` (asc) or `max` (desc) of `orderBy` over its rows, so a group sits where its first row would if the rows were sorted by `orderBy`.
14+
- **Both paths.** A group with no `orderBy` value sorts last in both directions; equal values fall back to the existing key comparison (declared option order, then label) in the same direction. A level without `orderBy`, or with `orderBy` naming the grouping field itself, orders exactly as before.

‎scripts/sdui-manifest.record.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,6 @@
4949
"ui/packages/types"
5050
],
5151
"registrySources": 922,
52-
"registrySourcesSha256": "ad9e0875b83f339e03c528fc3be774592615dfdc5ee5fe9becc4376ddb5f0d52"
52+
"registrySourcesSha256": "7734fe6bc3763fe8318db624b9e0dd4c48b2dc7b15bf2a3fb1f78e46bb8d959e"
5353
}
5454
}

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3430,18 +3430,29 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
34303430
// [objectui#7189] The entries the SERVER groups by: the render grouping less
34313431
// any key this principal may not read (see `groupingKeyReadable`). Keyed on
34323432
// the names, so a host rebuilding `grouping` every render re-asks nothing.
3433+
// [objectstack#25786] A level's `orderBy` rides along — the header query
3434+
// compiles its `min` / `max` column from it — under the same read gate: an
3435+
// `orderBy` this principal may not read is dropped, and the level orders by
3436+
// its own key, since the header row would carry that field's value and the
3437+
// group order would disclose it.
34333438
const serverGroupingKey = serverGroupedFetch
34343439
? JSON.stringify(
34353440
usableGroupingFields(groupingForRender?.fields)
34363441
.filter((gf) => groupingKeyReadable(gf.field))
3437-
.map((gf) => [gf.field, gf.order ?? null, gf.collapsed ?? null]),
3442+
.map((gf) => [
3443+
gf.field,
3444+
gf.order ?? null,
3445+
gf.collapsed ?? null,
3446+
gf.orderBy && groupingKeyReadable(gf.orderBy) ? gf.orderBy : null,
3447+
]),
34383448
)
34393449
: '[]';
34403450
const serverGroupingFields = React.useMemo(
3441-
() => (JSON.parse(serverGroupingKey) as Array<[string, 'asc' | 'desc' | null, boolean | null]>)
3442-
.map(([field, order, collapsed]) => ({
3451+
() => (JSON.parse(serverGroupingKey) as Array<[string, 'asc' | 'desc' | null, boolean | null, string | null]>)
3452+
.map(([field, order, collapsed, orderBy]) => ({
34433453
field,
34443454
...(order ? { order } : {}),
3455+
...(orderBy !== null ? { orderBy } : {}),
34453456
...(collapsed !== null ? { collapsed } : {}),
34463457
})),
34473458
[serverGroupingKey],

0 commit comments

Comments
 (0)