Skip to content

Commit 7bdff7e

Browse files
refactor(plugin-grid): move ObjectGrid's row-height sync, column state and mobile affordances into sibling hooks (slice 7) (#26773)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8KF8i7bmUmRJW1rodBW4KnP)_ Part of #25994 (slice 7 of the move-only split: regions C, D and E. Slices 1 to 6 have landed; slice 8, regions R, U and V, stays on the card and closes it.) Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:把 `ObjectGrid.tsx` 组件顶部三段代码(行高同步、列状态持久化、下拉刷新与卡片视图断点)原样搬进三个同目录 hook 文件,组件在原位置调用它们。 - 为什么改:维护者裁决「大文件互踩: 立重构卡把 ObjectView 和 ObjectGrid 拆成小模块。这个可以立卡」;这是第 7 片,文件从 5250 行降到 5160 行,并行卡更少撞在同一文件里。 - 风险与回滚:纯搬移,无逻辑、命名、渲染变化,hook 调用顺序不变;有问题直接 revert 本 PR 即可。 - 先看:`ui/packages/plugin-grid/src/objectGridColumnState.ts`(NON-AUTHOR SURFACE 注释仍紧贴 `columnState` 种子)、`ObjectGrid.tsx` 里三处调用、`gridNonAuthorKeys.test.tsx` 的路径改动。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/plugin-grid/ --maxWorkers=2`,预期 253 个文件、2256 个测试全绿,与 main 相同。 - 需要你做的:无,席位落地。 ## What changes Base `5cc31c78` (`origin/main`). `ui/packages/plugin-grid/src/ObjectGrid.tsx` is 5250 lines there. This slice moves regions **C**, **D** and **E** of the seam table in #26364, the three adjacent blocks at the top of the component body, verbatim into three custom hooks in new sibling modules: | # | Concern | New home | Inputs | Returns | |---|---|---|---|---| | C | Row-height sync: the effect that follows a changed `schema.rowHeight` | `useObjectGridRowHeightSync` in `objectGridRowHeight.ts` | `schema`, `rowHeightMode`, `setRowHeightMode` | nothing | | D | Column-state persistence: `columnStorageKey`, the `columnState` seed with its NON-AUTHOR SURFACE comment, the external `columnState` sync, `saveColumnState` | `useObjectGridColumnState` in `objectGridColumnState.ts` | `schema`, `onColumnStateChange` | `columnState`, `saveColumnState` | | E | Pull to refresh and the card-view breakpoint effect | `useObjectGridMobile` in `objectGridMobile.ts` | `schema`, `dataSource`, `setRefreshKey`, `setUseCardView` | `pullRef`, `isRefreshing`, `pullDistance` | - `ObjectGrid` calls the three hooks exactly where the blocks stood, in the same order, so the component's hook order does not change. The state the blocks read or set (`rowHeightMode`, `refreshKey`, `useCardView`) stays in the component, and its setters are passed in. - Inputs are typed from their source: setters as React's `Dispatch` of `SetStateAction` over the state's own type, `onColumnStateChange` as `ObjectGridExternalPaginationProps['onColumnStateChange']`, `dataSource` as `DataSource | undefined`. No context and no new `any`; the four `(schema as any).columnState` reads move as they were. - Each hook returns only the names the rest of the component reads. `columnStorageKey`, `setColumnState`, `externalColumnStateKey` and `handlePullRefresh` have no reader outside their block, so they stay inside the hooks. - Imports: `usePullToRefresh` moves with its only reader; the `type ObjectGridColumnState` import specifier leaves `ObjectGrid.tsx` (its only reader moved). The `export type { ObjectGridColumnState, … }` re-export is untouched, so the package entry and every exported name are unchanged. - `ObjectGrid.tsx` goes from 5250 to 5160 lines. The diff is 307 changed lines in 6 files. **Move-only proof.** The multiset of non-blank lines of the base `ObjectGrid.tsx` equals the new `ObjectGrid.tsx` plus the three modules. Missing: 1 line, the edited `./objectGridProps` import (it only lost the `type ObjectGridColumnState` specifier). Added: 81 lines, all of them the three module headers and docblocks, their imports, the three hook signatures and parameter types, the two `return { … }` lists, and in `ObjectGrid.tsx` the three new imports plus the three calls with their comments. **No open PR touches these files.** I checked the 18 open PRs at the claim: only #26556 touches plugin-grid, and only its `package.json`. ## Why The card's ruling, verbatim: 「大文件互踩: 立重构卡把 ObjectView 和 ObjectGrid 拆成小模块。这个可以立卡」. One move-only slice per PR, per the card's ask 1 to 5 and the hand-off notes on the card. ## Tests One test edit, path-only, forced by the move: - `ui/packages/plugin-grid/src/__tests__/gridNonAuthorKeys.test.tsx`: the control case "and the four keys ruled in objectui#5091 still carry theirs" now reads `ObjectGrid.tsx` and `objectGridColumnState.ts` joined, because the NON-AUTHOR SURFACE comment on `columnState` moved with the seed it documents. The `onNavigate: onNavigate ?? schema.onNavigate` read and its comment block stay in `ObjectGrid.tsx`, so the `gridLines` reads are unchanged. Assertions unchanged; one new path constant, one changed line. No other test reads the moved lines as source. `columnStatePersistence` and `rowHeightOffSpecBoundary` are behavioural and render the real `ObjectGrid`, so they cover the hooks. I matched every string literal of every tracked test and script against the lines that left `ObjectGrid.tsx`; the hits outside `gridNonAuthorKeys` were generic words (`innerWidth`, `JSON.parse`, `@object-ui/mobile`) in behavioural tests or in gates that scan their own surfaces, and all of those ran green below. ## Acceptance notes - Hook boundaries hide React lint findings, as on slices 4 to 6 (trap 3 of the hand-off). eslint on the four files: 0 errors; warnings 188 before, 189 after. `react-hooks/set-state-in-effect` on `setRowHeightMode` is no longer raised, because the rule only knows setters declared by `useState` in the same function: hidden, not fixed. `react-hooks/exhaustive-deps` now names `setRefreshKey` (the pull-to-refresh `useCallback`) and `setUseCardView` (the breakpoint effect) as missing dependencies, for the same reason: a setter passed in as a hook parameter is not known to be stable. No dependency array changed. The other messages are the same findings at shifted lines or in the new module. - Moved comments still say "`saveColumnState` below" and "the seed above" inside the new module, where they remain true; they are kept verbatim. - The failure message of the re-pointed control case still says "gone from ObjectGrid.tsx"; it is left unchanged so the test edit stays path-only. ## Verification Run on head `02d40dc8`: - `pnpm --dir ui exec vitest run packages/plugin-grid/ --maxWorkers=2` under the verification lock: 253 of 253 files, 2256 of 2256 tests after the move. Before: the same command on base `5cc31c78` (a separate worktree, deps built): 253 of 253 files, 2256 of 2256 tests. - The 108 test files outside plugin-grid that read source trees (every ui test that reads files and names plugin-grid or walks the package roots): 108 of 108 files, 3152 of 3152 tests. - `pnpm --filter @object-ui/plugin-grid type-check` exit 0, after `turbo run build --filter='@object-ui/plugin-grid^...'` (48 of 48 tasks). - The Type Check UI job's 17 `check:*` steps plus `type-check:coverage`: all exit 0 (`check:spec-symbols` and `check:doc-example-readers` included; no new local type shares a spec name, and no single-return wrapper is exported). - `pnpm --dir ui run check:new-line-citations`: 0 new citations. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 35 derived commands, all exit 0 (`pnpm --dir ui exec vitest run scripts/` under the lock: 178 passed and 2 skipped files, 5065 passed and 2 skipped tests); `--ran` reconciles 35 of 35 (0 NOT-MEASURED, 0 UNRUN); the 18 Type Check UI commands above are recorded as outside the derivation. Changeset: `.changeset/25994-objectgrid-column-state-mobile-hooks.md` (patch, `@object-ui/plugin-grid`). ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; eslint on the touched files reports 0 errors. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the maintainer quick-read section and the quoted ruling are Chinese by rule). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JQwxbXqCPeUA7ztE9YWKwP --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbXqCPeUA7ztE9YWKwP)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a5df094 commit 7bdff7e

