@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.
Files changed (50) hide show
  1. package/dist/index.d.ts +302 -11
  2. package/dist/index.js +2338 -2097
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +1 -1
  5. package/docs/columns.md +9 -1
  6. package/docs/components.md +1 -0
  7. package/docs/filtering.md +5 -1
  8. package/docs/grouping.md +1 -1
  9. package/docs/localization.md +3 -3
  10. package/docs/persistence.md +189 -2
  11. package/docs/row-details.md +5 -0
  12. package/docs/row-interaction.md +1 -1
  13. package/docs/row-styling.md +17 -0
  14. package/docs/typed-deep-links.md +169 -0
  15. package/docs/use-tm-data-grid.md +6 -2
  16. package/package.json +1 -1
  17. package/skills/appearance/SKILL.md +1 -1
  18. package/skills/appearance/references/labels.md +3 -3
  19. package/skills/cell-selection/SKILL.md +1 -1
  20. package/skills/columns/SKILL.md +6 -3
  21. package/skills/columns/references/columns-api.md +1 -0
  22. package/skills/data/SKILL.md +1 -1
  23. package/skills/editing/SKILL.md +1 -1
  24. package/skills/filtering/SKILL.md +26 -13
  25. package/skills/getting-started/SKILL.md +2 -2
  26. package/skills/grouping/SKILL.md +2 -2
  27. package/skills/migrating-to-2/SKILL.md +1 -1
  28. package/skills/options/SKILL.md +105 -10
  29. package/skills/options/references/typed-links.md +134 -0
  30. package/skills/rows/SKILL.md +18 -9
  31. package/skills/server-side/SKILL.md +1 -1
  32. package/skills/testing/SKILL.md +1 -1
  33. package/skills/testing-components/SKILL.md +1 -1
  34. package/skills/testing-editing/SKILL.md +1 -1
  35. package/src/components/TMDataGridFilterPills.module.css +9 -7
  36. package/src/components/TMDataGridFilterPills.tsx +15 -1
  37. package/src/components/TMDataGridGroupColumn.tsx +19 -4
  38. package/src/components/TMDataGridHeaderFilterRow.tsx +7 -1
  39. package/src/components/TMDataGridTable.module.css +15 -6
  40. package/src/components/TMDataGridTable.tsx +23 -11
  41. package/src/components/sticky.module.css +27 -7
  42. package/src/core/filterControls.ts +8 -0
  43. package/src/core/filterOperators.ts +54 -18
  44. package/src/core/grouping.ts +11 -3
  45. package/src/core/labels.ts +4 -3
  46. package/src/core/persistence.ts +330 -26
  47. package/src/core/urlState.ts +439 -0
  48. package/src/core/valueText.ts +46 -0
  49. package/src/index.ts +14 -0
  50. package/src/useTMDataGrid.tsx +145 -21
