Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

Commit edfcf5a

Browse files
feat(types): a form field of type grid declares the grid widget's field-level keys (objectui#11070 round 10) (#11381)
Refs #11070 Clause-②: yes (widening). `FormFieldSchema`, a published accept set, accepts eight more keys on a form field entry, each one a key the `grid` widget already reads and `grid.mdx` teaches. The tolerant face now also judges their values, so a wrong-typed value it used to strip is refused. Both halves are priced in the changeset. Round 10 of objectui#11070, under claim `5930921428` (`domain:devx` seat 2, maintainer direct dispatch R5). It has three items: - (i) the grid widget's field-level keys on the form-field face, the seat's answer A in `5928627070`; - (ii) one real run of objectui's strict authoring face over objectstack `examples/app-crm`, for objectstack-ai/objectstack#20960's acceptance item 3 (measure only, nothing changed for it); - (iii) `grid.mdx`'s "Integration with Advanced Grid" section (finding 2 of `5929940616`). ## (i) The form-field face mirror ### Measured first, at base `0a3e5409` - `GridFieldMetadata` declares, beyond `BaseFieldMetadata`, `columns` (round 7) and exactly the eight keys in the table below. - Neither form-field face declared any of the eight. `FormField` resolved each one through its `[key: string]: any` index signature. `FormFieldSchema`, a plain `z.object`, stripped each one. - The face is flat, not discriminated by `type`. `FormField` is one interface with `type?: string`, and `FormFieldSchema` is one `z.object(...).superRefine(...)`. Round 7 added `columns` to it the same way. - No collision: - no `FormField` member has any of the eight names; - `@objectstack/spec` 17.5.0's `FieldSchema` declares none of them, so no spec change is involved; - in `@object-ui/fields`, `GridField` is the only reader. `MasterDetailForm` and `LineItemsPanel` write the same keys into the grid's config with the same meaning, and the `form` renderer reads none of them. - Reach through the public door, by a scratch render probe (never committed) of the new fixture through `SchemaRenderer`, the real `form` renderer and `GridField`: - with the keys, the Add button reads "Add line item" and the footer shows the total 109.97; - the control, the same fixture without the keys, reads "Add line" and shows no total; - with `allow_add: false`, there is no Add button and no Duplicate action. ### Per key, both faces, before and after | key | `GridFieldMetadata` | `FormField`: base → head | `FormFieldSchema`: base → head | strict face, `fields[]` grid entry: base → head | tolerant face: base → head | | --- | --- | --- | --- | --- | --- | | `min_rows` | `number` | index signature (`any`) → `GridFieldMetadata['min_rows']` | undeclared → `z.number().optional()` | refused by name at `fields.0.min_rows` → accepted | value stripped → value kept; `"1"` refused | | `max_rows` | `number` | index signature (`any`) → `GridFieldMetadata['max_rows']` | undeclared → `z.number().optional()` | refused by name at `fields.0.max_rows` → accepted | value stripped → value kept; `"20"` refused | | `allow_add` | `boolean` | index signature (`any`) → `GridFieldMetadata['allow_add']` | undeclared → `z.boolean().optional()` | refused by name at `fields.0.allow_add` → accepted | value stripped → value kept; `"false"` refused | | `allow_delete` | `boolean` | index signature (`any`) → `GridFieldMetadata['allow_delete']` | undeclared → `z.boolean().optional()` | refused by name at `fields.0.allow_delete` → accepted | value stripped → value kept; `"false"` refused | | `allow_reorder` | `boolean` | index signature (`any`) → `GridFieldMetadata['allow_reorder']` | undeclared → `z.boolean().optional()` | refused by name at `fields.0.allow_reorder` → accepted | value stripped → value kept; `"false"` refused | | `total_field` | `string` | index signature (`any`) → `GridFieldMetadata['total_field']` | undeclared → `z.string().optional()` | refused by name at `fields.0.total_field` → accepted | value stripped → value kept; `3` refused | | `add_label` | `string` | index signature (`any`) → `GridFieldMetadata['add_label']` | undeclared → `z.string().optional()` | refused by name at `fields.0.add_label` → accepted | value stripped → value kept; `1` refused | | `sort_field` | `string` | index signature (`any`) → `GridFieldMetadata['sort_field']` | undeclared → `z.string().optional()` | refused by name at `fields.0.sort_field` → accepted | value stripped → value kept; `0` refused | The base column was read by a scratch probe on the built `@object-ui/types` dist at `0a3e5409`, with a lit control: the same entry without the keys parses on both faces. Each `.describe()` on the zod side says that only the `grid` widget reads the key. ### Pins - `strict-face-read-keys-11070.test.ts`: - block 1 gains one `fields[]` grid-entry row per key, each with a misspelled sibling still refused by name, plus an all-eight row whose tolerant output keeps every value; - block 2 gains one wrong-type row per key, refused on both faces; - block 4 gains `assertionGridFieldKeysOnTheFormFieldFace`. Every member `GridFieldMetadata` declares beyond `BaseFieldMetadata` must be a DECLARED `FormField` member (not the index signature) with the same type, so a key added to the grid's type and left unmirrored fails to compile. A lit control shows the key set it ranges over is non-empty. - `form-field-zod-coverage.test.ts`: the eight keys join `DECLARED_KEYS`. - `zod-mirror-parity.test.ts`: `FormFieldSchema` was already a registered pair. The eight keys are LOCAL on both sides, so no ledger total moves. Its `SPEC_DERIVED_PAIRS` note now says so. ### The refusal flip and the ablations Predictions were written before each run. Every mutation was committed first and restored to HEAD, with blob == HEAD and `git diff HEAD` empty. A1 and A2 went through objectstack's `scripts/ablation-replace.mjs` (anchor 1 → 0, blob changed). The subjects resolve from source, since the tests import `../zod` relatively, so no dist is on the path. - **R0, the refusal flip:** `form.zod.ts` and `form.ts` were set to base `0a3e5409`, and this head's pins were run against them. - Predicted: 18 red in vitest (8 per-key rows, the all-eight row, 8 wrong-type rows, the coverage key set) and tsc red only in the pin file. - Measured: `Tests 18 failed | 68 passed (86)`, and `tsc -p tsconfig.test.json` gave 10 errors, all in `assertionGridFieldKeysOnTheFormFieldFace` (completeness, the mapped equality, the 8 per-key rows). `zod-mirror-parity` compiled. - **A1, zod ablation:** the `allow_add` member was deleted from `FormFieldSchema`. - Predicted: 4 red (the `allow_add` row, the all-eight row, the wrong-type `allow_add` row, the coverage key set) and the parity file red at tsc. - Measured: `Tests 4 failed | 119 passed (123)` over the pin, coverage and parity files. tsc gave 1 error, in `zod-mirror-parity.test.ts` at `assertionUnmirroredMatchesLedger` (`"form.zod.ts#FormFieldSchema"` is not assignable to `never`). - **A2, TS ablation:** the `allow_add` member was deleted from `FormField`. - Predicted: vitest green, the type-level pin red, and the parity file red on the fourth direction. - Measured: `Tests 123 passed (123)`. tsc gave 5 errors: the pin file's completeness, mapped-equality and per-key `allow_add` rows, and `zod-mirror-parity.test.ts`'s `MirroredUndeclared` reconciliation, by pair and by key (`"allow_add"` is not assignable to `never`). ### Fixture and docs - `examples/schema-catalog/src/schemas/fields-grid/line-items-grid.json` writes all eight keys, with columns and seeded rows, so the corpus exercises the declared set. The catalog index is regenerated. - The three catalog DOM-leak census pins count the new entry, as their own text asks of an added example: `grid` goes from 26 to 27 in all three, and in the layout pin `text` goes from 698 to 699 rendered and from 154 to 155 with no element. - `grid.mdx` embeds the fixture and says that the keys are accepted on a form field entry of `type: 'grid'`, and that only the `grid` widget reads them. ## (ii) The app-crm strict-face run (measure only) **The instrument, and why it is the real one.** The run uses `StrictAnyComponentSchema`, exported by `@object-ui/types/zod` (the built `packages/types/dist/zod/index.zod.js`), called with `.safeParse`. That is the call PR #11069's `validateAuthoredDocument` makes for `objectui validate` and `objectui check`. - It is the shipped, derived face (objectui#8345), not a replica. - `scripts/measure-strict-authoring-face.mjs` is not the real face. Its own header says it keeps its own twin walker, PR #11069's M1 measured that walker behind the shipped face on registered inputs, and it walks only objectui's own corpora. - On `main` the CLI still validates through the tolerant face (PR #11069 is parked), and neither the CLI nor that script reads TypeScript modules. So nothing in this repository walks an objectstack app. - The walk itself is a scratch driver, never committed. It imports app-crm's UI modules with `tsx`, resolving `@objectstack/spec` to the installed 17.5.0, the version objectstack `main` declares. It hands each document to the face the way objectui's console hands it to a renderer. **Command and shas.** - objectui `0a3e5409`, the base, which is `main`. The types dist was built with `pnpm --filter @object-ui/types build`. The run was repeated on head `be58a328` with byte-identical output. - objectstack `f0cc16e8d`, `origin/main`. `399e3aa97b` is its ancestor (`merge-base --is-ancestor` exits 0), and `git diff 399e3aa97b f0cc16e8d -- examples/app-crm` is empty. - app-crm was extracted with `git archive f0cc16e8d examples/app-crm`, and the run was `pnpm exec tsx appcrm-strict-run.mts OBJECTUI_WORKTREE APP_CRM_EXTRACT BEFORE_DASHBOARD`. | app-crm document | projection | tolerant | strict | refusals | | --- | --- | --- | --- | --- | | `dashboards/pipeline.dashboard.ts` · `PipelineDashboard` | `{ type: 'dashboard', ...doc }`: `DashboardView` hands the stored document to `DashboardRenderer`, and objectui's node face for it is the `dashboard` arm | PASS | **PASS** | none | | `pages/welcome.page.ts` · `CrmWelcomePage` | `{ ...page, type: 'page', pageType: page.type }`: the `page` arm, with the kind on `pageType` | PASS | PASS | none | | the same page, as `PageView` writes it | `{ ...page, type: page.type, pageType: page.type }`, so `type: 'home'` | FAIL | FAIL | `invalid_union` at `type` (no matching discriminator) on both faces. This is the page-kind / node-type collision documented in `layout.zod.ts`'s `PAGE_SPEC_EXCLUDED` note (objectui#9642), not an unknown key. | | `apps/crm.app.ts` · `CrmApp` | `{ type: 'app', ...doc }` | PASS | PASS | none | | **lit control:** `pipeline.dashboard.ts` at `399e3aa97b^` (`39ab2940e`) | `{ type: 'dashboard', ...doc }` | PASS | FAIL | `unrecognized_keys` `[format, currency]` at `widgets.0.options`, `widgets.1.options` and `widgets.2.options` (plus the non-viable `metric-card` slot arm's own issues) | - The dashboard pair objectstack#20960 removed (`options.format` / `options.currency`) does not appear at `f0cc16e8d`, and the control shows that the face refuses exactly that pair before the removal. - Nothing in app-crm's UI documents is refused by an unknown key. - Not judged: objects, views, actions, flows, datasets, datasources, hooks, security, translations and seed data. These are server metadata, not objectui node documents, and the console builds its list and form nodes from views in code, so there is no single stored-document projection to judge. ## (iii) `grid.mdx` — "Integration with Advanced Grid" The section offered seven features as `@object-ui/plugin-grid`'s. The basic grid field has five of them too: inline editing, add and remove, row drag-reorder, computed columns, and a one-column footer total. `object-grid` has no computed columns, and it reorders columns, not rows. It is now a table of what each widget reads, feature by feature, each cell checked against the reader: - **inline editing:** `GridField`'s cell controls versus `object-grid`'s `editable`, ANDed with the principal's write verdict; - **add and remove:** `allow_add` / `allow_delete` versus `operations.create` and `operations.delete` with the host's `onDelete`; - **sorting and filtering:** none in the grid field, versus header sort, `sortable: false`, `filter` / `sort` and `searchableFields`; - **drag reorder:** rows through `allow_reorder` and `sort_field`, versus columns only through `reorderableColumns`; - **export:** none, versus `exportOptions`; - **computed columns:** `computed` with `expr` in the grid field, versus none in `object-grid` (a formula field is shown as served); - **totals:** `total_field`, versus `columns[].summary` and group `aggregations`. ## M3 (`scripts/measure-strict-authoring-face.mjs --json`, the types dist built at each ref) | reading | base `0a3e5409` | head `be58a328` | | --- | --: | --: | | documents | 601 | 602 | | nodes | 2229 | 2230 | | nodes strict-refused | 77 | 77 | | strict-only | 46 | 46 | | red today | 31 | 31 | | documents refused, whole tree | 72 | 72 | | documents red today | 24 | 24 | | `form`: documents / refused / strict-only / red today | 97 / 13 / 9 / 4 | 98 / 13 / 9 / 4 | - The only move is the new fixture, one strict-clean `form` document in the catalog corpus. - The per-component refusal rows, the undeclared-key occurrences and `rendererMentionsOfUndeclaredKeys` are identical. `unloadableModules` moved for an environmental reason: app-shell was unbuilt at base. - `corpusMatchesMain` is false at head because the corpus gains that fixture. ## Local verification Every heavy command went through `os-verify-lock` (slot `issue-11070`), with `NODE_OPTIONS=--max-old-space-size=4096`, vitest `--maxWorkers=2`, turbo `--concurrency=2`, and a memory watchdog that kills its own process group below 2500 MB `MemAvailable`. It never fired. - **Closure build:** `turbo run build` over `check-doc-snippet-types --build-filter` ran 35 of 35 tasks at `be58a328`. - **Type-check census:** `turbo run type-check` ran 48 tasks, with 13 of 13 type-checks green. The set is every package whose sources or tests reference `FormField` / `FormFieldSchema` (a `git grep` census), plus `@object-ui/types` and `@object-ui/example-schema-catalog`: `types`, `fields`, `components`, `core`, `plugin-form`, `app-shell`, `cli`, `plugin-calendar`, `plugin-detail`, `plugin-grid`, `plugin-kanban`, `console` and `example-schema-catalog`. - **vitest at `be58a328`:** `packages/types/`, `examples/schema-catalog/` and the two GridField pins ran 351 files, with 348 passed and 3 failed. The three failures were the catalog DOM-leak census pins, and `261d9c9e` records them. - **vitest at `261d9c9e`:** `examples/schema-catalog/` plus the eight catalog- and docs-reading `scripts/__tests__` ran 45 of 45 files and 2601 tests passed. The types sources did not change between the two heads. - **Docs and changeset gates, green:** `check:doc-snippets` (769 of 769 blocks, 0 failed), `check:doc-fences`, `docs:check-links`, `check:doc-example-ids`, `check:doc-types`, `check:doc-examples`, `check:spec-symbols`, `check:test-path-roots`, `check-type-check-coverage`, `check:control-bytes`, `check:new-line-citations` (0 new), the catalog `regenerate:check`, `check:changeset-claims`, `check:pending-changeset-literals`, `check-changeset-presence`, `changeset:check`, `check-changeset-overwrite`, and `check-governed-queue-guard --test` (NOT GOVERNED). - **Pending changesets:** `check:changeset-claims` names 31 pending changesets that describe a touched file. Each paragraph was read, and none is made false by this round, so no dated note is owed. - **Narrowed lint:** `eslint --no-inline-config --format json` over the 10 changed `.ts` / `.tsx` files gave 0 errors, with warnings per file equal to the base blobs. - The population is the config's `**/*.{ts,tsx}` blocks. The JSON, MDX and MD files match none. - The config has no `parserOptions.project` and no `projectService`, and no rule in `eslint-rules/` reads another file, so this diff cannot move an untouched file's verdict. - **Not run locally:** repository-wide `pnpm lint` and `pnpm test`, which CI runs. `main` did not move after the base (`0a3e5409`), so there was nothing to merge before opening. **Changeset:** `@object-ui/types` **minor**. A published accept set widens by eight keys, and the tolerant face narrows on their values. The breaking half is stated in the changeset and priced minor under the fixed group's policy. Nothing else ships: the `@object-ui/fields` edit is a test docblock, and the catalog is private. ## Acceptance notes - **The flat-face boundary.** The eight keys are accepted on every `fields[]` entry, and only the `grid` widget reads them, exactly as with `columns`. Moving them to a `type`-discriminated arm would be a ruling, not this round's work. - **Value rules.** `min_rows` and `max_rows` are typed `number` on both faces, as `GridFieldMetadata` types them. No integer or range rule was added: that would tighten `GridFieldMetadata` too, which is a contract decision. - **Two edits beyond the named surface:** - the docblock of `packages/fields/src/widgets/GridField.declaredSpelling.test.tsx` said "the three `fields-grid` catalog examples", which the new fixture made stale; it now says "every"; - the three DOM-leak census pins, as above. - **The as-served page projection.** `type: 'home'` has no arm on either face, which is the collision objectui#9642 documents. It is an observation and is not filed. --- _Generated by [Claude Code](https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8001068 commit edfcf5a

