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
20 changes: 20 additions & 0 deletions .changeset/12086-edit-inline-default-on.md
Original file line number Diff line number Diff line change
@@ -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.)
8 changes: 4 additions & 4 deletions .changeset/5144-editinline-fold.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,24 @@
'@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.

**The console toggle is session-only (`@object-ui/app-shell`).** Both keys are the author's permission. The console's toolbar toggle used to store a user's edit mode in the view's `inlineEdit`; after the fold, switching it off would have taken the toggle away for good. It now writes nothing. The toggle switches edit mode for the session, and each load starts from the view's own `inlineEdit`. `ListView` still reports the toggle through `onInlineEditChange`.

**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.
Expand Down
24 changes: 14 additions & 10 deletions content/docs/plugins/plugin-view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ const NOT_RELAYED: Record<string, NotRelayed> = {
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 ──────────────────────────────────────
Expand Down
13 changes: 9 additions & 4 deletions packages/app-shell/src/views/InterfaceListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
18 changes: 10 additions & 8 deletions packages/app-shell/src/views/ObjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1523,21 +1523,23 @@ 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
* `ListView` offers the wide toolbar toggle only to a host that wires one.
*
* 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.
Expand Down
Loading
Loading