Skip to content

Commit 16eefc6

Browse files
docs(spec): the master-detail detail entry's sortField and amountField describes state each renderer path (#21315) (#21355)
Fixes #21315 Clause-②: no ## What changed An `object-master-detail-form` detail entry has two describes for keys the renderer can fill in when they are omitted: `sortField` and `amountField`. Both now state what happens on each of the renderer's paths. The reference page that lifts them (`content/docs/references/ui/component.mdx`) is regenerated by `check:generated --fix`. No schema accepts or refuses anything new. There is no shape, default or nullability change, and no objectui edit. The model is PR #21307, which did the same for `inlineMode` and `formFields`. `sortField`, before: > Child field holding the line sort position, stamped on drag-reorder (derived from a `position` / `sort_order` / … field when omitted) after: > Child field holding the line sort position, stamped on drag-reorder. When omitted it is the child object's first field named `position`, `sort_order`, `sequence`, `line_no`, `line_number` or `sort`, if it has one — except on an entry that names `relationshipField` and at least one column and gives every column a `type`. The renderer keeps that entry exactly as authored: nothing is derived, the grid stamps no line position, and a drag-reorder is not saved `amountField`, before: > Numeric child column summed for the running total after: > Numeric child column summed for the running total and the `totalField` rollup. When omitted it is picked from the grid's number and currency columns: a computed one, else one named `amount`, `total`, `subtotal`, `line_total`, `line_amount` or `net_amount`, else the last currency column, else the last numeric one — except on an entry that names `relationshipField` and at least one column and gives every column a `type`. The renderer keeps that entry exactly as authored, so nothing is picked. With no `amountField` authored or picked, the sums read a child column named `amount`, and the grid shows a running total only when `totalField` is set ## What the renderer does (objectui at the `.objectui-sha` pin `31971ff1e28f`, read with `git show`) `packages/plugin-form/src/MasterDetailForm.tsx`: - 927–929, `needsDerive`: an entry needs resolution when it has no `relationshipField`, no columns, or a column without a `type`. At 958, when no entry in the block needs it, every entry is kept as authored. - 965–967, the fast path: `relationshipField` is set and every column (at least one) has a `type`. The entry is returned unchanged (`{ ...entry, status: 'ready' }`). An omitted `sortField` stays undefined, and 862 passes `sort_field: d.sortField`, which is undefined. An omitted `amountField` also stays undefined. - 1032–1038: every other entry that loads its child schema goes through `deriveDetail`. Then `amountField = d.amountField ?? derived.amountField` and `sortField = d.sortField ?? derived.sortField`. - 1048–1055, the hydrate path: `relationshipField` is set and columns are present, but some have no `type`. The config becomes `{ ...d, columns: derived.columns, amountField, sortField }`. So `formFields` and `inlineMode` are kept as authored here, but `sortField` and `amountField` ARE derived. The comment at 1040–1047 gives the reason (objectui#11144). - 1056–1069, the derived path: everything is derived, with `amountField` and `sortField` at 1065–1066. `packages/plugin-form/src/deriveMasterDetail.ts`: - 540 with 55: the derived `sortField` is the first child field whose name is one of `position`, `sort_order`, `sequence`, `line_no`, `line_number` or `sort`. - 534 with 476–495: the derived `amountField` is `pickAmountField(columns)`. That is a computed number or currency column, else one named in `AMOUNT_LIKE_FIELDS`, else the last currency column, else the last numeric column. `packages/fields/src/widgets/GridField.tsx` 726–737: the grid stamps `row[sortField] = index` only when `sort_field` is set. Its comment reads "Without it, reorder is order-of-entry." When no `amountField` is authored or picked (`MasterDetailForm.tsx`): - 728: the document subtotal sums `e.config.amountField || 'amount'`. - 861: the grid's `total_field` is `d.amountField || (d.totalField ? 'amount' : undefined)`, unless the tax stack below it takes over. - 1549–1550: the `totalField` rollup writes `sumRows(rows, d.amountField || 'amount')`. So the path split is not #21284's. `formFields` and `inlineMode` are kept as authored on both the fast path and the hydrate path, which is an entry that names `relationshipField` and at least one column. `sortField` and `amountField` are kept as authored only on the fast path, where every column also has a `type`. Each describe states its own split. ## Why `amountField` changes too The triage direction put it in this edit. Its old text made no false claim, but it said nothing about omission, and omission changes the result by path. On the fast path nothing is picked, and the sums read a column named `amount`. From 1549–1550 (a reading, not a run): take a fully typed entry whose line-total column is `line_total`, with `totalField` set and `amountField` omitted. It writes 0 into the parent's `totalField`, because `sumRows` counts a missing value as 0. The derived path would have picked `line_total`. An author cannot know to write the key on the fast path unless the describe says so. ## Every member of `masterDetailDetailEntry()`, read for the same gap - `childObject`: required, and nothing derives it. Holds. - `relationshipField`: "auto-detected … when omitted". The fast and hydrate paths both need the key, so an entry without it always takes the derived path, and `findRelationshipField` (`deriveMasterDetail.ts` 141–154) runs there. That function takes the `master_detail` field that references the parent, else a `lookup`. Holds. - `columns`: "derived from the child object when omitted". The fast and hydrate paths both need columns, so an entry without them always reaches `deriveColumns`. A column given only its `name` has no `type`, so the entry is hydrated. Holds. - `formFields` and `inlineMode`: #21284's text, re-read at the pin. They are kept as authored at 967 and 1048–1055, and derived at 1063–1064. The offer conditions are at 847 and 850. Holds. - `amountField` and `sortField`: changed above. - `totalField`: nothing derives it, and its describe says nothing about omission. The sum it receives is the one the `amountField` describe now explains. Holds. - `title`, `minRows`, `maxRows` and `addLabel`: passed through or given a default label (755, 863–865). None of their describes claims an omission behaviour. Holds. - The factory's TSDoc calls `sortField` "the line-position field the grid stamps on drag-reorder" and claims no derivation. Holds, not edited. ## The `sortField` retirement waits on the pin bump objectui `main` retired the authored `sortField` at `0a3e5409f` (objectui#11376), after this pin. `git merge-base --is-ancestor 31971ff 0a3e540` exits 0, so the pin is in that commit's history and `0a3e5409f` is not in the pin's. At the pin, `sortField` is still read (85, 862, 1038). This PR does not retire the key. The `.objectui-sha` bump that crosses `0a3e5409f` owes the spec half: retire `sortField` from this entry, with a tombstone and an ADR-0087 entry, in the same landing. The `sortField` describe written here is what the current pin does until then. For `amountField`, objectui `main` (`d8edfe2576`) still returns the fast-path entry unchanged at 979, and still sums `d.amountField || 'amount'` (740, 1564). So, as of that commit, the bump owes no `amountField` change. ## Acceptance notes - A form view's `subforms[]` entry (`FormViewSchema.subforms` in `packages/spec/src/ui/view.zod.ts`) has the same `amountField` describe, "Numeric child column summed for the running total". At the pin, `ObjectForm.tsx` 340 and 394 send a form with `subforms` to `MasterDetailForm` as `details`, so the same omission behaviour applies there. That describe makes no false claim, so this is an observation, not a filed defect. This PR keeps to its card's surface. Carrier: none. ## Verification (every reading below is on HEAD `e0b23743d5`) - `pnpm --filter @objectstack/spec build`: `VERDICT command-exit 0`. - `pnpm --filter @objectstack/spec check:generated`: the first run reported `1 of 15 artifact(s) stale: content/docs/references/**`, and every other artifact was current, including the authorable surface and the JSON schemas. `--fix` regenerated that one and its re-check gave `✓ check:docs`. In the gate union it exits 0. - `pnpm --filter @objectstack/spec typecheck`: exit 0. - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2`: `Test Files 597 passed (597)`, `Tests 17487 passed | 1 todo (17488)`. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, run with no paths: 102 commands. The sorted list is identical to the dispatch's derivation at `97239c3c8a`. All were run, with exit codes recorded. `--ran` answers `102 derived famil(ies) accounted for — 101 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)`. - Six gates first exited 3 because their prerequisite packages were not built: `check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples`, `check:docs-transcript-drift`, `check:lean-entry-closure` and `check:dual-build-cjs-loads`. After building that closure (`turbo run build` for formula, lint, client-react and objectql, 34 tasks), the first five exit 0. - NOT MEASURED: `pnpm check:dual-build-cjs-loads`. It needs every workspace package's `dist` (hono, account, setup, studio, client and more), which needs a full-repo build. As a narrower check, all 19 `require` entry points of `@objectstack/spec` load. CI runs the full gate. - eslint, narrowed. Of the diff's three files, only `component.zod.ts` is in eslint's population: for the `.md` and `.mdx` files, eslint answers "File ignored because no matching configuration was supplied". `--format json` reports 0 errors and 0 warnings for that file. Its resolved config has `parserOptions` without `project`, so linting is not type-aware and this diff cannot change the verdict for any untouched file. The full `pnpm lint` runs in CI. - `main` was not merged in. Since the base `97239c3c8a`, `origin/main` has gained three commits, and none of them touches `packages/spec` or the reference page. --- _Generated by [Claude Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 23365ea commit 16eefc6

