Skip to content

Commit 2abec3a

Browse files
objectstack-fleet[bot]claudeos-project-manager
authored
feat(types,fields)!: camelCase the grid widget's eight field-level keys; the snake_case spellings are refused by name (objectui#11610) (#11614)
Fixes #11610 Clause-②: yes (narrowing) The `grid` widget's eight field-level keys become camelCase in one move across the published type, its zod mirror, the form-field face and every reader. The snake_case spellings retire at once and are refused by name on all three faces, each refusal naming the camelCase key. Ground: objectstack-ai/objectstack#21704 fork 2, ruled B (record `5978663135`); claim `5979906990` (seat ruling: refuse by name, no load-time conversion, premised on no stored producer). Dispatch session `session_01CPvhwGcirXqBGEdPSb72TZ`, `mode:subagent`. ## Premise (measured before any edit): no stored producer outside objectui's own fixtures - **objectstack `origin/main` `316be321`** (read through `git grep` on a private ref, nothing edited): the eight snake_case words occur only in comments, test strings and one changeset line. `packages/spec` `component.zod.ts` names them in three comments (the line-items guidance block, the `customFields` fork note, the master-detail `sortField` retirement note); the retirement test for `details.sortField` carries one as a must-not-match judge string; `conversions/registry.ts`, `migrations/registry.ts` and the retired-key entry for `details.sortField` carry it in comments. No schema declares or emits any of the eight as a field's metadata, and the spec's `FieldType` enum has no `grid` member at all, so the spec cannot store a `grid` field. The seat's reading is confirmed. - **objectui at base `b508ac50`** (every tracked file, `git grep` on that commit), each hit classified: - grid field metadata, READERS: `GridField.tsx` (the eight reads). - grid field metadata, PRODUCERS (in-repo, render-time, not stored): `MasterDetailForm` and `LineItemsPanel` (both hand the grid an object cast through `any`), and `apps/console` `DevLookup` (a dev harness). - grid field metadata, DECLARATIONS: `GridFieldMetadata`, the form-field face (`FormField`), its zod mirror (`FormFieldSchema`). - grid field metadata, STORED fixture: `examples/schema-catalog` `fields-grid/line-items-grid.json` (the only stored document). - docs: `content/docs/fields/grid.mdx`; the clipboard-paste RFC (states `max_rows` / `allow_add` as GridFieldMetadata's API in its proposed wiring); ADR 0001 (reported below, not edited). - tests: seven `GridField` test files in `packages/fields` (the triage counted five), the master-detail members test and the line-items total test in `packages/plugin-form`, three `packages/types` tests (strict face, form-field coverage, mirror parity comment), the console registry pin's prose. - SAME NAME, DIFFERENT READER (not renamed): the `## MAX_ROWS vs MIN_ROWS` heading-anchor string in `plugin-markdown`'s toc test; the en locale comment (a comment about the grid's label, updated); every `CHANGELOG.md` (history, untouched). The master-detail `details.sortField` the spec retired is a different key on a different object and stays untouched. - Existing facts not re-measured: the objectstack#21464 census (0 `customFields` entries) and the triage's hotcrm / cloud reading (0 hits). ## Rename map | FROM | TO | | --- | --- | | `min_rows` | `minRows` | | `max_rows` | `maxRows` | | `allow_add` | `allowAdd` | | `allow_delete` | `allowDelete` | | `allow_reorder` | `allowReorder` | | `total_field` | `totalField` | | `add_label` | `addLabel` | | `sort_field` | `sortField` | One list holds it: `GRID_FIELD_RETIRED_KEYS` (new export of `@object-ui/types`, with its key type `GridFieldRetiredKey`). The zod arms and the widget both read it; a type-level pin holds its keys equal to the `never` tombstones and its values equal to the live camelCase members. ## The retirement form, per face (refused by name, never stripped) - **TypeScript.** `GridFieldMetadata` and `FormField` each declare the eight snake_case keys as `never` tombstones with a `@deprecated` note naming the replacement. `{ type: 'grid', name: 'lines', min_rows: 1 }` typed `GridFieldMetadata` does not compile; on the docs face the gate's compile of `grid.mdx` reports `TS2322: Type 'number' is not assignable to type 'undefined'` for the same key. - **zod (`@object-ui/types/zod`).** `FormFieldSchema` declares each snake_case key as an `aliasKeyRefusal` arm. On the mirror, the tolerant face (`safeValidateSchema`, the `objectui validate` door) and the strict authoring face, `fields[0].min_rows: 1` answers exactly one issue, `invalid_type` at `fields.0.min_rows`, whose message leads ``Unrecognized key(s) on this form field: `min_rows`. Did you mean `min_rows` → `minRows`?`` and goes on to say the value stays the same. Any value is refused (the refusal is by name). The camelCase key with the same value parses and is kept on all three doors. - **The widget (`@object-ui/fields`).** `GridField` is now a thin gate over the grid body: a field whose metadata carries any snake_case key (with a defined value) is drawn as `RetiredGridFieldKeys` instead of the grid, an inline `role="alert"` (`data-testid="grid-field-retired-keys"`) reading for example ``[object-ui] Grid field `lines` carries retired snake_case key(s): `min_rows` → `minRows`. …``, plus the same text on `console.error` once per message. This is `RetiredFieldTombstone`'s settled shape in this package: nothing thrown, nothing silently substituted, rows untouched (`onChange` is never called). Producers move with it: both plugin-form adapters write the camelCase keys, and their grid object is now `satisfies` Partial of `GridFieldMetadata` instead of `as any`, so a snake_case key written there no longer compiles. ## Pins and guards, with ablation readings All ablations ran through the anchored mutation tool (anchor must hit; blob change and restore proven on disk; restore leg `git checkout HEAD`, blob after restore equal to the HEAD blob, `git diff HEAD` empty), from committed state. 1. **The eight camelCase keys draw**, each through the real `GridField` with a lit control without the key (`GridField.retiredSnakeKeys-11610.test.tsx`): `minRows` disables every Remove at the minimum; `maxRows` disables Add and every Duplicate at the maximum; `allowAdd: false` removes Add and Duplicate; `allowDelete: false` removes Remove; `allowReorder: false` removes the drag handles; `totalField` draws the footer total (30); `addLabel` labels Add; `sortField` stamps the new indices after a drag. Through the real form path too: `fields-grid-keys-camelcase-11610.test.tsx` renders the catalog fixture through `SchemaRenderer` and the form renderer (label `Add line item`, total `109.97`, two drag handles / Duplicate / Remove, all enabled), with the snake_case respelling as its control. 2. **A snake_case key is answered by the refusal, never dropped**: per key, the alert names the key and its replacement, the grid is not drawn, `console.error` is called once with the alert's text; edges pinned: several keys in one alert in map order, a snake key beside its camelCase twin is still refused, an `undefined` value is not refused (as on the TS and zod faces), a nameless field reads "This grid field", a second mount logs nothing new. - ABLATION (widget gate disabled): 12 refusal tests red, the 10 others (the eight draws, the lit control, the `undefined` row) green. Restored. 3. **zod refusal by name** (`grid-field-keys-camelcase-11610.test.ts`, three doors × eight keys, plus lit controls and an any-value row). - ABLATION (delete the `min_rows` arm): 8 tests red (the `min_rows` row and the any-value row on each of the three doors, and both `form-field-zod-coverage` rows). Restored. 4. **TS tombstones, each with a deletion guard that does not lean on an index signature** (same file, read by `tsc -p tsconfig.test.json`): an `Equal` row per member on both faces, a `keyof` membership row on `FormField`, and the map-equals-tombstones rows. - ABLATION A (delete all eight `GridFieldMetadata` tombstones): the eight `Equal` rows red, `TS2339` naming each key; the two map rows red; the strict-face lit-control row red; the map's `satisfies` red (`TS2561`). ⚠️ The eight `@ts-expect-error` rows over fresh `GridFieldMetadata` literals stayed SILENT: with the member gone the key is still an excess property and the directive swallows it. That is the objectui#8347 lesson, reproduced; the `Equal` rows are what catch the deletion. - ABLATION A1 (delete `total_field` alone): exactly its `Equal` row (`TS2339`), the map row and the `satisfies` red. - ABLATION B (delete all eight `FormField` tombstones): the eight `Equal` rows and the `keyof` row red, four `TS2578` unused `@ts-expect-error` (the index signature now accepts the key), the strict-face by-reference rows red, and the mirror-parity ratchet red on `FormFieldSchema`. - ABLATION B1 (delete `sort_field` alone): its `Equal` row, the `keyof` row, its `TS2578`, the strict-face rows and the parity ratchet red, naming `sort_field`. - Every leg restored with blob equal to HEAD; the green `tsc -p tsconfig.test.json` runs are in the gates below. 5. **Docs face**: respelling one key of `grid.mdx`'s snippet snake_case turns `pnpm check:doc-snippets` red at that line (`TS2322`), restored green. Repo-wide pin sweep: every assertion that read a grid key by its snake_case spelling now asserts the camelCase key's substance (the master-detail grid object's exact key set and values, the strict face's accept and wrong-type rows, the coverage key set, the widget draws). The rejection rows for genuinely wrong values (a string `minRows`, a numeric `addLabel`, …) stay, respelled; the guarded surface grows by the eight refusals. ## Files and landing point The claim's surface, plus two adjacent files the census named: `apps/console` `DevLookup.tsx` (a producer) and its registry pin's prose; `packages/i18n` en locale (one comment naming `add_label`); the RFC. The landing point is the claim's list; no producer was found in another package. ## ADR and RFC - `docs/adr/0001-master-detail-subform.md` (governed, ⛔ not edited) names the keys in three sentences: "Honors `min_rows` / `max_rows` / `allow_add` / `allow_delete` from the …", "Drag reorder of lines (`allow_reorder`), grouping/subtotal rows." and "… **drag-to-reorder** (a `sort_field` …". Reported for whoever owns that record. - `content/docs/rfcs/0001-clipboard-paste.md` (status Draft) stated `max_rows` "from GridFieldMetadata" and wired `field.allow_add` / `field.max_rows` as the current API, so it is edited to the camelCase keys. ## Serial and overlap The claim's serial step is done. objectui#11605's PR #11612 (`fd060f07`) and objectui#8347's PR #11607 (`b403bb36`) both landed. `main` at `b403bb36` is merged into this branch by merge commit `8b56ef26`. Its combined diff is empty, so no hunk was hand-resolved. The four files this branch shares with them carry `main`'s hunks outside this branch's hunks: `packages/i18n/src/locales/en.ts`, `packages/types/src/form.ts`, `packages/types/src/index.ts` and `zod-mirror-parity.test.ts`. The contract review on the merged head (`5981193754`) reads every hunk of this branch as byte-identical to the reviewed head. The gate verdict on the merged head is its own check-runs. ## Gates (the dev's local readings, tree `82b400f9`; the current head's verdict is its check-runs) - type-check, package and test program: `@object-ui/types`, `@object-ui/fields`, `@object-ui/plugin-form`, `@object-ui/i18n`, `@object-ui/example-schema-catalog`, `@object-ui/console` — all exit 0 (the console run after building `@object-ui/plugin-tree`, which its import graph needs). The fields and plugin-form test programs list the touched test files (`--listFilesOnly`). - vitest, repo-root form: `packages/types/` 353 files / 9482 tests passed; `packages/fields/` 233 passed + 1 skipped / 3647 passed + 7 skipped; `packages/plugin-form/` 163 / 1889 passed + 1 skipped; `examples/schema-catalog/` + the console registry pin + `packages/i18n/` 121 / 3807 passed + 13 skipped. The `types` / `fields` / `plugin-form` runs measured a working tree byte-identical to `82b400f9` (its two later-committed files, the changeset and the catalog test, were already on disk); the rest ran at `82b400f9`. - lint (`eslint` with the root config, as each package's `lint` runs it) over the 25 touched TS/TSX files, counted from `--format json`: 0 errors; the one finding on a touched line is the pre-existing `as any` warning in `GridField.test.tsx`'s fixture. Narrowing statement: the population is every file under the touched packages, read by the one root `eslint.config.js`; it enables no type-aware linting and no custom rule reads the filesystem, so this diff cannot move a verdict on an untouched file. `pnpm lint` itself is CI's. - `check:doc-snippets` (777 blocks judged, 0 failed), `check:doc-examples`, `check:readme-exports`, `check:new-line-citations` (0 new), `check:control-bytes`, `check:spec-symbols`, `check:designer-field-key-parity`, `check:prompt-keys`, `check:handler-key-reads`, `check:unreferenced-sources`, `check:phantom-deps`, `check:self-import`, `check:test-path-roots`, `check:doc-types`, `check:doc-example-ids`, `check:doc-fences`, `check:doc-example-readers`, `check:i18n-drift`, `check:i18n-keys`, `check:esm-specifiers`, `check:side-effects-array`: all exit 0. - changeset checkers: presence, no-major, fixed, overwrite, claims, pending-literals: all exit 0. `.changeset/11610-grid-keys-camelcase.md`: `@object-ui/types` and `@object-ui/fields` `minor`, breaking banner, FROM → TO for all eight, `Clause-②: yes (narrowing)`. - Governed surface: `node scripts/check-governed-queue-guard.mjs --test` over the diff answers NOT GOVERNED. ## Acceptance notes - ⚠️ Same name, different meaning: the grid's `totalField` names the CHILD column summed (the value a spec `amountField` carries), while the spec's own `totalField` on a master-detail subform or `record:line_items` names the PARENT field the sum is saved to. The mechanical camelCase of `total_field` lands on that homonym. Every declaration, the doc page and the changeset say so; the open question in the report asks the seat whether the spec's runtime form field should carry it under this name. - When the spec seat adds the camelCase keys to the runtime form field (objectstack-ai/objectstack#21704), the three objectstack comments that still name the snake_case keys can move with it. Carrier: that stage. - `DevLookup.tsx` keeps its `as any` field (a dev harness; only the key moved). The dispatching session is `https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ`. --- _Generated by [Claude Code](https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Leehom <pm@objectstack.ai>
1 parent b403bb3 commit 2abec3a

