@ethisyscore/plugin-ui 1.70.0 → 1.71.1

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.
Files changed (34) hide show
  1. package/README.md +3 -1
  2. package/dist/components/data-grid/index.cjs +26 -1
  3. package/dist/components/data-grid/index.cjs.map +1 -1
  4. package/dist/components/data-grid/index.d.cts +36 -22
  5. package/dist/components/data-grid/index.d.ts +36 -22
  6. package/dist/components/data-grid/index.js +27 -2
  7. package/dist/components/data-grid/index.js.map +1 -1
  8. package/dist/components/date/index.cjs +466 -0
  9. package/dist/components/date/index.cjs.map +1 -0
  10. package/dist/components/date/index.d.cts +41 -0
  11. package/dist/components/date/index.d.ts +41 -0
  12. package/dist/components/date/index.js +437 -0
  13. package/dist/components/date/index.js.map +1 -0
  14. package/dist/components/layout/index.cjs +34 -466
  15. package/dist/components/layout/index.cjs.map +1 -1
  16. package/dist/components/layout/index.js +0 -410
  17. package/dist/components/layout/index.js.map +1 -1
  18. package/dist/components/shared/index.cjs +20 -452
  19. package/dist/components/shared/index.cjs.map +1 -1
  20. package/dist/components/shared/index.js +0 -410
  21. package/dist/components/shared/index.js.map +1 -1
  22. package/dist/components/ui/index.cjs +10 -447
  23. package/dist/components/ui/index.cjs.map +1 -1
  24. package/dist/components/ui/index.d.cts +1 -39
  25. package/dist/components/ui/index.d.ts +1 -39
  26. package/dist/components/ui/index.js +1 -411
  27. package/dist/components/ui/index.js.map +1 -1
  28. package/dist/platform-react/index.cjs +23 -0
  29. package/dist/platform-react/index.cjs.map +1 -1
  30. package/dist/platform-react/index.d.cts +98 -1
  31. package/dist/platform-react/index.d.ts +98 -1
  32. package/dist/platform-react/index.js +21 -1
  33. package/dist/platform-react/index.js.map +1 -1
  34. package/package.json +9 -3
package/README.md CHANGED
@@ -18,9 +18,11 @@ and the shared MUI component surface externalised to the host's instances.
18
18
  | `.` | `createPortBridgeClient`, `BridgeClientContext` / `useBridgeClient`, `useBridgeTheme` / `useBridgeLocale`, `ExtensionRuntimeProvider`, `useMcpResource` / `useMcpTool` / `useMcpQuery` / `unwrapItems`, `useHostIdentity` / `useCurrentUser`, `definePlatformReactPage` / `emitNavigation` / `PlatformReactPageProps` | Bridge client + theme/locale context, brokered-MCP data hooks, host identity, and the base PlatformReact page contract |
19
19
  | `./platform-react` | `definePlatformReactPluginPage` / `createPluginPageDefiner`, `definePlatformReactPluginOverlay` / `useOverlayHost`, `useView` / `createUseView` / `ViewComponent` / `isComponent`, `BaseMcpService` / `useToolInvoker` / `useToolInvokerMap`, `useAuthenticatedQuery` / `useAuthenticatedQueries`, `injectPluginStyles` / `PluginStyleScope`, `createReactRouterShim`, `usePluginRealtime`, `useMcpUpload`, `useManagedLifecycle` | The plugin authoring layer: page/overlay wrappers (react-query + manifest + style root), the static `useView` adapter pattern, the MCP service base + tool invokers, pre-authenticated react-query wrappers, scoped-style injection, router shim, realtime, and file upload |
20
20
  | `./icons` | `NextureIcons`, `IconMap`, 470+ `Ni*` icon components, `sizeHelper` / `strokeSizeHelper`, `IconName` / `IconSize` / `IconVariant` | The gogo-ui "nexture" SVG icon set with props types and size/stroke helpers |
