@jielga/tmdatagrid 0.5.0 → 1.0.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 (62) hide show
  1. package/README.md +25 -2
  2. package/dist/index.d.ts +856 -14
  3. package/dist/index.js +3370 -1546
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/dist/styles.layer.css +1 -0
  7. package/package.json +12 -2
  8. package/skills/columns/SKILL.md +1 -1
  9. package/skills/features/SKILL.md +1 -1
  10. package/skills/getting-started/SKILL.md +1 -1
  11. package/skills/options/SKILL.md +1 -1
  12. package/skills/server-side/SKILL.md +1 -1
  13. package/skills/testing/SKILL.md +198 -0
  14. package/src/tmdatagrid/components/TMDataGrid.module.css +11 -0
  15. package/src/tmdatagrid/components/TMDataGrid.tsx +34 -0
  16. package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +256 -0
  17. package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +35 -12
  18. package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +3 -0
  19. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +62 -0
  20. package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +237 -0
  21. package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +164 -0
  22. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +77 -62
  23. package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +6 -0
  24. package/src/tmdatagrid/components/TMDataGridFooter.tsx +10 -3
  25. package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +2 -0
  26. package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +15 -2
  27. package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +21 -4
  28. package/src/tmdatagrid/components/TMDataGridRowNumberColumn.tsx +44 -0
  29. package/src/tmdatagrid/components/TMDataGridSearch.tsx +2 -0
  30. package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +3 -0
  31. package/src/tmdatagrid/components/TMDataGridTable.module.css +206 -0
  32. package/src/tmdatagrid/components/TMDataGridTable.tsx +841 -57
  33. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +12 -7
  34. package/src/tmdatagrid/components/editors/TMDataGridBooleanEditor.tsx +29 -0
  35. package/src/tmdatagrid/components/editors/TMDataGridDateEditor.tsx +57 -0
  36. package/src/tmdatagrid/components/editors/TMDataGridMultiSelectEditor.tsx +45 -0
  37. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +38 -0
  38. package/src/tmdatagrid/components/editors/TMDataGridSelectEditor.tsx +53 -0
  39. package/src/tmdatagrid/components/editors/TMDataGridStringEditor.tsx +31 -0
  40. package/src/tmdatagrid/components/editors/editorShared.ts +48 -0
  41. package/src/tmdatagrid/components/filters/DgAutocompleteFilter.tsx +46 -0
  42. package/src/tmdatagrid/components/filters/DgDateRangeFilter.tsx +43 -0
  43. package/src/tmdatagrid/components/filters/DgRangeSliderFilter.tsx +52 -0
  44. package/src/tmdatagrid/components/filters/DgTriStateFilter.tsx +38 -0
  45. package/src/tmdatagrid/components/filters/TMDataGridFilterValueInput.tsx +122 -0
  46. package/src/tmdatagrid/components/icons.ts +4 -0
  47. package/src/tmdatagrid/core/autosize.ts +1 -1
  48. package/src/tmdatagrid/core/capabilities.ts +37 -1
  49. package/src/tmdatagrid/core/cellNavigation.ts +41 -0
  50. package/src/tmdatagrid/core/columnUtils.ts +31 -6
  51. package/src/tmdatagrid/core/editEngine.ts +857 -0
  52. package/src/tmdatagrid/core/filterControls.ts +45 -0
  53. package/src/tmdatagrid/core/filterOperators.ts +72 -4
  54. package/src/tmdatagrid/core/labels.ts +68 -1
  55. package/src/tmdatagrid/core/labelsSv.ts +24 -1
  56. package/src/tmdatagrid/core/matchHighlight.ts +96 -0
  57. package/src/tmdatagrid/core/persistence.ts +97 -3
  58. package/src/tmdatagrid/core/quickSearch.ts +144 -0
  59. package/src/tmdatagrid/core/rowPinning.ts +37 -0
  60. package/src/tmdatagrid/core/rowSelection.ts +10 -1
  61. package/src/tmdatagrid/index.ts +51 -0
  62. package/src/tmdatagrid/useTMDataGrid.tsx +456 -15
