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
43 changes: 43 additions & 0 deletions .changeset/24692-add-record-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
'@objectstack/spec': minor
'@objectstack/lint': patch
---

feat(spec)!: a list's `addRecord` block says what renders — `enabled` is required, `position` defaults to `top`, and `mode` / `formView` are retired

Clause-②: yes (narrowing)

<!-- adr-0087: registered add-record-mode-form-view-removed, add-record-enabled-defaulted, add-record-mode-form-view-retired, add-record-enabled-required -->

**BREAKING** (an authoring narrowing), shipped as `minor` under the launch-window convention for breaking changes, and in 18.x under the standing rule for breaking narrowings: an ADR-0087 conversion carries every body that parsed under 18.0, so no stored row or built artifact is refused.

`AddRecordConfigSchema` — the `addRecord` block on a list view, an object's or a view container's `listViews`, a list page's `interfaceConfig` and a page `list-view` node — declared defaults the renderer never applied (`enabled` `true`, `position` `bottom`, `mode` `inline`) and two keys no renderer read. The list's Add button is drawn only for an explicit `enabled: true`, an absent `position` renders at the top, and every Add opens the object's create form. The block now says exactly that:

- **`enabled` is REQUIRED, with no default.** `addRecord: {}` is refused at parse (`os validate`, `os build`, the save doors) with a prescription naming the key, instead of drawing nothing.
- **`position` defaults to `top`**, where the renderer has always placed an absent position. Values stay `top` / `bottom` / `both`. No screen moves.
- **`mode` and `formView` are `retiredKey()` tombstones** (ADR-0049 enforce-or-remove): authoring either is a `tsc` error and a parse error carrying the prescription. The create form is chosen by convention instead: the create entry opens `formViews.create` when the object's view container declares it, else `form` (or `formViews.default`). How that form opens stays the renderer's.

### FROM → TO

| before | after |
| --- | --- |
| `addRecord: {}` (parsed as `enabled: true`, drew nothing) | refused: write `addRecord: { enabled: true }` |
| `addRecord: { position: 'bottom' }` | refused: write `addRecord: { enabled: true, position: 'bottom' }` |
| `addRecord: { enabled: true }` (parsed `position: 'bottom'`) | parses as `position: 'top'`, where it always rendered |
| `addRecord: { enabled: true, mode: 'form' }` (any `mode`) | `addRecord: { enabled: true }` |
| `addRecord: { enabled: true, formView: 'create' }` | `addRecord: { enabled: true }`, with the form declared as `formViews.create` on the object's view container |

**The one-line fix:** delete `mode` and `formView`, and write `enabled: true` (or `enabled: false`) on every `addRecord` block. `os migrate meta --from 17` lists the edits for existing metadata sources; `--write` applies the ones it can prove, and you apply the rest by hand.

### The ADR-0087 disposition

