|
| 1 | +# ADR-0050: Form layout vs presentation — split the overloaded `FormView.type` |
| 2 | + |
| 3 | +**Status**: Proposed (2026-06-15) |
| 4 | +**Deciders**: ObjectStack Protocol Architects |
| 5 | +**Builds on**: [ADR-0014](./0014-record-form-field-type.md) (record form field type), [ADR-0017](./0017-object-has-many-view.md) (object-bound views), [ADR-0049](./0049-no-unenforced-security-properties.md) (declared ≠ enforced discipline) |
| 6 | +**Consumers**: `@objectstack/spec` (`ui/view.zod.ts` `FormViewSchema`), `@object-ui/plugin-form` (`ObjectForm` + `TabbedForm`/`WizardForm`/`SplitForm`/`DrawerForm`/`ModalForm`), `@object-ui/app-shell` (`RecordFormPage`, `AppContent`, `useActionModal`), examples + templates form views. |
| 7 | +**Surfaced by**: framework #1890 (viewschema liveness audit) → objectui #1762 (full-page form layout fix) → the modelling debt found while browser-verifying it. |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## TL;DR |
| 12 | + |
| 13 | +`FormViewSchema.type` is a single enum holding **two orthogonal dimensions**: |
| 14 | + |
| 15 | +| value | real meaning | dimension | |
| 16 | +|---|---|---| |
| 17 | +| `simple` / `tabbed` / `wizard` | the form's internal **layout** | **Layout** | |
| 18 | +| `drawer` / `modal` | where the form is **opened** | **Presentation / container** | |
| 19 | +| `split` | a list+detail master-detail mode | neither — a *view mode* | |
| 20 | + |
| 21 | +Because it's one field, **"a modal containing a tabbed form" is inexpressible** — `type` can only be `modal` *or* `tabbed`. That's exactly why the real modal create/edit entry points (`AppContent`, `useActionModal`) set `formType:'modal'` and the form inside can only ever be `simple`. |
| 22 | + |
| 23 | +**Decision: split the dimensions.** `FormView.type` becomes **layout only** (`simple` | `tabbed` | `wizard`). Presentation (drawer/modal/inline/page) is already a *caller* concern and already modelled elsewhere — reuse it. `split` is removed (master-detail is already `subforms` + the list's split-detail open mode). |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## Context — the presentation dimension already exists (3 places) |
| 28 | + |
| 29 | +The audit + the #1762 browser verification established that `ObjectForm` **already implements every variant** (real `TabbedForm`/`WizardForm`/`SplitForm`; `drawer`/`modal` fall through to `DrawerForm`/`ModalForm`). The gap was entry wiring (fixed for the full-page route in #1762). |
| 30 | + |
| 31 | +Crucially, "how a form is opened" is **already** spec'd, independently of `FormView.type`: |
| 32 | + |
| 33 | +- **Detail open mode** — `NavigationModeSchema`: `page` / `drawer` / `modal` / `split` / `popover` (`view.zod.ts`). |
| 34 | +- **Add-record mode** — list `addRecord.mode`: `inline` / `form` / `modal` + `formView`. |
| 35 | +- **Action open** — action `type:'modal'` + `target`. |
| 36 | + |
| 37 | +So `drawer`/`modal`/`split` *as `FormView.type` values* are **redundant with — and orthogonal to — these**. And they have **zero real business usage**: the only definitions are 5 showcase/template *named* views built to demo variants (`app-showcase task.view` `split`/`quick`; `hotcrm lead.view` `split`/`drawer`/`modal`). No default form view, no business flow depends on them. |
| 38 | + |
| 39 | +## Decision |
| 40 | + |
| 41 | +1. **`FormView.type` = layout only**: `z.enum(['simple','tabbed','wizard'])` (default `simple`). |
| 42 | +2. **Remove `drawer` / `modal` from `FormView.type`.** A form is *placed in* a drawer/modal by the **caller** (list `addRecord.mode`, `NavigationMode`, action `type:'modal'`), and the container renders an `ObjectForm` whose `type` is a *layout*. This makes "modal + tabbed" expressible — the whole point. |
| 43 | +3. **Remove `split` from `FormView.type`.** Master-detail is already `subforms` (single-record parent/child) + the list's `NavigationMode:'split'` (list+detail). A form-level `split` is a third, redundant spelling. |
| 44 | +4. **`ObjectForm` drops its `drawer`/`modal`/`split` branches** (`DrawerForm`/`ModalForm` become caller-supplied containers; `SplitForm` retires in favour of `subforms`). It keeps `simple`/`tabbed`/`wizard`. |
| 45 | + |
| 46 | +## Migration (low cost — breaking spec, tiny blast radius) |
| 47 | + |
| 48 | +- Spec: narrow the `FormViewSchema.type` enum (spec-major: the removed values are a breaking surface change). |
| 49 | +- Renderer: remove the 3 retired `ObjectForm` branches. |
| 50 | +- The 5 demo named views (showcase `task` split/quick, hotcrm `lead` split/drawer/modal): convert to `tabbed`/`simple` layout demos, and demo the *open modes* via the list's `NavigationMode` / `addRecord.mode` instead. No business metadata changes. |
| 51 | +- Reconcile with the deferred entry wiring: modal/drawer create/edit (`AppContent`/`useActionModal`) should forward the form view's *layout* into the container (so a modal can host a tabbed form) — the concrete follow-up this re-model unlocks. |
| 52 | + |
| 53 | +## Consequences |
| 54 | + |
| 55 | +- **Positive.** Authors describe *structure* with `type` (layout) and *placement* with the existing open-mode fields — no overloaded field, and modal/drawer forms can finally be tabbed/wizard. Smaller, honest `FormView` surface; one master-detail spelling. |
| 56 | +- **Negative / cost.** Breaking enum narrowing (spec-major). Requires a coordinated spec + `plugin-form` + examples/templates change. Mitigated by the near-zero real usage of the removed values. |
| 57 | +- **Sequencing.** This ADR is the design; implementation is a spec-major change (like ADR-0021's cutover) and should land after architect sign-off, ideally bundled with the modal/drawer layout-forwarding follow-up so the "modal + tabbed" capability ships demonstrably. |
| 58 | + |
| 59 | +## Non-goals |
| 60 | + |
| 61 | +- Re-modelling `NavigationMode` / `addRecord.mode` / action `modal` — they already carry presentation; this ADR *reuses* them. |
| 62 | +- The viewschema key-drift cleanup (objectui#1763) — separate. |
0 commit comments