3 files changed

Lines changed: 16 additions & 4 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): an `object-master-detail-form` detail entry's `sortField` and `amountField` describes say what happens when the key is omitted on each of the renderer's paths (#21315)
6+
7+
Clause-②: no
8+
9+
- **Entry the renderer resolves** (any entry that does not name `relationshipField` together with at least one column whose every column has a `type`): an omitted `sortField` is the child object's first field named `position`, `sort_order`, `sequence`, `line_no`, `line_number` or `sort`, and an omitted `amountField` is picked from the grid's number and currency columns. This is unchanged. It includes an entry that names `relationshipField` and columns of which some have no `type`: the renderer keeps that entry's `formFields` and `inlineMode` as authored, but it still derives these two.
10+
- **Entry kept exactly as authored** (one that names `relationshipField` and at least one column, and gives every column a `type`): the renderer derives neither. An omitted `sortField` means the grid stamps no line position, so a drag-reorder is not saved. An omitted `amountField` means the sums read a child column named `amount`, and the grid shows a running total only when `totalField` is set.
11+
- The `sortField` describe used to say only "derived from a `position` / `sort_order` / … field when omitted", which does not hold for an entry kept exactly as authored. The `amountField` describe said nothing about omission.
12+
- No schema accepts or refuses anything new. Only the two describes and the reference page that lifts them change.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -978,8 +978,8 @@ Sort field and direction pair
978978
| **columns** | `{ name: string; label?: string; type?: Enum<'text' \| 'number' \| 'currency' \| 'date' \| 'datetime' \| 'time' \| 'select' \| 'lookup' \| 'file'>; width?: number; … }[]` | optional | Editable grid columns (derived from the child object when omitted). Each entry is the strict, name-keyed inline grid column a relationship field's `inlineColumns` takes (`{ name, label?, type?, … }` — objectui GridColumn); identity-only entries (`{ name }`) hydrate everything else from the child object's fields. Unknown keys and the retired `field` spelling are refused at parse. |
979979
| **formFields** | `string[]` | optional | Child field names for the per-row expand form. When omitted they are derived from the child object's fields — except on an entry that names both `relationshipField` and at least one column, which is kept as authored: nothing is derived, and the per-row form is offered only when `inlineMode` is 'form', where it draws the child object's full field list |
980980
| **inlineMode** | `Enum<'grid' \| 'form'>` | optional | Inline-edit form factor: 'grid' = editable cells; 'form' = read-only list + per-row full form. When omitted it is resolved from the relationship field's `inlineEdit`, else from the child object's shape — except on an entry that names both `relationshipField` and at least one column, which is kept as authored: nothing is resolved, the collection renders as a grid, and the per-row form is offered only when `formFields` lists more fields than `columns` |
981-
| **amountField** | `string` | optional | Numeric child column summed for the running total |
982-
| **sortField** | `string` | optional | Child field holding the line sort position, stamped on drag-reorder (derived from a `position` / `sort_order` / … field when omitted) |
981+
| **amountField** | `string` | optional | Numeric child column summed for the running total and the `totalField` rollup. When omitted it is picked from the grid's number and currency columns: a computed one, else one named `amount`, `total`, `subtotal`, `line_total`, `line_amount` or `net_amount`, else the last currency column, else the last numeric one — except on an entry that names `relationshipField` and at least one column and gives every column a `type`. The renderer keeps that entry exactly as authored, so nothing is picked. With no `amountField` authored or picked, the sums read a child column named `amount`, and the grid shows a running total only when `totalField` is set |
982+
| **sortField** | `string` | optional | Child field holding the line sort position, stamped on drag-reorder. When omitted it is the child object's first field named `position`, `sort_order`, `sequence`, `line_no`, `line_number` or `sort`, if it has one — except on an entry that names `relationshipField` and at least one column and gives every column a `type`. The renderer keeps that entry exactly as authored: nothing is derived, the grid stamps no line position, and a drag-reorder is not saved |
983983
| **totalField** | `string` | optional | Parent field to receive the rolled-up sum |
984984
| **title** | `string` | optional | Section title |
985985
| **minRows** | `number` | optional | Minimum number of rows |

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5033,8 +5033,8 @@ function masterDetailDetailEntry() {
50335033
columns: z.array(InlineGridColumnSchema).optional().describe("Editable grid columns (derived from the child object when omitted). Each entry is the strict, name-keyed inline grid column a relationship field's `inlineColumns` takes ({ name, label?, type?, … } — objectui GridColumn); identity-only entries ({ name }) hydrate everything else from the child object's fields. Unknown keys and the retired `field` spelling are refused at parse."),
50345034
formFields: z.array(z.string()).optional().describe("Child field names for the per-row expand form. When omitted they are derived from the child object's fields — except on an entry that names both `relationshipField` and at least one column, which is kept as authored: nothing is derived, and the per-row form is offered only when `inlineMode` is 'form', where it draws the child object's full field list"),
50355035
inlineMode: z.enum(['grid', 'form']).optional().describe("Inline-edit form factor: 'grid' = editable cells; 'form' = read-only list + per-row full form. When omitted it is resolved from the relationship field's `inlineEdit`, else from the child object's shape — except on an entry that names both `relationshipField` and at least one column, which is kept as authored: nothing is resolved, the collection renders as a grid, and the per-row form is offered only when `formFields` lists more fields than `columns`"),
5036-
amountField: z.string().optional().describe('Numeric child column summed for the running total'),
5037-
sortField: z.string().optional().describe('Child field holding the line sort position, stamped on drag-reorder (derived from a `position` / `sort_order` / … field when omitted)'),
5036+
amountField: z.string().optional().describe("Numeric child column summed for the running total and the `totalField` rollup. When omitted it is picked from the grid's number and currency columns: a computed one, else one named `amount`, `total`, `subtotal`, `line_total`, `line_amount` or `net_amount`, else the last currency column, else the last numeric one — except on an entry that names `relationshipField` and at least one column and gives every column a `type`. The renderer keeps that entry exactly as authored, so nothing is picked. With no `amountField` authored or picked, the sums read a child column named `amount`, and the grid shows a running total only when `totalField` is set"),
5037+
sortField: z.string().optional().describe("Child field holding the line sort position, stamped on drag-reorder. When omitted it is the child object's first field named `position`, `sort_order`, `sequence`, `line_no`, `line_number` or `sort`, if it has one — except on an entry that names `relationshipField` and at least one column and gives every column a `type`. The renderer keeps that entry exactly as authored: nothing is derived, the grid stamps no line position, and a drag-reorder is not saved"),
50385038
totalField: z.string().optional().describe('Parent field to receive the rolled-up sum'),
50395039
title: z.string().optional().describe('Section title'),
50405040
minRows: z.number().optional().describe('Minimum number of rows'),

0 commit comments

Comments
 (0)