diff --git a/content/docs/protocol/objectui/actions.mdx b/content/docs/protocol/objectui/actions.mdx index 7d3fc06d38..53b2afc6ec 100644 --- a/content/docs/protocol/objectui/actions.mdx +++ b/content/docs/protocol/objectui/actions.mdx @@ -49,7 +49,7 @@ The `type` field selects how an action is dispatched. The complete enum is **`sc | `flow` | Invoke a Screen/automation Flow by name. | Yes | | `modal` | Open a modal/page by name. | Yes | | `api` | Call an API endpoint (`method` defaults to `POST`). | Yes | -| `form` | Open a FormView by name, routed to `/console/forms/:name`. | Yes | +| `form` | Open a FormView by name, routed to `/console/forms/:name` **inside the console shell**; the submit lands on the created record. | Yes | `target` is the canonical binding for every non-`script` type, and since protocol 17 it is the **only** one. The deprecated `execute` alias was removed (#3855): authoring it is rejected with an error naming the replacement, not silently stripped. Run `os migrate meta --from 16` to rewrite existing sources automatically — the removal also ships in the machine-readable change manifest (`spec-changes.json`), which composes across however many majors you are jumping. @@ -159,6 +159,33 @@ type: form target: customer_quick_edit ``` +#### What `type: 'form'` promises + +`type: 'form'` is the platform's first-class way to open an object's form — the shape that replaced the `type: 'modal'` object fallback — so its contract is stated here rather than left to each renderer to decide (ruled 2026-08-10 on [#7245](https://github.com/objectstack-ai/objectstack/issues/7245)): + +1. **It renders in-shell.** The internal form route nests **inside** the console shell: sidebar, app navigation and breadcrumb stay. A form action never drops the operator onto an unchromed standalone page with the browser Back button as the only way home. The route survives too — `/console/forms/:name` stays deep-linkable and bookmarkable, which is what a route buys you over a transient dialog. +2. **A successful submit lands on the created record.** On the internal path the default post-submit behavior is a **redirect to the record that was just created**, not a confirmation panel. The `thank-you` panel is the default for the **public** `/console/f/:slug` path only, where there is no record the anonymous submitter is allowed to see. +3. **An explicit `submitBehavior` on the FormView always wins**, in either mode. The defaults above are what you get for declaring nothing; they are not a ceiling. + +| | Public path (`/console/f/:slug`) | Form action / internal path (`/console/forms/:name`) | +| --- | --- | --- | +| Audience | anonymous visitors | authed operators — where `type: 'form'` sends them | +| Chrome | standalone page, no console chrome | nested inside the console shell | +| Default when `submitBehavior` is omitted | `thank-you` confirmation panel | redirect to the created record | + +The four `submitBehavior` kinds and their options are documented on the [Forms guide](/docs/ui/forms). + + +**Current renderer status (2026-08-10).** The console shipping at the pinned +`.objectui-sha` does **not** implement this contract yet: it renders +`/console/forms/:name` as a top-level page outside the shell, and it falls back +to the `thank-you` panel in both modes. The statement above is the contract — +the renderer half is tracked in ObjectUI and linked from the +[#7245 thread](https://github.com/objectstack-ai/objectstack/issues/7245). Until +it lands, an internal FormView that must land on its new record has to declare +`submitBehavior` explicitly. + + ### Modal Actions Open a modal/page by name. `target` is resolved as a **page** first, then as an object (which opens that object's create/edit form); when it names neither, the action falls through to its server-side handler. diff --git a/content/docs/ui/actions.mdx b/content/docs/ui/actions.mdx index 700cbb18cb..c7850372e0 100644 --- a/content/docs/ui/actions.mdx +++ b/content/docs/ui/actions.mdx @@ -21,7 +21,7 @@ The types you'll actually use: | `url` | Navigate / open a link | `target` is the URL (`${ctx.record.id}` interpolation supported) | | `modal` | Open a modal page — client-side only, no server dispatch | `target` names the modal page (to collect input *and* run logic, use `script` + `params`) | | `api` | Call an HTTP endpoint directly | `target` is the endpoint; `method` / `bodyShape` / `bodyExtra` shape the request | -| `form` | Open a form view, prefilled with the current record | `target` names the FormView; routed to `/forms/:target?recordId=…` | +| `form` | Open a form view, prefilled with the current record — in-shell, and the submit lands on the created record ([contract](/docs/protocol/objectui/actions#what-type-form-promises)) | `target` names the FormView; routed to `/forms/:target?recordId=…` | Prefer `confirmText`/`params` over the two action properties that are **not diff --git a/content/docs/ui/forms.mdx b/content/docs/ui/forms.mdx index b6f529c2d9..36a1e9c997 100644 --- a/content/docs/ui/forms.mdx +++ b/content/docs/ui/forms.mdx @@ -17,7 +17,7 @@ Both modes: - Use the same `FormView` Zod schema (`@objectstack/spec/ui`) - Render through the same `FormPage` renderer shipped by the ObjectUI console (a separate package/repo) - Honor `?prefill_=` URL params -- Honor `submitBehavior` (thank-you / redirect / continue / next-record) +- Honor `submitBehavior` (thank-you / redirect / continue / next-record) — with **mode-aware defaults** when it is omitted (see [§8](#8-submitbehavior--what-happens-after-submit)) A **public form** is the Salesforce *Web-to-Lead* style embeddable form — declare a `FormView` with `sharing.allowAnonymous: true`, give it a `publicLink`, and the framework wires the anonymous REST endpoints automatically. @@ -341,14 +341,27 @@ formViews: { | `kind` | Renderer behavior | |---|---| -| `thank-you` *(default)* | Replace the form with a confirmation panel (`title`, `message`). | +| `thank-you` | Replace the form with a confirmation panel (`title`, `message`). | | `redirect` | `window.location.assign(url)` after `delayMs` (defaults to 0). | | `continue` | Re-read prefill values and reset state — user can submit another response without reload. | | `next-record` | Stub for queue contexts; falls back to thank-you when no queue is wired. | +### The default is mode-aware + +`submitBehavior` is optional, and **what you get for omitting it depends on the mode** — a public collection form and an authed create have different right answers (ruled 2026-08-10 on [#7245](https://github.com/objectstack-ai/objectstack/issues/7245)): + +| Mode | Default when `submitBehavior` is omitted | Why | +|---|---|---| +| **Public** (`/console/f/:slug`) | `{ kind: 'thank-you' }` — the confirmation panel | The anonymous submitter may not read the record back, so a receipt is all there is to show. | +| **Internal** (`/console/forms/:name`) | Redirect to the **created record** | An operator who just created a record belongs on that record, not on a "submission received" receipt. | + +An explicit `submitBehavior` overrides the default in either mode, so nothing here removes an option — it only changes what an author gets for declaring nothing. + +> **Current renderer status (2026-08-10).** The console shipping at the pinned `.objectui-sha` still applies `thank-you` as the default in **both** modes. The table above is the contract; the renderer half is tracked in ObjectUI and linked from the [#7245 thread](https://github.com/objectstack-ai/objectstack/issues/7245). Until it lands, declare `submitBehavior` explicitly on an internal FormView that must land somewhere specific. + ## 9. `type: 'form'` action — declarative form launchers -App actions can declare `type: 'form'` to open a FormView without resorting to free-form URLs. The `target` is the FormView name; the runtime navigates to `/console/forms/:name`. +App actions can declare `type: 'form'` to open a FormView without resorting to free-form URLs. The `target` is the FormView name; the runtime navigates to `/console/forms/:name` **inside the console shell** — the operator keeps the sidebar, navigation and breadcrumb, and a successful submit lands on the created record per the mode-aware default above. The full contract is stated on the [Action Protocol page](/docs/protocol/objectui/actions#what-type-form-promises). {/* os:check */} ```ts diff --git a/content/docs/ui/public-data-collection.mdx b/content/docs/ui/public-data-collection.mdx index 8b6a170748..14fddf5e3a 100644 --- a/content/docs/ui/public-data-collection.mdx +++ b/content/docs/ui/public-data-collection.mdx @@ -34,6 +34,8 @@ formViews: { } ``` +The `submitBehavior` above sets the **copy**, not the behavior: `thank-you` is already the default on the public path, because an anonymous submitter may not read the record back. The internal path defaults the other way — a `type: 'form'` action lands the operator on the record it just created — so do not carry a public form's confirmation panel over to an internal one by habit. See [`submitBehavior` is mode-aware](/docs/ui/forms#the-default-is-mode-aware). + That `sharing` block wires the anonymous endpoints automatically: ```