13 files changed

Lines changed: 276 additions & 19 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@object-ui/types': minor
3+
---
4+
5+
A form field of `type: 'grid'` declares the grid widget's field-level keys (objectui#11070, round 10).
6+
7+
A `form`'s `fields[]` entry of `type: 'grid'` is the authored path to the `grid` widget, which reads its field-level keys off that entry. `GridFieldMetadata` declares them and the grid docs teach them, but the form-field face declared none of them, so the strict authoring face (`StrictAnyComponentSchema`) refused each one by name on a form field while the widget read it.
8+
9+
- **`FormField` (TypeScript)** declares `min_rows`, `max_rows`, `allow_add`, `allow_delete`, `allow_reorder`, `total_field`, `add_label` and `sort_field`, each as `GridFieldMetadata`'s own member by reference (`GridFieldMetadata['min_rows']`, and so on), so the two faces cannot drift. Before, each one resolved to the interface's `[key: string]: any` index signature.
10+
- **`FormFieldSchema` (the zod mirror)** declares the same eight keys with the same value types: numbers for the two row limits, booleans for the three switches, and strings for the total column, the Add label and the sort field. Each `.describe()` says that only the `grid` widget reads it.
11+
- On any other field type the keys are accepted and read by nothing, as `columns` already was.
12+
13+
**Clause-②: yes (widening).** `FormFieldSchema`, a published accept set, accepts eight more keys on a form field, so the strict authoring face now accepts a grid entry that writes them. The tolerant face narrows on their values: `FormFieldSchema` strips an undeclared key, so before this change a wrong-typed value (for example `allow_add: "false"` or `min_rows: "1"`) was dropped from the parsed field in silence, and now it is refused.
14+
15+
## ⚠️ BREAKING, priced as minor under the fixed group's version policy
16+
17+
- **Validation.** A form field whose grid key holds a value of the wrong type, which the tolerant face (`safeValidateSchema`, and so `objectui validate`) accepted by stripping the key, is refused. Fix: write the key with its declared type (`allow_add: false`, `min_rows: 1`).
18+
- **TypeScript.** A `FormField` literal that writes one of the eight keys with the wrong type is a compile error; before, the index signature accepted any value.
19+
20+
Rendering does not change: the `grid` widget reads the same keys as before.

