Skip to content
Merged
29 changes: 29 additions & 0 deletions .changeset/20456-view-console-round-trip-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@objectstack/spec': minor
---

feat(spec): the console's round-trip keys on a stored `view` row are declared on the wire, so a parse keeps them (#20456)

Clause-②: yes (narrowing)

<!-- adr-0087: not-required (no-migration-prescription) Nothing authored moves: no key an authoring door accepts changes its spelling, type or legality, because the four keys are console state the authoring doors refuse by name. The only producer, objectui's console, writes none of the values now refused (census at the `.objectui-sha` pin: a boolean pin, an integer reorder index, the marker as `true`, and `visibility` only carried forward from a stored value). So `objectstack migrate meta` has no mechanical rewrite to perform; a stored row holding a refused value is repaired by correcting it or deleting the key. The other categories are closed on facts: `@objectstack/spec` publishes (not `unpublished`); no ADR-0087 id is minted here or pre-dates the base to cover this (not `registered` / `already-registered`); and a Zod schema's accept set changes, so neither `runtime-interface-only` nor `type-surface-only` applies. -->

**BREAKING** accept-set narrowing on the `view` write door (`PUT /api/v1/meta/view/:name`, the Studio and MCP save) and on every door that parses `ViewMetadataSchema`, shipped as `minor` under the repo's launch-window convention. The newly declared keys are typed, so a non-boolean `isPinned`, a non-integer `sortOrder`, a `visibility` outside `private` / `team` / `organization` / `public`, or an `_isOverride` other than `true` is now refused at the parse (`422 INVALID_METADATA` at the save door), where the strip used to swallow the key and the save stored the body as sent. To fix a refused body, correct the value or delete the key. The console writes none of these values: its pin toggle writes a boolean, its reorder an integer index, and it stamps the marker as `true`. The diff also widens: the keys are now declared, and `VIEW_CONSOLE_ROUND_TRIP_KEYS` is a new export.

`saveMetaItem` stores a `view` body exactly as it was sent (ADR-0005 appendix (c)), and the members of `ViewMetadataSchema` that judge a stored row `.strip()` every key they do not declare. So the keys the console writes onto a stored view and reads back were in the store and nowhere in the contract. A census of objectui's console (at the `.objectui-sha` pin) measured which ones the parse dropped:

- `isPinned` and `sortOrder` on a flattened list overlay (they were already declared on the ViewItem record);
- `visibility`, on both the flattened list overlay and the ViewItem record;
- `_isOverride`, the marker that tells the console a row is the settings overlay of a code-defined view and not a saved view of its own.

## What it does now

- The ViewItem wire member (`ViewItemWireSchema`) and the flattened list overlay (`VIEW_METADATA_MEMBERS.listOverlay`) declare `isPinned`, `sortOrder` and `visibility` from one shared declaration, each with its meaning. The flattened list overlay also declares `_isOverride: true`, and its existing `isDefault` now carries its meaning. A parse of a console-written row keeps every one of them.
- **New export `VIEW_CONSOLE_ROUND_TRIP_KEYS`** (`@objectstack/spec/ui`): each round-trip key, mapped to the members whose rows the console writes it on (`isDefault`, `isPinned`, `sortOrder`, `visibility`, `columnState`, `_isOverride`).
- `visibility` is display grouping only (`private` / `team` / `organization` / `public` in the view switcher). It restricts nobody, and its declared meaning says so.
- None of these keys is authorable. `defineViewItem` still refuses each of them by name, and `visibility` now gets a prescription that says what it is.

## What does not change

