diff --git a/.changeset/28912-hide-fields-default-on.md b/.changeset/28912-hide-fields-default-on.md new file mode 100644 index 00000000000..b49f2f7d853 --- /dev/null +++ b/.changeset/28912-hide-fields-default-on.md @@ -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. diff --git a/content/docs/references/ui/page.mdx b/content/docs/references/ui/page.mdx index fd6124f2cb0..985d5c0b0d6 100644 --- a/content/docs/references/ui/page.mdx +++ b/content/docs/references/ui/page.mdx @@ -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 | diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index d0ea8c8c258..9ae054c37e6 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -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 | @@ -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 | @@ -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 | diff --git a/packages/spec/authorable-defaults/ui.json b/packages/spec/authorable-defaults/ui.json index 2f0fae539b7..df19866021e 100644 --- a/packages/spec/authorable-defaults/ui.json +++ b/packages/spec/authorable-defaults/ui.json @@ -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", diff --git a/packages/spec/scripts/lib/default-changes.ts b/packages/spec/scripts/lib/default-changes.ts index dc5548974fd..5c1b0c60ad5 100644 --- a/packages/spec/scripts/lib/default-changes.ts +++ b/packages/spec/scripts/lib/default-changes.ts @@ -716,5 +716,31 @@ export const DEFAULT_CHANGES_BY_MAJOR: Readonly { 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(); }); @@ -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); }); diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index dae4b859064..53cd12faac2 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -1883,10 +1883,20 @@ export const VisualizationTypeSchema = lazySchema(() => z.enum([ * fold (`SHOW_FLAG_TO_USER_ACTION` in normalize-list-view.ts; ruled Option A on * objectui#5435): the ListView toolbar already honoured all three, but no * spec-valid document could declare them, so the fold's own output failed the - * save gate. The defaults asymmetry is LOAD-BEARING, copied from the renderer's - * reads, not chosen here: `group` defaults ON (`ua?.group !== false`) like the - * other core toolbar controls, while `hideFields` / `rowColor` default OFF - * (`=== true`) — column hiding and row colouring are opt-in affordances. + * save gate. `group` defaults ON (`ua?.group !== false`) like the other core + * toolbar controls, and `rowColor` defaults OFF (`=== true`): row colouring is + * an opt-in affordance. + * + * `hideFields` defaults ON by maintainer ruling on #28912 + * (「Fable 评估(核对后) 同意立卡」, confirmed on the card: + * 「#28911 和 #28912 都保留,按卡上写的做。」): a reader + * can hide and show the view's columns without the author declaring it, and + * `hideFields: false` is the opt-out. The old OFF default was copied from the + * renderer's legacy `=== true` read, not chosen as product; the renderer now + * reads `ua?.hideFields !== false`. The choice stays 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` (objectui#10694). D3 entry: + * `list-view-hide-fields-default-on` (protocol 18). * * `editInline` defaults ON by maintainer ruling (2026-10-10, the v18 line: * 「乙 v18 把 spec 默认翻成 true,editInline: false 变成关法。」): a list view @@ -1918,7 +1928,7 @@ export const UserActionsConfigSchema = lazySchema(() => strictObject({ 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(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.'), + hideFields: z.boolean().default(true).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. On by default: a reader hides and shows columns for themselves; declare `hideFields: false` to withhold the control.'), 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'), }).describe('User action toggles for the view toolbar')); diff --git a/ui/packages/core/src/utils/__tests__/normalize-list-view.foldOutputAuthorable-5435.test.ts b/ui/packages/core/src/utils/__tests__/normalize-list-view.foldOutputAuthorable-5435.test.ts index 1ca86944a11..3798970cc2c 100644 --- a/ui/packages/core/src/utils/__tests__/normalize-list-view.foldOutputAuthorable-5435.test.ts +++ b/ui/packages/core/src/utils/__tests__/normalize-list-view.foldOutputAuthorable-5435.test.ts @@ -206,8 +206,9 @@ describe('the fold`s output is authorable (objectui#5435)', () => { // `showGroup: ua?.group !== false` — default ON expect(defaults.group).toBe(true); - // `showHideFields: ua?.hideFields === true` — default OFF (opt-in) - expect(defaults.hideFields).toBe(false); + // `showHideFields: ua?.hideFields !== false` — default ON since + // objectstack#28912 (the maintainer flipped both sides together) + expect(defaults.hideFields).toBe(true); // `showColor: ua?.rowColor === true` — default OFF (opt-in) expect(defaults.rowColor).toBe(false); }); diff --git a/ui/packages/core/src/utils/normalize-list-view.ts b/ui/packages/core/src/utils/normalize-list-view.ts index 2f29bf8989b..863ff47fcf3 100644 --- a/ui/packages/core/src/utils/normalize-list-view.ts +++ b/ui/packages/core/src/utils/normalize-list-view.ts @@ -397,8 +397,9 @@ function readListViewKind(value: unknown): string | undefined { * per-flag, so a view may carry `userActions` for some toggles and a legacy * flag for others. NOTE the fold does not apply any default: an absent flag * stays absent, because the defaults are per-toggle (search/sort/filter/ - * rowHeight/group default ON, hideFields/rowColor default OFF) and belong to - * the renderer, not to the vocabulary bridge. + * rowHeight/group/hideFields default ON, rowColor default OFF — the spec's + * own defaults, `hideFields` ON since objectstack#28912) and belong to the + * renderer, not to the vocabulary bridge. * - the view's `inlineEdit` → `userActions.editInline` (objectui#5144), with * the value carried over and an explicit `editInline` winning. `inlineEdit` * stays on the result, because `ListView` seeds the grid's edit mode from diff --git a/ui/packages/plugin-list/src/ListView.tsx b/ui/packages/plugin-list/src/ListView.tsx index 983c9f9fe4e..91728613547 100644 --- a/ui/packages/plugin-list/src/ListView.tsx +++ b/ui/packages/plugin-list/src/ListView.tsx @@ -1813,12 +1813,14 @@ export const ListView = React.forwardRef(({ // `show*` flags are folded into it by `normalizeListViewSchema` above, so // there is no dual-read here — one vocabulary, one place. // - // The DEFAULTS are per-toggle and deliberately asymmetric, matching what - // these flags have always done: the four view-shaping affordances plus - // grouping are on unless turned off; column visibility and row coloring are - // off unless turned on. (`hideFields`/`rowColor` default OFF is objectui's - // historical behavior, kept deliberately — flipping it would grow two - // buttons on every existing view.) + // The DEFAULTS are per-toggle and read the spec's own defaults + // (`UserActionsConfigSchema`): the four view-shaping affordances, grouping + // and column visibility are on unless turned off; row coloring is off + // unless turned on. `hideFields` defaults ON by maintainer ruling on #28912 + // — a reader hides and shows columns for themselves, and an app withholds + // the control with `hideFields: false`. Where the choice is stored (the + // reader's own list state, or session-only on a system view with no + // `columns`) is the host's call, not this read's. // // This is the VIEW half of a NAME COLLISION, and the right half here: // `userActions` on a VIEW is toolbar policy (`UserActionsConfigSchema`), @@ -1844,7 +1846,7 @@ export const ListView = React.forwardRef(({ showRefresh: ua?.refresh !== false, showDensity: ua?.rowHeight !== false, showGroup: ua?.group !== false, - showHideFields: ua?.hideFields === true, + showHideFields: ua?.hideFields !== false, showColor: ua?.rowColor === true, // The bundled "Display" form is the default (objectui#12423): the bar // keeps Filter, Sort, Display and Search. `compactToolbar: false` is the diff --git a/ui/packages/plugin-list/src/__tests__/ListView.test.tsx b/ui/packages/plugin-list/src/__tests__/ListView.test.tsx index 0e72135775f..1992351172e 100644 --- a/ui/packages/plugin-list/src/__tests__/ListView.test.tsx +++ b/ui/packages/plugin-list/src/__tests__/ListView.test.tsx @@ -1753,6 +1753,36 @@ describe('ListView', () => { expect(screen.getByText('Hide fields')).toBeInTheDocument(); }); + // objectstack#28912: `hideFields` defaults ON, matching the spec default + // (`UserActionsConfigSchema`). A view that says nothing offers the + // control; `hideFields: false` withholds it. + it('should show Hide fields when userActions omits hideFields (default ON)', () => { + const schema = { + type: 'list-view', + compactToolbar: false, + objectName: 'contacts', + viewType: 'grid', + columns: ['name', 'email'], + } as unknown as ListViewSchema; + + renderWithProvider(); + expect(screen.getByText('Hide fields')).toBeInTheDocument(); + }); + + it('should not show Hide fields when userActions.hideFields is false', () => { + const schema = { + type: 'list-view', + compactToolbar: false, + objectName: 'contacts', + viewType: 'grid', + columns: ['name', 'email'], + userActions: { hideFields: false }, + } as unknown as ListViewSchema; + + renderWithProvider(); + expect(screen.queryByText('Hide fields')).not.toBeInTheDocument(); + }); + it('should hide Group when userActions.group is false', () => { const schema = { type: 'list-view', @@ -1768,17 +1798,19 @@ describe('ListView', () => { it('should keep the legacy show* flags working — stored views carry them', () => { // Folded by normalizeListViewSchema, so the renderer sees only userActions. + // Since `hideFields` defaults ON (objectstack#28912) a legacy `true` is + // indistinguishable from silence, so the fold is pinned by its `false`. const schema = { type: 'list-view', compactToolbar: false, objectName: 'contacts', viewType: 'grid', columns: ['name', 'email'], - showHideFields: true, + showHideFields: false, } as unknown as ListViewSchema; renderWithProvider(); - expect(screen.getByText('Hide fields')).toBeInTheDocument(); + expect(screen.queryByText('Hide fields')).not.toBeInTheDocument(); }); it('should let userActions win over a conflicting legacy flag', () => {