@trackunit/react-table 2.11.8 → 2.11.10-alpha-e1e6a0f33ce.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/index.cjs.js +305 -324
- package/index.esm.js +306 -325
- package/package.json +10 -10
- package/src/hooks/useTablePersistence/useTablePersistence.d.ts +41 -44
- package/src/hooks/useTablePersistence/utils/tableUrlValue.d.ts +27 -0
- package/src/useTable.d.ts +6 -0
- package/src/hooks/useTablePersistence/utils/useCompactTableUrl.d.ts +0 -24
package/index.esm.js
CHANGED
|
@@ -3,7 +3,7 @@ import { registerTranslations, useNamespaceTranslation, NamespaceTrans } from '@
|
|
|
3
3
|
import { Icon, MenuItem, Button, Tooltip, useOverflowItems, MoreMenu, MenuContent, Spacer, usePersistedState, useDebounce, Text, cvaInteractableItem, Popover, PopoverTrigger, IconButton, PopoverContent, cvaMenuList, cvaMenu, useBidirectionalScroll, noPagination, Card, HorizontalOverflowScroller, Spinner, EmptyState, SkeletonLabel } from '@trackunit/react-components';
|
|
4
4
|
import { Link } from '@tanstack/react-router';
|
|
5
5
|
import { objectValues, VISIBLE_ONLY_COLUMN_VISIBILITY_KEY, objectKeys, nonNullable, objectEntries } from '@trackunit/shared-utils';
|
|
6
|
-
import { useMemo, Children, cloneElement,
|
|
6
|
+
import { useMemo, Children, cloneElement, useRef, useEffect, useState, useCallback, createElement } from 'react';
|
|
7
7
|
import { cva } from '@trackunit/css-class-variance-utilities';
|
|
8
8
|
import { useCurrentUser } from '@trackunit/react-core-hooks';
|
|
9
9
|
import { z } from 'zod';
|
|
@@ -364,142 +364,88 @@ const ActionSheet = ({ actions, dropdownActions, moreActions = [], selections, r
|
|
|
364
364
|
};
|
|
365
365
|
|
|
366
366
|
/**
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
367
|
+
* The URL projection of table state: everything except `expanded`.
|
|
368
|
+
*
|
|
369
|
+
* Row expansion is per-session, per-user detail and would make a shared link
|
|
370
|
+
* open rows the recipient has no interest in, so it lives only in
|
|
371
|
+
* localStorage. `usePersistedState`'s `mergeWithStorageOnRead` restores it on
|
|
372
|
+
* load. The remaining fields are written as-is; the gzip step in
|
|
373
|
+
* `useCustomEncoding` handles the size, and a value that reads back to the
|
|
374
|
+
* same shape it was written from cannot drift from storage.
|
|
372
375
|
*/
|
|
373
|
-
const
|
|
374
|
-
columnOrder: "co",
|
|
375
|
-
columnVisibility: "cv",
|
|
376
|
-
sorting: "s",
|
|
377
|
-
columnPinning: "cp",
|
|
378
|
-
columnSizing: "cs",
|
|
379
|
-
};
|
|
380
|
-
const REVERSE_URL_KEY_MAP = Object.fromEntries(Object.entries(URL_KEY_MAP).map(([full, short]) => [short, full]));
|
|
381
|
-
const compactPinningSchema = z.object({
|
|
382
|
-
l: z.array(z.string()).optional(),
|
|
383
|
-
r: z.array(z.string()).optional(),
|
|
384
|
-
left: z.array(z.string()).optional(),
|
|
385
|
-
right: z.array(z.string()).optional(),
|
|
386
|
-
});
|
|
376
|
+
const toTableUrlValue = ({ expanded: _expanded, ...shareable }) => shareable;
|
|
387
377
|
/**
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
* - Stores columnOrder as visible-column order when columnVisibility is available
|
|
392
|
-
* - Drops empty pinning arrays
|
|
393
|
-
* - Excludes `expanded` (not shareable)
|
|
378
|
+
* Short keys used by the compact URL format that was written before the hash
|
|
379
|
+
* carried the full state. Links and bookmarks with that format still exist,
|
|
380
|
+
* so reads keep accepting it.
|
|
394
381
|
*/
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const visibleColumnSet = new Set(visibleColumns);
|
|
402
|
-
const hasColumnVisibility = (columnVisibilityEntries?.length ?? 0) > 0;
|
|
403
|
-
if (hasColumnVisibility) {
|
|
404
|
-
compact[URL_KEY_MAP.columnVisibility] = visibleColumns;
|
|
405
|
-
}
|
|
406
|
-
if (state.columnOrder && state.columnOrder.length > 0) {
|
|
407
|
-
compact[URL_KEY_MAP.columnOrder] = hasColumnVisibility
|
|
408
|
-
? [
|
|
409
|
-
...state.columnOrder.filter(columnId => visibleColumnSet.has(columnId)),
|
|
410
|
-
...visibleColumns.filter(columnId => !state.columnOrder?.includes(columnId)),
|
|
411
|
-
]
|
|
412
|
-
: state.columnOrder;
|
|
413
|
-
}
|
|
414
|
-
if (state.sorting && state.sorting.length > 0) {
|
|
415
|
-
compact[URL_KEY_MAP.sorting] = state.sorting;
|
|
416
|
-
}
|
|
417
|
-
if (state.columnSizing && Object.keys(state.columnSizing).length > 0) {
|
|
418
|
-
compact[URL_KEY_MAP.columnSizing] = state.columnSizing;
|
|
419
|
-
}
|
|
420
|
-
if (state.columnPinning) {
|
|
421
|
-
const pinning = {};
|
|
422
|
-
if (state.columnPinning.left && state.columnPinning.left.length > 0) {
|
|
423
|
-
pinning.l = state.columnPinning.left;
|
|
424
|
-
}
|
|
425
|
-
if (state.columnPinning.right && state.columnPinning.right.length > 0) {
|
|
426
|
-
pinning.r = state.columnPinning.right;
|
|
427
|
-
}
|
|
428
|
-
if (Object.keys(pinning).length > 0) {
|
|
429
|
-
compact[URL_KEY_MAP.columnPinning] = pinning;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
return compact;
|
|
382
|
+
const LEGACY_URL_KEY_MAP = {
|
|
383
|
+
co: "columnOrder",
|
|
384
|
+
cv: "columnVisibility",
|
|
385
|
+
s: "sorting",
|
|
386
|
+
cp: "columnPinning",
|
|
387
|
+
cs: "columnSizing",
|
|
433
388
|
};
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
const
|
|
389
|
+
const recordSchema = z.record(z.string(), z.unknown());
|
|
390
|
+
const legacyVisibleListSchema = z.array(z.string());
|
|
391
|
+
const legacyVisibilitySchema = z
|
|
437
392
|
.object({
|
|
438
393
|
v: z.array(z.string()).optional(),
|
|
439
394
|
h: z.array(z.string()).optional(),
|
|
440
395
|
})
|
|
441
396
|
.strict();
|
|
397
|
+
const legacyPinningSchema = z.object({
|
|
398
|
+
l: z.array(z.string()).optional(),
|
|
399
|
+
r: z.array(z.string()).optional(),
|
|
400
|
+
left: z.array(z.string()).optional(),
|
|
401
|
+
right: z.array(z.string()).optional(),
|
|
402
|
+
});
|
|
442
403
|
/**
|
|
443
|
-
*
|
|
404
|
+
* Normalises a decoded URL value into the full `TableColumnOptions` shape.
|
|
405
|
+
*
|
|
406
|
+
* A value written by {@link toTableUrlValue} passes through unchanged, except
|
|
407
|
+
* that a `columnPinning` with one side missing gets the other side filled in
|
|
408
|
+
* as `[]`. Values in the legacy compact format are expanded:
|
|
409
|
+
* - short keys (`co`, `cv`, `s`, `cp`, `cs`) become full names;
|
|
410
|
+
* - a `columnVisibility` array of visible column IDs becomes a visible-only
|
|
411
|
+
* map (see `VISIBLE_ONLY_COLUMN_VISIBILITY_KEY`);
|
|
412
|
+
* - a `columnVisibility` `{ v, h }` pair becomes explicit booleans;
|
|
413
|
+
* - `columnPinning` `{ l, r }` becomes `{ left, right }`.
|
|
414
|
+
*
|
|
415
|
+
* Non-object input is returned as-is so validation can reject it.
|
|
444
416
|
*/
|
|
445
|
-
const
|
|
446
|
-
const parsed =
|
|
417
|
+
const fromTableUrlValue = (decoded) => {
|
|
418
|
+
const parsed = recordSchema.safeParse(decoded);
|
|
447
419
|
if (!parsed.success) {
|
|
448
420
|
return decoded;
|
|
449
421
|
}
|
|
450
422
|
const expanded = {};
|
|
451
423
|
for (const [key, value] of Object.entries(parsed.data)) {
|
|
452
|
-
|
|
453
|
-
expanded[fullKey] = value;
|
|
424
|
+
expanded[LEGACY_URL_KEY_MAP[key] ?? key] = value;
|
|
454
425
|
}
|
|
455
|
-
const
|
|
456
|
-
if (
|
|
426
|
+
const visibleList = legacyVisibleListSchema.safeParse(expanded.columnVisibility);
|
|
427
|
+
if (visibleList.success) {
|
|
457
428
|
expanded.columnVisibility = Object.fromEntries([
|
|
458
429
|
[VISIBLE_ONLY_COLUMN_VISIBILITY_KEY, true],
|
|
459
|
-
...
|
|
430
|
+
...visibleList.data.map(col => [col, true]),
|
|
460
431
|
]);
|
|
461
432
|
}
|
|
462
|
-
const
|
|
463
|
-
if (
|
|
433
|
+
const legacyVisibility = legacyVisibilitySchema.safeParse(expanded.columnVisibility);
|
|
434
|
+
if (legacyVisibility.success) {
|
|
464
435
|
expanded.columnVisibility = Object.fromEntries([
|
|
465
|
-
...(
|
|
466
|
-
...(
|
|
436
|
+
...(legacyVisibility.data.v ?? []).map(col => [col, true]),
|
|
437
|
+
...(legacyVisibility.data.h ?? []).map(col => [col, false]),
|
|
467
438
|
]);
|
|
468
439
|
}
|
|
469
|
-
const
|
|
470
|
-
if (
|
|
440
|
+
const legacyPinning = legacyPinningSchema.safeParse(expanded.columnPinning);
|
|
441
|
+
if (legacyPinning.success) {
|
|
471
442
|
expanded.columnPinning = {
|
|
472
|
-
left:
|
|
473
|
-
right:
|
|
443
|
+
left: legacyPinning.data.l ?? legacyPinning.data.left ?? [],
|
|
444
|
+
right: legacyPinning.data.r ?? legacyPinning.data.right ?? [],
|
|
474
445
|
};
|
|
475
446
|
}
|
|
476
447
|
return expanded;
|
|
477
448
|
};
|
|
478
|
-
/**
|
|
479
|
-
* Provides transform functions for compacting table state before URL encoding
|
|
480
|
-
* and expanding it back after URL decoding.
|
|
481
|
-
*
|
|
482
|
-
* The compaction strategy:
|
|
483
|
-
* - Short property keys (columnOrder → co, columnVisibility → cv, etc.)
|
|
484
|
-
* - columnVisibility stored as an array of visible column names only
|
|
485
|
-
* - columnOrder stored as visible-column order when visibility is available
|
|
486
|
-
* - columnSizing stored under a short key (`cs`)
|
|
487
|
-
* - `expanded` is excluded from the URL and restored from localStorage via
|
|
488
|
-
* `usePersistedState`'s `mergeWithStorageOnRead`
|
|
489
|
-
* - Empty pinning arrays are excluded
|
|
490
|
-
*
|
|
491
|
-
* These transforms are designed to plug into {@link usePersistedState}'s
|
|
492
|
-
* `toUrlValue` and `fromUrlValue` options.
|
|
493
|
-
*/
|
|
494
|
-
const useCompactTableUrl = () => {
|
|
495
|
-
const toUrlValue = useCallback((state) => {
|
|
496
|
-
return compactForUrl(state);
|
|
497
|
-
}, []);
|
|
498
|
-
const fromUrlValue = useCallback((decoded) => {
|
|
499
|
-
return expandFromUrl(decoded);
|
|
500
|
-
}, []);
|
|
501
|
-
return useMemo(() => ({ toUrlValue, fromUrlValue }), [toUrlValue, fromUrlValue]);
|
|
502
|
-
};
|
|
503
449
|
|
|
504
450
|
const PERSISTENCE_KEY_MAX_LENGTH = 15;
|
|
505
451
|
const camelCaseRegex = /^[a-z][a-zA-Z0-9]*$/;
|
|
@@ -530,69 +476,96 @@ const validateTableState = (raw) => {
|
|
|
530
476
|
const result = tableStateSchema.safeParse(raw);
|
|
531
477
|
return result.success ? result.data : undefined;
|
|
532
478
|
};
|
|
479
|
+
/**
|
|
480
|
+
* Drops fields that hold nothing (empty arrays, empty maps, pinning with no
|
|
481
|
+
* side) so a reset column is removed from storage rather than stored as empty.
|
|
482
|
+
*/
|
|
483
|
+
const pruneEmptyFields = (state) => {
|
|
484
|
+
const { columnOrder, columnSizing, columnVisibility, sorting, columnPinning, expanded } = state;
|
|
485
|
+
const pruned = {};
|
|
486
|
+
if (columnOrder && columnOrder.length > 0) {
|
|
487
|
+
pruned.columnOrder = columnOrder;
|
|
488
|
+
}
|
|
489
|
+
if (columnSizing && objectKeys(columnSizing).length > 0) {
|
|
490
|
+
pruned.columnSizing = columnSizing;
|
|
491
|
+
}
|
|
492
|
+
if (columnVisibility && objectKeys(columnVisibility).length > 0) {
|
|
493
|
+
pruned.columnVisibility = columnVisibility;
|
|
494
|
+
}
|
|
495
|
+
if (sorting && sorting.length > 0) {
|
|
496
|
+
pruned.sorting = sorting;
|
|
497
|
+
}
|
|
498
|
+
if (columnPinning && ((columnPinning.left?.length ?? 0) > 0 || (columnPinning.right?.length ?? 0) > 0)) {
|
|
499
|
+
pruned.columnPinning = columnPinning;
|
|
500
|
+
}
|
|
501
|
+
if (expanded === true || (typeof expanded === "object" && objectKeys(expanded).length > 0)) {
|
|
502
|
+
pruned.expanded = expanded;
|
|
503
|
+
}
|
|
504
|
+
return pruned;
|
|
505
|
+
};
|
|
506
|
+
const URL_WRITE_DEBOUNCE_MS = 300;
|
|
533
507
|
/**
|
|
534
508
|
* Changes reported before `clientSideUserId` resolves carry no owner — the user
|
|
535
509
|
* was already editing while their id was in flight — so the identity that
|
|
536
510
|
* resolves adopts them. Once a defined identity owns the reported state it must
|
|
537
|
-
* not be adopted by a later identity, including a
|
|
538
|
-
*
|
|
539
|
-
*
|
|
511
|
+
* not be adopted by a later identity, including through a gap where the current
|
|
512
|
+
* id is unset: treating that gap as unowned would copy one person's layout into
|
|
513
|
+
* the next user's scoped storage.
|
|
540
514
|
*/
|
|
541
|
-
const
|
|
515
|
+
const isOwnedBy = (ownerId, clientSideUserId) => ownerId === undefined || ownerId === clientSideUserId;
|
|
516
|
+
/** Whether anything of `state` would reach the URL; a layout of only `expanded` has nothing to share. */
|
|
517
|
+
const hasShareableFields = (state) => objectKeys(toTableUrlValue(state)).length > 0;
|
|
542
518
|
/**
|
|
543
519
|
* Persists and restores table column state (order, sizing, visibility, sorting, pinning, expanded) using
|
|
544
|
-
* the URL **hash fragment
|
|
520
|
+
* localStorage and the URL **hash fragment**.
|
|
545
521
|
*
|
|
546
522
|
* Storage scope:
|
|
547
|
-
* -
|
|
548
|
-
* -
|
|
523
|
+
* - localStorage: everything, scoped to the current user.
|
|
524
|
+
* - URL hash (shareable): everything except `expanded`.
|
|
549
525
|
*
|
|
550
526
|
* The hash slot is used instead of search params because some deployments
|
|
551
527
|
* route through AWS WAF, whose `SizeRestrictions_QUERYSTRING` managed rule
|
|
552
528
|
* caps the query string at ~5000 bytes — easily breached by wide tables.
|
|
553
529
|
* The fragment is client-only and bypasses that cap entirely (subject to a
|
|
554
|
-
* 64 KiB soft limit enforced by
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
* one-
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
* wrote it; the stamp is read only by the login redirect, which drops a hash
|
|
594
|
-
* left behind by a *different* account that also uses this browser so
|
|
595
|
-
* accounts sharing a PC do not hand their layouts to each other (GLU-2396).
|
|
530
|
+
* 64 KiB soft limit enforced by the URL sync).
|
|
531
|
+
*
|
|
532
|
+
* On load the hash wins, so a link or bookmark renders the same layout for
|
|
533
|
+
* whoever opens it, and localStorage fills in what the hash omits (`expanded`).
|
|
534
|
+
* Opening a URL that carries a layout saves it for the account that loaded the
|
|
535
|
+
* page, so a pasted link sticks without a further edit. A layout that exists
|
|
536
|
+
* only in localStorage is never copied into the hash on load: doing that put
|
|
537
|
+
* the current layout into every history entry, so a later visit via history,
|
|
538
|
+
* a bookmark, or a login redirect replayed an older layout and saved it over
|
|
539
|
+
* the current one (GLU-2396).
|
|
540
|
+
*
|
|
541
|
+
* `onTableStateChange` takes a *partial* state and merges it into whatever was
|
|
542
|
+
* reported earlier in the session, so a consumer can report a single slice
|
|
543
|
+
* (e.g. only `expanded`) without the omitted fields falling back to their
|
|
544
|
+
* mount-time values. A field reported empty is dropped from the stored state,
|
|
545
|
+
* and once every field is empty the entry is removed altogether.
|
|
546
|
+
*
|
|
547
|
+
* Each change is written to localStorage as soon as it is reported, so
|
|
548
|
+
* navigating away right after an edit cannot lose it. The hash is written
|
|
549
|
+
* after a {@link URL_WRITE_DEBOUNCE_MS} quiet period, replacing the current
|
|
550
|
+
* history entry: a drag or a run of toggles leaves one entry, not one per step.
|
|
551
|
+
*
|
|
552
|
+
* Every hash write is stamped with the identity that wrote it. The stamp is
|
|
553
|
+
* read only by the login redirect, which drops a hash left behind by another
|
|
554
|
+
* account that also uses this browser, so accounts sharing a PC do not hand
|
|
555
|
+
* their layouts to each other (GLU-2396).
|
|
556
|
+
*
|
|
557
|
+
* Changes reported before `clientSideUserId` resolves are held in React state
|
|
558
|
+
* and written to the user's scoped storage and stamped into the hash once it
|
|
559
|
+
* does. The id can also change while the table is mounted: releasing an
|
|
560
|
+
* impersonated identity renews the token, and the new claims are published
|
|
561
|
+
* before the page navigates away. Reported state is therefore tagged with the
|
|
562
|
+
* identity that produced it and persisted only while that identity is current,
|
|
563
|
+
* and the layout read on load is committed to storage only for the account
|
|
564
|
+
* that opened the page, so a layout never crosses from one account into
|
|
565
|
+
* another's scoped storage or into their hash on its own. An identity that
|
|
566
|
+
* takes over and then edits saves what it sees on screen — the opener's hash
|
|
567
|
+
* over its own stored layout — under its own key, the same as editing an
|
|
568
|
+
* opened link.
|
|
596
569
|
*
|
|
597
570
|
* @param persistenceKey - Unique key used to store and retrieve the table state.
|
|
598
571
|
* Must be camelCase and at most 15 characters (e.g. "userTable", "assetList").
|
|
@@ -602,145 +575,104 @@ const isOwnedByCurrentUser = (ownerId, clientSideUserId) => ownerId === undefine
|
|
|
602
575
|
* @throws {Error} If persistenceKey is not camelCase or exceeds 15 characters.
|
|
603
576
|
*/
|
|
604
577
|
const useTablePersistence = (persistenceKey) => {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
578
|
+
// The key is a constant per table; validate it once rather than every render.
|
|
579
|
+
useMemo(() => {
|
|
580
|
+
const parseResult = persistenceKeySchema.safeParse(persistenceKey);
|
|
581
|
+
if (!parseResult.success) {
|
|
582
|
+
throw new Error(parseResult.error.errors.map(e => e.message).join(", "));
|
|
583
|
+
}
|
|
584
|
+
}, [persistenceKey]);
|
|
609
585
|
const { clientSideUserId } = useCurrentUser();
|
|
610
586
|
const localStorageEnabled = clientSideUserId !== undefined;
|
|
611
|
-
const {
|
|
612
|
-
const { initialState, persistState, clearState, commitHydratedState } = usePersistedState({
|
|
587
|
+
const { initialState, persistToStorage, persistToUrl } = usePersistedState({
|
|
613
588
|
key: `${persistenceKey}Tp`,
|
|
614
589
|
validate: validateTableState,
|
|
615
|
-
toUrlValue,
|
|
616
|
-
fromUrlValue,
|
|
590
|
+
toUrlValue: toTableUrlValue,
|
|
591
|
+
fromUrlValue: fromTableUrlValue,
|
|
617
592
|
// Replace, so a second edit updates this visit's history entry. A push
|
|
618
593
|
// per column change left older layouts in history, and a later full load
|
|
619
594
|
// of one of them saved that layout over the newer one (GLU-2396).
|
|
620
595
|
replace: true,
|
|
621
596
|
clientSideUserId,
|
|
622
597
|
localStorageEnabled,
|
|
623
|
-
// `
|
|
624
|
-
//
|
|
625
|
-
// survive reloads.
|
|
598
|
+
// `expanded` is kept out of the URL; the merge restores it from
|
|
599
|
+
// localStorage on read so it survives reloads.
|
|
626
600
|
mergeWithStorageOnRead: true,
|
|
627
|
-
// Move the persisted blob into the URL hash so it bypasses the AWS WAF
|
|
628
|
-
// query-string cap. `usePersistedState` also migrates any legacy
|
|
629
|
-
// `?<key>=...` search param into the hash on first load.
|
|
630
601
|
urlLocation: "hash",
|
|
631
|
-
// The hash outlives the session that wrote it and is carried through the
|
|
632
|
-
// login redirect to the next account on the same browser. Stamp it with
|
|
633
|
-
// the writing identity so the redirect can drop another account's layout
|
|
634
|
-
// (GLU-2396). Reads are unaffected: the URL still wins for everyone.
|
|
635
602
|
stampUrlWithIdentity: true,
|
|
636
603
|
});
|
|
637
|
-
//
|
|
638
|
-
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
//
|
|
604
|
+
// Opening a shared URL saves its layout for the account that opened it: the
|
|
605
|
+
// first identity to resolve while the table is mounted. `persistToStorage`
|
|
606
|
+
// dedupes against what storage held, so this writes only when the hash
|
|
607
|
+
// changed something; a reload of the account's own link, or a layout that
|
|
608
|
+
// came from storage alone, writes nothing. `initialState` is re-read in the
|
|
609
|
+
// render where `clientSideUserId` resolves, so the write sees the merge for
|
|
610
|
+
// the resolved account. An identity that takes over later does not commit:
|
|
611
|
+
// the hash on screen is the opener's layout, not theirs.
|
|
643
612
|
const openingUserIdRef = useRef(undefined);
|
|
644
613
|
useEffect(() => {
|
|
645
|
-
if (clientSideUserId === undefined
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
if (!commitHydratedState()) {
|
|
614
|
+
if (clientSideUserId === undefined) {
|
|
649
615
|
return;
|
|
650
616
|
}
|
|
651
|
-
openingUserIdRef.current = clientSideUserId;
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const debouncedPending = useDebounce(pendingState, { delay: 300 });
|
|
655
|
-
const hasReportedChange = debouncedPending !== null;
|
|
656
|
-
const currentState = useMemo(() => {
|
|
657
|
-
if (!debouncedPending && !initialState) {
|
|
658
|
-
return undefined;
|
|
659
|
-
}
|
|
660
|
-
const columnOrder = debouncedPending?.state.columnOrder ?? initialState?.columnOrder;
|
|
661
|
-
const columnSizing = debouncedPending?.state.columnSizing ?? initialState?.columnSizing;
|
|
662
|
-
const columnVisibility = debouncedPending?.state.columnVisibility ?? initialState?.columnVisibility;
|
|
663
|
-
const sorting = debouncedPending?.state.sorting ?? initialState?.sorting;
|
|
664
|
-
const columnPinning = debouncedPending?.state.columnPinning ?? initialState?.columnPinning;
|
|
665
|
-
const expanded = debouncedPending?.state.expanded ?? initialState?.expanded;
|
|
666
|
-
const state = {};
|
|
667
|
-
if (columnOrder && columnOrder.length > 0) {
|
|
668
|
-
state.columnOrder = columnOrder;
|
|
669
|
-
}
|
|
670
|
-
if (columnSizing && objectKeys(columnSizing).length > 0) {
|
|
671
|
-
state.columnSizing = columnSizing;
|
|
617
|
+
openingUserIdRef.current ?? (openingUserIdRef.current = clientSideUserId);
|
|
618
|
+
if (initialState !== undefined && openingUserIdRef.current === clientSideUserId) {
|
|
619
|
+
persistToStorage(initialState);
|
|
672
620
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
state.columnPinning = columnPinning;
|
|
681
|
-
}
|
|
682
|
-
if (expanded === true || (typeof expanded === "object" && objectKeys(expanded).length > 0)) {
|
|
683
|
-
state.expanded = expanded;
|
|
684
|
-
}
|
|
685
|
-
return objectKeys(state).length > 0 || hasReportedChange ? state : undefined;
|
|
686
|
-
}, [debouncedPending, hasReportedChange, initialState]);
|
|
621
|
+
}, [clientSideUserId, initialState, persistToStorage]);
|
|
622
|
+
// Everything reported in this session, merged, with the identity it belongs
|
|
623
|
+
// to. `null` until the first report so a mount with no edits writes nothing.
|
|
624
|
+
// Reports merge into whatever is held, whoever holds it; which identity that
|
|
625
|
+
// is gets settled in render below, where `clientSideUserId` is current. The
|
|
626
|
+
// callback stays stable so consumers can hold on to it.
|
|
627
|
+
const [reported, setReported] = useState(null);
|
|
687
628
|
const onTableStateChange = useCallback((newTableState) => {
|
|
688
629
|
if (!newTableState) {
|
|
689
630
|
return;
|
|
690
631
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
632
|
+
setReported(previous => ({ ownerId: previous?.ownerId, state: { ...previous?.state, ...newTableState } }));
|
|
633
|
+
}, []);
|
|
634
|
+
// Settle ownership in the render where an identity is current: reports made
|
|
635
|
+
// while no identity was resolved belong to the one that resolves; reports
|
|
636
|
+
// owned by a different identity are discarded so the current one starts
|
|
637
|
+
// over from what it sees on screen. While the id is unset nothing is
|
|
638
|
+
// settled, so edits in the gap stay with their owner and are kept if that
|
|
639
|
+
// owner comes back.
|
|
640
|
+
if (reported !== null && clientSideUserId !== undefined && reported.ownerId !== clientSideUserId) {
|
|
641
|
+
setReported(previous => {
|
|
642
|
+
if (previous === null || previous.ownerId === clientSideUserId) {
|
|
643
|
+
return previous;
|
|
696
644
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
645
|
+
return previous.ownerId === undefined ? { ...previous, ownerId: clientSideUserId } : null;
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
// `undefined` before any report or while the report belongs to another
|
|
649
|
+
// identity; afterwards the full layout to persist, `{}` meaning "remove the
|
|
650
|
+
// entry", tagged with the identity it is persisted for.
|
|
651
|
+
const currentState = useMemo(() => {
|
|
652
|
+
if (reported === null || !isOwnedBy(reported.ownerId, clientSideUserId)) {
|
|
653
|
+
return undefined;
|
|
705
654
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
state: { ...previousState?.state, ...newTableState },
|
|
709
|
-
}));
|
|
710
|
-
}, [clientSideUserId]);
|
|
655
|
+
return { ownerId: clientSideUserId, state: pruneEmptyFields({ ...initialState, ...reported.state }) };
|
|
656
|
+
}, [clientSideUserId, initialState, reported]);
|
|
711
657
|
useEffect(() => {
|
|
712
|
-
|
|
713
|
-
// storage on its own, via `commitHydratedState`, and must not be echoed
|
|
714
|
-
// through here — `persistState` treats the hydrated value as already saved.
|
|
715
|
-
if (!hasReportedChange || !currentState) {
|
|
658
|
+
if (currentState === undefined) {
|
|
716
659
|
return;
|
|
717
660
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
// State reported before identity resolution is adopted by the first
|
|
730
|
-
// identity that resolves. Hydration-only runs cannot reach this branch.
|
|
731
|
-
if (debouncedOwnerId === undefined && clientSideUserId !== undefined) {
|
|
732
|
-
pendingOwnerIdRef.current = clientSideUserId;
|
|
733
|
-
}
|
|
734
|
-
if (objectKeys(currentState).length === 0) {
|
|
735
|
-
clearState();
|
|
661
|
+
persistToStorage(objectKeys(currentState.state).length > 0 ? currentState.state : undefined);
|
|
662
|
+
}, [currentState, persistToStorage]);
|
|
663
|
+
// The debounced value carries the identity it was computed for, so a change
|
|
664
|
+
// of identity during the quiet period discards the pending write instead of
|
|
665
|
+
// stamping the previous identity's layout into the new one's hash. A layout
|
|
666
|
+
// whose shareable part is empty removes the hash entry even when a
|
|
667
|
+
// storage-only field such as `expanded` remains.
|
|
668
|
+
const debouncedState = useDebounce(currentState, { delay: URL_WRITE_DEBOUNCE_MS });
|
|
669
|
+
useEffect(() => {
|
|
670
|
+
if (debouncedState === undefined || !isOwnedBy(debouncedState.ownerId, clientSideUserId)) {
|
|
736
671
|
return;
|
|
737
672
|
}
|
|
738
|
-
|
|
739
|
-
}, [
|
|
740
|
-
return useMemo(() => ({
|
|
741
|
-
onTableStateChange,
|
|
742
|
-
initialState,
|
|
743
|
-
}), [initialState, onTableStateChange]);
|
|
673
|
+
persistToUrl(hasShareableFields(debouncedState.state) ? debouncedState.state : undefined);
|
|
674
|
+
}, [clientSideUserId, debouncedState, persistToUrl]);
|
|
675
|
+
return useMemo(() => ({ onTableStateChange, initialState }), [initialState, onTableStateChange]);
|
|
744
676
|
};
|
|
745
677
|
|
|
746
678
|
const cvaColumnFilterGrabbable = cva({
|
|
@@ -1781,6 +1713,8 @@ const useColumnHelper = () => {
|
|
|
1781
1713
|
}, []);
|
|
1782
1714
|
};
|
|
1783
1715
|
|
|
1716
|
+
const areSameSequence = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1717
|
+
const isSamePinning = (left, right) => areSameSequence(left.left ?? [], right.left ?? []) && areSameSequence(left.right ?? [], right.right ?? []);
|
|
1784
1718
|
const computeMergedPinning = (initialPinning, columnConfigPinning) => {
|
|
1785
1719
|
const leftPinning = initialPinning?.left;
|
|
1786
1720
|
const mergedLeft = leftPinning !== undefined && leftPinning.length > 0 ? [...leftPinning] : [...(columnConfigPinning.left ?? [])];
|
|
@@ -1809,6 +1743,12 @@ const computeMergedPinning = (initialPinning, columnConfigPinning) => {
|
|
|
1809
1743
|
*
|
|
1810
1744
|
* `onTableStateChange` is called with the field that changed, not the whole state, so anything
|
|
1811
1745
|
* storing it has to merge each report into what it already holds. `useTablePersistence` does.
|
|
1746
|
+
* Within that field only what differs from the column definitions is reported — visibility
|
|
1747
|
+
* entries equal to `meta.hiddenByDefault`, sizes equal to `size`, an order equal to the
|
|
1748
|
+
* definition order, a pinning equal to `meta.pinned` — and an emptied field is reported empty.
|
|
1749
|
+
* Defaults therefore belong on the column definitions; a value in `initialState` is read as
|
|
1750
|
+
* saved state, not as a default. Columns without an explicit `id` take no part in the
|
|
1751
|
+
* defaults (as before), so they are always reported once they are in the state.
|
|
1812
1752
|
*
|
|
1813
1753
|
* @template TData - The type representing the data model associated with the table.
|
|
1814
1754
|
* @param {TableOptionsProps<TData>} props - The props object containing configuration options.
|
|
@@ -1843,42 +1783,52 @@ const computeMergedPinning = (initialPinning, columnConfigPinning) => {
|
|
|
1843
1783
|
*/
|
|
1844
1784
|
const useTable = ({ onTableStateChange, initialState, columns, ...reactTableProps }) => {
|
|
1845
1785
|
const stableColumns = useMemo(() => JSON.stringify(columns), [columns]);
|
|
1846
|
-
|
|
1786
|
+
// The column definitions are the single source of defaults: `meta.hiddenByDefault`, definition
|
|
1787
|
+
// order, `meta.pinned`, and `size`. They seed the state when `initialState` says nothing, and
|
|
1788
|
+
// they are the baseline for what `onTableStateChange` reports (see `persistTableState`).
|
|
1789
|
+
// Consumers that want a different default should set it on the column, not in `initialState`:
|
|
1790
|
+
// a value in `initialState` is treated as saved state.
|
|
1791
|
+
const columnDefaults = useMemo(() => {
|
|
1847
1792
|
const columnsList = JSON.parse(stableColumns);
|
|
1793
|
+
const visibilityById = {};
|
|
1794
|
+
const lockedVisibleIds = new Set();
|
|
1795
|
+
const order = [];
|
|
1796
|
+
columnsList.forEach(column => {
|
|
1797
|
+
if (column.id === undefined) {
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
order.push(column.id);
|
|
1801
|
+
const columnCannotHide = reactTableProps.enableHiding === false || column.enableHiding === false;
|
|
1802
|
+
if (columnCannotHide) {
|
|
1803
|
+
lockedVisibleIds.add(column.id);
|
|
1804
|
+
}
|
|
1805
|
+
visibilityById[column.id] = columnCannotHide ? true : column.meta?.hiddenByDefault !== true;
|
|
1806
|
+
});
|
|
1807
|
+
return { visibilityById, lockedVisibleIds, order };
|
|
1808
|
+
}, [stableColumns, reactTableProps.enableHiding]);
|
|
1809
|
+
const updatedInitialColumnVisibility = useMemo(() => {
|
|
1848
1810
|
const initialStateColumnVisibility = initialState?.columnVisibility;
|
|
1849
1811
|
const shouldHideColumnsMissingFromInitialVisibility = initialStateColumnVisibility?.[VISIBLE_ONLY_COLUMN_VISIBILITY_KEY] === true;
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
if (column.id && initialStateColumnVisibility[column.id] !== undefined) {
|
|
1854
|
-
const columnCannotHide = reactTableProps.enableHiding === false || column.enableHiding === false;
|
|
1855
|
-
return [column.id, columnCannotHide ? true : initialStateColumnVisibility[column.id]];
|
|
1856
|
-
}
|
|
1857
|
-
return null;
|
|
1858
|
-
})
|
|
1859
|
-
.filter(nonNullable))
|
|
1860
|
-
: {};
|
|
1861
|
-
columnsList.forEach((column) => {
|
|
1862
|
-
if (column.id && resultFromInitialState[column.id] === undefined) {
|
|
1863
|
-
const columnCannotHide = reactTableProps.enableHiding === false || column.enableHiding === false;
|
|
1864
|
-
resultFromInitialState[column.id] = columnCannotHide
|
|
1865
|
-
? true
|
|
1866
|
-
: shouldHideColumnsMissingFromInitialVisibility
|
|
1867
|
-
? false
|
|
1868
|
-
: column.meta?.hiddenByDefault !== true;
|
|
1812
|
+
return Object.fromEntries(objectEntries(columnDefaults.visibilityById).map(([columnId, visibleByDefault]) => {
|
|
1813
|
+
if (columnDefaults.lockedVisibleIds.has(columnId)) {
|
|
1814
|
+
return [columnId, true];
|
|
1869
1815
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1816
|
+
const savedVisibility = initialStateColumnVisibility?.[columnId];
|
|
1817
|
+
if (savedVisibility !== undefined) {
|
|
1818
|
+
return [columnId, savedVisibility];
|
|
1819
|
+
}
|
|
1820
|
+
return [columnId, shouldHideColumnsMissingFromInitialVisibility ? false : visibleByDefault];
|
|
1821
|
+
}));
|
|
1822
|
+
}, [columnDefaults, initialState?.columnVisibility]);
|
|
1873
1823
|
const updatedInitialColumnOrder = useMemo(() => {
|
|
1874
1824
|
const resultFromInitialState = initialState?.columnOrder ? [...initialState.columnOrder] : [];
|
|
1875
|
-
|
|
1876
|
-
if (
|
|
1877
|
-
resultFromInitialState.push(
|
|
1825
|
+
columnDefaults.order.forEach(columnId => {
|
|
1826
|
+
if (!resultFromInitialState.includes(columnId)) {
|
|
1827
|
+
resultFromInitialState.push(columnId);
|
|
1878
1828
|
}
|
|
1879
1829
|
});
|
|
1880
1830
|
return resultFromInitialState;
|
|
1881
|
-
}, [
|
|
1831
|
+
}, [columnDefaults, initialState?.columnOrder]);
|
|
1882
1832
|
const stableUpdatedInitialColumnPinning = useMemo(() => {
|
|
1883
1833
|
const pinningState = { left: [], right: [] };
|
|
1884
1834
|
JSON.parse(stableColumns).forEach((column) => {
|
|
@@ -1936,23 +1886,51 @@ const useTable = ({ onTableStateChange, initialState, columns, ...reactTableProp
|
|
|
1936
1886
|
// A ref exposes the latest derived state to the change callbacks without making them depend on
|
|
1937
1887
|
// it (callbacks run on user events, outside render). Every managed key on `state` is always
|
|
1938
1888
|
// defined — it originates from `baseState` — so the callbacks read it directly.
|
|
1889
|
+
//
|
|
1890
|
+
// `applyUpdate` resolves a TanStack updater against that ref and advances the ref at once, so
|
|
1891
|
+
// several updaters fired from one event (the column menu's Reset toggles every column that
|
|
1892
|
+
// differs from its default in a single click) build on each other instead of each starting
|
|
1893
|
+
// from the state as of the last render.
|
|
1939
1894
|
const stateRef = useRef(state);
|
|
1940
1895
|
stateRef.current = state;
|
|
1896
|
+
const applyUpdate = (key, value) => {
|
|
1897
|
+
const current = stateRef.current[key];
|
|
1898
|
+
const next = typeof value === "function" ? value(current) : value;
|
|
1899
|
+
stateRef.current = { ...stateRef.current, [key]: next };
|
|
1900
|
+
setUserEdits(prev => ({ ...prev, [key]: next }));
|
|
1901
|
+
return next;
|
|
1902
|
+
};
|
|
1941
1903
|
// Reports only the field that changed — `useTablePersistence` merges partial reports — so a
|
|
1942
|
-
// change can never store another field's unsettled or transient value.
|
|
1943
|
-
//
|
|
1904
|
+
// change can never store another field's unsettled or transient value. Within a field, only
|
|
1905
|
+
// what differs from the column defaults is reported: visibility entries equal to
|
|
1906
|
+
// `hiddenByDefault`, sizes equal to the default size, an order equal to the definition order and
|
|
1907
|
+
// a pinning equal to `meta.pinned` all read back the same when absent, so storing them would only
|
|
1908
|
+
// freeze today's defaults under the user's layout and bloat what is shared. A field with nothing
|
|
1909
|
+
// left is reported empty so the store can drop it.
|
|
1944
1910
|
const persistTableState = useCallback((nextState) => {
|
|
1945
1911
|
if (!onTableStateChange) {
|
|
1946
1912
|
return;
|
|
1947
1913
|
}
|
|
1948
|
-
const
|
|
1949
|
-
if (
|
|
1950
|
-
|
|
1951
|
-
|
|
1914
|
+
const reported = { ...nextState };
|
|
1915
|
+
if (nextState.columnVisibility) {
|
|
1916
|
+
reported.columnVisibility = Object.fromEntries(objectEntries(nextState.columnVisibility).filter(([columnId, visible]) => columnDefaults.visibilityById[columnId] !== visible));
|
|
1917
|
+
}
|
|
1918
|
+
if (nextState.columnSizing) {
|
|
1919
|
+
reported.columnSizing = Object.fromEntries(objectEntries(nextState.columnSizing).filter(([columnId, size]) => defaultColumnSizingById[columnId] !== size));
|
|
1920
|
+
}
|
|
1921
|
+
if (nextState.columnOrder && areSameSequence(nextState.columnOrder, columnDefaults.order)) {
|
|
1922
|
+
reported.columnOrder = [];
|
|
1952
1923
|
}
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1924
|
+
if (nextState.columnPinning && isSamePinning(nextState.columnPinning, stableUpdatedInitialColumnPinning)) {
|
|
1925
|
+
reported.columnPinning = { left: [], right: [] };
|
|
1926
|
+
}
|
|
1927
|
+
onTableStateChange(reported);
|
|
1928
|
+
}, [onTableStateChange, columnDefaults, defaultColumnSizingById, stableUpdatedInitialColumnPinning]);
|
|
1929
|
+
// Whether a column-resize drag is in progress, as of the last render (TanStack
|
|
1930
|
+
// flags it at pointer-down, before the first move arrives), and whether a
|
|
1931
|
+
// sizing change was held back during one.
|
|
1932
|
+
const isResizingColumnRef = useRef(false);
|
|
1933
|
+
const sizingReportPendingRef = useRef(false);
|
|
1956
1934
|
const table = useReactTable({
|
|
1957
1935
|
manualSorting: true,
|
|
1958
1936
|
enableSortingRemoval: true,
|
|
@@ -1961,44 +1939,37 @@ const useTable = ({ onTableStateChange, initialState, columns, ...reactTableProp
|
|
|
1961
1939
|
getCoreRowModel: getCoreRowModel(),
|
|
1962
1940
|
...reactTableProps,
|
|
1963
1941
|
onColumnVisibilityChange: value => {
|
|
1964
|
-
|
|
1965
|
-
const next = typeof value === "function" ? value(current) : value;
|
|
1966
|
-
setUserEdits(prev => ({ ...prev, columnVisibility: next }));
|
|
1967
|
-
persistTableState({ columnVisibility: next });
|
|
1942
|
+
persistTableState({ columnVisibility: applyUpdate("columnVisibility", value) });
|
|
1968
1943
|
reactTableProps.onColumnVisibilityChange?.(value);
|
|
1969
1944
|
},
|
|
1970
1945
|
onColumnSizingChange: value => {
|
|
1971
|
-
const
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1946
|
+
const next = applyUpdate("columnSizing", value);
|
|
1947
|
+
// A drag updates the sizing on every pointer move (`columnResizeMode:
|
|
1948
|
+
// "onChange"`); the store would write every move. The report waits for
|
|
1949
|
+
// the drag to end (see the effect below). Anything else — `resetSize`,
|
|
1950
|
+
// `setColumnSizing` — is reported at once.
|
|
1951
|
+
if (isResizingColumnRef.current) {
|
|
1952
|
+
sizingReportPendingRef.current = true;
|
|
1953
|
+
}
|
|
1954
|
+
else {
|
|
1955
|
+
persistTableState({ columnSizing: next });
|
|
1956
|
+
}
|
|
1975
1957
|
reactTableProps.onColumnSizingChange?.(value);
|
|
1976
1958
|
},
|
|
1977
1959
|
onColumnOrderChange: value => {
|
|
1978
|
-
|
|
1979
|
-
const next = typeof value === "function" ? value(current) : value;
|
|
1980
|
-
setUserEdits(prev => ({ ...prev, columnOrder: next }));
|
|
1981
|
-
persistTableState({ columnOrder: next });
|
|
1960
|
+
persistTableState({ columnOrder: applyUpdate("columnOrder", value) });
|
|
1982
1961
|
reactTableProps.onColumnOrderChange?.(value);
|
|
1983
1962
|
},
|
|
1984
1963
|
onSortingChange: value => {
|
|
1985
|
-
|
|
1986
|
-
const next = typeof value === "function" ? value(current) : value;
|
|
1987
|
-
setUserEdits(prev => ({ ...prev, sorting: next }));
|
|
1988
|
-
persistTableState({ sorting: next });
|
|
1964
|
+
persistTableState({ sorting: applyUpdate("sorting", value) });
|
|
1989
1965
|
reactTableProps.onSortingChange?.(value);
|
|
1990
1966
|
},
|
|
1991
1967
|
onColumnPinningChange: value => {
|
|
1992
|
-
|
|
1993
|
-
const next = typeof value === "function" ? value(current) : value;
|
|
1994
|
-
setUserEdits(prev => ({ ...prev, columnPinning: next }));
|
|
1995
|
-
persistTableState({ columnPinning: next });
|
|
1968
|
+
persistTableState({ columnPinning: applyUpdate("columnPinning", value) });
|
|
1996
1969
|
reactTableProps.onColumnPinningChange?.(value);
|
|
1997
1970
|
},
|
|
1998
1971
|
onExpandedChange: value => {
|
|
1999
|
-
const
|
|
2000
|
-
const next = typeof value === "function" ? value(current) : value;
|
|
2001
|
-
setUserEdits(prev => ({ ...prev, expanded: next }));
|
|
1972
|
+
const next = applyUpdate("expanded", value);
|
|
2002
1973
|
if (reactTableProps.state?.expanded === undefined) {
|
|
2003
1974
|
persistTableState({ expanded: next });
|
|
2004
1975
|
}
|
|
@@ -2007,6 +1978,16 @@ const useTable = ({ onTableStateChange, initialState, columns, ...reactTableProp
|
|
|
2007
1978
|
columns,
|
|
2008
1979
|
state,
|
|
2009
1980
|
});
|
|
1981
|
+
const isResizingColumn = table.getState().columnSizingInfo.isResizingColumn !== false;
|
|
1982
|
+
isResizingColumnRef.current = isResizingColumn;
|
|
1983
|
+
// The drag has ended: report the sizing it left behind, once. TanStack
|
|
1984
|
+
// applies the final size before it clears the flag, so the ref holds it.
|
|
1985
|
+
useEffect(() => {
|
|
1986
|
+
if (!isResizingColumn && sizingReportPendingRef.current) {
|
|
1987
|
+
sizingReportPendingRef.current = false;
|
|
1988
|
+
persistTableState({ columnSizing: stateRef.current.columnSizing });
|
|
1989
|
+
}
|
|
1990
|
+
}, [isResizingColumn, persistTableState]);
|
|
2010
1991
|
return useMemo(() => {
|
|
2011
1992
|
return { table };
|
|
2012
1993
|
}, [table]);
|