Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.
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
10 changes: 10 additions & 0 deletions .changeset/12949-display-reset-baseline.md
Original file line number Diff line number Diff line change
@@ -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<ListViewSchema, 'hiddenFields' | 'rowHeight'>`. 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.
25 changes: 24 additions & 1 deletion packages/app-shell/src/views/ObjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ListViewSchema, 'rowHeight'>).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
Expand Down Expand Up @@ -3221,6 +3238,9 @@ 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.
// 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<ListViewSchema, 'rowHeight'>).rowHeight
Expand Down Expand Up @@ -3280,7 +3300,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
Expand Down Expand Up @@ -3617,6 +3637,9 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co
<ListView
key={identityKey}
schema={fullSchema}
// objectui#12949 — the view's own values under the user's
// preference, for the Display control's reset and count.
viewDefaults={{ hiddenFields: viewHiddenFields, rowHeight: viewRowHeight }}
className={className}
onEdit={editHandler}
onDelete={(record: any) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:<object>` key is still honoured.
Expand Down Expand Up @@ -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));
Expand Down
28 changes: 28 additions & 0 deletions packages/plugin-list/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
<ListView
schema={{ type: 'list-view', objectName: 'deals', hiddenFields: ['amount', 'stage'] }}
viewDefaults={{ hiddenFields: ['amount'] }}
onHiddenFieldsChange={(hidden) => console.log('Hidden:', hidden)}
/>
```

## Schema

The ListView component accepts a `ListViewSchema`, exported from
Expand Down
47 changes: 38 additions & 9 deletions packages/plugin-list/src/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ListViewSchema, 'hiddenFields' | 'rowHeight'>;
/**
* 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
Expand Down Expand Up @@ -1451,6 +1467,7 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
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
Expand Down Expand Up @@ -5018,23 +5035,35 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
/**
* 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<string> | 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,
};
Expand All @@ -5044,13 +5073,13 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
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);
Expand Down
Loading
Loading