29 files changed

Lines changed: 1226 additions & 251 deletions
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@object-ui/types': minor
3+
'@object-ui/fields': minor
4+
---
5+
6+
The `grid` field's eight field-level keys are camelCase now, and their snake_case spellings are retired and refused by name on every face (objectui#11610).
7+
8+
BREAKING (`@object-ui/types`, `@object-ui/fields`): a `grid` field's metadata, and a `form` `fields[]` entry of `type: 'grid'`, must spell these keys in camelCase. (The bump is `minor` by this repo's release model: objectui's major follows the `@objectstack` family major, and its own breaking changes ship as `minor` with the breaking semantics stated here.)
9+
10+
- FROM `min_rows` → TO `minRows`
11+
- FROM `max_rows` → TO `maxRows`
12+
- FROM `allow_add` → TO `allowAdd`
13+
- FROM `allow_delete` → TO `allowDelete`
14+
- FROM `allow_reorder` → TO `allowReorder`
15+
- FROM `total_field` → TO `totalField`
16+
- FROM `add_label` → TO `addLabel`
17+
- FROM `sort_field` → TO `sortField`
18+
19+
Why: `@objectstack/spec`'s runtime form field declares config keys in camelCase only, so it could not declare these keys as they were written (objectstack-ai/objectstack#21704, fork 2, ruled B). There is no alias window and no dual read: no reader reads the snake_case spellings any more, and no stored producer outside this repository's own fixtures, which move with this change, was found to write them.
20+
21+
**Migration.** Rename each key; its value stays the same. `totalField` keeps its meaning: the CHILD column summed into the grid's footer, which is the value a spec `amountField` carries. It is not the parent field the spec's own `totalField` names on a master-detail subform.
22+
23+
What each face does with a snake_case key now:
24+
25+
- **TypeScript.** `GridFieldMetadata` and `FormField` declare each as a `never` member, so an authored value no longer compiles. The camelCase members carry the value types the snake_case members had, and `FormField` still takes each one by reference to `GridFieldMetadata`.
26+
- **zod (`@object-ui/types/zod`).** NARROWS on the tolerant face (`safeValidateSchema`, which `objectui validate` runs) and on the strict authoring face: a form field entry carrying a snake_case key used to parse with the value kept, and is now refused with one `invalid_type` issue at that key. The message leads with ``Did you mean `min_rows` → `minRows`?`` (each key names its own replacement). WIDENS on both faces: the camelCase keys parse, judged by the same value types.
27+
- **The `grid` widget (`@object-ui/fields`).** `GridField` reads the camelCase keys only. A field whose metadata still carries a snake_case key is drawn as an inline alert naming each retired key beside its replacement (`role="alert"`, `data-testid="grid-field-retired-keys"`) instead of the grid, and the same text goes to `console.error` once. Nothing is thrown, so the rest of the form still draws, and the rows are not changed.
28+
29+
New export from `@object-ui/types`: `GRID_FIELD_RETIRED_KEYS`, the snake_case to camelCase map that the zod refusals and the widget both read, with its key type `GridFieldRetiredKey`.
30+
31+
`@object-ui/plugin-form`'s master-detail and line-items adapters now hand the grid the camelCase keys, typed against `GridFieldMetadata` instead of cast through `any`. What they draw does not change.
32+
33+
**Clause-②: yes (narrowing)**: the camelCase spellings widen each face, and the snake_case spellings narrow it.

