@trackunit/react-table 2.4.1-alpha-7bd3cf90b54.0 → 2.5.2

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 CHANGED
@@ -568,6 +568,12 @@ const validateTableState = (raw) => {
568
568
  * the URL, such as `expanded`, still survive reloads. Changes are
569
569
  * debounced (300 ms) and synced to both the hash and localStorage.
570
570
  *
571
+ * `onTableStateChange` takes a *partial* state and merges it into whatever
572
+ * was reported earlier in the session, so a consumer can report a single
573
+ * slice (e.g. only `expanded`) without the omitted fields falling back to
574
+ * their mount-time values. A field reported empty is dropped from the stored
575
+ * state, and once every field is empty the entry is removed altogether.
576
+ *
571
577
  * @param persistenceKey - Unique key used to store and retrieve the table state.
572
578
  * Must be camelCase and at most 15 characters (e.g. "userTable", "assetList").
573
579
  * @returns {UseTablePersistenceResult} An object containing:
@@ -583,7 +589,7 @@ const useTablePersistence = (persistenceKey) => {
583
589
  const { clientSideUserId } = reactCoreHooks.useCurrentUser();
584
590
  const localStorageEnabled = clientSideUserId !== undefined;
585
591
  const { toUrlValue, fromUrlValue } = useCompactTableUrl();
586
- const { initialState, persistState } = reactComponents.usePersistedState({
592
+ const { initialState, persistState, clearState } = reactComponents.usePersistedState({
587
593
  key: `${persistenceKey}Tp`,
588
594
  validate: validateTableState,
589
595
  toUrlValue,
@@ -602,6 +608,7 @@ const useTablePersistence = (persistenceKey) => {
602
608
  });
603
609
  const [pendingState, setPendingState] = react.useState(null);
604
610
  const debouncedPending = reactComponents.useDebounce(pendingState, { delay: 300 });
611
+ const hasReportedChange = debouncedPending !== null;
605
612
  const currentState = react.useMemo(() => {
606
613
  if (!debouncedPending && !initialState) {
607
614
  return undefined;
@@ -616,10 +623,10 @@ const useTablePersistence = (persistenceKey) => {
616
623
  if (columnOrder && columnOrder.length > 0) {
617
624
  state.columnOrder = columnOrder;
618
625
  }
619
- if (columnSizing && Object.keys(columnSizing).length > 0) {
626
+ if (columnSizing && sharedUtils.objectKeys(columnSizing).length > 0) {
620
627
  state.columnSizing = columnSizing;
621
628
  }
622
- if (columnVisibility && Object.keys(columnVisibility).length > 0) {
629
+ if (columnVisibility && sharedUtils.objectKeys(columnVisibility).length > 0) {
623
630
  state.columnVisibility = columnVisibility;
624
631
  }
625
632
  if (sorting && sorting.length > 0) {
@@ -628,23 +635,27 @@ const useTablePersistence = (persistenceKey) => {
628
635
  if (columnPinning && ((columnPinning.left?.length ?? 0) > 0 || (columnPinning.right?.length ?? 0) > 0)) {
629
636
  state.columnPinning = columnPinning;
630
637
  }
631
- if (expanded === true || (typeof expanded === "object" && Object.keys(expanded).length > 0)) {
638
+ if (expanded === true || (typeof expanded === "object" && sharedUtils.objectKeys(expanded).length > 0)) {
632
639
  state.expanded = expanded;
633
640
  }
634
- return Object.keys(state).length > 0 ? state : undefined;
635
- }, [debouncedPending, initialState]);
641
+ return sharedUtils.objectKeys(state).length > 0 || hasReportedChange ? state : undefined;
642
+ }, [debouncedPending, hasReportedChange, initialState]);
636
643
  const onTableStateChange = react.useCallback((newTableState) => {
637
644
  if (!newTableState) {
638
645
  return;
639
646
  }
640
- setPendingState(newTableState);
647
+ setPendingState(previousState => ({ ...previousState, ...newTableState }));
641
648
  }, []);
642
649
  react.useEffect(() => {
643
650
  if (!currentState) {
644
651
  return;
645
652
  }
653
+ if (sharedUtils.objectKeys(currentState).length === 0) {
654
+ clearState();
655
+ return;
656
+ }
646
657
  persistState(currentState);
647
- }, [currentState, persistState]);
658
+ }, [clearState, currentState, persistState]);
648
659
  return react.useMemo(() => ({
649
660
  onTableStateChange,
650
661
  initialState,
@@ -1708,6 +1719,13 @@ const computeMergedPinning = (initialPinning, columnConfigPinning) => {
1708
1719
  * - Do **not** call `useReactTable` directly — `useTable` adds required column-state
1709
1720
  * orchestration that the raw hook does not provide.
1710
1721
  *
1722
+ * Row expansion is managed like the column state: it starts from `initialState.expanded`, and
1723
+ * expand/collapse changes are reported to `onTableStateChange` so they can be persisted — unless
1724
+ * the consumer passes `state.expanded`, in which case it owns what is worth storing.
1725
+ *
1726
+ * `onTableStateChange` is called with the field that changed, not the whole state, so anything
1727
+ * storing it has to merge each report into what it already holds. `useTablePersistence` does.
1728
+ *
1711
1729
  * @template TData - The type representing the data model associated with the table.
1712
1730
  * @param {TableOptionsProps<TData>} props - The props object containing configuration options.
1713
1731
  * @returns {object} An object containing the React Table instance and associated state management functions.
@@ -1732,7 +1750,7 @@ const computeMergedPinning = (initialPinning, columnConfigPinning) => {
1732
1750
  * data,
1733
1751
  * columns,
1734
1752
  * initialState: { sorting: [{ id: "name", desc: false }] },
1735
- * onTableStateChange: state => saveTablePreferences(state),
1753
+ * onTableStateChange: changedField => setPreferences(previous => ({ ...previous, ...changedField })),
1736
1754
  * });
1737
1755
  *
1738
1756
  * return <Table table={table} />;
@@ -1828,20 +1846,27 @@ const useTable = ({ onTableStateChange, initialState, columns, ...reactTableProp
1828
1846
  const columnPinning = react.useMemo(() => userEdits.columnPinning ?? computeMergedPinning(initialState?.columnPinning, stableUpdatedInitialColumnPinning), [userEdits.columnPinning, initialState?.columnPinning, stableUpdatedInitialColumnPinning]);
1829
1847
  const sorting = react.useMemo(() => userEdits.sorting ?? initialState?.sorting ?? [], [userEdits.sorting, initialState?.sorting]);
1830
1848
  const columnSizing = react.useMemo(() => userEdits.columnSizing ?? initialState?.columnSizing ?? {}, [userEdits.columnSizing, initialState?.columnSizing]);
1831
- const baseState = react.useMemo(() => ({ columnVisibility, columnOrder, sorting, columnSizing, columnPinning }), [columnVisibility, columnOrder, sorting, columnSizing, columnPinning]);
1849
+ const expanded = react.useMemo(() => userEdits.expanded ?? initialState?.expanded ?? {}, [userEdits.expanded, initialState?.expanded]);
1850
+ const baseState = react.useMemo(() => ({ columnVisibility, columnOrder, sorting, columnSizing, columnPinning, expanded }), [columnVisibility, columnOrder, sorting, columnSizing, columnPinning, expanded]);
1832
1851
  const state = react.useMemo(() => ({ ...baseState, ...reactTableProps.state }), [baseState, reactTableProps.state]);
1833
1852
  // A ref exposes the latest derived state to the change callbacks without making them depend on
1834
- // it (callbacks run on user events, outside render). Every column-state key on `state` is always
1853
+ // it (callbacks run on user events, outside render). Every managed key on `state` is always
1835
1854
  // defined — it originates from `baseState` — so the callbacks read it directly.
1836
1855
  const stateRef = react.useRef(state);
1837
1856
  stateRef.current = state;
1838
- // Persist on user actions only (not on mount or prop-driven recomputation): drop column sizes
1839
- // that match their default so only meaningful overrides are stored.
1857
+ // Reports only the field that changed `useTablePersistence` merges partial reports so a
1858
+ // change can never store another field's unsettled or transient value. Column sizes matching
1859
+ // their default are dropped so only meaningful overrides are stored.
1840
1860
  const persistTableState = react.useCallback((nextState) => {
1841
1861
  if (!onTableStateChange) {
1842
1862
  return;
1843
1863
  }
1844
- const columnSizingToPersist = Object.fromEntries(Object.entries(nextState.columnSizing ?? {}).filter(([columnId, size]) => defaultColumnSizingById[columnId] !== size));
1864
+ const nextColumnSizing = nextState.columnSizing;
1865
+ if (!nextColumnSizing) {
1866
+ onTableStateChange(nextState);
1867
+ return;
1868
+ }
1869
+ const columnSizingToPersist = Object.fromEntries(sharedUtils.objectEntries(nextColumnSizing).filter(([columnId, size]) => defaultColumnSizingById[columnId] !== size));
1845
1870
  onTableStateChange({ ...nextState, columnSizing: columnSizingToPersist });
1846
1871
  }, [onTableStateChange, defaultColumnSizingById]);
1847
1872
  const table = reactTable.useReactTable({
@@ -1855,37 +1880,46 @@ const useTable = ({ onTableStateChange, initialState, columns, ...reactTableProp
1855
1880
  const current = stateRef.current.columnVisibility;
1856
1881
  const next = typeof value === "function" ? value(current) : value;
1857
1882
  setUserEdits(prev => ({ ...prev, columnVisibility: next }));
1858
- persistTableState({ ...stateRef.current, columnVisibility: next });
1883
+ persistTableState({ columnVisibility: next });
1859
1884
  reactTableProps.onColumnVisibilityChange?.(value);
1860
1885
  },
1861
1886
  onColumnSizingChange: value => {
1862
1887
  const current = stateRef.current.columnSizing;
1863
1888
  const next = typeof value === "function" ? value(current) : value;
1864
1889
  setUserEdits(prev => ({ ...prev, columnSizing: next }));
1865
- persistTableState({ ...stateRef.current, columnSizing: next });
1890
+ persistTableState({ columnSizing: next });
1866
1891
  reactTableProps.onColumnSizingChange?.(value);
1867
1892
  },
1868
1893
  onColumnOrderChange: value => {
1869
1894
  const current = stateRef.current.columnOrder;
1870
1895
  const next = typeof value === "function" ? value(current) : value;
1871
1896
  setUserEdits(prev => ({ ...prev, columnOrder: next }));
1872
- persistTableState({ ...stateRef.current, columnOrder: next });
1897
+ persistTableState({ columnOrder: next });
1873
1898
  reactTableProps.onColumnOrderChange?.(value);
1874
1899
  },
1875
1900
  onSortingChange: value => {
1876
1901
  const current = stateRef.current.sorting;
1877
1902
  const next = typeof value === "function" ? value(current) : value;
1878
1903
  setUserEdits(prev => ({ ...prev, sorting: next }));
1879
- persistTableState({ ...stateRef.current, sorting: next });
1904
+ persistTableState({ sorting: next });
1880
1905
  reactTableProps.onSortingChange?.(value);
1881
1906
  },
1882
1907
  onColumnPinningChange: value => {
1883
1908
  const current = stateRef.current.columnPinning;
1884
1909
  const next = typeof value === "function" ? value(current) : value;
1885
1910
  setUserEdits(prev => ({ ...prev, columnPinning: next }));
1886
- persistTableState({ ...stateRef.current, columnPinning: next });
1911
+ persistTableState({ columnPinning: next });
1887
1912
  reactTableProps.onColumnPinningChange?.(value);
1888
1913
  },
1914
+ onExpandedChange: value => {
1915
+ const current = stateRef.current.expanded;
1916
+ const next = typeof value === "function" ? value(current) : value;
1917
+ setUserEdits(prev => ({ ...prev, expanded: next }));
1918
+ if (reactTableProps.state?.expanded === undefined) {
1919
+ persistTableState({ expanded: next });
1920
+ }
1921
+ reactTableProps.onExpandedChange?.(value);
1922
+ },
1889
1923
  columns,
1890
1924
  state,
1891
1925
  });
package/index.esm.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import { registerTranslations, useNamespaceTranslation, NamespaceTrans } from '@trackunit/i18n-library-translation';
3
3
  import { Icon, MenuItem, Button, Tooltip, useOverflowItems, MoreMenu, MenuList, Spacer, usePersistedState, useDebounce, Text, cvaInteractableItem, Popover, PopoverTrigger, IconButton, PopoverContent, useBidirectionalScroll, noPagination, Card, Spinner, EmptyState } from '@trackunit/react-components';
4
- import { objectValues, VISIBLE_ONLY_COLUMN_VISIBILITY_KEY, nonNullable, objectKeys, objectEntries } from '@trackunit/shared-utils';
4
+ import { objectValues, VISIBLE_ONLY_COLUMN_VISIBILITY_KEY, objectKeys, nonNullable, objectEntries } from '@trackunit/shared-utils';
5
5
  import { useMemo, Children, cloneElement, useCallback, useState, useEffect, useRef, createElement } from 'react';
6
6
  import { cvaMerge } from '@trackunit/css-class-variance-utilities';
7
7
  import { Link } from '@tanstack/react-router';
@@ -567,6 +567,12 @@ const validateTableState = (raw) => {
567
567
  * the URL, such as `expanded`, still survive reloads. Changes are
568
568
  * debounced (300 ms) and synced to both the hash and localStorage.
569
569
  *
570
+ * `onTableStateChange` takes a *partial* state and merges it into whatever
571
+ * was reported earlier in the session, so a consumer can report a single
572
+ * slice (e.g. only `expanded`) without the omitted fields falling back to
573
+ * their mount-time values. A field reported empty is dropped from the stored
574
+ * state, and once every field is empty the entry is removed altogether.
575
+ *
570
576
  * @param persistenceKey - Unique key used to store and retrieve the table state.
571
577
  * Must be camelCase and at most 15 characters (e.g. "userTable", "assetList").
572
578
  * @returns {UseTablePersistenceResult} An object containing:
@@ -582,7 +588,7 @@ const useTablePersistence = (persistenceKey) => {
582
588
  const { clientSideUserId } = useCurrentUser();
583
589
  const localStorageEnabled = clientSideUserId !== undefined;
584
590
  const { toUrlValue, fromUrlValue } = useCompactTableUrl();
585
- const { initialState, persistState } = usePersistedState({
591
+ const { initialState, persistState, clearState } = usePersistedState({
586
592
  key: `${persistenceKey}Tp`,
587
593
  validate: validateTableState,
588
594
  toUrlValue,
@@ -601,6 +607,7 @@ const useTablePersistence = (persistenceKey) => {
601
607
  });
602
608
  const [pendingState, setPendingState] = useState(null);
603
609
  const debouncedPending = useDebounce(pendingState, { delay: 300 });
610
+ const hasReportedChange = debouncedPending !== null;
604
611
  const currentState = useMemo(() => {
605
612
  if (!debouncedPending && !initialState) {
606
613
  return undefined;
@@ -615,10 +622,10 @@ const useTablePersistence = (persistenceKey) => {
615
622
  if (columnOrder && columnOrder.length > 0) {
616
623
  state.columnOrder = columnOrder;
617
624
  }
618
- if (columnSizing && Object.keys(columnSizing).length > 0) {
625
+ if (columnSizing && objectKeys(columnSizing).length > 0) {
619
626
  state.columnSizing = columnSizing;
620
627
  }
621
- if (columnVisibility && Object.keys(columnVisibility).length > 0) {
628
+ if (columnVisibility && objectKeys(columnVisibility).length > 0) {
622
629
  state.columnVisibility = columnVisibility;
623
630
  }
624
631
  if (sorting && sorting.length > 0) {
@@ -627,23 +634,27 @@ const useTablePersistence = (persistenceKey) => {
627
634
  if (columnPinning && ((columnPinning.left?.length ?? 0) > 0 || (columnPinning.right?.length ?? 0) > 0)) {
628
635
  state.columnPinning = columnPinning;
629
636
  }
630
- if (expanded === true || (typeof expanded === "object" && Object.keys(expanded).length > 0)) {
637
+ if (expanded === true || (typeof expanded === "object" && objectKeys(expanded).length > 0)) {
631
638
  state.expanded = expanded;
632
639
  }
633
- return Object.keys(state).length > 0 ? state : undefined;
634
- }, [debouncedPending, initialState]);
640
+ return objectKeys(state).length > 0 || hasReportedChange ? state : undefined;
641
+ }, [debouncedPending, hasReportedChange, initialState]);
635
642
  const onTableStateChange = useCallback((newTableState) => {
636
643
  if (!newTableState) {
637
644
  return;
638
645
  }
639
- setPendingState(newTableState);
646
+ setPendingState(previousState => ({ ...previousState, ...newTableState }));
640
647
  }, []);
641
648
  useEffect(() => {
642
649
  if (!currentState) {
643
650
  return;
644
651
  }
652
+ if (objectKeys(currentState).length === 0) {
653
+ clearState();
654
+ return;
655
+ }
645
656
  persistState(currentState);
646
- }, [currentState, persistState]);
657
+ }, [clearState, currentState, persistState]);
647
658
  return useMemo(() => ({
648
659
  onTableStateChange,
649
660
  initialState,
@@ -1707,6 +1718,13 @@ const computeMergedPinning = (initialPinning, columnConfigPinning) => {
1707
1718
  * - Do **not** call `useReactTable` directly — `useTable` adds required column-state
1708
1719
  * orchestration that the raw hook does not provide.
1709
1720
  *
1721
+ * Row expansion is managed like the column state: it starts from `initialState.expanded`, and
1722
+ * expand/collapse changes are reported to `onTableStateChange` so they can be persisted — unless
1723
+ * the consumer passes `state.expanded`, in which case it owns what is worth storing.
1724
+ *
1725
+ * `onTableStateChange` is called with the field that changed, not the whole state, so anything
1726
+ * storing it has to merge each report into what it already holds. `useTablePersistence` does.
1727
+ *
1710
1728
  * @template TData - The type representing the data model associated with the table.
1711
1729
  * @param {TableOptionsProps<TData>} props - The props object containing configuration options.
1712
1730
  * @returns {object} An object containing the React Table instance and associated state management functions.
@@ -1731,7 +1749,7 @@ const computeMergedPinning = (initialPinning, columnConfigPinning) => {
1731
1749
  * data,
1732
1750
  * columns,
1733
1751
  * initialState: { sorting: [{ id: "name", desc: false }] },
1734
- * onTableStateChange: state => saveTablePreferences(state),
1752
+ * onTableStateChange: changedField => setPreferences(previous => ({ ...previous, ...changedField })),
1735
1753
  * });
1736
1754
  *
1737
1755
  * return <Table table={table} />;
@@ -1827,20 +1845,27 @@ const useTable = ({ onTableStateChange, initialState, columns, ...reactTableProp
1827
1845
  const columnPinning = useMemo(() => userEdits.columnPinning ?? computeMergedPinning(initialState?.columnPinning, stableUpdatedInitialColumnPinning), [userEdits.columnPinning, initialState?.columnPinning, stableUpdatedInitialColumnPinning]);
1828
1846
  const sorting = useMemo(() => userEdits.sorting ?? initialState?.sorting ?? [], [userEdits.sorting, initialState?.sorting]);
1829
1847
  const columnSizing = useMemo(() => userEdits.columnSizing ?? initialState?.columnSizing ?? {}, [userEdits.columnSizing, initialState?.columnSizing]);
1830
- const baseState = useMemo(() => ({ columnVisibility, columnOrder, sorting, columnSizing, columnPinning }), [columnVisibility, columnOrder, sorting, columnSizing, columnPinning]);
1848
+ const expanded = useMemo(() => userEdits.expanded ?? initialState?.expanded ?? {}, [userEdits.expanded, initialState?.expanded]);
1849
+ const baseState = useMemo(() => ({ columnVisibility, columnOrder, sorting, columnSizing, columnPinning, expanded }), [columnVisibility, columnOrder, sorting, columnSizing, columnPinning, expanded]);
1831
1850
  const state = useMemo(() => ({ ...baseState, ...reactTableProps.state }), [baseState, reactTableProps.state]);
1832
1851
  // A ref exposes the latest derived state to the change callbacks without making them depend on
1833
- // it (callbacks run on user events, outside render). Every column-state key on `state` is always
1852
+ // it (callbacks run on user events, outside render). Every managed key on `state` is always
1834
1853
  // defined — it originates from `baseState` — so the callbacks read it directly.
1835
1854
  const stateRef = useRef(state);
1836
1855
  stateRef.current = state;
1837
- // Persist on user actions only (not on mount or prop-driven recomputation): drop column sizes
1838
- // that match their default so only meaningful overrides are stored.
1856
+ // Reports only the field that changed `useTablePersistence` merges partial reports so a
1857
+ // change can never store another field's unsettled or transient value. Column sizes matching
1858
+ // their default are dropped so only meaningful overrides are stored.
1839
1859
  const persistTableState = useCallback((nextState) => {
1840
1860
  if (!onTableStateChange) {
1841
1861
  return;
1842
1862
  }
1843
- const columnSizingToPersist = Object.fromEntries(Object.entries(nextState.columnSizing ?? {}).filter(([columnId, size]) => defaultColumnSizingById[columnId] !== size));
1863
+ const nextColumnSizing = nextState.columnSizing;
1864
+ if (!nextColumnSizing) {
1865
+ onTableStateChange(nextState);
1866
+ return;
1867
+ }
1868
+ const columnSizingToPersist = Object.fromEntries(objectEntries(nextColumnSizing).filter(([columnId, size]) => defaultColumnSizingById[columnId] !== size));
1844
1869
  onTableStateChange({ ...nextState, columnSizing: columnSizingToPersist });
1845
1870
  }, [onTableStateChange, defaultColumnSizingById]);
1846
1871
  const table = useReactTable({
@@ -1854,37 +1879,46 @@ const useTable = ({ onTableStateChange, initialState, columns, ...reactTableProp
1854
1879
  const current = stateRef.current.columnVisibility;
1855
1880
  const next = typeof value === "function" ? value(current) : value;
1856
1881
  setUserEdits(prev => ({ ...prev, columnVisibility: next }));
1857
- persistTableState({ ...stateRef.current, columnVisibility: next });
1882
+ persistTableState({ columnVisibility: next });
1858
1883
  reactTableProps.onColumnVisibilityChange?.(value);
1859
1884
  },
1860
1885
  onColumnSizingChange: value => {
1861
1886
  const current = stateRef.current.columnSizing;
1862
1887
  const next = typeof value === "function" ? value(current) : value;
1863
1888
  setUserEdits(prev => ({ ...prev, columnSizing: next }));
1864
- persistTableState({ ...stateRef.current, columnSizing: next });
1889
+ persistTableState({ columnSizing: next });
1865
1890
  reactTableProps.onColumnSizingChange?.(value);
1866
1891
  },
1867
1892
  onColumnOrderChange: value => {
1868
1893
  const current = stateRef.current.columnOrder;
1869
1894
  const next = typeof value === "function" ? value(current) : value;
1870
1895
  setUserEdits(prev => ({ ...prev, columnOrder: next }));
1871
- persistTableState({ ...stateRef.current, columnOrder: next });
1896
+ persistTableState({ columnOrder: next });
1872
1897
  reactTableProps.onColumnOrderChange?.(value);
1873
1898
  },
1874
1899
  onSortingChange: value => {
1875
1900
  const current = stateRef.current.sorting;
1876
1901
  const next = typeof value === "function" ? value(current) : value;
1877
1902
  setUserEdits(prev => ({ ...prev, sorting: next }));
1878
- persistTableState({ ...stateRef.current, sorting: next });
1903
+ persistTableState({ sorting: next });
1879
1904
  reactTableProps.onSortingChange?.(value);
1880
1905
  },
1881
1906
  onColumnPinningChange: value => {
1882
1907
  const current = stateRef.current.columnPinning;
1883
1908
  const next = typeof value === "function" ? value(current) : value;
1884
1909
  setUserEdits(prev => ({ ...prev, columnPinning: next }));
1885
- persistTableState({ ...stateRef.current, columnPinning: next });
1910
+ persistTableState({ columnPinning: next });
1886
1911
  reactTableProps.onColumnPinningChange?.(value);
1887
1912
  },
1913
+ onExpandedChange: value => {
1914
+ const current = stateRef.current.expanded;
1915
+ const next = typeof value === "function" ? value(current) : value;
1916
+ setUserEdits(prev => ({ ...prev, expanded: next }));
1917
+ if (reactTableProps.state?.expanded === undefined) {
1918
+ persistTableState({ expanded: next });
1919
+ }
1920
+ reactTableProps.onExpandedChange?.(value);
1921
+ },
1888
1922
  columns,
1889
1923
  state,
1890
1924
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-table",
3
- "version": "2.4.1-alpha-7bd3cf90b54.0",
3
+ "version": "2.5.2",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -11,15 +11,15 @@
11
11
  "react-dnd": "16.0.1",
12
12
  "react-dnd-html5-backend": "16.0.1",
13
13
  "tailwind-merge": "^2.0.0",
14
- "@trackunit/react-components": "2.5.1-alpha-7bd3cf90b54.0",
15
- "@trackunit/react-core-hooks": "1.20.1-alpha-7bd3cf90b54.0",
16
- "@trackunit/shared-utils": "1.16.1-alpha-7bd3cf90b54.0",
17
- "@trackunit/css-class-variance-utilities": "1.14.1-alpha-7bd3cf90b54.0",
18
- "@trackunit/ui-icons": "1.14.1-alpha-7bd3cf90b54.0",
19
- "@trackunit/react-table-base-components": "2.4.1-alpha-7bd3cf90b54.0",
20
- "@trackunit/react-form-components": "2.4.1-alpha-7bd3cf90b54.0",
21
- "@trackunit/i18n-library-translation": "2.3.1-alpha-7bd3cf90b54.0",
22
- "@trackunit/iris-app-runtime-core-api": "1.17.1-alpha-7bd3cf90b54.0",
14
+ "@trackunit/react-components": "2.5.4",
15
+ "@trackunit/react-core-hooks": "1.20.1",
16
+ "@trackunit/shared-utils": "1.16.1",
17
+ "@trackunit/css-class-variance-utilities": "1.14.1",
18
+ "@trackunit/ui-icons": "1.14.1",
19
+ "@trackunit/react-table-base-components": "2.4.4",
20
+ "@trackunit/react-form-components": "2.4.4",
21
+ "@trackunit/i18n-library-translation": "2.3.1",
22
+ "@trackunit/iris-app-runtime-core-api": "1.17.1",
23
23
  "zod": "^3.25.76"
24
24
  },
25
25
  "peerDependencies": {
@@ -84,6 +84,12 @@ type UseTablePersistenceResult = {
84
84
  * the URL, such as `expanded`, still survive reloads. Changes are
85
85
  * debounced (300 ms) and synced to both the hash and localStorage.
86
86
  *
87
+ * `onTableStateChange` takes a *partial* state and merges it into whatever
88
+ * was reported earlier in the session, so a consumer can report a single
89
+ * slice (e.g. only `expanded`) without the omitted fields falling back to
90
+ * their mount-time values. A field reported empty is dropped from the stored
91
+ * state, and once every field is empty the entry is removed altogether.
92
+ *
87
93
  * @param persistenceKey - Unique key used to store and retrieve the table state.
88
94
  * Must be camelCase and at most 15 characters (e.g. "userTable", "assetList").
89
95
  * @returns {UseTablePersistenceResult} An object containing:
package/src/useTable.d.ts CHANGED
@@ -16,6 +16,13 @@ export interface TableOptionsProps<TData extends object> extends Omit<TableOptio
16
16
  * - Do **not** call `useReactTable` directly — `useTable` adds required column-state
17
17
  * orchestration that the raw hook does not provide.
18
18
  *
19
+ * Row expansion is managed like the column state: it starts from `initialState.expanded`, and
20
+ * expand/collapse changes are reported to `onTableStateChange` so they can be persisted — unless
21
+ * the consumer passes `state.expanded`, in which case it owns what is worth storing.
22
+ *
23
+ * `onTableStateChange` is called with the field that changed, not the whole state, so anything
24
+ * storing it has to merge each report into what it already holds. `useTablePersistence` does.
25
+ *
19
26
  * @template TData - The type representing the data model associated with the table.
20
27
  * @param {TableOptionsProps<TData>} props - The props object containing configuration options.
21
28
  * @returns {object} An object containing the React Table instance and associated state management functions.
@@ -40,7 +47,7 @@ export interface TableOptionsProps<TData extends object> extends Omit<TableOptio
40
47
  * data,
41
48
  * columns,
42
49
  * initialState: { sorting: [{ id: "name", desc: false }] },
43
- * onTableStateChange: state => saveTablePreferences(state),
50
+ * onTableStateChange: changedField => setPreferences(previous => ({ ...previous, ...changedField })),
44
51
  * });
45
52
  *
46
53
  * return <Table table={table} />;