Skip to content

Commit d410d69

Browse files
committed
wip(spec): object-form contentLayout, submitBehavior, navigateOnSuccess and mobile typed; fields, sections held (#21464, stage 3)
Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
1 parent e909aa0 commit d410d69

1 file changed

Lines changed: 118 additions & 5 deletions

File tree

‎packages/spec/src/ui/component.zod.ts‎

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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). */
53585464
export 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

Comments
 (0)