diff --git a/.changeset/22605-spec-list-view-edit-inline-default-on.md b/.changeset/22605-spec-list-view-edit-inline-default-on.md new file mode 100644 index 00000000000..ca9a8e7a6cc --- /dev/null +++ b/.changeset/22605-spec-list-view-edit-inline-default-on.md @@ -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 + + + +**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. diff --git a/content/docs/references/ui/page.mdx b/content/docs/references/ui/page.mdx index c88eda893c9..74e9453e0f8 100644 --- a/content/docs/references/ui/page.mdx +++ b/content/docs/references/ui/page.mdx @@ -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 | diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 76fc5efec84..982391d7691 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -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 | @@ -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 | @@ -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 | diff --git a/packages/spec/authorable-defaults/ui.json b/packages/spec/authorable-defaults/ui.json index 9cd6a41865a..6c631261088 100644 --- a/packages/spec/authorable-defaults/ui.json +++ b/packages/spec/authorable-defaults/ui.json @@ -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", diff --git a/packages/spec/scripts/lib/default-changes.ts b/packages/spec/scripts/lib/default-changes.ts index d5206e12ac3..991abd11a7a 100644 --- a/packages/spec/scripts/lib/default-changes.ts +++ b/packages/spec/scripts/lib/default-changes.ts @@ -554,6 +554,30 @@ export const DEFAULT_CHANGES_BY_MAJOR: Readonly { expect(config.rowHeight).toBe(true); expect(config.group).toBe(true); expect(config.addRecordForm).toBe(false); - expect(config.editInline).toBe(false); + expect(config.editInline).toBe(true); expect(config.hideFields).toBe(false); expect(config.rowColor).toBe(false); expect(config.buttons).toBeUndefined(); }); + // Maintainer ruling 2026-10-10 (the v18 line): 「乙 v18 把 spec 默认翻成 true, + // editInline: false 变成关法。」 A list view is editable in place by default, + // under the permission gate that already exists; `editInline: false` is the + // opt-out. The rule of objectui#5144 (one vocabulary, the `inlineEdit` fold, + // declared = enforced) stands — only the default's value moves. + describe('editInline defaults ON (maintainer ruling, protocol 18)', () => { + it('an absent key parses to true', () => { + expect(UserActionsConfigSchema.parse({}).editInline).toBe(true); + const view = ListViewSchema.parse({ + name: 'audit_log', + label: 'Audit Log', + columns: [{ field: 'name' }], + userActions: {}, + }); + expect(view.userActions?.editInline).toBe(true); + }); + + it('an explicit false stays false — the opt-out the ruling names', () => { + expect(UserActionsConfigSchema.parse({ editInline: false }).editInline).toBe(false); + const view = ListViewSchema.parse({ + name: 'audit_log', + label: 'Audit Log', + columns: [{ field: 'name' }], + userActions: { editInline: false }, + }); + expect(view.userActions?.editInline).toBe(false); + }); + + it('an explicit true still parses to true', () => { + expect(UserActionsConfigSchema.parse({ editInline: true }).editInline).toBe(true); + }); + + // The fold (`inlineEdit` → `userActions.editInline`, an explicit `editInline` + // winning) lives in the renderer, not here: the spec keeps both spellings + // as declared, so this pins the inputs the fold reads are unchanged — the + // view-level `inlineEdit` is still optional with no default of its own, and + // it does not materialise a `userActions` block. + it('the view-level `inlineEdit: true` still parses as before, beside the new default', () => { + const view = ListViewSchema.parse({ + name: 'tasks', + label: 'Tasks', + columns: [{ field: 'name' }], + inlineEdit: true, + }); + expect(view.inlineEdit).toBe(true); + expect(view.userActions).toBeUndefined(); + const silent = ListViewSchema.parse({ + name: 'tasks', + label: 'Tasks', + columns: [{ field: 'name' }], + }); + expect(silent.inlineEdit).toBeUndefined(); + }); + }); + // The split is copied from the renderer's reads, not chosen by the spec // (ruled A on objectui#5435): ListView.tsx computes `showGroup` as // `ua?.group !== false` (absent ⇒ shown) but `showHideFields` / `showColor` diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 3b9089e2b5f..6979ade6158 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -1512,6 +1512,15 @@ export const VisualizationTypeSchema = lazySchema(() => z.enum([ * other core toolbar controls, while `hideFields` / `rowColor` default OFF * (`=== true`) — column hiding and row colouring are opt-in affordances. * + * `editInline` defaults ON by maintainer ruling (2026-10-10, 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 `editInline: false` is the opt-out. The renderer reads an absent + * key as this default; a boolean `inlineEdit` on the view folds into it, and + * an explicit `editInline` wins (objectui#5144's one-vocabulary rule, whose + * default value this flip changes and whose fold it leaves alone). D3 entry: + * `list-view-edit-inline-default-on` (protocol 18). + * * Name-collision note (same key NAME, different shape, on OTHER surfaces — * deliberate, each toggle is named after the config it gates): these three are * booleans HERE, while `rowColor` on the list view itself is a @@ -1532,7 +1541,7 @@ export const UserActionsConfigSchema = lazySchema(() => strictObject({ rowHeight: z.boolean().default(true).describe('Allow users to toggle row height/density'), group: z.boolean().default(true).describe('Allow users to change record grouping from the toolbar. Toggle only — the grouping itself is configured in the view-level `grouping` block.'), addRecordForm: z.boolean().default(false).describe('Add records through a form instead of inline'), - editInline: z.boolean().default(false).describe('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: z.boolean().default(true).describe('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: z.boolean().default(false).describe('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: z.boolean().default(false).describe('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: z.array(z.string()).optional().describe('Custom action button IDs to show in the toolbar'),