- D2 `add-record-mode-form-view-removed` strips `mode` and `formView` from every `addRecord` block it reaches (`stack.views[]` in all three persisted spellings, `objects[].listViews.*`, `pages[].interfaceConfig` and every page `list-view` node), with a notice per key. Lossless in pixels: no renderer read either key.
- D2 `add-record-enabled-defaulted` writes `enabled: true`, the default the 18.0 spec declared, onto a block that omits `enabled`, with a notice. Measured before choosing it: no producer in this repository (the showcase's two blocks, the lint fixture) or in objectui's Studio design surface writes `addRecord` without `enabled`, so the conversion carries the narrowing with a zero measured population. **One visible consequence, stated:** a stored block that omitted `enabled` drew no Add button, because the renderer read the raw body; converted, it draws the button its declaration always promised. To keep a list without one, write `enabled: false` or drop the block. An explicit `enabled` value is never touched.
- Both are retired from the load path: a live author is refused at parse and taught; stored rows, built artifacts inside their declared-floor window and `os migrate meta --from 17` replay them.
- `RETIRED_KEYS_BY_MAJOR[18]` gains `ui/AddRecordConfig:mode` and `ui/AddRecordConfig:formView`; the D3 entries are `add-record-mode-form-view-retired` and `add-record-enabled-required`; the default moves (`enabled` `true` → none, `position` `bottom` → `top`) are declared in `DEFAULT_CHANGES_BY_MAJOR[18]`.

### Also in this change

- The Studio view form and page form offer exactly `enabled` and `position` for `addRecord`.
- `@objectstack/lint`: `os explain list-view-field-unknown` no longer names `addRecord.formView` as a position another rule checks.
- The showcase's two `addRecord` blocks and the docs (`ui/create-vs-edit-form`, `ui/views`) drop `mode` / `formView` and state the create-form convention.
4 changes: 2 additions & 2 deletions content/docs/references/api/protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1863,7 +1863,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles for the view toolbar |
| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration |
| **tabs** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. 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. |
| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration |
| **addRecord** | `{ enabled: boolean; position?: Enum<'top' \| 'bottom' \| 'both'> }` | optional | Add record entry point configuration |
| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list |
| **allowPrinting** | `boolean` | optional | Allow users to print the view |
| **emptyState** | `{ title?: string \| Record<string, string>; message?: string \| Record<string, string>; icon?: string; link?: object }` | optional | Empty state configuration when no records found |
Expand Down Expand Up @@ -1949,7 +1949,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles for the view toolbar |
| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration |
| **tabs** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. 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. |
| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration |
| **addRecord** | `{ enabled: boolean; position?: Enum<'top' \| 'bottom' \| 'both'> }` | optional | Add record entry point configuration |
| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list |
| **allowPrinting** | `boolean` | optional | Allow users to print the view |
| **emptyState** | `{ title?: string \| Record<string, string>; message?: string \| Record<string, string>; icon?: string; link?: object }` | optional | Empty state configuration when no records found |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/data/object.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ const result = ApiMethod.parse(data);
| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles for the view toolbar |
| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration |
| **tabs** | `never` | optional | [REMOVED] `view.list.tabs` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer ever mounted a tab bar for it, so authoring it drew nothing: the tab strip above an object's records is the saved-view switcher (ViewTabBar), which renders one tab per named list view and never read this key. Delete the key, and move each tab you want to a named list view under the object's `listViews` instead: the tab's `name` becomes the entry's key, its `label` the entry's `label`, and its `filter` rules join the view's own `filter` on that entry (copy the view's `columns` too); a tab whose `view` already named a list view needs nothing more. Every `listViews` entry renders as a tab in the switcher. 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. |
| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration |
| **addRecord** | `{ enabled: boolean; position?: Enum<'top' \| 'bottom' \| 'both'> }` | optional | Add record entry point configuration |
| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list |
| **allowPrinting** | `boolean` | optional | Allow users to print the view |
| **emptyState** | `{ title?: string \| Record<string, string>; message?: string \| Record<string, string>; icon?: string; link?: object }` | optional | Empty state configuration when no records found |
Expand Down
12 changes: 6 additions & 6 deletions content/docs/references/ui/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Interface-level page configuration (Airtable parity)
| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration |
| **userFilters** | `{ element?: Enum<'dropdown' \| 'tabs' \| 'toggle'>; fields?: object[]; tabs?: object[]; showAllRecords?: boolean; … }` | optional | End-user quick-filter bar for this page (overrides the source view's userFilters) |
| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles |
| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration |
| **addRecord** | `{ enabled: boolean; position?: Enum<'top' \| 'bottom' \| 'both'> }` | optional | Add record entry point configuration |
| **buttons** | `string[]` | optional | Toolbar buttons — names of the source object's actions to surface in the page toolbar |
| **recordAction** | `Enum<'drawer' \| 'page' \| 'modal' \| 'none'>` | optional | How clicking a record opens its detail (drawer \| page \| modal \| none). Default: drawer |
| **showRecordCount** | `boolean` | optional | Show record count at page bottom |
Expand Down Expand Up @@ -177,10 +177,10 @@ View filter rule

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **enabled** | `boolean` | optional (default: `true`) | Show the add record entry point |
| **position** | `Enum<'top' \| 'bottom' \| 'both'>` | optional (default: `"bottom"`) | Position of the add record button |
| **mode** | `Enum<'inline' \| 'form' \| 'modal'>` | optional (default: `"inline"`) | How to add a new record |
| **formView** | `string` | optional | Named form view to use when mode is "form" or "modal" |
| **enabled** | `boolean` | ✅ | Show the add record entry point — REQUIRED: `true` draws the Add button, `false` declares it off |
| **position** | `Enum<'top' \| 'bottom' \| 'both'>` | optional (default: `"top"`) | Position of the add record button: `top` (the toolbar, the default), `bottom` (under the list) or `both` |
| **mode** | `never` | optional | [REMOVED] `addRecord.mode` was removed in @objectstack/spec 18.1.0 (ADR-0049 enforce-or-remove) — no renderer ever read it: the Add button always opens the create form, never an inline row or a mode of its own choosing. Delete the key; the create form opens the way the object list's New opens it. 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. |
| **formView** | `never` | optional | [REMOVED] `addRecord.formView` was removed in @objectstack/spec 18.1.0 (ADR-0049 enforce-or-remove) — no renderer ever read it: the Add button opens the create form chosen by convention, never a named form. Delete the key, and to give the Add button its own form declare it as `formViews.create` on the object's view container (without one, the default form is used). 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. |

### Nested Shape: `InterfacePageConfig.emptyState`

Expand Down Expand Up @@ -266,7 +266,7 @@ View filter rule
| **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration |
| **userFilters** | `{ element?: Enum<'dropdown' \| 'tabs' \| 'toggle'>; fields?: object[]; tabs?: object[]; showAllRecords?: boolean; … }` | optional | End-user quick-filter bar for this page (overrides the source view's userFilters) |
| **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles |
| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration |
| **addRecord** | `{ enabled: boolean; position?: Enum<'top' \| 'bottom' \| 'both'> }` | optional | Add record entry point configuration |
| **buttons** | `string[]` | optional | Toolbar buttons — names of the source object's actions to surface in the page toolbar |
| **recordAction** | `Enum<'drawer' \| 'page' \| 'modal' \| 'none'>` | optional | How clicking a record opens its detail (drawer \| page \| modal \| none). Default: drawer |
| **showRecordCount** | `boolean` | optional | Show record count at page bottom |
Expand Down
Loading
Loading