Skip to content

Commit e3782d2

Browse files
feat(types)!: an authored object-form takes its props in the spec's properties bag; the flat spelling is refused by name (objectui#10859, batch 4) (#11291)
Part of #10859 Clause-②: yes `Clause-②: yes`, copied from the PM claim `5920453930`: `@object-ui/types`' authoring faces narrow (a flat `object-form` node that parsed is refused) and widen (a spec-shaped bag node that was refused parses). This is objectui's narrowing-as-`minor` rule with a **BREAKING (authoring)** banner in the changeset, as batches 1 to 3 and the answer's step 2 state. Batch 4 of objectui#10859. It executes the seat's flat-arm answer at PR objectui#11248's ACCEPT (`5917261346`) for ONE block, `object-form`: "A governs, by inheritance" from objectui#10872's triage answer A (`5906817482`), "The `properties` bag is the contract". The card stays open for the rest of the flat-arm fork (`object-map`, `object-gantt`), the designer six and the (ii) / (iii) keys. ## What changed - `packages/types/src/zod/objectql.zod.ts`: new `ObjectFormBlockSchema`, the batch-2/3 construct. It is `BaseSchema`, plus the `object-form` literal, plus `NODE_ENVELOPE`, plus `properties` = `propsBag('object-form', stripImportedDefaults(SpecObjectFormPropsSchema))`: the shared helper and the installed 17.5.0 row by reference, with no copy. Beside it: - `dataSource`, the spec's `ElementDataSourceSchema` by reference (the registration is `elementDataSourceBlock`-wrapped; the flat mirror has declared it since objectui#11070); - one `aliasKeyRefusal` per member of the row, keyed by the row's own key set read off the crossed row, so each prop written flat is refused on both faces with `Did you mean KEY → properties.KEY?` and one prescription string that shows the whole bag document; - the five runtime slots `ObjectForm` reads off the node (`onSuccess`, `onCancel`, `onError`, `onOpenChange`, `onStepChange`), declared exactly as on the flat mirror; - `body` / `children` refused with the flat mirror's objectui#9256 string. - The arm joins `ObjectQLPublicBlockComponentSchema`. `ObjectFormSchema` (the flat mirror) leaves `ObjectQLComponentSchema`, stays exported, and stays paired with its TypeScript twin in the parity census. - `packages/types/src/objectql.ts`: the TypeScript `ObjectFormSchema` is **re-declared, not retired**. Its members are unchanged; its docblock now says what it is: the node as `ObjectForm` reads it after `SchemaRenderer` hoists `properties`, and as code composes it. The TypeScript union's docblock names the zod union's new count. - `packages/types/src/zod/form.zod.ts`: the `form` node's `mode` refusal taught a flat `object-form` document. Its prescription now writes the bag. - `packages/types/src/zod/index.zod.ts` (one barrel export) and `packages/types/src/zod/README.md` (the ObjectQL public-block group lists the arm). - objectui's authored flat producers, rewritten to the bag (census below): the root `README.md`, `content/docs/api/schema-reference.md`, `content/docs/guide/building-crud-app.md`, `content/docs/guide/objectos-integration.mdx`, `content/docs/plugins/plugin-form.mdx`, both schema-catalog `object-form` fixtures, and `packages/plugin-form/README.md`. - New pins: `packages/types/src/__tests__/object-form-properties-bag-10859-b4.test.ts` and `packages/plugin-form/src/__tests__/objectFormPropertiesBag-10859.test.tsx`. - Two changesets: `.changeset/10859-object-form-properties-bag.md` (`@object-ui/types` minor, BREAKING (authoring) banner) and `.changeset/10859-plugin-form-readme-object-form-bag.md` (`@object-ui/plugin-form` patch, README only). **Files beyond the claimed surface**, each forced and named: - `zod-mirror-parity.test.ts`: the one `EXCLUSIONS` row the new export forces; no pair moved (`EXPECTED_MIRROR_PAIRS` untouched). - `imported-defaults-8317.test.ts`: one `IMPORTED` row for the new spec import. - `registered-type-arms-10859-b2.test.ts`: its exact `ObjectQLPublicBlockComponentSchema` literal list gains `object-form`. - `objectql-union-arms-7363.test.ts`: `ObjectQLComponentSchema` holds 11 arms, not 12, and a row that `object-form` is not among them. - `public-block-responsive-styles-10872.test.ts`: `object-form` joins the named `DECLARES_DATA_SOURCE` set (it moved into the public-block population carrying the binding). - Fixture triage (respelled, the flat node is now refused): `strict-face-read-keys-11070.test.ts` (one `BOUND_NODES` row), `content-channel-e3-residual-9256.test.ts` (the authoring-face reach row reads the bag; the mirror rows keep the post-hoist spelling), `object-form-unmirrored-members-6152.test.ts` (details under Acceptance notes), `guideCrudAppRenders.test.tsx` (reads the guide snippet's bag), `catalog-gallery-render.test.tsx` (its control uses the entries' spelling). ## Zone 2: the PM's mechanism assumptions, measured before building 1. **Held (the gate for the batch).** Installed `@objectstack/spec` 17.5.0, and objectstack `origin/main` `75519e1c0a` (spec source run with `tsx`): - `ComponentPropsMap` carries an `object-form` row, and it is the exported `ObjectFormPropsSchema` (same key set at both refs). - `PageComponentSchema.safeParse({ type: 'object-form', objectName: 'x' })` refuses: `unrecognized_keys ['objectName']` ("Unrecognized key(s) on this page component … ADR-0089 D3a"). With `mode` beside it: `['objectName', 'mode']`. - The same props in `properties` are accepted, and the row accepts them. - Control: flat `action:button` `actionType` is refused the same way. 2. **Held, red first.** On base `f16c01e90e`, the showcase wizard node (objectstack `NewProjectWizardPage`) was refused by `safeValidateSchema` (`objectName` `invalid_type`, `mode` `invalid_value`) and by the strict face (the same two, plus `unrecognized_keys ['properties']`). The flat control was accepted on both faces. The new pin on base, before the arm: `Tests 58 failed | 7 passed (65)`. The 7 green rows are the lit controls, the row size check, the content-channel rows and the flat-mirror row, which all hold on base too. 3. **Settled: re-declared, not retired.** Readers of the TypeScript twin, measured: - `ObjectForm` and its five variant forms (the `schema` prop); - the code composers (`ObjectView`, `RecordFormPage`, `ScreenView`, `AppContent`, `useActionModal`, `EmbeddableForm`, `MasterDetailForm`, `ViewPreview`, `StudioDesignSurface`, `ObjectManager`, `FieldDesigner`); - the object-view `form` slot (`ObjectFormSlotKey`; the zod slot is `ObjectFormSchema.omit(…).partial()`); - the TypeScript unions; - the mirror-parity census pair. Every one reads the post-hoist or code-composed flat node. None needs the authored shape, so the twin keeps its members and its publication. The authored node is typed by `BaseSchema` (index signature) or the spec's `ObjectFormProps`. The bag arm is unpaired (one `EXCLUSIONS` row), as batch 2's `MasterDetailFormSchema` split is. Type-level pins: the bag input equals `ObjectFormProps | undefined`, every row key is a refused flat member, and the twin stays `type: 'object-form'` with `objectName: string` and `mode` required, still the `object-form` member of the TypeScript ObjectQL union. 4. **Census** (classifier: a document or doc example an author writes, versus code that builds the node or mounts a component with its post-hoist `schema` prop): | class | files | action | |:--|:--|:--| | authored, rewritten to the bag | root `README.md`, `schema-reference.md`, `building-crud-app.md`, `objectos-integration.mdx`, `plugin-form.mdx`, `object-form-record.json`, `object-form-tabbed-sections.json`, `packages/plugin-form/README.md` (2 of its 3 nodes), `form.zod.ts` (a refusal's prescription) | rewritten (9 files) | | authored, already bag-compatible | `content/docs/utilities/data-objectstack.mdx` (`{ type, dataSource }` only) | none | | authored, governed | `skills/objectui/guides/page-builder.md` ("Form plugin example") | **listed, not edited** (ruling step 4) | | authored, held by another PR | `packages/cli/src/commands/validate.ts` (a hint naming `type: 'object-form'` with `objectName` + `sections`) | not edited: the file is PR objectui#11069's | | component mount (post-hoist `schema` prop, no hoist) | `examples/byo-backend-console/src/App.tsx` (2), `packages/plugin-form/README.md` (the `WizardForm` mount), the `@example` blocks of `ObjectForm` / `TabbedForm` / `WizardForm` | kept flat: rewriting them would break them | | runtime composers | the eleven listed under item 3 | kept flat (item 5) | | test fixtures | the triage above; the plugin-form render tests build flat nodes as code does | respelled only where a face judges them | This differs from batch 3's count ("13 authored") in two classes: the two `byo-backend-console` nodes and the two designer nodes are component mounts and code composers, not authored documents. 5. **Held.** No composed node passes through `safeValidateSchema`. `SchemaRenderer` runs `@object-ui/core`'s structural `validateSchema`, and the only non-test callers of the zod faces are `objectui validate` / `objectui check`. The render pin draws the flat spelling beside the bag. 6. **Not forced.** `REFUSED_AT_TYPE` does not move: `object-form` was never refused at `type`. PR objectui#11069's files are untouched, and `git merge-tree` of this head with `origin/main` is clean. ## Tests (the union at head `a32a4470cd` unless marked; heavy runs through `os-verify-lock.sh`, slot `issue-10859`) - **Red first** (base `f16c01e90e`, pin present, arm absent): `Tests 58 failed | 7 passed (65)`. - **Green at `a32a4470cd`:** - `pnpm exec vitest run packages/types/ packages/cli/`: `Test Files 323 passed (323)`, `Tests 7875 passed (7875)`, VERDICT command-exit 0. - The render pin, the guide test, the b4 pin and `examples/schema-catalog/test/`: `Test Files 39 passed (39)`, `Tests 2290 passed (2290)`. - **Arm-list consumer sweep at `8e741e3fe4`:** - The files are every tracked test file outside `packages/types` and `packages/cli` that names `AnyComponentSchema`, `StrictAnyComponentSchema`, `safeValidateSchema`, `ObjectQLPublicBlockComponentSchema`, `ObjectQLComponentSchema`, `ObjectFormBlockSchema`, `nodeOracles`, `KNOWN_SCHEMA_TYPES`, `propsLevelOf`, `PUBLIC_BLOCKS`, `unmirroredAlias`, `collectArms`, `getPublicConfigs`, `ComponentPropsMap`, `validateSchema` or `StrictAuthoringFace`; or references `check-handler-key-read-sites`, `block-config-schema-parity`, `registry-inputs-spec-parity`, `strict-authoring-face`, `zod-mirror-parity`, `objectql.zod`, `index.zod`, `ObjectFormSchema` or `object-form`. - That is 312 files, run in two chunks: `Test Files 180 passed (180) / Tests 4016 passed (4016)` and `Test Files 132 passed (132) / Tests 1626 passed (1626)`. - The only changes since are the changeset text and the render pin's casts; that pin re-ran green at `a32a4470cd`. - **Build / type-check:** - `pnpm --filter @object-ui/types build && pnpm --filter @object-ui/types type-check`: VERDICT 0, `dist completeness: 1 package(s) complete (138 emitted files verified)`. The pin is in `tsconfig.test.json`'s program (`--listFilesOnly`). - `pnpm --filter @object-ui/plugin-form type-check`: exit 0, after building its dependency closure. Both plugin-form test files are in its test program. - **Runtime ablation** (objectstack `scripts/ablation-replace.mjs` WRAP, plus the script's own EXIT/INT/TERM trap on absolute paths): - The mutation removed `ObjectFormBlockSchema` from the public-block union: anchor x1 to x0, blob `03e9d3554ceb` to `5981e32df7dd`. - Pin + ratchet: `Tests 60 failed | 25 passed (85)`, with "`objectui validate` refuses 73 registered bare key(s) at `type`; the pin is 72". Direction: red, as expected. - Restored: blob == HEAD `03e9d3554ceb`, `git diff HEAD` empty. - **Type-level reverse validation** (same tool): - Bag narrowed to `{ objectName? }`: `tsc -p tsconfig.test.json` exit 2, `TS2344: Type 'false' does not satisfy the constraint 'true'` on the bag row. - Flat refusals removed: exit 2 (TS2339 on the three flat-member rows, TS2344 on the every-row-key row). - Restored as above. - **Render** (committed pin, real registry): the bag node fetches `getObjectSchema('showcase_project')` and draws the three step labels and the first step's field. The flat spelling draws the same. Control: a bag naming another object fetches that object and never `showcase_project`. ## Gates (exit codes read before any pipe) - exit 0, each with its own verdict line: - `check:handler-key-reads`: "OK 140 arm(s), 212 registration(s) (138 keyed onto an arm), 56 reachable handler read(s), 56 judged"; - `check:spec-symbols`; - `check:new-line-citations`: "VERDICT new-cross-file-line-citations: 0 new citation(s)"; - `check:control-bytes`: "OK (scanned 9718 tracked text file(s); skipped 85 binary)"; a self-scan over the 27 changed files gave 0 hits; - `node scripts/check-changeset-presence.mjs`: "16 source file(s) of 2 released package(s) changed, and this change declares 2 changeset(s)"; - `changeset:check`: "No changeset declares a `major` bump."; - `check:doc-fences`, `docs:check-links`, `markdown-test-inputs.mjs --audit` ("107 candidate test files, all adjudicated"); - `check:doc-snippets`: "697 of 697 block(s) judged, 0 failed", after its scoped build; - `check:doc-examples`, `check:doc-types`, `check:readme-exports`; - `check:element-data-source-declaration`, `check:installed-pin-claims`, `check:test-path-roots`, `check:pending-changeset-literals`, `check:unreferenced-sources`, `check:esm-specifiers`, `type-check:coverage`. - `check:changeset-claims` (report-only): the pending `.changeset/6152-object-form-unmirrored-members.md` describes the flat members this change moves into the bag. Its body is not edited. This PR's changeset names how those members are authored now, so the release notes read together. - `check-governed-queue-guard.mjs --test` over the 27 paths: NOT GOVERNED. - eslint narrowing: `eslint --no-inline-config --format json` over the 16 changed `.ts` / `.tsx` files gives 0 errors. Warnings equal the base blobs file for file (43; the two new files carry 0). `eslint.config.js` sets no `projectService` / `parserOptions.project`, so no untouched file's verdict can move. The full `pnpm lint` is CI's. ## Acceptance notes - **The bag follows the spec row, which is not the flat mirror.** Measured in the pin. The row keeps `objectName` / `mode` optional; `layout` is `vertical` / `horizontal` only (17.5.0 retired `inline` / `grid`); `recordId` takes a number (objectui#9511 refuses one on the flat mirror, and batch 2's `object-master-detail-form` bag already takes one). The row types `sections` entries as `unknown`, so a section's keys are unjudged inside the bag (the flat mirror closed them). - **`buttons` / `defaults` / `subforms` / `groups`** are the spec's form-VIEW members, not row members, so the bag refuses them. `object-form-unmirrored-members-6152.test.ts` now says so for the three objectui#6152 declared: refused in the bag on both faces, still taken by the object-view `form` slot. The "a section is a closed shape on the strict face" row reads that slot too. - **The TypeScript twin does not name `contentLayout` / `confirmOnDiscard`**, two row members `ModalForm` / `DrawerForm` read; `BaseSchema`'s index signature types them `any`. This is an observation, not filed. - **Governed producer left flat:** `skills/objectui/guides/page-builder.md` ("Form plugin example", `os:check`-marked). It still parses as JSON, which is all `check:skill-examples` asks of a JSON fence, but `objectui validate` now refuses it. It goes through the governed path. - **Hint held by PR objectui#11069:** `packages/cli/src/commands/validate.ts` still suggests `type: 'object-form'` with `objectName` + `sections` flat. - The `objectos-integration.mdx` fragment also dropped `fieldNames` (no such key; the row's is `fields`), a node-level adapter and an `onSubmit` function, none of which the node ever took. --- _Generated by [Claude Code](https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4840168 commit e3782d2

