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
38 changes: 38 additions & 0 deletions .changeset/22605-spec-list-view-edit-inline-default-on.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
'@objectstack/spec': minor
---

`ListView.userActions.editInline` defaults to `true` on the v18 line: a list view is editable in place by default, under the permission gate that already exists, and `userActions: { editInline: false }` is the opt-out.

Clause-②: no

<!-- adr-0087: registered list-view-edit-inline-default-on -->

**BREAKING** — a declared default moves, shipped as `minor` under the launch-window convention. The author who relied on the documented default (`false`, "the list is read-only unless the author opts in") for a list that is read-only by nature — a log, an audit trail, a history, a report roll-up — now gets an inline-edit toggle on it, for every user who may `update` the object. The remedy is one key on that view: `userActions: { editInline: false }`.

**The ruling.** Maintainer, 2026-10-10, verbatim: 「乙 v18 把 spec 默认翻成 true,editInline: false 变成关法。」 The rule of objectui#5144 stands — one vocabulary (`editInline`), a boolean view-level `inlineEdit` folds into it, an explicit `editInline` wins, declared = enforced; only the default's value changes. Not taken: declaring `editInline: true` on every existing view, and an app-level default between the spec default and the view.

**What moves.** `UserActionsConfigSchema.editInline` is `z.boolean().default(true)` where it was `.default(false)`, with the `describe` rewritten: on by default; a user who may update the object edits a cell in place with the field's type-aware widget; declare `editInline: false` to make the list read-only in place. A list view's `userActions` block, and a page's `interfaceConfig.userActions` block (the same schema), that omits `editInline` now parses to `true` where it parsed to `false`; the published JSON Schema and the reference pages (`content/docs/references/ui/view.mdx`, `page.mdx`) say so. A view with no `userActions` block at all parses with none on either side; the renderer reads an absent key as this default. The accept set is unchanged: `editInline` is still a boolean, and every authored value parses exactly as before. The permission gate is untouched: the renderer offers the toggle only where the object is editable in place and the current principal may `update` it.

**The consumer half** is objectui's (objectstack-ai/objectui#12086): `ListView.tsx` and `InterfaceListPage.tsx` read an absent key as `editInline !== false`. Until that lands, the console at the pinned objectui still reads an absent key as off, so nothing regresses against today in the window.

## FROM → TO

| you wrote | what it means now | write instead, to keep the old behaviour |
|:--|:--|:--|
| nothing (no `editInline`) on a list view or a page list | editable in place by a user who may `update` the object | `userActions: { editInline: false }` |
| `editInline: false` | unchanged: read-only in place | nothing |
| `editInline: true` | unchanged: editable in place | nothing |
| a view-level `inlineEdit: true` | unchanged: folds to on and opens the grid in edit mode | nothing |

**The one-line fix: on a list that must stay read-only in place, write `userActions: { editInline: false }`.** An `os compile` artifact that serialised an earlier parse carries a written `editInline: false` and stays read-only in place: recompile it, or delete the key.

**Who is affected, measured** at `e11ef3a7bd`. No metadata in this repository declares `editInline` at all (0 `editInline: false`, 0 `editInline: true` across `examples/**` and `packages/**`, tests and changelogs excluded). Every list view that declares nothing gains the toggle: 38 list-kind views declared with an explicit `type` across 10 example `*.view.ts` files (entries with no `type` are grid by default and count the same way), 12 `interfaceConfig` page lists across 5 showcase page files (11 declare a `userActions` block, none naming `editInline`), and 105 list-kind views across 40 platform `*.object.ts` files in `packages/**`. One list view declares the view-level `inlineEdit: true` (the showcase `task` grid saved view) and keeps opening in edit mode. The card's readings outside this repository: cloud's `sys-package.page.ts` already declares `editInline: false` explicitly; HotCRM's 41 undeclared views regain cell editing after both packages upgrade, with no metadata change there.

### The kit

