From aba3f606894bbe65ad2853f419db1b75a83c07e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 09:34:27 +0000 Subject: [PATCH 1/3] fix(plugin-list): Display reset compares hidden fields and density with the view's own On a console list the user's stored hidden fields and density are laid over the view's in `schema` (objectui#12474), so after a reload the Display control's "Reset to view default" compared the user's choice with itself and stayed disabled. `ListView` gains a `viewDefaults` host prop carrying the view's own values; the trigger count and the reset compare against it, and the reset writes it through the controls' doors. The console object page passes it. A host that does not pass it compares against `schema`, as before. Fixes objectui#12949 via the PR. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB --- .changeset/12949-display-reset-baseline.md | 10 ++ packages/app-shell/src/views/ObjectView.tsx | 27 ++- .../ObjectView.userListState-12418.test.tsx | 69 ++++++++ packages/plugin-list/src/ListView.tsx | 47 ++++- ...stView.displayResetBaseline-12949.test.tsx | 166 ++++++++++++++++++ 5 files changed, 305 insertions(+), 14 deletions(-) create mode 100644 .changeset/12949-display-reset-baseline.md create mode 100644 packages/plugin-list/src/__tests__/ListView.displayResetBaseline-12949.test.tsx diff --git a/.changeset/12949-display-reset-baseline.md b/.changeset/12949-display-reset-baseline.md new file mode 100644 index 0000000000..d76f45ed19 --- /dev/null +++ b/.changeset/12949-display-reset-baseline.md @@ -0,0 +1,10 @@ +--- +'@object-ui/plugin-list': minor +'@object-ui/app-shell': patch +--- + +The list toolbar's Display control compares hidden fields and density against the view's own value again (objectui#12949). On a console list, after a user hid a column (or changed the density) and reloaded, "Reset to view default" was disabled and the Display trigger counted nothing, although the list differed from the view. The console lays the user's stored choice over the view's value (objectui#12474), so the list compared that choice with itself. Now the reset is enabled whenever the user's hidden fields or density differ from the view's, the trigger count follows the same comparison, and pressing the reset writes the view's value back through the same doors the controls write through, so the user's stored preference follows, as Show all does. + +On a saved view that declares no `columns`, the view's own `hiddenFields` is never applied (objectui#12505), so the view hides nothing there: the reset returns to every drawn column. + +**Clause-②: yes (widening)**: the exported `ListViewProps` gains one optional member, `viewDefaults?: Pick`. A host that lays a user preference over those two schema keys passes the view's own values in it. When it is passed, its members are the whole baseline (an absent `hiddenFields` means the view hides nothing, an absent `rowHeight` means the default density). A host that does not pass it behaves exactly as before: `schema` is the view's own. No key is added to `ListViewSchema`, and no existing member changes type. `@object-ui/app-shell`'s object page passes it; its own exports do not change. diff --git a/packages/app-shell/src/views/ObjectView.tsx b/packages/app-shell/src/views/ObjectView.tsx index 4514b0062e..94d9ab0a22 100644 --- a/packages/app-shell/src/views/ObjectView.tsx +++ b/packages/app-shell/src/views/ObjectView.tsx @@ -3162,6 +3162,23 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co listPrefs.patchView(viewDef.id, { hiddenFields: hidden }); }; + /** + * objectui#12949 — the VIEW'S OWN density and hidden fields, before + * this user's preference (objectui#12474) is laid over them by the + * `rowHeight` and `hiddenFields` rungs below. `ListView` reads these + * through its `viewDefaults` prop: the Display control's count and its + * "Reset to view default" compare the user's list against the view's, + * not against the user's own stored choice. On a view that declares no + * `columns` the view's authored `hiddenFields` is never applied + * (objectui#10694, objectui#12505), so the view hides nothing there. + */ + const viewRowHeight: ListViewSchema['rowHeight'] = + (normalizeListViewSchema(viewDef ?? {}) as Pick).rowHeight + ?? listSchema.rowHeight; + const viewHiddenFields: ListViewSchema['hiddenFields'] = activeViewDeclaresColumns + ? ((viewDef as any).hiddenFields ?? listSchema.hiddenFields) + : undefined; + /** * ⚠️ THE RELAY. Every key below is a rung carrying the ACTIVE VIEW's @@ -3221,10 +3238,7 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co // here as `rowHeight`, and this relay never names the retired key. // objectui#12474 — the density this user chose is laid over it: // the view's own is the default for a user who has not chosen. - rowHeight: - viewPref?.rowHeight - ?? (normalizeListViewSchema(viewDef ?? {}) as Pick).rowHeight - ?? listSchema.rowHeight, + rowHeight: viewPref?.rowHeight ?? viewRowHeight, // Hydrate the persisted settings so they survive reload. The sort // and the column layout (objectui#12418), the density and the // hidden fields (objectui#12474) are the USER's: they are read @@ -3280,7 +3294,7 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co hiddenFields: activeViewDeclaresColumns // objectui#12474 — this user's own hidden fields over the // view's (a subtraction from the same `columns`). - ? (viewPref?.hiddenFields ?? (viewDef as any).hiddenFields ?? listSchema.hiddenFields) + ? (viewPref?.hiddenFields ?? viewHiddenFields) // objectui#12505 — on an unprojected SAVED view, this user's // own list alone, subtracted from the drawn defaults; the // view's authored key stays unapplied, as above. See @@ -3617,6 +3631,9 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co { diff --git a/packages/app-shell/src/views/ObjectView.userListState-12418.test.tsx b/packages/app-shell/src/views/ObjectView.userListState-12418.test.tsx index 91d25b4809..f35af32d42 100644 --- a/packages/app-shell/src/views/ObjectView.userListState-12418.test.tsx +++ b/packages/app-shell/src/views/ObjectView.userListState-12418.test.tsx @@ -42,6 +42,10 @@ * 6b. objectui#12505 — on a SAVED view that declares no `columns`, a Hide * Fields change is stored the same way and applied over the drawn * defaults; another user sees the defaults; "Reset view" drops it. + * 6c. objectui#12949 — the list is handed the view's own density and + * hidden fields in `viewDefaults`, beside the user's in `schema`, so + * its Display reset compares against the view; on an unprojected saved + * view the view hides nothing. * 7. CONTROL — no provider (a host without the console shell): the state is * this browser's, as the tab order and filters were before; a sort still * writes no view; the older `viewOrder:` key is still honoured. @@ -677,6 +681,71 @@ describe('on a saved view that declares no `columns`, the hidden fields are the }); }); +describe('the list is handed the view\'s own density and hidden fields beside the user\'s (objectui#12949)', () => { + /** A stored choice that differs from the view's on both settings. */ + const STORED = { + [LIST_KEY]: { perView: { [SYSTEM_ID]: { rowHeight: 'compact', hiddenFields: ['name'] } } }, + }; + + it('after a reload, `schema` carries the user\'s choice and `viewDefaults` the view\'s own', async () => { + const ds = makeDataSource(); + const { family, release } = makeFamily(structuredClone(STORED), { held: true }); + mountWithFamily(ds, family); + await screen.findByTestId('object-view-list-preferences-pending'); + await act(async () => release()); + await listReady(); + + expect(listProps.schema.rowHeight).toBe('compact'); + expect(listProps.schema.hiddenFields).toEqual(['name']); + expect(listProps.viewDefaults).toEqual({ hiddenFields: AUTHORED_HIDDEN, rowHeight: AUTHORED_ROW_HEIGHT }); + }); + + it('the list\'s reset writes the view\'s own through the controls\' doors, and it lands as the user\'s choice', async () => { + const ds = makeDataSource(); + const { family } = makeFamily(structuredClone(STORED)); + mountWithFamily(ds, family); + await listReady(); + + act(() => { + listProps.onHiddenFieldsChange(listProps.viewDefaults.hiddenFields); + listProps.schema.onDensityChange('spacious'); + }); + await wait(700); + + expect(viewWrites(ds)).toBe(0); + expect(family.save).toHaveBeenCalledTimes(1); + expect(family.save).toHaveBeenCalledWith(LIST_KEY, { + perView: { [SYSTEM_ID]: { rowHeight: AUTHORED_ROW_HEIGHT, hiddenFields: AUTHORED_HIDDEN } }, + }); + expect(listProps.schema.rowHeight).toBe(AUTHORED_ROW_HEIGHT); + expect(listProps.schema.hiddenFields).toEqual(AUTHORED_HIDDEN); + expect(listProps.viewDefaults).toEqual({ hiddenFields: AUTHORED_HIDDEN, rowHeight: AUTHORED_ROW_HEIGHT }); + }); + + it('CONTROL: another user, with no state of their own, is handed the same value twice', async () => { + session.userId = 'u2'; + const ds = makeDataSource(); + const { family } = makeFamily({}); + mountWithFamily(ds, family); + await listReady(); + + expect(listProps.schema.hiddenFields).toEqual(AUTHORED_HIDDEN); + expect(listProps.schema.rowHeight).toBe(AUTHORED_ROW_HEIGHT); + expect(listProps.viewDefaults).toEqual({ hiddenFields: listProps.schema.hiddenFields, rowHeight: listProps.schema.rowHeight }); + }); + + it('on a saved view that declares no `columns`, the view hides nothing: its authored key stays unapplied (objectui#12505)', async () => { + const UNPROJECTED_SAVED_ROW = { name: SAVED_ID, object: OBJECT_NAME, label: 'Pipeline', type: 'grid', hiddenFields: ['name'] }; + const ds = makeDataSource(UNPROJECTED_SAVED_ROW); + const { family } = makeFamily({ [LIST_KEY]: { perView: { [SAVED_ID]: { hiddenFields: ['amount'] } } } }); + mountWithFamily(ds, family, SAVED_ID); + await listReady(); + + expect(listProps.schema.hiddenFields).toEqual(['amount']); + expect(listProps.viewDefaults.hiddenFields).toBeUndefined(); + }); +}); + describe('CONTROL — no provider: this browser\'s state, as before (objectui#12418)', () => { function mountBare(ds: ConsoleObjectViewProps['dataSource'], viewId = SYSTEM_ID) { return render(page(ds, viewId)); diff --git a/packages/plugin-list/src/ListView.tsx b/packages/plugin-list/src/ListView.tsx index 48931cf121..f33d106eda 100644 --- a/packages/plugin-list/src/ListView.tsx +++ b/packages/plugin-list/src/ListView.tsx @@ -446,6 +446,22 @@ export interface ListViewProps { * posture of the console, not of the view. */ defaultRowNumbers?: boolean; + /** + * The VIEW'S OWN hidden fields and row height, for a host that lays the + * user's own preference over them in `schema` (objectui#12949). The Display + * control's trigger count and its "Reset to view default" compare these two + * settings against this value rather than against `schema`, and the reset + * hands it back through the same doors the controls write through + * (`onHiddenFieldsChange`, `schema.onDensityChange`), so the user's + * preference follows. + * + * When it is passed, its members are the whole baseline: an absent + * `hiddenFields` means the view hides nothing, an absent `rowHeight` means + * the view's default density. Absent ⇒ `schema` is the view's own, exactly + * as before. A HOST prop, never a schema key: it is the host's split of one + * composed value into its two sources, nothing an author writes. + */ + viewDefaults?: Pick; /** * Draw the COMPACT posture (objectui#12703): a header with the list's title * and record count over two-line rows (the view's first visible column, its @@ -1451,6 +1467,7 @@ export const ListView = React.forwardRef(({ toolbarLeading, defaultRowNumbers = true, compact = false, + viewDefaults, ...props }: ListViewProps & { [key: string]: any }, ref) => { // The switcher can be enabled either by the host component (prop) or by @@ -5018,23 +5035,35 @@ export const ListView = React.forwardRef(({ /** * objectui#12423 — what the Display control counts on its trigger and what * its "Reset to view default" hands back: each setting it offers whose value - * differs from the one this list was handed in `schema` (the view's own, as - * the host composed it). A setting the host persists back into the view - * (density and hidden fields in the console) stops counting once the write - * comes back, because the view's own value has moved with it. Plain - * per-render values, never memo identities (AGENTS.md #10). + * differs from the VIEW'S OWN. For most settings that is the value this list + * was handed in `schema`. + * + * objectui#12949 — hidden fields and density are the exception wherever a + * host persists them as the USER'S preference (the console, since + * objectui#12474): it lays that preference over the view's value in + * `schema`, so `schema` there is the user's own choice, and a comparison + * against it reads "nothing to reset" straight after a reload. Such a host + * hands the view's own value in `viewDefaults`, and these two settings + * compare against that; the reset writes it through the controls' doors, so + * the user's preference follows. Without `viewDefaults`, `schema` is the + * view's own. Plain per-render values, never memo identities (AGENTS.md #10). */ const authoredGalleryCardSize: GalleryCardSize = (schema.gallery?.cardSize as GalleryCardSize | undefined) ?? 'medium'; + const viewDefaultHiddenFields = viewDefaults ? viewDefaults.hiddenFields : schema.hiddenFields; + // The same resolution `resolvedDensity` applies to `schema.rowHeight`. + const viewDefaultDensity = viewDefaults + ? rowHeightToDensityMode(viewDefaults.rowHeight) ?? 'compact' + : resolvedDensity; const sortedNames = (names: Iterable | undefined) => JSON.stringify([...(names ?? [])].sort()); const displayDeviation = { layout: viewSwitcherOffered && !!schema.viewType && currentView !== schema.viewType, group: toolbarFlags.showGroup && JSON.stringify(groupingConfig?.fields ?? []) !== JSON.stringify(initialGroupingConfig?.fields ?? []), color: toolbarFlags.showColor && (rowColorConfig?.field ?? '') !== (schema.rowColor?.field ?? ''), - hiddenFields: toolbarFlags.showHideFields && sortedNames(hiddenFields) !== sortedNames(schema.hiddenFields), + hiddenFields: toolbarFlags.showHideFields && sortedNames(hiddenFields) !== sortedNames(viewDefaultHiddenFields), density: toolbarFlags.showDensity && (currentView === 'gallery' ? galleryCardSize !== authoredGalleryCardSize - : density.mode !== resolvedDensity), + : density.mode !== viewDefaultDensity), rowNumbers: currentView === 'grid' && rowNumbers !== defaultRowNumbers, inlineEdit: currentView === 'grid' && inlineEditOffered && inlineEdit !== !!schema.inlineEdit, }; @@ -5044,13 +5073,13 @@ export const ListView = React.forwardRef(({ if (displayDeviation.layout) handleViewChange(schema.viewType as ListViewVisualization); if (displayDeviation.group) changeGrouping(initialGroupingConfig); if (displayDeviation.color) setRowColorConfig(schema.rowColor); - if (displayDeviation.hiddenFields) updateHiddenFields(new Set(schema.hiddenFields || [])); + if (displayDeviation.hiddenFields) updateHiddenFields(new Set(viewDefaultHiddenFields || [])); if (displayDeviation.density) { if (currentView === 'gallery') { setGalleryCardSize(authoredGalleryCardSize); try { window.localStorage.setItem(galleryDensityKey, authoredGalleryCardSize); } catch { /* private mode */ } } else { - density.setMode(resolvedDensity); + density.setMode(viewDefaultDensity); } } if (displayDeviation.rowNumbers) setRowNumbers(defaultRowNumbers); diff --git a/packages/plugin-list/src/__tests__/ListView.displayResetBaseline-12949.test.tsx b/packages/plugin-list/src/__tests__/ListView.displayResetBaseline-12949.test.tsx new file mode 100644 index 0000000000..aee4fc7486 --- /dev/null +++ b/packages/plugin-list/src/__tests__/ListView.displayResetBaseline-12949.test.tsx @@ -0,0 +1,166 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#12949 — the Display control's "Reset to view default" and its + * trigger count compare hidden fields and density against the VIEW'S OWN + * value, not against the user's preference a host lays over it in `schema`. + * + * The console relays the user's stored hidden fields and row height into + * `schema` (objectui#12474). Compared against `schema`, a mount after a reload + * (each fixture here is one) read "nothing to reset" although the list differs + * from the view. A host that persists them that way hands the view's own in + * `viewDefaults`; a host that does not keeps the old comparison against + * `schema` (the CONTROL describe). + */ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, fireEvent, within, waitFor, cleanup } from '@testing-library/react'; +import React from 'react'; +import { ComponentRegistry } from '@object-ui/core'; +import { SchemaRendererProvider } from '@object-ui/react'; +import type { ListViewSchema } from '@object-ui/types'; +import { ListView, type ListViewProps } from '../ListView'; + +const RECORDS = [{ id: '1', name: 'Acme', stage: 'won', amount: 3 }]; + +function makeDataSource() { + return { + find: vi.fn().mockResolvedValue({ data: RECORDS, total: 1 }), + findOne: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + getObjectSchema: vi.fn().mockResolvedValue({ name: 'account', fields: {} }), + }; +} + +const BASE: ListViewSchema = { + type: 'list-view', + objectName: 'account', + viewType: 'grid', + fields: ['name', 'stage', 'amount'], + userActions: { hideFields: true }, +} as ListViewSchema; + +let previousGrid: ReturnType; + +function mount(schema: Partial, props: Partial = {}) { + const dataSource = makeDataSource(); + render( + + + , + ); +} + +async function openDisplay(): Promise { + await screen.findByTestId('grid-stub'); + fireEvent.click(screen.getByTestId('toolbar-display-trigger')); + return screen.findByTestId('view-settings-content'); +} + +beforeEach(() => { + previousGrid = ComponentRegistry.get('object-grid'); + ComponentRegistry.register('object-grid', () =>
); +}); + +afterEach(() => { + cleanup(); + if (previousGrid) ComponentRegistry.register('object-grid', previousGrid as any); +}); + +describe('a host that lays the user\'s preference over the view resets to the view\'s own (objectui#12949)', () => { + it('a hidden column the view shows: the reset is enabled on mount, counts, and writes the view\'s set', async () => { + const onHiddenFieldsChange = vi.fn(); + mount( + { hiddenFields: ['amount', 'stage'] }, + { viewDefaults: { hiddenFields: ['amount'] }, onHiddenFieldsChange }, + ); + const content = await openDisplay(); + expect(screen.getByTestId('toolbar-display-count')).toHaveTextContent('1'); + const reset = within(content).getByTestId('display-reset-button'); + expect(reset).toBeEnabled(); + + fireEvent.click(reset); + expect(onHiddenFieldsChange).toHaveBeenCalledTimes(1); + expect(onHiddenFieldsChange).toHaveBeenCalledWith(['amount']); + await waitFor(() => expect(screen.queryByTestId('toolbar-display-count')).toBeNull()); + expect(within(content).getByTestId('display-reset-button')).toBeDisabled(); + }); + + it('a view that hides nothing: an absent `viewDefaults.hiddenFields` is the empty set', async () => { + const onHiddenFieldsChange = vi.fn(); + mount({ hiddenFields: ['stage'] }, { viewDefaults: {}, onHiddenFieldsChange }); + const content = await openDisplay(); + expect(screen.getByTestId('toolbar-display-count')).toHaveTextContent('1'); + + fireEvent.click(within(content).getByTestId('display-reset-button')); + expect(onHiddenFieldsChange).toHaveBeenCalledWith([]); + }); + + it('a density other than the view\'s: the reset is enabled on mount, counts, and writes the view\'s density', async () => { + const onDensityChange = vi.fn(); + mount( + { rowHeight: 'compact', onDensityChange } as Partial, + { viewDefaults: { rowHeight: 'tall' } }, + ); + const content = await openDisplay(); + expect(within(content).getByTestId('view-settings-density-cycle')).toHaveTextContent('Compact'); + expect(screen.getByTestId('toolbar-display-count')).toHaveTextContent('1'); + const reset = within(content).getByTestId('display-reset-button'); + expect(reset).toBeEnabled(); + + fireEvent.click(reset); + await waitFor(() => expect(onDensityChange).toHaveBeenCalledWith('spacious')); + expect(onDensityChange).toHaveBeenCalledTimes(1); + await waitFor(() => expect(screen.queryByTestId('toolbar-display-count')).toBeNull()); + expect(within(content).getByTestId('view-settings-density-cycle')).toHaveTextContent('Spacious'); + }); + + it('both differ: the count reads 2', async () => { + mount( + { hiddenFields: ['stage'], rowHeight: 'compact' } as Partial, + { viewDefaults: { rowHeight: 'medium' }, onHiddenFieldsChange: vi.fn() }, + ); + await openDisplay(); + expect(screen.getByTestId('toolbar-display-count')).toHaveTextContent('2'); + }); + + it('CONTROL: a preference equal to the view\'s own leaves the reset disabled and nothing counted', async () => { + mount( + { hiddenFields: ['stage', 'amount'], rowHeight: 'tall' } as Partial, + { viewDefaults: { hiddenFields: ['amount', 'stage'], rowHeight: 'tall' }, onHiddenFieldsChange: vi.fn() }, + ); + const content = await openDisplay(); + expect(screen.queryByTestId('toolbar-display-count')).toBeNull(); + expect(within(content).getByTestId('display-reset-button')).toBeDisabled(); + }); +}); + +describe('CONTROL: a host passing no `viewDefaults` compares against `schema`, as before (objectui#12949)', () => { + it('hidden fields and density handed in `schema` are the view\'s own: nothing to reset', async () => { + mount({ hiddenFields: ['stage'], rowHeight: 'tall' } as Partial, { onHiddenFieldsChange: vi.fn() }); + const content = await openDisplay(); + expect(screen.queryByTestId('toolbar-display-count')).toBeNull(); + expect(within(content).getByTestId('display-reset-button')).toBeDisabled(); + }); + + it('a density change made in the list counts, and the reset hands back the density `schema` carried', async () => { + const onDensityChange = vi.fn(); + mount({ rowHeight: 'tall', onDensityChange } as Partial); + const content = await openDisplay(); + fireEvent.click(within(content).getByTestId('view-settings-density-cycle')); + await waitFor(() => expect(screen.getByTestId('toolbar-display-count')).toHaveTextContent('1')); + await waitFor(() => expect(onDensityChange).toHaveBeenCalledTimes(1)); + expect(onDensityChange).not.toHaveBeenLastCalledWith('spacious'); + + fireEvent.click(within(content).getByTestId('display-reset-button')); + await waitFor(() => expect(onDensityChange).toHaveBeenLastCalledWith('spacious')); + await waitFor(() => expect(screen.queryByTestId('toolbar-display-count')).toBeNull()); + }); +}); From 388e1f9059ff137037ecae414be6c8e7873905f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 09:49:58 +0000 Subject: [PATCH 2/3] fix(app-shell): keep the density fold spelled out on the rowHeight rung The relay census reads the `rowHeight` rung for the `normalizeListViewSchema` fold that carries the retired `densityMode`; the rung reads it inline again, and `viewRowHeight` stays the same value for `viewDefaults`. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB --- packages/app-shell/src/views/ObjectView.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/app-shell/src/views/ObjectView.tsx b/packages/app-shell/src/views/ObjectView.tsx index 94d9ab0a22..ce93229466 100644 --- a/packages/app-shell/src/views/ObjectView.tsx +++ b/packages/app-shell/src/views/ObjectView.tsx @@ -3238,7 +3238,13 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co // here as `rowHeight`, and this relay never names the retired key. // objectui#12474 — the density this user chose is laid over it: // the view's own is the default for a user who has not chosen. - rowHeight: viewPref?.rowHeight ?? viewRowHeight, + // The fold stays spelled out on this rung, where the relay census + // (`ObjectView.relayRungCensus-7559.test.ts`) reads it for the + // retired `densityMode`; `viewRowHeight` above is the same value. + rowHeight: + viewPref?.rowHeight + ?? (normalizeListViewSchema(viewDef ?? {}) as Pick).rowHeight + ?? listSchema.rowHeight, // Hydrate the persisted settings so they survive reload. The sort // and the column layout (objectui#12418), the density and the // hidden fields (objectui#12474) are the USER's: they are read From 60cb0129770f881d920c2361116d29f5732248a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 10:07:54 +0000 Subject: [PATCH 3/3] docs(plugin-list): document the viewDefaults host prop The Display control's count and "Reset to view default" compare hidden fields and density with `viewDefaults` when a host passes it (objectui#12949). Document it beside the other host props, per AGENTS.md commandment 2. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB --- packages/plugin-list/README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/plugin-list/README.md b/packages/plugin-list/README.md index 32ab37965a..b6e8a4bf78 100644 --- a/packages/plugin-list/README.md +++ b/packages/plugin-list/README.md @@ -224,6 +224,34 @@ function MyViewTabs() { /> ``` +### Host prop: `viewDefaults` + +A host that stores the user's own hidden fields or density and lays that +preference over `hiddenFields` / `rowHeight` in `schema` passes the view's own +values in `viewDefaults`. The Display control's trigger count and its "Reset to +view default" then compare these two settings against the view, not against +`schema`, and the reset writes the view's values back through the doors the +controls write through (`onHiddenFieldsChange`, `schema.onDensityChange`), so +the stored preference follows. This is a React prop for the host app, not a +schema key: list metadata cannot set it. + +When it is passed, its members are the whole baseline: an absent `hiddenFields` +means the view hides nothing, and an absent `rowHeight` means the default +density. Without it nothing changes: `schema` is taken as the view's own. The +console passes it on the object list page, where it persists both settings per +user. + +```tsx +import { ListView } from '@object-ui/plugin-list'; + +// The view hides `amount`; the user has since hidden `stage` as well. + console.log('Hidden:', hidden)} +/> +``` + ## Schema The ListView component accepts a `ListViewSchema`, exported from