Skip to content

Commit 7158025

Browse files
refactor(plugin-grid): move ObjectGrid's module-level helpers into sibling modules (slice 1) (#26323)
<!-- 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 1 of the move-only split; the card stays open for the later slices). Clause-②: no Metadata-protocol: none ## What moved Base `69aa630b`. This slice moves module-level helpers, types and constants, verbatim, out of `ui/packages/plugin-grid/src/ObjectGrid.tsx` into six sibling modules: - `objectGridQuery.ts`: `AuthoredGridFilters`, `HeldGridFilters`, `resolveGridFilters`, `useResolvedGridFilters`, `findParamsSignature` - `objectGridColumns.ts`: `CHIP_FIELD_TYPES`, `isChipFieldType`, `resolveColumnLabel`, `normalizeColumns`, and the column emit types with their docblocks (`RetiredListColumnKey`, `ObjectGridColumnHolds`, `ObjectGridRetiredOptionsTombstone`, `ObjectGridColumnDraft`, `ObjectGridColumn`) - `objectGridLinkCell.tsx`: `LINK_CELL_CLASS`, `LINK_CELL_CLASS_WRAP`, `rowRecordId`, `LinkCell`, `recordPointerFace` - `objectGridTranslations.ts`: `GRID_DEFAULT_TRANSLATIONS`, `useGridTranslation` - `objectGridDefaults.ts`: `RowHeightMode`, `resolveRowHeightMode`, `readSpecDisplayPageSize`, `DEFAULT_DISPLAY_PAGE_SIZE`, `DEFAULT_FETCH_BATCH_SIZE`, `DEFAULT_SELECTION_TYPE`, `readAuthoredPageSize`, `isUsablePageSize`, `resolvePageSize`, `describeNonPositivePageSize`, `NO_GROUPING_REFUSAL` - `objectGridRowVersion.ts`: `OCC_TOKEN_FIELD`, `rowVersionOf`, `WrittenRowVersion`, `rowVersionToSend`, `RowConflictChoice`, `BatchSaveOutcome` 898 lines move. `ObjectGrid.tsx` goes from 7689 to 6781 lines. Names the component still reads gain the `export` keyword in their new module. `ObjectGrid.tsx` imports them back and re-exports the four column emit types it exported before (`RetiredListColumnKey`, `ObjectGridColumnHolds`, `ObjectGridColumnDraft`, `ObjectGridColumn`), so every name it exported is still importable from the same path. The package entry (`index.tsx`) is untouched. The import specifiers whose only readers moved now live in the new modules. 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 six modules (each module's header comment dropped, added `export` keywords normalised). Nothing is missing. The only other lines are import lines and the one type re-export line. ## What stays in `ObjectGrid.tsx` on purpose - The group-heading date region (`TemporalGroupFace`, `temporalGroupHeading`): #25884 is editing it. - `parseSchemaSort` (and its sibling `toOrderByClause`): `gridArrayArmOrderby-8973.test.tsx` reads `ObjectGrid.tsx` and asserts `export function parseSchemaSort` is in it. - The public props types, `ObjectGridComponentProps` among them: `bulkExecutorDataSourceFace-9722.test.ts` asserts `export interface ObjectGridComponentProps` is in the file. - The schema-slot types (`RemoveIndexSignature`, `DeclaredDataTableSchema`, `ObjectGridDataTableSchemaHolds`, `ObjectGridDataTableSchema`): `cellClassNameCensusProse-6921.test.ts` reads the census prose in their docblock from `ObjectGrid.tsx`. - The relational-copy-set docblock and `FooterSummaryReport`. - `getDataConfig`, kept file-local as on `main` (commit 2978029). Exported from a module, it becomes a shared reader for `check:doc-example-readers`, which then flags core's `record-source.ts` `@example`. Re-pointing those scans would be a test-logic edit, which this card does not allow without the seat's word, so this slice leaves them where they are. ## Test edits None. ## Verification Run on head `fe3087e5`, before the merge of `main`: - `pnpm --dir ui exec vitest run packages/plugin-grid/ --maxWorkers=2`, the whole package suite: 251 of 251 files and 2247 of 2247 tests passed on the base files, and the same 251 / 2247 passed after the move. - 52 test files outside plugin-grid that read sources (every test naming plugin-grid that scans source, every test spelling `ObjectGrid.tsx`, plus `raw-key-call-sites-3546` and `defaults-maps-mirror-en-pack`): 52 of 52 files, 2257 of 2257 tests passed. - `pnpm --filter @object-ui/plugin-grid type-check` exit 0; `tsc --listFiles` includes all seven files. - eslint on the seven files: 0 errors. - `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 (38 to 44 sources). - `node scripts/pm/dispatch-gates.mjs --commands`: 41 derived gates, all exit 0; `--ran` reconciles 41 of 41. After merging `main` (`f00cbe36`, 11 commits, none in `ObjectGrid.tsx`): `check-sdui-manifest` is green and the Console builds. The full readings are in the os-dev-report comment on the card. ## Acceptance notes - Some moved prose still says "below" or "above", or "this file", about code that stays in `ObjectGrid.tsx` (for example the column emit docblock's "`generateColumns()` below"). It is kept verbatim because the slice is move-only. - `objectGridLinkCell.tsx` exports `rowRecordId` and `recordPointerFace` beside the `LinkCell` component, so eslint's `react-refresh/only-export-components` warns twice there. It is a warning, not an error. ## Landing This enqueues only after #25884 merges. Expect a merge of `main` and a re-record of `plugin-grid.json` before it lands. Changed lines are about 2,000, under the 3,000 line limit. Changeset: `.changeset/25994-objectgrid-helpers-split.md` (patch, `@object-ui/plugin-grid`). --- _Generated by [Claude Code](https://claude.ai/code/session_01Bdrd2yH4eqnz2QobtN3haE)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b9cd980 commit 7158025