21
- | `./components/ui` | `Button`, `Card`, `Dialog`, `Table`, `TextField`, `Select`, `Tabs`, `Typography`, `Grid`, `Stack`, `Menu`, … | MUI component re-exports plus a Dialog scroll-lock wrapper. These are **host externals** — never bundled; production builds bind them to the host's MUI instance |
21
+ | `./components/ui` | `Button`, `Card`, `Dialog`, `Table`, `TextField`, `Select`, `Tabs`, `Typography`, `Grid`, `Stack`, `Menu`, `DebouncedSearchInput`, … | MUI component re-exports plus the Dialog scroll-lock wrapper and the debounced search field. These are **host externals** — never bundled; production builds bind them to the host's MUI instance |
22
22
  | `./components/layout` | `PageHeader` / `usePageHeader` / `withPageHeader` / `buildBreadcrumbs`, `FillGrid` / `FillGridItem` / `FillGap` / `FillSpan`, sidebar-action context (`SidebarActionProvider`, `useSidebarAction` / `useSidebarActions` / `useTriggerSidebarAction`), `RouteMeta` / `RouteMetaEntry` | Layout primitives (flex-wrap grid), the page-header layer + breadcrumb builder, and the sidebar-action context/provider |
23
23
  | `./components/shared` | `ConfirmActionDialog`, `ConfirmDeleteDialog`, `useToast`, `showSuccessToast` / `showErrorToast` / `showWarningToast` | Composite confirmation dialogs and toast notifications |
24
+ | `./components/data-grid` | `PersistentDataGrid`, `setGridUserId` / `getGridUserId`, `buildDataGridPaginationProps`, `PAGE_SIZE_OPTIONS` / `DEFAULT_PAGE_SIZE`, grid-state persistence helpers | The shared list-page grid with per-user state persistence. Needs the **optional peer** `@mui/x-data-grid` |
25
+ | `./components/date` | `DateInput` / `DateInputProps`, `toIsoString` / `parseIsoValue`, `DATE_INPUT_MIN` / `DATE_INPUT_MAX`, `DateInputType` | The shared date / datetime field over MUI X pickers, speaking ISO strings. Needs the **optional peers** `@mui/x-date-pickers` and `dayjs` |
24
26
  | `./a11y` | `useA11yAnnounce` | Accessible live-region announcement hook |
25
27
  | `./l10n` | `usePluginLocale` / `PluginLocale` | Locale + direction/RTL helper |
26
28
  | `./token` | `useFrontendSessionToken`, `useAuth`, `decodeJwtPayload` | Frontend session token + JWT auth utilities |
@@ -2,9 +2,23 @@
2
2
 
3
3
  var react = require('react');
4
4
  var xDataGrid = require('@mui/x-data-grid');
5
+ var plugin = require('@ethisyscore/extension-runtime/plugin');
5
6
  var jsxRuntime = require('react/jsx-runtime');
6
7
 
7
8
  // src/components/data-grid/PersistentDataGrid.tsx
