Skip to content

Commit 1489952

Browse files
docs(spec): record:line_items columns hydrate from the child field, like the master-detail carriers (#24188)
Fixes #24140 Clause-②: no The spec half of route B from the triage grade (comment 6229394877): `record:line_items` `columns` now says that the block hydrates an identity-only column from the child object's field, as the master-detail carriers do. The heading of an identity-only column is then the child field's label, and it translates with that label. This change adds no key and changes no accept/reject result. The renderer half is objectstack-ai/objectui#12854 (its `Blocked-by:` names this card). ## What changed - **`packages/spec/src/ui/component.zod.ts`, `RecordLineItemsProps.columns` describe.** Before, it said "this block does NOT hydrate a column from the child object's field: declare `label`, `type` and `options` yourself". It now states the carriers' rule in the carriers' words: "identity-only entries ({ name }) hydrate everything else from the child object's fields, `label`, `type` and `options` included, so an identity-only column is headed by the child field's label and translates with it". It also states the opt-out in `InlineGridColumnSchema.type`'s words: "Declaring `type` opts a column out of hydration — supply its `label`, `options` and other extras yourself". The "Required, with at least one column" sentence is kept. - **The same file, `RecordLineItemsProps` TSDoc.** The paragraph that explained the old difference is rewritten. It now says that the column means what it means on the carriers, because `InlineGridColumnSchema.label`'s own describe governs every carrier. It names the renderer half (`LineItemsPanel` reusing `hydrateColumns`, not a second resolver). It also says that until the `.objectui-sha` pin carries that half, an identity-only column still draws as a text cell headed by its name. Last, it says that `defineStack`'s resolved-type check (`collectHydratedInlineColumnErrors`) walks only the three master-detail carriers. One phrase moved in the presence paragraph: "draws `columns` as given" became "draws only the authored `columns`". The column set is still never derived here. - **`content/docs/references/ui/component.mdx`.** Regenerated by `gen:docs`: one table row changed. `check:generated` named only this artifact as stale. - **`.changeset/24140-line-items-columns-hydrate.md`.** `@objectstack/spec` `patch`, because the describe text ships in `dist` and in the JSON Schema. `packages/spec/src/data/field.zod.ts` is untouched. `InlineGridColumnSchema.label` ("defaults to the child field's label via hydration") and `type` ("Declaring it opts the column out of schema hydration") were false for this one block. After this change they are true for every carrier, so they do not need to name `record:line_items`. ## Measured before editing (origin/main cb8756b) - **H1, the premise holds.** `component.zod.ts:2494` carried the "does NOT hydrate" describe, and `field.zod.ts:971`/`:972` carried the hydration describes. On the renderer side, objectui main `26323c4` `packages/plugin-form/src/LineItemsPanel.tsx` hands `applyColumnPermissions(schema.columns, …)` to the grid. A grep for `hydrate` in that file returns zero hits. Control: the same grep finds `hydrateColumns` in `deriveMasterDetail.ts`, `MasterDetailForm.tsx` and their tests. - **H2, the wording.** The carriers' describe (`inlineColumns`, and `columns` on an `object-master-detail-form` detail entry) is "identity-only entries ({ name }) hydrate everything else from the child object's fields". objectui `hydrateColumns` (`deriveMasterDetail.ts`, objectui `26323c4`) hydrates a column only when it declares no `type`, and it keeps an authored `label`. It fills `label`, `type`, `required`, `options`, the lookup target, the file props, `readonlyWhen`/`requiredWhen` and a computed `expr`. The new describe reuses the carriers' sentence and names the three keys the card names. It adds no narrower or wider rule. - **H3, the liveness ledger.** A case-insensitive grep for `line_items` in `packages/spec/liveness/` hits only `page.json:57` and `view.json:351`. In the `page.json` row `regions`, entry [15] cites `columns L#gridField (applyColumnPermissions)`. That is a read point, not a quote of the old describe. No row quotes "does NOT hydrate", so the ledger is unchanged. Control: `related_list` hits twice each in `field.json` and `page.json`. The `field.json` inline-column rows (`inlineColumns`, lines 410-423) cite `hydrateColumns`, which stays true. - **H5, other statements of the old behaviour.** Hand-written `content/docs/**` (outside `references/` and `releases/`) contains no sentence about line-items hydration. Positive control: the same grep finds `record:line_items` in `content/docs/ui/react-pages.mdx`, and `layout-dsl.mdx:788` states the carriers' rule for `object-master-detail-form`. `packages/lint` has no rule that prescribes or enforces a `label` on these columns. `validate-field-consumers.ts:392` says the block "derives nothing", which is about the column set and stays true. The `skills/**` and `examples/**` hits are listed under Acceptance notes and were not edited. ## H4: accept/reject unchanged, measured on the built package - `pnpm --filter @objectstack/spec build` ran at the base cb8756b and again at a66b471. The script compares every JSON Schema with `description` removed and lists the description paths that changed: - `ui/RecordLineItemsProps.json`: structure identical once descriptions are removed; one description path changed, `/properties/columns`. - `objectstack.json`: structure identical once descriptions are removed; one description path changed, `/$defs/ui/RecordLineItemsProps/properties/columns`. - Every other schema file is byte-identical. The only other file that differs is `.build-input-hash-schema`. - Control for the instrument: comparing `RecordHistoryProps.json` against `RecordLineItemsProps.json` reports structure `False`, so the comparison can fail. - `packages/spec/authorable-surface/` is byte-identical before and after the build. - `inline-grid-column-carriers.test.ts` passes unchanged, including the line-items section. Its test "CONTROL — defineStack does not judge an identity-only line-items column" still asserts that `build()` accepts `{ name: 'amount', scale: 2 }` over a currency child field on `record:line_items` and refuses the same column under `object-master-detail-form`. ## Tests (all at a66b471) - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 src/inline-grid-column-carriers.test.ts src/ui/component-props-unknown-members.pin.test.ts src/ui/page-print.test.ts`: `Test Files 3 passed (3)`, `Tests 138 passed (138)`. - `pnpm --filter @objectstack/spec test`: `Test Files 692 passed (692)`, `Tests 20974 passed | 1 todo (20975)`, exit 0. - `pnpm --filter @objectstack/spec typecheck`: exit 0. This covers `tsc --noEmit`, `check:scripts-typecheck` and `check:test-typecheck` (OK, the debt ledger held). - No ablation. No tested behaviour changed: the diff is describe text, TSDoc, a regenerated doc and a changeset. ## Gates (local subset, at a66b471) - Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 102 commands over 3 paths, measured against the merge base cb8756b. - 101 exit 0. Six of them first refused with exit 3 (PREREQUISITE NOT MET) and were re-run after the prerequisite was met: - The closure build `pnpm --workspace-concurrency=2 --filter '@objectstack/lint...' --filter '@objectstack/client-react...' --filter '@objectstack/objectql...' --filter '@objectstack/formula...' run build` unblocked `check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples`, `check:docs-transcript-drift` and `check:lean-entry-closure`. - A one-commit fetch of the self-test fixture `621a4876` unblocked `check-plugin-teardown-shape --self-test`. - 1 NOT MEASURED: `pnpm check:dual-build-cjs-loads` needs every workspace package built. CI runs it in `Lint & Repo Gates`. - `--ran` reconciliation: "102 derived famil(ies) accounted for — 101 run, 1 NOT-MEASURED". - `pnpm --filter @objectstack/spec check:generated` reported `content/docs/references/**` stale before regeneration, and only that artifact. ## Acceptance notes 1. **`defineStack`'s resolved-type check does not reach `record:line_items`.** `collectHydratedInlineColumnErrors` (`packages/spec/src/stack.zod.ts`) judges "inline grid columns whose TYPE comes from the child field" on three carriers. Once the renderer half lands, an identity-only `record:line_items` column takes its type from the child field too. A `{ name: 'amount', scale: 2 }` column over a `currency` field will then publish green on this block and be reported only at render time, while `object-master-detail-form` refuses the same column at publish. Bringing the block into that check changes what publishes, so it is out of this Clause-② `no` card. It is reported for a separate card that depends on objectstack-ai/objectui#12854. The carriers test's CONTROL title ("the panel draws it unhydrated") describes the pinned renderer, and it flips with that card. 2. **The transition window.** Until objectstack-ai/objectui#12854 lands and the `.objectui-sha` pin carries it, an identity-only `record:line_items` column still draws as a text cell headed by its field name. The TSDoc and the changeset both say this. The pin bump that carries objectui#12854 should delete the TSDoc sentence that begins "Until the `.objectui-sha` pin carries it". 3. **Showcase invoice page (held off).** The comment above the columns in `examples/app-showcase/src/ui/pages/invoice-document.page.ts` (around lines 59-66) still states the old behaviour, and so does the docblock of `examples/app-showcase/test/invoice-document-line-items.test.ts` (lines 12-15). These belong to the showcase invoice card (#23985): its PR decides whether to drop the literal labels. 4. **`skills/objectstack-ui/rules/pages.md:112-113` (Tier H, not edited).** The print-page example writes identity-only `record:line_items` columns (`{ name: 'product' }`, …). That matches the new contract. Before objectui#12854 lands, those columns print raw field names. 5. **`main` moved while this was open.** `main` gained #24170 (the `record:json` retirement), which edits `component.zod.ts` at `:458` and `:2571` and removes the `record:json` section of `component.mdx` (lines 1793-1805). Neither touches this diff's hunks, so `main` was not merged in, per the dispatch's merge-on-conflict-only instruction. The PR's CI and the merge queue validate the merge. --- _Generated by [Claude Code](https://claude.ai/code/session_01TyRhWbVbbQpQJTDbW8VahJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7d6ec91 commit 1489952

3 files changed

Lines changed: 33 additions & 11 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): `record:line_items` `columns` states that an identity-only column hydrates from the child object's field, as on the master-detail carriers, so its heading is the child field's label and translates with it (#24140)
6+
7+
Clause-②: no
8+
9+
`RecordLineItemsProps.columns` said that this block does not hydrate a column from the child object's field, so authors had to declare `label`, `type` and `options` on every column, and an identity-only `{ name }` column was headed by its field name. That contradicted the shared column contract the block takes by reference: `InlineGridColumnSchema.label` says it "defaults to the child field's label via hydration". It also left a declared column heading as a plain string that no translation bundle key reaches, so a non-English print of a line-items table read English headings. The describe now states the carriers' rule in the carriers' words. Identity-only entries (`{ name }`) hydrate everything else from the child object's fields, `label`, `type` and `options` included, so the heading is the child field's label, which translates through that field's own translation. Declaring `type` opts a column out of hydration.
10+
11+
Text only: every input that parsed before parses now, and every refusal is unchanged. The renderer half is objectui's `LineItemsPanel`, which will reuse the same column hydration the master-detail grid uses. Until the console carries it, an identity-only `record:line_items` column still draws as a text cell headed by its field name, so keep explicit labels on columns you need headed today.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1800,7 +1800,7 @@ Type: `string`
18001800
| :--- | :--- | :--- | :--- |
18011801
| **childObject** | `string` | optional | Child object whose records this grid lists, edits and saves. Optional because the component-level `dataSource` binding can supply the object instead; with neither, the panel shows a configuration hint and fetches nothing |
18021802
| **relationshipField** | `string` | ✅ | FK on the child object pointing back to the parent record — the rows are queried as `{ [relationshipField]: parentId }`. Required: this panel does not auto-detect it |
1803-
| **columns** | `{ name: string; label?: string; type?: Enum<'text' \| 'number' \| 'currency' \| 'date' \| 'datetime' \| 'time' \| 'select' \| 'lookup' \| 'file'>; width?: number; … }[]` | ✅ | Editable grid columns, drawn exactly as declared. Each entry is the strict, name-keyed inline grid column a relationship field's `inlineColumns` takes (`{ name, label?, type?, options?, … }` — objectui GridColumn); unknown keys and the retired `field` spelling are refused. Unlike the master-detail carriers, this block does NOT hydrate a column from the child object's field: declare `label`, `type` and `options` yourself — an identity-only `{ name }` column is a text cell headed by its name. Required, with at least one column: nothing derives them, and an empty list draws a grid with no cells |
1803+
| **columns** | `{ name: string; label?: string; type?: Enum<'text' \| 'number' \| 'currency' \| 'date' \| 'datetime' \| 'time' \| 'select' \| 'lookup' \| 'file'>; width?: number; … }[]` | ✅ | Editable grid columns. Each entry is the strict, name-keyed inline grid column a relationship field's `inlineColumns` takes (`{ name, label?, type?, options?, … }` — objectui GridColumn); unknown keys and the retired `field` spelling are refused. As on the master-detail carriers, identity-only entries (`{ name }`) hydrate everything else from the child object's fields, `label`, `type` and `options` included, so an identity-only column is headed by the child field's label and translates with it. Declaring `type` opts a column out of hydration — supply its `label`, `options` and other extras yourself. Required, with at least one column: nothing derives them, and an empty list draws a grid with no cells |
18041804
| **parentObject** | `string` | optional | Parent object the `totalField` rollup is written to (default: the object of the record on the page) |
18051805
| **parentId** | `string` | optional | Parent record id the rows belong to (default: the record on the page). Wins over `recordId` |
18061806
| **recordId** | `string` | optional | Alternate spelling of `parentId` the renderer also reads; `parentId` wins when both are set |

‎packages/spec/src/ui/component.zod.ts‎

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2430,22 +2430,33 @@ const RECORD_LINE_ITEMS_HISTORY =
24302430
* `object-master-detail-form` detail entry's `columns` take, not a copy: the
24312431
* panel hands its columns to the same objectui grid (`GridField`, whose
24322432
* `GridColumn` declares exactly that schema's twenty keys and IS the spec's
2433-
* type by reference). One difference is the
2434-
* carrier's, not the column's, and the `describe()` says it: this panel does
2435-
* NOT hydrate a column from the child object's field — it passes `columns`
2436-
* straight through the field-security pass to the grid — so a column draws exactly what it declares, and an identity-only `{ name }`
2437-
* column is a text cell headed by its name. For the same reason
2438-
* `defineStack`'s identity-only check (`collectHydratedInlineColumnErrors` in
2439-
* `stack.zod.ts`) does not reach this block: there is no hydrated type to
2440-
* judge.
2433+
* type by reference). The column also means here what it means on those
2434+
* carriers (#24140, route B), because the column schema's own `label` describe
2435+
* — "defaults to the child field's label via hydration" — governs every
2436+
* carrier: a column that declares no `type` hydrates from the child object's
2437+
* field through objectui's `hydrateColumns`, so an identity-only `{ name }`
2438+
* column takes the child field's label (and with it that label's
2439+
* translation), its cell type and its options; declaring a `type` opts the
2440+
* column out. The renderer half is objectstack-ai/objectui#12854:
2441+
* `LineItemsPanel` reuses `hydrateColumns`, not a second resolver. Until the
2442+
* `.objectui-sha` pin carries it, the panel hands `columns` to the grid
2443+
* through the field-security pass alone, and an identity-only column draws as
2444+
* a text cell headed by its name.
2445+
*
2446+
* `defineStack`'s resolved-type check (`collectHydratedInlineColumnErrors` in
2447+
* `stack.zod.ts`) walks the three master-detail carriers and not this block,
2448+
* so an identity-only column here that hydrates to a type carrying a
2449+
* type-conditional column rule (today `currency`, which refuses `scale`) is
2450+
* not judged at publish. Closing that gap changes what publishes, so it is a
2451+
* change of its own, not part of this block's describe.
24412452
*
24422453
* The keys this block shares with an `object-master-detail-form` detail entry
24432454
* take that entry's types and alias table, so one concept is spelled one way
24442455
* on every child-collection surface. Three of them differ in presence for a
24452456
* measured reason: `relationshipField` and `columns` are required here because
24462457
* nothing derives them (the entry auto-detects the FK and derives the
24472458
* columns; this panel queries `{ [relationshipField]: parentId }` and draws
2448-
* `columns` as given), and `childObject` is optional because the
2459+
* only the authored `columns`), and `childObject` is optional because the
24492460
* component-level `dataSource` binding can supply it instead. `title` is a
24502461
* plain string because the panel draws it as a React child.
24512462
*/
@@ -2492,7 +2503,7 @@ export const RecordLineItemsProps = lazySchema(() => strictObject({
24922503
relationshipField: z.string()
24932504
.describe("FK on the child object pointing back to the parent record — the rows are queried as `{ [relationshipField]: parentId }`. Required: this panel does not auto-detect it"),
24942505
columns: z.array(InlineGridColumnSchema).min(1)
2495-
.describe("Editable grid columns, drawn exactly as declared. Each entry is the strict, name-keyed inline grid column a relationship field's `inlineColumns` takes ({ name, label?, type?, options?, … } — objectui GridColumn); unknown keys and the retired `field` spelling are refused. Unlike the master-detail carriers, this block does NOT hydrate a column from the child object's field: declare `label`, `type` and `options` yourself — an identity-only `{ name }` column is a text cell headed by its name. Required, with at least one column: nothing derives them, and an empty list draws a grid with no cells"),
2506+
.describe("Editable grid columns. Each entry is the strict, name-keyed inline grid column a relationship field's `inlineColumns` takes ({ name, label?, type?, options?, … } — objectui GridColumn); unknown keys and the retired `field` spelling are refused. As on the master-detail carriers, identity-only entries ({ name }) hydrate everything else from the child object's fields, `label`, `type` and `options` included, so an identity-only column is headed by the child field's label and translates with it. Declaring `type` opts a column out of hydration — supply its `label`, `options` and other extras yourself. Required, with at least one column: nothing derives them, and an empty list draws a grid with no cells"),
24962507
parentObject: z.string().optional()
24972508
.describe("Parent object the `totalField` rollup is written to (default: the object of the record on the page)"),
24982509
parentId: z.string().optional()

0 commit comments

Comments
 (0)