‎apps/console/src/__tests__/registry-inputs-spec-parity.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3034,7 +3034,7 @@ const MEMBER_PINS: Record<string, MemberPin> = {
30343034
},
30353035
'object-master-detail-form.details': {
30363036
file: 'packages/plugin-form/src/__tests__/masterDetailDetailsMembers-8071.test.tsx',
3037-
pins: 'Members are detail-collection OBJECTS (`MasterDetailDetailConfig`), and unlike this block\'s parent keys the read site is THIS block: `MasterDetailForm` is the key\'s only reader. Each member renders as its own section in AUTHORED order, headed by `title` (fallback `Line Items`), with `columns` reaching the line grid in authored order. ⭐ The sharp row: four members reach the grid through ONE hand-written object, each RENAMED on the way (`minRows` to `min_rows`, `maxRows` to `max_rows`, `addLabel` to `add_label`, `amountField` to `total_field`), beside `columns` and the DERIVED `sort_field` (objectui#11070 round 9 retired the `sortField` member; row 2c pins that a written one is read by nothing). That object is pinned as a SORTED KEY SET of exactly six, with two off-list members (one in the grid\'s own snake_case spelling) asserted not forwarded and an all-empty control beside it. `inlineMode: \'form\'` makes a list whose Add opens the full form (labelled `Add` when unset), and in grid mode `formFields` wider than `columns` offers the row form; each arm is the other\'s control. `childObject` and `relationshipField` address the atomic batch (each line a create on the child, linked by `{ $ref: 0 }`) and the edit-mode read (`{ $filter: { FK: recordId }, $top: 500 }` per collection). `totalField` receives the sum of the lines\' `amountField` on the PARENT leg, and a collection without one puts nothing there. With `{ childObject }` alone the FK and the columns are DERIVED from the child object, the one member behaviour the spec\'s description promises, and the batch writes the derived FK. `details: []` is the non-vacuity control: no section, no grid, a one-leg batch. The line grid is the REAL `LineItemsField`, wrapped only to record the props it is handed. The spec row is `z.array(z.unknown())`, whose description names five of the eleven members the renderer reads, and the registration declares no `of`, so the read site is the whole member contract. New file (objectui#8071 slice 18).',
3037+
pins: 'Members are detail-collection OBJECTS (`MasterDetailDetailConfig`), and unlike this block\'s parent keys the read site is THIS block: `MasterDetailForm` is the key\'s only reader. Each member renders as its own section in AUTHORED order, headed by `title` (fallback `Line Items`), with `columns` reaching the line grid in authored order. ⭐ The sharp row: four members reach the grid through ONE hand-written object under the grid\'s camelCase keys (objectui#11610): `minRows`, `maxRows` and `addLabel` by the same name, and `amountField` RENAMED to the grid\'s `totalField` (the CHILD column summed; the detail\'s own `totalField`, the PARENT field, is not forwarded), beside `columns` and the DERIVED `sortField` (objectui#11070 round 9 retired the detail\'s `sortField` member; row 2c pins that a written one is read by nothing). That object is pinned as a SORTED KEY SET of exactly six, with two off-list members (one in the grid\'s own spelling, `allowAdd`) asserted not forwarded and an all-empty control beside it. `inlineMode: \'form\'` makes a list whose Add opens the full form (labelled `Add` when unset), and in grid mode `formFields` wider than `columns` offers the row form; each arm is the other\'s control. `childObject` and `relationshipField` address the atomic batch (each line a create on the child, linked by `{ $ref: 0 }`) and the edit-mode read (`{ $filter: { FK: recordId }, $top: 500 }` per collection). `totalField` receives the sum of the lines\' `amountField` on the PARENT leg, and a collection without one puts nothing there. With `{ childObject }` alone the FK and the columns are DERIVED from the child object, the one member behaviour the spec\'s description promises, and the batch writes the derived FK. `details: []` is the non-vacuity control: no section, no grid, a one-leg batch. The line grid is the REAL `LineItemsField`, wrapped only to record the props it is handed. The spec row is `z.array(z.unknown())`, whose description names five of the eleven members the renderer reads, and the registration declares no `of`, so the read site is the whole member contract. New file (objectui#8071 slice 18).',
30383038
},
30393039
'object-master-detail-form.fields': {
30403040
file: 'packages/plugin-form/src/__tests__/topLevelFieldsWarnCoverage-8847.test.tsx',

‎apps/console/src/dev/DevLookup.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const DevLookup: React.FC = () => {
1414
{ name: 'note', label: 'Note', type: 'text' },
1515
{ name: 'amount', label: 'Amount', type: 'currency' },
1616
],
17-
total_field: 'amount',
17+
totalField: 'amount',
1818
} as any;
1919
return (
2020
<div className="mx-auto max-w-3xl space-y-4 p-6">

‎content/docs/fields/grid.mdx‎

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,29 +45,30 @@ const lineItems: GridFieldMetadata = {
4545
{ name: 'unit_price', label: 'Unit Price', type: 'currency', width: 120 },
4646
{ name: 'amount', label: 'Amount', type: 'currency', computed: true, expr: 'quantity * unit_price' },
4747
],
48-
min_rows: 1,
49-
max_rows: 50,
50-
allow_add: true,
51-
allow_delete: true,
52-
allow_reorder: false,
53-
total_field: 'amount',
54-
add_label: 'Add line',
55-
sort_field: 'position',
48+
minRows: 1,
49+
maxRows: 50,
50+
allowAdd: true,
51+
allowDelete: true,
52+
allowReorder: false,
53+
totalField: 'amount',
54+
addLabel: 'Add line',
55+
sortField: 'position',
5656
};
5757
```
5858

5959
The field-level keys, each read under exactly this one spelling:
6060

61-
- `min_rows` / `max_rows` — the row-count limits.
62-
- `allow_add` / `allow_delete` — whether rows can be added or removed. Each row's
63-
duplicate action follows `allow_add`, since a duplicate is an add.
64-
- `allow_reorder` — set `false` to remove the drag handles; rows can be reordered
61+
- `minRows` / `maxRows` — the row-count limits.
62+
- `allowAdd` / `allowDelete` — whether rows can be added or removed. Each row's
63+
duplicate action follows `allowAdd`, since a duplicate is an add.
64+
- `allowReorder` — set `false` to remove the drag handles; rows can be reordered
6565
by dragging otherwise.
66-
- `total_field` — the `name` of the **child** column summed into the footer total.
67-
It is the spec's `amountField`, not its `totalField` (the parent field a
68-
master-detail save writes the sum to). No total shows when it is unset.
69-
- `add_label` — the Add button's label.
70-
- `sort_field` — the name of a field on each **row** that the grid stamps with the
66+
- `totalField` — the `name` of the **child** column summed into the footer total.
67+
It carries the spec's `amountField`. The spec's own `totalField` on a
68+
master-detail subform is a different thing with the same name: the parent field
69+
the save writes the sum to. No total shows when it is unset.
70+
- `addLabel` — the Add button's label.
71+
- `sortField` — the name of a field on each **row** that the grid stamps with the
7172
row's index (0, 1, 2, …) on every change, so the order a drag-reorder leaves is
7273
saved with the rows. It is not one of the columns: a column of that name would
7374
have its typed value overwritten. Rows carry no position when it is unset. When
@@ -95,6 +96,21 @@ per-column `defaultValue`: a new row starts with every cell empty.
9596
The value being edited, and the `className` / `disabled` a host supplies, are **not**
9697
metadata keys — they are runtime widget props. See [Field Widget Props](/docs/fields/widget-props).
9798

99+
### Retired snake_case keys
100+
101+
Until objectui#11610 these eight keys were spelled in snake_case: `min_rows`,
102+
`max_rows`, `allow_add`, `allow_delete`, `allow_reorder`, `total_field`,
103+
`add_label` and `sort_field`. Each is now refused by name, with the camelCase key
104+
to write instead, and its value carries over unchanged:
105+
106+
- `GridFieldMetadata` and `FormField` declare each as a `never` member, so
107+
TypeScript refuses it at the authoring site.
108+
- `objectui validate` refuses it on a `form` field entry with
109+
``Did you mean `min_rows` → `minRows`?`` (and so on for each key).
110+
- The `grid` widget draws an alert naming the retired keys and their replacements
111+
instead of the grid, and logs the same text with `console.error`. It does not
112+
quietly draw a grid that ignores them.
113+
98114
## Column Types
99115

100116
A column's `type` is one of the spec's nine cell controls: `text`, `number`,
@@ -329,9 +345,9 @@ it) and acts on them as records. What each one reads:
329345
| Feature | `grid` field (`@object-ui/fields`) | `object-grid` (`@object-ui/plugin-grid`) |
330346
| --- | --- | --- |
331347
| 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 |
332-
| 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 |
348+
| Add and remove rows | `allowAdd` and `allowDelete`, 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 |
333349
| Sorting and filtering | None | Column-header sorting (a column opts out with `sortable: false`), the query's `filter` and `sort`, and search over `searchableFields` |
334-
| 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 |
350+
| Drag-and-drop reordering | Rows, with `allowReorder` (on unless `false`); `sortField` keeps the order on save | Columns only, with `reorderableColumns: true`; rows are not reordered |
335351
| Export | None | `exportOptions` |
336352
| 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 |
337-
| Totals | One footer total, the sum of the `total_field` column | A footer summary per column (`columns[].summary`), and aggregates on group headers (`aggregations`) |
353+
| Totals | One footer total, the sum of the `totalField` column | A footer summary per column (`columns[].summary`), and aggregates on group headers (`aggregations`) |

‎content/docs/rfcs/0001-clipboard-paste.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ export interface UsePasteToGridOptions {
251251
rows: any[];
252252
/** Currently selected range, or null when nothing selected */
253253
selection?: CellRange | null;
254-
/** Cap rows after paste (e.g. max_rows from GridFieldMetadata) */
254+
/** Cap rows after paste (e.g. maxRows from GridFieldMetadata) */
255255
maxRows?: number;
256256
/** Whether append-beyond-selection is allowed */
257257
allowAppend?: boolean;
@@ -344,7 +344,7 @@ Dialog layout (Shadcn `Dialog` + `Table`):
344344
* **Row** — row is valid iff all mapped cells are `ok`. Invalid rows are
345345
dropped from the apply set but **kept in the preview** so the user can
346346
cancel and fix in the source spreadsheet.
347-
* **Table** — `max_rows` cap enforced: if `currentRows + valid > max_rows`,
347+
* **Table** — `maxRows` cap enforced: if `currentRows + valid > maxRows`,
348348
surface "Only first K rows will be appended" in the status bar.
349349

350350
### 6.4 Quick-paste optimisation (deferred to v1.1)
@@ -422,8 +422,8 @@ const { onPaste, previewDialog } = usePasteToGrid({
422422
columns: coercersFromGridFieldColumns(field.columns),
423423
rows: value ?? [],
424424
selection,
425-
allowAppend: field.allow_add !== false,
426-
maxRows: field.max_rows,
425+
allowAppend: field.allowAdd !== false,
426+
maxRows: field.maxRows,
427427
preview: true,
428428
onApply: (commands) => {
429429
onChange(applyCommands(value ?? [], commands));
@@ -468,7 +468,7 @@ RFC is marked Accepted.
468468
| `parseClipboard` | Vitest | 100% branch — covers TSV, CSV, mixed newlines, BOM, quoted with `""`, quoted with embedded newlines, single-cell, all-empty |
469469
| `coerceCell` | Vitest | 100% per supported type — locale-sensitive numbers, currency, percent, ISO date, Excel serial, boolean dictionary |
470470
| `usePasteToGrid` | RTL + Vitest | Selection-aware mode decision, header detection, mapping override, error rollup |
471-
| `PastePreviewDialog` | RTL + Vitest | Empty preview, all-valid, mixed valid/invalid, all-invalid, skipped columns, max_rows clamp |
471+
| `PastePreviewDialog` | RTL + Vitest | Empty preview, all-valid, mixed valid/invalid, all-invalid, skipped columns, maxRows clamp |
472472
| `ObjectGrid` integration | RTL + Playwright e2e | Paste flow end-to-end, staged toolbar, feature flag off path |
473473
| Cross-browser clipboard | Playwright | Chromium + WebKit (Safari has known clipboard quirks) |
474474

‎examples/schema-catalog/src/schemas/fields-grid/line-items-grid.json‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@
4040
"type": "currency"
4141
}
4242
],
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"
43+
"minRows": 1,
44+
"maxRows": 10,
45+
"allowAdd": true,
46+
"allowDelete": true,
47+
"allowReorder": true,
48+
"totalField": "amount",
49+
"addLabel": "Add line item",
50+
"sortField": "position"
5151
}
5252
]
5353
}

0 commit comments

Comments
 (0)