@jielga/tmdatagrid 2.1.4 → 2.3.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.
Files changed (57) hide show
  1. package/dist/index.d.ts +78 -5
  2. package/dist/index.js +2194 -1992
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +1 -1
  5. package/docs/cell-selection.md +1 -1
  6. package/docs/column-layout.md +13 -8
  7. package/docs/columns.md +59 -3
  8. package/docs/components.md +1 -0
  9. package/docs/export.md +2 -2
  10. package/docs/filtering.md +108 -15
  11. package/docs/getting-started.md +40 -0
  12. package/docs/grouping.md +9 -6
  13. package/docs/loading-and-empty.md +29 -9
  14. package/docs/localization.md +130 -3
  15. package/docs/menu.md +1 -1
  16. package/docs/pagination.md +26 -7
  17. package/docs/persistence.md +101 -2
  18. package/docs/row-details.md +41 -4
  19. package/docs/row-interaction.md +2 -2
  20. package/docs/row-selection.md +6 -7
  21. package/docs/row-styling.md +17 -0
  22. package/docs/scrolling.md +1 -1
  23. package/docs/server-side.md +3 -1
  24. package/docs/testing.md +18 -0
  25. package/docs/use-tm-data-grid.md +75 -36
  26. package/package.json +1 -1
  27. package/skills/appearance/SKILL.md +6 -4
  28. package/skills/appearance/references/labels.md +126 -0
  29. package/skills/cell-selection/SKILL.md +2 -2
  30. package/skills/columns/SKILL.md +44 -43
  31. package/skills/columns/references/columns-api.md +16 -1
  32. package/skills/data/SKILL.md +77 -50
  33. package/skills/editing/SKILL.md +1 -1
  34. package/skills/filtering/SKILL.md +94 -60
  35. package/skills/filtering/references/custom-filters.md +118 -0
  36. package/skills/getting-started/SKILL.md +37 -2
  37. package/skills/grouping/SKILL.md +16 -9
  38. package/skills/migrating-to-2/SKILL.md +1 -1
  39. package/skills/options/SKILL.md +206 -55
  40. package/skills/rows/SKILL.md +70 -39
  41. package/skills/server-side/SKILL.md +1 -1
  42. package/skills/testing/SKILL.md +12 -1
  43. package/skills/testing-components/SKILL.md +1 -1
  44. package/skills/testing-editing/SKILL.md +1 -1
  45. package/src/components/TMDataGridFilterPills.module.css +9 -7
  46. package/src/components/TMDataGridTable.module.css +15 -6
  47. package/src/components/TMDataGridTable.tsx +253 -41
  48. package/src/components/sticky.module.css +27 -7
  49. package/src/core/autosize.ts +19 -7
  50. package/src/core/cellRange.ts +11 -2
  51. package/src/core/colSpan.ts +78 -0
  52. package/src/core/columnUtils.ts +7 -0
  53. package/src/core/filterControls.ts +8 -0
  54. package/src/core/filterOperators.ts +29 -1
  55. package/src/core/persistence.ts +322 -26
  56. package/src/index.ts +5 -0
  57. package/src/useTMDataGrid.tsx +133 -14