@@ -0,0 +1,144 @@
1
+ import {
2
+ compareItems,
3
+ rankItem,
4
+ type RankingInfo,
5
+ } from "@tanstack/match-sorter-utils";
6
+ import {
7
+ createSortedRowModel,
8
+ type Row,
9
+ type RowData,
10
+ type RowModel,
11
+ type Table,
12
+ type TableFeatures,
13
+ } from "@tanstack/react-table";
14
+
15
+ /** How the quick search matches — see `quickSearchMode` on the hook options. */
16
+ export type TMDataGridQuickSearchMode = "fuzzy" | "contains";
17
+
18
+ /**
19
+ * The fuzzy filter's registry name — what `globalFilterFn` resolves to while
20
+ * `quickSearchMode` is `"fuzzy"`. The rank ordering keys off this name, so an
21
+ * explicit `globalFilterFn` override switches the ordering off with it.
22
+ */
23
+ export const FUZZY_FILTER_FN_NAME = "tmDataGridFuzzy";
24
+
25
+ /** What {@link fuzzyGlobalFilterFn} records per column via `addMeta`. */
26
+ type FuzzyFilterMeta = { fuzzyRank: RankingInfo };
27
+
28
+ function isFuzzyFilterMeta(value: unknown): value is FuzzyFilterMeta {
29
+ if (typeof value !== "object" || value === null) return false;
30
+ const rank = (value as { fuzzyRank?: unknown }).fuzzyRank;
31
+ return (
32
+ typeof rank === "object" &&
33
+ rank !== null &&
34
+ typeof (rank as { rank?: unknown }).rank === "number"
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Match-sorter ranking as the global filter: a row passes when a globally
40
+ * filterable column ranks above match-sorter's threshold — typos and skipped
41
+ * characters forgiven — and the rank is recorded for the ordering to read.
42
+ *
43
+ * TanStack stops probing a row's columns at the first passing one, so the
44
+ * recorded ranks cover the columns up to and including it; the best of those
45
+ * stands for the row. Good enough for a quick search: the first passing
46
+ * column is almost always the one the user is looking at.
47
+ */
48
+ export function fuzzyGlobalFilterFn<
49
+ TFeatures extends TableFeatures,
50
+ TData extends RowData,
51
+ >(
52
+ row: Row<TFeatures, TData>,
53
+ columnId: string,
54
+ filterValue: unknown,
55
+ addMeta?: (meta: FuzzyFilterMeta) => void,
56
+ ): boolean {
57
+ const rank = rankItem(
58
+ String(row.getValue(columnId) ?? ""),
59
+ String(filterValue),
60
+ );
61
+ addMeta?.({ fuzzyRank: rank });
62
+ return rank.passed;
63
+ }
64
+
65
+ /** The best fuzzy rank any column recorded for this row, or `null`. */
66
+ function bestRank<TFeatures extends TableFeatures, TData extends RowData>(
67
+ row: Row<TFeatures, TData>,
68
+ ): RankingInfo | null {
69
+ let best: RankingInfo | null = null;
70
+ // As a plain record, for the same generic-features reason as the options
71
+ // read in isRankingActive.
72
+ const { columnFiltersMeta } = row as {
73
+ columnFiltersMeta?: Record<string, unknown>;
74
+ };
75
+ for (const meta of Object.values(columnFiltersMeta ?? {})) {
76
+ if (!isFuzzyFilterMeta(meta)) continue;
77
+ if (best === null || meta.fuzzyRank.rank > best.rank) best = meta.fuzzyRank;
78
+ }
79
+ return best;
80
+ }
81
+
82
+ /**
83
+ * Whether the ranked ordering applies: the fuzzy quick search is narrowing
84
+ * the grid, and nothing else claims an order — no sort, no grouping.
85
+ */
86
+ function isRankingActive<TFeatures extends TableFeatures, TData extends RowData>(
87
+ table: Table<TFeatures, TData>,
88
+ ): boolean {
89
+ // Off the options as a plain record: a generic `TFeatures` does not carry
90
+ // the global-filtering feature's option types, and this file stays generic
91
+ // to avoid a cycle with useTMDataGrid.
92
+ const { globalFilterFn } = table.options as { globalFilterFn?: unknown };
93
+ if (globalFilterFn !== FUZZY_FILTER_FN_NAME) return false;
94
+ const state = table.store.state as {
95
+ sorting?: Array<unknown>;
96
+ grouping?: Array<unknown>;
97
+ globalFilter?: unknown;
98
+ };
99
+ return (
100
+ (state.sorting?.length ?? 0) === 0 &&
101
+ (state.grouping?.length ?? 0) === 0 &&
102
+ typeof state.globalFilter === "string" &&
103
+ state.globalFilter.trim() !== ""
104
+ );
105
+ }
106
+
107
+ /**
108
+ * The sorted row model, with one addition: while {@link isRankingActive},
109
+ * rows order by match quality, best first. Declarative — the ordering is
110
+ * derived from filter state, never written into `sorting`, so no column
111
+ * claims `aria-sort`, the persisted slice stays untouched, and the user's
112
+ * next sort click suspends it just by existing.
113
+ *
114
+ * Sits upstream of pagination, so a ranked first page really is the best
115
+ * matches of the whole set.
116
+ */
117
+ export function createFuzzyRankedSortedRowModel<
118
+ TFeatures extends TableFeatures,
119
+ TData extends RowData,
120
+ >(): (table: Table<TFeatures, TData>) => () => RowModel<TFeatures, TData> {
121
+ const base = createSortedRowModel<TFeatures, TData>();
122
+ return (table) => {
123
+ const getSorted = base(table);
124
+ // One-slot memo: every input of the ordering — filter text, data, the
125
+ // sorting and grouping states — changes the sorted model's identity, so
126
+ // the model alone is the cache key.
127
+ let lastModel: RowModel<TFeatures, TData> | null = null;
128
+ let ranked: RowModel<TFeatures, TData> | null = null;
129
+ return () => {
130
+ const model = getSorted();
131
+ if (!isRankingActive(table)) return model;
132
+ if (model === lastModel && ranked !== null) return ranked;
133
+ const rows = [...model.rows].sort((a, b) => {
134
+ const rankA = bestRank(a);
135
+ const rankB = bestRank(b);
136
+ if (rankA !== null && rankB !== null) return compareItems(rankA, rankB);
137
+ return rankA !== null ? -1 : rankB !== null ? 1 : 0;
138
+ });
139
+ ranked = { rows, flatRows: model.flatRows, rowsById: model.rowsById };
140
+ lastModel = model;
141
+ return ranked;
142
+ };
143
+ };
144
+ }
@@ -0,0 +1,37 @@
1
+ import type { Row, RowData } from "@tanstack/react-table";
2
+ import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
3
+
4
+ /**
5
+ * The rows pinned to one edge, resolved safely and in pin order.
6
+ *
7
+ * Not TanStack's `getTopRows()`: that resolves each pinned id with
8
+ * `getRow(id, true)`, which throws once the row's data is gone — and pinned
9
+ * ids outlive data. A delete, a server-side page swap, a refetch that comes
10
+ * back without the row: any of these would take the whole grid down. Here a
11
+ * stale id is skipped instead; it stays in state, harmless, and the row
12
+ * returns to its edge if its data comes back.
13
+ *
14
+ * The semantics are TanStack's `keepPinnedRows: true`: a pinned row stays at
15
+ * its edge even when a filter or the pager would have dropped it from the
16
+ * body. Ids resolve against the pre-pagination model first — those rows carry
17
+ * the live sort and filter context — with the core model as the fallback for
18
+ * rows the filters removed. A leaf whose group is collapsed stays hidden,
19
+ * as upstream does.
20
+ */
21
+ export function readPinnedRows<TData extends RowData>(
22
+ table: TMDataGridTable<TData>,
23
+ position: "top" | "bottom",
24
+ ): Array<Row<TMDataGridFeatures, TData>> {
25
+ const ids = table.store.state.rowPinning[position];
26
+ if (ids.length === 0) return [];
27
+ const visible = table.getPrePaginatedRowModel().rowsById;
28
+ const all = table.getCoreRowModel().rowsById;
29
+ const rows: Array<Row<TMDataGridFeatures, TData>> = [];
30
+ for (const id of ids) {
31
+ const row = visible[id] ?? all[id];
32
+ if (row === undefined) continue;
33
+ if (!row.getIsAllParentsExpanded()) continue;
34
+ rows.push(row);
35
+ }
36
+ return rows;
37
+ }
@@ -14,14 +14,23 @@ import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
14
14
  * is the right list for a range: it is what the user can see, and a range that
15
15
  * swept collapsed rows would select things off screen. What a group row in the
16
16
  * range contributes is decided by {@link getSelectableRowIds}.
17
+ *
18
+ * Rows pinned to an edge leave this list — they render in their own sticky
19
+ * blocks, outside the scrolling order a shift-click range or the virtualizer
20
+ * walks. See `readPinnedRows`.
17
21
  */
18
22
  export function getDisplayedRows<TData extends RowData>(
19
23
  table: TMDataGridTable<TData>,
20
24
  features: TMDataGridFeatureFlags,
21
25
  ): Array<Row<TMDataGridFeatures, TData>> {
22
- return isPagingActive(table, features)
26
+ const rows = isPagingActive(table, features)
23
27
  ? table.getPaginatedRowModel().rows
24
28
  : table.getPrePaginatedRowModel().rows;
29
+ if (!features.rowPinning) return rows;
30
+ const { top, bottom } = table.store.state.rowPinning;
31
+ if (top.length === 0 && bottom.length === 0) return rows;
32
+ const pinned = new Set([...top, ...bottom]);
33
+ return rows.filter((row) => !pinned.has(row.id));
25
34
  }
26
35
 
27
36
  /**
@@ -7,6 +7,8 @@ export {
7
7
  } from "./TMDataGridContext";
8
8
  export { SELECT_COLUMN_ID } from "./components/TMDataGridSelectColumn";
9
9
  export { DETAILS_COLUMN_ID } from "./components/TMDataGridDetailsColumn";
10
+ export { EDIT_COLUMN_ID } from "./components/TMDataGridEditColumn";
11
+ export { ROW_NUMBER_COLUMN_ID } from "./components/TMDataGridRowNumberColumn";
10
12
  export {
11
13
  formatGroupValue,
12
14
  GROUP_COLUMN_ID,
@@ -23,6 +25,9 @@ export {
23
25
  type TMDataGridTable,
24
26
  type TMDataGridTableMeta,
25
27
  type TMDataGridUiActions,
28
+ type TMDataGridEditingOptions,
29
+ type TMDataGridScrollAlign,
30
+ type TMDataGridScrollToRowArgs,
26
31
  type TMDataGridUiState,
27
32
  type TMDataGridUiStore,
28
33
  useTMDataGrid,
@@ -40,6 +45,7 @@ export {
40
45
  type TMDataGridColumnType,
41
46
  type TMDataGridFilterOperator,
42
47
  type TMDataGridFilterValue,
48
+ emptyValueForOperator,
43
49
  FILTER_OPERATOR_LABELS,
44
50
  formatFilterLabel,
45
51
  getDefaultOperator,
@@ -47,7 +53,12 @@ export {
47
53
  isFilterActive,
48
54
  operatorNeedsValue,
49
55
  operatorTakesArrayValue,
56
+ operatorTakesRangeValue,
50
57
  } from "./core/filterOperators";
58
+ export {
59
+ fuzzyGlobalFilterFn,
60
+ type TMDataGridQuickSearchMode,
61
+ } from "./core/quickSearch";
51
62
  export {
52
63
  optionsToComboboxData,
53
64
  resolveColumnOptions,
@@ -55,6 +66,42 @@ export {
55
66
  type TMDataGridOptionsArgs,
56
67
  type TMDataGridOptionsSource,
57
68
  } from "./core/columnOptions";
69
+ export { TMDataGridEditActions } from "./components/TMDataGridEditActions";
70
+ export {
71
+ clearedValueForType,
72
+ getEditFieldName,
73
+ normalizeFieldValidate,
74
+ type TMDataGridEditApi,
75
+ type TMDataGridEditChange,
76
+ type TMDataGridEditCommitArgs,
77
+ type TMDataGridEditCommitBatchArgs,
78
+ type TMDataGridEditField,
79
+ type TMDataGridEditMode,
80
+ type TMDataGridEditorArgs,
81
+ type TMDataGridEditorComponent,
82
+ type TMDataGridEditRowProjection,
83
+ type TMDataGridEditState,
84
+ type TMDataGridFieldValidate,
85
+ type TMDataGridRowAddArgs,
86
+ type TMDataGridRowDeleteArgs,
87
+ type TMDataGridRowEditForm,
88
+ type TMDataGridRowValidators,
89
+ } from "./core/editEngine";
90
+ export type {
91
+ TMDataGridFilterControlArgs,
92
+ TMDataGridFilterControlComponent,
93
+ } from "./core/filterControls";
94
+ export { TMDataGridFilterValueInput } from "./components/filters/TMDataGridFilterValueInput";
95
+ export { DgRangeSliderFilter } from "./components/filters/DgRangeSliderFilter";
96
+ export { DgDateRangeFilter } from "./components/filters/DgDateRangeFilter";
97
+ export { DgAutocompleteFilter } from "./components/filters/DgAutocompleteFilter";
98
+ export { DgTriStateFilter } from "./components/filters/DgTriStateFilter";
99
+ export { TMDataGridStringEditor } from "./components/editors/TMDataGridStringEditor";
100
+ export { TMDataGridNumberEditor } from "./components/editors/TMDataGridNumberEditor";
101
+ export { TMDataGridBooleanEditor } from "./components/editors/TMDataGridBooleanEditor";
102
+ export { TMDataGridDateEditor } from "./components/editors/TMDataGridDateEditor";
103
+ export { TMDataGridSelectEditor } from "./components/editors/TMDataGridSelectEditor";
104
+ export { TMDataGridMultiSelectEditor } from "./components/editors/TMDataGridMultiSelectEditor";
58
105
  export {
59
106
  mergeLabels,
60
107
  TMDATAGRID_LABELS_EN,
@@ -63,6 +110,7 @@ export {
63
110
  } from "./core/labels";
64
111
  export { TMDATAGRID_LABELS_SV } from "./core/labelsSv";
65
112
  export {
113
+ getColumnDefaultOperator,
66
114
  getColumnLabel,
67
115
  getColumnType,
68
116
  isColumnReorderable,
@@ -147,6 +195,7 @@ export {
147
195
  } from "./core/sizes";
148
196
  export {
149
197
  DATA_STATE_SLICES,
198
+ PERSIST_PAYLOAD_VERSION,
150
199
  SETTINGS_STATE_SLICES,
151
200
  type TMDataGridDataSlice,
152
201
  type TMDataGridPersistence,
@@ -155,10 +204,12 @@ export {
155
204
  type TMDataGridStorageMode,
156
205
  } from "./core/persistence";
157
206
  export type {
207
+ TMDataGridCellEventArgs,
158
208
  TMDataGridCellNav,
159
209
  TMDataGridColumnLayout,
160
210
  TMDataGridRowContextMenu,
161
211
  TMDataGridRowContextMenuArgs,
212
+ TMDataGridRowStyle,
162
213
  TMDataGridTableProps,
163
214
  } from "./components/TMDataGridTable";
164
215
  export {