From 0b62aad267bda08333c50ea0b0fb0a61a4875c2b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 22:43:57 +0000 Subject: [PATCH 1/3] refactor(plugin-grid): move ObjectGrid's host-facing props types and FooterSummaryReport into sibling modules (slice 2) Move-only: ObjectGridColumnState, ObjectGridFooterSummary and ObjectGridExternalPaginationProps go verbatim to objectGridProps.ts, and the renderless FooterSummaryReport to objectGridFooterSummaryReport.tsx. ObjectGrid.tsx imports them back and re-exports the three types, so its exported names and the package entry do not move. Also corrects slice 1's changeset: getDataConfig (the record-source ladder) stayed in ObjectGrid.tsx, so it is no longer listed as moved. Claude-Session: https://claude.ai/code/session_01Bdrd2yH4eqnz2QobtN3haE Co-authored-by: Claude --- .changeset/25994-objectgrid-helpers-split.md | 2 +- .changeset/25994-objectgrid-props-split.md | 12 ++ ui/packages/plugin-grid/src/ObjectGrid.tsx | 150 +----------------- .../src/objectGridFooterSummaryReport.tsx | 35 ++++ .../plugin-grid/src/objectGridProps.ts | 130 +++++++++++++++ 5 files changed, 181 insertions(+), 148 deletions(-) create mode 100644 .changeset/25994-objectgrid-props-split.md create mode 100644 ui/packages/plugin-grid/src/objectGridFooterSummaryReport.tsx create mode 100644 ui/packages/plugin-grid/src/objectGridProps.ts diff --git a/.changeset/25994-objectgrid-helpers-split.md b/.changeset/25994-objectgrid-helpers-split.md index ac43043e80e..186a20f5da5 100644 --- a/.changeset/25994-objectgrid-helpers-split.md +++ b/.changeset/25994-objectgrid-helpers-split.md @@ -7,6 +7,6 @@ refactor(plugin-grid): `ObjectGrid`'s module-level helpers move into sibling mod Clause-②: no Metadata-protocol: none -The query helpers (the held filter resolution, the record-source ladder, the find-params signature), the column helpers and column emit types, the record-link cell, the English fallback translations, the display defaults (row height, page sizes, selection default) and the row-version helpers move verbatim out of `ObjectGrid.tsx` into six modules beside it. No logic, name or rendered output changes. `ObjectGrid.tsx` re-exports every name it exported before, and the package entry is unchanged. +The query helpers (the held filter resolution and the find-params signature), the column helpers and column emit types, the record-link cell, the English fallback translations, the display defaults (row height, page sizes, selection default) and the row-version helpers move verbatim out of `ObjectGrid.tsx` into six modules beside it. No logic, name or rendered output changes. `ObjectGrid.tsx` re-exports every name it exported before, and the package entry is unchanged. What to do: nothing. diff --git a/.changeset/25994-objectgrid-props-split.md b/.changeset/25994-objectgrid-props-split.md new file mode 100644 index 00000000000..a139bb90fa9 --- /dev/null +++ b/.changeset/25994-objectgrid-props-split.md @@ -0,0 +1,12 @@ +--- +'@object-ui/plugin-grid': patch +--- + +refactor(plugin-grid): `ObjectGrid`'s host-facing props types and its footer-summary reporter move into sibling modules + +Clause-②: no +Metadata-protocol: none + +`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. + +What to do: nothing. diff --git a/ui/packages/plugin-grid/src/ObjectGrid.tsx b/ui/packages/plugin-grid/src/ObjectGrid.tsx index af84f5662ee..6ae0aa9206b 100644 --- a/ui/packages/plugin-grid/src/ObjectGrid.tsx +++ b/ui/packages/plugin-grid/src/ObjectGrid.tsx @@ -93,7 +93,10 @@ import { LinkCell, recordPointerFace, rowRecordId } from './objectGridLinkCell'; import { useGridTranslation } from './objectGridTranslations'; import { DEFAULT_DISPLAY_PAGE_SIZE, DEFAULT_FETCH_BATCH_SIZE, DEFAULT_SELECTION_TYPE, NO_GROUPING_REFUSAL, type RowHeightMode, describeNonPositivePageSize, resolvePageSize, resolveRowHeightMode } from './objectGridDefaults'; import { type BatchSaveOutcome, OCC_TOKEN_FIELD, type RowConflictChoice, type WrittenRowVersion, rowVersionOf, rowVersionToSend } from './objectGridRowVersion'; +import { type ObjectGridColumnState, type ObjectGridExternalPaginationProps, type ObjectGridFooterSummary } from './objectGridProps'; +import { FooterSummaryReport } from './objectGridFooterSummaryReport'; export type { RetiredListColumnKey, ObjectGridColumnHolds, ObjectGridColumnDraft, ObjectGridColumn } from './objectGridColumns'; +export type { ObjectGridColumnState, ObjectGridFooterSummary, ObjectGridExternalPaginationProps } from './objectGridProps'; /** * A declared `sort` → the `"field order"` join string THIS block sends as @@ -172,127 +175,6 @@ export function parseSchemaSort(sort: unknown): TableSortItem[] { return items; } -/** - * The column layout ObjectGrid persists and reports back — the merged result of - * a resize and a reorder, not either event on its own. Named so the state hook - * below and the `onColumnStateChange` prop cannot drift apart. - */ -export interface ObjectGridColumnState { - order?: string[]; - widths?: Record; -} - -/** - * One column summary as the grid's footer would draw it — `header: label`, - * e.g. `Annual Revenue` and `Sum: 109,800,000.00` — handed to a host through - * {@link ObjectGridComponentProps.onColumnSummaryChange} (objectui#12528). - */ -export interface ObjectGridFooterSummary { - /** The column's accessor key. */ - key: string; - /** The column's header text. */ - header: string; - /** The formatted summary, scope included (`Sum: 109,800,000.00`). */ - label: string; -} - -/** - * The HOST-DRIVEN ("external") mode of ObjectGrid — framework#2212. - * - * The ordinary authoring surface hands ObjectGrid a `schema` and a `dataSource` - * and lets it fetch, page, sort and search for itself. In this mode a host - * (ListView, a designer preview, an app screen with its own toolbar) has - * already fetched one window of a larger collection and drives the controls - * itself: it passes the window as `data` plus the real match total and the - * page/sort/search state, and ObjectGrid forwards them straight to its - * DataTable instead of client-slicing the window it was handed. - * - * Kept as its own named interface rather than flattened into `ObjectGridComponentProps` - * (#4277 裁决 B, 2026-08-11): the two are different classes of contract, and a - * dozen more members merged into the authoring surface would erase that - * boundary. Until this existed, every member below was read out of `...rest` - * through an `as any` cast and was declared nowhere at all. - * - * DERIVATION (#4277 裁决 §3, the anti-drift pin): this vocabulary is already - * declared once, on `DataTableSchema` — which is exactly where ObjectGrid - * forwards it — so the members that have a counterpart there are TYPE-DERIVED - * from that declaration rather than hand-copied into a second enumeration. Two - * hand-written copies of one vocabulary is how the next drift happens. The - * `Partial<...>` wrapper is deliberate and is the only shape change: the whole - * mode is opt-in, and `DataTableSchema['data']` is required because a table - * always has rows, while a grid that was given no `data` fetches its own. - * - * `search` is read outside this mode too (objectui#11021): a host that passes - * it owns the search term even where the grid fetches for itself, and the - * grid's own queries carry it. `ListView` hands its toolbar term to a grid - * that groups on the server that way, with no rows. - */ -export interface ObjectGridExternalPaginationProps - extends Partial< - Pick< - DataTableSchema, - // The host's already-fetched window. Highest-priority data source: it - // wins over `schema.data` / `schema.bind` when present. - | 'data' - // Turns off client slicing. With `rowCount` + `onPageChange` it is what - // makes the mode active at all (see `externalManualPagination` below). - | 'manualPagination' - | 'rowCount' - | 'page' - | 'pageSize' - | 'onPageChange' - | 'onPageSizeChange' - | 'sort' - | 'onSortChange' - | 'search' - | 'onSearchChange' - > - > { - /** - * Grid-only: `DataTableSchema` has no counterpart to derive from. - * - * The table vocabulary reports column changes as separate per-event - * callbacks — `onColumnResize(columnKey, width)` and - * `onColumnReorder(newOrder)` — whereas this reports the MERGED, persisted - * `{ order, widths }` layout after ObjectGrid has folded either event into - * the state it also writes to `localStorage`, so a host can save one blob - * (the console stores it on the signed-in user's own list state). Deriving - * it from either table - * callback would misstate both the payload and when it fires. - */ - onColumnStateChange?: (state: ObjectGridColumnState) => void; - - /** - * Grid-only: `DataTableSchema` has no counterpart to derive from — a table is - * handed rows, it never issues a query. - * - * The params the host passed to `dataSource.find()` for the window it is - * handing down in `data` — the SAME shape this grid's own loader stores in - * `lastFindParamsRef` (`$filter` / `$orderby` / `$select` / `$search` / - * `$searchFields` / `$expand` / `$top` / `$skip`), so both paths feed one - * reader. `$top`/`$skip` may be present and are ignored: the fan-out windows - * the replay itself. - * - * Why a prop rather than a fallback inside the grid (objectui#4501): the - * cross-page "select all N matching" escalation re-issues the view's query to - * collect the whole match set, and the query has to come from whichever side - * owns the fetch. Under external pagination that is the host, and the grid's - * own ref is either empty or stale — replaying it asked the server for the - * WHOLE OBJECT (no `$filter`) and handed up to 5000 unmatched records to a - * destructive executor. A grid-side `?? {}` default is what produced that, - * which is why the missing case is not defaulted but REFUSED: with no params - * for the current data path the escalation is not offered at all (see - * `bulkFanoutParams` in the component body). - * - * A changed value is also the host's query-change signal: it resets the - * escalation, mirroring the `setSelectAllMatching(false)` the internal loader - * runs next to its own `lastFindParamsRef` write. Compared by CONTENT, so a - * host re-render that rebuilds an equal object does not drop the user's - * escalation. - */ - findParams?: Record | null; -} - /** * Props of the `ObjectGrid` React component. * @@ -735,32 +617,6 @@ export type ObjectGridDataTableSchemaHolds = { export type ObjectGridDataTableSchema = DeclaredDataTableSchema & ObjectGridDataTableSchemaHolds; -/** - * [objectui#12528] Reports the grid's column summaries to a host that draws - * them in its own footer row (`onColumnSummaryChange`). Renders nothing. - * - * Keyed on the summaries' TEXT, never on the array's identity (AGENTS.md #10): - * every grid render builds a new array, and an equal one must send nothing. - * Unmounting — whatever stops the grid drawing the row — reports `null`. - */ -function FooterSummaryReport({ - summaries, - onChange, -}: { - summaries: ObjectGridFooterSummary[]; - onChange: (summaries: readonly ObjectGridFooterSummary[] | null) => void; -}) { - const key = JSON.stringify(summaries); - const onChangeRef = React.useRef(onChange); - useEffect(() => { - onChangeRef.current = onChange; - }); - useEffect(() => { - onChangeRef.current(JSON.parse(key) as ObjectGridFooterSummary[]); - }, [key]); - useEffect(() => () => onChangeRef.current(null), []); - return null; -} /** * [objectui#13234] The face a date-family CELL paints, for one grouping field: diff --git a/ui/packages/plugin-grid/src/objectGridFooterSummaryReport.tsx b/ui/packages/plugin-grid/src/objectGridFooterSummaryReport.tsx new file mode 100644 index 00000000000..09559973772 --- /dev/null +++ b/ui/packages/plugin-grid/src/objectGridFooterSummaryReport.tsx @@ -0,0 +1,35 @@ +/** + * `FooterSummaryReport`, the renderless reporter `ObjectGrid` (`./ObjectGrid.tsx`) + * mounts to hand its column summaries to a host, moved out of that file + * verbatim (objectstack#25994, slice 2). + */ + +import React, { useEffect } from 'react'; +import type { ObjectGridFooterSummary } from './objectGridProps'; + +/** + * [objectui#12528] Reports the grid's column summaries to a host that draws + * them in its own footer row (`onColumnSummaryChange`). Renders nothing. + * + * Keyed on the summaries' TEXT, never on the array's identity (AGENTS.md #10): + * every grid render builds a new array, and an equal one must send nothing. + * Unmounting — whatever stops the grid drawing the row — reports `null`. + */ +export function FooterSummaryReport({ + summaries, + onChange, +}: { + summaries: ObjectGridFooterSummary[]; + onChange: (summaries: readonly ObjectGridFooterSummary[] | null) => void; +}) { + const key = JSON.stringify(summaries); + const onChangeRef = React.useRef(onChange); + useEffect(() => { + onChangeRef.current = onChange; + }); + useEffect(() => { + onChangeRef.current(JSON.parse(key) as ObjectGridFooterSummary[]); + }, [key]); + useEffect(() => () => onChangeRef.current(null), []); + return null; +} diff --git a/ui/packages/plugin-grid/src/objectGridProps.ts b/ui/packages/plugin-grid/src/objectGridProps.ts new file mode 100644 index 00000000000..9a605b44cd6 --- /dev/null +++ b/ui/packages/plugin-grid/src/objectGridProps.ts @@ -0,0 +1,130 @@ +/** + * The host-facing props types of `ObjectGrid` (`./ObjectGrid.tsx`) that + * `ObjectGridComponentProps` builds on: the persisted column layout, one footer + * summary, and the host-driven pagination props, moved out of that file + * verbatim (objectstack#25994, slice 2). `ObjectGrid.tsx` re-exports every + * name it exported before. + */ + +import type { DataTableSchema } from '@object-ui/types'; + +/** + * The column layout ObjectGrid persists and reports back — the merged result of + * a resize and a reorder, not either event on its own. Named so the state hook + * below and the `onColumnStateChange` prop cannot drift apart. + */ +export interface ObjectGridColumnState { + order?: string[]; + widths?: Record; +} + +/** + * One column summary as the grid's footer would draw it — `header: label`, + * e.g. `Annual Revenue` and `Sum: 109,800,000.00` — handed to a host through + * {@link ObjectGridComponentProps.onColumnSummaryChange} (objectui#12528). + */ +export interface ObjectGridFooterSummary { + /** The column's accessor key. */ + key: string; + /** The column's header text. */ + header: string; + /** The formatted summary, scope included (`Sum: 109,800,000.00`). */ + label: string; +} + +/** + * The HOST-DRIVEN ("external") mode of ObjectGrid — framework#2212. + * + * The ordinary authoring surface hands ObjectGrid a `schema` and a `dataSource` + * and lets it fetch, page, sort and search for itself. In this mode a host + * (ListView, a designer preview, an app screen with its own toolbar) has + * already fetched one window of a larger collection and drives the controls + * itself: it passes the window as `data` plus the real match total and the + * page/sort/search state, and ObjectGrid forwards them straight to its + * DataTable instead of client-slicing the window it was handed. + * + * Kept as its own named interface rather than flattened into `ObjectGridComponentProps` + * (#4277 裁决 B, 2026-08-11): the two are different classes of contract, and a + * dozen more members merged into the authoring surface would erase that + * boundary. Until this existed, every member below was read out of `...rest` + * through an `as any` cast and was declared nowhere at all. + * + * DERIVATION (#4277 裁决 §3, the anti-drift pin): this vocabulary is already + * declared once, on `DataTableSchema` — which is exactly where ObjectGrid + * forwards it — so the members that have a counterpart there are TYPE-DERIVED + * from that declaration rather than hand-copied into a second enumeration. Two + * hand-written copies of one vocabulary is how the next drift happens. The + * `Partial<...>` wrapper is deliberate and is the only shape change: the whole + * mode is opt-in, and `DataTableSchema['data']` is required because a table + * always has rows, while a grid that was given no `data` fetches its own. + * + * `search` is read outside this mode too (objectui#11021): a host that passes + * it owns the search term even where the grid fetches for itself, and the + * grid's own queries carry it. `ListView` hands its toolbar term to a grid + * that groups on the server that way, with no rows. + */ +export interface ObjectGridExternalPaginationProps + extends Partial< + Pick< + DataTableSchema, + // The host's already-fetched window. Highest-priority data source: it + // wins over `schema.data` / `schema.bind` when present. + | 'data' + // Turns off client slicing. With `rowCount` + `onPageChange` it is what + // makes the mode active at all (see `externalManualPagination` below). + | 'manualPagination' + | 'rowCount' + | 'page' + | 'pageSize' + | 'onPageChange' + | 'onPageSizeChange' + | 'sort' + | 'onSortChange' + | 'search' + | 'onSearchChange' + > + > { + /** + * Grid-only: `DataTableSchema` has no counterpart to derive from. + * + * The table vocabulary reports column changes as separate per-event + * callbacks — `onColumnResize(columnKey, width)` and + * `onColumnReorder(newOrder)` — whereas this reports the MERGED, persisted + * `{ order, widths }` layout after ObjectGrid has folded either event into + * the state it also writes to `localStorage`, so a host can save one blob + * (the console stores it on the signed-in user's own list state). Deriving + * it from either table + * callback would misstate both the payload and when it fires. + */ + onColumnStateChange?: (state: ObjectGridColumnState) => void; + + /** + * Grid-only: `DataTableSchema` has no counterpart to derive from — a table is + * handed rows, it never issues a query. + * + * The params the host passed to `dataSource.find()` for the window it is + * handing down in `data` — the SAME shape this grid's own loader stores in + * `lastFindParamsRef` (`$filter` / `$orderby` / `$select` / `$search` / + * `$searchFields` / `$expand` / `$top` / `$skip`), so both paths feed one + * reader. `$top`/`$skip` may be present and are ignored: the fan-out windows + * the replay itself. + * + * Why a prop rather than a fallback inside the grid (objectui#4501): the + * cross-page "select all N matching" escalation re-issues the view's query to + * collect the whole match set, and the query has to come from whichever side + * owns the fetch. Under external pagination that is the host, and the grid's + * own ref is either empty or stale — replaying it asked the server for the + * WHOLE OBJECT (no `$filter`) and handed up to 5000 unmatched records to a + * destructive executor. A grid-side `?? {}` default is what produced that, + * which is why the missing case is not defaulted but REFUSED: with no params + * for the current data path the escalation is not offered at all (see + * `bulkFanoutParams` in the component body). + * + * A changed value is also the host's query-change signal: it resets the + * escalation, mirroring the `setSelectAllMatching(false)` the internal loader + * runs next to its own `lastFindParamsRef` write. Compared by CONTENT, so a + * host re-render that rebuilds an equal object does not drop the user's + * escalation. + */ + findParams?: Record | null; +} From f1e4df4c15df76f4cb1c147ef17550de8326e0ec Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 22:43:57 +0000 Subject: [PATCH 2/3] chore(sdui): re-record plugin-grid sources after the ObjectGrid slice 2 move Claude-Session: https://claude.ai/code/session_01Bdrd2yH4eqnz2QobtN3haE Co-authored-by: Claude --- scripts/sdui-manifest.sources/plugin-grid.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdui-manifest.sources/plugin-grid.json b/scripts/sdui-manifest.sources/plugin-grid.json index be3fa2493dc..9e85d6c9ec0 100644 --- a/scripts/sdui-manifest.sources/plugin-grid.json +++ b/scripts/sdui-manifest.sources/plugin-grid.json @@ -1 +1 @@ -{"package":"ui/packages/plugin-grid","sources":44,"sha256":"afbc05964d76c725b0d533f1bd28b1515591d0748eca468158f64fc3bb7f7a80","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} +{"package":"ui/packages/plugin-grid","sources":46,"sha256":"2c2577a854216b798b363a8751d8924e3e9205404c0eb592b5f3d53197cd3bc0","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} From ba1445b9b677f7622ec77408421ae26382284257 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 22:56:17 +0000 Subject: [PATCH 3/3] chore(changeset): keep slice 1's note as it is; state its correction in slice 2's note Claude-Session: https://claude.ai/code/session_01Bdrd2yH4eqnz2QobtN3haE Co-authored-by: Claude --- .changeset/25994-objectgrid-helpers-split.md | 2 +- .changeset/25994-objectgrid-props-split.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/25994-objectgrid-helpers-split.md b/.changeset/25994-objectgrid-helpers-split.md index 186a20f5da5..ac43043e80e 100644 --- a/.changeset/25994-objectgrid-helpers-split.md +++ b/.changeset/25994-objectgrid-helpers-split.md @@ -7,6 +7,6 @@ refactor(plugin-grid): `ObjectGrid`'s module-level helpers move into sibling mod Clause-②: no Metadata-protocol: none -The query helpers (the held filter resolution and the find-params signature), the column helpers and column emit types, the record-link cell, the English fallback translations, the display defaults (row height, page sizes, selection default) and the row-version helpers move verbatim out of `ObjectGrid.tsx` into six modules beside it. No logic, name or rendered output changes. `ObjectGrid.tsx` re-exports every name it exported before, and the package entry is unchanged. +The query helpers (the held filter resolution, the record-source ladder, the find-params signature), the column helpers and column emit types, the record-link cell, the English fallback translations, the display defaults (row height, page sizes, selection default) and the row-version helpers move verbatim out of `ObjectGrid.tsx` into six modules beside it. No logic, name or rendered output changes. `ObjectGrid.tsx` re-exports every name it exported before, and the package entry is unchanged. What to do: nothing. diff --git a/.changeset/25994-objectgrid-props-split.md b/.changeset/25994-objectgrid-props-split.md index a139bb90fa9..5edba09f8b0 100644 --- a/.changeset/25994-objectgrid-props-split.md +++ b/.changeset/25994-objectgrid-props-split.md @@ -9,4 +9,6 @@ Metadata-protocol: none `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. +Correction to the slice 1 note: the record-source ladder (`getDataConfig`) stayed in `ObjectGrid.tsx`; it moved in neither slice. + What to do: nothing.