@@ -28,6 +28,7 @@ import {
28
28
  type DeepValue,
29
29
  type DisplayColumnDef,
30
30
  filterFns,
31
+ getInitialTableState,
31
32
  type GroupColumnDef,
32
33
  globalFilteringFeature,
33
34
  type IdentifiedColumnDef,
@@ -79,6 +80,7 @@ import {
79
80
  tmDataGridFilterFn,
80
81
  } from "./core/filterOperators";
81
82
  import {
83
+ DEFAULT_COLUMN_MIN_SIZE,
82
84
  getColumnDefaultOperator,
83
85
  isControlColumn,
84
86
  isGeneratedColumn,
@@ -102,6 +104,8 @@ import {
102
104
  } from "./core/labels";
103
105
  import {
104
106
  collectLeafColumnIds,
107
+ collectUrlExcludedColumnIds,
108
+ findOverlappingSlices,
105
109
  hasPersistenceKeys,
106
110
  readPersistedState,
107
111
  type TMDataGridPersistence,
@@ -194,6 +198,28 @@ export type TMDataGridColumnMeta<TData extends RowData = TMDataGridRowData> = {
194
198
  /** Share of the leftover width this column claims. Defaults to `1`. */
195
199
  flex?: number;
196
200
  align?: "left" | "right" | "center";
201
+ /**
202
+ * How many columns this column's cell covers on a row, counting itself.
203
+ * Defaults to `1`.
204
+ *
205
+ * A cell spanning `n` is one cell `n` columns wide; the `n - 1` cells to its
206
+ * right do not render on that row. A function decides per row:
207
+ *
208
+ * ```tsx
209
+ * meta: {
210
+ * // An open row's title takes the rest of the row.
211
+ * colSpan: ({ row }) => (row.getIsExpanded() ? Infinity : 1),
212
+ * }
213
+ * ```
214
+ *
215
+ * Counted over the displayed columns, so hiding or moving a column changes
216
+ * what a span covers. A span stops at the last of your columns - it never
217
+ * covers the checkbox, tree, details or edit lane - and at the edge of its
218
+ * pinned block. Group rows, and a row open in row edit mode, ignore it.
219
+ *
220
+ * Display only: sorting, filtering, copy and export still see every column.
221
+ */
222
+ colSpan?: number | ((args: TMDataGridColSpanArgs<TData>) => number);
197
223
  /**
198
224
  * Size the column to its widest mounted content once, after the first rows
199
225
  * render - unless a persisted or user-set width already covers it. The same
@@ -385,6 +411,12 @@ export type TMDataGridDetailsArgs<TData extends RowData> = {
385
411
  table: TMDataGridTable<TData>;
386
412
  };
387
413
 
414
+ /** What a `meta.colSpan` function receives. */
415
+ export type TMDataGridColSpanArgs<TData extends RowData> = {
416
+ row: Row<TMDataGridFeatures, TData>;
417
+ table: TMDataGridTable<TData>;
418
+ };
419
+
388
420
  /** Builds the panel shown underneath an expanded row. See `renderDetails`. */
389
421
  export type TMDataGridDetailsRenderer<TData extends RowData> = (
390
422
  args: TMDataGridDetailsArgs<TData>,
@@ -790,8 +822,9 @@ export type UseTMDataGridOptions<TData extends RowData> = Omit<
790
822
  }
791
823
  ) & {
792
824
  /**
793
- * Restore and persist table state across mounts. Two keys, because the two
794
- * kinds of state have different lifetimes - see {@link TMDataGridPersistence}.
825
+ * Restore and persist table state across mounts. Two storage keys, because
826
+ * the two kinds of state have different lifetimes, and a URL search param
827
+ * for the data slices a link should carry - see {@link TMDataGridPersistence}.
795
828
  *
796
829
  * Keep the object referentially stable (module scope or `useMemo`); it is a
797
830
  * dependency of the subscription that writes back.
@@ -1103,6 +1136,17 @@ function withoutControlColumnVisibility(
1103
1136
  return scrubbed;
1104
1137
  }
1105
1138
 
1139
+ /**
1140
+ * The pagination the table starts with before anything is restored. Shared by
1141
+ * the table's `initialState` and the persistence baseline, so the URL param
1142
+ * compares against the page size the table really starts on.
1143
+ */
1144
+ function startingPagination(
1145
+ initial: Partial<TableState<TMDataGridFeatures>> | undefined,
1146
+ ): TableState<TMDataGridFeatures>["pagination"] {
1147
+ return { pageIndex: 0, pageSize: 25, ...initial?.pagination };
1148
+ }
1149
+
1106
1150
  /**
1107
1151
  * Point every column at the operator-dispatching filter function, and take
1108
1152
  * the aggregation defaults back off, unless the column opted into its own.
@@ -1348,13 +1392,34 @@ export function useTMDataGrid<TData extends RowData>({
1348
1392
  editColumnLabel,
1349
1393
  ]);
1350
1394
 
1395
+ // Leaf columns whose filter stays out of the `urlKey` param. Follows the
1396
+ // columns, since the write subscription below outlives a column change.
1397
+ const urlExcludedColumnIds = useMemo(
1398
+ () => collectUrlExcludedColumnIds(columns),
1399
+ [columns],
1400
+ );
1401
+ // What the table would start with if nothing were restored: TanStack's
1402
+ // feature defaults under the consumer's `initialState`, with the grid's own
1403
+ // page size. The `urlKey` param carries only the slices that differ from
1404
+ // it, and an opted-out column keeps its filter from it on read. Only the
1405
+ // data slices are used, so the layout part of the `initialState` recipe
1406
+ // below is not repeated here. Read once on mount, like `initialState`.
1407
+ const [persistBaseline] = useState(() =>
1408
+ getInitialTableState(tmDataGridFeatures, {
1409
+ ...options.initialState,
1410
+ pagination: startingPagination(options.initialState),
1411
+ }),
1412
+ );
1351
1413
  // Read once on mount: `initialState` is only consumed on the first render,
1352
1414
  // and re-reading later would fight the user's live edits.
1353
1415
  // Realigned against the ids this render is about to construct - lanes
1354
1416
  // included - so a column removed between deploys does not leave a ghost
1355
1417
  // sort, filter or width behind.
1356
1418
  const [persistedState] = useState(() =>
1357
- readPersistedState(persist, collectLeafColumnIds(columns)),
1419
+ readPersistedState(persist, collectLeafColumnIds(columns), {
1420
+ baseline: persistBaseline,
1421
+ excludedFilterColumnIds: urlExcludedColumnIds,
1422
+ }),
1358
1423
  );
1359
1424
 
1360
1425
  // Restored grouping decides whether the tree column starts out visible, so a
@@ -1556,6 +1621,14 @@ export function useTMDataGrid<TData extends RowData>({
1556
1621
  () => tableRef.current as unknown as TMDataGridQueryTable,
1557
1622
  [],
1558
1623
  );
1624
+ // The grid's `minSize` floor under the consumer's `defaultColumn`, which
1625
+ // still wins per key. Keyed on the consumer's object, because TanStack
1626
+ // memoizes the default column def on `options.defaultColumn` identity.
1627
+ const consumerDefaultColumn = options.defaultColumn;
1628
+ const defaultColumn = useMemo(
1629
+ () => ({ minSize: DEFAULT_COLUMN_MIN_SIZE, ...consumerDefaultColumn }),
1630
+ [consumerDefaultColumn],
1631
+ );
1559
1632
 
1560
1633
  // The sync of `state` into the table's atoms happens inside this call, in
1561
1634
  // the render body, and publishing from there makes React warn about the
@@ -1624,6 +1697,7 @@ export function useTMDataGrid<TData extends RowData>({
1624
1697
  ) as TableOptions<TMDataGridFeatures, TData>["onGroupingChange"],
1625
1698
  }
1626
1699
  : {}),
1700
+ defaultColumn,
1627
1701
  // The rows as shown - see `shown` above. The consumer's own array passes
1628
1702
  // through untouched while nothing is committed.
1629
1703
  data: shown.rows,
@@ -1693,9 +1767,7 @@ export function useTMDataGrid<TData extends RowData>({
1693
1767
  options.initialState?.columnPinning?.end,
1694
1768
  }),
1695
1769
  pagination: {
1696
- pageIndex: 0,
1697
- pageSize: 25,
1698
- ...options.initialState?.pagination,
1770
+ ...startingPagination(options.initialState),
1699
1771
  ...persistedState.pagination,
1700
1772
  },
1701
1773
  },
@@ -1788,6 +1860,19 @@ export function useTMDataGrid<TData extends RowData>({
1788
1860
  // eslint-disable-next-line react-hooks/exhaustive-deps
1789
1861
  }, []);
1790
1862
 
1863
+ // A slice listed under both `dataKey` and `urlKey` is handled by `urlKey`
1864
+ // alone - see resolveKeys in persistence.ts. Reported once, because the
1865
+ // consumer most likely expects the stored copy to still apply.
1866
+ useEffect(() => {
1867
+ for (const slice of findOverlappingSlices(persist)) {
1868
+ console.error(
1869
+ `TMDataGrid: persist.dataKey and persist.urlKey both list ${slice} - a slice lives in one group; urlKey takes it.`,
1870
+ );
1871
+ }
1872
+ // A mount-time contract, like the checks above.
1873
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1874
+ }, []);
1875
+
1791
1876
  // The tree column appears with the first grouped column and goes away with
1792
1877
  // the last, so an ungrouped grid looks exactly as it did before grouping
1793
1878
  // existed. Driven from a subscription rather than by rebuilding the column
@@ -1854,33 +1939,67 @@ export function useTMDataGrid<TData extends RowData>({
1854
1939
  // selection - and `setItem` serialises synchronously on the main thread. The
1855
1940
  // trailing edge is enough: storage only has to agree with the table once the
1856
1941
  // user stops.
1942
+ //
1943
+ // The `urlKey` inputs are read through a ref: the excluded columns follow
1944
+ // the column defs, and re-subscribing on a column change would write once
1945
+ // more for nothing.
1946
+ const urlPersistInputsRef = useRef({
1947
+ baseline: persistBaseline,
1948
+ excludedFilterColumnIds: urlExcludedColumnIds,
1949
+ });
1950
+ urlPersistInputsRef.current = {
1951
+ baseline: persistBaseline,
1952
+ excludedFilterColumnIds: urlExcludedColumnIds,
1953
+ };
1954
+ // Keyed on the store, not the table: `useTable` returns a new copy of the
1955
+ // table on every render (it memoizes on the options object, built inline
1956
+ // above), while every copy shares the one store. Keyed on the table, each
1957
+ // render re-ran this effect, whose cleanup flushed and whose setup wrote
1958
+ // again at once - no debounce at all.
1959
+ const tableStore = table.store;
1857
1960
  useEffect(() => {
1858
1961
  if (!hasPersistenceKeys(persist)) return;
1859
- writePersistedState(table.store.state, persist);
1962
+ writePersistedState(tableStore.state, persist, urlPersistInputsRef.current);
1860
1963
 
1861
1964
  let timeout: ReturnType<typeof setTimeout> | undefined;
1862
1965
  let pending: TableState<TMDataGridFeatures> | null = null;
1863
1966
 
1864
- const flush = () => {
1967
+ const flush = ({ writeUrl }: { writeUrl: boolean }) => {
1865
1968
  timeout = undefined;
1866
1969
  if (pending === null) return;
1867
- writePersistedState(pending, persist);
1970
+ writePersistedState(pending, persist, {
1971
+ ...urlPersistInputsRef.current,
1972
+ writeUrl,
1973
+ });
1868
1974
  pending = null;
1869
1975
  };
1870
1976
 
1871
- const subscription = table.store.subscribe((state) => {
1977
+ // Where the grid was when it subscribed, to tell an unmount by navigation
1978
+ // from an in-page one - see the cleanup.
1979
+ const subscribedPathname =
1980
+ typeof window === "undefined" ? null : window.location.pathname;
1981
+
1982
+ const subscription = tableStore.subscribe((state) => {
1872
1983
  pending = state;
1873
1984
  if (timeout !== undefined) clearTimeout(timeout);
1874
- timeout = setTimeout(flush, PERSIST_DEBOUNCE_MS);
1985
+ timeout = setTimeout(() => flush({ writeUrl: true }), PERSIST_DEBOUNCE_MS);
1875
1986
  });
1876
1987
 
1877
1988
  return () => {
1878
1989
  subscription.unsubscribe();
1879
1990
  if (timeout !== undefined) clearTimeout(timeout);
1880
- // An unmount mid-debounce would otherwise drop the last change.
1881
- flush();
1991
+ // An unmount mid-debounce would otherwise drop the last change. The URL
1992
+ // only while still on the page: an SPA navigation changes the pathname
1993
+ // before React unmounts the grid, so the param would land on the next
1994
+ // page's URL. An in-page unmount - a modal closing, a tab panel
1995
+ // switching - leaves the pathname alone and keeps its last filter.
1996
+ flush({
1997
+ writeUrl:
1998
+ subscribedPathname !== null &&
1999
+ subscribedPathname === window.location.pathname,
2000
+ });
1882
2001
  };
1883
- }, [persist, table]);
2002
+ }, [persist, tableStore]);
1884
2003
 
1885
2004
  const ui = useCreateStore<TMDataGridUiState, TMDataGridUiActions>(
1886
2005
  {