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
22 changes: 22 additions & 0 deletions .changeset/12504-retire-view-write-family.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@object-ui/types': minor
'@object-ui/data-objectstack': minor
'@object-ui/app-shell': patch
'@object-ui/i18n': patch
'@object-ui/plugin-grid': patch
'@object-ui/plugin-list': patch
'@object-ui/react': patch
'@object-ui/components': patch
'@object-ui/plugin-detail': patch
'@object-ui/console': patch
---

The console's list toolbar no longer has a write into the shared view, and the data-source method that write used is retired (objectui#12504).

**BREAKING:** `DataSource.updateViewConfig` is removed from `@object-ui/types`, and `ObjectStackAdapter.updateViewConfig` is removed from `@object-ui/data-objectstack`. The capability gate that only that method read goes with it: `ObjectStackAdapter.setSystemCapabilities`, the `systemCapabilities` option of the adapter constructor and of `createObjectStackAdapter`, `VIEW_CONFIG_CAPABILITY`, `ViewConfigPermissionDeniedError` and `isViewConfigPermissionDeniedError`. A class that implements `DataSource` and keeps an `updateViewConfig` method still compiles, but nothing calls the method any more; an object literal annotated as `DataSource` that carries it is refused as an unknown property. Code that calls `updateViewConfig` through a `DataSource` or an `ObjectStackAdapter`, or imports one of the gate names, no longer compiles. To change a view for everyone, save the view: `createView` / `updateView` on the data source, or the console's view editor, which writes through the runtime metadata seam.

Since objectui#12418 and objectui#12474, the toolbar's sort, column layout, density and Hide Fields are each user's own list state, so the toolbar write had no caller. The view editor's explicit save is now the one write path to the shared view, and it is unchanged.

Personalization rows that earlier consoles wrote into `/meta/view` stay readable: the overlay marker, `narrowPersonalizationOverlay` and `listViews()` exclusion still apply to them. Nothing new writes such a row.

