Skip to content

Commit 222755e

Browse files
refactor(plugin-grid): move ObjectGrid's host-facing props types and FooterSummaryReport into sibling modules (slice 2) (#26354)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi94Cr2fbhp665MVvsYFfqYFe)_ Part of #25994 (slice 2 of the move-only split; the card stays open for the later slices). Clause-②: no Metadata-protocol: none ## What moved Base `71580254` (main with slice 1, #26323). This slice finishes the module-level part of `ui/packages/plugin-grid/src/ObjectGrid.tsx` that no test pins. It moves two groups, verbatim, into two new sibling modules: - `objectGridProps.ts`: `ObjectGridColumnState`, `ObjectGridFooterSummary` and `ObjectGridExternalPaginationProps`, with their docblocks. These are the host-facing props types that `ObjectGridComponentProps` builds on. - `objectGridFooterSummaryReport.tsx`: `FooterSummaryReport`, the renderless component that reports the grid's column summaries to a host. 144 lines move, and `ObjectGrid.tsx` goes from 6781 to 6637 lines. `FooterSummaryReport` gains the `export` keyword in its new module. `ObjectGrid.tsx` imports the moved names back and re-exports the three types, so every name it exported is still importable from the same path. The package entry (`index.tsx`) is untouched. No identifier is renamed, no logic or DOM changes, and no new name is exported from `ObjectGrid.tsx`. Move-only proof: the multiset of non-blank lines of the base `ObjectGrid.tsx` equals the new `ObjectGrid.tsx` plus the two modules (each module's header comment dropped, the added `export` keyword normalised). Nothing is missing. The only other lines are the import lines and the one type re-export line. ## Changeset correction from slice 1 Slice 1's changeset (`.changeset/25994-objectgrid-helpers-split.md`, still unreleased) listed "the record-source ladder" among the moved query helpers, but `getDataConfig` stayed in `ObjectGrid.tsx`: exported, it would have become a shared reader for `check:doc-example-readers`. The correction lives as one sentence in this slice's own changeset (`.changeset/25994-objectgrid-props-split.md`). Slice 1's note is untouched, so `check-empty-changeset` has nothing to refuse. ## What module-level code stays in `ObjectGrid.tsx`, and why - `parseSchemaSort` and its sibling `toOrderByClause`: `gridArrayArmOrderby-8973.test.tsx` asserts `export function parseSchemaSort` is in the file, and `toOrderByClause`'s docblock is written about the call sites beside it. - `ObjectGridComponentProps`: `bulkExecutorDataSourceFace-9722.test.ts` asserts `export interface ObjectGridComponentProps` is in the file. Its docblock sits above `ObjectGridRowOperations`, so `ObjectGridRowOperations` stays too: moving it would re-attach that docblock to a different declaration. - `getDataConfig`, which stays file-local (see above). - The schema-slot types (`RemoveIndexSignature` and the rest): `cellClassNameCensusProse-6921.test.ts` reads the census prose in their docblock from `ObjectGrid.tsx`. - The relational-copy-set docblock, which is prose about code in this file. - The group-heading date region (`TemporalGroupFace`, `temporalGroupHeading`): #26341 (#25884) is editing it. So after this slice, the remaining work on the card is the component body. ## Test edits None. ## Verification Run on head `f1e4df4c` (tree identical to the commits the tests ran on; only the commit messages were reworded before the push): - `pnpm --dir ui exec vitest run packages/plugin-grid/ --maxWorkers=2`, the whole package suite, under the verification lock: 251 of 251 files and 2247 of 2247 tests passed after the move. The before reading is the same suite on slice 1's head (`8fe81ee9`, whose tree is `main` `71580254` for this package): 251 of 251 files, 2247 of 2247 tests, locally and in CI. - The 52 test files outside plugin-grid that read sources (the same list slice 1 used): 52 of 52 files, 2257 of 2257 tests passed. - `pnpm --filter @object-ui/plugin-grid type-check` exit 0. - eslint on the three files: 0 errors. - `pnpm --dir ui run check:doc-example-readers` exit 0. - `node scripts/gen-sdui-manifest-node.mjs`: the manifest is byte-identical (sha256 `1f6e1ccb…`). Only `scripts/sdui-manifest.sources/plugin-grid.json` is re-recorded. `node scripts/check-sdui-manifest.mjs` exit 0. - `node scripts/pm/dispatch-gates.mjs --commands`: 41 derived gates; `--ran` reconciles 41 of 41 (0 NOT-MEASURED, 0 UNRUN). All 41 exit 0; `check-empty-changeset` was re-run on `ba1445b9`. ## Landing Seat 2's #26307 and #26341 land first on the plugin-grid line, so this stays a draft. Expect a merge of `main` and a re-record of `plugin-grid.json` before it lands. Changesets: `.changeset/25994-objectgrid-props-split.md` (patch, `@object-ui/plugin-grid`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Bdrd2yH4eqnz2QobtN3haE --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0b76a1f commit 222755e

5 files changed

Lines changed: 183 additions & 148 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@object-ui/plugin-grid': patch
3+
---
4+
5+
refactor(plugin-grid): `ObjectGrid`'s host-facing props types and its footer-summary reporter move into sibling modules
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`ObjectGridColumnState`, `ObjectGridFooterSummary` and `ObjectGridExternalPaginationProps` move verbatim out of `ObjectGrid.tsx` into `objectGridProps.ts`, and the renderless `FooterSummaryReport` moves into `objectGridFooterSummaryReport.tsx`. No logic, name or rendered output changes. `ObjectGrid.tsx` re-exports the three types, so every name it exported before is still importable from the same path, and the package entry is unchanged.
11+
12+
Correction to the slice 1 note: the record-source ladder (`getDataConfig`) stayed in `ObjectGrid.tsx`; it moved in neither slice.
13+
14+
What to do: nothing.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"package":"ui/packages/plugin-grid","sources":44,"sha256":"afbc05964d76c725b0d533f1bd28b1515591d0748eca468158f64fc3bb7f7a80","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
1+
{"package":"ui/packages/plugin-grid","sources":46,"sha256":"2c2577a854216b798b363a8751d8924e3e9205404c0eb592b5f3d53197cd3bc0","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}

‎ui/packages/plugin-grid/src/ObjectGrid.tsx‎

Lines changed: 3 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ import { LinkCell, recordPointerFace, rowRecordId } from './objectGridLinkCell';
9393
import { useGridTranslation } from './objectGridTranslations';
9494
import { DEFAULT_DISPLAY_PAGE_SIZE, DEFAULT_FETCH_BATCH_SIZE, DEFAULT_SELECTION_TYPE, NO_GROUPING_REFUSAL, type RowHeightMode, describeNonPositivePageSize, resolvePageSize, resolveRowHeightMode } from './objectGridDefaults';
9595
import { type BatchSaveOutcome, OCC_TOKEN_FIELD, type RowConflictChoice, type WrittenRowVersion, rowVersionOf, rowVersionToSend } from './objectGridRowVersion';
96+
import { type ObjectGridColumnState, type ObjectGridExternalPaginationProps, type ObjectGridFooterSummary } from './objectGridProps';
97+
import { FooterSummaryReport } from './objectGridFooterSummaryReport';
9698
export type { RetiredListColumnKey, ObjectGridColumnHolds, ObjectGridColumnDraft, ObjectGridColumn } from './objectGridColumns';
99+
export type { ObjectGridColumnState, ObjectGridFooterSummary, ObjectGridExternalPaginationProps } from './objectGridProps';
97100

98101
/**
99102
* A declared `sort` → the `"field order"` join string THIS block sends as
@@ -172,127 +175,6 @@ export function parseSchemaSort(sort: unknown): TableSortItem[] {
172175
return items;
173176
}
174177

175-
/**
176-
* The column layout ObjectGrid persists and reports back — the merged result of
177-
* a resize and a reorder, not either event on its own. Named so the state hook
178-
* below and the `onColumnStateChange` prop cannot drift apart.
179-
*/
180-
export interface ObjectGridColumnState {
181-
order?: string[];
182-
widths?: Record<string, number>;
183-
}
184-
185-
/**
186-
* One column summary as the grid's footer would draw it — `header: label`,
187-
* e.g. `Annual Revenue` and `Sum: 109,800,000.00` — handed to a host through
188-
* {@link ObjectGridComponentProps.onColumnSummaryChange} (objectui#12528).
189-
*/
190-
export interface ObjectGridFooterSummary {
191-
/** The column's accessor key. */
192-
key: string;
193-
/** The column's header text. */
194-
header: string;
195-
/** The formatted summary, scope included (`Sum: 109,800,000.00`). */
196-
label: string;
197-
}
198-
199-
/**
200-
* The HOST-DRIVEN ("external") mode of ObjectGrid — framework#2212.
201-
*
202-
* The ordinary authoring surface hands ObjectGrid a `schema` and a `dataSource`
203-
* and lets it fetch, page, sort and search for itself. In this mode a host
204-
* (ListView, a designer preview, an app screen with its own toolbar) has
205-
* already fetched one window of a larger collection and drives the controls
206-
* itself: it passes the window as `data` plus the real match total and the
207-
* page/sort/search state, and ObjectGrid forwards them straight to its
208-
* DataTable instead of client-slicing the window it was handed.
209-
*
210-
* Kept as its own named interface rather than flattened into `ObjectGridComponentProps`
211-
* (#4277 裁决 B, 2026-08-11): the two are different classes of contract, and a
212-
* dozen more members merged into the authoring surface would erase that
213-
* boundary. Until this existed, every member below was read out of `...rest`
214-
* through an `as any` cast and was declared nowhere at all.
215-
*
216-
* DERIVATION (#4277 裁决 §3, the anti-drift pin): this vocabulary is already
217-
* declared once, on `DataTableSchema` — which is exactly where ObjectGrid
218-
* forwards it — so the members that have a counterpart there are TYPE-DERIVED
219-
* from that declaration rather than hand-copied into a second enumeration. Two
220-
* hand-written copies of one vocabulary is how the next drift happens. The
221-
* `Partial<...>` wrapper is deliberate and is the only shape change: the whole
222-
* mode is opt-in, and `DataTableSchema['data']` is required because a table
223-
* always has rows, while a grid that was given no `data` fetches its own.
224-
*
225-
* `search` is read outside this mode too (objectui#11021): a host that passes
226-
* it owns the search term even where the grid fetches for itself, and the
227-
* grid's own queries carry it. `ListView` hands its toolbar term to a grid
228-
* that groups on the server that way, with no rows.
229-
*/
230-
export interface ObjectGridExternalPaginationProps
231-
extends Partial<
232-
Pick<
233-
DataTableSchema,
234-
// The host's already-fetched window. Highest-priority data source: it
235-
// wins over `schema.data` / `schema.bind` when present.
236-
| 'data'
237-
// Turns off client slicing. With `rowCount` + `onPageChange` it is what
238-
// makes the mode active at all (see `externalManualPagination` below).
239-
| 'manualPagination'
240-
| 'rowCount'
241-
| 'page'
242-
| 'pageSize'
243-
| 'onPageChange'
244-
| 'onPageSizeChange'
245-
| 'sort'
246-
| 'onSortChange'
247-
| 'search'
248-
| 'onSearchChange'
249-
>
250-
> {
251-
/**
252-
* Grid-only: `DataTableSchema` has no counterpart to derive from.
253-
*
254-
* The table vocabulary reports column changes as separate per-event
255-
* callbacks — `onColumnResize(columnKey, width)` and
256-
* `onColumnReorder(newOrder)` — whereas this reports the MERGED, persisted
257-
* `{ order, widths }` layout after ObjectGrid has folded either event into
258-
* the state it also writes to `localStorage`, so a host can save one blob
259-
* (the console stores it on the signed-in user's own list state). Deriving
260-
* it from either table
261-
* callback would misstate both the payload and when it fires.
262-
*/
263-
onColumnStateChange?: (state: ObjectGridColumnState) => void;
264-
265-
/**
266-
* Grid-only: `DataTableSchema` has no counterpart to derive from — a table is
267-
* handed rows, it never issues a query.
268-
*
269-
* The params the host passed to `dataSource.find()` for the window it is
270-
* handing down in `data` — the SAME shape this grid's own loader stores in
271-
* `lastFindParamsRef` (`$filter` / `$orderby` / `$select` / `$search` /
272-
* `$searchFields` / `$expand` / `$top` / `$skip`), so both paths feed one
273-
* reader. `$top`/`$skip` may be present and are ignored: the fan-out windows
274-
* the replay itself.
275-
*
276-
* Why a prop rather than a fallback inside the grid (objectui#4501): the
277-
* cross-page "select all N matching" escalation re-issues the view's query to
278-
* collect the whole match set, and the query has to come from whichever side
279-
* owns the fetch. Under external pagination that is the host, and the grid's
280-
* own ref is either empty or stale — replaying it asked the server for the
281-
* WHOLE OBJECT (no `$filter`) and handed up to 5000 unmatched records to a
282-
* destructive executor. A grid-side `?? {}` default is what produced that,
283-
* which is why the missing case is not defaulted but REFUSED: with no params
284-
* for the current data path the escalation is not offered at all (see
285-
* `bulkFanoutParams` in the component body).
286-
*
287-
* A changed value is also the host's query-change signal: it resets the
288-
* escalation, mirroring the `setSelectAllMatching(false)` the internal loader
289-
* runs next to its own `lastFindParamsRef` write. Compared by CONTENT, so a
290-
* host re-render that rebuilds an equal object does not drop the user's
291-
* escalation.
292-
*/
293-
findParams?: Record<string, unknown> | null;
294-
}
295-
296178
/**
297179
* Props of the `ObjectGrid` React component.
298180
*
@@ -735,32 +617,6 @@ export type ObjectGridDataTableSchemaHolds = {
735617
export type ObjectGridDataTableSchema =
736618
DeclaredDataTableSchema & ObjectGridDataTableSchemaHolds;
737619

738-
/**
739-
* [objectui#12528] Reports the grid's column summaries to a host that draws
740-
* them in its own footer row (`onColumnSummaryChange`). Renders nothing.
741-
*
742-
* Keyed on the summaries' TEXT, never on the array's identity (AGENTS.md #10):
743-
* every grid render builds a new array, and an equal one must send nothing.
744-
* Unmounting — whatever stops the grid drawing the row — reports `null`.
745-
*/
746-
function FooterSummaryReport({
747-
summaries,
748-
onChange,
749-
}: {
750-
summaries: ObjectGridFooterSummary[];
751-
onChange: (summaries: readonly ObjectGridFooterSummary[] | null) => void;
752-
}) {
753-
const key = JSON.stringify(summaries);
754-
const onChangeRef = React.useRef(onChange);
755-
useEffect(() => {
756-
onChangeRef.current = onChange;
757-
});
758-
useEffect(() => {
759-
onChangeRef.current(JSON.parse(key) as ObjectGridFooterSummary[]);
760-
}, [key]);
761-
useEffect(() => () => onChangeRef.current(null), []);
762-
return null;
763-
}
764620

765621
/**
766622
* [objectui#13234] The face a date-family CELL paints, for one grouping field:
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* `FooterSummaryReport`, the renderless reporter `ObjectGrid` (`./ObjectGrid.tsx`)
3+
* mounts to hand its column summaries to a host, moved out of that file
4+
* verbatim (objectstack#25994, slice 2).
5+
*/
6+
7+
import React, { useEffect } from 'react';
8+
import type { ObjectGridFooterSummary } from './objectGridProps';
9+
10+
/**
11+
* [objectui#12528] Reports the grid's column summaries to a host that draws
12+
* them in its own footer row (`onColumnSummaryChange`). Renders nothing.
13+
*
14+
* Keyed on the summaries' TEXT, never on the array's identity (AGENTS.md #10):
15+
* every grid render builds a new array, and an equal one must send nothing.
16+
* Unmounting — whatever stops the grid drawing the row — reports `null`.
17+
*/
18+
export function FooterSummaryReport({
19+
summaries,
20+
onChange,
21+
}: {
22+
summaries: ObjectGridFooterSummary[];
23+
onChange: (summaries: readonly ObjectGridFooterSummary[] | null) => void;
24+
}) {
25+
const key = JSON.stringify(summaries);
26+
const onChangeRef = React.useRef(onChange);
27+
useEffect(() => {
28+
onChangeRef.current = onChange;
29+
});
30+
useEffect(() => {
31+
onChangeRef.current(JSON.parse(key) as ObjectGridFooterSummary[]);
32+
}, [key]);
33+
useEffect(() => () => onChangeRef.current(null), []);
34+
return null;
35+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/**
2+
* The host-facing props types of `ObjectGrid` (`./ObjectGrid.tsx`) that
3+
* `ObjectGridComponentProps` builds on: the persisted column layout, one footer
4+
* summary, and the host-driven pagination props, moved out of that file
5+
* verbatim (objectstack#25994, slice 2). `ObjectGrid.tsx` re-exports every
6+
* name it exported before.
7+
*/
8+
9+
import type { DataTableSchema } from '@object-ui/types';
10+
11+
/**
12+
* The column layout ObjectGrid persists and reports back — the merged result of
13+
* a resize and a reorder, not either event on its own. Named so the state hook
14+
* below and the `onColumnStateChange` prop cannot drift apart.
15+
*/
16+
export interface ObjectGridColumnState {
17+
order?: string[];
18+
widths?: Record<string, number>;
19+
}
20+
21+
/**
22+
* One column summary as the grid's footer would draw it — `header: label`,
23+
* e.g. `Annual Revenue` and `Sum: 109,800,000.00` — handed to a host through
24+
* {@link ObjectGridComponentProps.onColumnSummaryChange} (objectui#12528).
25+
*/
26+
export interface ObjectGridFooterSummary {
27+
/** The column's accessor key. */
28+
key: string;
29+
/** The column's header text. */
30+
header: string;
31+
/** The formatted summary, scope included (`Sum: 109,800,000.00`). */
32+
label: string;
33+
}
34+
35+
/**
36+
* The HOST-DRIVEN ("external") mode of ObjectGrid — framework#2212.
37+
*
38+
* The ordinary authoring surface hands ObjectGrid a `schema` and a `dataSource`
39+
* and lets it fetch, page, sort and search for itself. In this mode a host
40+
* (ListView, a designer preview, an app screen with its own toolbar) has
41+
* already fetched one window of a larger collection and drives the controls
42+
* itself: it passes the window as `data` plus the real match total and the
43+
* page/sort/search state, and ObjectGrid forwards them straight to its
44+
* DataTable instead of client-slicing the window it was handed.
45+
*
46+
* Kept as its own named interface rather than flattened into `ObjectGridComponentProps`
47+
* (#4277 裁决 B, 2026-08-11): the two are different classes of contract, and a
48+
* dozen more members merged into the authoring surface would erase that
49+
* boundary. Until this existed, every member below was read out of `...rest`
50+
* through an `as any` cast and was declared nowhere at all.
51+
*
52+
* DERIVATION (#4277 裁决 §3, the anti-drift pin): this vocabulary is already
53+
* declared once, on `DataTableSchema` — which is exactly where ObjectGrid
54+
* forwards it — so the members that have a counterpart there are TYPE-DERIVED
55+
* from that declaration rather than hand-copied into a second enumeration. Two
56+
* hand-written copies of one vocabulary is how the next drift happens. The
57+
* `Partial<...>` wrapper is deliberate and is the only shape change: the whole
58+
* mode is opt-in, and `DataTableSchema['data']` is required because a table
59+
* always has rows, while a grid that was given no `data` fetches its own.
60+
*
61+
* `search` is read outside this mode too (objectui#11021): a host that passes
62+
* it owns the search term even where the grid fetches for itself, and the
63+
* grid's own queries carry it. `ListView` hands its toolbar term to a grid
64+
* that groups on the server that way, with no rows.
65+
*/
66+
export interface ObjectGridExternalPaginationProps
67+
extends Partial<
68+
Pick<
69+
DataTableSchema,
70+
// The host's already-fetched window. Highest-priority data source: it
71+
// wins over `schema.data` / `schema.bind` when present.
72+
| 'data'
73+
// Turns off client slicing. With `rowCount` + `onPageChange` it is what
74+
// makes the mode active at all (see `externalManualPagination` below).
75+
| 'manualPagination'
76+
| 'rowCount'
77+
| 'page'
78+
| 'pageSize'
79+
| 'onPageChange'
80+
| 'onPageSizeChange'
81+
| 'sort'
82+
| 'onSortChange'
83+
| 'search'
84+
| 'onSearchChange'
85+
>
86+
> {
87+
/**
88+
* Grid-only: `DataTableSchema` has no counterpart to derive from.
89+
*
90+
* The table vocabulary reports column changes as separate per-event
91+
* callbacks — `onColumnResize(columnKey, width)` and
92+
* `onColumnReorder(newOrder)` — whereas this reports the MERGED, persisted
93+
* `{ order, widths }` layout after ObjectGrid has folded either event into
94+
* the state it also writes to `localStorage`, so a host can save one blob
95+
* (the console stores it on the signed-in user's own list state). Deriving
96+
* it from either table
97+
* callback would misstate both the payload and when it fires.
98+
*/
99+
onColumnStateChange?: (state: ObjectGridColumnState) => void;
100+
101+
/**
102+
* Grid-only: `DataTableSchema` has no counterpart to derive from — a table is
103+
* handed rows, it never issues a query.
104+
*
105+
* The params the host passed to `dataSource.find()` for the window it is
106+
* handing down in `data` — the SAME shape this grid's own loader stores in
107+
* `lastFindParamsRef` (`$filter` / `$orderby` / `$select` / `$search` /
108+
* `$searchFields` / `$expand` / `$top` / `$skip`), so both paths feed one
109+
* reader. `$top`/`$skip` may be present and are ignored: the fan-out windows
110+
* the replay itself.
111+
*
112+
* Why a prop rather than a fallback inside the grid (objectui#4501): the
113+
* cross-page "select all N matching" escalation re-issues the view's query to
114+
* collect the whole match set, and the query has to come from whichever side
115+
* owns the fetch. Under external pagination that is the host, and the grid's
116+
* own ref is either empty or stale — replaying it asked the server for the
117+
* WHOLE OBJECT (no `$filter`) and handed up to 5000 unmatched records to a
118+
* destructive executor. A grid-side `?? {}` default is what produced that,
119+
* which is why the missing case is not defaulted but REFUSED: with no params
120+
* for the current data path the escalation is not offered at all (see
121+
* `bulkFanoutParams` in the component body).
122+
*
123+
* A changed value is also the host's query-change signal: it resets the
124+
* escalation, mirroring the `setSelectAllMatching(false)` the internal loader
125+
* runs next to its own `lastFindParamsRef` write. Compared by CONTENT, so a
126+
* host re-render that rebuilds an equal object does not drop the user's
127+
* escalation.
128+
*/
129+
findParams?: Record<string, unknown> | null;
130+
}

0 commit comments

Comments
 (0)