Skip to content

Commit 7526058

Browse files
docs(spec): the master-detail detail entry's inlineMode and formFields describes state both renderer paths (#21284) (#21307)
Fixes #21284 Clause-②: no ## What changed An `object-master-detail-form` detail entry has two describes that say what an omitted key means: `inlineMode` and `formFields`. Both now state the renderer's two 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 edit to objectui or `packages/lint`. `inlineMode`, before: > Inline-edit form factor: 'grid' = editable cells; 'form' = read-only list + per-row full form. Resolved from the relationship field's `inlineEdit` when omitted after: > 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` `formFields`, before: > Child field names for the per-row expand form (derived from the child object's editable fields when omitted) after: > 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 ## 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 an untyped column. - 967, the fast path: `relationshipField` set and every column typed. The entry is returned unchanged (`{ ...entry, status: 'ready' }`). - 1048–1055, the hydrate path: `relationshipField` set and `columns` non-empty. The config becomes `{ ...d, columns: derived.columns, amountField, sortField }`, so `formFields` and `inlineMode` stay as authored. - 1056–1069, the derived path: `formFields: d.formFields ?? derived.formFields` (1063) and `inlineMode: d.inlineMode ?? derived.mode` (1064). `derived.mode` comes from `resolveInlineMode` (`deriveMasterDetail.ts` 536–539 and 456–470). It is the relationship field's `inlineEdit` when that is `grid` or `form`, and otherwise a default chosen from the child object's shape. - 847: the per-row expand control is offered when `d.inlineMode === 'form'`, or when `formFields` is longer than `columns`. At 850, `displayMode` is `list` only for `form`; otherwise it is `grid`. - 1821: the row form receives `fields` only when `formFields` is non-empty. Without them, `ObjectForm` (`ObjectForm.tsx` 961) draws `Object.keys(objectSchema.fields)`, which is every field the child object declares. So, for the question the card left open about `formFields` on the kept-as-authored path: when it is omitted, nothing is derived. A grid never offers the per-row form, because zero fields is never more than the column count. The form is offered only when `inlineMode` is `form`, and it then draws the child object's full field list. The old text was false there, so this PR rewords it. It also drops "editable": the derived list keeps `readonly` fields (`deriveFormFields` at the pin, 404–420, and `deriveInlineRowFormFields` in `packages/spec/src/data/inline-grid-columns.ts`). The wording matches what the spec already says about both paths. "Kept as authored" means an entry that names both `relationshipField` and at least one column, as in the `creditAuthoredRowForm` docblock in `packages/lint/src/validate-field-consumers.ts`. The offer condition is `isInlineRowFormOffered` in `@objectstack/spec/data`. ## A bounded in-place fix, declared The `masterDetailDetailEntry` factory's TSDoc (`component.zod.ts`, the paragraph above the factory) said `inlineMode`'s "absence takes the relationship's own resolution". That is the same claim, true on only one path, and it ships in the tarball under `src/**/*.zod.ts`. It now says this holds only on an entry the renderer derives, and points to the describe. This is outside the claim's declared file surface, which names the two describes. It is made under the bounded in-place rule: it is the same defect, this card's ruling fixes its form, no other claim holds that region of the file, and it adds no new gate. It is a comment-only change of one added line. ## Released surface The card says the sentence "is not yet in a release". Measured today, `@objectstack/spec@17.6.0` is published as `latest`, and its tarball carries the sentence once each in `src/ui/component.zod.ts` and `dist/ui/index.js`. So 17.6.0 already ships it. This `patch` corrects it in the next release. ## Acceptance notes - `sortField`'s describe, "(derived from a `position` / `sort_order` / … field when omitted)", is false on the fast path. At 967, an entry with `relationshipField` and every column typed is returned as authored, so an omitted `sortField` is not derived. The hydrate path at 1037–1051 does derive it, since objectui#11144. This PR does not touch it: it is outside this card's surface, and someone must decide whether the renderer's fast path or the describe changes. It is reported to the seat. ## Verification (every reading below is on HEAD `e21f622b65`) - `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, the authorable surface and JSON schemas included. `--fix` regenerated that one, and its re-check gave `✓ check:docs`. Run again in the gate union: exit 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 17484 passed | 1 todo`. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, run with no paths: 102 commands, the same set as the dispatch's derivation. All were run, and `--ran` answers `102 derived famil(ies) accounted for — 101 run, 1 NOT-MEASURED`. - Five 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` and `check:lean-entry-closure`. After building that closure (`turbo run build` for formula, lint, client-react and objectql: 34 tasks), all five exit 0. - NOT MEASURED: `pnpm check:dual-build-cjs-loads`. It needs every workspace package's `dist`, 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 1 file, 0 errors and 0 warnings. That file's 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. --- _Generated by [Claude Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent dabd1c5 commit 7526058

