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
19 changes: 19 additions & 0 deletions .changeset/28912-hide-fields-default-on.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@objectstack/spec': minor
'@object-ui/plugin-list': minor
---

spec(ui): a list view lets readers hide and show columns by default — `userActions.hideFields` defaults to `true`

Clause-②: yes (widening)
Metadata-protocol: semantic

A declared default moves. A list view whose `userActions` block says nothing about `hideFields` now offers the Hide Fields control, so a reader can remove a column they do not need without the app author having to turn the control on.

- **Before.** `UserActionsConfigSchema.hideFields` defaulted to `false`, and the list renderer drew the control only for an explicit `hideFields: true`. Generated apps and the CRM example never declared it, so their lists had no way to hide a column.
- **Now.** The default is `true`, and the list renderer reads `hideFields !== false`, so the spec default and the renderer agree. A view with no `userActions` block parses with none, and the renderer reads the absent key as the new default.
- **Interface pages.** A page `interfaceConfig.userActions` block that omits the key parses to `true` as well, but an interface page keeps its own closed-by-default toolbar, as it already does for `filter`, `rowHeight` and `group`: it draws the control only for an explicit `hideFields: true`.
- **Where the choice is kept** does not change. The control only subtracts from the columns the view draws. On a view that declares `columns`, or a saved view, the choice is stored on the reader's own list state and survives a reload for that reader only. On a system view that declares no `columns`, it lasts for the session. It never writes the shared view.
- **To withhold the control**, write `userActions: { hideFields: false }`. Every authored `hideFields` parses exactly as before, and a non-boolean is still refused.
- **Compiled artifacts.** A document that serialised an earlier parse (an `os compile` artifact) carries the old written `false` and keeps withholding the control. Recompile it, or delete the key.
- **Upgrade path.** The ADR-0087 D3 semantic entry `list-view-hide-fields-default-on` carries the change to `os migrate meta` and the upgrade guide. The default change is declared in the build's default-change ledger. No key is added or removed.
2 changes: 1 addition & 1 deletion content/docs/references/ui/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ View filter rule
| **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: `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. |
| **hideFields** | `boolean` | optional (default: `true`) | 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. On by default: a reader hides and shows columns for themselves; declare `hideFields: false` to withhold the control. |
| **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 @@ -1090,7 +1090,7 @@ View filter rule
| **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: `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. |
| **hideFields** | `boolean` | optional (default: `true`) | 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. On by default: a reader hides and shows columns for themselves; declare `hideFields: false` to withhold the control. |
| **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 @@ -1469,7 +1469,7 @@ View filter rule
| **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: `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. |
| **hideFields** | `boolean` | optional (default: `true`) | 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. On by default: a reader hides and shows columns for themselves; declare `hideFields: false` to withhold the control. |
| **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 @@ -1641,7 +1641,7 @@ User action toggles for the view toolbar
| **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: `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. |
| **hideFields** | `boolean` | optional (default: `true`) | 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. On by default: a reader hides and shows columns for themselves; declare `hideFields: false` to withhold the control. |
| **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 @@ -106,7 +106,7 @@
"ui/UserActionsConfig:editInline = true",
"ui/UserActionsConfig:filter = true",
"ui/UserActionsConfig:group = true",
"ui/UserActionsConfig:hideFields = false",
"ui/UserActionsConfig:hideFields = true",
"ui/UserActionsConfig:refresh = true",
"ui/UserActionsConfig:rowColor = false",
"ui/UserActionsConfig:rowHeight = true",
Expand Down
26 changes: 26 additions & 0 deletions packages/spec/scripts/lib/default-changes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,5 +716,31 @@ export const DEFAULT_CHANGES_BY_MAJOR: Readonly<Record<number, readonly Declared
+ 'page, write `mode: \'page\'`. The `semantic` entry `ui-record-open-overlay-modes-retired` '
+ 'carries the change to `os migrate meta` and the upgrade guide.',
},
{
key: 'ui/UserActionsConfig:hideFields',
from: 'false',
to: 'true',
reason:
'A RULED default change (maintainer ruling on #28912: 「Fable 评估(核对后) 同意立卡」, '
+ 'confirmed 「#28911 和 #28912 都保留,按卡上写的做。」): a list view lets a reader hide and '
+ 'show its columns by default, and `userActions: { hideFields: false }` is the opt-out. The '
+ 'old `false` was copied from the renderer\'s legacy `=== true` read, not chosen as product; '
+ 'the renderer now reads `ua?.hideFields !== false`, so the spec default and the renderer '
+ 'agree. What moves: a list view whose `userActions` block omits `hideFields` now parses to '
+ '`true` where it parsed to `false`, and the console\'s list offers the Hide Fields control. '
+ 'A view with NO `userActions` block parses with none on either side; the list renderer '
+ 'reads an absent key as this default. A page `interfaceConfig.userActions` block parses to '
+ '`true` too, but an interface page keeps its own closed-by-default toolbar (as it already '
+ 'does for `filter`, `rowHeight` and `group`) and still draws the control only for an '
+ 'explicit `hideFields: true`. The accept set does not move: '
+ '`hideFields` is still a boolean, and every authored value parses exactly as before. The '
+ 'control only subtracts from the columns the view draws, and the choice is the reader\'s '
+ 'own: stored on their list state on a view that declares `columns` or a saved view, '
+ 'session-only on a system view with no `columns`; it never writes the shared view. A '
+ 'document that SERIALISED an earlier parse (an `os compile` artifact) carries a written '
+ '`false` and keeps withholding the control: recompile it, or delete the key. To withhold '
+ 'the control, write `userActions: { hideFields: false }`. To take the platform default, '
+ 'change nothing. D3 entry: `list-view-hide-fields-default-on`.',
},
],
};
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.hideFields` moved from `false`
// to `true` (maintainer ruling on #28912, 2026-11-02, verbatim:
// 「Fable 评估(核对后) 同意立卡」, confirmed 「#28911 和 #28912 都保留,按卡上写的做。」).
// 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 its
// columns being fixed for every reader. The same class as
// `list-view-edit-inline-default-on` and `view-pagination-page-size-default-50`:
// 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 `hideFields: 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.
// Where the reader's choice is kept is objectui#10694's and objectui#12505's
// rule in the console's list renderer, which this change leaves alone; the
// citations live here, not in the printed guidance, which carries no tracker
// number.
export const entry: SemanticMigration = {
id: 'list-view-hide-fields-default-on',
surface: 'ui.UserActionsConfig.hideFields — an OMITTED hideFields inside a list view\'s '
+ 'userActions block',
replacement: 'nothing, to take the platform default: a reader can hide and show the list\'s '
+ 'columns for themselves. To withhold the control on a list whose columns must stay as '
+ 'authored for every reader, write it: `userActions: { hideFields: 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-11-02 made the Hide Fields control available on a '
+ 'list view by default, and the declared default of `UserActionsConfigSchema.hideFields` moved '
+ 'from `false` to `true`. The old `false` was copied from the list renderer\'s legacy read, not '
+ 'chosen as product; the renderer now reads an absent key as on. A `userActions` block that '
+ 'omits `hideFields` now parses to `true`, and the console\'s list offers the control. 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 control only subtracts from the columns the view draws, '
+ 'and the choice is the reader\'s own: stored on their own list state on a view that declares '
+ '`columns` or on a saved view, kept for the session only on a system view that declares no '
+ '`columns`, and never written into the shared view. An interface page keeps its own '
+ 'closed-by-default toolbar and still draws the control only for an explicit `true`. Not '
+ 'losslessly convertible because the question is intent, not text: only the deployment knows '
+ 'which silent lists were relying on fixed columns. The accept set is unchanged — a boolean — '
+ 'and every authored `hideFields` parses exactly as before. A document that serialised an '
+ 'earlier parse (an `os compile` artifact) carries a written `false` and keeps withholding the '
+ 'control; recompile it, or delete the key.',
acceptanceCriteria:
'An empty `userActions` block parses to `hideFields: true`, and a list view carrying '
+ '`userActions: {}` parses to `userActions.hideFields` true; an authored '
+ '`userActions: { hideFields: false }` still parses to `false`; a list view with no '
+ '`userActions` block still parses with none. In the console, a grid view that declares no '
+ '`hideFields` offers the Hide Fields control, and `userActions: { hideFields: false }` removes '
+ 'it. Hiding a column on a view that declares `columns`, or on a saved view, survives a reload '
+ 'for that reader only; on a system view with no `columns` it lasts for the session. Every list '
+ 'whose columns must stay as authored declares `userActions: { hideFields: false }` and shows '
+ 'no control.',
};
53 changes: 44 additions & 9 deletions packages/spec/src/ui/view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3435,7 +3435,7 @@ describe('UserActionsConfigSchema', () => {
expect(config.group).toBe(true);
expect(config.addRecordForm).toBe(false);
expect(config.editInline).toBe(true);
expect(config.hideFields).toBe(false);
expect(config.hideFields).toBe(true);
expect(config.rowColor).toBe(false);
expect(config.buttons).toBeUndefined();
});
Expand Down Expand Up @@ -3495,16 +3495,51 @@ describe('UserActionsConfigSchema', () => {
});
});

