From 05d9edc20b83f86b0729804a5a0b6b6458570c29 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 15:21:24 +0000 Subject: [PATCH 1/2] docs(spec): record:line_items columns hydrate like the master-detail carriers RecordLineItemsProps.columns said the block does not hydrate a column from the child object's field. The shared InlineGridColumnSchema it takes by reference says a column's label defaults to the child field's label via hydration, and the master-detail carriers honour that. The describe now states the carriers' rule in their words: identity-only entries hydrate label, type and options from the child field, so the heading translates with the child field's label; declaring type opts out. The TSDoc names the renderer half (objectui LineItemsPanel reusing hydrateColumns) and the defineStack resolved-type check that still walks only the three master-detail carriers. Describe text only: no key, no accept/reject change. Claude-Session: https://claude.ai/code/session_01TyRhWbVbbQpQJTDbW8VahJ Co-authored-by: Claude --- .../24140-line-items-columns-hydrate.md | 11 +++++++ packages/spec/src/ui/component.zod.ts | 31 +++++++++++++------ 2 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 .changeset/24140-line-items-columns-hydrate.md diff --git a/.changeset/24140-line-items-columns-hydrate.md b/.changeset/24140-line-items-columns-hydrate.md new file mode 100644 index 00000000000..43078363878 --- /dev/null +++ b/.changeset/24140-line-items-columns-hydrate.md @@ -0,0 +1,11 @@ +--- +'@objectstack/spec': patch +--- + +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) + +Clause-②: no + +`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. + +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. diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 0be4166f059..c809bacee26 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -2429,14 +2429,25 @@ const RECORD_LINE_ITEMS_HISTORY = * `object-master-detail-form` detail entry's `columns` take, not a copy: the * panel hands its columns to the same objectui grid (`GridField`, whose * `GridColumn` declares exactly that schema's twenty keys and IS the spec's - * type by reference). One difference is the - * carrier's, not the column's, and the `describe()` says it: this panel does - * NOT hydrate a column from the child object's field — it passes `columns` - * straight through the field-security pass to the grid — so a column draws exactly what it declares, and an identity-only `{ name }` - * column is a text cell headed by its name. For the same reason - * `defineStack`'s identity-only check (`collectHydratedInlineColumnErrors` in - * `stack.zod.ts`) does not reach this block: there is no hydrated type to - * judge. + * type by reference). The column also means here what it means on those + * carriers (#24140, route B), because the column schema's own `label` describe + * — "defaults to the child field's label via hydration" — governs every + * carrier: a column that declares no `type` hydrates from the child object's + * field through objectui's `hydrateColumns`, so an identity-only `{ name }` + * column takes the child field's label (and with it that label's + * translation), its cell type and its options; declaring a `type` opts the + * column out. The renderer half is objectstack-ai/objectui#12854: + * `LineItemsPanel` reuses `hydrateColumns`, not a second resolver. Until the + * `.objectui-sha` pin carries it, the panel hands `columns` to the grid + * through the field-security pass alone, and an identity-only column draws as + * a text cell headed by its name. + * + * `defineStack`'s resolved-type check (`collectHydratedInlineColumnErrors` in + * `stack.zod.ts`) walks the three master-detail carriers and not this block, + * so an identity-only column here that hydrates to a type carrying a + * type-conditional column rule (today `currency`, which refuses `scale`) is + * not judged at publish. Closing that gap changes what publishes, so it is a + * change of its own, not part of this block's describe. * * The keys this block shares with an `object-master-detail-form` detail entry * take that entry's types and alias table, so one concept is spelled one way @@ -2444,7 +2455,7 @@ const RECORD_LINE_ITEMS_HISTORY = * measured reason: `relationshipField` and `columns` are required here because * nothing derives them (the entry auto-detects the FK and derives the * columns; this panel queries `{ [relationshipField]: parentId }` and draws - * `columns` as given), and `childObject` is optional because the + * only the authored `columns`), and `childObject` is optional because the * component-level `dataSource` binding can supply it instead. `title` is a * plain string because the panel draws it as a React child. */ @@ -2491,7 +2502,7 @@ export const RecordLineItemsProps = lazySchema(() => strictObject({ relationshipField: z.string() .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"), columns: z.array(InlineGridColumnSchema).min(1) - .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"), + .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"), parentObject: z.string().optional() .describe("Parent object the `totalField` rollup is written to (default: the object of the record on the page)"), parentId: z.string().optional() From a66b47182e19561578a63d0c3c4c7955dabca68d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 15:27:08 +0000 Subject: [PATCH 2/2] docs(spec): regenerate the component reference for the line-items columns describe Claude-Session: https://claude.ai/code/session_01TyRhWbVbbQpQJTDbW8VahJ Co-authored-by: Claude --- content/docs/references/ui/component.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index d60241b6179..aa36193cc7c 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -1813,7 +1813,7 @@ Type: `string` | :--- | :--- | :--- | :--- | | **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 | | **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 | -| **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 | +| **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 | | **parentObject** | `string` | optional | Parent object the `totalField` rollup is written to (default: the object of the record on the page) | | **parentId** | `string` | optional | Parent record id the rows belong to (default: the record on the page). Wins over `recordId` | | **recordId** | `string` | optional | Alternate spelling of `parentId` the renderer also reads; `parentId` wins when both are set |