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