diff --git a/.changeset/11070-grid-form-face-round10.md b/.changeset/11070-grid-form-face-round10.md
new file mode 100644
index 0000000000..c109cc7ba3
--- /dev/null
+++ b/.changeset/11070-grid-form-face-round10.md
@@ -0,0 +1,20 @@
+---
+'@object-ui/types': minor
+---
+
+A form field of `type: 'grid'` declares the grid widget's field-level keys (objectui#11070, round 10).
+
+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.
+
+- **`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.
+- **`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.
+- On any other field type the keys are accepted and read by nothing, as `columns` already was.
+
+**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.
+
+## ⚠️ BREAKING, priced as minor under the fixed group's version policy
+
+- **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`).
+- **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.
+
+Rendering does not change: the `grid` widget reads the same keys as before.
diff --git a/content/docs/fields/grid.mdx b/content/docs/fields/grid.mdx
index 8895e7932f..7499ce9403 100644
--- a/content/docs/fields/grid.mdx
+++ b/content/docs/fields/grid.mdx
@@ -17,6 +17,10 @@ The Grid Field component provides an inline table for managing related records o
+## Field-Level Keys
+
+
+
## Field Schema
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:
`line_no`, `line_number` or `sort`; there is no master-detail key to override
that pick.
+The same keys are accepted on a `form` field entry of `type: 'grid'`, as the
+Field-Level Keys example above writes them. `FormField` declares each one by
+reference to `GridFieldMetadata`, and its zod mirror judges each one by that type,
+so the strict authoring face accepts them there and refuses a value of the wrong
+type. Only the `grid` widget reads them.
+
A read-only or disabled grid offers no add, delete, duplicate or reorder, whatever
these keys say. The line-number column always shows. There is no `reorderable`,
`amount_field`, `amountField`, `allow_duplicate` or `show_line_numbers` key: none
@@ -308,12 +318,17 @@ const validateGridData = (data: any[], columns: InlineGridColumn[]) => {
## Integration with Advanced Grid
-For full-featured grids, use the `@object-ui/plugin-grid` package which provides:
-
-- Inline editing
-- Add/remove rows
-- Sorting and filtering
-- Drag-and-drop reordering
-- Export functionality
-- Formula columns
-- Aggregation rows
\ No newline at end of file
+The `grid` field and `@object-ui/plugin-grid`'s `object-grid` node do different jobs.
+The `grid` field edits an array stored on the parent record, through the field-level
+keys above. `object-grid` lists records (fetched for its `objectName`, or handed to
+it) and acts on them as records. What each one reads:
+
+| Feature | `grid` field (`@object-ui/fields`) | `object-grid` (`@object-ui/plugin-grid`) |
+| --- | --- | --- |
+| 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 |
+| 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 |
+| Sorting and filtering | None | Column-header sorting (a column opts out with `sortable: false`), the query's `filter` and `sort`, and search over `searchableFields` |
+| 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 |
+| Export | None | `exportOptions` |
+| 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 |
+| Totals | One footer total, the sum of the `total_field` column | A footer summary per column (`columns[].summary`), and aggregates on group headers (`aggregations`) |
diff --git a/examples/schema-catalog/src/index.ts b/examples/schema-catalog/src/index.ts
index d8a195fbd4..a45e40b31e 100644
--- a/examples/schema-catalog/src/index.ts
+++ b/examples/schema-catalog/src/index.ts
@@ -336,6 +336,7 @@ import fields_formula_numeric_formula from './schemas/fields-formula/numeric-for
import fields_formula_text_concatenation from './schemas/fields-formula/text-concatenation.json' with { type: 'json' };
import fields_grid_basic_grid from './schemas/fields-grid/basic-grid.json' with { type: 'json' };
import fields_grid_grid_with_data from './schemas/fields-grid/grid-with-data.json' with { type: 'json' };
+import fields_grid_line_items_grid from './schemas/fields-grid/line-items-grid.json' with { type: 'json' };
import fields_grid_read_only_grid from './schemas/fields-grid/read-only-grid.json' with { type: 'json' };
import fields_image_basic_image_upload from './schemas/fields-image/basic-image-upload.json' with { type: 'json' };
import fields_image_multiple_image_upload from './schemas/fields-image/multiple-image-upload.json' with { type: 'json' };
@@ -3392,6 +3393,15 @@ const REGISTRY: Record = {
},
schema: fields_grid_grid_with_data,
},
+ 'fields-grid/line-items-grid': {
+ id: 'fields-grid/line-items-grid',
+ meta: {
+ title: "Line Items Grid",
+ description: "",
+ category: 'fields-grid',
+ },
+ schema: fields_grid_line_items_grid,
+ },
'fields-grid/read-only-grid': {
id: 'fields-grid/read-only-grid',
meta: {
diff --git a/examples/schema-catalog/src/schemas/fields-grid/line-items-grid.json b/examples/schema-catalog/src/schemas/fields-grid/line-items-grid.json
new file mode 100644
index 0000000000..07c4369d61
--- /dev/null
+++ b/examples/schema-catalog/src/schemas/fields-grid/line-items-grid.json
@@ -0,0 +1,53 @@
+{
+ "type": "form",
+ "showSubmit": false,
+ "showCancel": false,
+ "defaultValues": {
+ "line_items": [
+ {
+ "product": "Widget A",
+ "quantity": 2,
+ "amount": 59.98,
+ "position": 0
+ },
+ {
+ "product": "Widget B",
+ "quantity": 1,
+ "amount": 49.99,
+ "position": 1
+ }
+ ]
+ },
+ "fields": [
+ {
+ "name": "line_items",
+ "label": "Line Items",
+ "type": "grid",
+ "columns": [
+ {
+ "name": "product",
+ "label": "Product",
+ "type": "text"
+ },
+ {
+ "name": "quantity",
+ "label": "Qty",
+ "type": "number"
+ },
+ {
+ "name": "amount",
+ "label": "Amount",
+ "type": "currency"
+ }
+ ],
+ "min_rows": 1,
+ "max_rows": 10,
+ "allow_add": true,
+ "allow_delete": true,
+ "allow_reorder": true,
+ "total_field": "amount",
+ "add_label": "Add line item",
+ "sort_field": "position"
+ }
+ ]
+}
diff --git a/examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx b/examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx
index 4d1bd0d9a4..3cfb985336 100644
--- a/examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx
+++ b/examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx
@@ -150,7 +150,11 @@ const NODE_CENSUS: Readonly 27 with objectui#11070 round 10: the new `fields-grid/line-items-grid`
+ // fixture's form field entry is `type: 'grid'`, which this structural walk
+ // collects, as it collects the other `fields-grid` entries. Catalog
+ // authoring, not a renderer change.
+ grid: { rendered: 27, noElement: 0 },
};
function collect(node: unknown, out: Node[] = []): Node[] {
diff --git a/examples/schema-catalog/test/layout-dom-leak-5574.test.tsx b/examples/schema-catalog/test/layout-dom-leak-5574.test.tsx
index 5c82c1d919..bd532c80ee 100644
--- a/examples/schema-catalog/test/layout-dom-leak-5574.test.tsx
+++ b/examples/schema-catalog/test/layout-dom-leak-5574.test.tsx
@@ -103,7 +103,11 @@ const NODE_CENSUS: Readonly 27 with objectui#11070 round 10: the new `fields-grid/line-items-grid`
+ // fixture's form field entry is `type: 'grid'`, which this structural walk
+ // collects, as it collects the other `fields-grid` entries. Catalog
+ // authoring, not a renderer change.
+ grid: { rendered: 27, noElement: 0 },
// 699 -> 702: the two components-layout-box exemplars author 3 text nodes
// (objectui#3965). 702 -> 701 and 176 -> 162 with objectui#6942, and the two
// moves have different causes: `components-basic-text/muted.json` was deleted
@@ -129,7 +133,12 @@ const NODE_CENSUS: Readonly `box` before/after the migration guide was missing, and its
// two `text` nodes both carry a className, so both need an element — the
// no-element count is unmoved. Catalog authoring, not a renderer change.
- text: { rendered: 698, noElement: 154 },
+ // 698 -> 699 and 154 -> 155 with objectui#11070 round 10: the new
+ // `fields-grid/line-items-grid` fixture's `product` grid column is
+ // `type: 'text'`, which this structural walk collects as a `text` node;
+ // rendered on its own it draws no element. Catalog authoring, not a
+ // renderer change.
+ text: { rendered: 699, noElement: 155 },
// The objectui#3965 migration population (80 nodes retyped from `div`) plus
// the 4 nodes of the components-layout-box exemplars. `box` is born on
// `toDomProps` and class-transparency, so it joins the measured set as a
diff --git a/examples/schema-catalog/test/svg-host-dom-leak-5632.test.tsx b/examples/schema-catalog/test/svg-host-dom-leak-5632.test.tsx
index 0c4cf69d88..1e801b4145 100644
--- a/examples/schema-catalog/test/svg-host-dom-leak-5632.test.tsx
+++ b/examples/schema-catalog/test/svg-host-dom-leak-5632.test.tsx
@@ -115,7 +115,11 @@ const MEASURED_TYPES = ['icon', 'spinner', 'grid'] as const;
const NODE_CENSUS: Readonly> = {
icon: { rendered: 71, noElement: 0 },
spinner: { rendered: 6, noElement: 0 },
- grid: { rendered: 26, noElement: 0 },
+ // 26 -> 27 with objectui#11070 round 10: the new `fields-grid/line-items-grid`
+ // fixture's form field entry is `type: 'grid'`, which this structural walk
+ // collects, as it collects the other `fields-grid` entries. Catalog
+ // authoring, not a renderer change.
+ grid: { rendered: 27, noElement: 0 },
};
function collect(node: unknown, out: Node[] = []): Node[] {
diff --git a/packages/fields/src/widgets/GridField.declaredSpelling.test.tsx b/packages/fields/src/widgets/GridField.declaredSpelling.test.tsx
index 6ad5c1d18c..a69cce177a 100644
--- a/packages/fields/src/widgets/GridField.declaredSpelling.test.tsx
+++ b/packages/fields/src/widgets/GridField.declaredSpelling.test.tsx
@@ -10,8 +10,8 @@
* objectui#3951 — grid columns have ONE key spelling, and it is the declared
* one: `name`, the key of `GridFieldMetadata['columns']` (`@object-ui/types`),
* which is `@objectstack/spec`'s inline grid column by reference since
- * objectui#11070 — the same key the grid docs page and the three `fields-grid`
- * catalog examples author.
+ * objectui#11070 — the same key the grid docs page and every `fields-grid`
+ * catalog example author.
*
* `GridField` used to declare its own local column interface keyed by `field`
* and read `c.field` everywhere — `key={c.field}`, `row[c.field]`,
diff --git a/packages/types/src/__tests__/form-field-zod-coverage.test.ts b/packages/types/src/__tests__/form-field-zod-coverage.test.ts
index 81bdaae391..6a6dbc84fa 100644
--- a/packages/types/src/__tests__/form-field-zod-coverage.test.ts
+++ b/packages/types/src/__tests__/form-field-zod-coverage.test.ts
@@ -97,6 +97,16 @@ const DECLARED_KEYS = [
// objectui#11070 round 7 — the `grid` widget's columns: the spec's
// `inlineColumns` list (its strict inline grid column), by reference.
'columns',
+ // objectui#11070 round 10 — the `grid` widget's field-level keys, the
+ // members of `GridFieldMetadata` (the TS twin carries each by reference).
+ 'min_rows',
+ 'max_rows',
+ 'allow_add',
+ 'allow_delete',
+ 'allow_reorder',
+ 'total_field',
+ 'add_label',
+ 'sort_field',
];
describe('FormFieldSchema covers the FormField contract', () => {
diff --git a/packages/types/src/__tests__/strict-face-read-keys-11070.test.ts b/packages/types/src/__tests__/strict-face-read-keys-11070.test.ts
index c2493dda6b..5228331610 100644
--- a/packages/types/src/__tests__/strict-face-read-keys-11070.test.ts
+++ b/packages/types/src/__tests__/strict-face-read-keys-11070.test.ts
@@ -23,9 +23,11 @@
* - `form.fields[]` — field metadata a hand-authored form writes on the entry
* itself (`multiple`, `rows`, `accept`, `dimensions`, `reference`, `min`,
* `max`, `minLength`, `maxLength`, `pattern`, since round 3
- * `returnType` and `summaryOperations`, and since round 7 the `grid`
- * widget's `columns`, the spec's `inlineColumns` list), which the
- * renderer hands each field widget as its metadata carrier;
+ * `returnType` and `summaryOperations`, since round 7 the `grid`
+ * widget's `columns`, the spec's `inlineColumns` list, and since round 10
+ * the `grid` widget's eight field-level keys, `GridFieldMetadata`'s own
+ * members), which the renderer hands each field widget as its metadata
+ * carrier;
* - `dataSource` on `object-grid`, `object-form`, `object-kanban`,
* `list-view`, `object-gantt`, `object-map`, `object-calendar` and, since
* round 7, `object-chart` — the spec's per-element binding, which each
@@ -64,6 +66,7 @@ import type {
} from '../objectql.js';
import type { FormField, FormSchema } from '../form.js';
import type {
+ BaseFieldMetadata,
EmailFieldMetadata,
FormulaFieldMetadata,
GridFieldMetadata,
@@ -108,6 +111,31 @@ const form = (field: Record, node: Record = {}
fields: [{ name: 'f', label: 'F', ...field }],
});
+/** A `grid` entry the way the `fields-grid` catalog fixtures write one. */
+const GRID_ENTRY = {
+ type: 'grid',
+ columns: [{ name: 'product', type: 'text' }, { name: 'amount', type: 'currency' }],
+};
+
+/**
+ * Round 10: the `grid` widget's field-level keys — every member
+ * `GridFieldMetadata` declares besides `columns` (round 7) and the base field
+ * keys — each with a value of its declared type, on a grid entry.
+ */
+const GRID_FIELD_KEYS = {
+ min_rows: 1,
+ max_rows: 20,
+ allow_add: false,
+ allow_delete: false,
+ allow_reorder: false,
+ total_field: 'amount',
+ add_label: 'Add line',
+ sort_field: 'position',
+} as const;
+
+const GRID_FIELD_KEY_CASES: ReadonlyArray]> = Object.entries(GRID_FIELD_KEYS)
+ .map(([key, value]) => [key, { ...GRID_ENTRY, [key]: value }] as const);
+
/* ── 1. accepted on the strict face; a misspelling beside it is not ─────── */
describe('objectui#11070 — the declared read keys parse on the strict face', () => {
@@ -132,6 +160,10 @@ describe('objectui#11070 — the declared read keys parse on the strict face', (
['summaryOperations', { type: 'summary', summaryOperations: { object: 'orders', field: 'amount', function: 'sum' } }],
// Round 7: the `grid` widget's columns, the spec's `inlineColumns` list.
['columns', { type: 'grid', columns: [{ name: 'qty', type: 'number' }, { name: 'sku' }] }],
+ // Round 10: the `grid` widget's field-level keys, `GridFieldMetadata`'s
+ // members, each on a grid entry. Every row was refused by name at the
+ // round's base (`fields.0.`), so each one is a refusal that flipped.
+ ...GRID_FIELD_KEY_CASES,
];
it.each(FIELD_CASES)('`fields[].%s` parses; a misspelled sibling is refused at the field', (key, field) => {
@@ -140,6 +172,15 @@ describe('objectui#11070 — the declared read keys parse on the strict face', (
.toEqual([`fields.0.${key}x`]);
});
+ it('a grid entry carrying all eight field-level keys at once parses on the strict face (round 10)', () => {
+ expect(issuesOf(StrictAnyComponentSchema, form({ ...GRID_ENTRY, ...GRID_FIELD_KEYS }))).toBeNull();
+ // The parsed entry KEEPS each value: the tolerant face used to strip them.
+ const parsed = AnyComponentSchema.safeParse(form({ ...GRID_ENTRY, ...GRID_FIELD_KEYS }));
+ expect(parsed.success).toBe(true);
+ expect(parsed.success && (parsed.data as { fields: Record[] }).fields[0])
+ .toMatchObject(GRID_FIELD_KEYS);
+ });
+
const BINDING = { object: 'task', filter: [{ field: 'project', operator: 'equals', value: 'acme' }] };
const BOUND_NODES: ReadonlyArray]> = [
['object-grid', { objectName: 'task' }],
@@ -189,6 +230,17 @@ describe('objectui#11070 — a declared key is judged by its declared type on bo
['a grid column `defaultValue` (the spec column declares none, and the grid reads none)', form({ type: 'grid', columns: [{ name: 'qty', defaultValue: 1 }] })],
['a `scale` on a column declaring `type: \'currency\'` (the spec refuses it there)', form({ type: 'grid', columns: [{ name: 'amount', type: 'currency', scale: 2 }] })],
['a bare-object `columns` (the spec types it as an array)', form({ type: 'grid', columns: { name: 'qty' } })],
+ // Round 10: each grid field-level key is judged by `GridFieldMetadata`'s
+ // type. At the round's base the tolerant face STRIPPED every one of these
+ // and accepted the document, so each row is red there.
+ ['a string `min_rows`', form({ ...GRID_ENTRY, min_rows: '1' })],
+ ['a string `max_rows`', form({ ...GRID_ENTRY, max_rows: '20' })],
+ ['a string `allow_add`', form({ ...GRID_ENTRY, allow_add: 'false' })],
+ ['a string `allow_delete`', form({ ...GRID_ENTRY, allow_delete: 'false' })],
+ ['a string `allow_reorder`', form({ ...GRID_ENTRY, allow_reorder: 'false' })],
+ ['a numeric `total_field`', form({ ...GRID_ENTRY, total_field: 3 })],
+ ['a numeric `add_label`', form({ ...GRID_ENTRY, add_label: 1 })],
+ ['a numeric `sort_field`', form({ ...GRID_ENTRY, sort_field: 0 })],
['a `null` `object-chart` binding (the adapter placeholder the wrapper no longer writes)', { type: 'object-chart', properties: { objectName: 'task', chartType: 'bar' }, dataSource: null }],
['an adapter-shaped `object-chart` binding', { type: 'object-chart', properties: { objectName: 'task', chartType: 'bar' }, dataSource: 'objectstack' }],
];
@@ -315,6 +367,33 @@ export type assertionGridColumnsBySpecReference = [
Expect>,
Expect>,
];
+/**
+ * Round 10: the form-field face carries the `grid` widget's field-level keys
+ * BY REFERENCE to `GridFieldMetadata` — an exact match per key, so a restated
+ * value type fails here — and misses none of them: every member
+ * `GridFieldMetadata` declares beyond `BaseFieldMetadata` is a DECLARED member
+ * of `FormField` (not the index signature) with the same type. A key added to
+ * the grid's type and not mirrored on the form-field face fails to compile
+ * here; the zod side then follows through the `UnmirroredDeclared` ratchet in
+ * `zod-mirror-parity.test.ts` and the key set in `form-field-zod-coverage`.
+ */
+type DeclaredKeysOf = keyof { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K] };
+type GridFieldOwnKeys = Exclude;
+export type assertionGridFieldKeysOnTheFormFieldFace = [
+ // LIT CONTROL: the key set the two rows below range over is not empty, and
+ // it holds no base field key (an empty set would pass both in silence).
+ Expect, 'columns' | 'sort_field'>>,
+ Expect>, never>>,
+ Expect>,
+ Expect>,
+ Expect>,
+ Expect>,
+ Expect>,
+ Expect>,
+ Expect>,
+ Expect>,
+ Expect>,
+];
// @ts-expect-error objectui#11070 round 7 — `GridColumnDefinition` is RETIRED from `../field-types`: a grid column is the spec's `InlineGridColumn` (`GridFieldMetadata['columns']`).
type _GridColumnDefinitionRetiredFromTheModule = import('../field-types').GridColumnDefinition;
diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts
index 6de4898a8d..ae0ca549b5 100644
--- a/packages/types/src/__tests__/zod-mirror-parity.test.ts
+++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts
@@ -4765,7 +4765,11 @@ const SPEC_DERIVED_PAIRS: readonly string[] = [
// keys moves ONE side of this pair. The first spec reference in this mirror.
// Round 3 added `returnType` / `summaryOperations`, and round 7 the `grid`
// widget's `columns` (the spec's `inlineColumns` list, so a spec bump that
- // moves the inline grid column moves ONE side too).
+ // moves the inline grid column moves ONE side too). Round 10 added the `grid`
+ // widget's eight field-level keys (`min_rows` … `sort_field`); those are
+ // LOCAL on both sides (the TS twin is `GridFieldMetadata`'s member by
+ // reference, and the spec declares none of them), so they add no spec
+ // crossing and leave this membership resting on the members above.
'form.zod.ts#FormFieldSchema',
'form.zod.ts#SelectOptionSchema',
'layout.zod.ts#PageNodeSchema',
diff --git a/packages/types/src/form.ts b/packages/types/src/form.ts
index 38f72d5071..0108933915 100644
--- a/packages/types/src/form.ts
+++ b/packages/types/src/form.ts
@@ -17,6 +17,7 @@
import type { BaseSchema, SchemaNode } from './base.js';
import type { SelectOptionBase } from './select-option.js';
+import type { GridFieldMetadata } from './field-types.js';
// objectui#11070 — the field-metadata members `FormField` declares by
// reference to the spec's `FieldSchema`. Type-only: no runtime edge.
import type { Field as SpecField } from '@objectstack/spec/data';
@@ -2048,6 +2049,33 @@ export interface FormField {
* field is objectui's own type, whose key is `columns` (objectui#11070).
*/
columns?: SpecField['inlineColumns'];
+
+ // ── The `grid` widget's field-level keys (objectui#11070 round 10) ──────
+ //
+ // A `form` `fields[]` entry of `type: 'grid'` is the authored path to the
+ // `grid` widget, which reads its field-level keys off that entry (its
+ // metadata carrier, as for `columns` above). Each member below is
+ // `GridFieldMetadata`'s own member BY REFERENCE, so the form-field face and
+ // the grid field's published type cannot drift; the meaning of each key is
+ // documented there. Only the `grid` widget reads them: on any other field
+ // type they are accepted and read by nothing.
+
+ /** The grid's minimum row count: {@link GridFieldMetadata.min_rows}. */
+ min_rows?: GridFieldMetadata['min_rows'];
+ /** The grid's maximum row count: {@link GridFieldMetadata.max_rows}. */
+ max_rows?: GridFieldMetadata['max_rows'];
+ /** Whether the grid offers Add (on unless `false`): {@link GridFieldMetadata.allow_add}. */
+ allow_add?: GridFieldMetadata['allow_add'];
+ /** Whether the grid offers Delete (on unless `false`): {@link GridFieldMetadata.allow_delete}. */
+ allow_delete?: GridFieldMetadata['allow_delete'];
+ /** Whether rows can be drag-reordered (on unless `false`): {@link GridFieldMetadata.allow_reorder}. */
+ allow_reorder?: GridFieldMetadata['allow_reorder'];
+ /** The CHILD column summed into the footer total: {@link GridFieldMetadata.total_field}. */
+ total_field?: GridFieldMetadata['total_field'];
+ /** The Add button's label: {@link GridFieldMetadata.add_label}. */
+ add_label?: GridFieldMetadata['add_label'];
+ /** The row field stamped with each row's index: {@link GridFieldMetadata.sort_field}. */
+ sort_field?: GridFieldMetadata['sort_field'];
}
/**
diff --git a/packages/types/src/zod/form.zod.ts b/packages/types/src/zod/form.zod.ts
index 8a7ad167e2..51309b7176 100644
--- a/packages/types/src/zod/form.zod.ts
+++ b/packages/types/src/zod/form.zod.ts
@@ -1067,6 +1067,27 @@ export const FormFieldSchema = z.object({
// `name`-keyed inline grid column), by reference. objectui's `grid` field
// type spells the list `columns` (objectui#11070).
columns: stripImportedDefaults(SpecFieldSchema).shape.inlineColumns,
+ // objectui#11070 round 10 — the `grid` widget's field-level keys, which it
+ // reads off a `fields[]` entry of `type: 'grid'` (its metadata carrier). They
+ // are `GridFieldMetadata`'s members, which the TS twin carries by reference;
+ // `@objectstack/spec` declares none of them (a `grid` field is objectui's own
+ // type), so each value schema here is the TS member's own type, stated once.
+ min_rows: z.number().optional()
+ .describe('Minimum row count of a `grid` field; read only by the `grid` widget'),
+ max_rows: z.number().optional()
+ .describe('Maximum row count of a `grid` field; read only by the `grid` widget'),
+ allow_add: z.boolean().optional()
+ .describe('Whether a `grid` field offers Add (on unless false); read only by the `grid` widget'),
+ allow_delete: z.boolean().optional()
+ .describe('Whether a `grid` field offers Delete (on unless false); read only by the `grid` widget'),
+ allow_reorder: z.boolean().optional()
+ .describe('Whether a `grid` field\'s rows can be drag-reordered (on unless false); read only by the `grid` widget'),
+ total_field: z.string().optional()
+ .describe('Name of the CHILD column a `grid` field sums into its footer total; read only by the `grid` widget'),
+ add_label: z.string().optional()
+ .describe('Label of a `grid` field\'s Add button; read only by the `grid` widget'),
+ sort_field: z.string().optional()
+ .describe('Name of the row field a `grid` field stamps with each row\'s index, so a drag-reorder persists; read only by the `grid` widget'),
}).superRefine((field, ctx) => {
// objectui#5449 — the namespace rule `@object-ui/core` has enforced since
// objectui#5375, stated here so `objectui validate` (which reaches this