@@ -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,
@@ -76,6 +77,7 @@ import {
76
77
  import {
77
78
  emptyValueForOperator,
78
79
  type TMDataGridColumnType,
80
+ type TMDataGridFilterOperator,
79
81
  tmDataGridFilterFn,
80
82
  } from "./core/filterOperators";
81
83
  import {
@@ -103,6 +105,8 @@ import {
103
105
  } from "./core/labels";
104
106
  import {
105
107
  collectLeafColumnIds,
108
+ collectUrlExcludedColumnIds,
109
+ findOverlappingSlices,
106
110
  hasPersistenceKeys,
107
111
  readPersistedState,
108
112
  type TMDataGridPersistence,
@@ -134,6 +138,12 @@ import {
134
138
  type TMDataGridQueryTable,
135
139
  } from "./core/pageReset";
136
140
  import type { TMDataGridCellRange } from "./core/cellRange";
141
+ import type {
142
+ TMDataGridAccessorColumnId,
143
+ TMDataGridColumnInfoHints,
144
+ TMDataGridColumnInfoOf,
145
+ TMDataGridTypedColumn,
146
+ } from "./core/urlState";
137
147
  import {
138
148
  resolveExportOptions,
139
149
  type TMDataGridExportOptions,
@@ -358,6 +368,13 @@ type WithRowTypedMeta<TDef, TData extends RowData> = TDef extends unknown
358
368
  * TanStack's column helper with `meta` typed against `TData`, so the
359
369
  * `meta.options` and `meta.edit.enabled` callbacks receive
360
370
  * `Row<TMDataGridFeatures, TData>`.
371
+ *
372
+ * Every column it returns also carries `TMDataGridColumnInfo` in its type,
373
+ * read from the literal values of `id`, `meta.type`,
374
+ * `meta.filter.operators`, `meta.filter.url`, `enableColumnFilter` and
375
+ * `enableSorting`. `createTMDataGridUrlState` reads it to type the links into
376
+ * the grid. TanStack's own return type keeps none of this: it types
377
+ * `accessorKey` as a plain string.
361
378
  */
362
379
  export type TMDataGridColumnHelper<TData extends RowData> = {
363
380
  accessor: <
@@ -367,6 +384,12 @@ export type TMDataGridColumnHelper<TData extends RowData> = {
367
384
  : TAccessor extends DeepKeys<TData>
368
385
  ? DeepValue<TData, TAccessor>
369
386
  : never,
387
+ const TId extends string = never,
388
+ TType extends TMDataGridColumnType = "string",
389
+ TOperator extends TMDataGridFilterOperator = TMDataGridFilterOperator,
390
+ TFilterable extends boolean = true,
391
+ TSortable extends boolean = true,
392
+ TUrl extends boolean = true,
370
393
  >(
371
394
  accessor: TAccessor,
372
395
  column: WithRowTypedMeta<
@@ -374,20 +397,43 @@ export type TMDataGridColumnHelper<TData extends RowData> = {
374
397
  ? DisplayColumnDef<TMDataGridFeatures, TData, TValue>
375
398
  : IdentifiedColumnDef<TMDataGridFeatures, TData, TValue>,
376
399
  TData
377
- >,
378
- ) => TAccessor extends AccessorFn<TData>
400
+ > &
401
+ TMDataGridColumnInfoHints<
402
+ TId,
403
+ TType,
404
+ TOperator,
405
+ TFilterable,
406
+ TSortable,
407
+ TUrl
408
+ >,
409
+ ) => (TAccessor extends AccessorFn<TData>
379
410
  ? AccessorFnColumnDef<TMDataGridFeatures, TData, TValue>
380
- : AccessorKeyColumnDef<TMDataGridFeatures, TData, TValue>;
411
+ : AccessorKeyColumnDef<TMDataGridFeatures, TData, TValue>) &
412
+ TMDataGridTypedColumn<{
413
+ id: TMDataGridAccessorColumnId<TAccessor, TId>;
414
+ type: TType;
415
+ value: TValue;
416
+ operators: TOperator;
417
+ filterable: TFilterable;
418
+ sortable: TSortable;
419
+ url: TUrl;
420
+ }>;
381
421
  columns: ColumnHelper<TMDataGridFeatures, TData>["columns"];
382
422
  display: (
383
423
  column: WithRowTypedMeta<DisplayColumnDef<TMDataGridFeatures, TData>, TData>,
384
424
  ) => DisplayColumnDef<TMDataGridFeatures, TData, unknown>;
385
- group: (
425
+ /** A header group. Carries the column info of every column under it. */
426
+ group: <
427
+ const TChildren extends ReadonlyArray<
428
+ ColumnDef<TMDataGridFeatures, TData, any>
429
+ >,
430
+ >(
386
431
  column: WithRowTypedMeta<
387
- GroupColumnDef<TMDataGridFeatures, TData, unknown>,
432
+ Omit<GroupColumnDef<TMDataGridFeatures, TData, unknown>, "columns">,
388
433
  TData
389
- >,
390
- ) => GroupColumnDef<TMDataGridFeatures, TData, unknown>;
434
+ > & { columns: TChildren },
435
+ ) => GroupColumnDef<TMDataGridFeatures, TData, unknown> &
436
+ TMDataGridTypedColumn<TMDataGridColumnInfoOf<TChildren[number]>>;
391
437
  };
392
438
 
393
439
  export function createTMDataGridColumnHelper<
@@ -819,8 +865,9 @@ export type UseTMDataGridOptions<TData extends RowData> = Omit<
819
865
  }
820
866
  ) & {
821
867
  /**
822
- * Restore and persist table state across mounts. Two keys, because the two
823
- * kinds of state have different lifetimes - see {@link TMDataGridPersistence}.
868
+ * Restore and persist table state across mounts. Two storage keys, because
869
+ * the two kinds of state have different lifetimes, and a URL search param
870
+ * for the data slices a link should carry - see {@link TMDataGridPersistence}.
824
871
  *
825
872
  * Keep the object referentially stable (module scope or `useMemo`); it is a
826
873
  * dependency of the subscription that writes back.
@@ -1132,6 +1179,17 @@ function withoutControlColumnVisibility(
1132
1179
  return scrubbed;
1133
1180
  }
1134
1181
 
1182
+ /**
1183
+ * The pagination the table starts with before anything is restored. Shared by
1184
+ * the table's `initialState` and the persistence baseline, so the URL param
1185
+ * compares against the page size the table really starts on.
1186
+ */
1187
+ function startingPagination(
1188
+ initial: Partial<TableState<TMDataGridFeatures>> | undefined,
1189
+ ): TableState<TMDataGridFeatures>["pagination"] {
1190
+ return { pageIndex: 0, pageSize: 25, ...initial?.pagination };
1191
+ }
1192
+
1135
1193
  /**
1136
1194
  * Point every column at the operator-dispatching filter function, and take
1137
1195
  * the aggregation defaults back off, unless the column opted into its own.
@@ -1377,13 +1435,34 @@ export function useTMDataGrid<TData extends RowData>({
1377
1435
  editColumnLabel,
1378
1436
  ]);
1379
1437
 
1438
+ // Leaf columns whose filter stays out of the `urlKey` param. Follows the
1439
+ // columns, since the write subscription below outlives a column change.
1440
+ const urlExcludedColumnIds = useMemo(
1441
+ () => collectUrlExcludedColumnIds(columns),
1442
+ [columns],
1443
+ );
1444
+ // What the table would start with if nothing were restored: TanStack's
1445
+ // feature defaults under the consumer's `initialState`, with the grid's own
1446
+ // page size. The `urlKey` param carries only the slices that differ from
1447
+ // it, and an opted-out column keeps its filter from it on read. Only the
1448
+ // data slices are used, so the layout part of the `initialState` recipe
1449
+ // below is not repeated here. Read once on mount, like `initialState`.
1450
+ const [persistBaseline] = useState(() =>
1451
+ getInitialTableState(tmDataGridFeatures, {
1452
+ ...options.initialState,
1453
+ pagination: startingPagination(options.initialState),
1454
+ }),
1455
+ );
1380
1456
  // Read once on mount: `initialState` is only consumed on the first render,
1381
1457
  // and re-reading later would fight the user's live edits.
1382
1458
  // Realigned against the ids this render is about to construct - lanes
1383
1459
  // included - so a column removed between deploys does not leave a ghost
1384
1460
  // sort, filter or width behind.
1385
1461
  const [persistedState] = useState(() =>
1386
- readPersistedState(persist, collectLeafColumnIds(columns)),
1462
+ readPersistedState(persist, collectLeafColumnIds(columns), {
1463
+ baseline: persistBaseline,
1464
+ excludedFilterColumnIds: urlExcludedColumnIds,
1465
+ }),
1387
1466
  );
1388
1467
 
1389
1468
  // Restored grouping decides whether the tree column starts out visible, so a
@@ -1731,9 +1810,7 @@ export function useTMDataGrid<TData extends RowData>({
1731
1810
  options.initialState?.columnPinning?.end,
1732
1811
  }),
1733
1812
  pagination: {
1734
- pageIndex: 0,
1735
- pageSize: 25,
1736
- ...options.initialState?.pagination,
1813
+ ...startingPagination(options.initialState),
1737
1814
  ...persistedState.pagination,
1738
1815
  },
1739
1816
  },
@@ -1826,6 +1903,19 @@ export function useTMDataGrid<TData extends RowData>({
1826
1903
  // eslint-disable-next-line react-hooks/exhaustive-deps
1827
1904
  }, []);
1828
1905
 
1906
+ // A slice listed under both `dataKey` and `urlKey` is handled by `urlKey`
1907
+ // alone - see resolveKeys in persistence.ts. Reported once, because the
1908
+ // consumer most likely expects the stored copy to still apply.
1909
+ useEffect(() => {
1910
+ for (const slice of findOverlappingSlices(persist)) {
1911
+ console.error(
1912
+ `TMDataGrid: persist.dataKey and persist.urlKey both list ${slice} - a slice lives in one group; urlKey takes it.`,
1913
+ );
1914
+ }
1915
+ // A mount-time contract, like the checks above.
1916
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1917
+ }, []);
1918
+
1829
1919
  // The tree column appears with the first grouped column and goes away with
1830
1920
  // the last, so an ungrouped grid looks exactly as it did before grouping
1831
1921
  // existed. Driven from a subscription rather than by rebuilding the column
@@ -1892,33 +1982,67 @@ export function useTMDataGrid<TData extends RowData>({
1892
1982
  // selection - and `setItem` serialises synchronously on the main thread. The
1893
1983
  // trailing edge is enough: storage only has to agree with the table once the
1894
1984
  // user stops.
1985
+ //
1986
+ // The `urlKey` inputs are read through a ref: the excluded columns follow
1987
+ // the column defs, and re-subscribing on a column change would write once
1988
+ // more for nothing.
1989
+ const urlPersistInputsRef = useRef({
1990
+ baseline: persistBaseline,
1991
+ excludedFilterColumnIds: urlExcludedColumnIds,
1992
+ });
1993
+ urlPersistInputsRef.current = {
1994
+ baseline: persistBaseline,
1995
+ excludedFilterColumnIds: urlExcludedColumnIds,
1996
+ };
1997
+ // Keyed on the store, not the table: `useTable` returns a new copy of the
1998
+ // table on every render (it memoizes on the options object, built inline
1999
+ // above), while every copy shares the one store. Keyed on the table, each
2000
+ // render re-ran this effect, whose cleanup flushed and whose setup wrote
2001
+ // again at once - no debounce at all.
2002
+ const tableStore = table.store;
1895
2003
  useEffect(() => {
1896
2004
  if (!hasPersistenceKeys(persist)) return;
1897
- writePersistedState(table.store.state, persist);
2005
+ writePersistedState(tableStore.state, persist, urlPersistInputsRef.current);
1898
2006
 
1899
2007
  let timeout: ReturnType<typeof setTimeout> | undefined;
1900
2008
  let pending: TableState<TMDataGridFeatures> | null = null;
1901
2009
 
1902
- const flush = () => {
2010
+ const flush = ({ writeUrl }: { writeUrl: boolean }) => {
1903
2011
  timeout = undefined;
1904
2012
  if (pending === null) return;
1905
- writePersistedState(pending, persist);
2013
+ writePersistedState(pending, persist, {
2014
+ ...urlPersistInputsRef.current,
2015
+ writeUrl,
2016
+ });
1906
2017
  pending = null;
1907
2018
  };
1908
2019
 
1909
- const subscription = table.store.subscribe((state) => {
2020
+ // Where the grid was when it subscribed, to tell an unmount by navigation
2021
+ // from an in-page one - see the cleanup.
2022
+ const subscribedPathname =
2023
+ typeof window === "undefined" ? null : window.location.pathname;
2024
+
2025
+ const subscription = tableStore.subscribe((state) => {
1910
2026
  pending = state;
1911
2027
  if (timeout !== undefined) clearTimeout(timeout);
1912
- timeout = setTimeout(flush, PERSIST_DEBOUNCE_MS);
2028
+ timeout = setTimeout(() => flush({ writeUrl: true }), PERSIST_DEBOUNCE_MS);
1913
2029
  });
1914
2030
 
1915
2031
  return () => {
1916
2032
  subscription.unsubscribe();
1917
2033
  if (timeout !== undefined) clearTimeout(timeout);
1918
- // An unmount mid-debounce would otherwise drop the last change.
1919
- flush();
2034
+ // An unmount mid-debounce would otherwise drop the last change. The URL
2035
+ // only while still on the page: an SPA navigation changes the pathname
2036
+ // before React unmounts the grid, so the param would land on the next
2037
+ // page's URL. An in-page unmount - a modal closing, a tab panel
2038
+ // switching - leaves the pathname alone and keeps its last filter.
2039
+ flush({
2040
+ writeUrl:
2041
+ subscribedPathname !== null &&
2042
+ subscribedPathname === window.location.pathname,
2043
+ });
1920
2044
  };
1921
- }, [persist, table]);
2045
+ }, [persist, tableStore]);
1922
2046
 
1923
2047
  const ui = useCreateStore<TMDataGridUiState, TMDataGridUiActions>(
1924
2048
  {