9
+ function useCurrentUser() {
10
+ const identity = plugin.useHostIdentity();
11
+ const user = identity?.user ?? null;
12
+ if (!user) {
13
+ return null;
14
+ }
15
+ return {
16
+ id: user.id,
17
+ // The host binds `fullName`; fall back to composing first + last so a host
18
+ // that only sets the parts still yields a usable display name.
19
+ displayName: user.fullName?.trim() || `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim()
20
+ };
21
+ }
8
22
 
9
23
  // src/components/data-grid/gridUserContext.ts
10
24
  var gridUserIdRef = null;
@@ -84,12 +98,23 @@ function PersistentDataGrid({
84
98
  const apiRef = xDataGrid.useGridApiRef();
85
99
  const pathname = typeof window !== "undefined" ? window.location.pathname : "";
86
100
  const effectiveGridKey = gridKey ?? deriveGridKeyFromPath(pathname);
87
- const storageKey = react.useMemo(() => buildGridStorageKey(getGridUserId(), effectiveGridKey), [effectiveGridKey]);
101
+ const userId = useCurrentUser()?.id ?? getGridUserId();
102
+ const storageKey = react.useMemo(() => buildGridStorageKey(userId, effectiveGridKey), [userId, effectiveGridKey]);
88
103
  const restoredState = react.useMemo(() => readPersistedGridState(storageKey), [storageKey]);
89
104
  const effectiveInitialState = react.useMemo(
90
105
  () => mergeInitialState(callerInitialState, restoredState),
91
106
  [callerInitialState, restoredState]
92
107
  );
108
+ const appliedKeyRef = react.useRef(storageKey);
109
+ react.useEffect(() => {
110
+ if (storageKey === appliedKeyRef.current) {
111
+ return;
112
+ }
113
+ appliedKeyRef.current = storageKey;
114
+ if (storageKey && restoredState) {
115
+ apiRef.current?.restoreState(restoredState);
116
+ }
117
+ }, [storageKey, restoredState, apiRef]);
93
118
  const writerRef = react.useRef(null);
94
119
  if (writerRef.current === null) {
95
120
  writerRef.current = createDebouncedGridWriter(persistDebounceMs);
@@ -1 +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"]}
1
+ {"version":3,"sources":["../../../src/currentUser.ts","../../../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":["useHostIdentity","useGridApiRef","useMemo","useRef","useEffect","useCallback","jsx","DataGrid"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * `useCurrentUser` — the display-only current-user hook for plugin frontends\n * (WI 5154 follow-up #2, plugin-ui host-identity).\n *\n * A plugin page often needs the signed-in user for UX bits — a \"you are signed\n * in as …\" hint, pre-selecting the caller in a sign-off panel, or a\n * \"my acknowledgements\" heading. This hook surfaces that identity from the\n * host without the plugin importing the lower-level {@link useHostIdentity}\n * seam directly.\n *\n * It is a thin adapter over the host-provided {@link HostIdentity} context\n * (bound by the host through `ExtensionRuntimeProvider`'s `identity` prop):\n * it projects the host's richer {@link HostIdentityUser} down to the minimal\n * `{ id, displayName }` shape a plugin page needs for display.\n *\n * ⚠️ SECURITY — DISPLAY ONLY. This is presentation/UX data, NOT an\n * authorization source. Authorization is enforced HOST-SIDE at the MCP\n * boundary: the plugin backend re-authorises every tool/resource call from the\n * trusted server session. Never gate a mutation or a data read on this value.\n *\n * Returns `null` when the host has not provided an identity — standalone / mock\n * hosts, a host that predates the identity seam, or while host auth is still\n * loading or the caller is unauthenticated. Callers MUST handle `null`\n * (this matches the pre-existing plugin `useAuth().user` shim, which returned\n * `undefined`, so adopting this hook is a no-regression change).\n */\nimport { useHostIdentity } from \"@ethisyscore/extension-runtime/plugin\";\n\n/**\n * Minimal current-user shape a plugin page reads for display. Mirrors the\n * identity the host authenticates: a stable `id` and a human-readable\n * `displayName`. (No email — the host identity seam does not currently\n * forward one; if that changes, extend {@link HostIdentityUser} first and\n * project it here.)\n */\nexport interface CurrentUser {\n /** Stable user id (matches the host's authenticated caller id). */\n id: string;\n /** Human-readable display name (the host's full name). */\n displayName: string;\n}\n\n/**\n * Returns the host-authenticated current user projected to the display-only\n * {@link CurrentUser} shape, or `null` when no host identity is available.\n *\n * Display-only — see the module doc: authorization stays host-enforced at the\n * MCP boundary. Do NOT use the returned value as an authorization decision.\n */\nexport function useCurrentUser(): CurrentUser | null {\n const identity = useHostIdentity();\n const user = identity?.user ?? null;\n if (!user) {\n return null;\n }\n return {\n id: user.id,\n // The host binds `fullName`; fall back to composing first + last so a host\n // that only sets the parts still yields a usable display name.\n displayName:\n user.fullName?.trim() || `${user.firstName ?? \"\"} ${user.lastName ?? \"\"}`.trim(),\n };\n}\n","/**\n * Module-level mirror of the current signed-in user's id — the FALLBACK source for the grid\n * persistence key.\n *\n * ## Read this before using it\n *\n * For plugin surfaces you do NOT need to stamp anything. {@link PersistentDataGrid} reads the host\n * identity via `useCurrentUser` and only consults this ref when no identity is present. This exists\n * for an embedder that has no host identity to provide — the monolith arrangement, where a host-app\n * hook stamped the ref from its own auth context.\n *\n * ## Why it is the fallback and not the primary\n *\n * It used to be the only source, and that shipped a silent defect: the doc below described a\n * `useGridUserSync` hook in the host app, which was never carried over when these components were\n * lifted into plugin-ui. Measured across all eleven plugin repos on 2026-08-26, `setGridUserId` had\n * ZERO call sites — only its own declaration and this comment. So `getGridUserId()` always returned\n * `null`, `buildGridStorageKey` always returned `null`, and 17 grids ran a persistence wrapper that\n * read nothing and wrote nothing. It failed invisibly, because a grid with no saved preferences\n * looks exactly like a grid whose preferences were never saved.\n *\n * The lesson is in the ordering, not the code: a seam that requires the CONSUMER to remember a wiring\n * step will eventually meet a consumer who does not. Reading the identity the host already binds\n * removes the step.\n *\n * ## Why a module-level ref rather than a context\n *\n * The persistence layer needs the id synchronously when a grid mounts, to build the localStorage\n * key — not as a render-time prop, and never plumbed through every view.\n *\n * When `null` (pre-auth / signed out / standalone harness) persistence is simply off: 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.\n *\n * Plugin surfaces do not need this — {@link PersistentDataGrid} prefers the host identity, so\n * calling it is optional and stamping is not part of adopting the grid. Use it only in an embedder\n * with no host identity, each time its authenticated user resolves or changes. Pass `null` to\n * disable persistence.\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 IDENTITY (`useCurrentUser`, which reads\n * the identity the host binds to every mounted plugin surface), falling back to the\n * stamped module ref (`gridUserContext.getGridUserId`) for an embedder that provides\n * no identity. Either way NO `userId` prop is threaded through views. When there is\n * no user at all (pre-auth / signed out / standalone harness) the storage key is\n * `null` 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, useEffect, useMemo, useRef } from \"react\";\n\nimport { DataGrid, useGridApiRef, type DataGridProps, type GridInitialState } from \"@mui/x-data-grid\";\n\nimport { useCurrentUser } from \"../../currentUser\";\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. The HOST IDENTITY is the primary source:\n // `useCurrentUser` reads the identity the host binds to every mounted surface, so a plugin\n // gets persistence with nothing to wire up. `getGridUserId()` is the fallback, for an\n // embedder that stamps the ref instead of providing an identity (the monolith's arrangement).\n //\n // That order matters, and it is the fix for a real defect: this component previously read the\n // ref ONLY, and no plugin ever stamped it - the `useGridUserSync` hook its docs named was a\n // host-app hook that was not carried over on the lift. So the key was permanently null and\n // every plugin grid silently persisted nothing. Reading the identity means it cannot be\n // forgotten again, because there is nothing left to forget.\n //\n // `userId` is in the dependency list, unlike the bare `getGridUserId()` call it replaces: the\n // identity resolves ASYNCHRONOUSLY (`isLoading` while the host's /auth/user is in flight), so a\n // mount-time-only read would miss it and fall back to no persistence on every first paint.\n const userId = useCurrentUser()?.id ?? getGridUserId();\n const storageKey = useMemo(() => buildGridStorageKey(userId, effectiveGridKey), [userId, effectiveGridKey]);\n const restoredState = useMemo(() => readPersistedGridState(storageKey), [storageKey]);\n const effectiveInitialState = useMemo(\n () => mergeInitialState(callerInitialState, restoredState),\n [callerInitialState, restoredState],\n );\n\n // Apply the persisted state when the key changes AFTER mount, which is what the async identity\n // actually produces: the grid mounts pre-auth with no user, then the host resolves one.\n //\n // Recomputing `storageKey` and `restoredState` is not enough on its own, because MUI consumes\n // `initialState` on the FIRST RENDER ONLY — so the correct state would be read and then have\n // nowhere to go. `restoreState` is the documented route for injecting it later (\"These values can\n // then be passed to the `initialState` prop or injected using the `restoreState` method\").\n //\n // `appliedKeyRef` starts at the mount-time key, so the mount case stays with `initialState` and is\n // never restored twice. Only a genuine key CHANGE reaches the api call — in practice once per\n // session, when auth resolves — so this cannot clobber preferences a user is actively changing.\n const appliedKeyRef = useRef<string | null>(storageKey);\n useEffect(() => {\n if (storageKey === appliedKeyRef.current) {\n return;\n }\n appliedKeyRef.current = storageKey;\n if (storageKey && restoredState) {\n apiRef.current?.restoreState(restoredState);\n }\n }, [storageKey, restoredState, apiRef]);\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"]}
@@ -20,33 +20,47 @@ interface PersistentDataGridProps extends Omit<DataGridProps, "apiRef"> {
20
20
  declare function PersistentDataGrid({ gridKey, persistDebounceMs, initialState: callerInitialState, onStateChange: callerOnStateChange, ...rest }: PersistentDataGridProps): react.JSX.Element;
21
21
 
22
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
23
+ * Module-level mirror of the current signed-in user's id — the FALLBACK source for the grid
24
+ * persistence key.
25
+ *
26
+ * ## Read this before using it
27
+ *
28
+ * For plugin surfaces you do NOT need to stamp anything. {@link PersistentDataGrid} reads the host
29
+ * identity via `useCurrentUser` and only consults this ref when no identity is present. This exists
30
+ * for an embedder that has no host identity to provide — the monolith arrangement, where a host-app
31
+ * hook stamped the ref from its own auth context.
32
+ *
33
+ * ## Why it is the fallback and not the primary
34
+ *
35
+ * It used to be the only source, and that shipped a silent defect: the doc below described a
36
+ * `useGridUserSync` hook in the host app, which was never carried over when these components were
37
+ * lifted into plugin-ui. Measured across all eleven plugin repos on 2026-08-26, `setGridUserId` had
38
+ * ZERO call sites — only its own declaration and this comment. So `getGridUserId()` always returned
39
+ * `null`, `buildGridStorageKey` always returned `null`, and 17 grids ran a persistence wrapper that
40
+ * read nothing and wrote nothing. It failed invisibly, because a grid with no saved preferences
41
+ * looks exactly like a grid whose preferences were never saved.
42
+ *
43
+ * The lesson is in the ordering, not the code: a seam that requires the CONSUMER to remember a wiring
44
+ * step will eventually meet a consumer who does not. Reading the identity the host already binds
45
+ * removes the step.
46
+ *
47
+ * ## Why a module-level ref rather than a context
48
+ *
49
+ * The persistence layer needs the id synchronously when a grid mounts, to build the localStorage
50
+ * key — not as a render-time prop, and never plumbed through every view.
51
+ *
52
+ * When `null` (pre-auth / signed out / standalone harness) persistence is simply off: the key builder
42
53
  * returns `null`, reads fall back to grid defaults, and writes are ignored.
43
54
  */
44
55
  /** Returns the current user's id, or `null` while auth is unresolved / signed out. */
45
56
  declare function getGridUserId(): string | null;
46
57
  /**
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).
58
+ * Stamps the module-level user-id ref.
59
+ *
60
+ * Plugin surfaces do not need this — {@link PersistentDataGrid} prefers the host identity, so
61
+ * calling it is optional and stamping is not part of adopting the grid. Use it only in an embedder
62
+ * with no host identity, each time its authenticated user resolves or changes. Pass `null` to
63
+ * disable persistence.
50
64
  */
51
65
  declare function setGridUserId(userId: string | null): void;
52
66
 
@@ -20,33 +20,47 @@ interface PersistentDataGridProps extends Omit<DataGridProps, "apiRef"> {
20
20
  declare function PersistentDataGrid({ gridKey, persistDebounceMs, initialState: callerInitialState, onStateChange: callerOnStateChange, ...rest }: PersistentDataGridProps): react.JSX.Element;
21
21
 
22
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
23
+ * Module-level mirror of the current signed-in user's id — the FALLBACK source for the grid
24
+ * persistence key.
25
+ *
26
+ * ## Read this before using it
27
+ *
28
+ * For plugin surfaces you do NOT need to stamp anything. {@link PersistentDataGrid} reads the host
29
+ * identity via `useCurrentUser` and only consults this ref when no identity is present. This exists
30
+ * for an embedder that has no host identity to provide — the monolith arrangement, where a host-app
31
+ * hook stamped the ref from its own auth context.
32
+ *
33
+ * ## Why it is the fallback and not the primary
34
+ *
35
+ * It used to be the only source, and that shipped a silent defect: the doc below described a
36
+ * `useGridUserSync` hook in the host app, which was never carried over when these components were
37
+ * lifted into plugin-ui. Measured across all eleven plugin repos on 2026-08-26, `setGridUserId` had
38
+ * ZERO call sites — only its own declaration and this comment. So `getGridUserId()` always returned
39
+ * `null`, `buildGridStorageKey` always returned `null`, and 17 grids ran a persistence wrapper that
40
+ * read nothing and wrote nothing. It failed invisibly, because a grid with no saved preferences
41
+ * looks exactly like a grid whose preferences were never saved.
42
+ *
43
+ * The lesson is in the ordering, not the code: a seam that requires the CONSUMER to remember a wiring
44
+ * step will eventually meet a consumer who does not. Reading the identity the host already binds
45
+ * removes the step.
46
+ *
47
+ * ## Why a module-level ref rather than a context
48
+ *
49
+ * The persistence layer needs the id synchronously when a grid mounts, to build the localStorage
50
+ * key — not as a render-time prop, and never plumbed through every view.
51
+ *
52
+ * When `null` (pre-auth / signed out / standalone harness) persistence is simply off: the key builder
42
53
  * returns `null`, reads fall back to grid defaults, and writes are ignored.
43
54
  */
44
55
  /** Returns the current user's id, or `null` while auth is unresolved / signed out. */
45
56
  declare function getGridUserId(): string | null;
46
57
  /**
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).
58
+ * Stamps the module-level user-id ref.
59
+ *
60
+ * Plugin surfaces do not need this — {@link PersistentDataGrid} prefers the host identity, so
61
+ * calling it is optional and stamping is not part of adopting the grid. Use it only in an embedder
62
+ * with no host identity, each time its authenticated user resolves or changes. Pass `null` to
63
+ * disable persistence.
50
64
  */
51
65
  declare function setGridUserId(userId: string | null): void;
52
66
 
@@ -1,8 +1,22 @@
1
- import { useMemo, useRef, useCallback } from 'react';
1
+ import { useMemo, useRef, useEffect, useCallback } from 'react';
2
2
  import { useGridApiRef, DataGrid } from '@mui/x-data-grid';
3
+ import { useHostIdentity } from '@ethisyscore/extension-runtime/plugin';
3
4
  import { jsx } from 'react/jsx-runtime';
4
5
 
5
6
  // src/components/data-grid/PersistentDataGrid.tsx
7
+ function useCurrentUser() {
8
+ const identity = useHostIdentity();
9
+ const user = identity?.user ?? null;
10
+ if (!user) {
11
+ return null;
12
+ }
13
+ return {
14
+ id: user.id,
15
+ // The host binds `fullName`; fall back to composing first + last so a host
16
+ // that only sets the parts still yields a usable display name.
17
+ displayName: user.fullName?.trim() || `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim()
18
+ };
19
+ }
6
20
 
7
21
  // src/components/data-grid/gridUserContext.ts
8
22
  var gridUserIdRef = null;
@@ -82,12 +96,23 @@ function PersistentDataGrid({
82
96
  const apiRef = useGridApiRef();
83
97
  const pathname = typeof window !== "undefined" ? window.location.pathname : "";
84
98
  const effectiveGridKey = gridKey ?? deriveGridKeyFromPath(pathname);
85
- const storageKey = useMemo(() => buildGridStorageKey(getGridUserId(), effectiveGridKey), [effectiveGridKey]);
99
+ const userId = useCurrentUser()?.id ?? getGridUserId();
100
+ const storageKey = useMemo(() => buildGridStorageKey(userId, effectiveGridKey), [userId, effectiveGridKey]);
86
101
  const restoredState = useMemo(() => readPersistedGridState(storageKey), [storageKey]);
87
102
  const effectiveInitialState = useMemo(
88
103
  () => mergeInitialState(callerInitialState, restoredState),
89
104
  [callerInitialState, restoredState]
90
105
  );
106
+ const appliedKeyRef = useRef(storageKey);
107
+ useEffect(() => {
108
+ if (storageKey === appliedKeyRef.current) {
109
+ return;
110
+ }
111
+ appliedKeyRef.current = storageKey;
112
+ if (storageKey && restoredState) {
113
+ apiRef.current?.restoreState(restoredState);
114
+ }
115
+ }, [storageKey, restoredState, apiRef]);
91
116
  const writerRef = useRef(null);
92
117
  if (writerRef.current === null) {
93
118
  writerRef.current = createDebouncedGridWriter(persistDebounceMs);
@@ -1 +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":[],"mappings":"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","file":"index.js","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"]}
1
+ {"version":3,"sources":["../../../src/currentUser.ts","../../../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":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `useCurrentUser` — the display-only current-user hook for plugin frontends\n * (WI 5154 follow-up #2, plugin-ui host-identity).\n *\n * A plugin page often needs the signed-in user for UX bits — a \"you are signed\n * in as …\" hint, pre-selecting the caller in a sign-off panel, or a\n * \"my acknowledgements\" heading. This hook surfaces that identity from the\n * host without the plugin importing the lower-level {@link useHostIdentity}\n * seam directly.\n *\n * It is a thin adapter over the host-provided {@link HostIdentity} context\n * (bound by the host through `ExtensionRuntimeProvider`'s `identity` prop):\n * it projects the host's richer {@link HostIdentityUser} down to the minimal\n * `{ id, displayName }` shape a plugin page needs for display.\n *\n * ⚠️ SECURITY — DISPLAY ONLY. This is presentation/UX data, NOT an\n * authorization source. Authorization is enforced HOST-SIDE at the MCP\n * boundary: the plugin backend re-authorises every tool/resource call from the\n * trusted server session. Never gate a mutation or a data read on this value.\n *\n * Returns `null` when the host has not provided an identity — standalone / mock\n * hosts, a host that predates the identity seam, or while host auth is still\n * loading or the caller is unauthenticated. Callers MUST handle `null`\n * (this matches the pre-existing plugin `useAuth().user` shim, which returned\n * `undefined`, so adopting this hook is a no-regression change).\n */\nimport { useHostIdentity } from \"@ethisyscore/extension-runtime/plugin\";\n\n/**\n * Minimal current-user shape a plugin page reads for display. Mirrors the\n * identity the host authenticates: a stable `id` and a human-readable\n * `displayName`. (No email — the host identity seam does not currently\n * forward one; if that changes, extend {@link HostIdentityUser} first and\n * project it here.)\n */\nexport interface CurrentUser {\n /** Stable user id (matches the host's authenticated caller id). */\n id: string;\n /** Human-readable display name (the host's full name). */\n displayName: string;\n}\n\n/**\n * Returns the host-authenticated current user projected to the display-only\n * {@link CurrentUser} shape, or `null` when no host identity is available.\n *\n * Display-only — see the module doc: authorization stays host-enforced at the\n * MCP boundary. Do NOT use the returned value as an authorization decision.\n */\nexport function useCurrentUser(): CurrentUser | null {\n const identity = useHostIdentity();\n const user = identity?.user ?? null;\n if (!user) {\n return null;\n }\n return {\n id: user.id,\n // The host binds `fullName`; fall back to composing first + last so a host\n // that only sets the parts still yields a usable display name.\n displayName:\n user.fullName?.trim() || `${user.firstName ?? \"\"} ${user.lastName ?? \"\"}`.trim(),\n };\n}\n","/**\n * Module-level mirror of the current signed-in user's id — the FALLBACK source for the grid\n * persistence key.\n *\n * ## Read this before using it\n *\n * For plugin surfaces you do NOT need to stamp anything. {@link PersistentDataGrid} reads the host\n * identity via `useCurrentUser` and only consults this ref when no identity is present. This exists\n * for an embedder that has no host identity to provide — the monolith arrangement, where a host-app\n * hook stamped the ref from its own auth context.\n *\n * ## Why it is the fallback and not the primary\n *\n * It used to be the only source, and that shipped a silent defect: the doc below described a\n * `useGridUserSync` hook in the host app, which was never carried over when these components were\n * lifted into plugin-ui. Measured across all eleven plugin repos on 2026-08-26, `setGridUserId` had\n * ZERO call sites — only its own declaration and this comment. So `getGridUserId()` always returned\n * `null`, `buildGridStorageKey` always returned `null`, and 17 grids ran a persistence wrapper that\n * read nothing and wrote nothing. It failed invisibly, because a grid with no saved preferences\n * looks exactly like a grid whose preferences were never saved.\n *\n * The lesson is in the ordering, not the code: a seam that requires the CONSUMER to remember a wiring\n * step will eventually meet a consumer who does not. Reading the identity the host already binds\n * removes the step.\n *\n * ## Why a module-level ref rather than a context\n *\n * The persistence layer needs the id synchronously when a grid mounts, to build the localStorage\n * key — not as a render-time prop, and never plumbed through every view.\n *\n * When `null` (pre-auth / signed out / standalone harness) persistence is simply off: 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.\n *\n * Plugin surfaces do not need this — {@link PersistentDataGrid} prefers the host identity, so\n * calling it is optional and stamping is not part of adopting the grid. Use it only in an embedder\n * with no host identity, each time its authenticated user resolves or changes. Pass `null` to\n * disable persistence.\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 IDENTITY (`useCurrentUser`, which reads\n * the identity the host binds to every mounted plugin surface), falling back to the\n * stamped module ref (`gridUserContext.getGridUserId`) for an embedder that provides\n * no identity. Either way NO `userId` prop is threaded through views. When there is\n * no user at all (pre-auth / signed out / standalone harness) the storage key is\n * `null` 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, useEffect, useMemo, useRef } from \"react\";\n\nimport { DataGrid, useGridApiRef, type DataGridProps, type GridInitialState } from \"@mui/x-data-grid\";\n\nimport { useCurrentUser } from \"../../currentUser\";\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. The HOST IDENTITY is the primary source:\n // `useCurrentUser` reads the identity the host binds to every mounted surface, so a plugin\n // gets persistence with nothing to wire up. `getGridUserId()` is the fallback, for an\n // embedder that stamps the ref instead of providing an identity (the monolith's arrangement).\n //\n // That order matters, and it is the fix for a real defect: this component previously read the\n // ref ONLY, and no plugin ever stamped it - the `useGridUserSync` hook its docs named was a\n // host-app hook that was not carried over on the lift. So the key was permanently null and\n // every plugin grid silently persisted nothing. Reading the identity means it cannot be\n // forgotten again, because there is nothing left to forget.\n //\n // `userId` is in the dependency list, unlike the bare `getGridUserId()` call it replaces: the\n // identity resolves ASYNCHRONOUSLY (`isLoading` while the host's /auth/user is in flight), so a\n // mount-time-only read would miss it and fall back to no persistence on every first paint.\n const userId = useCurrentUser()?.id ?? getGridUserId();\n const storageKey = useMemo(() => buildGridStorageKey(userId, effectiveGridKey), [userId, effectiveGridKey]);\n const restoredState = useMemo(() => readPersistedGridState(storageKey), [storageKey]);\n const effectiveInitialState = useMemo(\n () => mergeInitialState(callerInitialState, restoredState),\n [callerInitialState, restoredState],\n );\n\n // Apply the persisted state when the key changes AFTER mount, which is what the async identity\n // actually produces: the grid mounts pre-auth with no user, then the host resolves one.\n //\n // Recomputing `storageKey` and `restoredState` is not enough on its own, because MUI consumes\n // `initialState` on the FIRST RENDER ONLY — so the correct state would be read and then have\n // nowhere to go. `restoreState` is the documented route for injecting it later (\"These values can\n // then be passed to the `initialState` prop or injected using the `restoreState` method\").\n //\n // `appliedKeyRef` starts at the mount-time key, so the mount case stays with `initialState` and is\n // never restored twice. Only a genuine key CHANGE reaches the api call — in practice once per\n // session, when auth resolves — so this cannot clobber preferences a user is actively changing.\n const appliedKeyRef = useRef<string | null>(storageKey);\n useEffect(() => {\n if (storageKey === appliedKeyRef.current) {\n return;\n }\n appliedKeyRef.current = storageKey;\n if (storageKey && restoredState) {\n apiRef.current?.restoreState(restoredState);\n }\n }, [storageKey, restoredState, apiRef]);\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"]}