27 files changed

Lines changed: 874 additions & 140 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@object-ui/types': minor
3+
---
4+
5+
feat(types): an authored `object-form` takes its props in the spec's `properties` bag; the flat spelling is refused by name (objectui#10859, batch 4)
6+
7+
**BREAKING (authoring):** an `object-form` node that writes its props on the node itself is now refused by `safeValidateSchema`, `validateSchema` and the strict authoring face, and so by `objectui validate`. Move each prop into the node's `properties` bag:
8+
9+
```json
10+
{ "type": "object-form", "properties": { "objectName": "account", "mode": "create", "fields": ["name", "rating"] } }
11+
```
12+
13+
Nothing changes at render time: `SchemaRenderer` hoists every `properties` key onto the node before `ObjectForm` runs, so the bag node draws what the flat one drew, and a node built in code keeps working with its flat keys.
14+
15+
**Clause-②: yes** — the accept set narrows (a flat `object-form` node that parsed is refused) and widens (a spec-shaped bag node that was refused parses). Released as `minor` under the objectui narrowing rule, with this banner.
16+
17+
**Why.** `@objectstack/spec`'s `ComponentPropsMap['object-form']` row is the published declaration of an authored `object-form` node's props, and the spec's own strict `PageComponentSchema` refuses a prop written on the node as mis-layered (ADR-0089 D3a). The arm here was the flat mirror of the TypeScript `ObjectFormSchema`, so the two validators disagreed both ways: the objectstack showcase's wizard page, written in the bag, was refused (`objectName` `invalid_type`, `mode` `invalid_value`), and the flat node `os validate` refuses was accepted.
18+
19+
**What changed, in observable terms.**
20+
21+
- `@object-ui/types/zod` exports `ObjectFormBlockSchema`, built the way `ObjectMetricBlockSchema` is: `BaseSchema`, the `object-form` literal, and `properties`, which is the spec row by reference, so the row's members, value types and strictness judge the bag. It declares the node's `dataSource` binding (the spec's `ElementDataSourceSchema`), refuses the five handler keys `ObjectForm` reads off the node as runtime slots (objectui#6124), and refuses `body` / `children` (objectui#9256).
22+
- Each member of the row written flat on the node is refused on both faces, with a message that names its bag member (`Did you mean \`mode\` → \`properties.mode\`?`). The refusal set is read off the row, so it follows the spec.
23+
- `ObjectFormBlockSchema` reaches `AnyComponentSchema` through `ObjectQLPublicBlockComponentSchema`. `ObjectFormSchema` leaves `ObjectQLComponentSchema`.
24+
- Inside the bag the spec row decides, and it differs from the flat mirror in a few places. `objectName` and `mode` are optional (a `dataSource` binding can name the object). `layout` takes `vertical` or `horizontal` only (the spec retired `inline` and `grid`; set `columns` for several columns). `recordId` takes a string or a number. `buttons`, `defaults`, `subforms` and `groups` are form-VIEW keys, not members of the row, so the bag refuses them; `buttons`, `defaults` and `subforms` stay authorable in an `object-view`'s `form` slot. The row types `sections` entries as `unknown`, so a section's own keys are not judged inside the bag.
25+
26+
**The same release's `.changeset/6152-object-form-unmirrored-members.md`** declared members on the flat mirror so that the strict face stopped refusing them on an authored node. On an authored node those members are now written in the bag, where the spec row judges them: every one of them except `buttons`, `defaults` and `subforms`, which the row does not declare. Those three stay authorable in an `object-view`'s `form` slot, which is built from the mirror, and a section there is still judged member by member.
27+
28+
**What did not move.** The TypeScript `ObjectFormSchema` and its zod mirror `ObjectFormSchema` stay published and unchanged in shape. They are the node as `ObjectForm` reads it after the hoist, and as code composes it (`ObjectView`, `RecordFormPage`, `ScreenView`, a host mounting `<ObjectForm schema={…}>`). The object-view `form` slot is still built from the mirror.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@object-ui/plugin-form': patch
3+
---
4+
5+
docs(plugin-form): the README's authored `object-form` examples write their props in the `properties` bag (objectui#10859, batch 4)
6+
7+
The multi-step and metadata-route examples now author `{ "type": "object-form", "properties": { … } }`, the spelling `@objectstack/spec`'s `ComponentPropsMap['object-form']` row declares and `objectui validate` now requires. The example that mounts `WizardForm` directly keeps the flat `schema` prop: a component mounted without `SchemaRenderer` receives the node as the renderer reads it after the hoist. No runtime change.

‎README.md‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -326,19 +326,22 @@ declare class MyCustomDataSource<T = unknown> implements DataSource<T> {
326326
**Stop writing repetitive UI code.** A form is a schema, not a component:
327327

328328
```tsx
329-
import type { ObjectFormSchema } from '@object-ui/types';
329+
import type { BaseSchema } from '@object-ui/types';
330330

331331
// Traditional React: useState, validation, handlers, JSX — per form
332332
function UserForm() {
333333
// ...
334334
}
335335

336-
// Object UI: declare it
337-
const schema: ObjectFormSchema = {
336+
// Object UI: declare it — the block's props go in its `properties` bag,
337+
// as `@objectstack/spec` declares them
338+
const schema: BaseSchema = {
338339
type: "object-form",
339-
objectName: "user",
340-
mode: "create",
341-
fields: ["name", "email", "role"]
340+
properties: {
341+
objectName: "user",
342+
mode: "create",
343+
fields: ["name", "email", "role"]
344+
}
342345
}
343346
```
344347

‎content/docs/api/schema-reference.md‎

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -815,30 +815,34 @@ presence enables paging.
815815

816816
A smart form that auto-generates fields from an ObjectQL object. Supports simple, tabbed, wizard, split, drawer, and modal layouts.
817817

818+
An authored `object-form` node takes its props in its `properties` bag, whose members are `@objectstack/spec`'s `ComponentPropsMap['object-form']` row. `objectui validate` judges the bag against that row and refuses a prop written flat on the node by name, naming its bag member, as the spec's own page component does (objectui#10859). `SchemaRenderer` hoists the bag onto the node before `ObjectForm` runs, so `ObjectFormSchema` is the node as the renderer reads it, and the table below lists its members.
819+
818820
```json
819821
{
820822
"type": "object-form",
821-
"objectName": "Contact",
822-
"mode": "create",
823-
"formType": "tabbed",
824-
"title": "New Contact",
825-
"layout": "vertical",
826-
"columns": 2,
827-
"fields": ["firstName", "lastName", "email", "phone", "company"],
828-
"sections": [
829-
{
830-
"label": "Basic Info",
831-
"fields": ["firstName", "lastName", "email"]
832-
},
833-
{
834-
"label": "Details",
835-
"fields": ["phone", "company", "address"]
836-
}
837-
],
838-
"showSubmit": true,
839-
"submitText": "Create Contact",
840-
"showCancel": true,
841-
"cancelText": "Cancel"
823+
"properties": {
824+
"objectName": "Contact",
825+
"mode": "create",
826+
"formType": "tabbed",
827+
"title": "New Contact",
828+
"layout": "vertical",
829+
"columns": 2,
830+
"fields": ["firstName", "lastName", "email", "phone", "company"],
831+
"sections": [
832+
{
833+
"label": "Basic Info",
834+
"fields": ["firstName", "lastName", "email"]
835+
},
836+
{
837+
"label": "Details",
838+
"fields": ["phone", "company", "address"]
839+
}
840+
],
841+
"showSubmit": true,
842+
"submitText": "Create Contact",
843+
"showCancel": true,
844+
"cancelText": "Cancel"
845+
}
842846
}
843847
```
844848

@@ -851,8 +855,8 @@ A smart form that auto-generates fields from an ObjectQL object. Supports simple
851855
| `fields` | `string[]` | Field API names to include (auto-resolved from object metadata). |
852856
| `customFields` | `FormField[]` | Manually defined fields that override auto-generated ones. |
853857
| `sections` | `ObjectFormSection[]` | Field sections (simple groups, tabs, or wizard steps depending on `formType`). Spec-aligned key. |
854-
| `groups` | `array` | **Deprecated.** Legacy alias of `sections` (spec defines `groups` as an alias); normalized into `sections` when `sections` is absent. Legacy shape: `title`→`label`, `defaultCollapsed`→`collapsed`. |
855-
| `layout` | `string` | Label layout: `"vertical"`, `"horizontal"`, `"inline"`, `"grid"`. |
858+
| `groups` | `array` | **Deprecated.** Legacy alias of `sections` (spec defines `groups` as an alias); normalized into `sections` when `sections` is absent. Legacy shape: `title`→`label`, `defaultCollapsed`→`collapsed`. Not a member of the spec's `object-form` row, so an authored bag refuses it; write `sections`. |
859+
| `layout` | `string` | Label layout: `"vertical"`, `"horizontal"`. The spec row retired `"inline"` and `"grid"` (both rendered as `"vertical"`), so an authored bag refuses them; for several columns set `columns`. |
856860
| `columns` | `number` | Number of form columns. |
857861
| `submitText` / `cancelText` | `string \| I18nLabel` | Button labels. |
858862
| `title` / `description` | `string \| I18nLabel` | Heading and subtitle of the drawer and modal presentations. |

‎content/docs/guide/building-crud-app.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,11 @@ carries a data source of its own:
286286
<SchemaRenderer
287287
schema={{
288288
type: 'object-form',
289-
objectName: 'task',
290-
mode: editId ? 'edit' : 'create',
291-
recordId: editId,
289+
properties: {
290+
objectName: 'task',
291+
mode: editId ? 'edit' : 'create',
292+
recordId: editId,
293+
},
292294
}}
293295
onSubmit={() => setShowForm(false)}
294296
onCancel={() => setShowForm(false)}

‎content/docs/guide/objectos-integration.mdx‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -539,19 +539,20 @@ unsubscribe();
539539

540540
### From Appsmith
541541

542-
{/* doc-snippet: fragment — a SHAPE excerpt showing the ObjectUI half of a side-by-side migration comparison; `dataSource` continues the "Using ObjectQL for Queries" block above, and a bare object literal at statement position parses as a block with labels (measured: TS1005 x4) */}
542+
{/* doc-snippet: fragment — a SHAPE excerpt showing the ObjectUI half of a side-by-side migration comparison; a bare object literal at statement position parses as a block with labels */}
543543

544544
```typescript
545-
// Appsmith form → ObjectUI form
545+
// Appsmith form → ObjectUI form. The props go in the node's `properties`
546+
// bag (the spec's `ComponentPropsMap['object-form']` row). The form saves
547+
// through the adapter `SchemaRendererProvider` supplies, so no submit
548+
// handler is written here.
546549
{
547550
type: 'object-form',
548-
objectName: 'contact',
549-
dataSource,
550-
mode: 'create',
551-
fieldNames: ['name', 'email', 'phone', 'company'],
552-
onSubmit: async (data) => {
553-
await dataSource.create('contact', data);
554-
}
551+
properties: {
552+
objectName: 'contact',
553+
mode: 'create',
554+
fields: ['name', 'email', 'phone', 'company'],
555+
},
555556
}
556557
```
557558

‎content/docs/plugins/plugin-form.mdx‎

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -531,26 +531,31 @@ no `multi-step-form` type. The registered type is `object-form`, and `formType:
531531
turns its `sections` into steps; `WizardFormSchema` (exported by
532532
`@object-ui/plugin-form`) declares the shape. Because the wizard resolves its fields from
533533
the object's own metadata, a section lists field **names** rather than field definitions.
534+
Like every `object-form` prop, they go in the node's `properties` bag — the members of
535+
`@objectstack/spec`'s `ComponentPropsMap['object-form']` row. `objectui validate` refuses a
536+
prop written flat on the node by name, as the spec's own page component does.
534537

535538
```json
536539
{
537540
"type": "object-form",
538-
"objectName": "contact",
539-
"mode": "create",
540-
"formType": "wizard",
541-
"showStepIndicator": true,
542-
"sections": [
543-
{
544-
"name": "personal",
545-
"label": "Personal Info",
546-
"fields": ["first_name", "last_name"]
547-
},
548-
{
549-
"name": "contact_details",
550-
"label": "Contact Info",
551-
"fields": ["email", "phone"]
552-
}
553-
]
541+
"properties": {
542+
"objectName": "contact",
543+
"mode": "create",
544+
"formType": "wizard",
545+
"showStepIndicator": true,
546+
"sections": [
547+
{
548+
"name": "personal",
549+
"label": "Personal Info",
550+
"fields": ["first_name", "last_name"]
551+
},
552+
{
553+
"name": "contact_details",
554+
"label": "Contact Info",
555+
"fields": ["email", "phone"]
556+
}
557+
]
558+
}
554559
}
555560
```
556561

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
{
22
"type": "object-form",
3-
"objectName": "users",
4-
"mode": "edit",
5-
"recordId": "1",
6-
"columns": 2,
7-
"fields": ["name", "email", "role", "department", "status"],
8-
"submitText": "Save changes"
3+
"properties": {
4+
"objectName": "users",
5+
"mode": "edit",
6+
"recordId": "1",
7+
"columns": 2,
8+
"fields": ["name", "email", "role", "department", "status"],
9+
"submitText": "Save changes"
10+
}
911
}
Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
{
22
"type": "object-form",
3-
"objectName": "users",
4-
"mode": "edit",
5-
"recordId": "1",
6-
"formType": "tabbed",
7-
"defaultTab": "identity",
8-
"sections": [
9-
{ "name": "identity", "label": "Identity", "columns": 2, "fields": ["name", "email"] },
10-
{ "name": "organisation", "label": "Organisation", "fields": ["department", "created_at"] },
11-
{ "name": "access", "label": "Access", "fields": ["role", "status"] }
12-
],
13-
"submitText": "Save changes"
3+
"properties": {
4+
"objectName": "users",
5+
"mode": "edit",
6+
"recordId": "1",
7+
"formType": "tabbed",
8+
"defaultTab": "identity",
9+
"sections": [
10+
{ "name": "identity", "label": "Identity", "columns": 2, "fields": ["name", "email"] },
11+
{ "name": "organisation", "label": "Organisation", "fields": ["department", "created_at"] },
12+
{ "name": "access", "label": "Access", "fields": ["role", "status"] }
13+
],
14+
"submitText": "Save changes"
15+
}
1416
}

‎examples/schema-catalog/test/catalog-gallery-render.test.tsx‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,12 +1135,15 @@ describe('objectui#6024 — the derivation this pin is built on', () => {
11351135
* its first paint until the fetch lands, which is why this shape settles.)
11361136
*/
11371137
it('the provenance instrument still fails on a surface with no record behind it', async () => {
1138+
// The entries' spelling: the props in the `properties` bag (objectui#10859 batch 4).
11381139
const r = await renderEntry({
11391140
type: 'object-form',
1140-
objectName: 'users',
1141-
mode: 'edit',
1142-
recordId: 'no-such-record',
1143-
fields: ['name', 'email', 'department'],
1141+
properties: {
1142+
objectName: 'users',
1143+
mode: 'edit',
1144+
recordId: 'no-such-record',
1145+
fields: ['name', 'email', 'department'],
1146+
},
11441147
});
11451148
try {
11461149
expect(

0 commit comments

Comments
 (0)