@ethisyscore/plugin-ui 1.63.0 → 1.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/data-grid/index.cjs +161 -0
- package/dist/components/data-grid/index.cjs.map +1 -0
- package/dist/components/data-grid/index.d.cts +234 -0
- package/dist/components/data-grid/index.d.ts +234 -0
- package/dist/components/data-grid/index.js +145 -0
- package/dist/components/data-grid/index.js.map +1 -0
- package/dist/components/layout/index.cjs +637 -25
- package/dist/components/layout/index.cjs.map +1 -1
- package/dist/components/layout/index.js +593 -3
- package/dist/components/layout/index.js.map +1 -1
- package/dist/components/shared/index.cjs +750 -57
- package/dist/components/shared/index.cjs.map +1 -1
- package/dist/components/shared/index.d.cts +21 -1
- package/dist/components/shared/index.d.ts +21 -1
- package/dist/components/shared/index.js +720 -50
- package/dist/components/shared/index.js.map +1 -1
- package/dist/components/ui/index.cjs +619 -0
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.cts +64 -1
- package/dist/components/ui/index.d.ts +64 -1
- package/dist/components/ui/index.js +594 -3
- package/dist/components/ui/index.js.map +1 -1
- package/package.json +21 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var xDataGrid = require('@mui/x-data-grid');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
|
|
7
|
+
// src/components/data-grid/PersistentDataGrid.tsx
|
|
8
|
+
|
|
9
|
+
// src/components/data-grid/gridUserContext.ts
|
|
10
|
+
var gridUserIdRef = null;
|
|
11
|
+
function getGridUserId() {
|
|
12
|
+
return gridUserIdRef;
|
|
13
|
+
}
|
|
14
|
+
function setGridUserId(userId) {
|
|
15
|
+
gridUserIdRef = userId;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/components/data-grid/gridStatePersistence.ts
|
|
19
|
+
var GRID_STATE_VERSION = "v1";
|
|
20
|
+
var GRID_STATE_KEY_PREFIX = "cc-grid-state";
|
|
21
|
+
var DEFAULT_GRID_DEBOUNCE_MS = 400;
|
|
22
|
+
function buildGridStorageKey(userId, gridKey) {
|
|
23
|
+
if (!userId) return null;
|
|
24
|
+
return `${GRID_STATE_KEY_PREFIX}:${GRID_STATE_VERSION}:${userId}:${gridKey}`;
|
|
25
|
+
}
|
|
26
|
+
var ID_ISH_SEGMENT = /^(?:\d+|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
|
|
27
|
+
function deriveGridKeyFromPath(pathname) {
|
|
28
|
+
const kept = pathname.split("/").filter((segment) => segment.length > 0 && !ID_ISH_SEGMENT.test(segment));
|
|
29
|
+
return kept.length > 0 ? `route:${kept.join("/")}` : "route:root";
|
|
30
|
+
}
|
|
31
|
+
function stripGridPagination(state) {
|
|
32
|
+
const { pagination: _pagination, ...rest } = state;
|
|
33
|
+
return rest;
|
|
34
|
+
}
|
|
35
|
+
function readPersistedGridState(storageKey) {
|
|
36
|
+
if (!storageKey) return void 0;
|
|
37
|
+
if (typeof localStorage === "undefined") return void 0;
|
|
38
|
+
try {
|
|
39
|
+
const raw = localStorage.getItem(storageKey);
|
|
40
|
+
if (!raw) return void 0;
|
|
41
|
+
return stripGridPagination(JSON.parse(raw));
|
|
42
|
+
} catch {
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function writePersistedGridState(storageKey, state) {
|
|
47
|
+
if (!storageKey || !state) return;
|
|
48
|
+
if (typeof localStorage === "undefined") return;
|
|
49
|
+
try {
|
|
50
|
+
localStorage.setItem(storageKey, JSON.stringify(stripGridPagination(state)));
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function createDebouncedGridWriter(debounceMs = DEFAULT_GRID_DEBOUNCE_MS) {
|
|
55
|
+
let timer = null;
|
|
56
|
+
const flushNow = (storageKey, state) => {
|
|
57
|
+
writePersistedGridState(storageKey, state);
|
|
58
|
+
};
|
|
59
|
+
const schedule = (storageKey, state) => {
|
|
60
|
+
if (!storageKey || !state) return;
|
|
61
|
+
if (timer) clearTimeout(timer);
|
|
62
|
+
timer = setTimeout(() => flushNow(storageKey, state), debounceMs);
|
|
63
|
+
};
|
|
64
|
+
const cancel = () => {
|
|
65
|
+
if (timer) {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
timer = null;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return { schedule, cancel };
|
|
71
|
+
}
|
|
72
|
+
function mergeInitialState(callerInitialState, restored) {
|
|
73
|
+
if (!restored) return callerInitialState;
|
|
74
|
+
if (!callerInitialState) return restored;
|
|
75
|
+
return { ...callerInitialState, ...restored };
|
|
76
|
+
}
|
|
77
|
+
function PersistentDataGrid({
|
|
78
|
+
gridKey,
|
|
79
|
+
persistDebounceMs = DEFAULT_GRID_DEBOUNCE_MS,
|
|
80
|
+
initialState: callerInitialState,
|
|
81
|
+
onStateChange: callerOnStateChange,
|
|
82
|
+
...rest
|
|
83
|
+
}) {
|
|
84
|
+
const apiRef = xDataGrid.useGridApiRef();
|
|
85
|
+
const pathname = typeof window !== "undefined" ? window.location.pathname : "";
|
|
86
|
+
const effectiveGridKey = gridKey ?? deriveGridKeyFromPath(pathname);
|
|
87
|
+
const storageKey = react.useMemo(() => buildGridStorageKey(getGridUserId(), effectiveGridKey), [effectiveGridKey]);
|
|
88
|
+
const restoredState = react.useMemo(() => readPersistedGridState(storageKey), [storageKey]);
|
|
89
|
+
const effectiveInitialState = react.useMemo(
|
|
90
|
+
() => mergeInitialState(callerInitialState, restoredState),
|
|
91
|
+
[callerInitialState, restoredState]
|
|
92
|
+
);
|
|
93
|
+
const writerRef = react.useRef(null);
|
|
94
|
+
if (writerRef.current === null) {
|
|
95
|
+
writerRef.current = createDebouncedGridWriter(persistDebounceMs);
|
|
96
|
+
}
|
|
97
|
+
const handleStateChange = react.useCallback(
|
|
98
|
+
(...args) => {
|
|
99
|
+
callerOnStateChange?.(...args);
|
|
100
|
+
if (storageKey) {
|
|
101
|
+
writerRef.current?.schedule(storageKey, apiRef.current?.exportState());
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
[callerOnStateChange, storageKey, apiRef]
|
|
105
|
+
);
|
|
106
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
107
|
+
xDataGrid.DataGrid,
|
|
108
|
+
{
|
|
109
|
+
...rest,
|
|
110
|
+
apiRef,
|
|
111
|
+
initialState: effectiveInitialState,
|
|
112
|
+
onStateChange: handleStateChange
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/components/data-grid/pagination.ts
|
|
118
|
+
var PAGE_SIZE_OPTIONS = [5, 10, 25, 50];
|
|
119
|
+
var DEFAULT_PAGE_SIZE = 10;
|
|
120
|
+
|
|
121
|
+
// src/components/data-grid/dataGridPagination.ts
|
|
122
|
+
function buildDataGridPaginationProps(pagination) {
|
|
123
|
+
return {
|
|
124
|
+
pagination: true,
|
|
125
|
+
paginationMode: "server",
|
|
126
|
+
rowCount: pagination.total,
|
|
127
|
+
paginationModel: {
|
|
128
|
+
page: pagination.page - 1,
|
|
129
|
+
pageSize: pagination.pageSize
|
|
130
|
+
},
|
|
131
|
+
onPaginationModelChange: (model) => {
|
|
132
|
+
const pageSizeChanged = model.pageSize !== pagination.pageSize;
|
|
133
|
+
if (pageSizeChanged) {
|
|
134
|
+
if (pagination.onPageSizeChange) {
|
|
135
|
+
pagination.onPageSizeChange(model.pageSize);
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
pagination.onPageChange(model.page + 1);
|
|
140
|
+
},
|
|
141
|
+
pageSizeOptions: [...PAGE_SIZE_OPTIONS]
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
exports.DEFAULT_GRID_DEBOUNCE_MS = DEFAULT_GRID_DEBOUNCE_MS;
|
|
146
|
+
exports.DEFAULT_PAGE_SIZE = DEFAULT_PAGE_SIZE;
|
|
147
|
+
exports.GRID_STATE_KEY_PREFIX = GRID_STATE_KEY_PREFIX;
|
|
148
|
+
exports.GRID_STATE_VERSION = GRID_STATE_VERSION;
|
|
149
|
+
exports.PAGE_SIZE_OPTIONS = PAGE_SIZE_OPTIONS;
|
|
150
|
+
exports.PersistentDataGrid = PersistentDataGrid;
|
|
151
|
+
exports.buildDataGridPaginationProps = buildDataGridPaginationProps;
|
|
152
|
+
exports.buildGridStorageKey = buildGridStorageKey;
|
|
153
|
+
exports.createDebouncedGridWriter = createDebouncedGridWriter;
|
|
154
|
+
exports.deriveGridKeyFromPath = deriveGridKeyFromPath;
|
|
155
|
+
exports.getGridUserId = getGridUserId;
|
|
156
|
+
exports.readPersistedGridState = readPersistedGridState;
|
|
157
|
+
exports.setGridUserId = setGridUserId;
|
|
158
|
+
exports.stripGridPagination = stripGridPagination;
|
|
159
|
+
exports.writePersistedGridState = writePersistedGridState;
|
|
160
|
+
//# sourceMappingURL=index.cjs.map
|
|
161
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/data-grid/gridUserContext.ts","../../../src/components/data-grid/gridStatePersistence.ts","../../../src/components/data-grid/PersistentDataGrid.tsx","../../../src/components/data-grid/pagination.ts","../../../src/components/data-grid/dataGridPagination.ts"],"names":["useGridApiRef","useMemo","useRef","useCallback","jsx","DataGrid"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Module-level mirror of the current signed-in user's id.\n *\n * Readable by non-React gogo-ui code (e.g. the per-user key builder in\n * `gridStatePersistence.ts` and the {@link PersistentDataGrid} wrapper). The\n * host app stamps this ref via {@link setGridUserId} once auth resolves,\n * following the same cross-layer pattern as `orgFormatContextValue.ts` /\n * `activeOrganisationContextValue.ts`.\n *\n * Why a module-level ref instead of a React context or a threaded prop:\n * - The shared persistence layer needs the user id synchronously at the moment\n * a grid mounts (to build the localStorage key), not as a render-time prop.\n * - The AB#4840 rollout adopts {@link PersistentDataGrid} across ~77 grids; we\n * must NOT plumb `userId` as a prop through every view. A single host-stamped\n * ref keeps adoption a one-line `<PersistentDataGrid gridKey=\"…\" />` swap.\n * - gogo-ui cannot import from the host app's `src/`, so the setter lives here\n * and is called by the host's `useGridUserSync` hook via the\n * `@gogo-ui/adapter/shared/gridUserContextValue` path alias.\n *\n * When `null` (pre-auth / signed out) persistence is disabled: the key builder\n * returns `null`, reads fall back to grid defaults, and writes are ignored.\n */\n\nlet gridUserIdRef: string | null = null;\n\n/** Returns the current user's id, or `null` while auth is unresolved / signed out. */\nexport function getGridUserId(): string | null {\n return gridUserIdRef;\n}\n\n/**\n * Stamps the module-level user-id ref. Called by the host app's `useGridUserSync`\n * hook each time the authenticated user resolves or changes. Pass `null` to\n * disable persistence (signed out / pre-auth).\n */\nexport function setGridUserId(userId: string | null): void {\n gridUserIdRef = userId;\n}\n","/**\n * Pure, gogo-ui-safe persistence logic for MUI DataGrid state (column\n * visibility, sort, filter, and column widths). This is the single source of\n * truth for the key shape, the localStorage read/write, the debounce window,\n * and the pagination-slice stripping that the AB#4840 column-visibility feature\n * relies on.\n *\n * It lives in gogo-ui (which MAY import `@mui/x-data-grid` types directly) so\n * the reusable {@link PersistentDataGrid} wrapper can own the apiRef and MUI\n * types in one place — the host app's `src/**` may NOT import `@mui/x-data-grid`\n * (values or types), so this logic cannot live in the host hook.\n *\n * Auth-agnostic: the user id comes from the host-stamped module ref in\n * `gridUserContextValue.ts` at the call sites (the wrapper), not from this\n * module — keeping these functions pure and unit-testable.\n *\n * No React, no MUI values — only the `GridInitialState` TYPE import, so this\n * module is a plain value module (`.ts`) free of `react-refresh` concerns.\n */\nimport type { GridInitialState } from \"@mui/x-data-grid\";\n\n/**\n * Schema version for the persisted grid-state payload. Bump this when the shape\n * of what we store changes incompatibly so stale entries from an older app\n * version are ignored (the key changes, the old key is simply orphaned) rather\n * than restored into a grid that can no longer interpret them.\n */\nexport const GRID_STATE_VERSION = \"v1\";\nexport const GRID_STATE_KEY_PREFIX = \"cc-grid-state\";\n\n/**\n * Default debounce window for persisting grid state. `onStateChange` fires very\n * frequently (every hover, focus, resize tick) — debouncing collapses a burst\n * of changes into a single localStorage write.\n */\nexport const DEFAULT_GRID_DEBOUNCE_MS = 400;\n\n/**\n * Builds the per-user, per-grid localStorage key. Keying on the user id keeps a\n * shared browser's saved preferences separate per signed-in user; keying on\n * `gridKey` keeps each grid's preferences independent. Returns `null` when there\n * is no user id yet (pre-auth / signed out) — callers then behave as a no-op\n * persistence layer and the grid renders with its built-in defaults.\n */\nexport function buildGridStorageKey(userId: string | null | undefined, gridKey: string): string | null {\n if (!userId) return null;\n return `${GRID_STATE_KEY_PREFIX}:${GRID_STATE_VERSION}:${userId}:${gridKey}`;\n}\n\n/**\n * Matches a single path segment that is \"id-ish\" — an entity identifier rather\n * than a stable grid-type segment — so {@link deriveGridKeyFromPath} can strip it\n * and key preferences per grid TYPE instead of per entity. A segment is dropped\n * when it is either:\n *\n * - **all-numeric** — `123`, `0042` (numeric DB ids / sequence numbers); or\n * - **a UUID** — canonical 8-4-4-4-12 hex form, case-insensitive\n * (e.g. `3fa85f64-5717-4562-b3fc-2c963f66afa6`).\n *\n * Deliberately NARROW: only these two shapes are treated as ids. Slugs, ULIDs,\n * mixed alphanumerics, and ordinary words (`new`, `all`, `documents`,\n * `position-history`) are KEPT, so two genuinely different grid pages never\n * collide on the same key. The trade-off — a non-numeric, non-UUID id (e.g. a\n * short code or a base-62 id) is not stripped, so its grid keys per entity — is\n * acceptable: such routes are rare, and an explicit `gridKey` prop overrides the\n * auto-key whenever per-entity keying is wrong (see {@link PersistentDataGrid}).\n */\nconst ID_ISH_SEGMENT = /^(?:\\d+|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;\n\n/**\n * Derives a STABLE, per-grid-TYPE key from a route pathname, used by\n * {@link PersistentDataGrid} when no explicit `gridKey` prop is supplied.\n *\n * Stripping rule (so prefs are per-grid-type, not per-entity instance):\n * 1. Split the pathname on `/` and drop empty segments (leading/trailing/double\n * slashes, and a bare `/` root).\n * 2. Drop every \"id-ish\" segment per {@link ID_ISH_SEGMENT} — all-numeric ids and\n * canonical UUIDs.\n * 3. Join the survivors with `/`.\n *\n * Examples:\n * - `/projects/123/documents` → `projects/documents`\n * - `/hr/employees/3fa85f64-…-66afa6` → `hr/employees`\n * - `/super-admin/organisations` → `super-admin/organisations`\n * - `/` (root) → `route:root` (fallback)\n *\n * The returned key is prefixed with `route:` so an auto-derived key can never\n * be mistaken for, or collide with, a hand-authored `\"<module>.<view>\"` gridKey\n * (which uses dots, not slashes). When stripping leaves nothing (e.g. a route\n * that is ONLY ids, or the bare root), it falls back to `route:root` rather than\n * an empty string, so the storage key stays well-formed.\n */\nexport function deriveGridKeyFromPath(pathname: string): string {\n const kept = pathname\n .split(\"/\")\n .filter((segment) => segment.length > 0 && !ID_ISH_SEGMENT.test(segment));\n return kept.length > 0 ? `route:${kept.join(\"/\")}` : \"route:root\";\n}\n\n/**\n * Strips the `pagination` slice from a grid state. Pagination on these grids is\n * server-owned and supplied as a controlled `paginationModel` via the pagination\n * helper; persisting/restoring it would clash with the controlled prop (MUI\n * warns when `initialState.pagination.paginationModel` is set alongside a\n * controlled model) and isn't part of the AB#4840 scope (visibility / sort /\n * filter / column widths). We keep everything else `exportState()` produces —\n * including `columns` (visibility + widths + order), `sorting`, and `filter`.\n */\nexport function stripGridPagination(state: GridInitialState): GridInitialState {\n const { pagination: _pagination, ...rest } = state;\n return rest;\n}\n\n/**\n * Reads a previously-persisted grid state from localStorage. SSR / throw-safe:\n * any failure (storage blocked, corrupted JSON, quota errors, no `localStorage`\n * during SSR) falls back to `undefined` so the grid renders with defaults rather\n * than crashing.\n */\nexport function readPersistedGridState(storageKey: string | null): GridInitialState | undefined {\n if (!storageKey) return undefined;\n if (typeof localStorage === \"undefined\") return undefined;\n try {\n const raw = localStorage.getItem(storageKey);\n if (!raw) return undefined;\n return stripGridPagination(JSON.parse(raw) as GridInitialState);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Writes a grid state to localStorage under `storageKey` (the server-owned\n * pagination slice stripped first). SSR / throw-safe: storage blocked, quota\n * exceeded, or serialization failure are swallowed — a persistence failure must\n * never crash the grid. No-ops on a nullish key (no user) or nullish state\n * (apiRef not yet attached).\n */\nexport function writePersistedGridState(storageKey: string | null, state: GridInitialState | undefined): void {\n if (!storageKey || !state) return;\n if (typeof localStorage === \"undefined\") return;\n try {\n localStorage.setItem(storageKey, JSON.stringify(stripGridPagination(state)));\n } catch {\n // Storage blocked / quota exceeded / serialization failure — preferences\n // just won't persist this session. Never let a persistence failure crash\n // the grid.\n }\n}\n\n/**\n * Creates a debounced writer bound to a fresh timer. Each returned function\n * collapses a burst of `onStateChange` calls into a single localStorage write\n * after `debounceMs`. The caller owns one instance per grid (e.g. via a ref) so\n * timers don't leak across grids.\n */\nexport function createDebouncedGridWriter(debounceMs: number = DEFAULT_GRID_DEBOUNCE_MS) {\n let timer: ReturnType<typeof setTimeout> | null = null;\n const flushNow = (storageKey: string | null, state: GridInitialState | undefined) => {\n writePersistedGridState(storageKey, state);\n };\n const schedule = (storageKey: string | null, state: GridInitialState | undefined) => {\n if (!storageKey || !state) return;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => flushNow(storageKey, state), debounceMs);\n };\n const cancel = () => {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n };\n return { schedule, cancel };\n}\n","/**\n * PersistentDataGrid — a thin, drop-in wrapper around MUI `<DataGrid>` that\n * persists the user's column visibility, sort, filter, and column-width\n * preferences to localStorage, keyed per-user + per-grid (AB#4840).\n *\n * Adoption for ANY paginated grid is a near-one-line swap:\n *\n * ```tsx\n * // before\n * import { DataGrid } from \"@mui/x-data-grid\";\n * <DataGrid rows={rows} columns={cols} {...buildDataGridPaginationProps(pagination)} ... />\n *\n * // after\n * import { PersistentDataGrid } from \"../shared/PersistentDataGrid\";\n * <PersistentDataGrid gridKey=\"projects.all\" rows={rows} columns={cols} {...buildDataGridPaginationProps(pagination)} ... />\n * ```\n *\n * It composes with `buildDataGridPaginationProps` / `buildDataGridSlots`: those\n * prop bundles are spread straight through (`...rest`) untouched. The wrapper\n * owns the grid `apiRef` (created internally via `useGridApiRef()` so it can\n * read `exportState()`) and two props it MERGES rather than clobbers:\n *\n * - `initialState`: persisted state is layered OVER any caller default, so a\n * view can still seed default column visibility while the user's saved\n * preferences win on the keys they've touched.\n * - `onStateChange`: the caller's handler (if any) still fires; the wrapper\n * additionally schedules a debounced persist.\n *\n * The current user id comes from the host-stamped module ref\n * (`gridUserContextValue.getGridUserId`) — NO `userId` prop is threaded through\n * views. When there's no user (pre-auth / signed out) the storage key is `null`\n * and the wrapper degrades to a plain DataGrid (no reads, no writes).\n *\n * gogo-ui MAY import `@mui/x-data-grid` directly, so the apiRef + MUI types live\n * here, keeping the host `src/**` free of any `@mui/x-data-grid` import.\n *\n * ## Auto-key (no `gridKey` prop)\n *\n * `gridKey` is OPTIONAL. When omitted, the grid key is derived from the current\n * route via `window.location.pathname` and {@link deriveGridKeyFromPath} — id-ish\n * segments (all-numeric ids, UUIDs) are stripped so preferences are keyed per\n * grid TYPE, not per entity (e.g. `/projects/123/documents` and\n * `/projects/456/documents` share one key `route:projects/documents`). This makes\n * adoption a one-line IMPORT REDIRECT for the common single-grid-per-page case —\n * a view swaps its DataGrid import to the barrel and gets persistence for free.\n *\n * An explicit `gridKey` ALWAYS overrides the auto-key. Pass one for:\n * - multi-grid pages (2+ grids on one route would otherwise share a key), and\n * - long-lived grids whose key must stay stable independent of route changes\n * (e.g. the canonical list grids keep `\"projects.all\"` etc.).\n *\n * NOTE: the wrapper creates its OWN apiRef. A view that also needs direct apiRef\n * access for unrelated reasons should keep a plain `<DataGrid>` — that's rare;\n * persistence is the only thing 99% of grids want the apiRef for.\n */\nimport { useCallback, useMemo, useRef } from \"react\";\n\nimport { DataGrid, useGridApiRef, type DataGridProps, type GridInitialState } from \"@mui/x-data-grid\";\n\nimport { getGridUserId } from \"./gridUserContext\";\nimport {\n buildGridStorageKey,\n createDebouncedGridWriter,\n deriveGridKeyFromPath,\n readPersistedGridState,\n DEFAULT_GRID_DEBOUNCE_MS,\n} from \"./gridStatePersistence\";\n\nexport interface PersistentDataGridProps extends Omit<DataGridProps, \"apiRef\"> {\n /**\n * Stable identifier for this grid, unique per logical grid across the app.\n * Used as the per-grid segment of the localStorage key. Convention:\n * `\"<module>.<view>\"` — e.g. `\"projects.all\"`, `\"legal.risks\"`,\n * `\"superAdmin.organisations\"`. Must not change between renders/sessions or\n * the saved preferences orphan.\n *\n * OPTIONAL: when omitted, an auto-key is derived from the current route (see\n * the file-level \"Auto-key\" note). Pass an explicit key for multi-grid pages\n * or to pin a key independent of the route. An explicit key always wins.\n */\n gridKey?: string;\n /** Debounce window (ms) for the persist write. Defaults to 400ms. */\n persistDebounceMs?: number;\n}\n\n/**\n * Shallow-merges a restored grid state OVER a caller-provided `initialState`.\n * Top-level slices (`columns`, `sorting`, `filter`, `pinnedColumns`, …) from the\n * restored state replace the caller's; slices the user never touched fall back\n * to the caller default. A deep merge isn't warranted — MUI's `exportState()`\n * emits a complete slice per touched feature, so slice-level replacement is the\n * correct granularity (and matches how `initialState` is consumed once on mount).\n */\nfunction mergeInitialState(\n callerInitialState: GridInitialState | undefined,\n restored: GridInitialState | undefined,\n): GridInitialState | undefined {\n if (!restored) return callerInitialState;\n if (!callerInitialState) return restored;\n return { ...callerInitialState, ...restored };\n}\n\nexport function PersistentDataGrid({\n gridKey,\n persistDebounceMs = DEFAULT_GRID_DEBOUNCE_MS,\n initialState: callerInitialState,\n onStateChange: callerOnStateChange,\n ...rest\n}: PersistentDataGridProps) {\n // Own an apiRef so we can read exportState() on every state change.\n const apiRef = useGridApiRef();\n\n // Resolve the effective grid key: an explicit `gridKey` prop ALWAYS wins;\n // otherwise derive a stable per-grid-type key from the current route (id-ish\n // segments stripped). We read `window.location.pathname` directly rather than\n // `useLocation()` so this leaf wrapper needs no `<Router>` context — accurate at\n // render time (react-router drives navigation through the history API, which\n // updates `window.location`), and grid views remount on navigation so the path\n // is re-read on the next mount. Guarded for SSR / non-DOM environments (falls\n // back to \"\" → `route:root`).\n const pathname = typeof window !== \"undefined\" ? window.location.pathname : \"\";\n const effectiveGridKey = gridKey ?? deriveGridKeyFromPath(pathname);\n\n // Resolve the per-user storage key + restore once on mount. Reading the user\n // id at mount is correct: the host stamps it in AuthenticatedShell, which sits\n // above every page, so it's populated before any grid mounts. A different user\n // on a shared browser remounts the app (fresh login), so a mount-time read is\n // sufficient — no need to react to mid-session id changes.\n const storageKey = useMemo(() => buildGridStorageKey(getGridUserId(), effectiveGridKey), [effectiveGridKey]);\n const restoredState = useMemo(() => readPersistedGridState(storageKey), [storageKey]);\n const effectiveInitialState = useMemo(\n () => mergeInitialState(callerInitialState, restoredState),\n [callerInitialState, restoredState],\n );\n\n // One debounced writer per grid instance, kept stable across renders.\n const writerRef = useRef<ReturnType<typeof createDebouncedGridWriter> | null>(null);\n if (writerRef.current === null) {\n writerRef.current = createDebouncedGridWriter(persistDebounceMs);\n }\n\n // Signature-agnostic forwarder: `onStateChange`'s exact param tuple varies by\n // MUI x-data-grid minor, so we accept the args as a rest tuple typed off the\n // prop itself and forward them verbatim. This stays assignable to\n // `DataGridProps[\"onStateChange\"]` regardless of the concrete arity.\n type StateChangeArgs = Parameters<NonNullable<DataGridProps[\"onStateChange\"]>>;\n const handleStateChange = useCallback(\n (...args: StateChangeArgs) => {\n callerOnStateChange?.(...args);\n // exportState() returns the full persistable snapshot; the writer strips\n // the server-owned pagination slice before persisting.\n if (storageKey) {\n writerRef.current?.schedule(storageKey, apiRef.current?.exportState());\n }\n },\n [callerOnStateChange, storageKey, apiRef],\n );\n\n return (\n <DataGrid\n {...rest}\n apiRef={apiRef}\n initialState={effectiveInitialState}\n onStateChange={handleStateChange}\n />\n );\n}\n","/**\n * Canonical page-size options and default, promoted out of the plugins.\n *\n * A `constants/pagination.ts` appeared in three of the five plugin repos measured on\n * 2026-08-20, each declaring its own option list. Four copies of a list of page sizes is four\n * answers to \"what page sizes does this product offer\", and the divergence shows up as a grid\n * that offers 25 rows next to one that offers 20.\n */\n\n/** Page sizes a data grid offers. */\nexport const PAGE_SIZE_OPTIONS = [5, 10, 25, 50] as const;\n\n/** Page size a list starts on. */\nexport const DEFAULT_PAGE_SIZE = 10;\n","import type { GridPaginationModel } from \"@mui/x-data-grid\";\nimport { PAGE_SIZE_OPTIONS } from \"./pagination\";\n\n/**\n * Pagination object accepted by {@link buildDataGridPaginationProps}. Aligns with\n * `ContractPagination` in `@coreconnect/contracts`: 1-based page numbers with separate\n * page-change and page-size-change callbacks. The `STATIC_PAGINATION` placeholder used by\n * client-side-paginated views is also accepted (it omits `onPageSizeChange` — the helper\n * detects the omission and skips the size-change branch).\n */\nexport interface DataGridPaginationInput {\n page: number;\n pageSize: number;\n total: number;\n onPageChange: (page: number) => void;\n onPageSizeChange?: (pageSize: number) => void;\n}\n\n/**\n * Bundle of MUI DataGrid pagination props. Spread into a `<DataGrid {...}>` to wire\n * server-paginated lists in one line and avoid the per-view `onPaginationModelChange`\n * boilerplate.\n */\nexport interface DataGridPaginationProps {\n pagination: true;\n paginationMode: \"server\";\n rowCount: number;\n paginationModel: { page: number; pageSize: number };\n onPaginationModelChange: (model: GridPaginationModel) => void;\n pageSizeOptions: readonly number[];\n}\n\n/**\n * Builds the canonical pagination prop bundle for a server-paginated MUI DataGrid.\n *\n * The hand-written equivalent across the codebase looked like:\n *\n * ```tsx\n * paginationMode=\"server\"\n * rowCount={pagination.total}\n * paginationModel={{ page: pagination.page - 1, pageSize: pagination.pageSize }}\n * onPaginationModelChange={(model) => {\n * pagination.onPageChange(model.page + 1);\n * if (\"onPageSizeChange\" in pagination && pagination.onPageSizeChange) {\n * pagination.onPageSizeChange(model.pageSize);\n * }\n * }}\n * pageSizeOptions={[...PAGE_SIZE_OPTIONS]}\n * ```\n *\n * The naive form had a latent bug: changing page size called BOTH callbacks, so the\n * size-change handler (which typically resets page to 1) cancelled out subsequent\n * page-N navigation, pinning the user to page 1. This helper distinguishes the two\n * cases and only fires the relevant callback.\n *\n * Usage:\n *\n * ```tsx\n * <DataGrid\n * {...buildDataGridPaginationProps(effectivePagination)}\n * rows={items}\n * columns={columns}\n * ...\n * />\n * ```\n */\nexport function buildDataGridPaginationProps(\n pagination: DataGridPaginationInput,\n): DataGridPaginationProps {\n return {\n pagination: true,\n paginationMode: \"server\",\n rowCount: pagination.total,\n paginationModel: {\n page: pagination.page - 1,\n pageSize: pagination.pageSize,\n },\n onPaginationModelChange: (model) => {\n // Distinguish page-only navigation from page-size change. The naive\n // \"always call both callbacks\" approach pinned the grid to page 1 forever:\n // a page-size change would call onPageSizeChange (which resets page to 1)\n // AND onPageChange(model.page + 1), and on the next page navigation\n // onPageSizeChange would fire again from the updated model.pageSize.\n const pageSizeChanged = model.pageSize !== pagination.pageSize;\n if (pageSizeChanged) {\n if (pagination.onPageSizeChange) {\n pagination.onPageSizeChange(model.pageSize);\n }\n return;\n }\n pagination.onPageChange(model.page + 1);\n },\n pageSizeOptions: [...PAGE_SIZE_OPTIONS],\n };\n}\n"]}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { DataGridProps, GridPaginationModel, GridInitialState } from '@mui/x-data-grid';
|
|
3
|
+
|
|
4
|
+
interface PersistentDataGridProps extends Omit<DataGridProps, "apiRef"> {
|
|
5
|
+
/**
|
|
6
|
+
* Stable identifier for this grid, unique per logical grid across the app.
|
|
7
|
+
* Used as the per-grid segment of the localStorage key. Convention:
|
|
8
|
+
* `"<module>.<view>"` — e.g. `"projects.all"`, `"legal.risks"`,
|
|
9
|
+
* `"superAdmin.organisations"`. Must not change between renders/sessions or
|
|
10
|
+
* the saved preferences orphan.
|
|
11
|
+
*
|
|
12
|
+
* OPTIONAL: when omitted, an auto-key is derived from the current route (see
|
|
13
|
+
* the file-level "Auto-key" note). Pass an explicit key for multi-grid pages
|
|
14
|
+
* or to pin a key independent of the route. An explicit key always wins.
|
|
15
|
+
*/
|
|
16
|
+
gridKey?: string;
|
|
17
|
+
/** Debounce window (ms) for the persist write. Defaults to 400ms. */
|
|
18
|
+
persistDebounceMs?: number;
|
|
19
|
+
}
|
|
20
|
+
declare function PersistentDataGrid({ gridKey, persistDebounceMs, initialState: callerInitialState, onStateChange: callerOnStateChange, ...rest }: PersistentDataGridProps): react.JSX.Element;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Module-level mirror of the current signed-in user's id.
|
|
24
|
+
*
|
|
25
|
+
* Readable by non-React gogo-ui code (e.g. the per-user key builder in
|
|
26
|
+
* `gridStatePersistence.ts` and the {@link PersistentDataGrid} wrapper). The
|
|
27
|
+
* host app stamps this ref via {@link setGridUserId} once auth resolves,
|
|
28
|
+
* following the same cross-layer pattern as `orgFormatContextValue.ts` /
|
|
29
|
+
* `activeOrganisationContextValue.ts`.
|
|
30
|
+
*
|
|
31
|
+
* Why a module-level ref instead of a React context or a threaded prop:
|
|
32
|
+
* - The shared persistence layer needs the user id synchronously at the moment
|
|
33
|
+
* a grid mounts (to build the localStorage key), not as a render-time prop.
|
|
34
|
+
* - The AB#4840 rollout adopts {@link PersistentDataGrid} across ~77 grids; we
|
|
35
|
+
* must NOT plumb `userId` as a prop through every view. A single host-stamped
|
|
36
|
+
* ref keeps adoption a one-line `<PersistentDataGrid gridKey="…" />` swap.
|
|
37
|
+
* - gogo-ui cannot import from the host app's `src/`, so the setter lives here
|
|
38
|
+
* and is called by the host's `useGridUserSync` hook via the
|
|
39
|
+
* `@gogo-ui/adapter/shared/gridUserContextValue` path alias.
|
|
40
|
+
*
|
|
41
|
+
* When `null` (pre-auth / signed out) persistence is disabled: the key builder
|
|
42
|
+
* returns `null`, reads fall back to grid defaults, and writes are ignored.
|
|
43
|
+
*/
|
|
44
|
+
/** Returns the current user's id, or `null` while auth is unresolved / signed out. */
|
|
45
|
+
declare function getGridUserId(): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Stamps the module-level user-id ref. Called by the host app's `useGridUserSync`
|
|
48
|
+
* hook each time the authenticated user resolves or changes. Pass `null` to
|
|
49
|
+
* disable persistence (signed out / pre-auth).
|
|
50
|
+
*/
|
|
51
|
+
declare function setGridUserId(userId: string | null): void;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pagination object accepted by {@link buildDataGridPaginationProps}. Aligns with
|
|
55
|
+
* `ContractPagination` in `@coreconnect/contracts`: 1-based page numbers with separate
|
|
56
|
+
* page-change and page-size-change callbacks. The `STATIC_PAGINATION` placeholder used by
|
|
57
|
+
* client-side-paginated views is also accepted (it omits `onPageSizeChange` — the helper
|
|
58
|
+
* detects the omission and skips the size-change branch).
|
|
59
|
+
*/
|
|
60
|
+
interface DataGridPaginationInput {
|
|
61
|
+
page: number;
|
|
62
|
+
pageSize: number;
|
|
63
|
+
total: number;
|
|
64
|
+
onPageChange: (page: number) => void;
|
|
65
|
+
onPageSizeChange?: (pageSize: number) => void;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Bundle of MUI DataGrid pagination props. Spread into a `<DataGrid {...}>` to wire
|
|
69
|
+
* server-paginated lists in one line and avoid the per-view `onPaginationModelChange`
|
|
70
|
+
* boilerplate.
|
|
71
|
+
*/
|
|
72
|
+
interface DataGridPaginationProps {
|
|
73
|
+
pagination: true;
|
|
74
|
+
paginationMode: "server";
|
|
75
|
+
rowCount: number;
|
|
76
|
+
paginationModel: {
|
|
77
|
+
page: number;
|
|
78
|
+
pageSize: number;
|
|
79
|
+
};
|
|
80
|
+
onPaginationModelChange: (model: GridPaginationModel) => void;
|
|
81
|
+
pageSizeOptions: readonly number[];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Builds the canonical pagination prop bundle for a server-paginated MUI DataGrid.
|
|
85
|
+
*
|
|
86
|
+
* The hand-written equivalent across the codebase looked like:
|
|
87
|
+
*
|
|
88
|
+
* ```tsx
|
|
89
|
+
* paginationMode="server"
|
|
90
|
+
* rowCount={pagination.total}
|
|
91
|
+
* paginationModel={{ page: pagination.page - 1, pageSize: pagination.pageSize }}
|
|
92
|
+
* onPaginationModelChange={(model) => {
|
|
93
|
+
* pagination.onPageChange(model.page + 1);
|
|
94
|
+
* if ("onPageSizeChange" in pagination && pagination.onPageSizeChange) {
|
|
95
|
+
* pagination.onPageSizeChange(model.pageSize);
|
|
96
|
+
* }
|
|
97
|
+
* }}
|
|
98
|
+
* pageSizeOptions={[...PAGE_SIZE_OPTIONS]}
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* The naive form had a latent bug: changing page size called BOTH callbacks, so the
|
|
102
|
+
* size-change handler (which typically resets page to 1) cancelled out subsequent
|
|
103
|
+
* page-N navigation, pinning the user to page 1. This helper distinguishes the two
|
|
104
|
+
* cases and only fires the relevant callback.
|
|
105
|
+
*
|
|
106
|
+
* Usage:
|
|
107
|
+
*
|
|
108
|
+
* ```tsx
|
|
109
|
+
* <DataGrid
|
|
110
|
+
* {...buildDataGridPaginationProps(effectivePagination)}
|
|
111
|
+
* rows={items}
|
|
112
|
+
* columns={columns}
|
|
113
|
+
* ...
|
|
114
|
+
* />
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
declare function buildDataGridPaginationProps(pagination: DataGridPaginationInput): DataGridPaginationProps;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Canonical page-size options and default, promoted out of the plugins.
|
|
121
|
+
*
|
|
122
|
+
* A `constants/pagination.ts` appeared in three of the five plugin repos measured on
|
|
123
|
+
* 2026-08-20, each declaring its own option list. Four copies of a list of page sizes is four
|
|
124
|
+
* answers to "what page sizes does this product offer", and the divergence shows up as a grid
|
|
125
|
+
* that offers 25 rows next to one that offers 20.
|
|
126
|
+
*/
|
|
127
|
+
/** Page sizes a data grid offers. */
|
|
128
|
+
declare const PAGE_SIZE_OPTIONS: readonly [5, 10, 25, 50];
|
|
129
|
+
/** Page size a list starts on. */
|
|
130
|
+
declare const DEFAULT_PAGE_SIZE = 10;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Pure, gogo-ui-safe persistence logic for MUI DataGrid state (column
|
|
134
|
+
* visibility, sort, filter, and column widths). This is the single source of
|
|
135
|
+
* truth for the key shape, the localStorage read/write, the debounce window,
|
|
136
|
+
* and the pagination-slice stripping that the AB#4840 column-visibility feature
|
|
137
|
+
* relies on.
|
|
138
|
+
*
|
|
139
|
+
* It lives in gogo-ui (which MAY import `@mui/x-data-grid` types directly) so
|
|
140
|
+
* the reusable {@link PersistentDataGrid} wrapper can own the apiRef and MUI
|
|
141
|
+
* types in one place — the host app's `src/**` may NOT import `@mui/x-data-grid`
|
|
142
|
+
* (values or types), so this logic cannot live in the host hook.
|
|
143
|
+
*
|
|
144
|
+
* Auth-agnostic: the user id comes from the host-stamped module ref in
|
|
145
|
+
* `gridUserContextValue.ts` at the call sites (the wrapper), not from this
|
|
146
|
+
* module — keeping these functions pure and unit-testable.
|
|
147
|
+
*
|
|
148
|
+
* No React, no MUI values — only the `GridInitialState` TYPE import, so this
|
|
149
|
+
* module is a plain value module (`.ts`) free of `react-refresh` concerns.
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Schema version for the persisted grid-state payload. Bump this when the shape
|
|
154
|
+
* of what we store changes incompatibly so stale entries from an older app
|
|
155
|
+
* version are ignored (the key changes, the old key is simply orphaned) rather
|
|
156
|
+
* than restored into a grid that can no longer interpret them.
|
|
157
|
+
*/
|
|
158
|
+
declare const GRID_STATE_VERSION = "v1";
|
|
159
|
+
declare const GRID_STATE_KEY_PREFIX = "cc-grid-state";
|
|
160
|
+
/**
|
|
161
|
+
* Default debounce window for persisting grid state. `onStateChange` fires very
|
|
162
|
+
* frequently (every hover, focus, resize tick) — debouncing collapses a burst
|
|
163
|
+
* of changes into a single localStorage write.
|
|
164
|
+
*/
|
|
165
|
+
declare const DEFAULT_GRID_DEBOUNCE_MS = 400;
|
|
166
|
+
/**
|
|
167
|
+
* Builds the per-user, per-grid localStorage key. Keying on the user id keeps a
|
|
168
|
+
* shared browser's saved preferences separate per signed-in user; keying on
|
|
169
|
+
* `gridKey` keeps each grid's preferences independent. Returns `null` when there
|
|
170
|
+
* is no user id yet (pre-auth / signed out) — callers then behave as a no-op
|
|
171
|
+
* persistence layer and the grid renders with its built-in defaults.
|
|
172
|
+
*/
|
|
173
|
+
declare function buildGridStorageKey(userId: string | null | undefined, gridKey: string): string | null;
|
|
174
|
+
/**
|
|
175
|
+
* Derives a STABLE, per-grid-TYPE key from a route pathname, used by
|
|
176
|
+
* {@link PersistentDataGrid} when no explicit `gridKey` prop is supplied.
|
|
177
|
+
*
|
|
178
|
+
* Stripping rule (so prefs are per-grid-type, not per-entity instance):
|
|
179
|
+
* 1. Split the pathname on `/` and drop empty segments (leading/trailing/double
|
|
180
|
+
* slashes, and a bare `/` root).
|
|
181
|
+
* 2. Drop every "id-ish" segment per {@link ID_ISH_SEGMENT} — all-numeric ids and
|
|
182
|
+
* canonical UUIDs.
|
|
183
|
+
* 3. Join the survivors with `/`.
|
|
184
|
+
*
|
|
185
|
+
* Examples:
|
|
186
|
+
* - `/projects/123/documents` → `projects/documents`
|
|
187
|
+
* - `/hr/employees/3fa85f64-…-66afa6` → `hr/employees`
|
|
188
|
+
* - `/super-admin/organisations` → `super-admin/organisations`
|
|
189
|
+
* - `/` (root) → `route:root` (fallback)
|
|
190
|
+
*
|
|
191
|
+
* The returned key is prefixed with `route:` so an auto-derived key can never
|
|
192
|
+
* be mistaken for, or collide with, a hand-authored `"<module>.<view>"` gridKey
|
|
193
|
+
* (which uses dots, not slashes). When stripping leaves nothing (e.g. a route
|
|
194
|
+
* that is ONLY ids, or the bare root), it falls back to `route:root` rather than
|
|
195
|
+
* an empty string, so the storage key stays well-formed.
|
|
196
|
+
*/
|
|
197
|
+
declare function deriveGridKeyFromPath(pathname: string): string;
|
|
198
|
+
/**
|
|
199
|
+
* Strips the `pagination` slice from a grid state. Pagination on these grids is
|
|
200
|
+
* server-owned and supplied as a controlled `paginationModel` via the pagination
|
|
201
|
+
* helper; persisting/restoring it would clash with the controlled prop (MUI
|
|
202
|
+
* warns when `initialState.pagination.paginationModel` is set alongside a
|
|
203
|
+
* controlled model) and isn't part of the AB#4840 scope (visibility / sort /
|
|
204
|
+
* filter / column widths). We keep everything else `exportState()` produces —
|
|
205
|
+
* including `columns` (visibility + widths + order), `sorting`, and `filter`.
|
|
206
|
+
*/
|
|
207
|
+
declare function stripGridPagination(state: GridInitialState): GridInitialState;
|
|
208
|
+
/**
|
|
209
|
+
* Reads a previously-persisted grid state from localStorage. SSR / throw-safe:
|
|
210
|
+
* any failure (storage blocked, corrupted JSON, quota errors, no `localStorage`
|
|
211
|
+
* during SSR) falls back to `undefined` so the grid renders with defaults rather
|
|
212
|
+
* than crashing.
|
|
213
|
+
*/
|
|
214
|
+
declare function readPersistedGridState(storageKey: string | null): GridInitialState | undefined;
|
|
215
|
+
/**
|
|
216
|
+
* Writes a grid state to localStorage under `storageKey` (the server-owned
|
|
217
|
+
* pagination slice stripped first). SSR / throw-safe: storage blocked, quota
|
|
218
|
+
* exceeded, or serialization failure are swallowed — a persistence failure must
|
|
219
|
+
* never crash the grid. No-ops on a nullish key (no user) or nullish state
|
|
220
|
+
* (apiRef not yet attached).
|
|
221
|
+
*/
|
|
222
|
+
declare function writePersistedGridState(storageKey: string | null, state: GridInitialState | undefined): void;
|
|
223
|
+
/**
|
|
224
|
+
* Creates a debounced writer bound to a fresh timer. Each returned function
|
|
225
|
+
* collapses a burst of `onStateChange` calls into a single localStorage write
|
|
226
|
+
* after `debounceMs`. The caller owns one instance per grid (e.g. via a ref) so
|
|
227
|
+
* timers don't leak across grids.
|
|
228
|
+
*/
|
|
229
|
+
declare function createDebouncedGridWriter(debounceMs?: number): {
|
|
230
|
+
schedule: (storageKey: string | null, state: GridInitialState | undefined) => void;
|
|
231
|
+
cancel: () => void;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export { DEFAULT_GRID_DEBOUNCE_MS, DEFAULT_PAGE_SIZE, type DataGridPaginationInput, type DataGridPaginationProps, GRID_STATE_KEY_PREFIX, GRID_STATE_VERSION, PAGE_SIZE_OPTIONS, PersistentDataGrid, type PersistentDataGridProps, buildDataGridPaginationProps, buildGridStorageKey, createDebouncedGridWriter, deriveGridKeyFromPath, getGridUserId, readPersistedGridState, setGridUserId, stripGridPagination, writePersistedGridState };
|