Skip to content

Commit 492e79e

Browse files
fix(plugin-view): the registered object-view reads a named view's grid members (objectui#10885) (#10937)
Part of #10885 Clause-②: no Member 1 of objectui#10885: route 2, the registered `object-view` renderer's own grid path, reads a named grid view's grid members off the named view. Member 2 is not taken and member 3 is not in this PR (see Acceptance notes), so #10885 remains open. Claim `5865109811`, seat `domain:ui#2`, PM session `session_014mXUNuFomfj24w7s1pZzhN` (`https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN`), mode `subagent`. ## What changed - **`packages/plugin-view/src/ObjectView.tsx`**, the `gridSchema` memo: the `object-grid` node `ObjectView` hands `ObjectGrid` when no host passes `renderListView`. - Ten members are read off the active named view first. `pagination` and `selection` then fall back to the node's `table`, as they did before. `rowHeight`, `resizable`, `searchableFields`, `conditionalFormatting`, `rowActions`, `bulkActions`, `bulkActionDefs` and `exportOptions` are named-view sourced, like `grouping` / `rowColor` beside them. There is no `activeView` rung and no new node read. - A named view's `hiddenFields` is subtracted from the resolved column projection, in both the `fields` and the `columns` slot (new helper `withoutHiddenFields`). It is not relayed as a key: `object-grid` does not declare it, and `ObjectGrid` has no read of it. - A named view's `exportOptions` written in the protocol's legacy bare-array spelling is lifted to `{ formats }` (new helper `gridExportOptions`). That is the lift the protocol's own parse makes, and the one `ListView` makes for the delegation. `ObjectGrid` reads `exportOptions.formats`. - `conditionalFormatting` and `bulkActionDefs` carry a type assertion to the grid's declared element type. The protocol's expression wire is wider than that type (measured by `tsc`), and `ObjectGrid` hands both to the shared evaluator, which reads the wire. - **The objectui#5097 HOST-COMPOSITION fence (the delegation) is byte-identical.** The text between the two `#region` markers hashes to `40bc2292` on the base and on the head. - **Docs** (the docs-driven rule of AGENTS.md section 5): one prose paragraph each in `packages/plugin-view/README.md` and `content/docs/plugins/plugin-view.mdx`, beside the objectui#10758 paragraph. Neither gains a code fence. - **Changeset** `.changeset/10885-object-view-route2-named-grid.md`: `'@object-ui/plugin-view': patch`, with the claim's `Clause-②: no` line. `@object-ui/app-shell` gains only a test file, so it is not listed; `check-changeset-presence` passes. - **No shared helper.** The order allowed one helper for both routes only if the delegation's behaviour and pins stay untouched. `objectViewHostSurface.test.tsx` section 1b derives the fence's named-view read set by name from the fence text, and walks the rungs of the `renderListView` literal. Moving the reads out of the fence changes both derivations, so those pins would have to be edited. The two sinks also differ: `ListView` subtracts `hiddenFields` and lifts `exportOptions` itself, and `ObjectGrid` does neither. So the two routes share the precedence, not the code. ## H1: reproduced on `main` (`733fd5ac65`) Read from the DOM through the real stack: the real `ViewPreview`, the real `SchemaRenderer` and registry, the registered `object-view` renderer and the real `ObjectGrid`. The pin file `ViewPreview.routeTwoGridMembers-10885.test.tsx` was run on an untouched base tree. - A stored grid view `{ type: 'grid', columns: ['name', 'amount', 'stage'], hiddenFields: ['amount'] }` still draws the hidden column. The header span reading `Budget Amount` is rendered (`expected SPAN to be null`). - The same view with `rowHeight: 'compact'` draws no row-height control: `Unable to find an element with the title: Row height: compact.` - **Lit control, green on the base:** `columns: ['name', 'stage']` off the same named view IS honoured on route 2. `Pipeline Stage` is drawn and `Record Owner`, which is not projected, is not. ## Members, per route The measured set is bucket ① of the objectui#7924 ruling, all 23 members. Route 2 takes a member when three things hold: the installed `@objectstack/spec` 17.4.0 declares it under the same name on a named view (`ObjectListViewSchema`) and on `object-grid` (`ComponentPropsMap['object-grid']`), and `ObjectGrid` reads it. The pin re-derives both declarations. | member | delegation (route 3, unchanged) | route 2 before | route 2 after | |---|---|---|---| | `pagination` | named, host, node | `table.pagination` | named, then `table.pagination` | | `selection` | named, host, node | `table.selection` | named, then `table.selection` | | `rowHeight` | named, host (through the fold) | none | named | | `resizable` | named, host, node | none | named | | `searchableFields` | named, host, node | none | named | | `conditionalFormatting` | named, host, node | none | named | | `rowActions` | named, host, node | none | named | | `bulkActions` | named, host, node | none | named | | `bulkActionDefs` | named, host | none | named | | `exportOptions` | named, host | none | named (a bare array lifted to `{ formats }`) | | `hiddenFields` | named, host, node | none | named, subtracted from the projection (not relayed) | | `navigation` | named, host, node | none | none: excluded (Acceptance notes) | | `inlineEdit` | named, host, node | none | none: excluded (Acceptance notes) | | `description` | named, host | none | none: `object-grid` does not declare it | | `emptyState` | named, host, node | none | none: `object-grid` does not declare it | | `addRecord`, `allowPrinting`, `aria`, `compactToolbar`, `filterableFields`, `sharing`, `showRecordCount`, `userFilters` | named, host, node | none | none: list chrome with no `object-grid` slot | Route 2's named-view reads on the grid node go from 5 (`columns`, `filter`, `sort`, `grouping`, `rowColor`) to 16. The ten are not the whole set that meets the test. Outside bucket ①, `label` and `data` are also declared on both and read by `ObjectGrid`, and `navigation` meets it too; none of the three is read off the named view on route 2 (Acceptance notes). ## Pins and ablation Red on the base `733fd5ac65` (the two new files copied onto an untouched base worktree), green on the head `56ad7e8774`. - **`packages/plugin-view/src/__tests__/ObjectView.routeTwoNamedGridMembers-10885.test.tsx`**, 11 tests. It mounts through `SchemaRenderer` and the registered renderer, with `ObjectGrid` mocked as the sink. - Member set, 3 tests, green on both trees: each relayed member is declared on both protocol schemas; `hiddenFields` is declared on the named view and not on `object-grid`; the full fixture parses under the strict `ObjectListViewSchema`. The control is `allowExport`, which is on neither. - THE FIX, red on the base: `pagination did not arrive from the named view: expected { pageSize: 100 } to deeply equal { pageSize: 5, …(1) }`. The bare-array `exportOptions` lift is also red on the base. - The node control is green on both: `table.pagination` and `table.selection` apply when the named view omits them, and the other eight stay undefined although the node's `table` carries them. - `hiddenFields`, 5 tests. Four are red on the base: both slots lose the hidden field and no `hiddenFields` key reaches the grid; an object-shaped column keeps its shape; the node's `table.columns` is narrowed too; hiding every projected field keeps `[]`. One is green on both: with no projection anywhere, nothing is invented. - **`packages/app-shell/src/views/metadata-admin/previews/ViewPreview.routeTwoGridMembers-10885.test.tsx`**, 4 tests, the whole real stack with DOM readings. `hiddenFields` and `rowHeight` are red on the base. The lit `columns` control and the neither-declared control are green on both. - **Base: 8 failed, 7 passed (15). Head: 15 passed.** - The delegation's objectui#10884 pins (`ObjectView.namedViewProtocolKeys-8980.test.tsx`, `objectViewHostSurface.test.tsx`) are untouched and green. **Ablation**, run with `ablation-replace.mjs` on the committed head `695a6ce635` (the later `56ad7e8774` changes one test-header comment only). The directions were predicted before the run. - Leg A put the base's two `table` rungs back in place of the ten member rungs: anchor 1 → 0, blob `dcce880456e3` → `57515b34b699`. - Leg B neutralised the `hiddenFields` read: anchor 1 → 0, blob → `a01a7bdbbcf4`. - On disk, the named `rowHeight` reads went 2 → 1 (the fence's own stays) and the `gridExportOptions` call 1 → 0. - The run covered the whole `packages/plugin-view/` suite, both ViewPreview pin files and three types census files. **Result: 8 failed, 789 passed, 59 files.** The 8 are exactly the new rows. - The restore was proven: the blob equals HEAD (`dcce880456e3`) and `git diff HEAD` is empty. ## Consumer sweep `git grep` over test files for `'object-view'`, `ObjectView` mounts and imports, `ViewPreview`, and the tests that read plugin-view's `ObjectView.tsx`, plus the objectui#7924 / objectui#7779 census tests. Everything ran on the head under the verify lock. - **Batch A: 75 files, 1882 tests passed**, `VERDICT command-exit 0`, at `695a6ce635`. It covers the whole `packages/plugin-view/` suite and the ten `packages/types` tests that read named views or the object-view docs. The census `object-view-unmirrored-keys-7779`, `schema-reference-named-list-view-keys-7923` and `object-view-listviews-by-reference-7928` are among those ten. The rest are from plugin-map, plugin-list, plugin-grid, core, react, examples and `scripts`. - **Batch B: 38 files, 350 tests passed**, `VERDICT command-exit 0`, at `695a6ce635`. It covers app-shell's `ViewPreview`, `StudioDesignSurface`, `ResourceEditPage` and `AppContent` tests, the `ObjectView.*` tests (the relay census `ObjectView.relayRungCensus-7559` among them), and five `apps/console` tests. - **The 18 tests that read the changed markdown** (from `scripts/markdown-test-inputs.mjs --list`): 829 tests passed. - **Census: no number moved.** The census counts a member as read when any route reads it off a named view. Every member route 2 now reads was already read by the delegation, so the partition stays 43 read / 21 unread of 64, with bucket ① empty. No census test was edited. ## Gates Each exit was captured redirect-first. - **Exit 0 at the head `56ad7e8774`:** - `check-changeset-presence` ("3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)"), `check-changeset-no-major`, `check-changeset-fixed`, `check-changeset-overwrite`, and `check-changeset-claims` (6 pending paragraphs name a touched file; each was read, and none is made false); - `check:pending-changeset-literals`, `check:new-line-citations` ("0 new citation(s)"), `check:control-bytes`, `check:test-path-roots`, and the three `check:vi-mock-*` gates; - `check:spec-symbols`, `check:phantom-deps` and `check:handler-key-reads`; - `check:doc-types`, `check:doc-fences`, `check:doc-example-ids`, `check:doc-example-readers`, `check-doc-expression-carriage` and `check-doc-links`; - `check-governed-queue-guard --test` on the 6 paths: "An ordinary pull request". - **`type-check` exit 0 at `695a6ce635`** for `@object-ui/plugin-view` and for `@object-ui/app-shell`. Each runs `tsc --noEmit && tsc -p tsconfig.test.json`, after its dependency closure was built. `--listFilesOnly` finds each new test file in its package's test program. The app-shell run used a 6144 MB heap, because its test program is the largest in the repo. - **ESLint `--no-inline-config`** on the 3 edited TS/TSX files, which is the whole population: `eslint.config.js` covers them with `files: ['**/*.{ts,tsx}']`, and `--format json` reports 3 files. - 0 errors. `ObjectView.tsx` has 60 warnings, per rule identical to the base (53 / 4 / 3). The new pins have 6 and 1 `no-explicit-any` warnings. - Not type-aware: `projectService` / `parserOptions` have 0 hits against a `languageOptions` control of 1, so no untouched file's verdict can move. - **NOT MEASURED:** - `check:readme-exports`, reason: prerequisite not met. 86 self-imports could not be judged because the workspace dists are not all built. This diff adds no fenced block to any README. - `check:doc-snippets` and `check:doc-examples`: not run, because no fenced block was added. - The full app-shell and types suites, and the repo-wide lint: CI runs them. ## Serial - PR objectui#10914 (head `2b3d2061a2`) edits `ObjectView.tsx` in one comment of the view-switcher icon map. This PR's hunks are the column helpers and `gridSchema`. `git merge-tree --write-tree` against its head is clean (tree `4a03a230b6`). Against a fresh `origin/main` (`733fd5ac65`) it is clean too (tree `749f02ef74`). - `ViewPreview.tsx` is not edited (PR objectui#10906). The pin is a new file. - `packages/plugin-grid/src/ObjectGrid.tsx` is not edited (PR objectui#10278). ## Fix round 1 The contract review of `56ad7e8774` failed on wording only. The code, the precedence, the `hiddenFields` composition, the `exportOptions` lift, the fence, the census, the pins, the ablation and the semver level all held. One commit, `097ba79141`, text only: - **Changeset, third paragraph**, replaced with the review's text: it names the ten as ten members read off the named view first, and adds that `label`, `data` and `navigation`, also declared on both a named view and `object-grid`, are still not read off the named view on this path. - **README and docs paragraphs**, the same edit in each: "Ten grid members …" instead of "The grid members …", and the sentence that `label`, `data` and `navigation`, also declared on both, are not read off the named view on this path. - **The `withoutHiddenFields` docblock** now states its one divergence from `ListView`: an entry with no field identity is kept, where `ListView` drops such an entry whenever `hiddenFields` is non-empty. Comment only: the diff of `ObjectView.tsx` changes no code line. - **Re-run at `097ba79141`**, under the verify lock: - the two pin files, `object-view-unmirrored-keys-7779` and the 18 tests that read the changed markdown: 21 files, 1077 tests passed, `VERDICT command-exit 0`; - sweep batch A (the whole `packages/plugin-view/` suite and every test that reads plugin-view's `ObjectView.tsx`): 75 files, 1882 tests passed, `VERDICT command-exit 0`. - **Gates at `097ba79141`, exit 0 each:** `check-changeset-claims`, `check:pending-changeset-literals`, `check:control-bytes`, `check:new-line-citations` ("0 new citation(s)"), `check-doc-links`, `check:doc-fences`, `check-changeset-presence`, `check-changeset-overwrite` and `check:handler-key-reads`. - **Fence:** the text between the two `#region` markers still hashes to `40bc2292`. - **Serial:** `git merge-tree --write-tree` is clean against a fresh `origin/main` (`733fd5ac65`, tree `46037fd4fc`) and against PR objectui#10914's head (`2b3d2061a2`, tree `605c556b1d`). ## Acceptance notes - **Premise correction for member 1.** The card says `ObjectGrid` reads each of the eight members. Measured: it reads seven. It has no `hiddenFields` read, and `object-grid` does not declare the key. - `hiddenFields` is therefore honoured the way the protocol composes it (objectstack#15184 ruling B: `columns` projects, `hiddenFields` subtracts). It is subtracted from a declared projection, as `ListView` does for the delegation. - With no `columns` anywhere, the grid derives its own columns and the hidden field still shows. `ListView` has the same limit. - **The full set beyond the eight.** `searchableFields`, `bulkActionDefs` and `exportOptions` are also taken: each is declared on both schemas and read by `ObjectGrid`. - **`navigation` is excluded.** It is declared on both schemas, but `ObjectView` passes `ObjectGrid` its own `onRowClick`, and `useNavigationOverlay` obeys an `onRowClick` first. Relayed, the member would be read by nothing. - Route 2's row click is owned by `ObjectView`'s `navigationConfig`, which reads the node's `navigation`. - Making it read the named view would also move the delegation's click path, because the delegation hands `ListView` the same `handleRowClick`. That is outside this rung. - **`inlineEdit` is excluded.** `ObjectGrid` reads `editable`, not `inlineEdit`. `ListView` translates one into the other under its objectui#4647 grant gate. That is a key translation with a permission contract, a new host path, not the named-view-first rung. - **`fieldOrder`, `label` and `data`, which are not in bucket ①.** The delegation reads all three off a named view, and route 2 reads none of them. - `fieldOrder` is the composition's third key, so route 2 does not apply a named view's order. - `ObjectGrid` reads `label` for its caption and export filename, while route 2 passes `table.title`. - `ObjectGrid` reads `data` for its fetch source; route 2 passes none. - **Member 4 of objectui#10885.** `fieldOrder`, `label`, `data`, `navigation` and `inlineEdit` on route 2 are member 4 of objectui#10885, recorded by the seat on the card. - **Member 2 is not taken.** The delegation does not relay a named view's eight view-kind blocks. - The rung this PR adds is on route 2. Covering member 2 would add named-view reads inside the objectui#5097 fence. - That moves the delegation's behaviour and its pinned named-view read set (`objectViewHostSurface.test.tsx` section 1b), and this order keeps both untouched. - Reach is unchanged: no in-tree host passes both `listViews` and `renderListView`. - **Member 3 is not in this PR.** The three census changesets are untouched, and #10885 remains open for its own prose-only PR. This change moves no census figure: 43 read / 21 unread of 64, with bucket ① empty. - **Type narrowness, noted.** The protocol's named view declares a `conditionalFormatting` rule's `condition` as an expression input, which admits the `{ dialect, source }` wire, and a `bulkActionDefs` `visible` wire that may carry `ast` and no `source`. `ObjectGridSchema` in `@object-ui/types` spells neither; the spec's `object-grid` declares both slots loosely (`conditionalFormatting` as unknown, `bulkActionDefs` as an array of unknown), so the gap is against the named view's wire, not against `object-grid`. The runtime reads both through the shared evaluator. The two rungs assert the declared type. Widening the declared `condition` type would be a Clause-② type move, so it belongs on the backlog; not filed here. - **An observation, not filed.** The protocol's `object-grid` also accepts `exportOptions` as a bare format array, and keeps it an array at parse. `ObjectGrid` reads only `exportOptions.formats`. This is a source reading, not a measured render. Carrier: PR objectui#10278, which holds `ObjectGrid.tsx`. - **Beyond the claimed file surface:** the README and docs paragraphs (the docs-driven rule of AGENTS.md section 5), and the app-shell pin file (the order's `ViewPreview` pin). --- _Generated by [Claude Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9ce90ba commit 492e79e

