@redsift/table 12.6.1 → 12.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -365,16 +365,53 @@ declare const clearPreviousVersionStorage: (id: string, previousLocalStorageVers
365
365
  * Useful when the consumer wants to wipe all persisted grid state regardless of which version is active.
366
366
  */
367
367
  declare const clearAllVersionStorage: (id: string, maxVersion: number) => void;
368
+ /** Options for `resetStatefulDataGridState` and `resetColumnVisibility`. */
369
+ interface StatefulDataGridResetOptions {
370
+ /** The grid's localStorage id: the `pathname` its `useRouter` returns. */
371
+ id: string;
372
+ /** The localStorage versions to clear. */
373
+ versions: number[];
374
+ /**
375
+ * The router's query string, as the caller captured it. Under a browser
376
+ * router this is only a fallback: `window.location.search` is read instead,
377
+ * because a snapshot captured at render can be stale. It is used as given
378
+ * only when `window.location` is unavailable (SSR, tests without jsdom), and
379
+ * it is ignored when `getSearch` is given.
380
+ */
381
+ search?: string;
382
+ /**
383
+ * Returns the router's live query string, with or without the leading `?`.
384
+ * Called once, at reset time, and used instead of both
385
+ * `window.location.search` and `search`.
386
+ *
387
+ * Pass it under a memory history (`createMemoryHistory`, `MemoryRouter`),
388
+ * for example in a sift rendered in an iframe. There `window.location` is
389
+ * the document's URL, not the router's, so without it the reset would
390
+ * rebuild the router's query string from the wrong URL: it would drop the
391
+ * router's non-grid params and add the document's. Use
392
+ * `() => history.location.search`, or the `getSearch` of a
393
+ * `createReactRouterV5Adapter` router.
394
+ */
395
+ getSearch?: () => string;
396
+ /** Writes the new query string. Called once, with `{ authoritative: true }`. */
397
+ historyReplace: (newSearch: string, options?: {
398
+ authoritative?: boolean;
399
+ }) => void;
400
+ }
368
401
  /**
369
402
  * Fully reset a `StatefulDataGrid` — clear localStorage for the given versions
370
403
  * and strip all grid-owned URL params (`_*` and `v=`), preserving any non-grid params.
371
404
  *
372
- * The function reads `window.location.search` directly so the strip always
373
- * operates on the live URL. This matters because consumers typically call this
374
- * from a component whose captured `search` snapshot may be stale — for example
375
- * when the grid has just written its own keys to the URL via the router adapter
376
- * but the parent hasn't re-rendered yet. Falling back to the supplied `search`
377
- * is only used in non-browser environments (SSR, unit tests without jsdom).
405
+ * The strip always operates on the live URL, because consumers typically call
406
+ * this from a component whose captured `search` snapshot may be stale — for
407
+ * example when the grid has just written its own keys to the URL via the
408
+ * router adapter but the parent hasn't re-rendered yet. The live URL is
409
+ * `getSearch()` when given, otherwise `window.location.search`. The supplied
410
+ * `search` is only used in non-browser environments (SSR, unit tests without
411
+ * jsdom).
412
+ *
413
+ * Under a browser router `window.location` is the router's URL. Under a memory
414
+ * history it is not, so pass `getSearch` (see `StatefulDataGridResetOptions`).
378
415
  *
379
416
  * The write is performed via `historyReplace(newSearch, { authoritative: true })`.
380
417
  * Adapters with delta/coalescing logic (e.g. `createReactRouterV5Adapter`)
@@ -387,20 +424,14 @@ declare const clearAllVersionStorage: (id: string, maxVersion: number) => void;
387
424
  * const { search, historyReplace } = useMyRouter();
388
425
  * resetStatefulDataGridState({ id: 'myGrid', versions: [0, 1, 2], search, historyReplace });
389
426
  * ```
427
+ *
428
+ * Under a memory history, with `createReactRouterV5Adapter`:
429
+ * ```ts
430
+ * const { pathname, getSearch, historyReplace } = useRouter();
431
+ * resetStatefulDataGridState({ id: pathname, versions: [0, 1], getSearch, historyReplace });
432
+ * ```
390
433
  */
391
- declare const resetStatefulDataGridState: ({ id, versions, search, historyReplace, }: {
392
- id: string;
393
- versions: number[];
394
- /**
395
- * Fallback search string used only when `window.location.search` is
396
- * unavailable (SSR, non-jsdom tests). In a browser the live URL is used
397
- * regardless of this value.
398
- */
399
- search: string;
400
- historyReplace: (newSearch: string, options?: {
401
- authoritative?: boolean;
402
- }) => void;
403
- }) => void;
434
+ declare const resetStatefulDataGridState: ({ id, versions, search, getSearch, historyReplace, }: StatefulDataGridResetOptions) => void;
404
435
  /**
405
436
  * Reset only the column-visibility state of a `StatefulDataGrid` back to its
406
437
  * default (all columns visible). Clears the persisted `visibilityModel`
@@ -415,9 +446,10 @@ declare const resetStatefulDataGridState: ({ id, versions, search, historyReplac
415
446
  * other params alone it will not clobber a filter the caller is writing in the
416
447
  * same transition.
417
448
  *
418
- * Like `resetStatefulDataGridState`, the URL strip reads `window.location.search`
419
- * directly so it operates on the live URL (the caller's captured snapshot may
420
- * be stale), and writes via `historyReplace(newSearch, { authoritative: true })`.
449
+ * Like `resetStatefulDataGridState`, the URL strip operates on the live URL
450
+ * (the caller's captured snapshot may be stale): `getSearch()` when given,
451
+ * otherwise `window.location.search`. Pass `getSearch` under a memory history.
452
+ * It writes via `historyReplace(newSearch, { authoritative: true })`.
421
453
  *
422
454
  * This clears the *persisted* state only. The caller is responsible for
423
455
  * resetting the *live* grid in the same tick:
@@ -426,19 +458,7 @@ declare const resetStatefulDataGridState: ({ id, versions, search, historyReplac
426
458
  * apiRef.current?.setColumnVisibilityModel?.({});
427
459
  * ```
428
460
  */
429
- declare const resetColumnVisibility: ({ id, versions, search, historyReplace, }: {
430
- id: string;
431
- versions: number[];
432
- /**
433
- * Fallback search string used only when `window.location.search` is
434
- * unavailable (SSR, non-jsdom tests). In a browser the live URL is used
435
- * regardless of this value.
436
- */
437
- search: string;
438
- historyReplace: (newSearch: string, options?: {
439
- authoritative?: boolean;
440
- }) => void;
441
- }) => void;
461
+ declare const resetColumnVisibility: ({ id, versions, search, getSearch, historyReplace, }: StatefulDataGridResetOptions) => void;
442
462
 
443
463
  /**
444
464
  * Type for legacy array-based row selection (v7 and earlier)
@@ -818,16 +838,25 @@ interface StatefulDataGridProps extends DataGridProps {
818
838
  }
819
839
 
820
840
  /**
821
- * StatefulDataGrid extends DataGrid with automatic state persistence to localStorage.
822
- * Preserves filters, sorting, pagination, column visibility, and column widths
823
- * across page refreshes.
841
+ * StatefulDataGrid extends DataGrid with automatic state persistence to the URL and localStorage.
842
+ * Filters, sorting, pagination, column visibility, column order, pinned
843
+ * columns, density, row grouping, aggregation and pivot are written to the URL
844
+ * query string and to localStorage; column widths are kept in localStorage
845
+ * only. On load the URL wins, then localStorage, then `initialState`, so state
846
+ * survives page refreshes and a copied URL reopens the same view.
824
847
  *
825
- * Use when users need persistent table preferences. Requires unique `localStorageKey`.
848
+ * Use when users need persistent table preferences or shareable filtered views.
849
+ * Requires a `useRouter` hook returning `{ pathname, search, historyReplace }`
850
+ * (`createReactRouterV5Adapter` builds one from a React Router v5 `history`).
851
+ * State is stored in localStorage under the `pathname` it returns, and
852
+ * versioned by `localStorageVersion` (a number, default `1`).
826
853
  *
827
854
  * @example
828
- * // Basic stateful grid (persists to localStorage)
855
+ * // Basic stateful grid (URL + localStorage)
856
+ * const useRouter = createReactRouterV5Adapter(history);
857
+ *
829
858
  * <StatefulDataGrid
830
- * localStorageKey="users-table"
859
+ * useRouter={useRouter}
831
860
  * columns={columns}
832
861
  * rows={users}
833
862
  * pagination
@@ -836,34 +865,24 @@ interface StatefulDataGridProps extends DataGridProps {
836
865
  * @example
837
866
  * // With version migration (clears old preferences)
838
867
  * <StatefulDataGrid
839
- * localStorageKey="reports-table"
840
- * localStorageVersion="v2"
841
- * previousLocalStorageVersions={['v1']}
868
+ * useRouter={useRouter}
869
+ * localStorageVersion={2}
870
+ * previousLocalStorageVersions={[1]}
842
871
  * columns={columns}
843
872
  * rows={reports}
844
873
  * />
845
874
  *
846
875
  * @example
847
- * // With URL state sync (shareable filtered views)
848
- * <StatefulDataGrid
849
- * localStorageKey="domains-table"
850
- * useRouter={true}
851
- * columns={columns}
852
- * rows={domains}
853
- * pagination
854
- * />
855
- *
856
- * @example
857
876
  * // All features combined
858
877
  * <StatefulDataGrid
859
- * localStorageKey="audit-log"
860
- * localStorageVersion="v3"
861
- * previousLocalStorageVersions={['v1', 'v2']}
862
- * useRouter={true}
878
+ * useRouter={useRouter}
879
+ * localStorageVersion={3}
880
+ * previousLocalStorageVersions={[1, 2]}
863
881
  * columns={columns}
864
882
  * rows={logs}
865
883
  * pagination
866
- * pageSize={50}
884
+ * pageSizeOptions={[25, 50, 100]}
885
+ * initialState={{ pagination: { paginationModel: { page: 0, pageSize: 50 } } }}
867
886
  * checkboxSelection
868
887
  * />
869
888
  */
@@ -1112,6 +1131,14 @@ declare const areFilterModelsEquivalent: (filterModel: GridFilterModel, filterMo
1112
1131
  * `historyReplace`. Authoritative writes bypass the delta logic and write
1113
1132
  * synchronously to the live URL.
1114
1133
  *
1134
+ * The router also returns `getSearch`, which reads `history.location.search`
1135
+ * when it is called rather than when `useRouter()` ran. Pass it to
1136
+ * `resetStatefulDataGridState` and `resetColumnVisibility` so that they strip
1137
+ * the router's live URL. Under a memory history (`createMemoryHistory`, e.g.
1138
+ * a sift rendered in an iframe) this is required: without it they read
1139
+ * `window.location.search`, which is the document's URL and not the router's.
1140
+ * Its identity is stable for the adapter's lifetime.
1141
+ *
1115
1142
  * @example
1116
1143
  * ```tsx
1117
1144
  * import { createReactRouterV5Adapter } from '@redsift/table';
@@ -1119,6 +1146,10 @@ declare const areFilterModelsEquivalent: (filterModel: GridFilterModel, filterMo
1119
1146
  * const useRouter = createReactRouterV5Adapter(history);
1120
1147
  *
1121
1148
  * <StatefulDataGrid useRouter={useRouter} … />
1149
+ *
1150
+ * // Reset, reading the router's URL (required under a memory history):
1151
+ * const { pathname, getSearch, historyReplace } = useRouter();
1152
+ * resetStatefulDataGridState({ id: pathname, versions: [0, 1], getSearch, historyReplace });
1122
1153
  * ```
1123
1154
  */
1124
1155
  /** Minimal subset of React Router v5 `history` used by the adapter. */
@@ -1139,6 +1170,8 @@ interface ReactRouterV5History {
1139
1170
  declare const createReactRouterV5Adapter: (history: ReactRouterV5History) => (() => {
1140
1171
  pathname: string;
1141
1172
  search: string;
1173
+ /** Reads the router's live query string, without the leading `?`. */
1174
+ getSearch: () => string;
1142
1175
  historyReplace: (newSearch: string, options?: {
1143
1176
  authoritative?: boolean;
1144
1177
  }) => void;
@@ -1422,4 +1455,4 @@ type ToolbarWrapperProps = {
1422
1455
  */
1423
1456
  declare const ToolbarWrapper: React$1.FC<ToolbarWrapperProps>;
1424
1457
 
1425
- export { AGGREGATION_MODEL_KEY, ARRAY_IS_EMPTY, ARRAY_IS_NOT_EMPTY, BaseButton, BaseCheckbox, BaseIcon, BaseIconButton, BaseTextField, CATEGORIES, COLUMN_ORDER_MODEL_KEY, CONTAINS_ANY_OF, Category, ColumnOptions, ColumnOrderModel, ColumnType, CompletionResponse, ControlledPagination, ControlledPaginationProps, DATE_IS_BETWEEN, DEFAULT_OPERATORS, DENSITY_MODEL_KEY, DETAIL_PANEL_TOGGLE_COL_DEF, DIMENSION_MODEL_KEY, DOES_NOT_CONTAIN, DOES_NOT_CONTAIN_ANY_OF, DOES_NOT_END_WITH, DOES_NOT_END_WITH_ANY_OF, DOES_NOT_EQUAL, DOES_NOT_HAVE, DOES_NOT_HAVE_ANY_OF, DOES_NOT_HAVE_ANY_OF_WITH_SELECT, DOES_NOT_HAVE_WITH_SELECT, DOES_NOT_START_WITH, DOES_NOT_START_WITH_ANY_OF, DataGrid, DataGridModel, DataGridProps, DateOperatorName, EMPTY_ROW_SELECTION_MODEL, ENDS_WITH_ANY_OF, FILTER_MODEL_KEY, FILTER_SEARCH_KEY, FilterConfig, FilterConfigColumn, GridDensityValue, GridToolbarFilterSemanticField, GridToolbarFilterSemanticFieldProps, HAS, HAS_ALL_OF, HAS_ALL_OF_WITH_SELECT, HAS_ANY_OF, HAS_ANY_OF_WITH_SELECT, HAS_ONLY, HAS_ONLY_WITH_SELECT, HAS_WITH_SELECT, IS, IS_ANY_OF, IS_ANY_OF_I, IS_ANY_OF_I_WITH_SELECT, IS_ANY_OF_WITH_SELECT, IS_BETWEEN, IS_NOT, IS_NOT_ANY_OF, IS_NOT_ANY_OF_WITH_SELECT, IS_NOT_WITH_SELECT, IS_WITH_SELECT, LegacyRowSelectionModel, MultiSelectOperatorName, NumberOperatorName, PAGINATION_MODEL_KEY, PINNED_COLUMNS, PIVOT_ACTIVE_KEY, PIVOT_MODEL_KEY, PinnedColumns, PivotModel, ROW_GROUPING_MODEL_KEY, ReactRouterV5History, RowSelectionModelInput, SORT_MODEL_KEY, STARTS_WITH_ANY_OF, SelectionStatus, ServerSideControlledPagination, ServerSideControlledPaginationProps, SingleSelectOperatorName, StatefulDataGrid, StatefulDataGridProps, StringOperatorName, StyledDataGridProps, TagsOperatorName, TextCell, Toolbar, ToolbarWrapper, ToolbarWrapperProps, UseLinkedFilterModelOptions, UseLinkedFilterModelReturn, VISIBILITY_MODEL_KEY, areFilterModelsEquivalent, areSearchStringsEqual, buildQueryParamsString, buildStorageKey, clearAllVersionStorage, clearPreviousVersionStorage, convertFromDisplayFormat, convertToDisplayFormat, createColumn, createReactRouterV5Adapter, createRowSelectionModel, customColumnTypes, decodeValue, encodeValue, fixServerSideHeaderCheckboxSelection, fromGridPivotModel, getAggregationFromString, getColumnOrderFromString, getColumnVisibilityFromString, getCompletion, getDecodedSearchFromUrl, getDensityFromString, getDensityModel, getFilterModelFromString, getFinalSearch, getGridDateOperatorsExtended, getGridNumericOperators, getGridStringArrayOperators, getGridStringArrayOperatorsWithSelect, getGridStringArrayOperatorsWithSelectOnStringArrayColumns, getGridStringOperators, getModelsParsedOrUpdateLocalStorage, getPaginationFromString, getPinnedColumnsFromString, getPivotActiveFromString, getPivotFromString, getRowGroupingFromString, getRsMultipleSelectColumnType, getRsMultipleSelectWithShortOperatorListColumnType, getRsNumberColumnType, getRsSingleSelectColumnType, getRsSingleSelectWithShortOperatorListColumnType, getRsStringColumnType, getSearchParamsFromAggregation, getSearchParamsFromColumnOrder, getSearchParamsFromColumnVisibility, getSearchParamsFromDensity, getSearchParamsFromFilterModel, getSearchParamsFromPagination, getSearchParamsFromPinnedColumns, getSearchParamsFromPivot, getSearchParamsFromPivotActive, getSearchParamsFromRowGrouping, getSearchParamsFromSorting, getSearchParamsFromTab, getSearchParamsFromVersion, getSelectableRowIdsInPage, getSelectedIds, getSelectionCount, getSortingFromString, isOperatorValueValid, isRowSelected, isValueValid, makeCaseInsensitive, muiIconToDSIcon, normalizeDateValue, normalizeRowSelectionModel, numberOperatorDecoder, numberOperatorEncoder, onServerSideSelectionStatusChange, operatorList, resetColumnVisibility, resetStatefulDataGridState, updateUrl, urlSearchParamsToString, useLinkedFilterModel, wrapCaseInsensitive };
1458
+ export { AGGREGATION_MODEL_KEY, ARRAY_IS_EMPTY, ARRAY_IS_NOT_EMPTY, BaseButton, BaseCheckbox, BaseIcon, BaseIconButton, BaseTextField, CATEGORIES, COLUMN_ORDER_MODEL_KEY, CONTAINS_ANY_OF, Category, ColumnOptions, ColumnOrderModel, ColumnType, CompletionResponse, ControlledPagination, ControlledPaginationProps, DATE_IS_BETWEEN, DEFAULT_OPERATORS, DENSITY_MODEL_KEY, DETAIL_PANEL_TOGGLE_COL_DEF, DIMENSION_MODEL_KEY, DOES_NOT_CONTAIN, DOES_NOT_CONTAIN_ANY_OF, DOES_NOT_END_WITH, DOES_NOT_END_WITH_ANY_OF, DOES_NOT_EQUAL, DOES_NOT_HAVE, DOES_NOT_HAVE_ANY_OF, DOES_NOT_HAVE_ANY_OF_WITH_SELECT, DOES_NOT_HAVE_WITH_SELECT, DOES_NOT_START_WITH, DOES_NOT_START_WITH_ANY_OF, DataGrid, DataGridModel, DataGridProps, DateOperatorName, EMPTY_ROW_SELECTION_MODEL, ENDS_WITH_ANY_OF, FILTER_MODEL_KEY, FILTER_SEARCH_KEY, FilterConfig, FilterConfigColumn, GridDensityValue, GridToolbarFilterSemanticField, GridToolbarFilterSemanticFieldProps, HAS, HAS_ALL_OF, HAS_ALL_OF_WITH_SELECT, HAS_ANY_OF, HAS_ANY_OF_WITH_SELECT, HAS_ONLY, HAS_ONLY_WITH_SELECT, HAS_WITH_SELECT, IS, IS_ANY_OF, IS_ANY_OF_I, IS_ANY_OF_I_WITH_SELECT, IS_ANY_OF_WITH_SELECT, IS_BETWEEN, IS_NOT, IS_NOT_ANY_OF, IS_NOT_ANY_OF_WITH_SELECT, IS_NOT_WITH_SELECT, IS_WITH_SELECT, LegacyRowSelectionModel, MultiSelectOperatorName, NumberOperatorName, PAGINATION_MODEL_KEY, PINNED_COLUMNS, PIVOT_ACTIVE_KEY, PIVOT_MODEL_KEY, PinnedColumns, PivotModel, ROW_GROUPING_MODEL_KEY, ReactRouterV5History, RowSelectionModelInput, SORT_MODEL_KEY, STARTS_WITH_ANY_OF, SelectionStatus, ServerSideControlledPagination, ServerSideControlledPaginationProps, SingleSelectOperatorName, StatefulDataGrid, StatefulDataGridProps, StatefulDataGridResetOptions, StringOperatorName, StyledDataGridProps, TagsOperatorName, TextCell, Toolbar, ToolbarWrapper, ToolbarWrapperProps, UseLinkedFilterModelOptions, UseLinkedFilterModelReturn, VISIBILITY_MODEL_KEY, areFilterModelsEquivalent, areSearchStringsEqual, buildQueryParamsString, buildStorageKey, clearAllVersionStorage, clearPreviousVersionStorage, convertFromDisplayFormat, convertToDisplayFormat, createColumn, createReactRouterV5Adapter, createRowSelectionModel, customColumnTypes, decodeValue, encodeValue, fixServerSideHeaderCheckboxSelection, fromGridPivotModel, getAggregationFromString, getColumnOrderFromString, getColumnVisibilityFromString, getCompletion, getDecodedSearchFromUrl, getDensityFromString, getDensityModel, getFilterModelFromString, getFinalSearch, getGridDateOperatorsExtended, getGridNumericOperators, getGridStringArrayOperators, getGridStringArrayOperatorsWithSelect, getGridStringArrayOperatorsWithSelectOnStringArrayColumns, getGridStringOperators, getModelsParsedOrUpdateLocalStorage, getPaginationFromString, getPinnedColumnsFromString, getPivotActiveFromString, getPivotFromString, getRowGroupingFromString, getRsMultipleSelectColumnType, getRsMultipleSelectWithShortOperatorListColumnType, getRsNumberColumnType, getRsSingleSelectColumnType, getRsSingleSelectWithShortOperatorListColumnType, getRsStringColumnType, getSearchParamsFromAggregation, getSearchParamsFromColumnOrder, getSearchParamsFromColumnVisibility, getSearchParamsFromDensity, getSearchParamsFromFilterModel, getSearchParamsFromPagination, getSearchParamsFromPinnedColumns, getSearchParamsFromPivot, getSearchParamsFromPivotActive, getSearchParamsFromRowGrouping, getSearchParamsFromSorting, getSearchParamsFromTab, getSearchParamsFromVersion, getSelectableRowIdsInPage, getSelectedIds, getSelectionCount, getSortingFromString, isOperatorValueValid, isRowSelected, isValueValid, makeCaseInsensitive, muiIconToDSIcon, normalizeDateValue, normalizeRowSelectionModel, numberOperatorDecoder, numberOperatorEncoder, onServerSideSelectionStatusChange, operatorList, resetColumnVisibility, resetStatefulDataGridState, updateUrl, urlSearchParamsToString, useLinkedFilterModel, wrapCaseInsensitive };
package/index.js CHANGED
@@ -441,6 +441,14 @@ function getOperatorsForType(type, compact) {
441
441
  * `historyReplace`. Authoritative writes bypass the delta logic and write
442
442
  * synchronously to the live URL.
443
443
  *
444
+ * The router also returns `getSearch`, which reads `history.location.search`
445
+ * when it is called rather than when `useRouter()` ran. Pass it to
446
+ * `resetStatefulDataGridState` and `resetColumnVisibility` so that they strip
447
+ * the router's live URL. Under a memory history (`createMemoryHistory`, e.g.
448
+ * a sift rendered in an iframe) this is required: without it they read
449
+ * `window.location.search`, which is the document's URL and not the router's.
450
+ * Its identity is stable for the adapter's lifetime.
451
+ *
444
452
  * @example
445
453
  * ```tsx
446
454
  * import { createReactRouterV5Adapter } from '@redsift/table';
@@ -448,6 +456,10 @@ function getOperatorsForType(type, compact) {
448
456
  * const useRouter = createReactRouterV5Adapter(history);
449
457
  *
450
458
  * <StatefulDataGrid useRouter={useRouter} … />
459
+ *
460
+ * // Reset, reading the router's URL (required under a memory history):
461
+ * const { pathname, getSearch, historyReplace } = useRouter();
462
+ * resetStatefulDataGridState({ id: pathname, versions: [0, 1], getSearch, historyReplace });
451
463
  * ```
452
464
  */
453
465
 
@@ -481,18 +493,23 @@ const createReactRouterV5Adapter = history => {
481
493
  });
482
494
  });
483
495
  };
496
+
497
+ // Read at call time, unlike the `search` snapshot below. Defined once so its
498
+ // identity is stable across renders.
499
+ const getSearch = () => history.location.search.replace(/^\?/, '');
484
500
  return () => {
485
501
  // Capture the search snapshot AT THE TIME useRouter() is called. The
486
502
  // grid hands this string to its change handlers, which build a complete
487
503
  // new search string from it. Diffing the handler's `newSearch` against
488
504
  // this captured snapshot tells us which keys the handler intentionally
489
505
  // changed (vs. which are stale passthrough from sibling handlers' models).
490
- const captured = history.location.search.replace(/^\?/, '');
506
+ const captured = getSearch();
491
507
  const capturedParams = new URLSearchParams(captured);
492
508
  const capturedKeys = new Set(capturedParams.keys());
493
509
  return {
494
510
  pathname: history.location.pathname,
495
511
  search: captured,
512
+ getSearch,
496
513
  historyReplace: (newSearch, options) => {
497
514
  // Authoritative writes (e.g. `resetStatefulDataGridState`) bypass the
498
515
  // delta-vs-captured-snapshot logic. The caller asserts that `newSearch`