6 files changed

Lines changed: 204 additions & 103 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 row-height sync, column state and mobile affordances move into three sibling hooks
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Three adjacent blocks at the top of the `ObjectGrid` component move verbatim out of `ObjectGrid.tsx`. The effect that follows a changed `schema.rowHeight` goes to `useObjectGridRowHeightSync` (`objectGridRowHeight.ts`). The column-state persistence (the storage key, the `columnState` seed, the sync of a host-supplied `columnState` and `saveColumnState`) goes to `useObjectGridColumnState` (`objectGridColumnState.ts`). Pull to refresh and the card-view breakpoint go to `useObjectGridMobile` (`objectGridMobile.ts`). `ObjectGrid` calls each hook where its block stood, so its hook order, its stored column state and its rendered output do not change. No name is renamed, and the package entry is unchanged.
11+
12+
What to do: nothing.

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

Lines changed: 12 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ import {
5757
HorizontalOverflowBox, PINNED_RIGHT_SEAM_SHADOW_CLASSES,
5858
toast,
5959
} from '@object-ui/components';
60-
import { usePullToRefresh } from '@object-ui/mobile';
6160
import { resolveConditionalFormatting, isDeadlineSettled, leadWithNameField, declaredNameField, buildExportFileName, columnIdentity, resolveAffordance, resolveFieldAffordance, type SchemaLike, isExpandableFieldType, isUnmaterializedFieldType, readObjectSortability, isPlatformSortableField, filterPlatformSortableSort, filterRefusalSubject, FilterOperatorError, resolveRecordSourceConfig, resolveRecordSourceObjectName, partitionRowsByPredicate } from '@object-ui/core';
6261
import { usePermissions } from '@object-ui/permissions';
6362
import {
@@ -88,14 +87,17 @@ import { LinkCell, recordPointerFace, rowRecordId } from './objectGridLinkCell';
8887
import { useGridTranslation } from './objectGridTranslations';
8988
import { DEFAULT_DISPLAY_PAGE_SIZE, DEFAULT_SELECTION_TYPE, type RowHeightMode, resolvePageSize, resolveRowHeightMode } from './objectGridDefaults';
9089
import { type BatchSaveOutcome, type RowConflictChoice, type WrittenRowVersion, rowVersionOf, rowVersionToSend } from './objectGridRowVersion';
91-
import { type ObjectGridColumnState, type ObjectGridExternalPaginationProps, type ObjectGridFooterSummary } from './objectGridProps';
90+
import { type ObjectGridExternalPaginationProps, type ObjectGridFooterSummary } from './objectGridProps';
9291
import { FooterSummaryReport } from './objectGridFooterSummaryReport';
9392
import { useObjectGridDiagnostics } from './objectGridDiagnostics';
9493
import { useObjectGridQueryInputs } from './objectGridQueryInputs';
9594
import { useObjectGridGroupingDecisions } from './objectGridGroupingDecisions';
9695
import { useObjectGridRowLoad } from './objectGridRowLoad';
9796
import { temporalGroupHeading, useObjectGridGrouping } from './objectGridGrouping';
9897
import { useObjectGridSummaries } from './objectGridSummaries';
98+
import { useObjectGridRowHeightSync } from './objectGridRowHeight';
99+
import { useObjectGridColumnState } from './objectGridColumnState';
100+
import { useObjectGridMobile } from './objectGridMobile';
99101
export type { RetiredListColumnKey, ObjectGridColumnHolds, ObjectGridColumnDraft, ObjectGridColumn } from './objectGridColumns';
100102
export type { ObjectGridColumnState, ObjectGridFooterSummary, ObjectGridExternalPaginationProps } from './objectGridProps';
101103

@@ -678,110 +680,18 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
678680
resolvePageSize(schema, DEFAULT_DISPLAY_PAGE_SIZE),
679681
);
680682