6 files changed

Lines changed: 516 additions & 4 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@object-ui/plugin-view': patch
3+
---
4+
5+
The registered `object-view` renderer reads a named grid view's own grid members off the named view (objectui#10885).
6+
7+
`object-view` is registered without a `renderListView`, so an authored node, and the Studio's view preview (which renders a stored view through `SchemaRenderer`), draws a grid view as the `object-grid` node `ObjectView` builds for `ObjectGrid`. That node read `columns`, `filter`, `sort`, `grouping` and `rowColor` off the active named view and nothing else, so a stored or authored grid view's `rowHeight`, `pagination` and the rest were accepted and not shown.
8+
9+
It now also reads, off the active named view first, ten members the protocol declares under the same name on both a named view and `object-grid` and that `ObjectGrid` reads: `pagination`, `selection`, `rowHeight`, `resizable`, `searchableFields`, `conditionalFormatting`, `rowActions`, `bulkActions`, `bulkActionDefs` and `exportOptions`. `pagination` and `selection` still fall back to the node's `table` when the named view omits them; the others have no node fallback on this path, as before. Three other members declared on both, `label`, `data` and `navigation`, are still not read off the named view on this path.
10+
11+
A named view's `hiddenFields` is applied the way the protocol composes it: the hidden fields are removed from the column projection the grid receives. Only a declared projection is narrowed; with no `columns` anywhere the grid still derives its own.
12+
13+
What moves: only a grid view that authors one of these members. A named view that carries none of them renders exactly as before, and the host `renderListView` delegation is unchanged.
14+
15+
`Clause-②: no` — no declared type, accepted key or published export moves. A renderer starts honouring members the spec already declares on a named view and the grid already reads.

