Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/olive-pandas-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@objectstack/spec': minor
---

Declare 14 registry-published props on the react-tier `ObjectForm` block (ADR-0082 D4 declaration parity, #9392): `modalCloseButton`, `contentLayout`, `confirmOnDiscard`, `customFields`, `readOnly`, `submitText`, `cancelText`, `nextText`, `prevText`, `showSubmit`, `showCancel`, `showReset`, `successMessage`, `resetOnSuccess` — the inputs objectui#4648/objectui#4901 published on the `object-form` registration that the react-blocks channel of the spec never declared. Descriptions are adapted from objectui's own registration; the generated react-blocks contract (`skills/objectstack-ui`) picks them up.

Three registry inputs are deliberately NOT declared and are instead baselined with recorded reasons (maintainer ruling 2026-08-18 on #9392): `initialData` (alias spelling of `initialValues` — aliases are not promoted into spec), `mobile` (internal presentation override, not an authoring surface), and `navigateOnSuccess` (parked pending the action-success-navigation family ruling; revisit tracked on #9392).
36 changes: 35 additions & 1 deletion packages/spec/react-declaration-parity.baseline.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
{
"_comment": "Accepted spec↔registry DECLARATION-PARITY baseline (react blocks). Per block: the registry-only input set (the registry config declares it, the spec does not) and whether the block is missing. Regenerate with: MANIFEST=… check:react-declaration-parity --baseline <this> --update. The ratchet flags only NEW registry-only inputs or newly-missing blocks. It compares two declarations and inspects no renderer, so a prop both sides declare and nothing reads records as agreement here (#4413/#4472).",
"_acceptedReasons": {
"_note": "Hand-maintained record of WHY each accepted registry-only input is deliberately not declared in spec (maintainer ruling 2026-08-18 on #9392). `--update` rewrites this file and drops this block — re-add it (updated) whenever the accepted set changes; an entry with no reason here is an accept nobody ruled.",
"ObjectForm.initialData": "Alias spelling of `initialValues` (the drawer/modal presentations read it first). Aliases are not promoted into spec, per the alias-convergence direction.",
"ObjectForm.mobile": "Internal presentation override, not an authoring surface.",
"ObjectForm.navigateOnSuccess": "Explicitly parked pending the 'action success navigation' family ruling (#9474/#9566, objectui#5034) — declaring it now risks freezing a spelling that family is shaping; revisit when `onSuccess` lands. The family landed on 2026-08-18 (PR #9601); the revisit is tracked on #9392."
},
"blocks": {
"ObjectForm": {
"registryOnly": [],
"registryOnly": [
"initialData",
"mobile",
"navigateOnSuccess"
],
"missing": false
},
"ListView": {
Expand All @@ -12,6 +22,30 @@
"ObjectChart": {
"registryOnly": [],
"missing": false
},
"object-calendar": {
"registryOnly": [],
"missing": false
},
"object-form": {
"registryOnly": [],
"missing": false
},
"object-grid": {
"registryOnly": [],
"missing": false
},
"object-kanban": {
"registryOnly": [],
"missing": false
},
"object-master-detail-form": {
"registryOnly": [],
"missing": false
},
"object-metric": {
"registryOnly": [],
"missing": false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,24 @@ describe('check:react-declaration-parity — the blind spot is stated, every run
const baseline = path.join(PKG, 'react-declaration-parity.baseline.json');
// Every baselined block must be present, or the run reports them vanished
// instead of clean. Each declares only spec props, so registry-only is empty
// — the committed baseline's accepted state.
// — the committed baseline's accepted state. Since #9392's `--update` the
// committed baseline also carries the SDUI `object-*` blocks (#7751 put
// them in `current`, so an accept snapshots them too), hence the derived
// tail: a future baselined block is covered here the day it lands instead
// of reporting as vanished.
const committed: BaselineFile = JSON.parse(fs.readFileSync(baseline, 'utf8'));
const manifest: Manifest = {
components: {
// The react blocks are keyed by PascalCase tag in the baseline but by
// schemaType in the manifest — spell those three out.
...manifestFor('object-form', [SCHEMA_PROP]).components,
...manifestFor('list-view', []).components,
...manifestFor('object-chart', []).components,
...Object.fromEntries(
Object.keys(committed.blocks)
.filter((k) => k.startsWith('object-') && k !== 'object-form')
.map((type) => Object.entries(manifestFor(type, []).components)[0]),
),
},
};
const out = run(manifest, ['--baseline', baseline]);
Expand Down
27 changes: 27 additions & 0 deletions packages/spec/src/ui/react-blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,33 @@ export const REACT_BLOCKS: ReactBlockDef[] = [
{ name: 'recordId', type: 'string | number', kind: 'controlled', description: 'Which record to load (edit/view). The hook for master/detail.' },
{ name: 'fields', type: 'string[]', kind: 'binding', description: 'Limit/order the fields shown (defaults to the object form fields).' },
{ name: 'initialValues', type: 'Record<string, any>', kind: 'binding', description: 'Prefill values in create mode.' },
// #9392 — catch-up with the registry inputs objectui#4648/#4901 published
// for keys ObjectFormRenderer already read (descriptions adapted from the
// `object-form` registration in objectui `plugin-form/src/index.tsx`, the
// one registration both parity channels enumerate). Published from the
// OVERLAY, not FormViewSchema `dataProps`, for the same reason as
// `<ObjectChart drillDown>` above: FormViewSchema is also what a
// declarative form VIEW parses, and only the react/registry tier
// measurably reads these. Three registry inputs are deliberately NOT
// declared (baselined in react-declaration-parity.baseline.json):
// `initialData` (alias spelling of `initialValues` — aliases are not
// promoted into spec), `mobile` (internal override, not an authoring
// surface), `navigateOnSuccess` (parked pending the action-success-
// navigation family ruling — see the baseline note on #9392).
{ name: 'customFields', type: 'any[]', kind: 'binding', description: 'Field definitions merged over the set generated from object metadata. With inline definitions and no data source, this becomes the only field source.' },
{ name: 'readOnly', type: 'boolean', kind: 'binding', description: 'Render every field read-only, whatever `mode` says.' },
{ name: 'modalCloseButton', type: 'boolean', kind: 'binding', description: "Show the modal presentation's close button (formType 'modal'; honoured by the modal form)." },
{ name: 'contentLayout', type: "'simple' | 'tabbed'", kind: 'binding', description: "How the modal presentation lays out sections; 'tabbed' needs more than one section to differ from 'simple'." },
{ name: 'confirmOnDiscard', type: 'boolean', kind: 'binding', description: 'Ask before discarding unsaved edits when a drawer/modal form is dismissed. Set false to close immediately.' },
{ name: 'submitText', type: 'string', kind: 'binding', description: 'Label of the submit button.' },
{ name: 'cancelText', type: 'string', kind: 'binding', description: 'Label of the cancel button.' },
{ name: 'nextText', type: 'string', kind: 'binding', description: 'Label of the next-step button (wizard).' },
{ name: 'prevText', type: 'string', kind: 'binding', description: 'Label of the previous-step button (wizard).' },
{ name: 'showSubmit', type: 'boolean', kind: 'binding', description: 'Show the submit button.' },
{ name: 'showCancel', type: 'boolean', kind: 'binding', description: 'Show the cancel button.' },
{ name: 'showReset', type: 'boolean', kind: 'binding', description: 'Show the reset button.' },
{ name: 'successMessage', type: 'string', kind: 'binding', description: 'Toast shown after a successful submit. Ignored when `submitBehavior` is set.' },
{ name: 'resetOnSuccess', type: 'boolean', kind: 'binding', description: 'Clear the form after a successful submit instead of keeping the saved values.' },
{ name: 'onSuccess', type: '(record) => void', kind: 'callback', description: 'Called after a successful save with the saved record (e.g. close a panel + reload).' },
{ name: 'onError', type: '(error: Error) => void', kind: 'callback', description: 'Called when the save fails.' },
{ name: 'onCancel', type: '() => void', kind: 'callback', description: 'Called when the user cancels.' },
Expand Down
98 changes: 98 additions & 0 deletions skills/objectstack-ui/contracts/react-blocks.contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,104 @@
"required": false,
"description": "Prefill values in create mode."
},
{
"name": "customFields",
"type": "any[]",
"kind": "binding",
"required": false,
"description": "Field definitions merged over the set generated from object metadata. With inline definitions and no data source, this becomes the only field source."
},
{
"name": "readOnly",
"type": "boolean",
"kind": "binding",
"required": false,
"description": "Render every field read-only, whatever `mode` says."
},
{
"name": "modalCloseButton",
"type": "boolean",
"kind": "binding",
"required": false,
"description": "Show the modal presentation's close button (formType 'modal'; honoured by the modal form)."
},
{
"name": "contentLayout",
"type": "'simple' | 'tabbed'",
"kind": "binding",
"required": false,
"description": "How the modal presentation lays out sections; 'tabbed' needs more than one section to differ from 'simple'."
},
{
"name": "confirmOnDiscard",
"type": "boolean",
"kind": "binding",
"required": false,
"description": "Ask before discarding unsaved edits when a drawer/modal form is dismissed. Set false to close immediately."
},
{
"name": "submitText",
"type": "string",
"kind": "binding",
"required": false,
"description": "Label of the submit button."
},
{
"name": "cancelText",
"type": "string",
"kind": "binding",
"required": false,
"description": "Label of the cancel button."
},
{
"name": "nextText",
"type": "string",
"kind": "binding",
"required": false,
"description": "Label of the next-step button (wizard)."
},
{
"name": "prevText",
"type": "string",
"kind": "binding",
"required": false,
"description": "Label of the previous-step button (wizard)."
},
{
"name": "showSubmit",
"type": "boolean",
"kind": "binding",
"required": false,
"description": "Show the submit button."
},
{
"name": "showCancel",
"type": "boolean",
"kind": "binding",
"required": false,
"description": "Show the cancel button."
},
{
"name": "showReset",
"type": "boolean",
"kind": "binding",
"required": false,
"description": "Show the reset button."
},
{
"name": "successMessage",
"type": "string",
"kind": "binding",
"required": false,
"description": "Toast shown after a successful submit. Ignored when `submitBehavior` is set."
},
{
"name": "resetOnSuccess",
"type": "boolean",
"kind": "binding",
"required": false,
"description": "Clear the form after a successful submit instead of keeping the saved values."
},
{
"name": "mode",
"type": "'create' | 'edit' | 'view'",
Expand Down
14 changes: 14 additions & 0 deletions skills/objectstack-ui/references/react-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ Server-connected create/edit/view form for one object. Config props come from th
| `formType` | `'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'` | binding | | Form presentation; drawer/modal render the form in a built-in overlay (use drawerSide/drawerWidth/modalSize). |
| `fields` | `string[]` | binding | | Limit/order the fields shown (defaults to the object form fields). |
| `initialValues` | `Record<string, any>` | binding | | Prefill values in create mode. |
| `customFields` | `any[]` | binding | | Field definitions merged over the set generated from object metadata. With inline definitions and no data source, this becomes the only field source. |
| `readOnly` | `boolean` | binding | | Render every field read-only, whatever `mode` says. |
| `modalCloseButton` | `boolean` | binding | | Show the modal presentation's close button (formType 'modal'; honoured by the modal form). |
| `contentLayout` | `'simple' \| 'tabbed'` | binding | | How the modal presentation lays out sections; 'tabbed' needs more than one section to differ from 'simple'. |
| `confirmOnDiscard` | `boolean` | binding | | Ask before discarding unsaved edits when a drawer/modal form is dismissed. Set false to close immediately. |
| `submitText` | `string` | binding | | Label of the submit button. |
| `cancelText` | `string` | binding | | Label of the cancel button. |
| `nextText` | `string` | binding | | Label of the next-step button (wizard). |
| `prevText` | `string` | binding | | Label of the previous-step button (wizard). |
| `showSubmit` | `boolean` | binding | | Show the submit button. |
| `showCancel` | `boolean` | binding | | Show the cancel button. |
| `showReset` | `boolean` | binding | | Show the reset button. |
| `successMessage` | `string` | binding | | Toast shown after a successful submit. Ignored when `submitBehavior` is set. |
| `resetOnSuccess` | `boolean` | binding | | Clear the form after a successful submit instead of keeping the saved values. |
| `mode` | `'create' \| 'edit' \| 'view'` | controlled | | Create a new record, or edit/view an existing one — drive from React state. |
| `recordId` | `string \| number` | controlled | | Which record to load (edit/view). The hook for master/detail. |
| `onSuccess` | `(record) => void` | callback | | Called after a successful save with the saved record (e.g. close a panel + reload). |
Expand Down
Loading