@@ -34,6 +34,11 @@ import {
3434 RowHeightSchema ,
3535 RowColorConfigSchema ,
3636 ListViewSchema ,
37+ // [#21464] `object-form.submitBehavior` is the form view's own member (read
38+ // off `FormViewSchema.shape`, the one place that declares the post-submit
39+ // union): both form renderers switch on exactly that union, so one
40+ // declaration judges the form view and the block.
41+ FormViewSchema ,
3742} from './view.zod' ;
3843// [#21445] `object-grid.bulkActionDefs` is the list view's bulk-action def,
3944// by identity — the element `ListViewSchema.bulkActionDefs` declares.
@@ -5283,6 +5288,44 @@ const OBJECT_FORM_LAYOUT_RETIRED: ReadonlyMap<string, string> = new Map([
52835288 + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.' ] ,
52845289] ) ;
52855290
5291+ /**
5292+ * [#21464] An `object-form`'s `mobile` block — the five members the form reads
5293+ * at the `.objectui-sha` pin `89cad75d55` (`plugin-form/src/ObjectForm.tsx:1857-1942`,
5294+ * the flat arm only), and nothing else. No form view declares the block, so
5295+ * the shape is the read's own, as objectui declares it (`ObjectFormSchema.mobile`):
5296+ *
5297+ * - `stickyActions` pins the submit / cancel bar to the bottom of the viewport
5298+ * (`:1935`, `:1941`);
5299+ * - `stepper` routes a flat form through the wizard: `true` always (a desktop
5300+ * viewport included), `'auto'` only on a phone-width viewport once the form
5301+ * has `stepperMinFields` fields (`:1874-1884`, default 8);
5302+ * `stepperFieldsPerStep` is the fields per step (`:1876`, default 1 — the
5303+ * read clamps a smaller count to 1);
5304+ * - `fullscreenLongText` offers the fullscreen editor on every long-text field
5305+ * (`:1858-1869`).
5306+ *
5307+ * The block's presence marks the wrapper (`data-mobile-form`, `:1942`), so an
5308+ * empty `{}` is legal and changes nothing else. Module-private, as
5309+ * {@link ObjectKanbanLaneSchema} is.
5310+ */
5311+ const ObjectFormMobileSchema = lazySchema ( ( ) => strictObject ( {
5312+ surface : 'this `object-form` `mobile` block' ,
5313+ history :
5314+ 'Until this shape was declared, `mobile` was `z.unknown()`: a misspelled member or a `stepper` '
5315+ + 'value outside `true` / `false` / `\'auto\'` passed, and the form rendered without it.' ,
5316+ } , {
5317+ stickyActions : z . boolean ( ) . optional ( )
5318+ . describe ( 'Pin the submit / cancel bar to the bottom of a phone-width viewport' ) ,
5319+ stepper : z . union ( [ z . boolean ( ) , z . literal ( 'auto' ) ] ) . optional ( )
5320+ . describe ( "One-step-at-a-time wizard for a flat form — `true` always, `'auto'` only on a phone-width viewport once the form has `stepperMinFields` fields, `false` (the default) never" ) ,
5321+ stepperMinFields : z . number ( ) . int ( ) . positive ( ) . optional ( )
5322+ . describe ( "The field count at which `stepper: 'auto'` steps up (default 8)" ) ,
5323+ stepperFieldsPerStep : z . number ( ) . int ( ) . positive ( ) . optional ( )
5324+ . describe ( 'Fields shown per stepper step (default 1)' ) ,
5325+ fullscreenLongText : z . boolean ( ) . optional ( )
5326+ . describe ( 'Offer a fullscreen editor on textarea and rich-text fields' ) ,
5327+ } ) ) ;
5328+
52865329/**
52875330 * `object-form` (objectui `plugin-form/src/ObjectForm.tsx` @ `eb7f586b`, plus
52885331 * the sub-forms it forwards the whole bag into: `TabbedForm`, `WizardForm`,
@@ -5318,8 +5361,36 @@ export const ObjectFormPropsSchema = lazySchema(() => strictObject({
53185361 } ) . optional ( )
53195362 . describe ( "Field layout — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set `columns`" ) ,
53205363 columns : z . number ( ) . optional ( ) . describe ( 'Number of field columns (multi-column forms), honoured under either `layout`' ) ,
5364+ /**
5365+ * [#21464] HELD at `z.unknown()` entries, in the enumeration pin's ledger as
5366+ * held for a ruling. The renderer's own declared type is field-name strings
5367+ * (objectui `ObjectFormSchema.fields: string[]`), but its read also draws a
5368+ * `{ name }` entry by that name (`plugin-form/src/ObjectForm.tsx:961-981`,
5369+ * `flatFields.ts:71-79` at the pin `89cad75d55`), and measured writers author
5370+ * one — objectui's published page-builder guide among them — so typing the
5371+ * member to strings would refuse a value the form draws.
5372+ */
53215373 fields : z . array ( z . unknown ( ) ) . optional ( ) . describe ( 'Limit/order the fields shown' ) ,
5374+ /**
5375+ * [#21464] Kept `z.unknown()`, in the enumeration pin's ledger with the
5376+ * objectui-held contracts: each member is objectui's runtime form field
5377+ * (`FormField`, identity key `name`), drawn whole by the form renderer
5378+ * (`plugin-form/src/customFieldsMerge.ts:78-108` at the pin `89cad75d55`).
5379+ * The spec declares no such field — its own form field (`FormFieldSchema`,
5380+ * `view.zod.ts`) is keyed by `field` and is never matched by the merge — so
5381+ * the spec declares that contract first, then this member takes it.
5382+ */
53225383 customFields : z . unknown ( ) . optional ( ) . describe ( 'Custom field definitions merged into the generated set' ) ,
5384+ /**
5385+ * [#21464] HELD at `z.unknown()` entries, in the enumeration pin's ledger as
5386+ * held for a ruling. The form view's own `sections` (`FormSectionSchema`) is
5387+ * the by-reference shape, and every section key the renderer reads is
5388+ * declared there, but a section's `fields` also draws an inline runtime form
5389+ * field `{ name, type, … }` as it stands (`plugin-form/src/sectionFields.ts:367-369`
5390+ * at the pin `89cad75d55`, its "shape 3"), which the form view's field entry
5391+ * (keyed by `field`) refuses; objectui's README and its own pins author that
5392+ * shape and assert that it renders and submits.
5393+ */
53235394 sections : z . array ( z . unknown ( ) ) . optional ( )
53245395 . describe ( 'Form sections ({ label, description?, fields } — wizard steps / tab panes)' ) ,
53255396 title : I18nLabelSchema . optional ( ) . describe ( 'Form title' ) ,
@@ -5335,7 +5406,15 @@ export const ObjectFormPropsSchema = lazySchema(() => strictObject({
53355406 drawerWidth : z . union ( [ z . string ( ) , z . number ( ) ] ) . optional ( ) . describe ( 'Drawer width (drawer)' ) ,
53365407 modalSize : z . enum ( [ 'sm' , 'default' , 'lg' , 'xl' , 'full' ] ) . optional ( ) . describe ( 'Modal size (modal)' ) ,
53375408 modalCloseButton : z . boolean ( ) . optional ( ) . describe ( 'Show the modal close button (modal)' ) ,
5338- contentLayout : z . unknown ( ) . optional ( ) . describe ( 'Modal content layout config (modal)' ) ,
5409+ /**
5410+ * [#21464] Typed to the read: the modal presentation is its only reader,
5411+ * and it tests `schema.contentLayout === 'tabbed' && groups.length > 1`
5412+ * (`plugin-form/src/ModalForm.tsx:854` at the pin `89cad75d55`, declared
5413+ * `'simple' | 'tabbed'` at `:151`); any other value stacks the sections, so
5414+ * a misspelled `'tabs'` used to draw the stacked layout with no report.
5415+ */
5416+ contentLayout : z . enum ( [ 'simple' , 'tabbed' ] ) . optional ( )
5417+ . describe ( "How the modal presentation lays out its sections (modal) — 'simple' stacks them (the default); 'tabbed' puts each section on its own tab once more than one section has a field to show" ) ,
53395418 confirmOnDiscard : z . boolean ( ) . optional ( ) . describe ( 'Confirm before discarding edits (drawer/modal)' ) ,
53405419 submitText : I18nLabelSchema . optional ( ) . describe ( 'Submit button label' ) ,
53415420 cancelText : I18nLabelSchema . optional ( ) . describe ( 'Cancel button label' ) ,
@@ -5344,15 +5423,42 @@ export const ObjectFormPropsSchema = lazySchema(() => strictObject({
53445423 showSubmit : z . boolean ( ) . optional ( ) . describe ( 'Show the submit button' ) ,
53455424 showCancel : z . boolean ( ) . optional ( ) . describe ( 'Show the cancel button' ) ,
53465425 showReset : z . boolean ( ) . optional ( ) . describe ( 'Show the reset button' ) ,
5347- submitBehavior : z . unknown ( ) . optional ( )
5348- . describe ( "What happens after a successful submit ({ kind: 'thank-you' | …, title?, message? })" ) ,
5426+ /**
5427+ * [#21464] The form view's own `submitBehavior` member, by reference
5428+ * (`FormViewSchema.shape.submitBehavior`, the union discriminated on
5429+ * `kind`). Both form renderers switch on `kind` and read exactly that
5430+ * union's members — `url` and `delayMs` on `redirect`, `title` and `message`
5431+ * on `thank-you` (`plugin-form/src/ObjectForm.tsx:1312-1370` and
5432+ * `WizardForm.tsx:1005-1065` at the pin `89cad75d55`) — and judge a redirect
5433+ * `url` at submit by parsing a form view through this same schema
5434+ * (`submitRedirect.ts:202`), so one declaration judges the form view, this
5435+ * block and the renderer's own verdict. A `kind` outside the four fell
5436+ * through to the thank-you arm in silence.
5437+ */
5438+ submitBehavior : FormViewSchema . shape . submitBehavior
5439+ . describe ( "What happens after a successful submit — the same block a form view's `submitBehavior` declares: `{ kind: 'thank-you', title?, message? }`, `{ kind: 'redirect', url, delayMs? }` (a relative `url`, interpolating declared record fields as `{{record.field_name}}`), `{ kind: 'continue' }` or `{ kind: 'next-record' }`. Takes precedence over `navigateOnSuccess` and `resetOnSuccess`" ) ,
53495440 successMessage : I18nLabelSchema . optional ( ) . describe ( 'Toast message on successful submit' ) ,
53505441 resetOnSuccess : z . boolean ( ) . optional ( ) . describe ( 'Reset the form after a successful submit' ) ,
5351- navigateOnSuccess : z . unknown ( ) . optional ( ) . describe ( 'Navigate after a successful submit' ) ,
5442+ /**
5443+ * [#21464] Typed to the read: `resolveSuccessNavigate`
5444+ * (`plugin-form/src/successBehavior.ts:118-131` at the pin `89cad75d55`,
5445+ * called from `ObjectForm.tsx:1373` and `WizardForm.tsx:1071`) takes a
5446+ * string template, replaces `{id}` / `{recordId}` with the saved record's
5447+ * id (URL-escaped) and follows the result only when it is a relative
5448+ * reference; a declared value it refuses is reported on the success toast
5449+ * (`ObjectForm.tsx:1412-1429`). Any other value reached `template.replace`
5450+ * after the record had been written, and the submit then reported a failure.
5451+ * It is read only when `submitBehavior` is absent, and the renderer marks it
5452+ * deprecated in that key's favour.
5453+ */
5454+ navigateOnSuccess : z . string ( ) . optional ( )
5455+ . describe ( "Relative path to navigate to after a successful create/update — `{id}` / `{recordId}` are replaced with the saved record's id, URL-escaped; an absolute URL is refused at submit and reported on the success toast. Ignored when `submitBehavior` is set — prefer `submitBehavior`" ) ,
53525456 readOnly : z . boolean ( ) . optional ( ) . describe ( 'Render every field read-only' ) ,
53535457 initialValues : z . record ( z . string ( ) , z . unknown ( ) ) . optional ( ) . describe ( 'Prefill values (create mode)' ) ,
53545458 initialData : z . record ( z . string ( ) , z . unknown ( ) ) . optional ( ) . describe ( 'Alternate spelling of `initialValues` the renderer also reads' ) ,
5355- mobile : z . unknown ( ) . optional ( ) . describe ( 'Mobile presentation overrides' ) ,
5459+ /** [#21464] The five members the form reads — see {@link ObjectFormMobileSchema}. */
5460+ mobile : ObjectFormMobileSchema . optional ( )
5461+ . describe ( 'Phone presentation options, each opt-in — `{ stickyActions?, stepper?, stepperMinFields?, stepperFieldsPerStep?, fullscreenLongText? }`. Read by the flat (simple, sectionless) form' ) ,
53565462} ) ) ;
53575463/** Author state (ADR-0122: the bare name is the author state). */
53585464export type ObjectFormProps = z . input < typeof ObjectFormPropsSchema > ;
@@ -5503,6 +5609,13 @@ export const ObjectMasterDetailFormPropsSchema = lazySchema(() => strictObject({
55035609 error : ( issue ) =>
55045610 typeof issue . input === 'string' ? MASTER_DETAIL_FORM_TYPE_RETIRED . get ( issue . input ) : undefined ,
55055611 } ) . optional ( ) . describe ( "Parent form presentation — the two variants the renderer honours for the parent half" ) ,
5612+ /**
5613+ * [#21464] `sections` and `fields` are handed to the parent `object-form`
5614+ * verbatim (`plugin-form/src/MasterDetailForm.tsx:1692-1693` at the pin
5615+ * `89cad75d55`), so each is read exactly as that block's member is — and is
5616+ * HELD with it, in the enumeration pin's ledger, for the same reason (see
5617+ * {@link ObjectFormPropsSchema}'s `sections` and `fields`).
5618+ */
55065619 sections : z . array ( z . unknown ( ) ) . optional ( ) . describe ( 'Parent form sections' ) ,
55075620 fields : z . array ( z . unknown ( ) ) . optional ( ) . describe ( 'Parent fields shown' ) ,
55085621 details : z . array ( masterDetailDetailEntry ( ) ) . optional ( )
0 commit comments