You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit ac3de5a
Browse filesBrowse the repository at this point in the historyBrowse files
merge(cutover): bring main e2aaaf1 into the cutover branch (run 6)
main gained objectstack#25833 (the sdui lockstep and manifest gates learn
the in-tree ui/ oracle; pin mode unchanged, measured green here) and
objectstack#25828 (list-view grouping orderBy). The ui/ import and the
adaptation series are untouched; this run measures the merged tree
against the base the sitting will merge onto.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSDL3SFtJaKzcymvCD8RsU
feat(spec,lint): a list-view grouping level can order its groups by another field (`grouping.fields[].orderBy`)
7
+
8
+
Clause-②: yes
9
+
Metadata-protocol: added
10
+
11
+
-**Before.** A grouping level ordered its groups only by its own key. A period label grouped as text (`Nov 2026`, `Oct 2026`, `Q1 2027`, `Q4 2025`), so putting periods in time order took a second, outer grouping level on the period start, which showed two header rows for every group.
12
+
-**Now.**`GroupingFieldSchema` gains `orderBy`, the name of a field on the same object whose value orders this level's groups: `{ field: 'period_label', orderBy: 'period_start', order: 'desc' }`. When it is absent, or names the grouping field itself, the groups order by their own key as before. Nothing is defaulted in.
13
+
-**What the header query carries.** The consumer still sorts the header rows. `compileListViewGroupQuery` now adds the value to sort by: one `min` node over `orderBy` for `order: 'asc'`, or one `max` node for `'desc'`, under `min_<field>` / `max_<field>`. Each group sits where its first row would sit if the rows were sorted by `orderBy` in `order`. The grouping key stays the only `groupBy` column, so there is one header per label. A column summary that already declares the same `min` is the same node and the same column. In a multi-level grouping the outer level's value folds exactly from the leaf rows (the `min` of mins, the `max` of maxes).
14
+
-**`listViewGroupOrderColumn(grouping, level)`** (`@objectstack/spec/ui`) names the header-row column a level sorts by: the grouping field itself, or the order node's alias. A consumer reads this instead of rebuilding the alias. A group whose order value is `null` sorts last in both directions, and equal values fall back to the group key in the same direction.
15
+
- **Refused, at compile and at authoring.** An empty `orderBy` is refused by the schema (`too_small`), and a padded one, whitespace-only included, by name, as a padded `field` is, because the name is compiled verbatim. The compiler refuses a blank one in-process (`grouping_order_by_blank`, `INVALID_QUERY` / 400). An order alias that lands on a grouped field's own column is `alias_collision`. `os validate`, `os build` and the runtime save door judge `orderBy` the way the server doors do. An unknown name is `list-view-field-unknown`. A dotted path is `list-view-field-dotted`, because the header query compiles no join. A field whose type `min` / `max` cannot order on every backend is the new `list-view-group-order-unorderable` (`error`). That covers the string classes, whose order depends on the collation, the virtual `formula`, and every other type outside the numeric, temporal and boolean classes. Each of these is refused `400 INVALID_FIELD` on the server, so the grouped view would render no group: an unknown or dotted name by the data door's ingress (`findData`), and an unorderable type by `engine.aggregate`'s type door.
16
+
-**Unchanged.**`order` and `collapsed` keep their meaning and defaults. The row page query (`compileListViewGroupRowsQuery`) does not change. `EngineAggregateOptions` gains no `orderBy`, because the group sort stays the consumer's. That sort is right only over the whole header set, which is what the compiler asks for: a `limit` on the header query is sliced by `findData` before any sort, so a paged header set honours neither `order` nor `orderBy`, the limit already recorded on `GroupingConfigSchema`. The new key is purely additive, so a grouping declared today compiles to exactly the same query.
Copy file name to clipboardExpand all lines: content/docs/references/ui/component.mdx
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -887,7 +887,7 @@ Sort field and direction pair
887
887
888
888
| Property | Type | Required | Description |
889
889
| :--- | :--- | :--- | :--- |
890
-
|**fields**|`{ field: string; order?: Enum<'asc' \| 'desc'>; collapsed?: boolean }[]`| ✅ | Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field); the same order as the group header query's `groupBy`|
890
+
|**fields**|`{ field: string; order?: Enum<'asc' \| 'desc'>; orderBy?: string; collapsed?: boolean }[]`| ✅ | Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field); the same order as the group header query's `groupBy`|
|**fields**|`{ field: string; order?: Enum<'asc' \| 'desc'>; collapsed?: boolean }[]`| ✅ | Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field); the same order as the group header query's `groupBy`|
1040
+
|**fields**|`{ field: string; order?: Enum<'asc' \| 'desc'>; orderBy?: string; collapsed?: boolean }[]`| ✅ | Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field); the same order as the group header query's `groupBy`|
Copy file name to clipboardExpand all lines: content/docs/references/ui/view.mdx
+7-5Lines changed: 7 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -613,14 +613,15 @@ Record grouping configuration — SERVER-SIDE: the set of groups and every numbe
613
613
614
614
| Property | Type | Required | Description |
615
615
| :--- | :--- | :--- | :--- |
616
-
|**fields**|`{ field: string; order?: Enum<'asc' \| 'desc'>; collapsed?: boolean }[]`| ✅ | Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field); the same order as the group header query's `groupBy`|
616
+
|**fields**|`{ field: string; order?: Enum<'asc' \| 'desc'>; orderBy?: string; collapsed?: boolean }[]`| ✅ | Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field); the same order as the group header query's `groupBy`|
617
617
618
618
### Nested Shape: `GroupingConfig.fields[number]`
619
619
620
620
| Property | Type | Required | Description |
621
621
| :--- | :--- | :--- | :--- |
622
622
|**field**|`string`| ✅ | Field name to group by — one `groupBy` column of the group header query; the header row carries its raw stored value (null for the empty group). NO leading or trailing whitespace: the name is compiled into the aggregate query verbatim, so a padded spelling buckets every row into one empty group instead of grouping them. |
623
-
|**order**|`Enum<'asc' \| 'desc'>`| optional (default: `"asc"`) | Group sort order — applied by the consumer over the header rows (the aggregate query carries no orderBy) |
623
+
|**order**|`Enum<'asc' \| 'desc'>`| optional (default: `"asc"`) | Group sort order — applied by the consumer over the header rows (the aggregate query itself sorts nothing): by the group key, or by the `orderBy` value each header row carries when `orderBy` is set |
624
+
|**orderBy**|`string`| optional | Field whose value orders this level's groups, when that is not the grouping field — a field of the same object; default: the grouping field itself, so the groups order by their own key. The group header query then carries, per group, the smallest value of this field for `order: asc` and the largest for `desc` (each group sits where its first row would if the rows were sorted by this field), and the consumer sorts the header rows by it; a group with no value here sorts last in both directions, and equal values fall back to the group key in the same direction. NO leading or trailing whitespace (compiled verbatim, as `field` is). A numeric, temporal or boolean field: min/max over a string orders by collation, which differs between backends, so the server refuses it |
624
625
|**collapsed**|`boolean`| optional (default: `false`) | Collapse groups by default (presentation only) |
625
626
626
627
@@ -633,7 +634,8 @@ Record grouping configuration — SERVER-SIDE: the set of groups and every numbe
633
634
| Property | Type | Required | Description |
634
635
| :--- | :--- | :--- | :--- |
635
636
|**field**|`string`| ✅ | Field name to group by — one `groupBy` column of the group header query; the header row carries its raw stored value (null for the empty group). NO leading or trailing whitespace: the name is compiled into the aggregate query verbatim, so a padded spelling buckets every row into one empty group instead of grouping them. |
636
-
|**order**|`Enum<'asc' \| 'desc'>`| optional (default: `"asc"`) | Group sort order — applied by the consumer over the header rows (the aggregate query carries no orderBy) |
637
+
|**order**|`Enum<'asc' \| 'desc'>`| optional (default: `"asc"`) | Group sort order — applied by the consumer over the header rows (the aggregate query itself sorts nothing): by the group key, or by the `orderBy` value each header row carries when `orderBy` is set |
638
+
|**orderBy**|`string`| optional | Field whose value orders this level's groups, when that is not the grouping field — a field of the same object; default: the grouping field itself, so the groups order by their own key. The group header query then carries, per group, the smallest value of this field for `order: asc` and the largest for `desc` (each group sits where its first row would if the rows were sorted by this field), and the consumer sorts the header rows by it; a group with no value here sorts last in both directions, and equal values fall back to the group key in the same direction. NO leading or trailing whitespace (compiled verbatim, as `field` is). A numeric, temporal or boolean field: min/max over a string orders by collation, which differs between backends, so the server refuses it |
637
639
|**collapsed**|`boolean`| optional (default: `false`) | Collapse groups by default (presentation only) |
638
640
639
641
@@ -1018,7 +1020,7 @@ View filter rule
1018
1020
1019
1021
| Property | Type | Required | Description |
1020
1022
| :--- | :--- | :--- | :--- |
1021
-
|**fields**|`{ field: string; order?: Enum<'asc' \| 'desc'>; collapsed?: boolean }[]`| ✅ | Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field); the same order as the group header query's `groupBy`|
1023
+
|**fields**|`{ field: string; order?: Enum<'asc' \| 'desc'>; orderBy?: string; collapsed?: boolean }[]`| ✅ | Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field); the same order as the group header query's `groupBy`|
1022
1024
1023
1025
### Nested Shape: `ListView.rowColor`
1024
1026
@@ -1395,7 +1397,7 @@ View filter rule
1395
1397
1396
1398
| Property | Type | Required | Description |
1397
1399
| :--- | :--- | :--- | :--- |
1398
-
|**fields**|`{ field: string; order?: Enum<'asc' \| 'desc'>; collapsed?: boolean }[]`| ✅ | Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field); the same order as the group header query's `groupBy`|
1400
+
|**fields**|`{ field: string; order?: Enum<'asc' \| 'desc'>; orderBy?: string; collapsed?: boolean }[]`| ✅ | Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field); the same order as the group header query's `groupBy`|
0 commit comments