// 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`
// as `ua?.hideFields === true` / `ua?.rowColor === true` (absent ⇒ hidden).
// Normalising the three to one default would silently flip a toolbar
// affordance on every view that never wrote the key.
it('pins the group-ON / hideFields-OFF / rowColor-OFF defaults asymmetry, copied from what the renderer reads', () => {
// Maintainer ruling on #28912: 「Fable 评估(核对后) 同意立卡」, confirmed on
// the card 「#28911 和 #28912 都保留,按卡上写的做。」 A reader can hide and
// show the view's columns by default; `hideFields: false` is the opt-out.
// The old OFF default was copied from the renderer's legacy `=== true` read;
// the renderer now reads `ua?.hideFields !== false`, so the two agree again.
describe('hideFields defaults ON (maintainer ruling)', () => {
it('an absent key parses to true', () => {
expect(UserActionsConfigSchema.parse({}).hideFields).toBe(true);
const view = ListViewSchema.parse({
name: 'accounts',
label: 'Accounts',
columns: [{ field: 'name' }],
userActions: {},
});
expect(view.userActions?.hideFields).toBe(true);
});

it('an explicit false stays false — the opt-out', () => {
expect(UserActionsConfigSchema.parse({ hideFields: false }).hideFields).toBe(false);
const view = ListViewSchema.parse({
name: 'accounts',
label: 'Accounts',
columns: [{ field: 'name' }],
userActions: { hideFields: false },
});
expect(view.userActions?.hideFields).toBe(false);
});

it('a view with no `userActions` block still materialises none', () => {
const view = ListViewSchema.parse({
name: 'accounts',
label: 'Accounts',
columns: [{ field: 'name' }],
});
expect(view.userActions).toBeUndefined();
});
});

// `group` is ON unless turned off (`ua?.group !== false`), `rowColor` is OFF
// unless turned on (`ua?.rowColor === true`) — the renderer's reads, ruled A
// on objectui#5435. `hideFields` left this pair by the ruling above.
it('pins the group-ON / hideFields-ON / rowColor-OFF defaults, matching what the renderer reads', () => {
const config = UserActionsConfigSchema.parse({});
expect(config.group).toBe(true);
expect(config.hideFields).toBe(false);
expect(config.hideFields).toBe(true);
expect(config.rowColor).toBe(false);
});

Expand Down
Loading
Loading