diff --git a/.changeset/10872-flat-arm-responsive-styles.md b/.changeset/10872-flat-arm-responsive-styles.md index f0460e5f7a..5fc1b5eba5 100644 --- a/.changeset/10872-flat-arm-responsive-styles.md +++ b/.changeset/10872-flat-arm-responsive-styles.md @@ -18,3 +18,5 @@ - The tolerant face (`safeValidateSchema`, the face `objectui validate` runs) used to keep any node-level `responsiveStyles` value on these arms unjudged. It now refuses a value the spec refuses, at `responsiveStyles`, with the spec's own issue: a breakpoint the spec does not have (`md`), a number instead of a map, or a style value that is neither a string nor a number. `SchemaRenderer` applied nothing for a number or for a map with no spec breakpoint. It emitted a non-CSS value inside a valid breakpoint verbatim, where the browser dropped it, and it applied the valid breakpoints of a mixed map. - In TypeScript, a `responsiveStyles` value that is not the spec's `ResponsiveStyles` no longer type-checks on these three interfaces. - An `object-view`'s `table` slot, which takes `ObjectGridSchema`'s members, does not gain the key. `ObjectView` draws its grid as a component, not as a schema node, so nothing compiles a `responsiveStyles` map written in `table`. The slot therefore refuses `table.responsiveStyles` by name, with that reason, on both faces, as it refuses the other node-level keys (objectui#10976). The tolerant face used to keep it unjudged, and the TypeScript slot never declared it. + +⚠️ **Dated note, 2026-10-01 — `object-chart` declares `dataSource` — objectui#11070.** "`dataSource` on `flex` and `object-chart`" in the list of node-level keys that stay undeclared held when this change landed. Later in this same release objectui#11070 (round 7) declared `dataSource` on `object-chart`, as the spec's `ElementDataSourceSchema` by reference; `flex` is unchanged. `.changeset/11070-grid-columns-chart-binding-round7.md` states what ships; the text above is kept as the reading of this change. diff --git a/.changeset/11070-grid-columns-chart-binding-round7.md b/.changeset/11070-grid-columns-chart-binding-round7.md new file mode 100644 index 0000000000..447c47f1c1 --- /dev/null +++ b/.changeset/11070-grid-columns-chart-binding-round7.md @@ -0,0 +1,23 @@ +--- +'@object-ui/types': minor +'@object-ui/fields': minor +--- + +The grid field's `columns` is `@objectstack/spec`'s inline grid column list, by reference, and `object-chart` declares the per-element `dataSource` binding like the other gate-wrapped blocks (objectui#11070, round 7). + +- **Grid columns (`@object-ui/types`).** `GridFieldMetadata.columns` and the form-field face's `FormField.columns` (TypeScript and the zod mirror `FormFieldSchema`) are `FieldSchema.inlineColumns` by reference: an array of the spec's strict, `name`-keyed inline grid column (`InlineGridColumn`), which the spec declares as the mirror of the `grid` widget's column. The key stays `columns`: a `grid` field is objectui's own field type, and the spec spells the same list `inlineColumns` on a `master_detail` field. +- **`GridColumnDefinition` is retired (`@object-ui/types`).** Nothing read it, and it was not the shape the widget read: it required a free-form `type` and declared `defaultValue` and `validate`, which no reader consumed. Use `InlineGridColumn` from `@objectstack/spec/data`, or `NonNullable[number]`. +- **`GridColumn` (`@object-ui/fields`)** is `InlineGridColumn` by reference instead of a hand-written copy. `GridField` already read each column by exactly the spec's keys (twenty, with no second spelling), so nothing changes at render time. +- **`object-chart` · `dataSource` (`@object-ui/types`).** `ObjectChartSchema` (TypeScript and the zod mirror) and the authored arm `ObjectChartBlockSchema` declare `dataSource` as the spec's `ElementDataSourceSchema`, by reference, at node level beside the `properties` bag, as `ObjectFormBlockSchema` and `ObjectMapBlockSchema` do. The registration is gate-wrapped, so `ElementDataSourceGate` reads the binding off the node and lands its `object` on `objectName`; the react-page wrapper no longer writes the host adapter under that key (objectui#11070, round 2). + +**Clause-②: yes (narrowing).** The strict face's accept set widens: a grid field's `columns` and an `object-chart` node's `dataSource` binding used to be refused by name there and now parse. The tolerant face narrows, because both keys are now judged by their declared type on both faces: + +- a grid column the spec refuses is refused: the retired `field` spelling, a `title`, a per-column `defaultValue`, a `type` outside the nine cell controls (`text`, `number`, `currency`, `date`, `datetime`, `time`, `select`, `lookup`, `file`), or a `scale` on a column declaring `type: 'currency'`. `FormFieldSchema` strips an undeclared key, so before this change such a column list was dropped from the parsed field in silence; +- an `object-chart` node whose `dataSource` is not a binding is refused, `null` or an adapter object included. The authored arm is `.passthrough()`, so before this change that value was kept unjudged. + +## ⚠️ BREAKING, priced as minor under the fixed group's version policy + +- **TypeScript.** `import type { GridColumnDefinition } from '@object-ui/types'` no longer resolves. A grid column literal typed `GridFieldMetadata['columns']`, `FormField['columns']` or `GridColumn` that carries a key the spec column does not declare, or a `type` outside the nine, is a compile error. A `readonlyWhen` / `requiredWhen` written as an object must name its `dialect`, as the spec's expression envelope does. An `ObjectChartSchema` literal whose `dataSource` holds an adapter is a compile error; pass the adapter as the component's `dataSource` prop, or through `SchemaRendererProvider`. +- **Validation.** The documents above that the tolerant face (`safeValidateSchema`, and so `objectui validate`) accepted are refused. Fix: write the spec's column keys (`name`, `label`, `type`, …), or drop a key the column does not have; write a binding (`{ "object": "…" }`) or no `dataSource` on an `object-chart` node. + +Rendering does not change: `GridField` reads the same keys as before, and `ElementDataSourceGate` ignores a value that is not a binding. diff --git a/.changeset/11070-strict-face-read-keys.md b/.changeset/11070-strict-face-read-keys.md index e6acc55e51..fbf99d9f41 100644 --- a/.changeset/11070-strict-face-read-keys.md +++ b/.changeset/11070-strict-face-read-keys.md @@ -11,3 +11,5 @@ The strict authoring face accepts keys a registered renderer reads, which it use **What now refuses that did not.** A declared key is judged by its declared type on BOTH faces. On `FormFieldSchema`, which strips undeclared keys, a wrong-typed value for one of the ten field keys used to be dropped silently and is now refused (for example `accept: "application/pdf"`, since the spec types `accept` as an array, or `rows: 0`). On the passthrough nodes, a wrong-typed `showSubmit` or `dataSource` used to be kept unjudged and is now refused. That includes a `dataSource` holding the host's adapter object or `null`, which is what the react-page wrapper writes on the nodes it builds in memory: those nodes are rendered, not validated, and `ElementDataSourceGate` ignores a value that is not a binding, so their rendering does not change. An `object-view`'s `table` slot now refuses `dataSource` by name, like the other record sources the view owns (`data`, `staticData`, `bind`), because the view does not hand it to its grid; its `form` slot carries `dataSource` as it carries `bind` and `data`. On the TypeScript face these keys are now typed members rather than the `[key: string]: any` index signature, so a wrong-typed value is a compile error. Measured over the schema catalog, the docs JSON fences and the apps' authored documents, at this change's base (`88fbd793d`) and on this change: the tolerant face refused the same documents both times. **What is deliberately NOT declared.** The legacy spellings `reference_to` and `min_length` stay refused on the strict face; write the spec's `reference` and `minLength`. `return_type`, `summary_type`, the grid field's `columns`, `object-chart`'s `dataSource` and the dashboard widget keys stay refused until objectui#11070 settles them. + +⚠️ **Dated note, 2026-10-01 — the grid field's `columns` and `object-chart`'s `dataSource` are settled — objectui#11070.** "the grid field's `columns`, `object-chart`'s `dataSource` … stay refused until objectui#11070 settles them" above held when this change landed. Later in this same release round 7 of objectui#11070 declared both: `columns` as the spec's `FieldSchema.inlineColumns` and the binding as the spec's `ElementDataSourceSchema`, each by reference. `.changeset/11070-grid-columns-chart-binding-round7.md` states what ships; the text above is kept as the reading of this change. diff --git a/.changeset/11276-object-chart-properties-bag.md b/.changeset/11276-object-chart-properties-bag.md index 7de0a8b9c3..3842a12d80 100644 --- a/.changeset/11276-object-chart-properties-bag.md +++ b/.changeset/11276-object-chart-properties-bag.md @@ -27,3 +27,5 @@ Nothing changes at render time: `SchemaRenderer` hoists every `properties` key o **The same release's `.changeset/10518-object-chart-y-axis-declared.md`, `.changeset/10608-object-chart-legacy-axis-keys-retired.md`, `.changeset/10770-object-chart-react-tier-node.md` and `.changeset/10872-flat-arm-responsive-styles.md`** describe what the validators say about an `object-chart` written flat. On an authored node those readings now happen in the bag: a malformed `xAxis` is refused at `properties.xAxis`, a retired key at `properties.KEY`, a missing family at `properties.chartType`; `responsiveStyles` stays node-level and is judged there as they say. The flat mirror they name still behaves as they say. **What did not move.** The TypeScript `ObjectChartSchema` and its zod mirror `ObjectChartSchema` stay published and unchanged in shape. They are the node as `ObjectChart` reads it after the hoist, and as code composes it or hands it to `` directly. + +⚠️ **Dated note, 2026-10-01 — `dataSource` is declared on this node — objectui#11070.** "`dataSource` stays undeclared on this node, as objectui#11070 left it" above held when this change landed. Later in this same release objectui#11070 (round 7) declared it on `ObjectChartSchema` and on `ObjectChartBlockSchema`, at node level beside the bag, as the spec's `ElementDataSourceSchema` by reference: a binding parses on both faces, and a `dataSource` that is not a binding is refused. `.changeset/11070-grid-columns-chart-binding-round7.md` states what ships; the text above is kept as the reading of this change. diff --git a/.changeset/6138-fields-schema-block-parity-pr2.md b/.changeset/6138-fields-schema-block-parity-pr2.md index c13855e8e8..51d13451d2 100644 --- a/.changeset/6138-fields-schema-block-parity-pr2.md +++ b/.changeset/6138-fields-schema-block-parity-pr2.md @@ -55,3 +55,5 @@ The gate's blocks-to-compile count rises from 248 to 249 — 21 conversions are one-block-for-one-block and `lookup.mdx` becomes two blocks (data-source-backed and static-option) — with diagnostics at 0, no new `FRAGMENT_MARKER` declarations, and the declared-fragment count unmoved at 111. + +⚠️ **Dated note, 2026-10-01 — `GridColumnDefinition` is retired — objectui#11070.** The two mentions of `GridColumnDefinition` above held when this change landed. Later in this same release objectui#11070 (round 7) retired it: `GridFieldMetadata.columns` is `@objectstack/spec`'s `FieldSchema.inlineColumns` by reference, and `grid.mdx` annotates against that. The spec's inline grid column is closed, so the page still cannot teach a column key the type does not have, and it declares neither a column `editable` nor a string `width`. `.changeset/11070-grid-columns-chart-binding-round7.md` states what ships; the text above is kept as the reading of this change. diff --git a/.changeset/8209-datetime-widget-faces.md b/.changeset/8209-datetime-widget-faces.md index be8324ed87..4a50dba5c0 100644 --- a/.changeset/8209-datetime-widget-faces.md +++ b/.changeset/8209-datetime-widget-faces.md @@ -48,3 +48,5 @@ stored string for an unreadable value, unchanged (objectui#3569). With this, objectui#7443's "`datetime` has one home" holds for all six bare no-bag sites objectui#8194 enumerated. + +⚠️ **Dated note, 2026-10-01 — `GridColumn` is the spec's inline grid column — objectui#11070.** "(`GridColumn`, mirroring the published `GridColumnDefinition`)" above held when this change landed. Later in this same release objectui#11070 (round 7) retired `GridColumnDefinition` and made `GridColumn` `@objectstack/spec`'s `InlineGridColumn` by reference. That shape declares no `format` key either, so the reasoning above is unchanged. `.changeset/11070-grid-columns-chart-binding-round7.md` states what ships; the text above is kept as the reading of this change. diff --git a/content/docs/fields/grid.mdx b/content/docs/fields/grid.mdx index 54729ef513..6f8f3f75f5 100644 --- a/content/docs/fields/grid.mdx +++ b/content/docs/fields/grid.mdx @@ -21,8 +21,11 @@ The Grid Field component provides an inline table for managing related records o A grid field is authored as `GridFieldMetadata` (`@object-ui/types`), which is the source of truth for the key set: it extends `BaseFieldMetadata` with the column list -and the row-count and row-action limits. Each column is a `GridColumnDefinition`, so -the columns are checked by the same compiler that checks the field. +and the row-count and row-action limits. Each column is `@objectstack/spec`'s inline +grid column (`InlineGridColumn`, the element of the spec's `inlineColumns` list on a +`master_detail` field), typed by reference, so the columns are checked by the same +compiler that checks the field, and `objectui validate` refuses a column key the spec +does not declare. ```ts import type { GridFieldMetadata } from '@object-ui/types'; @@ -33,7 +36,7 @@ const lineItems: GridFieldMetadata = { label: 'Line Items', columns: [ { name: 'product', label: 'Product', type: 'lookup', required: true, width: 240 }, - { name: 'quantity', label: 'Qty', type: 'number', defaultValue: 1, width: 80 }, + { name: 'quantity', label: 'Qty', type: 'number', width: 80 }, { name: 'unit_price', label: 'Unit Price', type: 'currency', width: 120 }, ], min_rows: 1, @@ -44,15 +47,18 @@ const lineItems: GridFieldMetadata = { }; ``` -A column's `width` is a **number** of pixels, and there is no per-column `editable` -key: whether cells can be edited follows the field's own read-only state. +A column's `width` is a **number** of pixels. There is no per-column `editable` +key (whether cells can be edited follows the field's own read-only state) and no +per-column `defaultValue`: a new row starts with every cell empty. The value being edited, and the `className` / `disabled` a host supplies, are **not** metadata keys — they are runtime widget props. See [Field Widget Props](/docs/fields/widget-props). ## Column Types -Columns can use any field type: +A column's `type` is one of the spec's nine cell controls: `text`, `number`, +`currency`, `date`, `datetime`, `time`, `select`, `lookup` and `file`. Any other +value is refused. ```plaintext columns: [ @@ -61,7 +67,6 @@ columns: [ { name: 'price', label: 'Price', type: 'currency' }, { name: 'date', label: 'Date', type: 'date' }, { name: 'status', label: 'Status', type: 'select', options: [...] }, - { name: 'active', label: 'Active', type: 'boolean' }, { name: 'receipt', label: 'Receipt', type: 'file', accept: ['image/*', '.pdf'] } ] ``` @@ -134,7 +139,7 @@ const gridValue = [ { name: 'product', label: 'Product', type: 'lookup', reference: 'products' }, { name: 'quantity', label: 'Qty', type: 'number' }, { name: 'price', label: 'Price', type: 'currency' }, - { name: 'discount', label: 'Discount', type: 'percent' }, + { name: 'discount', label: 'Discount', type: 'number' }, { name: 'total', label: 'Total', type: 'currency' } ] } @@ -149,9 +154,9 @@ const gridValue = [ label: 'Tasks', columns: [ { name: 'task', label: 'Task', type: 'text', required: true }, - { name: 'assigned_to', label: 'Assigned To', type: 'user' }, + { name: 'assigned_to', label: 'Assigned To', type: 'lookup', reference: 'sys_user' }, { name: 'due_date', label: 'Due Date', type: 'date' }, - { name: 'completed', label: 'Done', type: 'boolean' } + { name: 'completed', label: 'Done', type: 'select', options: [{ label: 'Yes', value: 'true' }, { label: 'No', value: 'false' }] } ] } ``` @@ -248,7 +253,8 @@ await db.insert('orders', order); Example validation for grid data: ```plaintext -const validateGridData = (data: any[], columns: ColumnDefinition[]) => { +// InlineGridColumn: the spec's grid column type (@objectstack/spec/data) +const validateGridData = (data: any[], columns: InlineGridColumn[]) => { const errors: string[] = []; data.forEach((row, index) => { @@ -262,11 +268,6 @@ const validateGridData = (data: any[], columns: ColumnDefinition[]) => { if (col.type === 'number' && isNaN(row[col.name])) { errors.push(`Row ${index + 1}: ${col.label} must be a number`); } - - // Check min/max - if (col.min !== undefined && row[col.name] < col.min) { - errors.push(`Row ${index + 1}: ${col.label} must be >= ${col.min}`); - } }); }); diff --git a/packages/fields/src/widgets/GridField.declaredSpelling.test.tsx b/packages/fields/src/widgets/GridField.declaredSpelling.test.tsx index 85ad794294..6ad5c1d18c 100644 --- a/packages/fields/src/widgets/GridField.declaredSpelling.test.tsx +++ b/packages/fields/src/widgets/GridField.declaredSpelling.test.tsx @@ -8,8 +8,10 @@ /** * objectui#3951 — grid columns have ONE key spelling, and it is the declared - * one: `GridColumnDefinition.name` (`@object-ui/types`), the same key the grid - * docs page and the three `fields-grid` catalog examples author. + * 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. * * `GridField` used to declare its own local column interface keyed by `field` * and read `c.field` everywhere — `key={c.field}`, `row[c.field]`, @@ -19,9 +21,9 @@ * column (the key was `undefined`). The three demos on `/docs/fields/grid` * shipped in exactly that state. * - * The fixtures below are typed as `GridColumnDefinition[]` on purpose: the pin - * is anchored to the DECLARED contract, not to the widget's own idea of it, so - * the two can never silently drift apart again. Per AGENTS.md #0.1 the fix is + * The fixtures below are typed as `GridFieldMetadata['columns']` on purpose: + * the pin is anchored to the DECLARED contract, not to the widget's own idea + * of it, so the two can never silently drift apart again. Per AGENTS.md #0.1 the fix is * one spelling at the producer — there is deliberately no `c.field ?? c.name` * alias to make the retired spelling keep working. * @@ -37,11 +39,11 @@ import { describe, it, expect, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import React from 'react'; -import type { GridColumnDefinition, GridFieldMetadata } from '@object-ui/types'; +import type { GridFieldMetadata } from '@object-ui/types'; import { GridField } from './GridField'; -/** Authored exactly as `GridColumnDefinition` declares — keyed by `name`. */ -const columns: GridColumnDefinition[] = [ +/** Authored exactly as `GridFieldMetadata['columns']` declares — keyed by `name`. */ +const columns: NonNullable = [ { name: 'product', label: 'Product', type: 'text' }, { name: 'quantity', label: 'Qty', type: 'number' }, { name: 'price', label: 'Price', type: 'currency' }, @@ -56,7 +58,7 @@ const rows = [ const field = { type: 'grid', name: 'order_items', columns } as GridFieldMetadata; describe('GridField reads the DECLARED column spelling (objectui#3951)', () => { - it('renders every cell populated from metadata authored as GridColumnDefinition', () => { + it('renders every cell populated from metadata authored as the declared column type', () => { render( {}} field={field} />); // One cell input per column per row, each echoing the row's stored value — diff --git a/packages/fields/src/widgets/GridField.keyWarning.test.tsx b/packages/fields/src/widgets/GridField.keyWarning.test.tsx index 81e1acb01f..5c3cd7b149 100644 --- a/packages/fields/src/widgets/GridField.keyWarning.test.tsx +++ b/packages/fields/src/widgets/GridField.keyWarning.test.tsx @@ -8,7 +8,7 @@ /** * objectui#3951, diagnostics half — a grid authored in the declared spelling - * (`GridColumnDefinition.name`) must render without React's missing-key + * (`name`, the key of `GridFieldMetadata['columns']`) must render without React's missing-key * warning. While `GridField` read a divergent `field` key, every header, chip * and cell was emitted with `key={undefined}`, so a spec-compliant grid logged * the warning on top of rendering blank cells. @@ -30,10 +30,10 @@ import { describe, it, expect, vi } from 'vitest'; import { render } from '@testing-library/react'; import React from 'react'; -import type { GridColumnDefinition, GridFieldMetadata } from '@object-ui/types'; +import type { GridFieldMetadata } from '@object-ui/types'; import { GridField } from './GridField'; -const columns: GridColumnDefinition[] = [ +const columns: NonNullable = [ { name: 'product', label: 'Product', type: 'text' }, { name: 'quantity', label: 'Qty', type: 'number' }, { name: 'price', label: 'Price', type: 'currency' }, diff --git a/packages/fields/src/widgets/GridField.tsx b/packages/fields/src/widgets/GridField.tsx index 719fc0b801..dc5b9cc15b 100644 --- a/packages/fields/src/widgets/GridField.tsx +++ b/packages/fields/src/widgets/GridField.tsx @@ -25,6 +25,7 @@ import { toDateInputValue, toDateTimeInputValue, fromDateTimeInputValue, isImpos import { useFieldTranslation } from './useFieldTranslation.js'; import { toDomProps } from './toDomProps.js'; import { toHostGroupProps } from './toHostGroupProps.js'; +import type { InlineGridColumn } from '@objectstack/spec/data'; /** * GridField / LineItemsField — editable child-grid ("line items") widget. @@ -72,118 +73,63 @@ import { toHostGroupProps } from './toHostGroupProps.js'; * column. This is the renderer for the `field:grid` widget and the cell * engine behind the master-detail subform (see ADR-0001). * - * Column config (a subset of `GridColumnDefinition`): - * { name, label?, type?, options?, width?, required?, prefix?, step? } - * type ∈ 'text' | 'number' | 'currency' | 'date' | 'datetime' | 'time' - * | 'select' | 'lookup' | 'file' + * Column config: `@objectstack/spec`'s inline grid column, by reference — + * see {@link GridColumn}. * * Field-level config (from `GridFieldMetadata`): * columns, min_rows, max_rows, allow_add, allow_delete, total_field */ -export interface GridColumn { - /** - * The column's field name — the key it reads and writes on each row object. - * - * Spelled `name`, exactly as the declared `GridColumnDefinition` - * (`@object-ui/types`) and the grid docs page say (objectui#3951). This - * widget used to read a divergent `field` key, so metadata authored against - * the published type rendered every cell empty plus a React key warning. - * There is deliberately no tolerant alias bridging the retired spelling to - * this one: a single spelling, enforced at the producer — AGENTS.md #0.1. - * - * (Wording note: do not restate that rule as an alternation expression over - * the two key names. `column-identity.ratchet.test.ts` (objectui#3104) scans - * these files line by line and cannot tell prose from code, so spelling the - * shape out here registers as a new dual read and fails the gate.) - */ - name: string; - label?: string; - /** - * Cell control + read/write adapter for the column. - * - * `date` / `datetime` / `time` are three DISTINCT controls, not one - * (objectui#3569). Collapsing `datetime` onto the `date` control did not - * merely under-render it — `` hands back a bare - * `YYYY-MM-DD` on change, so touching the day of a `datetime` cell silently - * DELETED its time component from the record. - */ - type?: 'text' | 'number' | 'currency' | 'date' | 'datetime' | 'time' | 'select' | 'lookup' | 'file'; - options?: Array<{ label: string; value: string }>; - width?: number; - required?: boolean; - /** - * Symbol shown in a `currency` cell IN PLACE OF the resolved currency's own - * symbol. When absent, the cell shows the symbol of the currency it - * resolves (objectui#10355) — there is no default symbol: this used to fall - * back to a literal `¥` whatever the column's currency was. - */ - prefix?: string; - step?: number; - /** For `type: 'lookup'` — the referenced object and label/id fields. */ - reference?: string; - displayField?: string; - idField?: string; - /** Multi-value column: multi-record lookup, or multi-file upload cell. */ - multiple?: boolean; - /** For `type: 'file'` — accepted MIME types / extensions for the picker - * (e.g. `['image/*', '.pdf']`). Omit to accept anything. */ - accept?: string[]; - /** - * Hidden from the grid by default but revealable via the column chooser. - * Set by `deriveColumns` for fields beyond the default-visible budget — the - * data is NOT dropped (it's just collapsed, like Odoo's `optional` columns / - * Salesforce column personalization), so business-critical fields stay - * reachable. Required columns are never default-hidden. - */ - defaultHidden?: boolean; - /** - * A computed (read-only) column whose value is derived live from sibling - * cells via {@link expr} — e.g. an invoice line's `amount = quantity * - * unit_price`. The grid renders it read-only, recomputes it as the row's - * inputs change, and writes the result back into the row so it persists - * (and any running total reflects it). The classic spreadsheet pattern used - * by QuickBooks / Stripe / NetSuite line grids — nobody types the amount. - */ - computed?: boolean; - /** Arithmetic expression for a {@link computed} column. Supports `+ - * / %`, - * parentheses, numeric literals and field refs (`record.qty` or bare `qty`). */ - expr?: string; - /** - * Decimal places to round a computed `number` result to — the spec's - * `InlineGridColumnSchema.scale`. - * - * ⛔ Not read on a `currency` column (objectui#10783). A currency amount's - * decimal places are its currency's: the resolved currency's ISO 4217 minor - * unit decides both the stored and the shown width (`currencyWidth`), and - * `@objectstack/spec` 17.5.0 refuses `scale` on an inline grid column that - * declares `type: 'currency'`, as it refuses it on the currency FIELD - * (ruling B on objectstack-ai/objectstack#19629, ruling 乙 on - * objectstack-ai/objectstack#19910). The spec cannot see a column that - * declares no `type` and takes `currency` from its child field at render - * time, so `hydrateColumns` in `@object-ui/plugin-form` reports a `scale` - * on such a column instead of letting it go unread in silence. - */ - scale?: number; - /** For `type: 'lookup'` — when a record is picked, copy its fields into any - * sibling columns of the same name (e.g. a product's unit_price/description). - * On by default for lookup columns; set `false` to disable the auto-fill. */ - autofill?: boolean; - /** - * CEL predicate: when TRUE for this row, the cell is **read-only** (B2 field - * rules, generalized to grid cells). Evaluated per row against the row as - * `record` plus the header as `parent` (so a line locks when - * `parent.status == 'paid'` *or* on an intra-row condition like - * `record.kind == 'auto'`). Client-side UX; fails open (stays editable). - */ - readonlyWhen?: string | { dialect?: string; source: string }; - /** - * CEL predicate: when TRUE for this row, the cell is **required** (flagged - * inline-invalid while empty). Same `record` + `parent` scope as - * {@link readonlyWhen}. - */ - requiredWhen?: string | { dialect?: string; source: string }; -} +/** + * One grid column — `@objectstack/spec`'s inline grid column + * (`InlineGridColumn`, the element of `FieldSchema.inlineColumns`), BY + * REFERENCE (objectui#11070). The spec declares that shape as the strict + * mirror of this widget's column, so the widget's type IS the spec's: one + * declaration, the same one `GridFieldMetadata.columns` and + * `FormField.columns` (`@object-ui/types`) carry. This widget reads each + * column by exactly the spec's keys — every key below is one the spec + * declares, and there is no second spelling of any of them. + * + * How the widget reads the keys (the spec's own descriptions say what each + * MEANS; these are the renderer's notes): + * + * - `name` — the key a column reads and writes on each row object. There is + * deliberately no tolerant alias bridging the spelling objectui#3951 + * retired to this one: a single spelling, enforced at the producer — + * AGENTS.md #0.1 (the spec refuses the retired spelling by name). + * (Wording note: do not restate that rule as an alternation expression over + * the two key names. `column-identity.ratchet.test.ts` (objectui#3104) scans + * these files line by line and cannot tell prose from code, so spelling the + * shape out here registers as a new dual read and fails the gate.) + * - `type` — the cell control and its read/write adapter. `date` / + * `datetime` / `time` are three DISTINCT controls, not one + * (objectui#3569): `` hands back a bare `YYYY-MM-DD` on + * change, so collapsing `datetime` onto it silently DELETED the time + * component from the record. + * - `prefix` — a symbol shown in a `currency` cell IN PLACE OF the resolved + * currency's own symbol. When absent, the cell shows the symbol of the + * currency it resolves (objectui#10355); there is no default symbol. + * - `defaultHidden` — collapsed into the column chooser, not dropped + * (`deriveColumns` sets it beyond the default-visible budget). Required + * columns are never default-hidden. + * - `computed` + `expr` — a read-only column recomputed live from sibling + * cells by this file's own safe arithmetic evaluator (`+ - * / %`, + * parentheses, numeric literals, `record.qty` or bare `qty`), and written + * back into the row so it persists and any running total reflects it. + * - `scale` — decimal places for a computed `number` result. ⛔ Not read on + * a `currency` column (objectui#10783): the resolved currency's ISO 4217 + * minor unit decides both the stored and the shown width + * (`currencyWidth`), and the spec refuses `scale` on a column declaring + * `type: 'currency'`. A column that takes `currency` from its child field + * at render time is reported by `hydrateColumns` in + * `@object-ui/plugin-form` instead of being read in silence. + * - `autofill` — for a `lookup` column: picking a record copies its fields + * into sibling columns of the same name. On unless set `false`. + * - `readonlyWhen` / `requiredWhen` — CEL predicates evaluated per row + * against the row as `record` plus the header as `parent`. Client-side UX; + * a predicate that faults fails open. + */ +export type GridColumn = InlineGridColumn; type Row = Record; @@ -341,9 +287,9 @@ export function lookupAutofillPatch(columns: GridColumn[], col: GridColumn, reco * `defaultCurrency` → the tenant default) — ⛔ never a second copy of it. * * The field-level legs are handed nothing, deliberately: a grid column - * declares none of those keys — not `GridColumn`, not `GridColumnDefinition` - * in `@object-ui/types`, and not the spec's strict `InlineGridColumnSchema`, - * which refuses them — and the column derivation in `@object-ui/plugin-form` + * declares none of those keys — not `GridColumn`, which is the spec's strict + * `InlineGridColumnSchema` element by reference, and that schema refuses + * them — and the column derivation in `@object-ui/plugin-form` * copies none of them from the child field. Reading them off the column would * add a renderer read that no authored metadata can reach. So the precedence * lands on the tenant default, and `undefined` when none is configured: the @@ -491,7 +437,7 @@ function temporalText(type: string | undefined, value: any, locale: string): str // ⛔ The style is a LITERAL, not an authored read, and that departs from the // call shape the ruling wrote (`field.format ?? 'compact'`). It has to: // `temporalText` is handed a column `type`, and the `GridColumn` its caller - // holds — like the published `GridColumnDefinition` it mirrors — declares no + // holds — the spec's inline grid column, by reference — declares no // `format` key at all, so there is nothing here to reuse the way // `DateTimeCellRenderer` reuses `DateTimeFieldMetadata.format`. Spelling the // read anyway would mean DECLARING that key, which the same ruling forbids diff --git a/packages/plugin-form/src/deriveMasterDetail.declaredSpelling.test.tsx b/packages/plugin-form/src/deriveMasterDetail.declaredSpelling.test.tsx index ea76190cfa..3c6efa7d89 100644 --- a/packages/plugin-form/src/deriveMasterDetail.declaredSpelling.test.tsx +++ b/packages/plugin-form/src/deriveMasterDetail.declaredSpelling.test.tsx @@ -9,8 +9,9 @@ /** * objectui#3951, master-detail half — the derivation is a PRODUCER of the very * columns `GridField` consumes, so it must emit the declared spelling - * (`GridColumnDefinition.name`) and read author-supplied columns by the same - * key. Before the fix `deriveColumns` emitted `{ field: name, … }` and + * (`name`, the spec's inline grid column key — objectui#11070) and read + * author-supplied columns by the same key. Before the fix `deriveColumns` + * emitted `{ field: name, … }` and * `hydrateColumns` looked the child field up as `fields[col.field]`: that pair * agreed with the widget's old `c.field` reader, which is precisely why the * master-detail path worked while spec-compliant hand-authored metadata did 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 9d724540fe..81bdaae391 100644 --- a/packages/types/src/__tests__/form-field-zod-coverage.test.ts +++ b/packages/types/src/__tests__/form-field-zod-coverage.test.ts @@ -94,6 +94,9 @@ const DECLARED_KEYS = [ // widgets read (their snake_case forms are retired), by reference too. 'returnType', 'summaryOperations', + // objectui#11070 round 7 — the `grid` widget's columns: the spec's + // `inlineColumns` list (its strict inline grid column), by reference. + 'columns', ]; describe('FormFieldSchema covers the FormField contract', () => { diff --git a/packages/types/src/__tests__/object-chart-properties-bag-11276.test.ts b/packages/types/src/__tests__/object-chart-properties-bag-11276.test.ts index f752d85091..2506cff822 100644 --- a/packages/types/src/__tests__/object-chart-properties-bag-11276.test.ts +++ b/packages/types/src/__tests__/object-chart-properties-bag-11276.test.ts @@ -88,8 +88,8 @@ type InputOf = T extends z.ZodType ? z.input : never; type Arm = ShapeOf; type Mirror = ShapeOf; type Bag = ShapeOf>>; -/** The node-level keys: the node base's and the envelope's. */ -type NodeLevel = keyof ShapeOf | 'responsiveStyles'; +/** The node-level keys: the node base's, the envelope's, and the binding (objectui#11070). */ +type NodeLevel = keyof ShapeOf | 'responsiveStyles' | 'dataSource'; /** * The bag's key set is exactly the mirror's own members (its shape less the @@ -258,7 +258,8 @@ describe('the spec has no `object-chart` row, and none is invented (objectui#112 describe('the bag is the flat mirror\'s own members, by reference (objectui#11276)', () => { it('its key set is the mirror\'s shape less the node-level keys, read on every run', () => { - const nodeLevel = new Set([...Object.keys(BaseSchema.shape), 'responsiveStyles']); + // `dataSource` is the node's binding (objectui#11070): node-level, beside the bag. + const nodeLevel = new Set([...Object.keys(BaseSchema.shape), 'responsiveStyles', 'dataSource']); const expected = Object.keys(ObjectChartSchema.shape).filter((key) => !nodeLevel.has(key)).sort(); expect(bagKeys().sort()).toEqual(expected); // Non-vacuity: the chart's own vocabulary is in it. @@ -348,7 +349,6 @@ describe('the flat spelling is refused by name, with the bag member as the remed it.each([ ['an invented key', 'inventedKey11276'], - ['the node-level `dataSource` this arm leaves undeclared (objectui#11070)', 'dataSource'], ] as const)('%s written flat stays unjudged on the tolerant face and is refused on the strict face', (_label, key) => { const doc = { ...SHOWCASE_BAR, [key]: { object: 'x' } }; expect(safeValidateSchema(doc).success).toBe(true); @@ -356,6 +356,12 @@ describe('the flat spelling is refused by name, with the bag member as the remed expect((issue as { keys?: string[] } | undefined)?.keys).toEqual([key]); }); + it('the node-level `dataSource` binding parses on both faces and is not a bag member (objectui#11070)', () => { + const doc = { ...SHOWCASE_BAR, dataSource: { object: 'x' } }; + for (const [, parse] of FACES) expect(parse(doc).error?.issues ?? []).toEqual([]); + expect(bagKeys()).not.toContain('dataSource'); + }); + it('a `BaseSchema` key stays on the node, as on every arm (control)', () => { for (const [, parse] of FACES) expect(parse({ ...SHOWCASE_BAR, className: 'min-h-0' }).success).toBe(true); }); diff --git a/packages/types/src/__tests__/object-chart-react-tier-node-10770.test.ts b/packages/types/src/__tests__/object-chart-react-tier-node-10770.test.ts index 53e36a04d6..5541dffce3 100644 --- a/packages/types/src/__tests__/object-chart-react-tier-node-10770.test.ts +++ b/packages/types/src/__tests__/object-chart-react-tier-node-10770.test.ts @@ -53,12 +53,14 @@ import { safeValidateSchema } from '../zod/index.zod'; /** * The node the react-page wrapper builds from the showcase `renewals-pipeline` - * page's ``: `{ dataSource, ...props, specType, type: tag }`. - * `dataSource` is the page adapter, `null` before the host connects one - * (objectui#7912). + * page's ``: `{ ...props, specType, type: tag }`. The wrapper no + * longer writes the host adapter (or `null`) under `dataSource`: the adapter + * reaches the block through the page's `SchemaRendererProvider` alone + * (objectui#11070, round 2), and `dataSource` on this node is the spec's + * per-element BINDING, declared since round 7 — so a `null` there is refused + * (pinned in (c) below). */ const SHOWCASE_NODE = { - dataSource: null, objectName: 'showcase_invoice', aggregate: { field: 'total', function: 'sum', groupBy: 'status' }, xAxis: { field: 'status' }, @@ -72,12 +74,13 @@ const SHOWCASE_NODE = { /** * The same props as an AUTHORED node writes them (objectui#11276): in the - * `properties` bag, the binding beside it at node level. The door judges this - * spelling; the wrapper's flat node above is what the mirror judges. + * `properties` bag. A binding, when the author writes one, sits beside the bag + * at node level (see (b)). The door judges this spelling; the wrapper's flat + * node above is what the mirror judges. */ const SHOWCASE_AUTHORED = (() => { - const { type, dataSource, ...props } = SHOWCASE_NODE; - return { type, dataSource, properties: props }; + const { type, ...props } = SHOWCASE_NODE; + return { type, properties: props }; })(); const issuesOf = (doc: unknown) => { @@ -136,6 +139,12 @@ describe('objectui#10770 (b) — the react tier\'s showcase node parses', () => expect(r.success).toBe(true); }); + it('with a per-element binding at node level, beside the bag (objectui#11070)', () => { + const r = safeValidateSchema({ ...SHOWCASE_AUTHORED, dataSource: { object: 'showcase_invoice' } }); + expect(r.error?.issues ?? []).toEqual([]); + expect(r.success).toBe(true); + }); + it('keeps the authored series as written: the spec\'s defaults are not injected', () => { const r = ObjectChartMirror.safeParse(SHOWCASE_NODE); expect(r.data?.series).toEqual(SHOWCASE_NODE.series); @@ -162,6 +171,11 @@ describe('objectui#10770 (c) — what stays refused', () => { expect(ObjectChartMirror.safeParse({ type: 'object-chart', specType: 'line' }).success).toBe(true); }); + it('a `null` `dataSource`, the adapter placeholder the wrapper no longer writes (objectui#11070)', () => { + const issues = issuesOf({ ...SHOWCASE_NODE, dataSource: null }); + expect(issues.map((i) => [i.code, i.path])).toEqual([['invalid_type', ['dataSource']]]); + }); + it('the internal `{ dataKey }` arm is unchanged', () => { const r = ObjectChartMirror.safeParse({ type: 'object-chart', chartType: 'bar', series: [{ dataKey: 'amount', chartType: 'line' }] }); expect(r.error?.issues ?? []).toEqual([]); diff --git a/packages/types/src/__tests__/public-block-responsive-styles-10872.test.ts b/packages/types/src/__tests__/public-block-responsive-styles-10872.test.ts index d7979cdf7b..941c356ee5 100644 --- a/packages/types/src/__tests__/public-block-responsive-styles-10872.test.ts +++ b/packages/types/src/__tests__/public-block-responsive-styles-10872.test.ts @@ -113,6 +113,11 @@ const DECLARES_DATA_SOURCE: ReadonlySet = new Set([ // objectui#10859 batch 6: `object-gantt` moved here the same way, with the // same binding. 'object-gantt', + // objectui#11070 round 7: `object-chart` (here since objectui#11276) declares + // the same binding at node level, beside its bag — the gate-wrapped + // registration reads it, and the react-page wrapper no longer writes the + // host adapter under that key. + 'object-chart', ]); /** 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 66d6f1e5f5..c2493dda6b 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 @@ -22,13 +22,14 @@ * - `form.showSubmit` — the `form` renderer's submit-button switch; * - `form.fields[]` — field metadata a hand-authored form writes on the entry * itself (`multiple`, `rows`, `accept`, `dimensions`, `reference`, `min`, - * `max`, `minLength`, `maxLength`, `pattern`, and since round 3 - * `returnType` and `summaryOperations`), which the renderer hands each - * field widget as its metadata carrier; + * `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; * - `dataSource` on `object-grid`, `object-form`, `object-kanban`, - * `list-view`, `object-gantt`, `object-map` and `object-calendar` — the - * spec's per-element binding, which each block's - * gate-wrapped registration reads off the node through + * `list-view`, `object-gantt`, `object-map`, `object-calendar` and, since + * round 7, `object-chart` — the spec's per-element binding, which each + * block's gate-wrapped registration reads off the node through * `ElementDataSourceGate`. * * Each read is reasoned on the TypeScript member that declares it. @@ -39,9 +40,9 @@ * sibling is still refused BY NAME at the same path — the control that * shows the face did not open up; * 2. the declared key is judged by its declared type on BOTH faces; - * 3. the read keys this card deliberately did NOT declare are still refused - * on the strict face — each waits on a ruling the card records — and the - * snake_case spellings rounds 3, 4 and 5 retired are refused by name; + * 3. the snake_case spellings rounds 3, 4 and 5 retired, and the inline + * dashboard dialect round 6 retired (objectui#11228 ruling C), are + * refused by name on the strict face; * 4. the TypeScript faces type the binding, not `any`, and the field * metadata types carry the spec members by reference with the retired * snake_case members gone (type-level, read by @@ -54,6 +55,7 @@ import { z } from 'zod'; import type { ListViewSchema, ObjectCalendarSchema, + ObjectChartSchema, ObjectFormSchema, ObjectGanttSchema, ObjectGridSchema, @@ -64,6 +66,7 @@ import type { FormField, FormSchema } from '../form.js'; import type { EmailFieldMetadata, FormulaFieldMetadata, + GridFieldMetadata, HtmlFieldMetadata, LookupFieldMetadata, MarkdownFieldMetadata, @@ -127,6 +130,8 @@ describe('objectui#11070 — the declared read keys parse on the strict face', ( ['pattern', { type: 'input', pattern: '^[^@]+@[^@]+$' }], ['returnType', { type: 'formula', returnType: 'number' }], ['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' }] }], ]; it.each(FIELD_CASES)('`fields[].%s` parses; a misspelled sibling is refused at the field', (key, field) => { @@ -152,6 +157,9 @@ describe('objectui#11070 — the declared read keys parse on the strict face', ( // objectui#10859 batch 5: `object-map` takes its props in the bag too. ['object-map', { properties: { objectName: 'task' } }], ['object-calendar', { objectName: 'task' }], + // Round 7: `object-chart`, authored with its props in the bag + // (objectui#11276); the binding stays on the node. + ['object-chart', { properties: { objectName: 'task', chartType: 'bar' } }], ]; it.each(BOUND_NODES)('`%s.dataSource` parses; `dataSourc` beside it is refused by name', (type, rest) => { @@ -175,6 +183,14 @@ describe('objectui#11070 — a declared key is judged by its declared type on bo ['an unknown member inside `summaryOperations` (the spec closes it)', form({ type: 'summary', summaryOperations: { object: 'orders', field: 'amount', function: 'sum', functon: 'avg' } })], ['a binding that names no `object`', { type: 'object-kanban', dataSource: { filter: { a: 1 } } }], ['an adapter-shaped `dataSource`', { type: 'object-grid', objectName: 'task', dataSource: 'objectstack' }], + // Round 7: the grid's columns are the spec's strict inline grid column. + ['a grid column keyed by the retired `field` spelling', form({ type: 'grid', columns: [{ field: 'qty' }] })], + ['a grid column `type` outside the spec\'s nine cell controls (`boolean`)', form({ type: 'grid', columns: [{ name: 'done', type: 'boolean' }] })], + ['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' } })], + ['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' }], ]; it.each(WRONG)('refuses %s', (_label, doc) => { @@ -183,22 +199,14 @@ describe('objectui#11070 — a declared key is judged by its declared type on bo }); }); -/* ── 3. the read keys this card did NOT declare stay refused ─────────────── */ - -describe('objectui#11070 — the read keys left undeclared pending a ruling stay refused on the strict face', () => { - // Read by a widget and not declared: the grid field's `columns` has an - // element shape not yet decided. ⛔ Declaring it is a contract ruling, not a - // fix to this list. (`min_length` stood here until round 5 retired it: no - // reader reads it any more, so it moved to the RETIRED list below.) - const PENDING: ReadonlyArray]> = [ - ['columns', { type: 'grid', columns: [{ name: 'qty', type: 'number' }] }], - ]; - - it.each(PENDING)('`fields[].%s` is refused by name, and the tolerant face still accepts the document', (key, field) => { - expect(undeclared(issuesOf(StrictAnyComponentSchema, form(field)))).toEqual([`fields.0.${key}`]); - expect(issuesOf(AnyComponentSchema, form(field))).toBeNull(); - }); +/* ── 3. the retired spellings stay refused ──────────────────────────────── */ +describe('objectui#11070 — the retired spellings stay refused on the strict face', () => { + // Round 7 emptied the PENDING list this block opened with: the grid field's + // `columns` (the last read key left undeclared) is the spec's + // `inlineColumns` list now (block 1), and `object-chart.dataSource` is + // declared with the other gate-wrapped bindings. + // // Round 3 (the seat's answer A): the `formula` and `summary` widgets read the // spec's `returnType` and `summaryOperations` only, so these snake_case // spellings are read by nothing and retired at once — no alias, no dual @@ -261,12 +269,11 @@ describe('objectui#11070 — the read keys left undeclared pending a ruling stay expect(undeclared(issuesOf(StrictAnyComponentSchema, doc))).toContain(`widgets.0.${key}`); }); - it('`object-chart.dataSource` is refused by name until it is declared (the react-page wrapper no longer puns the adapter into that key; the declaration and the objectui#10770 node pin move together)', () => { - // objectui#11276: the authored node takes its props in the `properties` bag; - // `dataSource` stays node-level, and stays undeclared on that arm too. - const doc = { type: 'object-chart', properties: { objectName: 'task', chartType: 'bar' }, dataSource: { object: 'task' } }; - expect(undeclared(issuesOf(StrictAnyComponentSchema, doc))).toEqual(['dataSource']); - expect(issuesOf(AnyComponentSchema, doc)).toBeNull(); + it('`object-chart.dataSource` stays on the NODE: in the bag it is refused by name, not taken for the binding (round 7)', () => { + // The binding is a node-level key of the spec's `PageComponentSchema`, so + // the bag (the flat mirror's own members) does not carry it. + const doc = { type: 'object-chart', properties: { objectName: 'task', chartType: 'bar', dataSource: { object: 'task' } } }; + expect(undeclared(issuesOf(StrictAnyComponentSchema, doc))).toEqual(['properties.dataSource']); }); }); @@ -280,7 +287,7 @@ type IsAny = 0 extends 1 & T ? true : false; export type assertionFormFieldMembersAreTyped = Expect, + | FormField['returnType'] | FormField['summaryOperations'] | FormField['columns']>, false >>; /** @@ -298,6 +305,28 @@ export type assertionSpecMembersByReference = [ // Round 6: the formula itself is the spec's `expression`, by reference. Expect>, ]; +/** + * Round 7: the grid's columns are the spec's `inlineColumns` list BY + * REFERENCE, on the form-field face and on the grid field metadata type — an + * exact match, so a restated column shape (or a drift after a spec release) + * fails here. + */ +export type assertionGridColumnsBySpecReference = [ + 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; +// @ts-expect-error objectui#11070 round 7 — and RETIRED from the package entry, the face an external consumer imports. +type _GridColumnDefinitionRetiredFromTheEntry = import('../index').GridColumnDefinition; +/** LIT CONTROLS for the two directives above: a sibling of the same export block resolves through the same forms. */ +type _GridSiblingResolvesFromTheModule = import('../field-types').GridFieldMetadata; +type _GridSiblingResolvesFromTheEntry = import('../index').GridFieldMetadata; +export type assertionGridSiblingControlsResolve = [ + Expect>, + Expect>, +]; /** * Round 5: the text family carries the spec's length members BY REFERENCE — * an exact match on every type that declares one. @@ -364,6 +393,7 @@ export type assertionBindingIsTheSpecBinding = [ Expect, SpecElementDataSource>>, Expect, SpecElementDataSource>>, Expect, SpecElementDataSource>>, + Expect, SpecElementDataSource>>, ]; // @ts-expect-error — an adapter is not a binding: the binding names an `object`. diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index 661e849c85..982594f957 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -4763,6 +4763,9 @@ const SPEC_DERIVED_PAIRS: readonly string[] = [ // reference (`multiple`, `rows`, `accept`, `dimensions`, `reference`, `min`, // `max`, `minLength`, `maxLength`), so a spec bump that moves one of those field // 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). 'form.zod.ts#FormFieldSchema', 'form.zod.ts#SelectOptionSchema', 'layout.zod.ts#PageNodeSchema', @@ -4812,8 +4815,8 @@ const SPEC_DERIVED_PAIRS: readonly string[] = [ // reference (the per-element binding `PageComponentSchema.dataSource` // declares) — the first spec reference in this mirror. `ObjectGridSchema`, // `ObjectFormSchema`, `ListViewSchema`, `ObjectGanttSchema`, - // `ObjectMapSchema` and `ObjectCalendarSchema` gained the same member and - // were already spec-derived. + // `ObjectMapSchema`, `ObjectCalendarSchema` and (round 7) `ObjectChartSchema` + // gained the same member and were already spec-derived. 'objectql.zod.ts#ObjectKanbanSchema', 'objectql.zod.ts#ObjectMapSchema', // objectui#7779: BACK, by a real code reference this time — `navigation`, diff --git a/packages/types/src/field-types.ts b/packages/types/src/field-types.ts index 99a4636b52..73f6bd0aef 100644 --- a/packages/types/src/field-types.ts +++ b/packages/types/src/field-types.ts @@ -1007,9 +1007,23 @@ export interface VectorFieldMetadata extends BaseFieldMetadata { export interface GridFieldMetadata extends BaseFieldMetadata { type: 'grid'; /** - * Column definitions for the grid + * The grid's columns — `@objectstack/spec`'s `FieldSchema.inlineColumns`, + * typed BY REFERENCE (objectui#11070): an array of the spec's strict, + * `name`-keyed inline grid column (`InlineGridColumn`), the shape the spec + * declares as the mirror of this widget's column. `GridField` reads each + * column by exactly the spec's keys and no other spelling, so a column the + * spec accepts is a column the grid renders, and a key the spec refuses + * (the retired `field`, a `title`, a per-column `defaultValue`) is refused + * here at compile time instead of being dropped at render time. + * + * The key is `columns` because a `grid` field is objectui's own field type + * (`@objectstack/spec` has no `grid` field type); the spec spells the same + * list `inlineColumns` on a `master_detail` field, whose inline editor this + * widget is. The former local `GridColumnDefinition` is retired: it was not + * the shape the widget read (it required a free-form `type` and declared + * `defaultValue` / `validate`, which nothing read). */ - columns?: GridColumnDefinition[]; + columns?: SpecField['inlineColumns']; /** * Minimum number of rows */ @@ -1032,40 +1046,6 @@ export interface GridFieldMetadata extends BaseFieldMetadata { allow_reorder?: boolean; } -/** - * Grid column definition - */ -export interface GridColumnDefinition { - /** - * Column field name - */ - name: string; - /** - * Column label - */ - label?: string; - /** - * Field type - */ - type: string; - /** - * Whether column is required - */ - required?: boolean; - /** - * Default value for new rows - */ - defaultValue?: any; - /** - * Column width - */ - width?: number; - /** - * Validation rules - */ - validate?: FieldConstraints; -} - export interface ColorFieldMetadata extends BaseFieldMetadata { type: 'color'; } diff --git a/packages/types/src/form.ts b/packages/types/src/form.ts index 2fce3dbb30..4981dc55c2 100644 --- a/packages/types/src/form.ts +++ b/packages/types/src/form.ts @@ -1940,9 +1940,7 @@ export interface FormField { // Their snake_case forms (`min_length`, `max_length`, `reference_to`, // `return_type`, `summary_type`, …) are retired, read by nothing and // refused by the strict face (objectui#11070). The grid field's `columns` - // is read too, but its element shape is undecided: the declared - // `GridColumnDefinition` (`./field-types.ts`) is not the shape `GridField` - // reads. It remains open on objectui#11070. + // is the spec's inline grid column list, by reference (below). /** * Hold several values instead of one. Read by the `file`, `image`, @@ -2028,6 +2026,18 @@ export interface FormField { * are read by nothing and refused. */ summaryOperations?: SpecField['summaryOperations']; + /** + * The columns of a `grid` field: `@objectstack/spec`'s + * `FieldSchema.inlineColumns`, by reference — an array of the spec's strict, + * `name`-keyed inline grid column (`{ name, label?, type?, width?, … }`). + * The `grid` widget reads `columns` off its metadata carrier and each column + * by exactly the spec's keys, so a column the spec refuses (the retired + * `field` spelling, a `title`, a `type` outside its nine cell controls) is + * refused here too rather than rendered as an empty or plain-text cell. The + * spec spells the list `inlineColumns` on a `master_detail` field; a `grid` + * field is objectui's own type, whose key is `columns` (objectui#11070). + */ + columns?: SpecField['inlineColumns']; } /** diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index d0a6048180..d7b419fb40 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -535,7 +535,6 @@ export type { ObjectFieldMetadata, VectorFieldMetadata, GridFieldMetadata, - GridColumnDefinition, ColorFieldMetadata, CodeFieldMetadata, AvatarFieldMetadata, diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index 55be66e3e3..4cd2b0295d 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -812,12 +812,9 @@ export interface ObjectGridSchema extends BaseSchema { * which the strict authoring face refused until it was written. Same * declaration on {@link ObjectFormSchema}, {@link ObjectKanbanSchema}, * {@link ObjectGanttSchema}, {@link ObjectMapSchema}, - * {@link ObjectCalendarSchema} and `ListViewSchema` (derived from its zod - * mirror; its absence from `@object-ui/app-shell`'s relay census is declared - * there, objectui#7559). ⚠️ Not on {@link ObjectChartSchema}: the react-page - * wrapper writes the host's ADAPTER (or `null`) under this key on every data - * block it builds, and objectui#10770 pins that `object-chart` node as - * valid, so declaring the binding there waits on objectui#11070's seat. + * {@link ObjectCalendarSchema}, {@link ObjectChartSchema} and + * `ListViewSchema` (derived from its zod mirror; its absence from + * `@object-ui/app-shell`'s relay census is declared there, objectui#7559). */ dataSource?: ElementDataSource; @@ -5183,6 +5180,18 @@ export interface ObjectChartSchema extends BaseSchema { * it on `object-chart` nodes (the objectstack showcase). */ responsiveStyles?: SpecResponsiveStyles; + /** + * Per-element data binding — the spec's `ElementDataSource`, by reference + * (objectui#11070). This block's registration is gate-wrapped + * (`elementDataSourceBlock`), so `ElementDataSourceGate` reads the binding + * off the node and lands its `object` on {@link objectName}. Metadata, ⛔ not + * the adapter: see {@link ObjectGridSchema.dataSource}. It stays on the node + * on the authored spelling too, beside the `properties` bag, as the spec's + * `PageComponentSchema.dataSource` does. The react-page wrapper no longer + * writes the host adapter under this key (objectui#11070, round 2), which is + * what kept the binding undeclared here. + */ + dataSource?: ElementDataSource; /** ObjectQL object name (legacy inline path; optional under ADR-0021 dataset binding) */ objectName?: string; /** Chart type. Includes donut / horizontal-bar / column — all rendered by diff --git a/packages/types/src/zod/form.zod.ts b/packages/types/src/zod/form.zod.ts index c2a86e9946..d583617bed 100644 --- a/packages/types/src/zod/form.zod.ts +++ b/packages/types/src/zod/form.zod.ts @@ -1063,6 +1063,10 @@ export const FormFieldSchema = z.object({ // `summary_field`) are retired and stay undeclared (objectui#11070). returnType: stripImportedDefaults(SpecFieldSchema).shape.returnType, summaryOperations: stripImportedDefaults(SpecFieldSchema).shape.summaryOperations, + // The `grid` widget's columns: the spec's `inlineColumns` list (its strict, + // `name`-keyed inline grid column), by reference. objectui's `grid` field + // type spells the list `columns` (objectui#11070). + columns: stripImportedDefaults(SpecFieldSchema).shape.inlineColumns, }).superRefine((field, ctx) => { // objectui#5449 — the namespace rule `@object-ui/core` has enforced since // objectui#5375, stated here so `objectui validate` (which reaches this diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index 2cbaf544c0..7838eb8a64 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -3166,6 +3166,15 @@ export const ObjectChartSchema = BaseSchema.extend({ // `NODE_ENVELOPE` fragment as `ObjectGridSchema` above. A producer writes // it on `object-chart` nodes. The TS twin declares it too. ...NODE_ENVELOPE, + // objectui#11070 — the spec's per-element binding, by reference, as on the + // other gate-wrapped arms (see `ObjectGridSchema.dataSource`). The + // registration is `elementDataSourceBlock`-wrapped, so + // `ElementDataSourceGate` reads it off the node and lands its `object` on + // `objectName`. A NODE-level key: `OBJECT_CHART_NODE_LEVEL_KEYS` below keeps + // it out of the authored `properties` bag. + dataSource: stripImportedDefaults(SpecElementDataSourceSchema) + .optional() + .describe(ELEMENT_DATA_SOURCE_BINDING_DESCRIPTION), // Legacy inline path (objectName + aggregate). Optional now that a chart may // instead bind to a semantic-layer dataset (ADR-0021, objectstack-ai/objectstack#1890). objectName: z.string().optional().describe('ObjectQL object name (legacy inline path)'), @@ -3381,15 +3390,18 @@ export const ObjectChartSchema = BaseSchema.extend({ /** * The node-level keys of `ObjectChartSchema` above: everything the node base - * declares (`BaseSchema`, `type` and the two content channels among them) and - * the node envelope (`NODE_ENVELOPE`). The chart's OWN members are the rest of - * the mirror's shape, and they are what the bag below holds (objectui#11276). - * Read off the two declarations, not transcribed, so a key either one gains - * stays at node level the day it lands. + * declares (`BaseSchema`, `type` and the two content channels among them), the + * node envelope (`NODE_ENVELOPE`), and the per-element `dataSource` binding, + * which the spec's `PageComponentSchema` declares on the node beside + * `properties` (objectui#11070), as `ObjectFormBlockSchema` and + * `ObjectMapBlockSchema` carry it. The chart's OWN members are the rest of the + * mirror's shape, and they are what the bag below holds (objectui#11276). Read + * off the declarations, not transcribed, so a key `BaseSchema` or the envelope + * gains stays at node level the day it lands. */ const OBJECT_CHART_NODE_LEVEL_KEYS = Object.fromEntries( - [...Object.keys(BaseSchema.shape), ...Object.keys(NODE_ENVELOPE)].map((key) => [key, true]), -) as { [K in keyof typeof BaseSchema.shape | keyof typeof NODE_ENVELOPE]: true }; + [...Object.keys(BaseSchema.shape), ...Object.keys(NODE_ENVELOPE), 'dataSource'].map((key) => [key, true]), +) as { [K in keyof typeof BaseSchema.shape | keyof typeof NODE_ENVELOPE | 'dataSource']: true }; /** * The `object-chart` props bag (objectui#11276): the flat mirror's own members, @@ -3513,11 +3525,13 @@ const OBJECT_CHART_FLAT_PROP_REFUSALS = Object.fromEntries( * ## The chart family, `dataSource` and the content channels * * The flat mirror's chart-family floor (objectui#10770) stays on the authored - * node, read in the bag (`requireObjectChartFamilyInBag`). `dataSource` stays - * undeclared, as on the flat mirror: objectui#11070 left `object-chart`'s - * binding refused by name on the strict face until it is declared, and this - * arm does not decide that. Neither content channel is read, so both are - * refused with the objectui#9256 string the flat mirror uses. + * node, read in the bag (`requireObjectChartFamilyInBag`). The registration is + * `elementDataSourceBlock`-wrapped, so `dataSource` is the spec's + * `ElementDataSourceSchema` by reference, on the NODE beside the bag, as on + * `ObjectFormBlockSchema` and `ObjectMapBlockSchema` (objectui#11070; it is + * one of `OBJECT_CHART_NODE_LEVEL_KEYS`, so it is not a bag member and is not + * refused flat). Neither content channel is read, so both are refused with + * the objectui#9256 string the flat mirror uses. * * ## What did not move * @@ -3538,6 +3552,9 @@ export const ObjectChartBlockSchema = BaseSchema.extend({ + 'by reference. `@objectstack/spec` has no `ComponentPropsMap[\'object-chart\']` row, so these are ' + 'objectui\'s own members (objectui#11276). The chart family is required here: `chartType` (or `specType`).', ), + // objectui#11070 — the node's binding, the flat mirror's own member by + // reference (the spec's `ElementDataSourceSchema`). + dataSource: ObjectChartSchema.shape.dataSource, ...OBJECT_CHART_FLAT_PROP_REFUSALS, // objectui#10608: the three retired list-view spellings keep their retirement // when written flat — the flat mirror's own tombstones, by reference.