- **The default and its describe.** `packages/spec/src/ui/view.zod.ts`, `UserActionsConfigSchema.editInline`; the schema docblock records the ruling beside the group/hideFields/rowColor asymmetry it already explains.
- **The declared default change.** `DEFAULT_CHANGES_BY_MAJOR[17]` (`packages/spec/scripts/lib/default-changes.ts`) carries `ui/UserActionsConfig:editInline` `false` → `true`, which is what lets `check:authorable-surface` authorise the moved fingerprint in `authorable-defaults/ui.json`; `CURRENT_MAJOR` is the package major (17) until the version pass takes the group to 18.
- **The ledger.** The D3 semantic entry `list-view-edit-inline-default-on` (protocol 18), one file under `src/migrations/entries/semantic/`, concatenated into `registry.ts` by `gen:migration-registry`, projected into `spec-changes.json` and `docs/protocol-upgrade-guide.md`. No key is removed, so there is no tombstone and no `RETIRED_KEYS_BY_MAJOR` row; no D2 conversion exists, because a mechanical pass writing `editInline: false` into every silent view would preserve the old posture and defeat the ruling, and one writing `true` would add nothing the default does not already do. The same class as `view-pagination-page-size-default-50` and protocol 12's `rest-requireauth-default-flip`.
- **The pins.** `packages/spec/src/ui/view.test.ts`: an absent key parses to `true` (on `UserActionsConfigSchema` and through `ListViewSchema`), an explicit `false` stays `false`, an explicit `true` stays `true`, and the view-level `inlineEdit: true` still parses as before with no `userActions` block materialised.
- **The skill.** `skills/objectstack-ui/rules/list-views.md` gains its paragraph in a separate pull request, because `skills/**` is Tier H.
2 changes: 1 addition & 1 deletion content/docs/references/ui/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ View filter rule
| **rowHeight** | `boolean` | optional (default: `true`) | Allow users to toggle row height/density |
| **group** | `boolean` | optional (default: `true`) | Allow users to change record grouping from the toolbar. Toggle only — the grouping itself is configured in the view-level `grouping` block. |
| **addRecordForm** | `boolean` | optional (default: `false`) | Add records through a form instead of inline |
| **editInline** | `boolean` | optional (default: `false`) | Allow users to edit records inline — click a cell to edit it with the field's type-aware widget (the same control the form uses). Off by default: the list is read-only unless the author opts in. |
| **editInline** | `boolean` | optional (default: `true`) | Allow users to edit records inline. On by default: a user who may update the object edits a cell in place with the field's type-aware widget (the same control the form uses), under the permission gate that already decides whether the object is editable at all. Declare `editInline: false` to make the list read-only in place. |
| **hideFields** | `boolean` | optional (default: `false`) | Allow users to hide/show fields from the toolbar (the affordance behind the view-level `hiddenFields` list). Boolean toggle — distinct from the record-details component's `hideFields`, which is an array of field names to omit. Off by default: column hiding is opt-in. |
| **rowColor** | `boolean` | optional (default: `false`) | Allow users to configure row colouring from the toolbar. Boolean toggle — the colour rules themselves live in the view-level `rowColor` block. Off by default: row colouring is opt-in. |
| **buttons** | `string[]` | optional | Custom action button IDs to show in the toolbar |
Expand Down
6 changes: 3 additions & 3 deletions content/docs/references/ui/view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1081,7 +1081,7 @@ View filter rule
| **rowHeight** | `boolean` | optional (default: `true`) | Allow users to toggle row height/density |
| **group** | `boolean` | optional (default: `true`) | Allow users to change record grouping from the toolbar. Toggle only — the grouping itself is configured in the view-level `grouping` block. |
| **addRecordForm** | `boolean` | optional (default: `false`) | Add records through a form instead of inline |
| **editInline** | `boolean` | optional (default: `false`) | Allow users to edit records inline — click a cell to edit it with the field's type-aware widget (the same control the form uses). Off by default: the list is read-only unless the author opts in. |
| **editInline** | `boolean` | optional (default: `true`) | Allow users to edit records inline. On by default: a user who may update the object edits a cell in place with the field's type-aware widget (the same control the form uses), under the permission gate that already decides whether the object is editable at all. Declare `editInline: false` to make the list read-only in place. |
| **hideFields** | `boolean` | optional (default: `false`) | Allow users to hide/show fields from the toolbar (the affordance behind the view-level `hiddenFields` list). Boolean toggle — distinct from the record-details component's `hideFields`, which is an array of field names to omit. Off by default: column hiding is opt-in. |
| **rowColor** | `boolean` | optional (default: `false`) | Allow users to configure row colouring from the toolbar. Boolean toggle — the colour rules themselves live in the view-level `rowColor` block. Off by default: row colouring is opt-in. |
| **buttons** | `string[]` | optional | Custom action button IDs to show in the toolbar |
Expand Down Expand Up @@ -1464,7 +1464,7 @@ View filter rule
| **rowHeight** | `boolean` | optional (default: `true`) | Allow users to toggle row height/density |
| **group** | `boolean` | optional (default: `true`) | Allow users to change record grouping from the toolbar. Toggle only — the grouping itself is configured in the view-level `grouping` block. |
| **addRecordForm** | `boolean` | optional (default: `false`) | Add records through a form instead of inline |
| **editInline** | `boolean` | optional (default: `false`) | Allow users to edit records inline — click a cell to edit it with the field's type-aware widget (the same control the form uses). Off by default: the list is read-only unless the author opts in. |
| **editInline** | `boolean` | optional (default: `true`) | Allow users to edit records inline. On by default: a user who may update the object edits a cell in place with the field's type-aware widget (the same control the form uses), under the permission gate that already decides whether the object is editable at all. Declare `editInline: false` to make the list read-only in place. |
| **hideFields** | `boolean` | optional (default: `false`) | Allow users to hide/show fields from the toolbar (the affordance behind the view-level `hiddenFields` list). Boolean toggle — distinct from the record-details component's `hideFields`, which is an array of field names to omit. Off by default: column hiding is opt-in. |
| **rowColor** | `boolean` | optional (default: `false`) | Allow users to configure row colouring from the toolbar. Boolean toggle — the colour rules themselves live in the view-level `rowColor` block. Off by default: row colouring is opt-in. |
| **buttons** | `string[]` | optional | Custom action button IDs to show in the toolbar |
Expand Down Expand Up @@ -1635,7 +1635,7 @@ User action toggles for the view toolbar
| **rowHeight** | `boolean` | optional (default: `true`) | Allow users to toggle row height/density |
| **group** | `boolean` | optional (default: `true`) | Allow users to change record grouping from the toolbar. Toggle only — the grouping itself is configured in the view-level `grouping` block. |
| **addRecordForm** | `boolean` | optional (default: `false`) | Add records through a form instead of inline |
| **editInline** | `boolean` | optional (default: `false`) | Allow users to edit records inline — click a cell to edit it with the field's type-aware widget (the same control the form uses). Off by default: the list is read-only unless the author opts in. |
| **editInline** | `boolean` | optional (default: `true`) | Allow users to edit records inline. On by default: a user who may update the object edits a cell in place with the field's type-aware widget (the same control the form uses), under the permission gate that already decides whether the object is editable at all. Declare `editInline: false` to make the list read-only in place. |
| **hideFields** | `boolean` | optional (default: `false`) | Allow users to hide/show fields from the toolbar (the affordance behind the view-level `hiddenFields` list). Boolean toggle — distinct from the record-details component's `hideFields`, which is an array of field names to omit. Off by default: column hiding is opt-in. |
| **rowColor** | `boolean` | optional (default: `false`) | Allow users to configure row colouring from the toolbar. Boolean toggle — the colour rules themselves live in the view-level `rowColor` block. Off by default: row colouring is opt-in. |
| **buttons** | `string[]` | optional | Custom action button IDs to show in the toolbar |
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/authorable-defaults/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
"ui/TimelineConfig:scale = \"week\"",
"ui/UrlNavItem:target = \"_self\"",
"ui/UserActionsConfig:addRecordForm = false",
"ui/UserActionsConfig:editInline = false",
"ui/UserActionsConfig:editInline = true",
"ui/UserActionsConfig:filter = true",
"ui/UserActionsConfig:group = true",
"ui/UserActionsConfig:hideFields = false",
Expand Down
24 changes: 24 additions & 0 deletions packages/spec/scripts/lib/default-changes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,30 @@ export const DEFAULT_CHANGES_BY_MAJOR: Readonly<Record<number, readonly Declared
+ 'To keep 25 rows per page, write it: `pagination: { pageSize: 25 }`. To take the platform '
+ 'default, change nothing.',
},
{
key: 'ui/UserActionsConfig:editInline',
from: 'false',
to: 'true',
reason:
'A RULED behaviour change, not a correction: the maintainer ruled on 2026-10-10, for the '
+ 'v18 line, 「乙 v18 把 spec 默认翻成 true,editInline: false 变成关法。」 — a list view is '
+ 'editable in place by default, under the permission gate that already exists, and '
+ '`userActions: { editInline: false }` is the opt-out. The one-vocabulary rule objectui '
+ 'ruled for the fold (a boolean `inlineEdit` folds into `editInline`, an explicit `editInline` '
+ 'wins, declared = enforced) stands; only the default\'s value moves. '
+ 'What moves: a list view (or a page `interfaceConfig`) whose `userActions` block omits '
+ '`editInline` now parses to `true` where it parsed to `false`, so the renderer offers the '
+ 'inline-edit toggle to a user who may `update` the object — and to nobody else, because '
+ 'the permission gate is untouched. A view with NO `userActions` block parses with none on '
+ 'either side; the renderer reads an absent key as this default. The accept set does not '
+ 'move: `editInline` is still a boolean, and every authored value parses exactly as before. '
+ 'A document that SERIALISED an earlier parse (an `os compile` artifact) carries a written '
+ '`false` and stays read-only in place: recompile it, or delete the key. '
+ 'Who is affected: the author who relied on the documented default for a list that is '
+ 'read-only by nature (a log, an audit trail, a history, a report roll-up). To keep that '
+ 'list read-only in place, write it: `userActions: { editInline: false }`. To take the '
+ 'platform default, change nothing. D3 entry: `list-view-edit-inline-default-on`.',
},
{
key: 'data/Cube:public',
from: 'false',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import type { SemanticMigration } from '../../types.js';

// The declared default of `ListView.userActions.editInline` moved from `false`
// to `true` (maintainer ruling 2026-10-10, the v18 line, verbatim:
// 「乙 v18 把 spec 默认翻成 true,editInline: false 变成关法。」). A default move
// reaches every silent document with no parse error and nothing in the
// author's diff, so the upgrade path carries it as a TODO: only the deployment
// can say which list that never declared the key was relying on read-only
// cells. The same class as `view-pagination-page-size-default-50` and
// protocol 12's `rest-requireauth-default-flip`: no key is removed, so there
// is no tombstone and no RETIRED_KEYS_BY_MAJOR row, and no D2 conversion
// exists — a mechanical pass writing `editInline: false` into every silent
// view would preserve the old posture and defeat the ruling, and one writing
// `true` would add nothing the default does not already do. The one-vocabulary
// rule the printed `reason` names is objectui#5144 (a boolean view-level
// `inlineEdit` folds into `editInline`; an explicit `editInline` wins); the
// citation lives here, not in the printed guidance, which carries no tracker
// number.
export const entry: SemanticMigration = {
id: 'list-view-edit-inline-default-on',
// No backticks in `surface` — build-upgrade-guide.ts renders it inside a code
// span AND a table cell.
surface: 'ui.UserActionsConfig.editInline — an OMITTED editInline inside a list view\'s '
+ 'userActions block, or a page interfaceConfig.userActions block',
replacement: 'nothing, to take the platform default: the list is editable in place by a user who '
+ 'may update the object, under the permission gate that already exists. To keep a list '
+ 'read-only in place — a log, an audit trail, a history, a report roll-up — write it: '
+ '`userActions: { editInline: false }`',
reason:
'A RULED behaviour change on a default, so there is nothing to rewrite and nothing to '
+ 'refuse: the maintainer\'s ruling of 2026-10-10 made a list view editable in place by '
+ 'default on the v18 line, and the declared default of `UserActionsConfigSchema.editInline` '
+ 'moved from `false` to `true`. A `userActions` block that omits `editInline` now parses to '
+ '`true` — the renderer offers the inline-edit toggle, and a user who may `update` the object '
+ 'edits a cell in place with the field\'s type-aware widget; a user without `update` sees no '
+ 'toggle, because the permission gate is untouched. A view with no `userActions` block at all '
+ 'parses with none on either side; the renderer reads an absent key as the spec default. The '
+ 'one-vocabulary rule objectui ruled for the fold stands: a boolean view-level `inlineEdit` folds into '
+ '`editInline` (`true` reads on and opens the grid in edit mode), and an explicit `editInline` '
+ 'wins. Not losslessly convertible because the question is intent, not text: only the '
+ 'deployment knows which silent lists were relying on read-only cells. The accept set is '
+ 'unchanged — a boolean — and every authored `editInline` parses exactly as before. A '
+ 'document that serialised an earlier parse (an `os compile` artifact) carries a written '
+ '`false` and stays read-only in place; recompile it, or delete the key.',
acceptanceCriteria:
'An empty `userActions` block parses to `editInline: true`, and a list view carrying '
+ '`userActions: {}` parses to `userActions.editInline` true; an authored '
+ '`userActions: { editInline: false }` still parses to `false`; a view-level `inlineEdit: true` '
+ 'still parses as before, with no `userActions` block materialised. In the console, a grid view '
+ 'that declares neither key offers the inline-edit toggle to a user with `update` on the object '
+ 'and not to a user without it; `userActions: { editInline: false }` removes it; '
+ '`inlineEdit: true` still opens the grid in edit mode. Every list that must stay read-only in '
+ 'place declares `userActions: { editInline: false }` and shows no toggle.',
};
Loading
Loading