‎content/docs/plugins/plugin-view.mdx‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,18 @@ const productViews: ObjectViewSchema = {
431431
};
432432
```
433433

434+
**On the registered renderer.** An authored `object-view` node, and the Studio's
435+
view preview, draws a grid named view through `ObjectGrid`. Ten grid members the
436+
protocol declares under the same name on a named view and on `object-grid` —
437+
`pagination`, `selection`, `rowHeight`, `resizable`, `searchableFields`,
438+
`conditionalFormatting`, `rowActions`, `bulkActions`, `bulkActionDefs` and
439+
`exportOptions` — come from the active named view first; `pagination` and
440+
`selection` still fall back to `table` (objectui#10885). `label`, `data` and
441+
`navigation`, also declared on both, are not read off the named view on this
442+
path. A named view's `hiddenFields` removes those fields from the columns the
443+
grid draws, when a column list is declared.
444+
`src/__tests__/ObjectView.routeTwoNamedGridMembers-10885.test.tsx` pins each member.
445+
434446
**On a host's `renderListView`.** A host that composes `ObjectView` with both
435447
`listViews` and its own `renderListView` receives a `list-view` node for the
436448
active view. For the list members the protocol declares on a named view — list
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectui#10885 member 1 — the Studio view preview shows a stored grid view's
5+
* own grid members.
6+
*
7+
* `ViewPreview` injects the stored view body as a named `listViews` entry and
8+
* renders it through `SchemaRenderer`, which resolves the REGISTERED
9+
* `object-view` renderer: `ObjectView` with no `renderListView`, so a grid view
10+
* lands on route 2, the `object-grid` node `ObjectView` hands `ObjectGrid`.
11+
* Before this card that node read `columns`, `filter`, `sort`, `grouping` and
12+
* `rowColor` off the named view and nothing else, so a stored view's
13+
* `rowHeight` and `hiddenFields` were accepted and dropped from the preview.
14+
*
15+
* Everything here is the real stack: the real `ViewPreview`, the real
16+
* `SchemaRenderer` and registry, the registered `object-view` renderer and the
17+
* real `ObjectGrid`. The readings are the rendered DOM.
18+
*
19+
* - `rowHeight` — `ObjectGrid` draws its row-height control only when its
20+
* node carries `rowHeight`, titled with the resolved mode.
21+
* - `hiddenFields` — the protocol's column composition: `columns` projects
22+
* and `hiddenFields` subtracts. The hidden column's header is not drawn.
23+
* - Lit control — `columns` off the SAME named view was already honoured on
24+
* route 2: a field the view does not project is not drawn, and a projected
25+
* one is.
26+
*/
27+
28+
import * as React from 'react';
29+
import { describe, it, expect, vi, afterEach } from 'vitest';
30+
import { render, screen, waitFor, cleanup } from '@testing-library/react';
31+
import { SchemaRendererProvider } from '@object-ui/react';
32+
// Module scope, not a hook: this import IS the `object-view` registration the
33+
// preview's `SchemaRenderer` resolves.
34+
import '@object-ui/plugin-view';
35+
import { ViewPreview } from './ViewPreview';
36+
37+
const objectDef = {
38+
name: 'task',
39+
label: 'Task',
40+
fields: {
41+
name: { name: 'name', type: 'text', label: 'Task Name' },
42+
amount: { name: 'amount', type: 'number', label: 'Budget Amount' },
43+
stage: { name: 'stage', type: 'text', label: 'Pipeline Stage' },
44+
owner: { name: 'owner', type: 'text', label: 'Record Owner' },
45+
},
46+
};
47+
48+
function createDataSource(): any {
49+
return {
50+
find: vi.fn(async () => ({
51+
data: [{ id: 't1', name: 'Alpha', amount: 5, stage: 'open', owner: 'ada' }],
52+
total: 1,
53+
})),
54+
findOne: vi.fn(async () => null),
55+
create: vi.fn(async () => ({})),
56+
update: vi.fn(async () => ({})),
57+
delete: vi.fn(async () => ({})),
58+
getObjectSchema: vi.fn(async () => objectDef),
59+
};
60+
}
61+
62+
/**
63+
* The host's authenticated fetch. `ObjectGrid` asks the explain engine for its
64+
* per-row edit and delete verdicts through it; a 501 is the engine's
65+
* "not implemented", which the grid reads fail-open. Handed in through the
66+
* provider, as the console does, so no read reaches a real socket.
67+
*/
68+
const apiFetch = vi.fn(async () => new Response(null, { status: 501 }));
69+
70+
/** Mount the Studio preview of one stored grid view, the way the editor does. */
71+
function renderPreview(config: Record<string, unknown>) {
72+
render(
73+
<SchemaRendererProvider dataSource={createDataSource()} apiFetch={apiFetch}>
74+
<ViewPreview
75+
type="view"
76+
name="all_tasks"
77+
draft={{ name: 'task.all_tasks', object: 'task', viewKind: 'list', config }}
78+
/>
79+
</SchemaRendererProvider>,
80+
);
81+
}
82+
83+
/** The grid has drawn its headers from the object schema. */
84+
async function gridDrawn() {
85+
await waitFor(() => expect(screen.getAllByText('Task Name').length).toBeGreaterThan(0));
86+
}
87+
88+
afterEach(() => cleanup());
89+
90+
describe('objectui#10885 — the Studio view preview honours a stored grid view\'s grid members', () => {
91+
it('LIT CONTROL: the named view\'s `columns` projects the grid on route 2 already', async () => {
92+
renderPreview({ type: 'grid', columns: ['name', 'stage'] });
93+
await gridDrawn();
94+
expect(screen.getAllByText('Pipeline Stage').length).toBeGreaterThan(0);
95+
expect(screen.queryByText('Record Owner')).toBeNull();
96+
});
97+
98+
it('`hiddenFields` subtracts from the projected columns — the hidden header is not drawn', async () => {
99+
renderPreview({ type: 'grid', columns: ['name', 'amount', 'stage'], hiddenFields: ['amount'] });
100+
await gridDrawn();
101+
expect(screen.getAllByText('Pipeline Stage').length).toBeGreaterThan(0);
102+
expect(screen.queryByText('Budget Amount')).toBeNull();
103+
});
104+
105+
it('`rowHeight` reaches the grid — its row-height control is drawn in the stored mode', async () => {
106+
renderPreview({ type: 'grid', columns: ['name', 'stage'], rowHeight: 'compact' });
107+
await gridDrawn();
108+
expect(screen.getByTitle('Row height: compact')).toBeTruthy();
109+
});
110+
111+
it('CONTROL: a stored view that declares neither draws every projected column and no row-height control', async () => {
112+
renderPreview({ type: 'grid', columns: ['name', 'amount', 'stage'] });
113+
await gridDrawn();
114+
expect(screen.getAllByText('Budget Amount').length).toBeGreaterThan(0);
115+
expect(screen.queryByTitle(/^Row height:/)).toBeNull();
116+
});
117+
});

‎packages/plugin-view/README.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,18 @@ const schema: ObjectViewSchema = {
497497
};
498498
```
499499