9 files changed

Lines changed: 993 additions & 922 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/plugin-grid': patch
3+
---
4+
5+
refactor(plugin-grid): `ObjectGrid`'s module-level helpers move into sibling modules
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
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.
11+
12+
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":38,"sha256":"f6f7b1cbd89a1536a6e33febf4db131b8d526bd7281be460c401dccb8e97876a","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
1+
{"package":"ui/packages/plugin-grid","sources":44,"sha256":"afbc05964d76c725b0d533f1bd28b1515591d0748eca468158f64fc3bb7f7a80","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}

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

Lines changed: 13 additions & 921 deletions
Large diffs are not rendered by default.

‎ui/packages/plugin-grid/src/objectGridColumns.ts‎

Lines changed: 304 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/**
2+
* The display defaults of `ObjectGrid` (`./ObjectGrid.tsx`): the row-height
3+
* resolver, the page sizes, the selection default and the no-refusal
4+
* grouping signature, moved out of that file verbatim (objectstack#25994,
5+
* slice 1).
6+
*/
7+
8+
import { PaginationConfigSchema } from '@objectstack/spec/ui';
9+
import { ROW_HEIGHT_TO_DENSITY_MODE } from '@object-ui/core';
10+
11+
/** The row heights this grid styles — the five `RowHeight` values the spec admits. */
12+
export type RowHeightMode = 'compact' | 'short' | 'medium' | 'tall' | 'extra_tall';
13+
14+
/**
15+
* The ONE answer this component gives for a `rowHeight` it does not recognize
16+
* (objectui#4443).
17+
*
18+
* The seed used to be `schema.rowHeight ?? 'compact'`, which made the component
19+
* answer the same question two ways: an ABSENT `rowHeight` landed on `compact`,
20+
* an OFF-SPEC one fell through the density ternaries below to their terminal
21+
* `else` — the `medium` styling. That is the absent-vs-off-spec split #4440
22+
* removed from `ListView`, and a third answer to a question `@object-ui/core`
23+
* (`rowHeightToDensityMode`, which abstains) and the `@object-ui/react` spec
24+
* bridge (#4352, which abstains) had already settled. One metadata-driven
25+
* system, one answer: off-spec renders exactly like absent.
26+
*
27+
* The ternary chains are deliberately NOT touched — `medium` is a real value
28+
* with its own arm, and a leaf renderer's terminal `else` is legitimate styling.
29+
* Narrowing happens here, at the boundary, so nothing off-spec ever reaches it.
30+
*
31+
* Membership is tested against `ROW_HEIGHT_TO_DENSITY_MODE` rather than a local
32+
* list so the admitted values have one definition in the repo; that table is
33+
* typed `Record<RowHeight, DensityMode>`, so the build fails if the spec grows a
34+
* sixth row height and this resolver is not taught about it.
35+
*
36+
* `hasOwnProperty`, not `in`: `in` walks the prototype chain, so `'toString'`
37+
* would come back admitted. That is not hypothetical here — the toolbar's icon
38+
* map is looked up by the same key, and a prototype member reached
39+
* `Object.prototype.toString` and rendered it as a React component, while a
40+
* plain off-spec value produced `undefined` and threw outright.
41+
*/
42+
export function resolveRowHeightMode(rowHeight: unknown): RowHeightMode {
43+
if (typeof rowHeight !== 'string') return 'compact';
44+
if (!Object.prototype.hasOwnProperty.call(ROW_HEIGHT_TO_DENSITY_MODE, rowHeight)) return 'compact';
45+
return rowHeight as RowHeightMode;
46+
}
47+
48+
/**
49+
* The page size every display surface of this component falls back to when the
50+
* author declared none (objectui#9853, rulings 5749197629 and 5824040487): the
51+
* flat table, the server-paged table, the grouped view's page of groups, and a
52+
* server-grouped leaf's page of rows. Wherever there is a pager, this is its
53+
* page.
54+
*
55+
* READ from `@objectstack/spec`, not restated. The protocol's pagination config
56+
* declares `pageSize` with a default, and that declaration is the one answer an
57+
* author can look up; a literal here would be a second answer free to drift
58+
* from it. Parsing an empty object is the spec's own way of saying what an
59+
* undeclared member means, so this is the value the schema would fill in.
60+
*
61+
* ⚠️ There is deliberately no local fallback number behind this read. If a
62+
* future `@objectstack/spec` stops declaring the default (the same principle
63+
* `packages/types` applies at its own import boundary is that a validator does
64+
* not write values), this throws at module load instead of quietly
65+
* substituting a number nobody declared. The spec version is locked, so that
66+
* can only surface on a spec upgrade, where the pin
67+
* `ObjectGrid.displayPageSizeDefault-9853.test.tsx` reddens first.
68+
*/
69+
function readSpecDisplayPageSize(): number {
70+
const declared: unknown = PaginationConfigSchema.parse({}).pageSize;
71+
if (typeof declared !== 'number' || !Number.isInteger(declared) || declared <= 0) {
72+
throw new Error(
73+
'[ObjectUI] ObjectGrid: @objectstack/spec no longer declares a positive default '
74+
+ `for pagination.pageSize (read ${String(declared)}); the grid has no display `
75+
+ 'page size to fall back to.',
76+
);
77+
}
78+
return declared;
79+
}
80+
export const DEFAULT_DISPLAY_PAGE_SIZE = readSpecDisplayPageSize();
81+
82+
/**
83+
* How many rows this grid FETCHES when it buckets a window into groups in the
84+
* browser and the author declared no page size (`bucketsFetchedWindow`). A
85+
* fetch batch, ⛔ not a page size: no display path reads it, and it does not
86+
* follow the display default (objectui#9853, ruling 5824040487, structure B).
87+
* Before ruling 5749197629 this number was named as a "server window" and also
88+
* sized the server-paged table's visible page, so one constant meant two
89+
* quantities.
90+
*
91+
* It stays a constant of this component rather than the protocol's value
92+
* because the protocol declares no fetch batch: it is how much of the result
93+
* set a client-grouped view holds, which is an implementation choice.
94+
*/
95+
export const DEFAULT_FETCH_BATCH_SIZE = 50;
96+
97+
/**
98+
* The mode a `selection` object with no `type` member asks for (objectui#9837,
99+
* ruling A-prime: presence enables, an explicit off wins).
100+
*
101+
* ⚠️ Derived, not chosen freely. The ruling's first clause is that writing the
102+
* object turns selection ON, so the only values that satisfy it are the two
103+
* enabling ones — and `'multiple'` is already this renderer's own answer to
104+
* "selection is on, nothing said which kind": it is what the bulk-action
105+
* auto-enable arm resolves to, and what the legacy `selectable: true` means
106+
* (`packages/types/src/data-display.ts`, "boolean: Enable/disable selection
107+
* (true = multiple selection)"). `'single'` would be a third, unstated opinion.
108+
*
109+
* ⚠️ `@objectstack/spec`'s own `SelectionConfigSchema` declares `type` with
110+
* `.default('none')`, which this constant deliberately does NOT follow: honouring
111+
* it would make a written object mean OFF and contradict the ruling's first
112+
* clause outright. It is reachable only through a PARSE, and this repo's mirror
113+
* strips imported defaults at the import boundary
114+
* (`packages/types/src/zod/imported-defaults.ts`), so an omitted `type` stays
115+
* omitted on the way to this read — but metadata parsed by the spec bundle
116+
* itself arrives carrying `type: 'none'` and is then an EXPLICIT off here.
117+
* Handed back as a question on objectui#9837 rather than decided here.
118+
*/
119+
export const DEFAULT_SELECTION_TYPE = 'multiple' as const;
120+
121+
/**
122+
* The ONE resolver for an authored page size, for the reason the `rowHeight`
123+
* resolver just above exists: one resolver at every entry is what keeps the
124+
* answer single (objectui#4443).
125+
*
126+
* Before objectui#9853 this value was spelled out separately at each of its
127+
* three read points, and the spellings disagreed about a non-positive number:
128+
* the flat site used `||`, so `0` was falsy and fell through to a default,
129+
* while both seeds used `??`, so `0` was not nullish and survived as a real
130+
* page size. It then divided the grouped pager and reached the wire as
131+
* `$top: 0`, so the grid asked the server for nothing and drew an empty table
132+
* — with no error, no warning and no empty state naming the cause.
133+
*
134+
* ## Why refusing `0` is not this renderer inventing a meaning
135+
*
136+
* `@objectstack/spec` has already answered what a `pageSize` of `0` means.
137+
* Its pagination config declares the member as a POSITIVE integer with a
138+
* default, and the spec's own suite pins that refusal under the names
139+
* `should reject zero pageSize` and `should reject negative pageSize`. The
140+
* `limit` that this block's `ElementDataSourceMapping` lowers
141+
* `pagination.pageSize` into is declared positive as well. So `0` is not a
142+
* spelling whose meaning a consumer may choose; it is a value the contract
143+
* refuses, and a renderer that quietly divides by it is the only party not
144+
* saying so.
145+
*
146+
* ⚠️ The refusal is FAIL-SOFT on purpose. Throwing would take out the whole
147+
* subtree for a declaration the flat path already tolerated, which is a worse
148+
* outcome than the defect. The value is dropped, the site's own default is
149+
* used, and `describeNonPositivePageSize` states it once through the channel
150+
* this component already uses for "you declared it, the renderer dropped it".
151+
*
152+
* Reads the canonical key first and the deprecated flat shorthand second, in
153+
* that precedence, with `??` so an explicit `0` is SEEN by the guard instead
154+
* of skipped by falsiness — that skipping is the defect, not the fix.
155+
*/
156+
function readAuthoredPageSize(schema: {
157+
pagination?: unknown;
158+
pageSize?: unknown;
159+
}): unknown {
160+
const fromObject = (schema.pagination as { pageSize?: unknown } | undefined)?.pageSize;
161+
return fromObject ?? schema.pageSize;
162+
}
163+
164+
function isUsablePageSize(value: unknown): value is number {
165+
return typeof value === 'number' && Number.isInteger(value) && value > 0;
166+
}
167+
168+
export function resolvePageSize(
169+
schema: { pagination?: unknown; pageSize?: unknown },
170+
fallback: number,
171+
): number {
172+
const authored = readAuthoredPageSize(schema);
173+
return isUsablePageSize(authored) ? authored : fallback;
174+
}
175+
176+
/**
177+
* The diagnostic half. `null` means "nothing to say" — an absent key is not a
178+
* mistake, and a usable page size is not either, so the message is CONDITIONAL
179+
* and a control asserting its silence is what keeps it from being an
180+
* always-on marker that states nothing.
181+
*/
182+
export function describeNonPositivePageSize(
183+
schema: { pagination?: unknown; pageSize?: unknown },
184+
context: { blockType?: unknown; objectName?: unknown },
185+
): string | null {
186+
const authored = readAuthoredPageSize(schema);
187+
if (authored === undefined || authored === null) return null;
188+
if (isUsablePageSize(authored)) return null;
189+
const where = [
190+
typeof context.blockType === 'string' ? context.blockType : 'object-grid',
191+
typeof context.objectName === 'string' ? context.objectName : undefined,
192+
]
193+
.filter(Boolean)
194+
.join(' on ');
195+
return (
196+
`[ObjectUI] ObjectGrid pagination: ${where} declared pageSize: ${String(authored)}, `
197+
+ 'which is not a positive integer. A page size must be a positive integer '
198+
+ '(the spec refuses zero and negative values), so it was ignored and this '
199+
+ 'grid fell back to its default page size.'
200+
);
201+
}
202+
203+
/**
204+
* The grouping-refusal signature when no entry is refused (objectui#10583,
205+
* objectui#10657): the authored grouping config is then handed on as it is.
206+
*/
207+
export const NO_GROUPING_REFUSAL = JSON.stringify({ refused: [], warned: [] });

0 commit comments

Comments
 (0)