@jielga/tmdatagrid 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +302 -11
- package/dist/index.js +2338 -2097
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/columns.md +9 -1
- package/docs/components.md +1 -0
- package/docs/filtering.md +5 -1
- package/docs/grouping.md +1 -1
- package/docs/localization.md +3 -3
- package/docs/persistence.md +189 -2
- package/docs/row-details.md +5 -0
- package/docs/row-interaction.md +1 -1
- package/docs/row-styling.md +17 -0
- package/docs/typed-deep-links.md +169 -0
- package/docs/use-tm-data-grid.md +6 -2
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +1 -1
- package/skills/appearance/references/labels.md +3 -3
- package/skills/cell-selection/SKILL.md +1 -1
- package/skills/columns/SKILL.md +6 -3
- package/skills/columns/references/columns-api.md +1 -0
- package/skills/data/SKILL.md +1 -1
- package/skills/editing/SKILL.md +1 -1
- package/skills/filtering/SKILL.md +26 -13
- package/skills/getting-started/SKILL.md +2 -2
- package/skills/grouping/SKILL.md +2 -2
- package/skills/migrating-to-2/SKILL.md +1 -1
- package/skills/options/SKILL.md +105 -10
- package/skills/options/references/typed-links.md +134 -0
- package/skills/rows/SKILL.md +18 -9
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +1 -1
- package/skills/testing-components/SKILL.md +1 -1
- package/skills/testing-editing/SKILL.md +1 -1
- package/src/components/TMDataGridFilterPills.module.css +9 -7
- package/src/components/TMDataGridFilterPills.tsx +15 -1
- package/src/components/TMDataGridGroupColumn.tsx +19 -4
- package/src/components/TMDataGridHeaderFilterRow.tsx +7 -1
- package/src/components/TMDataGridTable.module.css +15 -6
- package/src/components/TMDataGridTable.tsx +23 -11
- package/src/components/sticky.module.css +27 -7
- package/src/core/filterControls.ts +8 -0
- package/src/core/filterOperators.ts +54 -18
- package/src/core/grouping.ts +11 -3
- package/src/core/labels.ts +4 -3
- package/src/core/persistence.ts +330 -26
- package/src/core/urlState.ts +439 -0
- package/src/core/valueText.ts +46 -0
- package/src/index.ts +14 -0
- package/src/useTMDataGrid.tsx +145 -21
package/src/core/persistence.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { TableState } from "@tanstack/react-table";
|
|
2
2
|
import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
3
|
+
import { sameStateValue } from "./controlledState";
|
|
4
|
+
import { filterNarrowsRows } from "./filterOperators";
|
|
3
5
|
|
|
4
6
|
type GridState = TableState<TMDataGridFeatures>;
|
|
5
7
|
|
|
@@ -65,11 +67,32 @@ export type TMDataGridPersistKey<TSlice extends string> =
|
|
|
65
67
|
/** Same values as Mantine's `StorageType`, which the package does not re-export. */
|
|
66
68
|
export type TMDataGridStorageMode = "localStorage" | "sessionStorage";
|
|
67
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Reads and writes the one URL search param that `urlKey` names. Replaces the
|
|
72
|
+
* default, which reads `window.location.search` and writes with
|
|
73
|
+
* `history.replaceState`, for a router that owns the URL.
|
|
74
|
+
*/
|
|
75
|
+
export type TMDataGridUrlStorage = {
|
|
76
|
+
/** The param's current value, or `null` when the URL has none. */
|
|
77
|
+
get(name: string): string | null;
|
|
78
|
+
/** Sets the param to `value`, or removes it when `value` is `null`. */
|
|
79
|
+
set(name: string, value: string | null): void;
|
|
80
|
+
};
|
|
81
|
+
|
|
68
82
|
export type TMDataGridPersistence = {
|
|
69
83
|
/** Key for filters, global filter, sorting and pagination. */
|
|
70
84
|
dataKey?: TMDataGridPersistKey<TMDataGridDataSlice>;
|
|
71
85
|
/** Key for column visibility, sizing, order and pinning. */
|
|
72
86
|
settingsKey?: TMDataGridPersistKey<TMDataGridSettingsSlice>;
|
|
87
|
+
/**
|
|
88
|
+
* Search param for data slices kept in the URL, so a link opens the grid on
|
|
89
|
+
* the same state. Takes the slices it lists away from `dataKey`. The param
|
|
90
|
+
* holds only the slices that differ from the initial state, and carries no
|
|
91
|
+
* version stamp.
|
|
92
|
+
*/
|
|
93
|
+
urlKey?: TMDataGridPersistKey<TMDataGridDataSlice>;
|
|
94
|
+
/** Where `urlKey` reads and writes. Defaults to the browser's URL. */
|
|
95
|
+
urlStorage?: TMDataGridUrlStorage;
|
|
73
96
|
/** Which storage area to use. Defaults to `"localStorage"`. */
|
|
74
97
|
storageMode?: TMDataGridStorageMode;
|
|
75
98
|
/** Serializes a payload before it is stored. Defaults to `JSON.stringify`. */
|
|
@@ -104,6 +127,59 @@ function resolveKey<TSlice extends string>(
|
|
|
104
127
|
return { key, slices: slices as ReadonlyArray<keyof GridState> };
|
|
105
128
|
}
|
|
106
129
|
|
|
130
|
+
/**
|
|
131
|
+
* The browser's URL. Writes replace the current history entry and never push
|
|
132
|
+
* one: a filter change is not a navigation, and Back should leave the page
|
|
133
|
+
* rather than undo filters one at a time. Path, hash, every other param and
|
|
134
|
+
* `history.state` are kept - a router keeps its own bookkeeping there.
|
|
135
|
+
*/
|
|
136
|
+
const defaultUrlStorage: TMDataGridUrlStorage = {
|
|
137
|
+
get: (name) => new URLSearchParams(window.location.search).get(name),
|
|
138
|
+
set: (name, value) => {
|
|
139
|
+
// Spliced by hand rather than through `URLSearchParams`, whose serializer
|
|
140
|
+
// re-encodes every param (`/a%20b` comes back as `%2Fa+b`). Only the
|
|
141
|
+
// grid's own pair changes; every other entry is kept byte for byte.
|
|
142
|
+
const { pathname, search, hash, href } = window.location;
|
|
143
|
+
const entries = search
|
|
144
|
+
.replace(/^\?/, "")
|
|
145
|
+
.split("&")
|
|
146
|
+
.filter((entry) => entry !== "");
|
|
147
|
+
const kept = entries.filter((entry) => paramEntryKey(entry) !== name);
|
|
148
|
+
if (value === null && kept.length === entries.length) return;
|
|
149
|
+
if (value !== null) {
|
|
150
|
+
kept.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
|
|
151
|
+
}
|
|
152
|
+
const next = `${pathname}${kept.length === 0 ? "" : `?${kept.join("&")}`}${hash}`;
|
|
153
|
+
if (new URL(next, href).href === href) return;
|
|
154
|
+
window.history.replaceState(window.history.state, "", next);
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** The decoded key of one `key=value` query entry, `+` read as a space. */
|
|
159
|
+
function paramEntryKey(entry: string): string {
|
|
160
|
+
const separator = entry.indexOf("=");
|
|
161
|
+
const raw = (separator === -1 ? entry : entry.slice(0, separator)).replaceAll(
|
|
162
|
+
"+",
|
|
163
|
+
" ",
|
|
164
|
+
);
|
|
165
|
+
try {
|
|
166
|
+
return decodeURIComponent(raw);
|
|
167
|
+
} catch {
|
|
168
|
+
// A malformed escape cannot be the grid's own key, which it encoded.
|
|
169
|
+
return raw;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function resolveUrlStorage(
|
|
174
|
+
persistence: TMDataGridPersistence | undefined,
|
|
175
|
+
): TMDataGridUrlStorage | null {
|
|
176
|
+
if (!persistence) return null;
|
|
177
|
+
if (persistence.urlStorage) return persistence.urlStorage;
|
|
178
|
+
// Absent under SSR, like web storage. Access that throws is caught where the
|
|
179
|
+
// param is read or written.
|
|
180
|
+
return typeof window === "undefined" ? null : defaultUrlStorage;
|
|
181
|
+
}
|
|
182
|
+
|
|
107
183
|
function resolveStorage(
|
|
108
184
|
persistence: TMDataGridPersistence | undefined,
|
|
109
185
|
): Storage | null {
|
|
@@ -213,30 +289,44 @@ function isValidSlice(slice: keyof GridState, value: unknown): boolean {
|
|
|
213
289
|
return guard === undefined || guard(value);
|
|
214
290
|
}
|
|
215
291
|
|
|
216
|
-
function readSlices(
|
|
217
|
-
|
|
218
|
-
resolved
|
|
219
|
-
deserialize
|
|
220
|
-
|
|
221
|
-
|
|
292
|
+
function readSlices({
|
|
293
|
+
read,
|
|
294
|
+
resolved,
|
|
295
|
+
deserialize,
|
|
296
|
+
versioned,
|
|
297
|
+
}: {
|
|
298
|
+
/** Returns the raw payload stored under a key, or `null`. */
|
|
299
|
+
read: ((key: string) => string | null) | null;
|
|
300
|
+
resolved: ResolvedKey | null;
|
|
301
|
+
deserialize: (value: string) => unknown;
|
|
302
|
+
/** Whether the payload must carry the current version stamp. */
|
|
303
|
+
versioned: boolean;
|
|
304
|
+
}): Partial<GridState> {
|
|
305
|
+
if (!read || !resolved) return {};
|
|
222
306
|
try {
|
|
223
|
-
const raw =
|
|
307
|
+
const raw = read(resolved.key);
|
|
224
308
|
if (!raw) return {};
|
|
225
309
|
const parsed = deserialize(raw);
|
|
226
310
|
if (typeof parsed !== "object" || parsed === null) return {};
|
|
227
311
|
|
|
228
312
|
const record = parsed as Record<string, unknown>;
|
|
229
|
-
if (record[VERSION_FIELD] !== PERSIST_PAYLOAD_VERSION)
|
|
313
|
+
if (versioned && record[VERSION_FIELD] !== PERSIST_PAYLOAD_VERSION) {
|
|
314
|
+
return {};
|
|
315
|
+
}
|
|
230
316
|
const restored: Record<string, unknown> = {};
|
|
231
317
|
// Only copy the selected slices. A payload written before the selection was
|
|
232
318
|
// narrowed cannot reintroduce slices the caller has since opted out of.
|
|
233
319
|
for (const slice of resolved.slices) {
|
|
234
320
|
if (!(slice in record)) continue;
|
|
235
|
-
|
|
321
|
+
// A `null` slice stands for `undefined` - see the writers. An own key
|
|
322
|
+
// holding `undefined` still overrides `initialState` in the merge, which
|
|
323
|
+
// is the point: the user cleared the slice. The slice's guard then
|
|
324
|
+
// judges `undefined`, so only a slice that may be `undefined` (the
|
|
325
|
+
// untyped global filter) restores that way.
|
|
326
|
+
const value = record[slice] === null ? undefined : record[slice];
|
|
327
|
+
if (!isValidSlice(slice, value)) continue;
|
|
236
328
|
restored[slice] =
|
|
237
|
-
slice === "columnPinning"
|
|
238
|
-
? normalizeColumnPinning(record[slice])
|
|
239
|
-
: record[slice];
|
|
329
|
+
slice === "columnPinning" ? normalizeColumnPinning(value) : value;
|
|
240
330
|
}
|
|
241
331
|
return restored as Partial<GridState>;
|
|
242
332
|
} catch {
|
|
@@ -252,7 +342,13 @@ function writeSlices(
|
|
|
252
342
|
): void {
|
|
253
343
|
if (!storage || !resolved) return;
|
|
254
344
|
const payload: Record<string, unknown> = { [VERSION_FIELD]: PERSIST_PAYLOAD_VERSION };
|
|
255
|
-
for (const slice of resolved.slices)
|
|
345
|
+
for (const slice of resolved.slices) {
|
|
346
|
+
// `null` for a slice the user cleared to `undefined` - the global filter
|
|
347
|
+
// over an `initialState.globalFilter`. JSON drops an `undefined` key, and
|
|
348
|
+
// a missing slice means the initial state, so a reload would come back on
|
|
349
|
+
// the very value the user cleared. readSlices turns it back.
|
|
350
|
+
payload[slice] = state[slice] === undefined ? null : state[slice];
|
|
351
|
+
}
|
|
256
352
|
try {
|
|
257
353
|
storage.setItem(resolved.key, serialize(payload as Partial<GridState>));
|
|
258
354
|
} catch {
|
|
@@ -260,9 +356,100 @@ function writeSlices(
|
|
|
260
356
|
}
|
|
261
357
|
}
|
|
262
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Writes the URL payload: the selected slices that differ from `baseline`,
|
|
361
|
+
* with no version stamp, or no param at all when none differs.
|
|
362
|
+
*
|
|
363
|
+
* Only the difference goes in, so a link names what the user changed and not
|
|
364
|
+
* what the page starts with anyway: the address bar stays short, and an old
|
|
365
|
+
* link does not pin a default the page has since changed. No stamp, because a
|
|
366
|
+
* bookmarked link has to survive a release; the shape guards and the
|
|
367
|
+
* realignment on read are what keep an old link safe.
|
|
368
|
+
*/
|
|
369
|
+
function writeUrlSlices({
|
|
370
|
+
urlStorage,
|
|
371
|
+
resolved,
|
|
372
|
+
state,
|
|
373
|
+
baseline,
|
|
374
|
+
excludedFilterColumnIds,
|
|
375
|
+
serialize,
|
|
376
|
+
}: {
|
|
377
|
+
urlStorage: TMDataGridUrlStorage | null;
|
|
378
|
+
resolved: ResolvedKey | null;
|
|
379
|
+
state: GridState;
|
|
380
|
+
baseline: Partial<GridState>;
|
|
381
|
+
excludedFilterColumnIds: ReadonlySet<string>;
|
|
382
|
+
serialize: (value: Partial<GridState>) => string;
|
|
383
|
+
}): void {
|
|
384
|
+
if (!urlStorage || !resolved) return;
|
|
385
|
+
const payload: Record<string, unknown> = {};
|
|
386
|
+
for (const slice of resolved.slices) {
|
|
387
|
+
// Both sides reduced the same way, so a change to an opted-out column or
|
|
388
|
+
// to a filter that narrows nothing does not put the param in the URL.
|
|
389
|
+
const value =
|
|
390
|
+
slice === "columnFilters"
|
|
391
|
+
? linkableColumnFilters(state.columnFilters, excludedFilterColumnIds)
|
|
392
|
+
: state[slice];
|
|
393
|
+
const initial =
|
|
394
|
+
slice === "columnFilters"
|
|
395
|
+
? linkableColumnFilters(baseline.columnFilters, excludedFilterColumnIds)
|
|
396
|
+
: baseline[slice];
|
|
397
|
+
if (sameStateValue(value, initial)) continue;
|
|
398
|
+
// `null` for a slice the user cleared to `undefined` - the global filter
|
|
399
|
+
// over an `initialState.globalFilter`. JSON drops an `undefined` key, and
|
|
400
|
+
// a missing slice means the initial state, so the link would reopen on
|
|
401
|
+
// the very value the user cleared. readSlices turns it back.
|
|
402
|
+
payload[slice] = value === undefined ? null : value;
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const next =
|
|
406
|
+
Object.keys(payload).length === 0
|
|
407
|
+
? null
|
|
408
|
+
: serialize(payload as Partial<GridState>);
|
|
409
|
+
// Unchanged: no write. A router-backed storage may navigate on every
|
|
410
|
+
// `set`, and the debounced write runs after a resize or a selection too.
|
|
411
|
+
if (urlStorage.get(resolved.key) === next) return;
|
|
412
|
+
urlStorage.set(resolved.key, next);
|
|
413
|
+
} catch {
|
|
414
|
+
// History access refused, or a router's own storage threw. Best effort,
|
|
415
|
+
// like storage.
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function withoutColumnFilters(
|
|
420
|
+
filters: GridState["columnFilters"] | undefined,
|
|
421
|
+
excluded: ReadonlySet<string>,
|
|
422
|
+
): GridState["columnFilters"] | undefined {
|
|
423
|
+
if (filters === undefined || excluded.size === 0) return filters;
|
|
424
|
+
return filters.filter((filter) => !excluded.has(filter.id));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The column filters a link carries: not on an opted-out column, and narrowing
|
|
429
|
+
* the rows. A filter that narrows nothing is a row the filter panel seeded or
|
|
430
|
+
* the user has not filled in yet; in a link it would only reopen an empty row.
|
|
431
|
+
*/
|
|
432
|
+
function linkableColumnFilters(
|
|
433
|
+
filters: GridState["columnFilters"] | undefined,
|
|
434
|
+
excluded: ReadonlySet<string>,
|
|
435
|
+
): GridState["columnFilters"] | undefined {
|
|
436
|
+
return withoutColumnFilters(filters, excluded)?.filter(filterNarrowsRows);
|
|
437
|
+
}
|
|
438
|
+
|
|
263
439
|
function resolveKeys(persistence: TMDataGridPersistence | undefined) {
|
|
440
|
+
const url = resolveKey(persistence?.urlKey, DATA_STATE_SLICES);
|
|
441
|
+
const data = resolveKey(persistence?.dataKey, DATA_STATE_SLICES);
|
|
264
442
|
return {
|
|
265
|
-
|
|
443
|
+
url,
|
|
444
|
+
// A slice lives in one group. The URL takes what both keys list, so a
|
|
445
|
+
// stored copy can never override the link that was opened.
|
|
446
|
+
data:
|
|
447
|
+
data !== null && url !== null
|
|
448
|
+
? {
|
|
449
|
+
key: data.key,
|
|
450
|
+
slices: data.slices.filter((slice) => !url.slices.includes(slice)),
|
|
451
|
+
}
|
|
452
|
+
: data,
|
|
266
453
|
settings: resolveKey(persistence?.settingsKey, SETTINGS_STATE_SLICES),
|
|
267
454
|
};
|
|
268
455
|
}
|
|
@@ -271,8 +458,27 @@ function resolveKeys(persistence: TMDataGridPersistence | undefined) {
|
|
|
271
458
|
export function hasPersistenceKeys(
|
|
272
459
|
persistence: TMDataGridPersistence | undefined,
|
|
273
460
|
): boolean {
|
|
274
|
-
const { data, settings } = resolveKeys(persistence);
|
|
275
|
-
return data !== null || settings !== null;
|
|
461
|
+
const { url, data, settings } = resolveKeys(persistence);
|
|
462
|
+
return url !== null || data !== null || settings !== null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* The data slices that a `dataKey` tuple names and `urlKey` also lists;
|
|
467
|
+
* `urlKey` takes them. A bare `dataKey` names no slice, so it reports nothing:
|
|
468
|
+
* "every data slice" plus a `urlKey` is the ordinary setup, and resolveKeys
|
|
469
|
+
* hands the URL its slices without a word. Only a slice named twice is a
|
|
470
|
+
* contradiction worth reporting.
|
|
471
|
+
*/
|
|
472
|
+
export function findOverlappingSlices(
|
|
473
|
+
persistence: TMDataGridPersistence | undefined,
|
|
474
|
+
): TMDataGridDataSlice[] {
|
|
475
|
+
if (typeof persistence?.dataKey === "string") return [];
|
|
476
|
+
const url = resolveKey(persistence?.urlKey, DATA_STATE_SLICES);
|
|
477
|
+
const data = resolveKey(persistence?.dataKey, DATA_STATE_SLICES);
|
|
478
|
+
if (url === null || data === null) return [];
|
|
479
|
+
return DATA_STATE_SLICES.filter(
|
|
480
|
+
(slice) => url.slices.includes(slice) && data.slices.includes(slice),
|
|
481
|
+
);
|
|
276
482
|
}
|
|
277
483
|
|
|
278
484
|
const defaultDeserialize = (value: string): unknown => JSON.parse(value);
|
|
@@ -290,11 +496,42 @@ export function collectLeafColumnIds(
|
|
|
290
496
|
defs: ReadonlyArray<unknown>,
|
|
291
497
|
into: string[] = [],
|
|
292
498
|
): string[] {
|
|
499
|
+
forEachLeafColumnDef(defs, (id) => into.push(id));
|
|
500
|
+
return into;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* The leaf column ids whose `meta.filter.url` is `false`, by the same id
|
|
505
|
+
* derivation as {@link collectLeafColumnIds}. Their filters stay out of the
|
|
506
|
+
* `urlKey` param, on read and on write.
|
|
507
|
+
*/
|
|
508
|
+
export function collectUrlExcludedColumnIds(
|
|
509
|
+
defs: ReadonlyArray<unknown>,
|
|
510
|
+
into: string[] = [],
|
|
511
|
+
): string[] {
|
|
512
|
+
forEachLeafColumnDef(defs, (id, record) => {
|
|
513
|
+
if (prop(prop(record.meta, "filter"), "url") === false) into.push(id);
|
|
514
|
+
});
|
|
515
|
+
return into;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Visits every leaf column definition under `defs` with the id TanStack gives
|
|
520
|
+
* it: `id`, else the `accessorKey` with dots replaced by underscores, else a
|
|
521
|
+
* string `header`.
|
|
522
|
+
*
|
|
523
|
+
* @internal Shared by persistence and the typed links in urlState.ts; not
|
|
524
|
+
* exported from the package.
|
|
525
|
+
*/
|
|
526
|
+
export function forEachLeafColumnDef(
|
|
527
|
+
defs: ReadonlyArray<unknown>,
|
|
528
|
+
visit: (id: string, record: Record<string, unknown>) => void,
|
|
529
|
+
): void {
|
|
293
530
|
for (const def of defs) {
|
|
294
531
|
if (typeof def !== "object" || def === null) continue;
|
|
295
532
|
const record = def as Record<string, unknown>;
|
|
296
533
|
if (Array.isArray(record.columns) && record.columns.length > 0) {
|
|
297
|
-
|
|
534
|
+
forEachLeafColumnDef(record.columns, visit);
|
|
298
535
|
continue;
|
|
299
536
|
}
|
|
300
537
|
const accessorKey =
|
|
@@ -303,9 +540,8 @@ export function collectLeafColumnIds(
|
|
|
303
540
|
(typeof record.id === "string" ? record.id : undefined) ??
|
|
304
541
|
accessorKey?.replaceAll(".", "_") ??
|
|
305
542
|
(typeof record.header === "string" ? record.header : undefined);
|
|
306
|
-
if (id !== undefined)
|
|
543
|
+
if (id !== undefined) visit(id, record);
|
|
307
544
|
}
|
|
308
|
-
return into;
|
|
309
545
|
}
|
|
310
546
|
|
|
311
547
|
/**
|
|
@@ -351,8 +587,25 @@ function realignToColumns(
|
|
|
351
587
|
return realigned;
|
|
352
588
|
}
|
|
353
589
|
|
|
590
|
+
/** What the `urlKey` group needs beyond the persistence config itself. */
|
|
591
|
+
export type TMDataGridUrlPersistInputs = {
|
|
592
|
+
/**
|
|
593
|
+
* The state the table starts with when nothing is restored: the grid's
|
|
594
|
+
* defaults merged with `initialState`. The URL carries only the slices that
|
|
595
|
+
* differ from it. Without it, every slice that has a value is written. On
|
|
596
|
+
* read, a column opted out of the URL keeps its filter from it.
|
|
597
|
+
*/
|
|
598
|
+
baseline?: Partial<GridState>;
|
|
599
|
+
/** See {@link collectUrlExcludedColumnIds}. */
|
|
600
|
+
excludedFilterColumnIds?: ReadonlyArray<string>;
|
|
601
|
+
/** Whether a write touches the URL. Defaults to `true`; ignored on read. */
|
|
602
|
+
writeUrl?: boolean;
|
|
603
|
+
};
|
|
604
|
+
|
|
354
605
|
/**
|
|
355
|
-
* State to merge into `initialState`, read once on mount
|
|
606
|
+
* State to merge into `initialState`, read once on mount: settings, then data,
|
|
607
|
+
* then the URL. The groups are disjoint, since the URL takes the slices it
|
|
608
|
+
* shares with `dataKey`.
|
|
356
609
|
*
|
|
357
610
|
* With `knownColumnIds` - every id the grid is about to construct, generated
|
|
358
611
|
* lanes included - the column-shaped slices are realigned first; see
|
|
@@ -361,28 +614,79 @@ function realignToColumns(
|
|
|
361
614
|
export function readPersistedState(
|
|
362
615
|
persistence?: TMDataGridPersistence,
|
|
363
616
|
knownColumnIds?: ReadonlyArray<string>,
|
|
617
|
+
{
|
|
618
|
+
baseline = {},
|
|
619
|
+
excludedFilterColumnIds = [],
|
|
620
|
+
}: TMDataGridUrlPersistInputs = {},
|
|
364
621
|
): Partial<GridState> {
|
|
365
622
|
const storage = resolveStorage(persistence);
|
|
366
|
-
const
|
|
623
|
+
const readStorage = storage ? (key: string) => storage.getItem(key) : null;
|
|
624
|
+
const urlStorage = resolveUrlStorage(persistence);
|
|
625
|
+
const readUrl = urlStorage ? (key: string) => urlStorage.get(key) : null;
|
|
626
|
+
const { url, data, settings } = resolveKeys(persistence);
|
|
367
627
|
const deserialize = persistence?.deserialize ?? defaultDeserialize;
|
|
628
|
+
const fromUrl = readSlices({
|
|
629
|
+
read: readUrl,
|
|
630
|
+
resolved: url,
|
|
631
|
+
deserialize,
|
|
632
|
+
versioned: false,
|
|
633
|
+
});
|
|
634
|
+
if (fromUrl.columnFilters) {
|
|
635
|
+
// The link never speaks for an opted-out column - the write leaves those
|
|
636
|
+
// out of both sides - so such a column keeps its initial filter, which
|
|
637
|
+
// the restored slice would otherwise replace along with the rest.
|
|
638
|
+
const excluded = new Set(excludedFilterColumnIds);
|
|
639
|
+
fromUrl.columnFilters = [
|
|
640
|
+
...(baseline.columnFilters ?? []).filter((filter) =>
|
|
641
|
+
excluded.has(filter.id),
|
|
642
|
+
),
|
|
643
|
+
...(withoutColumnFilters(fromUrl.columnFilters, excluded) ?? []),
|
|
644
|
+
];
|
|
645
|
+
}
|
|
368
646
|
const restored = {
|
|
369
|
-
...readSlices(
|
|
370
|
-
|
|
647
|
+
...readSlices({
|
|
648
|
+
read: readStorage,
|
|
649
|
+
resolved: settings,
|
|
650
|
+
deserialize,
|
|
651
|
+
versioned: true,
|
|
652
|
+
}),
|
|
653
|
+
...readSlices({
|
|
654
|
+
read: readStorage,
|
|
655
|
+
resolved: data,
|
|
656
|
+
deserialize,
|
|
657
|
+
versioned: true,
|
|
658
|
+
}),
|
|
659
|
+
...fromUrl,
|
|
371
660
|
};
|
|
372
661
|
return knownColumnIds === undefined
|
|
373
662
|
? restored
|
|
374
663
|
: realignToColumns(restored, new Set(knownColumnIds));
|
|
375
664
|
}
|
|
376
665
|
|
|
377
|
-
/** Writes
|
|
666
|
+
/** Writes every configured payload. Called on every table state change. */
|
|
378
667
|
export function writePersistedState(
|
|
379
668
|
state: GridState,
|
|
380
669
|
persistence?: TMDataGridPersistence,
|
|
670
|
+
{
|
|
671
|
+
baseline = {},
|
|
672
|
+
excludedFilterColumnIds = [],
|
|
673
|
+
writeUrl = true,
|
|
674
|
+
}: TMDataGridUrlPersistInputs = {},
|
|
381
675
|
): void {
|
|
676
|
+
const { url, data, settings } = resolveKeys(persistence);
|
|
677
|
+
const serialize = persistence?.serialize ?? defaultSerialize;
|
|
678
|
+
if (writeUrl) {
|
|
679
|
+
writeUrlSlices({
|
|
680
|
+
urlStorage: resolveUrlStorage(persistence),
|
|
681
|
+
resolved: url,
|
|
682
|
+
state,
|
|
683
|
+
baseline,
|
|
684
|
+
excludedFilterColumnIds: new Set(excludedFilterColumnIds),
|
|
685
|
+
serialize,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
382
688
|
const storage = resolveStorage(persistence);
|
|
383
689
|
if (!storage) return;
|
|
384
|
-
const { data, settings } = resolveKeys(persistence);
|
|
385
|
-
const serialize = persistence?.serialize ?? defaultSerialize;
|
|
386
690
|
writeSlices(storage, data, state, serialize);
|
|
387
691
|
writeSlices(storage, settings, state, serialize);
|
|
388
692
|
}
|