500+
**On the registered renderer.** An authored `object-view` node, and the Studio's
501+
view preview, draws a grid named view through `ObjectGrid`. Ten grid members the
502+
protocol declares under the same name on a named view and on `object-grid` —
503+
`pagination`, `selection`, `rowHeight`, `resizable`, `searchableFields`,
504+
`conditionalFormatting`, `rowActions`, `bulkActions`, `bulkActionDefs` and
505+
`exportOptions` — come from the active named view first; `pagination` and
506+
`selection` still fall back to `table` (objectui#10885). `label`, `data` and
507+
`navigation`, also declared on both, are not read off the named view on this
508+
path. A named view's `hiddenFields` removes those fields from the columns the
509+
grid draws, when a column list is declared.
510+
`src/__tests__/ObjectView.routeTwoNamedGridMembers-10885.test.tsx` pins each member.
511+
500512
**On a host's `renderListView`.** A host that composes `ObjectView` with both
501513
`listViews` and its own `renderListView` receives a `list-view` node for the
502514
active view. For the list members the protocol declares on a named view — list

‎packages/plugin-view/src/ObjectView.tsx‎

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,63 @@ function viewColumnFieldNames(columns: unknown): string[] | undefined {
244244
return columns.map(columnIdentity).filter((n): n is string => !!n);
245245
}
246246

247+
/**
248+
* objectui#10885 — a named view's `hiddenFields`, applied to the column
249+
* projection route 2 hands `ObjectGrid`.
250+
*
251+
* The protocol composes three members of a named list view: `columns`
252+
* projects, `hiddenFields` subtracts, `fieldOrder` orders what survives
253+
* (objectstack#15184 ruling B). `ObjectGrid` has no `hiddenFields` read, and
254+
* `object-grid` declares no such key, so relaying the member by name would put
255+
* a key on the grid node that nothing reads. The subtraction is therefore made
256+
* here, on the resolved projection, as `ListView` subtracts it for the host
257+
* delegation (its `effectiveFields`), except that an entry with no field
258+
* identity is kept: `ListView` drops such an entry whenever `hiddenFields` is
259+
* non-empty.
260+
*
261+
* - Only a DECLARED projection is narrowed. With no `columns` anywhere the grid
262+
* derives its own defaults, and this returns `undefined` unchanged, as
263+
* `ListView` does.
264+
* - An entry is dropped only when `columnIdentity` names a hidden field. Every
265+
* other entry, and the entry's own shape, is kept.
266+
* - A projection that every entry leaves is kept as the empty projection the
267+
* author wrote; it does not fall back to the grid's defaults.
268+
*
269+
* Generic over the whole array type because the `columns` slot is a union of
270+
* two array types (`string[] | ListColumn[]`): filtering keeps each entry as it
271+
* was, so the result has the type the input had, which the one cast states.
272+
*/
273+
function withoutHiddenFields<C extends readonly unknown[]>(columns: C | undefined, hidden: readonly string[] | undefined): C | undefined {
274+
if (!Array.isArray(columns) || !Array.isArray(hidden) || hidden.length === 0) return columns;
275+
const drop = new Set(hidden);
276+
return (columns as readonly unknown[]).filter((entry) => {
277+
const name = columnIdentity(entry);
278+
return !name || !drop.has(name);
279+
}) as unknown as C;
280+
}
281+
282+
/**
283+
* objectui#10885 — a named view's `exportOptions`, in the one shape the
284+
* `object-grid` slot holds.
285+
*
286+
* The protocol declares the member on a named view as `{ formats?, maxRecords?,
287+
* includeHeaders?, fileNamePrefix?, streaming? }`, and declares a bare format
288+
* array as its legacy spelling, which "lifts to `{ formats: [...] }` at parse"
289+
* (the member's own description in `@objectstack/spec`). A named view reaches
290+
* this component unparsed, so the bare array can arrive as written. `ObjectGrid`
291+
* reads `exportOptions.formats`, which an array does not have: relayed as is,
292+
* the grid would offer its default formats instead of the ones the author
293+
* listed. So the declared union is narrowed to the slot's branch here, by the
294+
* protocol's own lift, the way `ListView` narrows it for the host delegation.
295+
* It accepts no spelling the protocol does not; the node-shape fold of a
296+
* declared union at the boundary is the objectui#5269 / objectui#8254 pattern
297+
* `viewColumnFieldNames` follows.
298+
*/
299+
function gridExportOptions(options: NamedViewConfig['exportOptions']): ObjectGridSchema['exportOptions'] {
300+
if (Array.isArray(options)) return { formats: options };
301+
return options;
302+
}
303+
247304
/**
248305
* One entry of `ObjectViewSchema.listViews` — the protocol's
249306
* `ObjectListViewSchema`, by reference (objectui#7928). Derived from the member
@@ -2202,6 +2259,8 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
22022259
// objectui#7928: see the non-grid fetch above. A retired string `sort` on a
22032260
// named view reaches `ObjectGrid` unchanged, which refuses it out loud.
22042261
const viewSort = (currentNamedViewConfig?.sort as ObjectGridSchema['sort']) || activeView?.sort;
2262+
// objectui#10885 — subtracted from whichever projection wins below.
2263+
const hiddenFields = currentNamedViewConfig?.hiddenFields;
22052264

22062265
return {
22072266
type: 'object-grid',
@@ -2214,8 +2273,16 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
22142273
// folded to identities, and `columns` is `string[] | ListColumn[]` so it
22152274
// takes the authored value raw. One value, two slots, each given the
22162275
// shape it declares.
2217-
fields: viewColumnFieldNames(currentNamedViewConfig?.columns) || activeView?.columns || schema.table?.fields,
2218-
columns: currentNamedViewConfig?.columns || activeView?.columns || schema.table?.columns,
2276+
// objectui#10885 — both slots lose the named view's `hiddenFields`
2277+
// (see `withoutHiddenFields`).
2278+
fields: withoutHiddenFields(
2279+
viewColumnFieldNames(currentNamedViewConfig?.columns) || activeView?.columns || schema.table?.fields,
2280+
hiddenFields,
2281+
),
2282+
columns: withoutHiddenFields(
2283+
currentNamedViewConfig?.columns || activeView?.columns || schema.table?.columns,
2284+
hiddenFields,
2285+
),
22192286
operations: {
22202287
...operations,
22212288
create: false, // Create is handled by the view's create button
@@ -2233,8 +2300,41 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
22332300
// express; what changes is only WHICH slot a view's sort arrives in, and
22342301
// this is the one whose declared arity can hold it.
22352302
sort: viewSort || schema.table?.sort,
2236-
pagination: schema.table?.pagination,
2237-
selection: schema.table?.selection,
2303+
// ⭐ objectui#10885 — the named view's own GRID MEMBERS, read here the
2304+
// way the host delegation below reads them since objectui#10758: the
2305+
// named view first, then the node only where this branch already read
2306+
// the node (`table.pagination`, `table.selection`). Each is a member the
2307+
// protocol declares on a named view AND on `object-grid`, under the same
2308+
// name, and `ObjectGrid` reads each one. The Studio's view preview renders
2309+
// a stored view through this branch, so before this a stored view's
2310+
// `rowHeight`, `pagination` and the rest were accepted and not shown.
2311+
//
2312+
// NAMED-VIEW SOURCED, like `grouping` / `rowColor` below: ⛔ no
2313+
// `activeView` rung (the host `views` path never fed these slots on this
2314+
// branch) and ⛔ no node read this branch did not already have. ⛔ No
2315+
// alias and no key `object-grid` does not declare: `hiddenFields` is
2316+
// applied to the projection above rather than relayed, and `navigation`
2317+
// is not relayed because this component passes `ObjectGrid` its own
2318+
// `onRowClick`, which the grid's navigation hook obeys first.
2319+
pagination: currentNamedViewConfig?.pagination ?? schema.table?.pagination,
2320+
selection: currentNamedViewConfig?.selection ?? schema.table?.selection,
2321+
rowHeight: currentNamedViewConfig?.rowHeight,
2322+
resizable: currentNamedViewConfig?.resizable,
2323+
searchableFields: currentNamedViewConfig?.searchableFields,
2324+
// The protocol's rule `condition` is a string or the `{ dialect, source }`
2325+
// expression wire; `ObjectGrid` hands every rule to the shared evaluator
2326+
// (`resolveConditionalFormatting`), which reads both. The grid's declared
2327+
// rule type spells the string form only, hence the assertion — the same
2328+
// one `viewSort` above makes for the named view's `sort`.
2329+
conditionalFormatting: currentNamedViewConfig?.conditionalFormatting as ObjectGridSchema['conditionalFormatting'],
2330+
rowActions: currentNamedViewConfig?.rowActions,
2331+
bulkActions: currentNamedViewConfig?.bulkActions,
2332+
// Same reason as `conditionalFormatting`: the protocol's `visible`
2333+
// expression wire may carry `ast` without `source`, which the grid's
2334+
// declared `BulkActionDef` does not spell. It is the value the host
2335+
// delegation already hands `ObjectGrid` through `ListView`.
2336+
bulkActionDefs: currentNamedViewConfig?.bulkActionDefs as ObjectGridSchema['bulkActionDefs'],
2337+
exportOptions: gridExportOptions(currentNamedViewConfig?.exportOptions),
22382338
// ⭐ objectui#8980 — the AUTHOR-REACHABLE read point for two of the
22392339
// seventeen. `ObjectGrid` already reads both (`schema.grouping` in its
22402340
// group-field memo and its reference collector, `useRowColor(schema.rowColor)`),

0 commit comments

Comments
 (0)