diff --git a/.changeset/12086-edit-inline-default-on.md b/.changeset/12086-edit-inline-default-on.md new file mode 100644 index 0000000000..07052811d9 --- /dev/null +++ b/.changeset/12086-edit-inline-default-on.md @@ -0,0 +1,20 @@ +--- +'@object-ui/plugin-list': minor +'@object-ui/app-shell': minor +--- + +List views read an absent `userActions.editInline` as on, the v18 protocol default (objectui#12086, the consumer half of objectstack#22605). A list view is editable in place by default, under the permission gate that already exists, and `userActions: { editInline: false }` makes it read-only in place. + +**A list view that declares neither key.** The maintainer's v18 ruling flipped the spec's `ListViewSchema.userActions.editInline` from `.default(false)` to `.default(true)`. `ListView` (`@object-ui/plugin-list`) reads an absent `editInline` as on (`!== false`). A grid view that declares neither `userActions.editInline` nor `inlineEdit` offers the inline-edit toggle, on the wide toolbar and in the compact settings popover, and opens out of edit mode. The previous release offered it there too, reading an absent key as "defer to the host"; objectui#5144 read it off on unreleased `main` only, and its entry in this release states the default as on. The fold objectui#5144 added is unchanged. + +**Behaviour change: the ADR-0047 interface page (`@object-ui/app-shell`).** The page reads its config's `userActions.editInline` with the same default and hands it to `ListView` as the grid's edit mode, because the page wires no inline-edit toggle. A page that declares nothing now opens its grid editable in place; `editInline: false` keeps it read-only. + +**Unchanged.** + +- The permission gate. The toggle, the compact entry and the edit mode are offered only where the object is editable in place and the current principal holds `update` on it. A principal without the grant sees no toggle. +- The `inlineEdit` fold (objectui#5144): `inlineEdit: true` reads on and opens the grid in edit mode, `inlineEdit: false` reads off, and an explicit `editInline` wins either way. +- The console toggle stays session-only (objectui#5144, ruling E). + +**What to do.** A list whose records are read-only by nature (a log, an audit trail, a history, a roll-up) declares `userActions: { editInline: false }`. A view or overlay where an earlier console stored `inlineEdit: false` still reads off; declaring `userActions.editInline: true` brings the toggle back. + +Nothing is added to a package entry: no export, prop, type member or language-pack key. (The bump is `minor` by this repo's release model: objectui's major follows the `@objectstack` family major, and its own behaviour changes ship as `minor` with the semantics stated here.) diff --git a/.changeset/5144-editinline-fold.md b/.changeset/5144-editinline-fold.md index 8a528c2365..0949c2056b 100644 --- a/.changeset/5144-editinline-fold.md +++ b/.changeset/5144-editinline-fold.md @@ -5,16 +5,16 @@ '@object-ui/plugin-view': patch --- -List views read `userActions.editInline` with the spec's default, off. A view's `inlineEdit` folds into it, and the console's inline-edit toggle no longer writes to the view (objectui#5144). +List views read `userActions.editInline` with the spec's default, on since objectstack-ai/objectstack#22605 (`editInline: z.boolean().default(true)`), and `editInline: false` is the opt-out. A view's `inlineEdit` folds into it, and the console's inline-edit toggle no longer writes to the view (objectui#5144). -**Breaking for a view that relied on the old default.** The spec declares `userActions.editInline` with `.default(false)`: the list is read-only unless the author opts in. The interface page already read it that way. The object-list toolbar did not: it read an absent `editInline` as "defer to the host", so every console grid offered the inline-edit toggle unless a view said `editInline: false`. The toolbar now reads it as the spec does. A grid view that declares neither `userActions.editInline` nor `inlineEdit` no longer offers the inline-edit toggle, on the wide toolbar or in the compact settings popover, and never opens in edit mode. +**The spec's default, read as declared.** The spec declares `userActions.editInline` with `.default(true)` (objectstack-ai/objectstack#22605): a list view is editable in place by default, under the permission gate, and `editInline: false` is the opt-out. The object-list toolbar used to read an absent `editInline` as "defer to the host", so every console grid offered the inline-edit toggle unless a view said `editInline: false`. The toolbar now reads it as the spec does. A grid view that declares neither `userActions.editInline` nor `inlineEdit` offers the inline-edit toggle, on the wide toolbar and in the compact settings popover, and opens out of edit mode. The flip of the default, and the interface page that reads it, are the objectui#12086 entry of this release. **The fold.** `normalizeListViewSchema` (`@object-ui/core`) now folds a boolean `inlineEdit` into `userActions.editInline`, the way it already folds the `show*` flags into the other toggles: - `inlineEdit: true` reads as `editInline: true`: the toggle is offered and the grid opens in edit mode; - `inlineEdit: false` reads as `editInline: false`: no toggle; - an explicit `userActions.editInline` wins over `inlineEdit`, in both directions; -- a view with neither key reads off. +- a view with neither key reads on, the spec's default. `inlineEdit` stays on the folded view, because `ListView` opens the grid in edit mode from it. Nothing migrates stored views. @@ -22,7 +22,7 @@ List views read `userActions.editInline` with the spec's default, off. A view's **A named view's `inlineEdit` keeps its precedence (`@object-ui/plugin-view`).** On a host's `renderListView`, `ObjectView` merges `userActions` from the node, the host's `views` entry and the active named view, the named view last. The named view's `userActions` now go through the same fold as the other two. So a named view's `inlineEdit` decides whether inline editing is offered ahead of a host entry's, as it already decided the edit mode. -**What to do.** A view that should offer inline editing declares `userActions.editInline: true`. The toggle then stays offered whatever the user does with it. Two costs come with the session-only toggle: +**What to do.** A list whose records are read-only by nature declares `userActions.editInline: false`. A view that declares `userActions.editInline: true` keeps the toggle offered whatever the user does with it. Two costs come with the session-only toggle: - the edit mode is not remembered across loads; - a view, or a personalization overlay, where the old toggle stored `inlineEdit: false` still reads off. That is existing data, and it is not migrated. Declaring `userActions.editInline: true` on the view brings the toggle back. diff --git a/content/docs/plugins/plugin-view.mdx b/content/docs/plugins/plugin-view.mdx index 41e88cfee1..6658bf75c4 100644 --- a/content/docs/plugins/plugin-view.mdx +++ b/content/docs/plugins/plugin-view.mdx @@ -514,16 +514,18 @@ node takes the value from the active named view first, ahead of the host's `views` entry (objectui#10758). `src/__tests__/ObjectView.namedViewProtocolKeys-8980.test.tsx` pins each family. -**Inline editing on that node is opt-in.** Both keys below are the author's -permission. `ListView` reads the node's `userActions.editInline` with the -spec's default, off, so a view that declares neither key offers no inline -editing (objectui#5144). When the node carries no `editInline` of its own, its -`inlineEdit` stands in for it. The named view's keys win over the host -`views` entry's, as for every member above: +**Inline editing on that node is on by default.** Both keys below are the +author's permission. `ListView` reads the node's `userActions.editInline` with +the spec's default, on since the v18 ruling (objectstack#22605), so a view that +declares neither key offers the inline-edit toggle (objectui#12086), and +`userActions.editInline: false` is how a view withdraws it. When the node +carries no `editInline` of its own, its `inlineEdit` stands in for it +(objectui#5144). The named view's keys win over the host `views` entry's, as +for every member above: | The view declares | Inline-edit toggle | Grid opens in edit mode | | --- | --- | --- | -| neither key | not offered | no | +| neither key | offered | no | | `inlineEdit: true` | offered | yes | | `inlineEdit: false` | not offered | no | | `userActions.editInline: true` | offered | only with `inlineEdit: true` | @@ -533,9 +535,11 @@ The toggle switches the edit mode for the session and reports it through `ListView`'s `onInlineEditChange`. The console stores nothing for it: each load opens in the mode the view's `inlineEdit` declares, so a user's choice is not remembered across loads. A view where an earlier console stored -`inlineEdit: false` still reads off; that data is not migrated. To offer inline -editing, declare `userActions.editInline: true`. Neither key opens editing past -the object's own editability or the user's permission to update the object. +`inlineEdit: false` still reads off; that data is not migrated, and declaring +`userActions.editInline: true` brings the toggle back. A list whose records are +read-only by nature declares `userActions.editInline: false`. Neither key opens +editing past the object's own editability or the user's permission to update +the object. ### Update diff --git a/packages/app-shell/src/views/InterfaceListPage.editInlineFold-5144.test.tsx b/packages/app-shell/src/views/InterfaceListPage.editInlineFold-5144.test.tsx index 967b236912..9a49c6e67c 100644 --- a/packages/app-shell/src/views/InterfaceListPage.editInlineFold-5144.test.tsx +++ b/packages/app-shell/src/views/InterfaceListPage.editInlineFold-5144.test.tsx @@ -1,22 +1,23 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. /** - * objectui#5144, the interface-page half. + * objectui#5144, the interface-page half, with the v18 default (objectui#12086). * * An ADR-0047 interface page reads the page config's `userActions.editInline` - * as `=== true` and hands it to `ListView` as the view's `inlineEdit`. It does - * not forward `editInline` itself, so the `userActions` block it builds carries - * no `editInline` key. Before objectui#5144, `ListView` read that absent key as - * "defer to the host channel" and offered inline editing on this page's compact - * toolbar. The page itself read the absent case as off. + * with the protocol's default, on (`!== false`, objectstack#22605), and hands + * it to `ListView` as the view's `inlineEdit`. It does not forward `editInline` + * itself, so the `userActions` block it builds carries no `editInline` key. The + * page wires no inline-edit toggle, so that `inlineEdit` is the grid's edit + * mode: a page that declares nothing opens its grid editable in place, and + * `editInline: false` is the opt-out. * - * After the B-fold, `ListView` reads what this page composes through - * `normalizeListViewSchema`, which folds the `inlineEdit` the page sets into - * `userActions.editInline`. So the two readings agree. This file pins it by - * capturing the schema the page hands `ListView` and running it through the - * real fold, which is the input `ListView`'s `inlineEditOffered` reads. What - * `ListView` does with `editInline` is pinned in `@object-ui/plugin-list`'s - * `ListView.permissions.test.tsx`. + * `ListView` reads what this page composes through `normalizeListViewSchema`, + * which folds the `inlineEdit` the page sets into `userActions.editInline`. So + * the two readings agree. This file pins it by capturing the schema the page + * hands `ListView` and running it through the real fold, which is the input + * `ListView`'s `inlineEditOffered` reads. What `ListView` does with + * `editInline`, the permission gate included, is pinned in + * `@object-ui/plugin-list`'s `ListView.permissions.test.tsx`. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; @@ -104,29 +105,29 @@ describe('InterfaceListPage: editInline reads the same as ListView after the fol testObjects = []; }); - it('an ABSENT page editInline reads off on both: no edit mode, and `editInline` folds to false', async () => { + it('an ABSENT page editInline reads on, on both: edit mode, and `editInline` folds to true (objectui#12086)', async () => { const schema = await composedSchema({ search: true }); - // The page's own reading (unchanged): `=== true`. - expect(schema.inlineEdit).toBe(false); + // The page's own reading: the protocol default, `!== false`. + expect(schema.inlineEdit).toBe(true); // The page forwards no `editInline` of its own… expect(schema.userActions).not.toHaveProperty('editInline'); - // …so `ListView` reads the folded one, which is off. - expect(editInlineRead(schema)).toBe(false); + // …so `ListView` reads the folded one, which is on. + expect(editInlineRead(schema)).toBe(true); }); - it('a page with no `userActions` block at all reads off the same way', async () => { + it('a page with no `userActions` block at all reads on the same way', async () => { const schema = await composedSchema(undefined); - expect(schema.inlineEdit).toBe(false); - expect(editInlineRead(schema)).toBe(false); + expect(schema.inlineEdit).toBe(true); + expect(editInlineRead(schema)).toBe(true); }); - it('a page that opts in reads on, on both', async () => { + it('an explicit page `editInline: true` reads on, on both', async () => { const schema = await composedSchema({ editInline: true }); expect(schema.inlineEdit).toBe(true); expect(editInlineRead(schema)).toBe(true); }); - it('an explicit page `editInline: false` stays off', async () => { + it('an explicit page `editInline: false` is the opt-out: off on both', async () => { const schema = await composedSchema({ editInline: false }); expect(schema.inlineEdit).toBe(false); expect(editInlineRead(schema)).toBe(false); diff --git a/packages/app-shell/src/views/InterfaceListPage.relayCensus-11572.test.ts b/packages/app-shell/src/views/InterfaceListPage.relayCensus-11572.test.ts index 9ae44b4a36..84b56d0a72 100644 --- a/packages/app-shell/src/views/InterfaceListPage.relayCensus-11572.test.ts +++ b/packages/app-shell/src/views/InterfaceListPage.relayCensus-11572.test.ts @@ -499,7 +499,7 @@ const NOT_RELAYED: Record = { showRecordCount: { kind: 'page-owned', from: 'showRecordCount', reason: "The page config's record-count toggle, page presentation policy; the referenced view's toggle does not stand in for it." }, allowPrinting: { kind: 'page-owned', from: 'allowPrinting', reason: "The page config's \"Allow users to print the page\". It went unwritten until objectui#11572, so the declared toggle drew no print button; the page's value only, like its siblings." }, userActions: { kind: 'page-owned', from: 'userActions', reason: "The page config's toolbar toggles, with interface-mode defaults (closed unless opted in, #2890). The view's toolbar vocabulary is the object page's policy, not this page's." }, - inlineEdit: { kind: 'page-owned', from: 'userActions', reason: "Read from the page's `userActions.editInline` (default off). Whether this page's cells are editable is the page author's decision, not the referenced view's." }, + inlineEdit: { kind: 'page-owned', from: 'userActions', reason: "Read from the page's `userActions.editInline` (default on since objectstack#22605; `editInline: false` opts out). Whether this page's cells are editable is the page author's decision, not the referenced view's." }, navigation: { kind: 'page-prop', from: 'recordAction', prop: 'onRowClick', reason: "The page config's `recordAction` (drawer / page / modal / none) decides how a record opens, wired through `onRowClick` to this page's own overlay. The view's `navigation` would be a second, competing answer." }, // ── The page's own binding and text ────────────────────────────────────── diff --git a/packages/app-shell/src/views/InterfaceListPage.tsx b/packages/app-shell/src/views/InterfaceListPage.tsx index 7b535d7805..00a41f16bf 100644 --- a/packages/app-shell/src/views/InterfaceListPage.tsx +++ b/packages/app-shell/src/views/InterfaceListPage.tsx @@ -565,10 +565,15 @@ export function InterfaceListPage({ page, className, onConfigChange, reserveEdit rowColor: userActions.rowColor === true, }, allowExport: false, - // Inline record editing is a page-authored property: a list block opts in - // via `userActions.editInline` (default off). When on, clicking a cell - // edits it with the dedicated field widgets, same as the object views. - inlineEdit: userActions.editInline === true, + // Inline record editing is a page-authored property read from + // `userActions.editInline`, on by default (the v18 protocol default, + // objectstack#22605): a page opts out with `editInline: false`. This page + // wires no inline-edit toggle, so the value is the grid's edit mode + // itself. When on, a cell edits in place with the dedicated field + // widgets, same as the object views, and only where `ListView`'s + // permission gate admits the principal (the object editable in place, and + // the `update` grant). + inlineEdit: userActions.editInline !== false, }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [objectDefName, viewDefJson, cfg, orgAttribution]); diff --git a/packages/app-shell/src/views/ObjectView.inlineEditSessionOnly-5144.test.tsx b/packages/app-shell/src/views/ObjectView.inlineEditSessionOnly-5144.test.tsx index 47843efbfd..8005d25d74 100644 --- a/packages/app-shell/src/views/ObjectView.inlineEditSessionOnly-5144.test.tsx +++ b/packages/app-shell/src/views/ObjectView.inlineEditSessionOnly-5144.test.tsx @@ -367,7 +367,7 @@ describe('objectui#5144 (ruling E) — the console inline-edit toggle writes not expect(folded()).toEqual({ editInline: false, inlineEdit: false }); }); - it('control: the same served view with no overlay declares nothing, and reads off by the spec default', async () => { + it('control: the same served view with no overlay declares nothing, which `ListView` reads on by the v18 spec default (objectui#12086)', async () => { const { meta } = makeStore([]); await openPage(meta, PLAIN_OBJECT, 'all'); expect(folded()).toEqual({ editInline: undefined, inlineEdit: undefined }); diff --git a/packages/app-shell/src/views/ObjectView.tsx b/packages/app-shell/src/views/ObjectView.tsx index c40ccec4f1..878bcee197 100644 --- a/packages/app-shell/src/views/ObjectView.tsx +++ b/packages/app-shell/src/views/ObjectView.tsx @@ -1523,12 +1523,14 @@ export interface ConsoleObjectViewProps { * nothing (objectui#5144, triage's ruling E). * * The view's `inlineEdit` and `userActions.editInline` are the AUTHOR's - * permission keys. The spec says so for both ("the list is read-only unless - * the author opts in"), and `normalizeListViewSchema` folds the first into the - * second. This toggle used to persist a USER's edit mode into `inlineEdit` - * through `persistViewPatch`. After the fold, switching it off stored - * `inlineEdit: false`, which reads as "not offered", so the toggle was gone - * from the next load and nothing in the console could bring it back. + * permission keys, and `normalizeListViewSchema` folds the first into the + * second. `editInline` is on by default since the v18 ruling (objectstack#22605, + * objectui#12086), so a view that declares neither key offers the toggle, and + * `editInline: false` withdraws it. This toggle used to persist a USER's edit + * mode into `inlineEdit` through `persistViewPatch`. After the fold, switching + * it off stored `inlineEdit: false`, which reads as "not offered", so the + * toggle was gone from the next load and nothing in the console could bring it + * back. * * The edit mode is now session state. `ListView` keeps it, and seeds it from * the view's `inlineEdit` on each load. The callback stays wired because @@ -1536,8 +1538,8 @@ export interface ConsoleObjectViewProps { * * Recorded costs: the edit mode is not remembered across loads, and an overlay * that already stores `inlineEdit: false` still reads off. That is existing - * data, and the maintainer's ruling rejects migrating it. A view that should - * offer inline editing declares `userActions.editInline: true`. + * data, and the maintainer's ruling rejects migrating it. Declaring + * `userActions.editInline: true` on such a view brings the toggle back. */ function keepInlineEditModeForTheSession(): void { // Deliberately empty: see the docblock. diff --git a/packages/core/src/utils/__tests__/normalize-list-view.inlineEditFold-5144.test.ts b/packages/core/src/utils/__tests__/normalize-list-view.inlineEditFold-5144.test.ts index 30c95fc954..b871667f83 100644 --- a/packages/core/src/utils/__tests__/normalize-list-view.inlineEditFold-5144.test.ts +++ b/packages/core/src/utils/__tests__/normalize-list-view.inlineEditFold-5144.test.ts @@ -9,14 +9,22 @@ /** * objectui#5144 — a view's `inlineEdit` folds into `userActions.editInline`. * - * The spec declares `userActions.editInline` with `.default(false)`. Stored - * views never carried that key: they carry `inlineEdit`, which authors declare - * and the console's list toolbar used to write. Until this fold, `ListView` - * could not read `editInline` with the spec default without taking inline - * editing away from every stored console view, so it read an absent - * `editInline` as "defer to the host". The maintainer ruled the B-fold: the - * stored key folds into the spec key, and the spec default is read as written. - * Triage's ruling E then stopped the toolbar writing `inlineEdit` at all. + * The spec declared `userActions.editInline` with `.default(false)` when this + * fold landed. Stored views never carried that key: they carry `inlineEdit`, + * which authors declare and the console's list toolbar used to write. Until + * this fold, `ListView` could not read `editInline` with the spec default + * without taking inline editing away from every stored console view, so it read + * an absent `editInline` as "defer to the host". The maintainer ruled the + * B-fold: the stored key folds into the spec key, and the spec default is read + * as written. Triage's ruling E then stopped the toolbar writing `inlineEdit` + * at all. + * + * The maintainer's v18 ruling (objectstack#22605) flipped that default to + * `true`, and `ListView` reads an absent `editInline` as on (objectui#12086). + * The fold is unchanged by it, and so is this table: the fold applies no + * default, it carries a boolean `inlineEdit` over. What the flip changes is + * the reader's verdict on the `neither key` row, pinned with the rest of the + * toolbar's behaviour. * * This file pins the fold's table. `@object-ui/plugin-list`'s * `ListView.permissions.test.tsx` pins what the toolbar does with its output. diff --git a/packages/core/src/utils/normalize-list-view.ts b/packages/core/src/utils/normalize-list-view.ts index bb73cf5d5f..dda379f7d2 100644 --- a/packages/core/src/utils/normalize-list-view.ts +++ b/packages/core/src/utils/normalize-list-view.ts @@ -127,13 +127,14 @@ const SHOW_FLAG_TO_USER_ACTION: Record = { * Whether a view's `inlineEdit` folds into `userActions.editInline` * (objectui#5144, the maintainer's B-fold). * - * The spec declares `userActions.editInline` with `.default(false)`: "the list - * is read-only unless the author opts in". It declares the view's `inlineEdit` - * as the same kind of permission ("allow inline editing"). Stored views carry - * `inlineEdit`, not `editInline`: authored views declare it, and the console's - * list toolbar used to write it. Folding it here puts both keys on one - * vocabulary, so `ListView` can read `editInline` with the spec default and a - * view that has inline editing keeps it. + * The spec declares `userActions.editInline` with `.default(true)` since the + * maintainer's v18 ruling (objectstack#22605): a list is editable in place by + * default, and `editInline: false` is the opt-out. It declares the view's + * `inlineEdit` as the same kind of permission ("allow inline editing"). Stored + * views carry `inlineEdit`, not `editInline`: authored views declare it, and + * the console's list toolbar used to write it. Folding it here puts both keys + * on one vocabulary, so `ListView` can read `editInline` with the spec default + * and a view's own `inlineEdit` still decides it when `editInline` is absent. * * The console's toolbar no longer writes `inlineEdit` (objectui#5144, ruling * E). It persisted a user's edit mode into this permission key, so switching @@ -402,7 +403,8 @@ function readListViewKind(value: unknown): string | undefined { * the value carried over and an explicit `editInline` winning. `inlineEdit` * stays on the result, because `ListView` seeds the grid's edit mode from * it. See {@link foldsInlineEditIntoEditInline}. An absent pair stays - * absent here as well; `ListView` reads that as off, the spec's default. + * absent here as well; `ListView` reads that as on, the spec's default + * (objectstack#22605). * - `aria: { label, describedBy }` → the spec's `AriaProps` * (`{ ariaLabel, ariaDescribedBy }`), and `sharing: { visibility, enabled }` * → the spec's `ViewSharing` (`{ type }`) — #2890 scope A step 5. `aria.live` diff --git a/packages/plugin-list/src/ListView.tsx b/packages/plugin-list/src/ListView.tsx index c12c9e3eef..9a536ecae8 100644 --- a/packages/plugin-list/src/ListView.tsx +++ b/packages/plugin-list/src/ListView.tsx @@ -1979,11 +1979,13 @@ export const ListView = React.forwardRef(({ * * ## Gap 2 — the declared `userActions.editInline`, with the spec's default * - * `ListViewSchema.userActions.editInline` is spec-declared with - * `.default(false)`: "the list is read-only unless the author opts in". It is - * read here with that default (`=== true`), so a view that declares nothing - * is not offered inline editing. That is the same reading the ADR-0047 - * interface page takes. + * `ListViewSchema.userActions.editInline` defaults to `true`: a list view is + * editable in place by default, under the gate above, and + * `editInline: false` is the opt-out (the maintainer's v18 ruling, + * objectstack#22605, which flipped the earlier `.default(false)`). It is read + * here with that default (`!== false`), so a view that declares nothing is + * offered inline editing wherever the gate admits the principal. The ADR-0047 + * interface page reads the same default. * * A view's `inlineEdit` reaches this read through the fold, not around it * (objectui#5144). `normalizeListViewSchema` folds a boolean `inlineEdit` @@ -1992,7 +1994,7 @@ export const ListView = React.forwardRef(({ * - a view with `inlineEdit: true` offers the toggle; * - a view with `inlineEdit: false` reads off; * - an explicit `editInline` wins over `inlineEdit`, either way; - * - a view with neither key reads off. + * - a view with neither key offers the toggle and opens out of edit mode. * * Both keys are the author's permission. This gate decides whether the toggle * is offered. Whether the grid is in edit mode is the `inlineEdit` state above: @@ -2001,16 +2003,17 @@ export const ListView = React.forwardRef(({ * (objectui#5144, ruling E). That write made the toggle one-way, because * switching it off stored `inlineEdit: false`, which this gate then read as * "not offered". A view with `editInline: true` and no `inlineEdit` offers the - * toggle and opens out of edit mode. + * toggle and opens out of edit mode, the same as a view with neither key. * - * ⚠️ The remedy for a view that relied on the old default: declare - * `userActions.editInline: true`. Two costs are recorded with the ruling. The - * edit mode is not remembered across loads. A view or overlay that already - * stores `inlineEdit: false`, written by the old toggle, still reads off; - * that is existing data, and it is not migrated. + * ⚠️ A list that is read-only by nature declares + * `userActions.editInline: false`. Two costs are recorded with the + * objectui#5144 ruling. The edit mode is not remembered across loads. A view + * or overlay that already stores `inlineEdit: false`, written by the old + * toggle, still reads off; that is existing data, it is not migrated, and + * declaring `userActions.editInline: true` brings the toggle back. */ const inlineEditOffered = React.useMemo(() => { - if ((schema.userActions as Record | undefined)?.editInline !== true) { + if ((schema.userActions as Record | undefined)?.editInline === false) { return false; } // [objectui#12082] The `listInlineEdit` row of the affordance-to-grant map: diff --git a/packages/plugin-list/src/__tests__/ListView.permissions.test.tsx b/packages/plugin-list/src/__tests__/ListView.permissions.test.tsx index 9e15b0d6e4..488d5bc360 100644 --- a/packages/plugin-list/src/__tests__/ListView.permissions.test.tsx +++ b/packages/plugin-list/src/__tests__/ListView.permissions.test.tsx @@ -305,12 +305,15 @@ function registerRecordingGrid() { } /** - * The author's opt-in (objectui#5144). `userActions.editInline` is read with the - * spec's `.default(false)`, so a view that declares nothing is offered no inline - * editing at all. A permission-gate case opts in first. Without it, "the - * principal loses the toggle" would pass for every principal. + * The author's opt-out (objectui#12086). `userActions.editInline` is read with + * the v18 spec default, on (objectstack#22605), so a view that declares nothing + * is offered inline editing wherever the permission gate admits the principal, + * and `editInline: false` is how a view withdraws it. The permission-gate cases + * below declare nothing, the shape the acceptance names; each "loses the + * toggle" case has a permitted twin with the same schema, so the absence reads + * the gate and not the declared switch. */ -const OPTED_IN = { userActions: { editInline: true } } as Partial; +const OPTED_OUT = { userActions: { editInline: false } } as Partial; /** `permissions: null` renders with NO PermissionProvider at all. */ function renderInlineEdit( @@ -354,20 +357,20 @@ describe('ListView – inline-edit toggle vs the principal permission gate (#464 else ComponentRegistry.unregister('object-grid'); }); - it('a principal WITH update keeps the inline-edit toggle', async () => { - renderInlineEdit(makeUpdatePermissions(true), OPTED_IN); + it('a principal WITH update is offered the inline-edit toggle on a view that declares nothing (objectui#12086)', async () => { + renderInlineEdit(makeUpdatePermissions(true)); await waitFor(() => expect(screen.getByTestId('grid-stub')).toBeInTheDocument()); expect(screen.queryByTestId('toolbar-inline-edit-toggle')).not.toBeNull(); }); - it('a principal WITHOUT update loses it', async () => { - renderInlineEdit(makeUpdatePermissions(false), OPTED_IN); + it('a principal WITHOUT update loses it, on the same view that declares nothing', async () => { + renderInlineEdit(makeUpdatePermissions(false)); await waitFor(() => expect(screen.getByTestId('grid-stub')).toBeInTheDocument()); expect(screen.queryByTestId('toolbar-inline-edit-toggle')).toBeNull(); }); it('with NO PermissionProvider the toggle survives (fail-open preserved)', async () => { - renderInlineEdit(null, OPTED_IN); + renderInlineEdit(null); await waitFor(() => expect(screen.getByTestId('grid-stub')).toBeInTheDocument()); expect(screen.queryByTestId('toolbar-inline-edit-toggle')).not.toBeNull(); }); @@ -444,9 +447,7 @@ describe('ListView – the declared userActions.editInline switch (#4647 gap 2)' }); it('`editInline: false` withholds the toggle from a fully-permitted principal', async () => { - renderInlineEdit(makeUpdatePermissions(true), { - userActions: { editInline: false }, - } as Partial); + renderInlineEdit(makeUpdatePermissions(true), OPTED_OUT); await waitFor(() => expect(screen.getByTestId('grid-stub')).toBeInTheDocument()); expect(screen.queryByTestId('toolbar-inline-edit-toggle')).toBeNull(); }); @@ -471,27 +472,27 @@ describe('ListView – the declared userActions.editInline switch (#4647 gap 2)' expect(screen.queryByTestId('toolbar-inline-edit-toggle')).not.toBeNull(); }); - it('an ABSENT editInline reads OFF, the spec default (objectui#5144)', async () => { - // This case used to pin the opposite: "an ABSENT editInline defers to the - // host channel, keeping existing views intact". That was the interim - // reading, kept because nothing folded the console's channel into - // `editInline`, so enforcing `.default(false)` would have taken the toggle - // off every stored console view. objectui#5144 is the maintainer's B-fold: - // `normalizeListViewSchema` now folds a stored `inlineEdit` into - // `editInline` (the stored-view cases below), so the spec default can be - // read as written. A view with neither key is not offered inline editing. + it('an ABSENT editInline reads ON, the v18 spec default: offered, and the grid opens out of edit mode (objectui#12086)', async () => { + // This case has pinned three readings. Before objectui#5144 an absent + // `editInline` deferred to the host channel. objectui#5144 read it with the + // spec's then `.default(false)`, so a view with neither key was offered no + // inline editing. The maintainer's v18 ruling (objectstack#22605) flipped + // the spec default to `true`, so a view with neither key is offered the + // toggle. The mode is still the view's `inlineEdit`, absent here, so the + // grid opens out of edit mode. `userActions` carries an unrelated toggle so + // the block exists and only `editInline` is absent from it. renderInlineEdit(makeUpdatePermissions(true), { userActions: { search: false }, } as Partial); await waitFor(() => expect(screen.getByTestId('grid-stub')).toBeInTheDocument()); - expect(screen.queryByTestId('toolbar-inline-edit-toggle')).toBeNull(); + expect(screen.queryByTestId('toolbar-inline-edit-toggle')).not.toBeNull(); expect(screen.getByTestId('grid-stub')).toHaveTextContent('false'); }); - it('…and the COMPACT entry reads off for an absent editInline too', async () => { + it('…and the COMPACT entry is offered for an absent editInline too', async () => { // The second render site of the same verdict. The entry's checkbox sits in // a section whose `defaultOpen` is `!!inlineEdit`, so with `inlineEdit` - // absent it would be missing for every input. The section's TITLE renders + // absent the checkbox itself is not rendered. The section's TITLE renders // whether or not the section is open, so the title is what this case and // its control below look for. renderInlineEdit(makeUpdatePermissions(true), { @@ -500,18 +501,18 @@ describe('ListView – the declared userActions.editInline switch (#4647 gap 2)' await waitFor(() => expect(screen.getByTestId('grid-stub')).toBeInTheDocument()); fireEvent.click(screen.getByTestId('view-settings-trigger')); const content = await screen.findByTestId('view-settings-content'); - expect(content).not.toHaveTextContent('Record editing'); + expect(content).toHaveTextContent('Record editing'); }); - it('…control: the same compact popover offers the entry once the view opts in', async () => { + it('…control: the same compact popover withholds the entry once the view declares `editInline: false`', async () => { renderInlineEdit(makeUpdatePermissions(true), { compactToolbar: true, - ...OPTED_IN, + ...OPTED_OUT, } as Partial); await waitFor(() => expect(screen.getByTestId('grid-stub')).toBeInTheDocument()); fireEvent.click(screen.getByTestId('view-settings-trigger')); const content = await screen.findByTestId('view-settings-content'); - expect(content).toHaveTextContent('Record editing'); + expect(content).not.toHaveTextContent('Record editing'); }); }); diff --git a/packages/plugin-list/src/__tests__/ListView.test.tsx b/packages/plugin-list/src/__tests__/ListView.test.tsx index b97b448441..b2502f5f8e 100644 --- a/packages/plugin-list/src/__tests__/ListView.test.tsx +++ b/packages/plugin-list/src/__tests__/ListView.test.tsx @@ -2654,9 +2654,9 @@ describe('ListView — inline-edit toggle drives grid editability', () => { objectName: 'contacts', viewType: 'grid', fields: ['name', 'email'], - // objectui#5144: inline editing is offered only where the view opts in - // (the spec's `.default(false)`). This case is about the toggle, so the - // view opts in and starts out of edit mode. + // The toggle is offered by default since objectui#12086 (the v18 spec + // default, on). This case is about the toggle, so the view declares it + // explicitly and starts out of edit mode. userActions: { editInline: true }, }; @@ -2688,7 +2688,7 @@ describe('ListView — inline-edit toggle drives grid editability', () => { objectName: 'contacts', viewType: 'grid', fields: ['name', 'email'], - // objectui#5144: the view opts in, as in the case above. + // Declared explicitly, as in the case above. userActions: { editInline: true }, }; diff --git a/packages/plugin-view/src/__tests__/ObjectView.namedViewEditInlineFold-5144.test.tsx b/packages/plugin-view/src/__tests__/ObjectView.namedViewEditInlineFold-5144.test.tsx index 2fef8dd5f8..426b2dfe4c 100644 --- a/packages/plugin-view/src/__tests__/ObjectView.namedViewEditInlineFold-5144.test.tsx +++ b/packages/plugin-view/src/__tests__/ObjectView.namedViewEditInlineFold-5144.test.tsx @@ -11,10 +11,10 @@ * outranks the host's, for whether inline editing is offered too. * * `normalizeListViewSchema` folds a view's `inlineEdit` into - * `userActions.editInline`, and `ListView` offers inline editing only where - * that reads `true`. The relay that builds the `list-view` node merges - * `userActions` across three layers: the node, the host's `views` entry, and - * the named view, most specific last. It folded the first two and spread the + * `userActions.editInline`, and `ListView` withholds inline editing where that + * reads `false` (an absent key reads on, objectui#12086). The relay that builds + * the `list-view` node merges `userActions` across three layers: the node, the + * host's `views` entry, and the named view, most specific last. It folded the first two and spread the * named view's raw. So once the fold existed, a host-layer `inlineEdit` became * a folded `editInline` that outranked the named view's own `inlineEdit`, the * inverse of the named-first `inlineEdit` rung on the same node. The seat's