Skip to content

Commit e2899f6

Browse files
os-elonclaude
andauthored
feat(spec): declare 14 registry-published ObjectForm props on the react-blocks tier (#9392) (#9728)
Catch-up with the registry inputs objectui#4648/#4901 published on the object-form registration: 14 keys are declared on the react-blocks ObjectForm overlay (descriptions adapted from objectui's registration), and 3 are baselined with recorded reasons per the maintainer's 2026-08-18 mixed-disposition ruling — initialData (alias of initialValues), mobile (internal override), navigateOnSuccess (parked pending the action-success- navigation family; revisit tracked on #9392). The parity baseline is regenerated via --update, which also snapshots the six SDUI object-* blocks (#7751) as additive coverage; the clean-baseline test now derives that tail from the committed file. Claude-Session: https://claude.ai/code/session_016D9wdJR14KKCxz1WgdAzcw Co-authored-by: Claude <noreply@anthropic.com>
1 parent 079b457 commit e2899f6

6 files changed

Lines changed: 194 additions & 2 deletions

File tree

‎.changeset/olive-pandas-repeat.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
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.
6+
7+
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).

‎packages/spec/react-declaration-parity.baseline.json‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
{
22
"_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).",
3+
"_acceptedReasons": {
4+
"_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.",
5+
"ObjectForm.initialData": "Alias spelling of `initialValues` (the drawer/modal presentations read it first). Aliases are not promoted into spec, per the alias-convergence direction.",
6+
"ObjectForm.mobile": "Internal presentation override, not an authoring surface.",
7+
"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."
8+
},
39
"blocks": {
410
"ObjectForm": {
5-
"registryOnly": [],
11+
"registryOnly": [
12+
"initialData",
13+
"mobile",
14+
"navigateOnSuccess"
15+
],
616
"missing": false
717
},
818
"ListView": {
@@ -12,6 +22,30 @@
1222
"ObjectChart": {
1323
"registryOnly": [],
1424
"missing": false
25+
},
26+
"object-calendar": {
27+
"registryOnly": [],
28+
"missing": false
29+
},
30+
"object-form": {
31+
"registryOnly": [],
32+
"missing": false
33+
},
34+
"object-grid": {
35+
"registryOnly": [],
36+
"missing": false
37+
},
38+
"object-kanban": {
39+
"registryOnly": [],
40+
"missing": false
41+
},
42+
"object-master-detail-form": {
43+
"registryOnly": [],
44+
"missing": false
45+
},
46+
"object-metric": {
47+
"registryOnly": [],
48+
"missing": false
1549
}
1650
}
1751
}

‎packages/spec/scripts/check-react-blocks-declaration-parity.test.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,24 @@ describe('check:react-declaration-parity — the blind spot is stated, every run
182182
const baseline = path.join(PKG, 'react-declaration-parity.baseline.json');
183183
// Every baselined block must be present, or the run reports them vanished
184184
// instead of clean. Each declares only spec props, so registry-only is empty
185-
// — the committed baseline's accepted state.
185+
// — the committed baseline's accepted state. Since #9392's `--update` the
186+
// committed baseline also carries the SDUI `object-*` blocks (#7751 put
187+
// them in `current`, so an accept snapshots them too), hence the derived
188+
// tail: a future baselined block is covered here the day it lands instead
189+
// of reporting as vanished.
190+
const committed: BaselineFile = JSON.parse(fs.readFileSync(baseline, 'utf8'));
186191
const manifest: Manifest = {
187192
components: {
193+
// The react blocks are keyed by PascalCase tag in the baseline but by
194+
// schemaType in the manifest — spell those three out.
188195
...manifestFor('object-form', [SCHEMA_PROP]).components,
189196
...manifestFor('list-view', []).components,
190197
...manifestFor('object-chart', []).components,
198+
...Object.fromEntries(
199+
Object.keys(committed.blocks)
200+
.filter((k) => k.startsWith('object-') && k !== 'object-form')
201+
.map((type) => Object.entries(manifestFor(type, []).components)[0]),
202+
),
191203
},
192204
};
193205
const out = run(manifest, ['--baseline', baseline]);

‎packages/spec/src/ui/react-blocks.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,33 @@ export const REACT_BLOCKS: ReactBlockDef[] = [
199199
{ name: 'recordId', type: 'string | number', kind: 'controlled', description: 'Which record to load (edit/view). The hook for master/detail.' },
200200
{ name: 'fields', type: 'string[]', kind: 'binding', description: 'Limit/order the fields shown (defaults to the object form fields).' },
201201
{ name: 'initialValues', type: 'Record<string, any>', kind: 'binding', description: 'Prefill values in create mode.' },
202+
// #9392 — catch-up with the registry inputs objectui#4648/#4901 published
203+
// for keys ObjectFormRenderer already read (descriptions adapted from the
204+
// `object-form` registration in objectui `plugin-form/src/index.tsx`, the
205+
// one registration both parity channels enumerate). Published from the
206+
// OVERLAY, not FormViewSchema `dataProps`, for the same reason as
207+
// `<ObjectChart drillDown>` above: FormViewSchema is also what a
208+
// declarative form VIEW parses, and only the react/registry tier
209+
// measurably reads these. Three registry inputs are deliberately NOT
210+
// declared (baselined in react-declaration-parity.baseline.json):
211+
// `initialData` (alias spelling of `initialValues` — aliases are not
212+
// promoted into spec), `mobile` (internal override, not an authoring
213+
// surface), `navigateOnSuccess` (parked pending the action-success-
214+
// navigation family ruling — see the baseline note on #9392).
215+
{ 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.' },
216+
{ name: 'readOnly', type: 'boolean', kind: 'binding', description: 'Render every field read-only, whatever `mode` says.' },
217+
{ name: 'modalCloseButton', type: 'boolean', kind: 'binding', description: "Show the modal presentation's close button (formType 'modal'; honoured by the modal form)." },
218+
{ 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'." },
219+
{ name: 'confirmOnDiscard', type: 'boolean', kind: 'binding', description: 'Ask before discarding unsaved edits when a drawer/modal form is dismissed. Set false to close immediately.' },
220+
{ name: 'submitText', type: 'string', kind: 'binding', description: 'Label of the submit button.' },
221+
{ name: 'cancelText', type: 'string', kind: 'binding', description: 'Label of the cancel button.' },
222+
{ name: 'nextText', type: 'string', kind: 'binding', description: 'Label of the next-step button (wizard).' },
223+
{ name: 'prevText', type: 'string', kind: 'binding', description: 'Label of the previous-step button (wizard).' },
224+
{ name: 'showSubmit', type: 'boolean', kind: 'binding', description: 'Show the submit button.' },
225+
{ name: 'showCancel', type: 'boolean', kind: 'binding', description: 'Show the cancel button.' },
226+
{ name: 'showReset', type: 'boolean', kind: 'binding', description: 'Show the reset button.' },
227+
{ name: 'successMessage', type: 'string', kind: 'binding', description: 'Toast shown after a successful submit. Ignored when `submitBehavior` is set.' },
228+
{ name: 'resetOnSuccess', type: 'boolean', kind: 'binding', description: 'Clear the form after a successful submit instead of keeping the saved values.' },
202229
{ name: 'onSuccess', type: '(record) => void', kind: 'callback', description: 'Called after a successful save with the saved record (e.g. close a panel + reload).' },
203230
{ name: 'onError', type: '(error: Error) => void', kind: 'callback', description: 'Called when the save fails.' },
204231
{ name: 'onCancel', type: '() => void', kind: 'callback', description: 'Called when the user cancels.' },

‎skills/objectstack-ui/contracts/react-blocks.contract.json‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,104 @@
3838
"required": false,
3939
"description": "Prefill values in create mode."
4040
},
41+
{
42+
"name": "customFields",
43+
"type": "any[]",
44+
"kind": "binding",
45+
"required": false,
46+
"description": "Field definitions merged over the set generated from object metadata. With inline definitions and no data source, this becomes the only field source."
47+
},
48+
{
49+
"name": "readOnly",
50+
"type": "boolean",
51+
"kind": "binding",
52+
"required": false,
53+
"description": "Render every field read-only, whatever `mode` says."
54+
},
55+
{
56+
"name": "modalCloseButton",
57+
"type": "boolean",
58+
"kind": "binding",
59+
"required": false,
60+
"description": "Show the modal presentation's close button (formType 'modal'; honoured by the modal form)."
61+
},
62+
{
63+
"name": "contentLayout",
64+
"type": "'simple' | 'tabbed'",
65+
"kind": "binding",
66+
"required": false,
67+
"description": "How the modal presentation lays out sections; 'tabbed' needs more than one section to differ from 'simple'."
68+
},
69+
{
70+
"name": "confirmOnDiscard",
71+
"type": "boolean",
72+
"kind": "binding",
73+
"required": false,
74+
"description": "Ask before discarding unsaved edits when a drawer/modal form is dismissed. Set false to close immediately."
75+
},
76+
{
77+
"name": "submitText",
78+
"type": "string",
79+
"kind": "binding",
80+
"required": false,
81+
"description": "Label of the submit button."
82+
},
83+
{
84+
"name": "cancelText",
85+
"type": "string",
86+
"kind": "binding",
87+
"required": false,
88+
"description": "Label of the cancel button."
89+
},
90+
{
91+
"name": "nextText",
92+
"type": "string",
93+
"kind": "binding",
94+
"required": false,
95+
"description": "Label of the next-step button (wizard)."
96+
},
97+
{
98+
"name": "prevText",
99+
"type": "string",
100+
"kind": "binding",
101+
"required": false,
102+
"description": "Label of the previous-step button (wizard)."
103+
},
104+
{
105+
"name": "showSubmit",
106+
"type": "boolean",
107+
"kind": "binding",
108+
"required": false,
109+
"description": "Show the submit button."
110+
},
111+
{
112+
"name": "showCancel",
113+
"type": "boolean",
114+
"kind": "binding",
115+
"required": false,
116+
"description": "Show the cancel button."
117+
},
118+
{
119+
"name": "showReset",
120+
"type": "boolean",
121+
"kind": "binding",
122+
"required": false,
123+
"description": "Show the reset button."
124+
},
125+
{
126+
"name": "successMessage",
127+
"type": "string",
128+
"kind": "binding",
129+
"required": false,
130+
"description": "Toast shown after a successful submit. Ignored when `submitBehavior` is set."
131+
},
132+
{
133+
"name": "resetOnSuccess",
134+
"type": "boolean",
135+
"kind": "binding",
136+
"required": false,
137+
"description": "Clear the form after a successful submit instead of keeping the saved values."
138+
},
41139
{
42140
"name": "mode",
43141
"type": "'create' | 'edit' | 'view'",

‎skills/objectstack-ui/references/react-blocks.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ Server-connected create/edit/view form for one object. Config props come from th
2121
| `formType` | `'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'` | binding | | Form presentation; drawer/modal render the form in a built-in overlay (use drawerSide/drawerWidth/modalSize). |
2222
| `fields` | `string[]` | binding | | Limit/order the fields shown (defaults to the object form fields). |
2323
| `initialValues` | `Record<string, any>` | binding | | Prefill values in create mode. |
24+
| `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. |
25+
| `readOnly` | `boolean` | binding | | Render every field read-only, whatever `mode` says. |
26+
| `modalCloseButton` | `boolean` | binding | | Show the modal presentation's close button (formType 'modal'; honoured by the modal form). |
27+
| `contentLayout` | `'simple' \| 'tabbed'` | binding | | How the modal presentation lays out sections; 'tabbed' needs more than one section to differ from 'simple'. |
28+
| `confirmOnDiscard` | `boolean` | binding | | Ask before discarding unsaved edits when a drawer/modal form is dismissed. Set false to close immediately. |
29+
| `submitText` | `string` | binding | | Label of the submit button. |
30+
| `cancelText` | `string` | binding | | Label of the cancel button. |
31+
| `nextText` | `string` | binding | | Label of the next-step button (wizard). |
32+
| `prevText` | `string` | binding | | Label of the previous-step button (wizard). |
33+
| `showSubmit` | `boolean` | binding | | Show the submit button. |
34+
| `showCancel` | `boolean` | binding | | Show the cancel button. |
35+
| `showReset` | `boolean` | binding | | Show the reset button. |
36+
| `successMessage` | `string` | binding | | Toast shown after a successful submit. Ignored when `submitBehavior` is set. |
37+
| `resetOnSuccess` | `boolean` | binding | | Clear the form after a successful submit instead of keeping the saved values. |
2438
| `mode` | `'create' \| 'edit' \| 'view'` | controlled | | Create a new record, or edit/view an existing one — drive from React state. |
2539
| `recordId` | `string \| number` | controlled | | Which record to load (edit/view). The hook for master/detail. |
2640
| `onSuccess` | `(record) => void` | callback | | Called after a successful save with the saved record (e.g. close a panel + reload). |

0 commit comments

Comments
 (0)