681-
// Sync internal rowHeightMode when schema.rowHeight prop changes (e.g., parent ListView density toggle).
682-
// Routed through the same resolver as the seed above: this is the component's
683-
// second entry point for an author-supplied `rowHeight`, and one resolver at
684-
// every entry is what keeps the answer single (objectui#4443).
685-
React.useEffect(() => {
686-
if (!schema.rowHeight) return;
687-
const next = resolveRowHeightMode(schema.rowHeight);
688-
if (next !== rowHeightMode) {
689-
setRowHeightMode(next);
690-
}
691-
// eslint-disable-next-line react-hooks/exhaustive-deps
692-
}, [schema.rowHeight]);
683+
// The row-height sync (`./objectGridRowHeight`), run here in hook order.
684+
useObjectGridRowHeightSync({ schema, rowHeightMode, setRowHeightMode });
693685

694-
// Column state persistence (order and widths)
695-
const columnStorageKey = React.useMemo(() => {
696-
return schema.id
697-
? `grid-columns-${schema.objectName}-${schema.id}`
698-
: `grid-columns-${schema.objectName}`;
699-
}, [schema.objectName, schema.id]);
686+
// Column state persistence: the seed, the external sync and the save, in
687+
// hook order (`./objectGridColumnState`).
688+
const { columnState, saveColumnState } = useObjectGridColumnState({ schema, onColumnStateChange });
700689