‎content/docs/fields/grid.mdx‎

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ The Grid Field component provides an inline table for managing related records o
1717

1818
<SchemaExample id="fields-grid/read-only-grid" />
1919

20+
## Field-Level Keys
21+
22+
<SchemaExample id="fields-grid/line-items-grid" />
23+
2024
## Field Schema
2125

2226
A grid field is authored as `GridFieldMetadata` (`@object-ui/types`), which is the
@@ -73,6 +77,12 @@ The field-level keys, each read under exactly this one spelling:
7377
`line_no`, `line_number` or `sort`; there is no master-detail key to override
7478
that pick.
7579

80+
The same keys are accepted on a `form` field entry of `type: 'grid'`, as the
81+
Field-Level Keys example above writes them. `FormField` declares each one by
82+
reference to `GridFieldMetadata`, and its zod mirror judges each one by that type,
83+
so the strict authoring face accepts them there and refuses a value of the wrong
84+
type. Only the `grid` widget reads them.
85+
7686
A read-only or disabled grid offers no add, delete, duplicate or reorder, whatever
7787
these keys say. The line-number column always shows. There is no `reorderable`,
7888
`amount_field`, `amountField`, `allow_duplicate` or `show_line_numbers` key: none
@@ -308,12 +318,17 @@ const validateGridData = (data: any[], columns: InlineGridColumn[]) => {
308318

309319
## Integration with Advanced Grid
310320

311-
For full-featured grids, use the `@object-ui/plugin-grid` package which provides:
312-
313-
- Inline editing
314-
- Add/remove rows
315-
- Sorting and filtering
316-
- Drag-and-drop reordering
317-
- Export functionality
318-
- Formula columns
319-
- Aggregation rows
321+
The `grid` field and `@object-ui/plugin-grid`'s `object-grid` node do different jobs.
322+
The `grid` field edits an array stored on the parent record, through the field-level
323+
keys above. `object-grid` lists records (fetched for its `objectName`, or handed to
324+
it) and acts on them as records. What each one reads:
325+
326+
| Feature | `grid` field (`@object-ui/fields`) | `object-grid` (`@object-ui/plugin-grid`) |
327+
| --- | --- | --- |
328+
| Inline editing | Every cell but a computed one is its column's control, unless the field is read-only or disabled | `editable: true` (off by default), only where the current user may edit the object |
329+
| Add and remove rows | `allow_add` and `allow_delete`, each on unless `false` | An add-record row with `operations.create`, and a row's Delete with `operations.delete` and the host's `onDelete`, each only where the current user may create or delete the object's records |
330+
| Sorting and filtering | None | Column-header sorting (a column opts out with `sortable: false`), the query's `filter` and `sort`, and search over `searchableFields` |
331+
| Drag-and-drop reordering | Rows, with `allow_reorder` (on unless `false`); `sort_field` keeps the order on save | Columns only, with `reorderableColumns: true`; rows are not reordered |
332+
| Export | None | `exportOptions` |
333+
| Computed columns | A column with `computed: true` and an arithmetic `expr` over the row's other cells | None: a formula field's value is shown as the server computed it |
334+
| Totals | One footer total, the sum of the `total_field` column | A footer summary per column (`columns[].summary`), and aggregates on group headers (`aggregations`) |

‎examples/schema-catalog/src/index.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ import fields_formula_numeric_formula from './schemas/fields-formula/numeric-for
336336
import fields_formula_text_concatenation from './schemas/fields-formula/text-concatenation.json' with { type: 'json' };
337337
import fields_grid_basic_grid from './schemas/fields-grid/basic-grid.json' with { type: 'json' };
338338
import fields_grid_grid_with_data from './schemas/fields-grid/grid-with-data.json' with { type: 'json' };
339+
import fields_grid_line_items_grid from './schemas/fields-grid/line-items-grid.json' with { type: 'json' };
339340
import fields_grid_read_only_grid from './schemas/fields-grid/read-only-grid.json' with { type: 'json' };
340341
import fields_image_basic_image_upload from './schemas/fields-image/basic-image-upload.json' with { type: 'json' };
341342
import fields_image_multiple_image_upload from './schemas/fields-image/multiple-image-upload.json' with { type: 'json' };
@@ -3392,6 +3393,15 @@ const REGISTRY: Record<string, Example> = {
33923393
},
33933394
schema: fields_grid_grid_with_data,
33943395
},
3396+
'fields-grid/line-items-grid': {
3397+
id: 'fields-grid/line-items-grid',
3398+
meta: {
3399+
title: "Line Items Grid",
3400+
description: "",
3401+
category: 'fields-grid',
3402+
},
3403+
schema: fields_grid_line_items_grid,
3404+
},
33953405
'fields-grid/read-only-grid': {
33963406
id: 'fields-grid/read-only-grid',
33973407
meta: {
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"type": "form",
3+
"showSubmit": false,
4+
"showCancel": false,
5+
"defaultValues": {
6+
"line_items": [
7+
{
8+
"product": "Widget A",
9+
"quantity": 2,
10+
"amount": 59.98,
11+
"position": 0
12+
},
13+
{
14+
"product": "Widget B",
15+
"quantity": 1,
16+
"amount": 49.99,
17+
"position": 1
18+
}
19+
]
20+
},
21+
"fields": [
22+
{
23+
"name": "line_items",
24+
"label": "Line Items",
25+
"type": "grid",
26+
"columns": [
27+
{
28+
"name": "product",
29+
"label": "Product",
30+
"type": "text"
31+
},
32+
{
33+
"name": "quantity",
34+
"label": "Qty",
35+
"type": "number"
36+
},
37+
{
38+
"name": "amount",
39+
"label": "Amount",
40+
"type": "currency"
41+
}
42+
],
43+
"min_rows": 1,
44+
"max_rows": 10,
45+
"allow_add": true,
46+
"allow_delete": true,
47+
"allow_reorder": true,
48+
"total_field": "amount",
49+
"add_label": "Add line item",
50+
"sort_field": "position"
51+
}
52+
]
53+
}

‎examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,11 @@ const NODE_CENSUS: Readonly<Record<string, { rendered: number; noElement: number
150150
slider: { rendered: 1, noElement: 0 },
151151
toggle: { rendered: 17, noElement: 0 },
152152
'sidebar-menu-button': { rendered: 15, noElement: 0 },
153-
grid: { rendered: 26, noElement: 0 },
153+
// 26 -> 27 with objectui#11070 round 10: the new `fields-grid/line-items-grid`
154+
// fixture's form field entry is `type: 'grid'`, which this structural walk
155+
// collects, as it collects the other `fields-grid` entries. Catalog
156+
// authoring, not a renderer change.
157+
grid: { rendered: 27, noElement: 0 },
154158
};
155159

156160
function collect(node: unknown, out: Node[] = []): Node[] {

‎examples/schema-catalog/test/layout-dom-leak-5574.test.tsx‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,11 @@ const NODE_CENSUS: Readonly<Record<string, { rendered: number; noElement: number
103103
flex: { rendered: 248, noElement: 0 },
104104
stack: { rendered: 153, noElement: 0 },
105105
container: { rendered: 15, noElement: 0 },
106-
grid: { rendered: 26, noElement: 0 },
106+
// 26 -> 27 with objectui#11070 round 10: the new `fields-grid/line-items-grid`
107+
// fixture's form field entry is `type: 'grid'`, which this structural walk
108+
// collects, as it collects the other `fields-grid` entries. Catalog
109+
// authoring, not a renderer change.
110+
grid: { rendered: 27, noElement: 0 },
107111
// 699 -> 702: the two components-layout-box exemplars author 3 text nodes
108112
// (objectui#3965). 702 -> 701 and 176 -> 162 with objectui#6942, and the two
109113
// moves have different causes: `components-basic-text/muted.json` was deleted
@@ -129,7 +133,12 @@ const NODE_CENSUS: Readonly<Record<string, { rendered: number; noElement: number
129133
// the `div` -> `box` before/after the migration guide was missing, and its
130134
// two `text` nodes both carry a className, so both need an element — the
131135
// no-element count is unmoved. Catalog authoring, not a renderer change.
132-
text: { rendered: 698, noElement: 154 },
136+
// 698 -> 699 and 154 -> 155 with objectui#11070 round 10: the new
137+
// `fields-grid/line-items-grid` fixture's `product` grid column is
138+
// `type: 'text'`, which this structural walk collects as a `text` node;
139+
// rendered on its own it draws no element. Catalog authoring, not a
140+
// renderer change.
141+
text: { rendered: 699, noElement: 155 },
133142
// The objectui#3965 migration population (80 nodes retyped from `div`) plus
134143
// the 4 nodes of the components-layout-box exemplars. `box` is born on
135144
// `toDomProps` and class-transparency, so it joins the measured set as a

‎examples/schema-catalog/test/svg-host-dom-leak-5632.test.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,11 @@ const MEASURED_TYPES = ['icon', 'spinner', 'grid'] as const;
115115
const NODE_CENSUS: Readonly<Record<string, { rendered: number; noElement: number }>> = {
116116
icon: { rendered: 71, noElement: 0 },
117117
spinner: { rendered: 6, noElement: 0 },
118-
grid: { rendered: 26, noElement: 0 },
118+
// 26 -> 27 with objectui#11070 round 10: the new `fields-grid/line-items-grid`
119+
// fixture's form field entry is `type: 'grid'`, which this structural walk
120+
// collects, as it collects the other `fields-grid` entries. Catalog
121+
// authoring, not a renderer change.
122+
grid: { rendered: 27, noElement: 0 },
119123
};
120124

121125
function collect(node: unknown, out: Node[] = []): Node[] {

‎packages/fields/src/widgets/GridField.declaredSpelling.test.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
* objectui#3951 — grid columns have ONE key spelling, and it is the declared
1111
* one: `name`, the key of `GridFieldMetadata['columns']` (`@object-ui/types`),
1212
* which is `@objectstack/spec`'s inline grid column by reference since
13-
* objectui#11070 — the same key the grid docs page and the three `fields-grid`
14-
* catalog examples author.
13+
* objectui#11070 — the same key the grid docs page and every `fields-grid`
14+
* catalog example author.
1515
*
1616
* `GridField` used to declare its own local column interface keyed by `field`
1717
* and read `c.field` everywhere — `key={c.field}`, `row[c.field]`,

‎packages/types/src/__tests__/form-field-zod-coverage.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ const DECLARED_KEYS = [
9797
// objectui#11070 round 7 — the `grid` widget's columns: the spec's
9898
// `inlineColumns` list (its strict inline grid column), by reference.
9999
'columns',
100+
// objectui#11070 round 10 — the `grid` widget's field-level keys, the
101+
// members of `GridFieldMetadata` (the TS twin carries each by reference).
102+
'min_rows',
103+
'max_rows',
104+
'allow_add',
105+
'allow_delete',
106+
'allow_reorder',
107+
'total_field',
108+
'add_label',
109+
'sort_field',
100110
];
101111

102112
describe('FormFieldSchema covers the FormField contract', () => {

0 commit comments

Comments
 (0)