Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/27153-form-view-buttons-reset-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'@objectstack/spec': minor
'@object-ui/plugin-form': minor
'@object-ui/types': minor
---

feat(spec)!: a form view's `buttons` block holds `submit` and `cancel` only; `buttons.reset` is retired, because no form ever drew a reset button

Clause-②: no (narrowing)

Metadata-protocol: narrowed (`FormViewSchema.buttons.reset` is a tombstone)

<!-- adr-0087: registered form-view-buttons-reset-removed, ui-form-view-buttons-reset-retired -->

**BREAKING** accept-set narrowing on the form view surface (`@objectstack/spec`), ADR-0049 enforce-or-remove.

The console draws a form's Submit and Cancel buttons on every entry point (the form page, the New and Edit dialogs, the record form page, master-detail forms). None of them drew a reset button, and `buttons.reset.label` was never read. The one renderer fold that read `buttons.reset.show` passed it to ObjectForm's clear-after-save flag, so `show: true` emptied the form after a successful save instead of adding a button.

- **FROM → TO.** `buttons: { submit, cancel, reset }` → `buttons: { submit, cancel }`. No button replaces it: Cancel leaves the form without saving. `reset.show: true` used to clear the form after each save on ObjectView and the record form page; a converted form no longer clears after a save. A form that should clear after a save declares `submitBehavior: { kind: 'continue' }`, which ObjectForm and the console form page honour.
- **The one-line fix.** Delete `buttons.reset`. `os migrate meta --from 17` lists the edit for existing sources, and `--write` applies it. A stored `sys_metadata` row is converted when it is read (the `form-view-buttons-reset-removed` conversion), so a form view saved with the key keeps loading.
- **What the parse says.** "`view.form.buttons.reset` was removed in @objectstack/spec 18.3.0 (ADR-0049) — no form ever drew a reset button, and its label was never read. …", at every form view door (a view's `form` and `formViews.*`, a form view item record, a flattened form overlay) and at `defineStack` as `STACK_SCHEMA_INVALID` / 422. TypeScript rejects the key at the authoring site.
- **Unchanged.** `buttons.submit` and `buttons.cancel` parse and render as before.

The renderer (`@object-ui/plugin-form`, `@object-ui/types`): `ObjectForm` no longer folds `buttons.reset.show` onto `showReset`, and the `ObjectFormSchema.buttons` mirror (type and zod) drops `reset`. The flat `showReset` prop itself is unchanged.
4 changes: 2 additions & 2 deletions content/docs/references/api/protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1918,7 +1918,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
| **subforms** | `{ childObject: string; relationshipField?: string; columns?: object[]; amountField?: string; … }[]` | optional | Inline master-detail child collections |
| **sharing** | `{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }` | optional | Public sharing configuration for this form |
| **submitBehavior** | `{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }` | optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped. |
| **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. |
| **buttons** | `{ submit?: object; cancel?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. |
| **defaults** | `Record<string, any>` | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;). |

### Nested Shape: `GetUiViewResponse.listViews[string]`
Expand Down Expand Up @@ -1997,7 +1997,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
| **subforms** | `{ childObject: string; relationshipField?: string; columns?: object[]; amountField?: string; … }[]` | optional | Inline master-detail child collections |
| **sharing** | `{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }` | optional | Public sharing configuration for this form |
| **submitBehavior** | `{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }` | optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped. |
| **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. |
| **buttons** | `{ submit?: object; cancel?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. |
| **defaults** | `Record<string, any>` | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;). |

### Nested Shape: `GetUiViewResponse.protection`
Expand Down
12 changes: 6 additions & 6 deletions content/docs/references/ui/view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ Form-view select option — the object-field option shape minus the per-option `
| **subforms** | `{ childObject: string; relationshipField?: string; columns?: object[]; amountField?: string; … }[]` | optional | Inline master-detail child collections |
| **sharing** | `{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }` | optional | Public sharing configuration for this form |
| **submitBehavior** | `{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }` | optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped. |
| **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. |
| **buttons** | `{ submit?: object; cancel?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. |
| **defaults** | `Record<string, any>` | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;). |

### Nested Shape: `FormView.data[provider='object']`
Expand Down Expand Up @@ -504,7 +504,7 @@ Form-view select option — the object-field option shape minus the per-option `
| :--- | :--- | :--- | :--- |
| **submit** | `{ show?: boolean; label?: string \| Record<string, string> }` | optional | Submit button |
| **cancel** | `{ show?: boolean; label?: string \| Record<string, string> }` | optional | Cancel button |
| **reset** | `{ show?: boolean; label?: string \| Record<string, string> }` | optional | Reset button |
| **reset** | `never` | optional | [REMOVED] `view.form.buttons.reset` was removed in @objectstack/spec 18.3.0 (ADR-0049) — no form ever drew a reset button, and its label was never read. Delete the key: a form has `submit` and `cancel` buttons only, and Cancel leaves the form without saving. `reset.show: true` used to clear the form after each save (the ObjectForm fold that ObjectView and the record form page render); a form converted by this release no longer clears after a save, and a form that should declares `submitBehavior: { kind: 'continue' }`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |


---
Expand Down Expand Up @@ -1788,7 +1788,7 @@ Tab configuration for multi-tab view interface
| **subforms** | `{ childObject: string; relationshipField?: string; columns?: object[]; amountField?: string; … }[]` | optional | Inline master-detail child collections |
| **sharing** | `{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }` | optional | Public sharing configuration for this form |
| **submitBehavior** | `{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }` | optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped. |
| **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. |
| **buttons** | `{ submit?: object; cancel?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. |
| **defaults** | `Record<string, any>` | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;). |

### Nested Shape: `View.listViews[string]`
Expand Down Expand Up @@ -1867,7 +1867,7 @@ Tab configuration for multi-tab view interface
| **subforms** | `{ childObject: string; relationshipField?: string; columns?: object[]; amountField?: string; … }[]` | optional | Inline master-detail child collections |
| **sharing** | `{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }` | optional | Public sharing configuration for this form |
| **submitBehavior** | `{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }` | optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped. |
| **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. |
| **buttons** | `{ submit?: object; cancel?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. |
| **defaults** | `Record<string, any>` | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;). |

### Nested Shape: `View.protection`
Expand Down Expand Up @@ -2104,7 +2104,7 @@ This schema accepts one of the following structures:
| **subforms** | `{ childObject: string; relationshipField?: string; columns?: object[]; amountField?: string; … }[]` | optional | Inline master-detail child collections |
| **sharing** | `{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }` | optional | Public sharing configuration for this form |
| **submitBehavior** | `{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }` | optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped. |
| **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. |
| **buttons** | `{ submit?: object; cancel?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. |
| **defaults** | `Record<string, any>` | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;). |

### Nested Shape: `ViewItem[viewKind='form'].protection`
Expand Down Expand Up @@ -2288,7 +2288,7 @@ This schema accepts one of the following structures:
| **subforms** | `{ childObject: string; relationshipField?: string; columns?: object[]; amountField?: string; … }[]` | optional | Inline master-detail child collections |
| **sharing** | `{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }` | optional | Public sharing configuration for this form |
| **submitBehavior** | `{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }` | optional | Post-submit behavior. On the `redirect` arm, `url` is relative-only and interpolates only declared record fields as `{{record.field_name}}`, URL-escaped. |
| **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. |
| **buttons** | `{ submit?: object; cancel?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm. |
| **defaults** | `Record<string, any>` | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values;). |

### Nested Shape: `ViewItemWire[viewKind='form'].protection`
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/liveness/view.json
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@
},
"buttons": {
"status": "live",
"note": "objectui: ObjectForm.tsx foldFormButtons() reads schema.buttons and folds submit/cancel/reset {show,label} onto the flat showSubmit/submitText/showCancel/cancelText/showReset props (framework#1894 / #2998); ObjectView + RecordFormPage forward it. Flat keys kept as deprecated back-compat (explicit flat key wins)."
"note": "objectui: ObjectForm.tsx foldFormButtons() reads schema.buttons and folds submit/cancel {show,label} onto the flat showSubmit/submitText/showCancel/cancelText props (framework#1894 / #2998); ObjectView forwards it, and every console form entry point resolves submit/cancel through resolveFormViewChrome / readFormButton (#26843). Flat keys kept as deprecated back-compat (explicit flat key wins). SUB-KEY buttons.reset REMOVED 2026-10-28 (#27153, ADR-0049 enforce-or-remove, ruled retire): measured at origin/main fbe344b3, six button bars (components form.tsx, console FormPage, ModalForm, DrawerForm, WizardForm, MasterDetailForm) drew submit/cancel and none drew reset; reset.label had no reader; the one fold of reset.show landed on ObjectForm showReset, which means clear-after-save (resetOnSubmit), not a button. Tombstoned inside the buttons block (retiredKey carries the prescription) and stripped from stored rows and sources by the protocol-18 form-view-buttons-reset-removed conversion; the fold was removed in the same change. Sub-keys cannot be drilled (one level only), so the removal is recorded here; submit and cancel keep this row live."
},
"defaults": {
"status": "live",
Expand Down
Loading
Loading