Skip to content

Commit f750119

Browse files
fix(spec): type object-grid and object-kanban grouping by GroupingConfigSchema (#20856)
Fixes #20831 Clause-②: yes ## What this changes `ComponentPropsMap['object-grid'].grouping` and `ComponentPropsMap['object-kanban'].grouping` were `z.unknown().optional()`. Both now take the list view's own `GroupingConfigSchema` **by reference**, imported from `./view.zod` beside the six schemas `component.zod.ts` already takes from there. There is one schema, not a copy of its shape, and each row keeps its describe. A padded grouping field name, a number, a bare string, an empty `fields` list or an undeclared key inside the config is now refused on these two doors exactly as `list-view` refuses it. The prop is typed, not retired. Both renderers are live readers of this exact shape. Readings are at objectui `db11afd49`, the pin in `.objectui-sha`: - **Grid**: `ObjectGridSchema.grouping` is typed as the spec's `GroupingConfig` (`packages/types/src/objectql.ts:1123`). `ObjectGrid.tsx` reads `schema.grouping?.fields` (`:2050`, `:2839`), and `useGroupedData.ts` reads `.field`, `.order` and `.collapsed` per entry (`:281`, `:443-547`). - **Kanban**: `effectiveSwimlaneField = schema.swimlaneField || schema.grouping?.fields?.[0]?.field` (`ObjectKanban.tsx:1212`). - A read-point grep of every `grouping.` member across objectui `packages/*/src` (tests excluded) at the pin finds `fields` and nothing else. ## Files (7, +338 / -7 against `origin/main` `688ddef3c`) | File | Why | | --- | --- | | `packages/spec/src/ui/component.zod.ts` | the import, the two members, and a comment on each | | `packages/spec/src/ui/component-object-block-grouping.pin.test.ts` | the pins (below) | | `packages/spec/src/migrations/entries/semantic/18.ui-object-block-grouping-config-typed.ts` + regenerated `registry.ts` | the ADR-0087 disposition (below) | | `content/docs/references/ui/component.mdx` | regenerated by `gen:docs`: `grouping` now shows its shape on both rows | | `packages/spec/dropped-refinements.baseline.json` | **outside the claim's file surface; see below** | | `.changeset/20831-object-block-grouping-typed.md` | `minor`, BREAKING banner, migration table, `Clause-②: yes (narrowing)` and the registered ADR-0087 marker | **File-surface breach, declared.** The spec build refuses the schema change until `dropped-refinements.baseline.json` records two new sites, `grouping.fields.element.field` on `ui/ObjectGridProps` and on `ui/ObjectKanbanProps`, and moves `droppedRefinementSites` 610 to 612. The build prints the corrected entry itself. The cause is the non-padded field-name `superRefine` that `GroupingFieldSchema` carries. JSON Schema cannot express it, and the ledger already records it at every list-view grouping site (`ui/GroupingConfig`, `ui/GroupingField`, `ui/ListView`, the manifest views). The ledger is hand-edited by design, so a new site is a reviewed line. The alternatives are both out: deleting the refinement is forbidden by the ledger's own rule, and teaching the projection list a new named pattern is a public-contract decision of its own. The ledger's `refinementSitesThatDidProject` (369) is left alone. This build prints 421, but none of the grouping refinements is one of the five projected patterns, so this diff adds zero to it. ## Pins The pins are in `component-object-block-grouping.pin.test.ts`: 22 cases, each asserting issue `code` and `path`, on both rows. - **Refused:** - padded name: `custom` at `grouping.fields.0.field`, and the message names the received value and the unpadded spelling; - `grouping: 42`: `invalid_type` at `grouping`. There is no `fields[0]` for a number to fail at, so the triage's `grouping.fields[0].field` path applies to the padded case only; - bare string: `invalid_type` at `grouping`; - `{ fields: [] }`: `too_small` at `grouping.fields`; - an undeclared key: `unrecognized_keys` at `grouping`. - **Controls:** - a fully spelled grouping parses byte-identical (`toStrictEqual` on the input); - the short form `{ field }` parses clean and gains the list view's defaults (`order: 'asc'`, `collapsed: false`) on the parse output. The component-props gate discards that output and the envelope audit walks raw keys only, so nothing downstream sees them; - an absent `grouping` stays absent. - **One schema:** the member unwraps to `GroupingConfigSchema._zod.def` by identity, and answers what `GroupingConfigSchema` answers on the whole corpus, one path level down. **Ablation**, run at `69a1be23f` (the `component.zod.ts` blob `39065a62` is unchanged at HEAD): both members were reverted to `z.unknown()` through `scripts/ablation-replace.mjs` (anchor 2 to 0, `--expect 2`, trap restore). Result: **18 failed / 4 passed**. The 4 that stay green are the byte-identical and absent controls, which `z.unknown()` also passes. The restore was proven by blob equals HEAD and an empty `git diff HEAD`. No `dist/` was involved: the test imports `./component.zod` from source. ## ADR-0087 disposition: `registered ui-object-block-grouping-config-typed` A new semantic entry for protocol 18 carries the rewrite table: - padded name: write it unpadded; - bare string: wrap it as `{ fields: [{ field }] }`; - empty list, number or other value: delete the key; - on `object-kanban`, `swimlaneField` still wins over `grouping`. It is deliberately not a D2 conversion: a padded name is refused with guidance, never trimmed. The entry states what happens at rest, and it was measured: - `PageComponentSchema` does not parse `properties` against `ComponentPropsMap`. - The component-props rule is `tier: 'advisory'`, `surfaces: CLI_ONLY` (`packages/lint/src/authoring-rules.ts:1022`). - So a stored page keeps loading and rendering. The refusal reaches authors as `component-props-invalid` on `os validate` / `os build` / `os lint`. - Measured through `validateComponentProps` on the rebuilt spec: a padded kanban grouping reports at `…properties.grouping.fields.0.field`, and `42` on a grid at `…properties.grouping`. The valid control reports nothing. `node scripts/check-adr-0087-registration.mjs --base origin/main` answers exit 0, reporting `[BREAKING+clause-②-narrowing] registered ui-object-block-grouping-config-typed (new here …)`. ## Dispatch measurements 1. Both members read `z.unknown()` at base `1741c5dcb` (`:3596`, `:3993`). 2. No import cycle: `view.zod.ts` imports nothing from `component.zod.ts`, and `component.zod.ts` already takes six lazy schemas from it. The identity pin is green under the lazy proxy. 3. The grid reader consumes the `GroupingConfigSchema` shape (readings above), so both rows are typed. There is no `needs_decision` fork. 4. The ADR-0087 marker is `registered`, judged by the gate (above). 5. Consumer radius, all measured at the merge base `688ddef3c`: - `examples/**` has 2 `object-grid` nodes (`app-showcase` `command-center.page.ts:172`, `my-work.page.ts:62`). Neither authors `grouping`, and there are 0 `object-kanban` nodes. - No fixture under `packages/**` authors `grouping` on either row. - The only non-spec package that parses these rows is `@objectstack/lint` (`validate-component-props.ts`, `page-envelope-audit.ts`), downstream of spec (the `...@objectstack/spec` direction). Its suite is green (below). - Full-repo pin sweep: no test anywhere asserts the old `z.unknown()` acceptance on these rows, and the padded-name refusal text is unchanged, so no pin flips. ## Verification, at HEAD `47c76064d` (after merging `origin/main` through `os-regen-merge.sh`; no conflicts) - `pnpm --filter @objectstack/spec build`: exit 0; 612 dropped sites across 212 schemas, all declared. - `pnpm --filter @objectstack/spec check:generated --fix`: "All 15 generated artifacts are up to date", measured against a dist built by that run. authorable-surface, authorable-defaults and api-surface are unchanged. - `pnpm --filter @objectstack/spec typecheck`: exit 0. The test layer compiles with debt unchanged (53 files / 251 errors / 138 signatures). - Spec suite (`vitest run --project local --maxWorkers=2`): **579 files, 17088 passed**, 1 todo. - Consumer `@objectstack/lint` suite: **117 files, 5436 passed**, against a spec dist rebuilt from this tree. - `node scripts/pm/dispatch-gates.mjs --commands` (no paths): 112 commands. `--ran` reconciles **112 derived, 110 run (all exit 0), 2 NOT MEASURED, 0 UNRUN**. - Five gates first exited 3 on missing builds: `check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples`, `check:docs-transcript-drift`, `check:lean-entry-closure`. Each was rerun green after building lint, client, client-react and objectql. - `check-changeset-no-major` with this body as the event: LEVEL AXIS reads `Clause-②: yes`, and no package is graded `patch`. - **Lint narrowing** (the repo-wide `pnpm lint` is CI's): - eslint `--no-inline-config --format json` over the 4 changed TS files reports 4 files, 0 errors, 0 warnings; - `eslint.config.mjs` puts every `packages/**/*.ts` in the population, and the other 3 changed files are md, mdx and json, outside its globs; - the config never enables type-aware linting (no `parserOptions.project`), so this diff cannot move a verdict on an untouched file. **NOT MEASURED locally:** - `check:dual-build-cjs-loads` and `check:type-check-debt`, reason: both need every workspace package built (84 have no `dist/`). - The CLI `integration` tier, reason: declared for CI; this diff touches no CLI file. Its `migrate-meta-engine-guidance.test.ts` prints every semantic entry. I checked the new entry against its tracker-id regex (no hit), but not its verbatim print. - The four CI type-check lanes and the six path-scheduled CI jobs, reason: CI's own runs. ## Acceptance notes These were noted, not filed. Carrier: none. - The protocol-18 entry `ui-object-grid-page-size-positive-integer-refused` says a stored page "is refused on its next authoring-path save". Its sibling `object-grid-default-filters-rule-array`, and the rule's `surfaces: CLI_ONLY`, say the refusal is an advisory CLI finding only. This is a read-only reading: I did not exercise a save. - The `component.zod.ts` section header still lists "filter shapes … grouping configs" as the `z.unknown()` value posture. Both have since been typed. This is historical text, left as it stands. - `dropped-refinements.baseline.json` `measured.refinementSitesThatDidProject` reads 369 while the build prints 421. The gap predates this diff and is ungated. - For the objectui mirror card (the seat files it): the `object-kanban` registration description (`plugin-kanban/src/index.tsx:684`) and `structuredKeysAreDeclaredAndHonoured-8313.test.ts` both describe the spec row as `z.unknown()`. Its row-4 fixture `{ fields: [{ field: 'owner' }] }` stays valid under the typed row. Concurrent work: #20785 edits `component.zod.ts` only at `PageHeaderProps` and one `component.mdx` line in that section, disjoint from these regions; this diff touches neither `authorable-surface/ui.json` nor `authorable-defaults/ui.json`. #20694 is not addressed here; it runs after this lands. --- _Generated by [Claude Code](https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 76bd58f commit f750119

7 files changed

Lines changed: 338 additions & 7 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
The `grouping` prop of the `object-grid` and `object-kanban` page blocks is now judged by the list view's own `GroupingConfigSchema`, so a padded grouping field name or a wrong-shaped value is refused on these blocks exactly as it is on `list-view` (#20831).
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: registered ui-object-block-grouping-config-typed -->
10+
11+
**BREAKING**: `ComponentPropsMap['object-grid'].grouping` and `ComponentPropsMap['object-kanban'].grouping` were `z.unknown()`, so any value parsed. Both now take `GroupingConfigSchema` by reference: `{ fields: [{ field, order?, collapsed? }, ...] }`, at least one entry, each `field` the stored name with no leading or trailing whitespace. Both renderers already read exactly that shape: the grid groups by every `grouping.fields[i].field` and reads `order` / `collapsed`, and the kanban board takes `grouping.fields[0].field` as its swimlane field when no `swimlaneField` is authored. A value outside it validated green before and rendered one `(empty)` group on the grid, or one swimlane holding every card on the board, with no error.
12+
13+
What is refused now, and the one-line fix for each:
14+
15+
| Authored `grouping` | Refused as | Write instead |
16+
| --- | --- | --- |
17+
| `{ fields: [{ field: ' business_unit ' }] }` | `custom` at `grouping.fields.0.field`, naming the received value | `{ fields: [{ field: 'business_unit' }] }` |
18+
| `'business_unit'` (a bare string) | `invalid_type` at `grouping` | `{ fields: [{ field: 'business_unit' }] }` |
19+
| `42`, `true`, or any other non-object | `invalid_type` at `grouping` | delete the key; it never grouped anything |
20+
| `{ fields: [] }` | `too_small` at `grouping.fields` | delete the key |
21+
| `{ fields: [...], showCounts: true }` (an undeclared key) | `unrecognized_keys` at `grouping` | delete the undeclared key |
22+
23+
On `object-kanban`, an authored `swimlaneField` still wins over `grouping`; when both are set, deleting `grouping` is the whole migration. A fully-spelled grouping parses byte-identically; a short entry `{ field }` parses clean and gains the list view's defaults (`order: 'asc'`, `collapsed: false`), which is how the grid already read it.
24+
25+
Where it surfaces: the component-props gate reports a refused value as a `component-props-invalid` finding at the offending path on `os validate`, `os build` and `os lint` (advisory, as every finding of that gate is). A page component's `properties` are not parsed on the metadata save path, so a stored page keeps loading and rendering as it does today until its source is fixed; the ADR-0087 semantic entry `ui-object-block-grouping-config-typed` carries the same table for `os migrate meta`.
26+
27+
The published JSON Schemas for `ObjectGridProps` and `ObjectKanbanProps` now describe the `grouping` shape; the non-padded field-name rule is a runtime refinement the JSON Schema does not express, recorded for both as `grouping.fields.element.field` in `dropped-refinements.baseline.json`, beside the same site on every list-view schema.

‎content/docs/references/ui/component.mdx‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,7 @@ Sort field and direction pair
705705
| **searchableFields** | `string[]` | optional | Fields the toolbar search queries; a non-empty list enables search |
706706
| **showSearch** | `boolean` | optional | Show the search box (read only when `searchableFields` is absent) |
707707
| **rowHeight** | `any` | optional | Row density mode (e.g. compact / comfortable) |
708-
| **grouping** | `any` | optional | Row grouping config |
708+
| **grouping** | `{ fields: object[] }` | optional | Row grouping config |
709709
| **aggregations** | `any` | optional | Group aggregation config (sum/avg/… per column) |
710710
| **conditionalFormatting** | `any` | optional | Conditional row/cell formatting rules |
711711
| **rowColor** | `any` | optional | Row color rules |
@@ -757,6 +757,12 @@ Sort field and direction pair
757757
| **field** | `string` | ✅ | Field name to sort by |
758758
| **order** | `Enum<'asc' \| 'desc'>` | ✅ | Sort direction |
759759

760+
### Nested Shape: `ObjectGridProps.grouping`
761+
762+
| Property | Type | Required | Description |
763+
| :--- | :--- | :--- | :--- |
764+
| **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` |
765+
760766
### Nested Shape: `ObjectGridProps.data[provider='object']`
761767

762768
| Property | Type | Required | Description |
@@ -806,7 +812,7 @@ Sort field and direction pair
806812
| **titleField** | `string` | optional | Legacy fallback for `cardTitle` (the board reads `cardTitle \|\| titleField`). Prefer `cardTitle` |
807813
| **cardFields** | `string[]` | optional | Fields rendered on each card |
808814
| **swimlaneField** | `string` | optional | Field for horizontal swimlanes (in addition to columns) |
809-
| **grouping** | `any` | optional | View grouping config; its first field is the swimlane fallback |
815+
| **grouping** | `{ fields: object[] }` | optional | View grouping config; its first field is the swimlane fallback |
810816
| **quickAdd** | `never` | optional | [REMOVED] `object-kanban` property `quickAdd` was removed in @objectstack/spec 17 (ADR-0049) — the board forwarded it, but the per-column affordance is gated on both `quickAdd` and `onQuickAdd`, and `onQuickAdd` is a host-supplied function JSON cannot carry and no producer ever put on an `object-kanban` node, so authoring it was a parse-clean no-op. Delete the key. The quick-add control is unchanged on the `kanban-ui` block, where a React host supplies the `onQuickAdd` slot the control needs. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
811817
| **coverImageField** | `string` | optional | Image field rendered as the card cover |
812818
| **conditionalFormatting** | `any` | optional | Card conditional formatting rules |
@@ -822,6 +828,12 @@ View filter rule
822828
| **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator |
823829
| **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. |
824830

831+
### Nested Shape: `ObjectKanbanProps.grouping`
832+
833+
| Property | Type | Required | Description |
834+
| :--- | :--- | :--- | :--- |
835+
| **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` |
836+
825837
### Nested Shape: `ObjectKanbanProps.navigation`
826838

827839
| Property | Type | Required | Description |

‎packages/spec/dropped-refinements.baseline.json‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"measured": {
44
"zod": "4.4.3",
55
"publishedSchemasWithDroppedRefinements": 212,
6-
"droppedRefinementSites": 610,
6+
"droppedRefinementSites": 612,
77
"refinementSitesThatDidProject": 369,
88
"refinementSitesWithNoJsonFormToCompare": 0
99
},
@@ -1326,12 +1326,14 @@
13261326
},
13271327
"ui/ObjectGridProps": {
13281328
"sites": [
1329-
"filter.element"
1329+
"filter.element",
1330+
"grouping.fields.element.field"
13301331
]
13311332
},
13321333
"ui/ObjectKanbanProps": {
13331334
"sites": [
1334-
"filter.element"
1335+
"filter.element",
1336+
"grouping.fields.element.field"
13351337
]
13361338
},
13371339
"ui/ObjectListView": {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
// The grouping prop on the two object blocks whose renderers read a grouping
6+
// config. Typed by reference to the list view's own GroupingConfigSchema, so
7+
// every door that carries a grouping judges it with the one schema. Deliberately
8+
// NOT a D2 conversion: a padded field name is refused with guidance naming the
9+
// field, never trimmed in silence.
10+
export const entry: SemanticMigration = {
11+
id: 'ui-object-block-grouping-config-typed',
12+
// No backticks in `surface` — build-upgrade-guide renders it inside a code
13+
// span already, and a nested backtick would close it.
14+
surface:
15+
'the grouping property of the object-grid and object-kanban page blocks '
16+
+ "(ComponentPropsMap['object-grid' | 'object-kanban'].grouping), which was z.unknown and "
17+
+ 'therefore accepted any value: a padded field name such as '
18+
+ "{ fields: [{ field: ' business_unit ' }] }, a number, a bare field-name string, an "
19+
+ 'empty fields list, or keys the grouping config does not declare',
20+
replacement:
21+
'the grouping config a list view carries, `GroupingConfigSchema` from '
22+
+ '`@objectstack/spec/ui`: `{ fields: [{ field, order?, collapsed? }, ...] }` with at least '
23+
+ 'one entry, each `field` naming the record field exactly as it is stored, with no leading '
24+
+ 'or trailing whitespace, `order` one of `asc` / `desc` and `collapsed` a boolean. A padded '
25+
+ "name is rewritten unpadded (`' business_unit '` becomes `'business_unit'`); a bare "
26+
+ "string `'business_unit'` becomes `{ fields: [{ field: 'business_unit' }] }`; an empty "
27+
+ '`fields` list, a number, or any other value is deleted, since it never grouped anything. '
28+
+ 'On `object-kanban`, `swimlaneField` still wins when both are authored, and deleting '
29+
+ '`grouping` is the whole migration there when `swimlaneField` is set',
30+
reason:
31+
'Both blocks\' renderers read the list view\'s grouping shape and nothing else: the grid '
32+
+ 'groups its rows by every `grouping.fields[i].field` (its server-side group header query '
33+
+ 'and its row projection) and reads `order` and `collapsed` per level, and the kanban board '
34+
+ 'takes `grouping.fields[0].field` as its swimlane field when no `swimlaneField` is '
35+
+ 'authored, looking that raw name up on every card. The list view has refused a padded '
36+
+ 'grouping field name since protocol 17.5, and a list view\'s `grouping` is a closed shape; '
37+
+ 'these two doors declared the same prop as `z.unknown`, so the same value that list view '
38+
+ 'refuses validated green here and rendered wrong with no error — the grid showed one '
39+
+ '`(empty)` group holding every row, the board one swimlane holding every card. The prop is '
40+
+ 'kept, not retired: the board\'s fallback is a live reader of the grouping config. '
41+
+ 'The rewrite is left to the author on purpose: a trimming rule would make a padded and an '
42+
+ 'unpadded name silently equivalent, which is the consumer tolerance the contract refuses, '
43+
+ 'and a bare string, a number or an empty list has no mapping that says what grouping was '
44+
+ 'meant. Metadata AT REST is left exactly as stored — `properties` on a page component is '
45+
+ 'not parsed on the save path, so a stored page keeps loading and renders as it does today; '
46+
+ 'the component-props gate reports such a value as an advisory `component-props-invalid` '
47+
+ 'finding at the offending path on `os validate`, `os build` and `os lint`, a padded name '
48+
+ 'with the received value and the unpadded name to write. ADR-0049 / ADR-0087.',
49+
acceptanceCriteria:
50+
'Every `object-grid` and `object-kanban` node in your pages either omits `grouping` or '
51+
+ 'carries `{ fields: [...] }` with at least one entry whose `field` is the unpadded stored '
52+
+ 'name. `os validate` reports no `component-props-invalid` finding under '
53+
+ '`properties.grouping` for these blocks. A well-formed grouping parses byte-identically to '
54+
+ 'before when `order` and `collapsed` are spelled out; a short entry `{ field }` parses clean '
55+
+ 'and gains the list view\'s defaults (`order: asc`, `collapsed: false`), which is how the '
56+
+ 'grid already read it. After the rewrite, a grid that showed one `(empty)` group shows one '
57+
+ 'group per value of that field, and a board that showed one swimlane shows one swimlane per value — '
58+
+ 'check that this is the grouping you meant.',
59+
};

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17882,6 +17882,61 @@ const step18: MigrationStep = {
1788217882
+ 'anything), and behaviour that seems to need one is a renderer capability request '
1788317883
+ 'against objectui, not a metadata key.',
1788417884
},
17885+
// The grouping prop on the two object blocks whose renderers read a grouping
17886+
// config. Typed by reference to the list view's own GroupingConfigSchema, so
17887+
// every door that carries a grouping judges it with the one schema. Deliberately
17888+
// NOT a D2 conversion: a padded field name is refused with guidance naming the
17889+
// field, never trimmed in silence.
17890+
{
17891+
id: 'ui-object-block-grouping-config-typed',
17892+
// No backticks in `surface` — build-upgrade-guide renders it inside a code
17893+
// span already, and a nested backtick would close it.
17894+
surface:
17895+
'the grouping property of the object-grid and object-kanban page blocks '
17896+
+ "(ComponentPropsMap['object-grid' | 'object-kanban'].grouping), which was z.unknown and "
17897+
+ 'therefore accepted any value: a padded field name such as '
17898+
+ "{ fields: [{ field: ' business_unit ' }] }, a number, a bare field-name string, an "
17899+
+ 'empty fields list, or keys the grouping config does not declare',
17900+
replacement:
17901+
'the grouping config a list view carries, `GroupingConfigSchema` from '
17902+
+ '`@objectstack/spec/ui`: `{ fields: [{ field, order?, collapsed? }, ...] }` with at least '
17903+
+ 'one entry, each `field` naming the record field exactly as it is stored, with no leading '
17904+
+ 'or trailing whitespace, `order` one of `asc` / `desc` and `collapsed` a boolean. A padded '
17905+
+ "name is rewritten unpadded (`' business_unit '` becomes `'business_unit'`); a bare "
17906+
+ "string `'business_unit'` becomes `{ fields: [{ field: 'business_unit' }] }`; an empty "
17907+
+ '`fields` list, a number, or any other value is deleted, since it never grouped anything. '
17908+
+ 'On `object-kanban`, `swimlaneField` still wins when both are authored, and deleting '
17909+
+ '`grouping` is the whole migration there when `swimlaneField` is set',
17910+
reason:
17911+
'Both blocks\' renderers read the list view\'s grouping shape and nothing else: the grid '
17912+
+ 'groups its rows by every `grouping.fields[i].field` (its server-side group header query '
17913+
+ 'and its row projection) and reads `order` and `collapsed` per level, and the kanban board '
17914+
+ 'takes `grouping.fields[0].field` as its swimlane field when no `swimlaneField` is '
17915+
+ 'authored, looking that raw name up on every card. The list view has refused a padded '
17916+
+ 'grouping field name since protocol 17.5, and a list view\'s `grouping` is a closed shape; '
17917+
+ 'these two doors declared the same prop as `z.unknown`, so the same value that list view '
17918+
+ 'refuses validated green here and rendered wrong with no error — the grid showed one '
17919+
+ '`(empty)` group holding every row, the board one swimlane holding every card. The prop is '
17920+
+ 'kept, not retired: the board\'s fallback is a live reader of the grouping config. '
17921+
+ 'The rewrite is left to the author on purpose: a trimming rule would make a padded and an '
17922+
+ 'unpadded name silently equivalent, which is the consumer tolerance the contract refuses, '
17923+
+ 'and a bare string, a number or an empty list has no mapping that says what grouping was '
17924+
+ 'meant. Metadata AT REST is left exactly as stored — `properties` on a page component is '
17925+
+ 'not parsed on the save path, so a stored page keeps loading and renders as it does today; '
17926+
+ 'the component-props gate reports such a value as an advisory `component-props-invalid` '
17927+
+ 'finding at the offending path on `os validate`, `os build` and `os lint`, a padded name '
17928+
+ 'with the received value and the unpadded name to write. ADR-0049 / ADR-0087.',
17929+
acceptanceCriteria:
17930+
'Every `object-grid` and `object-kanban` node in your pages either omits `grouping` or '
17931+
+ 'carries `{ fields: [...] }` with at least one entry whose `field` is the unpadded stored '
17932+
+ 'name. `os validate` reports no `component-props-invalid` finding under '
17933+
+ '`properties.grouping` for these blocks. A well-formed grouping parses byte-identically to '
17934+
+ 'before when `order` and `collapsed` are spelled out; a short entry `{ field }` parses clean '
17935+
+ 'and gains the list view\'s defaults (`order: asc`, `collapsed: false`), which is how the '
17936+
+ 'grid already read it. After the rewrite, a grid that showed one `(empty)` group shows one '
17937+
+ 'group per value of that field, and a board that showed one swimlane shows one swimlane per value — '
17938+
+ 'check that this is the grouping you meant.',
17939+
},
1788517940
{
1788617941
id: 'ui-object-grid-page-size-positive-integer-refused',
1788717942
surface: '`object-grid` page-component page sizes '

0 commit comments

Comments
 (0)