diff --git a/.changeset/olive-pandas-repeat.md b/.changeset/olive-pandas-repeat.md new file mode 100644 index 0000000000..f2079f4654 --- /dev/null +++ b/.changeset/olive-pandas-repeat.md @@ -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). diff --git a/packages/spec/react-declaration-parity.baseline.json b/packages/spec/react-declaration-parity.baseline.json index 51a6586aee..636a4c7424 100644 --- a/packages/spec/react-declaration-parity.baseline.json +++ b/packages/spec/react-declaration-parity.baseline.json @@ -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 --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": { @@ -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 } } } diff --git a/packages/spec/scripts/check-react-blocks-declaration-parity.test.ts b/packages/spec/scripts/check-react-blocks-declaration-parity.test.ts index a751dc7e2e..52820a051c 100644 --- a/packages/spec/scripts/check-react-blocks-declaration-parity.test.ts +++ b/packages/spec/scripts/check-react-blocks-declaration-parity.test.ts @@ -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]); diff --git a/packages/spec/src/ui/react-blocks.ts b/packages/spec/src/ui/react-blocks.ts index 4f9eb7bb9d..94cf05fc27 100644 --- a/packages/spec/src/ui/react-blocks.ts +++ b/packages/spec/src/ui/react-blocks.ts @@ -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', 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 + // `` 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.' }, diff --git a/skills/objectstack-ui/contracts/react-blocks.contract.json b/skills/objectstack-ui/contracts/react-blocks.contract.json index 4354edfa2b..ad1f08234d 100644 --- a/skills/objectstack-ui/contracts/react-blocks.contract.json +++ b/skills/objectstack-ui/contracts/react-blocks.contract.json @@ -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'", diff --git a/skills/objectstack-ui/references/react-blocks.md b/skills/objectstack-ui/references/react-blocks.md index d38add0d96..5088bacaba 100644 --- a/skills/objectstack-ui/references/react-blocks.md +++ b/skills/objectstack-ui/references/react-blocks.md @@ -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` | 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). |