- **What is persisted.** The save still stores the request body verbatim. Storing the parsed body is a later, separate change.
- The alias spellings the census found keep their declared spellings: `objectName` is `object`, and a top-level `id` is `name`. The console's filter / sort builder row ids stay `VIEW_CONSOLE_ROW_DECORATIONS`, removed before the parse.
22 changes: 12 additions & 10 deletions content/docs/references/ui/view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2256,9 +2256,10 @@ This schema accepts one of the following structures:
| **_packageId** | `string` | optional | Owning package machine id. |
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **isPinned** | `boolean` | optional | Studio round-trip: view pinned in the switcher (per-user state, written by the console — not authored). |
| **sortOrder** | `integer` | optional | Studio round-trip: position within the switcher (per-user state, written by the console — not authored). |
| **columnState** | `{ order?: string[]; widths?: Record<string, number> }` | optional | Studio round-trip: per-user column order/widths (runtime-only state, written by the console grid — not authored) |
| **isPinned** | `boolean` | optional | Console round-trip: the view is pinned in the object's view switcher. Written by the console's pin toggle through the `view` metadata API and read back to draw the pinned group. Stored on the view's row, which has no per-user scope. Not authored. |
| **sortOrder** | `integer` | optional | Console round-trip: the view's position among the object's saved views in the switcher, 0-based and counted over saved views only (a code-defined view carries none). Written by the console's drag-reorder and read back to order the tabs. Not authored: `order` is the authored default position. |
| **visibility** | `Enum<'private' \| 'team' \| 'organization' \| 'public'>` | optional | Console round-trip: the group the switcher files this view's tab under (private, team, organization or public). Display grouping only, NOT access control: nothing restricts who can list or open the view by this value. No console control sets it; the console carries a stored value forward when it re-saves the row. Not authored. |
| **columnState** | `{ order?: string[]; widths?: Record<string, number> }` | optional | Studio round-trip: column order/widths (runtime-only state, written by the console grid and stored on the view's row, which has no per-user scope — not authored) |

### Nested Shape: `ViewItemWire[viewKind='list'].config`

Expand Down Expand Up @@ -2327,8 +2328,8 @@ This schema accepts one of the following structures:

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **order** | `string[]` | optional | Column order as field names, leftmost first (runtime-only per-user state — written by the console grid, never authored). |
| **widths** | `Record<string, number>` | optional | Column widths in pixels, keyed by field name (runtime-only per-user state — written by the console grid, never authored). |
| **order** | `string[]` | optional | Column order as field names, leftmost first (runtime-only state — written by the console grid, never authored). |
| **widths** | `Record<string, number>` | optional | Column widths in pixels, keyed by field name (runtime-only state — written by the console grid, never authored). |

---

Expand Down Expand Up @@ -2356,9 +2357,10 @@ This schema accepts one of the following structures:
| **_packageId** | `string` | optional | Owning package machine id. |
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **isPinned** | `boolean` | optional | Studio round-trip: view pinned in the switcher (per-user state, written by the console — not authored). |
| **sortOrder** | `integer` | optional | Studio round-trip: position within the switcher (per-user state, written by the console — not authored). |
| **columnState** | `{ order?: string[]; widths?: Record<string, number> }` | optional | Studio round-trip: per-user column order/widths (runtime-only state, written by the console grid — not authored) |
| **isPinned** | `boolean` | optional | Console round-trip: the view is pinned in the object's view switcher. Written by the console's pin toggle through the `view` metadata API and read back to draw the pinned group. Stored on the view's row, which has no per-user scope. Not authored. |
| **sortOrder** | `integer` | optional | Console round-trip: the view's position among the object's saved views in the switcher, 0-based and counted over saved views only (a code-defined view carries none). Written by the console's drag-reorder and read back to order the tabs. Not authored: `order` is the authored default position. |
| **visibility** | `Enum<'private' \| 'team' \| 'organization' \| 'public'>` | optional | Console round-trip: the group the switcher files this view's tab under (private, team, organization or public). Display grouping only, NOT access control: nothing restricts who can list or open the view by this value. No console control sets it; the console carries a stored value forward when it re-saves the row. Not authored. |
| **columnState** | `{ order?: string[]; widths?: Record<string, number> }` | optional | Studio round-trip: column order/widths (runtime-only state, written by the console grid and stored on the view's row, which has no per-user scope — not authored) |

### Nested Shape: `ViewItemWire[viewKind='form'].config`

Expand Down Expand Up @@ -2402,8 +2404,8 @@ This schema accepts one of the following structures:

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **order** | `string[]` | optional | Column order as field names, leftmost first (runtime-only per-user state — written by the console grid, never authored). |
| **widths** | `Record<string, number>` | optional | Column widths in pixels, keyed by field name (runtime-only per-user state — written by the console grid, never authored). |
| **order** | `string[]` | optional | Column order as field names, leftmost first (runtime-only state — written by the console grid, never authored). |
| **widths** | `Record<string, number>` | optional | Column widths in pixels, keyed by field name (runtime-only state — written by the console grid, never authored). |

---

Expand Down
1 change: 1 addition & 0 deletions packages/spec/api-surface/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@
"UserFilters (type)",
"UserFiltersParsed (type)",
"UserFiltersSchema (const)",
"VIEW_CONSOLE_ROUND_TRIP_KEYS (const)",
"VIEW_CONSOLE_ROW_DECORATIONS (const)",
"VIEW_FILTER_LIST_VALUE_OPERATORS (const)",
"VIEW_FILTER_OPERATORS (const)",
Expand Down
1 change: 1 addition & 0 deletions packages/spec/export-origins/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@
"UserFilters": "src/ui/view.zod.ts#UserFilters (type)",
"UserFiltersParsed": "src/ui/view.zod.ts#UserFiltersParsed (type)",
"UserFiltersSchema": "src/ui/view.zod.ts#UserFiltersSchema (const)",
"VIEW_CONSOLE_ROUND_TRIP_KEYS": "src/ui/view.zod.ts#VIEW_CONSOLE_ROUND_TRIP_KEYS (const)",
"VIEW_CONSOLE_ROW_DECORATIONS": "src/ui/view.zod.ts#VIEW_CONSOLE_ROW_DECORATIONS (const)",
"VIEW_FILTER_LIST_VALUE_OPERATORS": "src/ui/view.zod.ts#VIEW_FILTER_LIST_VALUE_OPERATORS (const)",
"VIEW_FILTER_OPERATORS": "src/ui/view.zod.ts#VIEW_FILTER_OPERATORS (const)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,21 +304,21 @@ const LEDGER: ReadonlyArray<OmitEntry | SubsetEntry> = [
type: 'view',
path: ROOT_PATH,
key: 'columnState',
why: "platform-written, never authored — `Studio round-trip: per-user column order/widths (runtime-only state, written by the console grid — not authored)`. Declared on the wire members only so the console's own write parses; the authoring door (`ViewItemSchema`) rejects the key by name",
why: "platform-written, never authored — `Studio round-trip: column order/widths (runtime-only state, written by the console grid and stored on the view's row, which has no per-user scope — not authored)`. Declared on the wire members only so the console's own write parses; the authoring door (`ViewItemSchema`) rejects the key by name",
},
{
kind: 'omit',
type: 'view',
path: ROOT_PATH,
key: 'isPinned',
why: "platform-written, never authored — `Studio round-trip: view pinned in the switcher (per-user state, written by the console — not authored)`; the authoring door (`ViewItemSchema`) rejects the key by name",
why: "platform-written, never authored — `Console round-trip: the view is pinned in the object's view switcher. … Not authored.`; the authoring door (`ViewItemSchema`) rejects the key by name",
},
{
kind: 'omit',
type: 'view',
path: ROOT_PATH,
key: 'sortOrder',
why: "platform-written, never authored — `Studio round-trip: position within the switcher (per-user state, written by the console — not authored)`; the authoring door (`ViewItemSchema`) rejects the key by name and points the author at `order`, the authored default",
why: "platform-written, never authored — `Console round-trip: the view's position among the object's saved views in the switcher … Not authored`; the authoring door (`ViewItemSchema`) rejects the key by name and points the author at `order`, the authored default",
},

// Deprecated or legacy alias — deliberately not offered to new authors, the
Expand Down
Loading
Loading