`@object-ui/i18n` drops `console.objectView.viewConfigPermissionDenied` from all ten packs. It was the toast the retired write raised when the gate refused it. The other packages change only in comments and tests that named the retired path.
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ const DATA_SOURCE_METHODS = [
'getView',
'listViews',
'listViewOverrides',
'updateViewConfig',
'onMutation',
] as const;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ const PROBE_OBJECT_SCHEMA = {
const DATA_SOURCE_METHODS = [
'find', 'findOne', 'create', 'update', 'delete', 'aggregate', 'count',
'getObjectSchema', 'getObjects', 'getView', 'listViews', 'listViewOverrides',
'updateViewConfig', 'onMutation',
'onMutation',
] as const;

/**
Expand Down
18 changes: 5 additions & 13 deletions content/docs/utilities/data-objectstack.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,6 @@ function createObjectStackAdapter<T = unknown>(config: {
autoReconnect?: boolean; // default true
maxReconnectAttempts?: number; // default 3
reconnectDelay?: number; // default 1000 ms

/**
* [ADR-0066] The session's system capabilities, when the host already has them
* at construction time. Most hosts do NOT — `/me/permissions` resolves after
* the adapter exists — and push them in later via `setSystemCapabilities`.
* Omit for "unreported".
*/
systemCapabilities?: string[];
}): ObjectStackAdapter<T>;
```

Expand Down Expand Up @@ -250,11 +242,11 @@ either form):
- `onBatchProgress(listener)` - Subscribe to bulk progress (returns unsubscribe)

Those six bullets cover nine members, and they are the documented subset.
`ObjectStackAdapter` declares **20** members beyond `DataSource` in all
(`Exclude<keyof ObjectStackAdapter<unknown>, keyof DataSource<unknown>>`); the
other eleven are lower-level seams — client discovery, cache-key invalidation,
dataset and dashboard access, advisory subscriptions and capability configuration
among them — which this page does not document and which carry no compatibility
`ObjectStackAdapter` declares more members beyond `DataSource` than these —
`Exclude<keyof ObjectStackAdapter<unknown>, keyof DataSource<unknown>>` lists
them all; the rest are lower-level seams — client discovery, cache-key
invalidation, dataset and dashboard access and advisory subscriptions among
them — which this page does not document and which carry no compatibility
promise here.

### Per-element data binding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@
* NO metadata write on the wire, raise no error, and land as ONE
* preference write each, of the same row.
* 3. CONTROL — the wall is live: a metadata write made in this same tree
* (the real adapter's own view write, the call the density toggle used
* to reach) is answered 403. Since objectui#12474 no list toolbar control
* makes one, so the control is the adapter's call itself.
* (the real adapter's own view write, `createView`) is answered 403.
* Since objectui#12474 no list toolbar control makes one, so the control
* is the adapter's call itself. (It was `updateViewConfig`, the call the
* density toggle used to reach, until objectui#12504 retired it.)
*/

import '@testing-library/jest-dom/vitest';
Expand Down Expand Up @@ -263,12 +264,12 @@ describe('a user\'s list state under a walled posture (objectui#12418)', () => {
boot();
await waitFor(() => expect(typeof listProps?.schema?.onDensityChange).toBe('function'), { timeout: 5000 });
await wait(100);
expect(typeof pageAdapter?.updateViewConfig).toBe('function');
expect(typeof pageAdapter?.createView).toBe('function');

let refused: unknown = null;
await act(async () => {
try {
await pageAdapter.updateViewConfig(OBJECT_NAME, ALL_ID, { rowHeight: 'compact' }, { isSavedView: false });
await pageAdapter.createView(OBJECT_NAME, { name: `${OBJECT_NAME}.probe`, label: 'Probe', type: 'grid' });
} catch (err) {
refused = err;
}
Expand Down
7 changes: 4 additions & 3 deletions packages/app-shell/src/utils/viewIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,10 @@ export function defaultListViewId(objectName: string, list: unknown): string | u
* stored override therefore REDEFINED the tab's identity.
*
* The third spelling is the defect. A stored view body can legitimately carry
* an `id` key — `persistViewPatch` writes the whole tab object (`id` included)
* back through `updateViewConfig`, and a duplicated view copies its source
* artifact's `id` verbatim — so the spread handed the tab an identity the
* an `id` key — the console's toolbar write (`persistViewPatch` →
* `updateViewConfig`, retired in objectui#12504) wrote the whole tab object
* (`id` included), and those rows are still at rest; a duplicated view copies
* its source artifact's `id` verbatim — so the spread handed the tab an identity the
* overlay read had never heard of. `isSavedView` then answered false for a view
* that *is* saved, which both hid the set-default / rename / delete menu
* entries (`readonly: !saved`) and short-circuited the handlers behind them.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,27 @@
* > DOCUMENT … and `InterfaceListPage` hydrates a hollow view out of that
* > document.
*
* `InterfaceListPage`'s hollow-view hydration effect (lines ~270-315) fetches
* `InterfaceListPage`'s hollow-view hydration effect fetches
* a view's per-view overlay row through `listViewOverrides`/`getView` — the
* SAME undocked-narrowing read path the docblock is talking about — whenever
* the ADR-0017 expansion serves a source view with no `columns`. Before
* objectui#5233, `persistViewPatch` wrote a system view's overlay as
* `{ ...baseViewDef, ...patch }`, so that row carried a frozen COPY of the
* source view's columns at write time; the merge `{ ...resolvedView,
* ...hydratedView }` picked those up and the page rendered STALE columns.
* After #5233 the same write site stores the patch alone — no `columns` key
* After #5233 the same write site stored the patch alone — no `columns` key
* — so the merge contributes nothing towards `hasColumns`, and the page's own
* defaulting (`defaultColumnsFromObject`) fires instead.
*
* This is the card's step 1: CONSTRUCT the case (a hollow ADR-0017 expansion
* item for a system view that also carries a personalization overlay row),
* driven through the REAL `ObjectStackAdapter` write (`updateViewConfig` /
* `buildPersistedViewBody`, the same seam `ObjectView.overlayPatchOnly.test.ts`
* uses) and the REAL `InterfaceListPage` render — not reasoned about, and not
* a hand-written override fixture, because the coupling lives in what the
* hydration effect does with a row it actually fetched.
* That write site is retired (objectui#12504: `persistViewPatch` and the
* adapter's `updateViewConfig`), so both row shapes are now rows an install
* already carries, and the hydration effect still reads them. This is the
* card's step 1: CONSTRUCT the case (a hollow ADR-0017 expansion item for a
* system view that also carries a personalization overlay row), with the row
* put in the store in the shape the retired write stored it, read through the
* REAL `ObjectStackAdapter` and the REAL `InterfaceListPage` render — not
* reasoned about, because the coupling lives in what the hydration effect does
* with a row it actually fetched.
*
* Two cases, so the second is a control that PROVES the first discriminates
* something real: with the CURRENT (post-#5233) thin overlay shape the page
Expand Down Expand Up @@ -87,7 +89,6 @@ vi.mock('@object-ui/react', async (importOriginal) => {
});

import { ObjectStackAdapter } from '@object-ui/data-objectstack';
import { buildPersistedViewBody } from './ObjectView';
import { InterfaceListPage } from './InterfaceListPage';

const OBJECT_NAME = 'crm_lead';
Expand Down Expand Up @@ -119,6 +120,15 @@ function makeMetaStore() {
return { meta, rows };
}

/**
* Put a personalization overlay in the store the way the retired toolbar write
* (`updateViewConfig`, objectui#12504) stored one: the body, with `object`,
* `name` and the `_isOverride` marker stamped on it.
*/
function plantOverlay(rows: Map<string, any>, body: Record<string, unknown>) {
rows.set(`view::${VIEW_ID}`, { ...body, object: OBJECT_NAME, name: VIEW_ID, _isOverride: true });
}

function makeAdapter(meta: any) {
const ds: any = new ObjectStackAdapter({
baseUrl: 'http://test.local',
Expand Down Expand Up @@ -162,20 +172,15 @@ const page = {

describe('objectui#5773 — InterfaceListPage hollow view + personalization overlay hydration', () => {
it('REACHABLE: a hollow system view + a thin post-#5233 overlay row hydrates to renderer DEFAULTS, not a hollow grid', async () => {
const { meta } = makeMetaStore();
const { meta, rows } = makeMetaStore();
const ds = makeAdapter(meta);
testDataSource = ds;
testObjects = [makeObjectDef()];

// The CURRENT `persistViewPatch` write for a toolbar toggle on this
// system view (a density change) — patch alone, no `columns`, exactly
// objectui#5233's write half. Written through the REAL adapter, not
// dropped into the store as a fixture.
await ds.updateViewConfig(
OBJECT_NAME,
VIEW_ID,
buildPersistedViewBody({ viewKind: 'list' }, { rowHeight: 'compact' }, { isSavedView: false }),
);
// The LAST shape the retired toolbar write stored for a toggle on this
// system view (a density change): the patch alone plus the tab's
// `viewKind`, no `columns` — objectui#5233's write half.
plantOverlay(rows, { rowHeight: 'compact', viewKind: 'list' });

// Confirms the row really is thin at rest — the premise this case
// depends on, measured rather than assumed.
Expand All @@ -198,18 +203,17 @@ describe('objectui#5773 — InterfaceListPage hollow view + personalization over
});

it('CONTROL: the same hollow view with a PRE-#5233 fat overlay row still renders the frozen stale columns (proves the pin discriminates old vs. new row shapes)', async () => {
const { meta } = makeMetaStore();
const { meta, rows } = makeMetaStore();
const ds = makeAdapter(meta);
testDataSource = ds;
testObjects = [makeObjectDef()];

// What `persistViewPatch` USED TO send — the whole active tab (frozen at
// write time) plus the one changed key. An install that has not touched
// this view since before objectui#5233 is still carrying a row shaped
// exactly like this one (`ObjectView.overlayPatchOnly.test.ts`'s own
// "PRE-FIX" framing) — this is that row, written through the REAL write
// path (`updateViewConfig`), not hand-assembled as a display fixture.
await ds.updateViewConfig(OBJECT_NAME, VIEW_ID, {
// What `persistViewPatch` USED TO send before objectui#5233 — the whole
// active tab (frozen at write time) plus the one changed key. An install
// that has not touched this view since then is still carrying a row
// shaped exactly like this one (`ObjectView.overlayPatchOnly.test.ts`'s
// own "PRE-FIX" framing).
plantOverlay(rows, {
viewKind: 'list',
type: 'grid',
columns: ['status'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,12 +290,15 @@ describe('default list view identity → _views translation key (objectui#3770)'
/**
* Stored-data surface, measured before the behaviour change and pinned here.
*
* `ObjectView.persistViewPatch` writes a view override by VIEW ID:
* `dataSource.updateViewConfig(objectName, view.id, cfg)` →
* `client.meta.saveItem('view', view.id, { …cfg, object, name: view.id })`, i.e.
* one `sys_metadata` row of type `view` NAMED by the id. The id therefore doubles
* as the persistence key, and `ViewItemNameSchema` is the spec's judgment on
* which strings are legal view identities:
* A view's stored row is keyed by VIEW ID: the explicit save writes
* `persistRuntimeMetadata('view', vid, …)`, and the override rows the page
* reads back (`loadViewOverrides`) are found by the same id — one
* `sys_metadata` row of type `view` NAMED by the id. (The toolbar's
* `persistViewPatch` → `updateViewConfig` wrote its overlays the same way,
* `saveItem('view', view.id, { …cfg, object, name: view.id })`, until
* objectui#12504 retired it; those rows are still at rest.) The id therefore
* doubles as the persistence key, and `ViewItemNameSchema` is the spec's
* judgment on which strings are legal view identities:
*
* /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/
*
Expand Down
Loading
Loading