3 files changed

Lines changed: 18 additions & 5 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 `inlineMode` and `formFields` describes say what happens when the key is omitted on both of the renderer's paths (#21284)
6+
7+
Clause-②: no
8+
9+
- **Derived entry** (any entry that does not name both `relationshipField` and at least one column): an omitted `inlineMode` is resolved from the relationship field's `inlineEdit`, else from the child object's shape, and an omitted `formFields` is derived from the child object's fields. This is unchanged.
10+
- **Entry kept as authored** (one that names both `relationshipField` and at least one column): the renderer resolves and derives nothing. An omitted `inlineMode` renders the collection as a grid, which offers the per-row form only when `formFields` lists more fields than `columns`. An omitted `formFields` means the per-row form is offered only when `inlineMode` is `form`, and it then draws the child object's full field list.
11+
- The `inlineMode` describe used to say only "resolved from the relationship field's `inlineEdit` when omitted", and the `formFields` describe only "derived from the child object's editable fields when omitted". Neither holds for an entry kept as authored. The `formFields` describe also no longer says "editable": the derived list keeps `readonly` fields, as `deriveInlineRowFormFields` (`@objectstack/spec/data`) does.
12+
- No schema accepts or refuses anything new. Only the two describes, the reference page that lifts them, and one source comment change.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -976,8 +976,8 @@ Sort field and direction pair
976976
| **childObject** | `string` | ✅ | Child object whose records are entered inline |
977977
| **relationshipField** | `string` | optional | FK on the child pointing back to the parent (auto-detected from the child's master_detail/lookup field when omitted) |
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. |
979-
| **formFields** | `string[]` | optional | Child field names for the per-row expand form (derived from the child object's editable fields when omitted) |
980-
| **inlineMode** | `Enum<'grid' \| 'form'>` | optional | Inline-edit form factor: 'grid' = editable cells; 'form' = read-only list + per-row full form. Resolved from the relationship field's `inlineEdit` when omitted |
979+
| **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 |
980+
| **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` |
981981
| **amountField** | `string` | optional | Numeric child column summed for the running total |
982982
| **sortField** | `string` | optional | Child field holding the line sort position, stamped on drag-reorder (derived from a `position` / `sort_order` / … field when omitted) |
983983
| **totalField** | `string` | optional | Parent field to receive the rolled-up sum |

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5006,7 +5006,8 @@ const MASTER_DETAIL_DETAIL_HISTORY =
50065006
* child-collection surfaces. The three it adds are the renderer's own:
50075007
* `formFields` (the per-row expand form), `inlineMode` (the two form factors
50085008
* a relationship field's `inlineEdit` names; absence takes the relationship's
5009-
* own resolution) and `sortField` (the line-position field the grid stamps on
5009+
* own resolution only on an entry the renderer derives, and its describe
5010+
* states both paths) and `sortField` (the line-position field the grid stamps on
50105011
* drag-reorder). `title` and `addLabel` are plain strings because the
50115012
* renderer draws them as a React child and a button label without resolving a
50125013
* locale map.
@@ -5030,8 +5031,8 @@ function masterDetailDetailEntry() {
50305031
childObject: z.string().describe('Child object whose records are entered inline'),
50315032
relationshipField: z.string().optional().describe('FK on the child pointing back to the parent (auto-detected from the child\'s master_detail/lookup field when omitted)'),
50325033
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."),
5033-
formFields: z.array(z.string()).optional().describe("Child field names for the per-row expand form (derived from the child object's editable fields when omitted)"),
5034-
inlineMode: z.enum(['grid', 'form']).optional().describe("Inline-edit form factor: 'grid' = editable cells; 'form' = read-only list + per-row full form. Resolved from the relationship field's `inlineEdit` when omitted"),
5034+
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"),
5035+
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`"),
50355036
amountField: z.string().optional().describe('Numeric child column summed for the running total'),
50365037
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)'),
50375038
totalField: z.string().optional().describe('Parent field to receive the rolled-up sum'),

0 commit comments

Comments
 (0)