701-
/**
702-
* NON-AUTHOR SURFACE — `columnState` is deliberately absent from
703-
* `GRID_QUERY_INPUTS` (`index.tsx`), by the maintainer ruling of 2026-08-18
704-
* on objectui#5091. The cast reads in this block are therefore DELIBERATELY
705-
* unlisted, not missed.
706-
*
707-
* It is the USER-STATE persistence payload, not authoring surface: the host
708-
* hands in whatever the user last saved (app-shell's `ObjectView` reads it
709-
* off the user's own list state, then off the view def) and stores their
710-
* drags back on that state (`persistListColumnState` there; `saveColumnState`
711-
* below is the outbound half). Publishing it would put
712-
* column widths and order in the designer panel and in the generated
713-
* `sdui-intrinsics.d.ts` — i.e. bless hand-authoring a payload the product
714-
* writes on the user's behalf.
715-
*
716-
* The contract says the same thing independently: `ComponentPropsMap
717-
* ['object-grid']` (`@objectstack/spec@17`) is a `strictObject` and rejects
718-
* `columnState` BY NAME (`unrecognized_keys`), so an author who took the
719-
* offer could not save the document. Both halves — unlisted here, rejected
720-
* there — are pinned by `__tests__/gridNonAuthorKeys.test.tsx`, together with
721-
* the reads themselves, so this exemption cannot decay into a silent drop.
722-
*/
723-
const [columnState, setColumnState] = useState<ObjectGridColumnState>(() => {
724-
// Priority: 1) externally provided (e.g. persisted view override),
725-
// 2) localStorage (per-browser fallback), 3) empty.
726-
const fromProps = (schema as any).columnState;
727-
if (fromProps && typeof fromProps === 'object') return fromProps;
728-
try {
729-
const saved = localStorage.getItem(columnStorageKey);
730-
return saved ? JSON.parse(saved) : {};
731-
} catch {
732-
return {};
733-
}
690+
// Pull to refresh and the card-view breakpoint, in hook order (`./objectGridMobile`).
691+
const { pullRef, isRefreshing, pullDistance } = useObjectGridMobile({
692+
schema, dataSource, setRefreshKey, setUseCardView,
734693
});
735694

736-
// Sync when external columnState changes (e.g. switching views, reload pulls
737-
// a saved override from the server). Wrapped in a stable string key to
738-
// avoid re-renders when the parent passes a fresh-but-equal object.
739-
// Non-author surface, same ruling as the seed above (objectui#5091): these
740-
// two reads are the host's inbound channel, never an authored key.
741-
const externalColumnStateKey = React.useMemo(
742-
() => JSON.stringify((schema as any).columnState ?? null),
743-
[(schema as any).columnState]
744-
);
745-
React.useEffect(() => {
746-
const fromProps = (schema as any).columnState;
747-
if (fromProps && typeof fromProps === 'object') {
748-
setColumnState(fromProps);
749-
}
750-
// eslint-disable-next-line react-hooks/exhaustive-deps
751-
}, [externalColumnStateKey]);
752-
753-
const saveColumnState = useCallback((state: typeof columnState) => {
754-
setColumnState(state);
755-
try {
756-
localStorage.setItem(columnStorageKey, JSON.stringify(state));
757-
} catch (e) {
758-
console.warn('Failed to persist column state:', e);
759-
}
760-
// Notify the host so it can persist the layout (the console stores it on
761-
// the signed-in user's own list state).
762-
if (typeof onColumnStateChange === 'function') {
763-
try { onColumnStateChange(state); } catch (e) { console.warn('onColumnStateChange threw:', e); }
764-
}
765-
}, [columnStorageKey, onColumnStateChange]);
766-
767-
const handlePullRefresh = useCallback(async () => {
768-
setRefreshKey(k => k + 1);
769-
}, []);
770-
771-
const { ref: pullRef, isRefreshing, pullDistance } = usePullToRefresh<HTMLDivElement>({
772-
onRefresh: handlePullRefresh,
773-
enabled: !!dataSource && !!schema.objectName,
774-
});
775-
776-
// Activate the mobile card view below the 768px app mobile breakpoint so
777-
// phones and tablet-portrait never need to side-scroll a wide grid.
778-
useEffect(() => {
779-
const checkWidth = () => setUseCardView(window.innerWidth < 768);
780-
checkWidth();
781-
window.addEventListener('resize', checkWidth);
782-
return () => window.removeEventListener('resize', checkWidth);
783-
}, []);
784-
785695
// `passedData` — data handed down directly (from ListView) — is destructured
786696
// from props above.
787697

‎ui/packages/plugin-grid/src/__tests__/gridNonAuthorKeys.test.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,7 @@ const here = path.dirname(fileURLToPath(import.meta.url));
638638
// packages/plugin-grid/src/__tests__ -> repo root
639639
const repoRoot = path.resolve(here, '../../../..');
640640
const GRID_SOURCE = path.join(repoRoot, 'packages/plugin-grid/src/ObjectGrid.tsx');
641+
const COLUMN_STATE_SOURCE = path.join(repoRoot, 'packages/plugin-grid/src/objectGridColumnState.ts');
641642
const TYPES_SOURCE = path.join(repoRoot, 'packages/types/src/objectql.ts');
642643

643644
const gridLines = readFileSync(GRID_SOURCE, 'utf8').split('\n');
@@ -844,7 +845,7 @@ describe('`onNavigate` — the exemption is DOCUMENTED, which is what this card
844845
// The control for the two cases above: they assert "a comment is present",
845846
// which would also pass if this file had quietly stopped being able to see
846847
// comments at all. The four older exemptions are the independent witness.
847-
const gridSource = gridLines.join('\n');
848+
const gridSource = [gridLines.join('\n'), readFileSync(COLUMN_STATE_SOURCE, 'utf8')].join('\n');
848849
for (const { key } of NON_AUTHOR_KEYS) {
849850
expect(
850851
gridSource,
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
* The column-state persistence of `ObjectGrid` (`./ObjectGrid.tsx`): the
3+
* storage key, the `columnState` seed with the host's inbound channel, the
4+
* sync of an external `columnState` and the outbound `saveColumnState`, moved
5+
* out of the component verbatim (objectstack#25994, slice 7). The
6+
* NON-AUTHOR SURFACE comment stays directly above the seed it documents.
7+
*/
8+
9+
import React, { useState, useCallback } from 'react';
10+
import type { ObjectGridSchema } from '@object-ui/types';
11+
import type { ObjectGridColumnState, ObjectGridExternalPaginationProps } from './objectGridProps';
12+
13+
/**
14+
* `ObjectGrid`'s column order and widths: the persisted state and the save
15+
* the column handlers call. `ObjectGrid` calls it where these lines used to
16+
* stand, so the order of the component's hooks does not change.
17+
*/
18+
export function useObjectGridColumnState({
19+
schema, onColumnStateChange,
20+
}: {
21+
schema: ObjectGridSchema;
22+
onColumnStateChange: ObjectGridExternalPaginationProps['onColumnStateChange'];
23+
}) {
24+
// Column state persistence (order and widths)
25+
const columnStorageKey = React.useMemo(() => {
26+
return schema.id
27+
? `grid-columns-${schema.objectName}-${schema.id}`
28+
: `grid-columns-${schema.objectName}`;
29+
}, [schema.objectName, schema.id]);
30+
31+
/**
32+
* NON-AUTHOR SURFACE — `columnState` is deliberately absent from
33+
* `GRID_QUERY_INPUTS` (`index.tsx`), by the maintainer ruling of 2026-08-18
34+
* on objectui#5091. The cast reads in this block are therefore DELIBERATELY
35+
* unlisted, not missed.
36+
*
37+
* It is the USER-STATE persistence payload, not authoring surface: the host
38+
* hands in whatever the user last saved (app-shell's `ObjectView` reads it
39+
* off the user's own list state, then off the view def) and stores their
40+
* drags back on that state (`persistListColumnState` there; `saveColumnState`
41+
* below is the outbound half). Publishing it would put
42+
* column widths and order in the designer panel and in the generated
43+
* `sdui-intrinsics.d.ts` — i.e. bless hand-authoring a payload the product
44+
* writes on the user's behalf.
45+
*
46+
* The contract says the same thing independently: `ComponentPropsMap
47+
* ['object-grid']` (`@objectstack/spec@17`) is a `strictObject` and rejects
48+
* `columnState` BY NAME (`unrecognized_keys`), so an author who took the
49+
* offer could not save the document. Both halves — unlisted here, rejected
50+
* there — are pinned by `__tests__/gridNonAuthorKeys.test.tsx`, together with
51+
* the reads themselves, so this exemption cannot decay into a silent drop.
52+
*/
53+
const [columnState, setColumnState] = useState<ObjectGridColumnState>(() => {
54+
// Priority: 1) externally provided (e.g. persisted view override),
55+
// 2) localStorage (per-browser fallback), 3) empty.
56+
const fromProps = (schema as any).columnState;
57+
if (fromProps && typeof fromProps === 'object') return fromProps;
58+
try {
59+
const saved = localStorage.getItem(columnStorageKey);
60+
return saved ? JSON.parse(saved) : {};
61+
} catch {
62+
return {};
63+
}
64+
});
65+
66+
// Sync when external columnState changes (e.g. switching views, reload pulls
67+
// a saved override from the server). Wrapped in a stable string key to
68+
// avoid re-renders when the parent passes a fresh-but-equal object.
69+
// Non-author surface, same ruling as the seed above (objectui#5091): these
70+
// two reads are the host's inbound channel, never an authored key.
71+
const externalColumnStateKey = React.useMemo(
72+
() => JSON.stringify((schema as any).columnState ?? null),
73+
[(schema as any).columnState]
74+
);
75+
React.useEffect(() => {
76+
const fromProps = (schema as any).columnState;
77+
if (fromProps && typeof fromProps === 'object') {
78+
setColumnState(fromProps);
79+
}
80+
// eslint-disable-next-line react-hooks/exhaustive-deps
81+
}, [externalColumnStateKey]);
82+
83+
const saveColumnState = useCallback((state: typeof columnState) => {
84+
setColumnState(state);
85+
try {
86+
localStorage.setItem(columnStorageKey, JSON.stringify(state));
87+
} catch (e) {
88+
console.warn('Failed to persist column state:', e);
89+
}
90+
// Notify the host so it can persist the layout (the console stores it on
91+
// the signed-in user's own list state).
92+
if (typeof onColumnStateChange === 'function') {
93+
try { onColumnStateChange(state); } catch (e) { console.warn('onColumnStateChange threw:', e); }
94+
}
95+
}, [columnStorageKey, onColumnStateChange]);
96+
97+
return { columnState, saveColumnState };
98+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* The mobile affordances of `ObjectGrid` (`./ObjectGrid.tsx`): pull to
3+
* refresh and the card-view breakpoint effect, moved out of the component
4+
* verbatim (objectstack#25994, slice 7). The refresh key and the card-view
5+
* flag stay component state; their setters are passed in.
6+
*/
7+
8+
import React, { useEffect, useCallback } from 'react';
9+
import type { ObjectGridSchema, DataSource } from '@object-ui/types';
10+
import { usePullToRefresh } from '@object-ui/mobile';
11+
12+
/**
13+
* Wires `ObjectGrid`'s pull to refresh and its card-view breakpoint. Returns
14+
* what the grid's scroll container reads. `ObjectGrid` calls it where these
15+
* lines used to stand, so the order of the component's hooks does not change.
16+
*/
17+
export function useObjectGridMobile({
18+
schema, dataSource, setRefreshKey, setUseCardView,
19+
}: {
20+
schema: ObjectGridSchema;
21+
dataSource: DataSource | undefined;
22+
setRefreshKey: React.Dispatch<React.SetStateAction<number>>;
23+
setUseCardView: React.Dispatch<React.SetStateAction<boolean>>;
24+
}) {
25+
const handlePullRefresh = useCallback(async () => {
26+
setRefreshKey(k => k + 1);
27+
}, []);
28+
29+
const { ref: pullRef, isRefreshing, pullDistance } = usePullToRefresh<HTMLDivElement>({
30+
onRefresh: handlePullRefresh,
31+
enabled: !!dataSource && !!schema.objectName,
32+
});
33+
34+
// Activate the mobile card view below the 768px app mobile breakpoint so
35+
// phones and tablet-portrait never need to side-scroll a wide grid.
36+
useEffect(() => {
37+
const checkWidth = () => setUseCardView(window.innerWidth < 768);
38+
checkWidth();
39+
window.addEventListener('resize', checkWidth);
40+
return () => window.removeEventListener('resize', checkWidth);
41+
}, []);
42+
43+
return { pullRef, isRefreshing, pullDistance };
44+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* The row-height sync of `ObjectGrid` (`./ObjectGrid.tsx`): the effect that
3+
* follows a changed `schema.rowHeight` into the grid's own row-height state,
4+
* moved out of the component verbatim (objectstack#25994, slice 7). The state
5+
* and its setter stay in the component, which passes them in.
6+
*/
7+
8+
import React from 'react';
9+
import type { ObjectGridSchema } from '@object-ui/types';
10+
import { type RowHeightMode, resolveRowHeightMode } from './objectGridDefaults';
11+
12+
/**
13+
* Runs `ObjectGrid`'s row-height sync effect. It declares no state and returns
14+
* nothing. `ObjectGrid` calls it where the effect used to stand, so the order
15+
* of the component's hooks does not change.
16+
*/
17+
export function useObjectGridRowHeightSync({
18+
schema, rowHeightMode, setRowHeightMode,
19+
}: {
20+
schema: ObjectGridSchema;
21+
rowHeightMode: RowHeightMode;
22+
setRowHeightMode: React.Dispatch<React.SetStateAction<RowHeightMode>>;
23+
}): void {
24+
// Sync internal rowHeightMode when schema.rowHeight prop changes (e.g., parent ListView density toggle).
25+
// Routed through the same resolver as the seed above: this is the component's
26+
// second entry point for an author-supplied `rowHeight`, and one resolver at
27+
// every entry is what keeps the answer single (objectui#4443).
28+
React.useEffect(() => {
29+
if (!schema.rowHeight) return;
30+
const next = resolveRowHeightMode(schema.rowHeight);
31+
if (next !== rowHeightMode) {
32+
setRowHeightMode(next);
33+
}
34+
// eslint-disable-next-line react-hooks/exhaustive-deps
35+
}, [schema.rowHeight]);
36+
}

0 commit comments

Comments
 (0)