@jielga/tmdatagrid 0.1.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/LICENSE +21 -0
- package/README.md +162 -0
- package/dist/index.d.ts +592 -0
- package/dist/index.js +1590 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +2 -0
- package/package.json +111 -0
- package/skills/columns/SKILL.md +195 -0
- package/skills/features/SKILL.md +214 -0
- package/skills/getting-started/SKILL.md +209 -0
- package/skills/options/SKILL.md +208 -0
- package/skills/server-side/SKILL.md +150 -0
- package/src/tmdatagrid/TMDataGrid.module.css +410 -0
- package/src/tmdatagrid/TMDataGrid.tsx +101 -0
- package/src/tmdatagrid/TMDataGridColumnsPanel.tsx +91 -0
- package/src/tmdatagrid/TMDataGridContext.ts +32 -0
- package/src/tmdatagrid/TMDataGridFilterPanel.tsx +182 -0
- package/src/tmdatagrid/TMDataGridFooter.tsx +74 -0
- package/src/tmdatagrid/TMDataGridHeaderCell.tsx +466 -0
- package/src/tmdatagrid/TMDataGridTable.tsx +297 -0
- package/src/tmdatagrid/TMDataGridToolbar.tsx +131 -0
- package/src/tmdatagrid/capabilities.ts +117 -0
- package/src/tmdatagrid/columnOrdering.ts +140 -0
- package/src/tmdatagrid/columnUtils.ts +43 -0
- package/src/tmdatagrid/filterOperators.ts +171 -0
- package/src/tmdatagrid/icons.ts +25 -0
- package/src/tmdatagrid/index.ts +68 -0
- package/src/tmdatagrid/persistence.ts +187 -0
- package/src/tmdatagrid/sizes.ts +29 -0
- package/src/tmdatagrid/useTMDataGrid.tsx +378 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { Checkbox } from "@mantine/core";
|
|
2
|
+
import { useCreateStore } from "@tanstack/react-store";
|
|
3
|
+
import {
|
|
4
|
+
type ColumnDef,
|
|
5
|
+
columnFacetingFeature,
|
|
6
|
+
columnFilteringFeature,
|
|
7
|
+
columnOrderingFeature,
|
|
8
|
+
columnPinningFeature,
|
|
9
|
+
columnResizingFeature,
|
|
10
|
+
columnSizingFeature,
|
|
11
|
+
columnVisibilityFeature,
|
|
12
|
+
createColumnHelper,
|
|
13
|
+
createFacetedMinMaxValues,
|
|
14
|
+
createFacetedRowModel,
|
|
15
|
+
createFacetedUniqueValues,
|
|
16
|
+
createFilteredRowModel,
|
|
17
|
+
createPaginatedRowModel,
|
|
18
|
+
createSortedRowModel,
|
|
19
|
+
filterFns,
|
|
20
|
+
globalFilteringFeature,
|
|
21
|
+
metaHelper,
|
|
22
|
+
type RowData,
|
|
23
|
+
rowPaginationFeature,
|
|
24
|
+
rowSelectionFeature,
|
|
25
|
+
rowSortingFeature,
|
|
26
|
+
sortFns,
|
|
27
|
+
type Table,
|
|
28
|
+
tableFeatures,
|
|
29
|
+
type TableOptions,
|
|
30
|
+
useTable,
|
|
31
|
+
} from "@tanstack/react-table";
|
|
32
|
+
import type { Store } from "@tanstack/store";
|
|
33
|
+
import { useEffect, useMemo, useState } from "react";
|
|
34
|
+
import {
|
|
35
|
+
type TMDataGridColumnType,
|
|
36
|
+
tmDataGridFilterFn,
|
|
37
|
+
getDefaultOperator,
|
|
38
|
+
} from "./filterOperators";
|
|
39
|
+
import {
|
|
40
|
+
hasPersistenceKeys,
|
|
41
|
+
readPersistedState,
|
|
42
|
+
type TMDataGridPersistence,
|
|
43
|
+
writePersistedState,
|
|
44
|
+
} from "./persistence";
|
|
45
|
+
import { readFeatureFlags, type TMDataGridFeatureFlags } from "./capabilities";
|
|
46
|
+
|
|
47
|
+
export const SELECT_COLUMN_ID = "__select__";
|
|
48
|
+
|
|
49
|
+
/** Per-column configuration the TMDataGrid chrome reads. */
|
|
50
|
+
export type TMDataGridColumnMeta = {
|
|
51
|
+
/** Name shown in menus and the column manager. Falls back to a string header. */
|
|
52
|
+
label?: string;
|
|
53
|
+
/** Drives which filter operators are offered. Defaults to `"string"`. */
|
|
54
|
+
type?: TMDataGridColumnType;
|
|
55
|
+
/** Share of the leftover width this column claims. Defaults to `1`. */
|
|
56
|
+
flex?: number;
|
|
57
|
+
align?: "left" | "right" | "center";
|
|
58
|
+
/**
|
|
59
|
+
* `false` keeps the column where it is: no header dragging, no move items.
|
|
60
|
+
* Column ordering is the one feature TanStack defines no column option for,
|
|
61
|
+
* so its switch lives here rather than on the column definition.
|
|
62
|
+
*/
|
|
63
|
+
enableOrdering?: boolean;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Grid-wide configuration passed through `options.meta`. */
|
|
67
|
+
export type TMDataGridTableMeta = {
|
|
68
|
+
loading?: boolean;
|
|
69
|
+
noResultsLabel?: string;
|
|
70
|
+
/** Row height used by the virtualizer. Defaults to `52`. */
|
|
71
|
+
rowHeight?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Unfiltered row total. Only needed for server-driven grids, where the client
|
|
74
|
+
* never holds the full data set — `TMDataGrid.SummaryCount` uses it as denominator.
|
|
75
|
+
*/
|
|
76
|
+
totalRowCount?: number;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Every feature the TMDataGrid chrome can drive. Defined at module scope so the
|
|
81
|
+
* reference stays stable across renders (see the TanStack table-state skill).
|
|
82
|
+
*/
|
|
83
|
+
export const tmDataGridFeatures = tableFeatures({
|
|
84
|
+
columnFilteringFeature,
|
|
85
|
+
globalFilteringFeature,
|
|
86
|
+
rowSortingFeature,
|
|
87
|
+
rowPaginationFeature,
|
|
88
|
+
rowSelectionFeature,
|
|
89
|
+
columnPinningFeature,
|
|
90
|
+
columnVisibilityFeature,
|
|
91
|
+
columnOrderingFeature,
|
|
92
|
+
columnSizingFeature,
|
|
93
|
+
columnResizingFeature,
|
|
94
|
+
columnFacetingFeature,
|
|
95
|
+
|
|
96
|
+
filteredRowModel: createFilteredRowModel(),
|
|
97
|
+
sortedRowModel: createSortedRowModel(),
|
|
98
|
+
paginatedRowModel: createPaginatedRowModel(),
|
|
99
|
+
facetedRowModel: createFacetedRowModel(),
|
|
100
|
+
facetedMinMaxValues: createFacetedMinMaxValues(),
|
|
101
|
+
facetedUniqueValues: createFacetedUniqueValues(),
|
|
102
|
+
|
|
103
|
+
filterFns: { ...filterFns, tmDataGrid: tmDataGridFilterFn },
|
|
104
|
+
sortFns,
|
|
105
|
+
|
|
106
|
+
tableMeta: metaHelper<TMDataGridTableMeta>(),
|
|
107
|
+
columnMeta: metaHelper<TMDataGridColumnMeta>(),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
export type TMDataGridFeatures = typeof tmDataGridFeatures;
|
|
111
|
+
|
|
112
|
+
export type TMDataGridTable<TData extends RowData> = Table<
|
|
113
|
+
TMDataGridFeatures,
|
|
114
|
+
TData
|
|
115
|
+
>;
|
|
116
|
+
|
|
117
|
+
export function createTMDataGridColumnHelper<TData extends RowData>() {
|
|
118
|
+
return createColumnHelper<TMDataGridFeatures, TData>();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Chrome state that is *not* table state: which panels are open, and which
|
|
123
|
+
* column opened the filter panel. Kept in a TanStack Store so consumers can
|
|
124
|
+
* subscribe to it the same way they subscribe to `table.store`.
|
|
125
|
+
*/
|
|
126
|
+
export type TMDataGridUiState = {
|
|
127
|
+
filterPanelOpen: boolean;
|
|
128
|
+
columnsPanelOpen: boolean;
|
|
129
|
+
/** Column whose filter row should be focused when the panel opens. */
|
|
130
|
+
filterPanelColumnId: string | null;
|
|
131
|
+
/**
|
|
132
|
+
* Column being dragged by its header, if any. Held here rather than read from
|
|
133
|
+
* `dataTransfer`, which browsers keep unreadable until the drop.
|
|
134
|
+
*/
|
|
135
|
+
draggedColumnId: string | null;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export type TMDataGridUiActions = {
|
|
139
|
+
openFilterPanel: (columnId?: string | null) => void;
|
|
140
|
+
closeFilterPanel: () => void;
|
|
141
|
+
setColumnsPanelOpen: (open: boolean) => void;
|
|
142
|
+
toggleColumnsPanel: () => void;
|
|
143
|
+
startColumnDrag: (columnId: string) => void;
|
|
144
|
+
endColumnDrag: () => void;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export type TMDataGridUiStore = Store<TMDataGridUiState, TMDataGridUiActions>;
|
|
148
|
+
|
|
149
|
+
/** What `useTMDataGrid` returns — spread straight onto `<TMDataGrid />`. */
|
|
150
|
+
export type TMDataGridApi<TData extends RowData> = {
|
|
151
|
+
table: TMDataGridTable<TData>;
|
|
152
|
+
ui: TMDataGridUiStore;
|
|
153
|
+
/** Table-level feature switches, re-read from options on every render. */
|
|
154
|
+
features: TMDataGridFeatureFlags;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export type UseTMDataGridOptions<TData extends RowData> = Omit<
|
|
158
|
+
TableOptions<TMDataGridFeatures, TData>,
|
|
159
|
+
"features"
|
|
160
|
+
> & {
|
|
161
|
+
/**
|
|
162
|
+
* Restore and persist table state across mounts. Two keys, because the two
|
|
163
|
+
* kinds of state have different lifetimes — see {@link TMDataGridPersistence}.
|
|
164
|
+
*
|
|
165
|
+
* Keep the object referentially stable (module scope or `useMemo`); it is a
|
|
166
|
+
* dependency of the subscription that writes back.
|
|
167
|
+
*/
|
|
168
|
+
persist?: TMDataGridPersistence;
|
|
169
|
+
/**
|
|
170
|
+
* Header drag-and-drop and the move items in the column menu. Defaults to
|
|
171
|
+
* `true`.
|
|
172
|
+
*
|
|
173
|
+
* The one feature switch the grid defines itself: TanStack's
|
|
174
|
+
* `columnOrderingFeature` ships state and APIs but no `enable` option, since
|
|
175
|
+
* reordering is entirely a matter of interface.
|
|
176
|
+
*/
|
|
177
|
+
enableColumnOrdering?: boolean;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
type TMDataGridColumnDef<TData extends RowData> = ColumnDef<
|
|
181
|
+
TMDataGridFeatures,
|
|
182
|
+
TData,
|
|
183
|
+
unknown
|
|
184
|
+
>;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Point every column at the operator-dispatching filter function unless the
|
|
188
|
+
* column opted into its own.
|
|
189
|
+
*/
|
|
190
|
+
function withTMDataGridDefaults<TData extends RowData>(
|
|
191
|
+
columns: ReadonlyArray<TMDataGridColumnDef<TData>>,
|
|
192
|
+
): Array<TMDataGridColumnDef<TData>> {
|
|
193
|
+
return columns.map((column) => {
|
|
194
|
+
if ("columns" in column && Array.isArray(column.columns)) {
|
|
195
|
+
return {
|
|
196
|
+
...column,
|
|
197
|
+
columns: withTMDataGridDefaults<TData>(column.columns),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return { filterFn: "tmDataGrid", ...column } as TMDataGridColumnDef<TData>;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function createSelectColumn<TData extends RowData>(): TMDataGridColumnDef<TData> {
|
|
205
|
+
return {
|
|
206
|
+
id: SELECT_COLUMN_ID,
|
|
207
|
+
meta: {
|
|
208
|
+
label: "Checkbox selection",
|
|
209
|
+
align: "center",
|
|
210
|
+
// Structurally the first column; it also anchors the left pinned lane, so
|
|
211
|
+
// no other column can be moved in front of it.
|
|
212
|
+
enableOrdering: false,
|
|
213
|
+
},
|
|
214
|
+
size: 48,
|
|
215
|
+
minSize: 48,
|
|
216
|
+
maxSize: 48,
|
|
217
|
+
enableResizing: false,
|
|
218
|
+
enableSorting: false,
|
|
219
|
+
enableColumnFilter: false,
|
|
220
|
+
enableGlobalFilter: false,
|
|
221
|
+
// Structurally pinned to the left; users shouldn't be able to move it.
|
|
222
|
+
enablePinning: false,
|
|
223
|
+
header: ({ table }) => (
|
|
224
|
+
<Checkbox
|
|
225
|
+
size="xs"
|
|
226
|
+
aria-label="Select all rows"
|
|
227
|
+
checked={table.getIsAllRowsSelected()}
|
|
228
|
+
indeterminate={table.getIsSomeRowsSelected()}
|
|
229
|
+
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
230
|
+
/>
|
|
231
|
+
),
|
|
232
|
+
cell: ({ row }) => (
|
|
233
|
+
<Checkbox
|
|
234
|
+
size="xs"
|
|
235
|
+
aria-label="Select row"
|
|
236
|
+
checked={row.getIsSelected()}
|
|
237
|
+
disabled={!row.getCanSelect()}
|
|
238
|
+
indeterminate={row.getIsSomeSelected()}
|
|
239
|
+
onChange={row.getToggleSelectedHandler()}
|
|
240
|
+
onClick={(event) => event.stopPropagation()}
|
|
241
|
+
/>
|
|
242
|
+
),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Builds a TMDataGrid table plus its chrome store.
|
|
248
|
+
*
|
|
249
|
+
* Every `TableOptions` field passes straight through, so a server-driven grid
|
|
250
|
+
* only needs `manualPagination` / `manualFiltering` / `manualSorting`,
|
|
251
|
+
* `rowCount` and the matching `onXChange` callbacks — the chrome reads
|
|
252
|
+
* `getRowCount()` / `getPageCount()` / `getPaginatedRowModel()`, all of which
|
|
253
|
+
* already respect manual mode.
|
|
254
|
+
*/
|
|
255
|
+
export function useTMDataGrid<TData extends RowData>({
|
|
256
|
+
persist,
|
|
257
|
+
// Not a TanStack option, so it is kept out of what `useTable` receives.
|
|
258
|
+
enableColumnOrdering,
|
|
259
|
+
...options
|
|
260
|
+
}: UseTMDataGridOptions<TData>): TMDataGridApi<TData> {
|
|
261
|
+
const selectionEnabled = options.enableRowSelection !== false;
|
|
262
|
+
const pinningEnabled = options.enableColumnPinning !== false;
|
|
263
|
+
|
|
264
|
+
const columns = useMemo(() => {
|
|
265
|
+
const base = withTMDataGridDefaults<TData>(
|
|
266
|
+
options.columns as ReadonlyArray<TMDataGridColumnDef<TData>>,
|
|
267
|
+
);
|
|
268
|
+
return selectionEnabled ? [createSelectColumn<TData>(), ...base] : base;
|
|
269
|
+
}, [options.columns, selectionEnabled]);
|
|
270
|
+
|
|
271
|
+
// Read once on mount: `initialState` is only consumed on the first render,
|
|
272
|
+
// and re-reading later would fight the user's live edits.
|
|
273
|
+
const [persistedState] = useState(() => readPersistedState(persist));
|
|
274
|
+
|
|
275
|
+
const table = useTable({
|
|
276
|
+
columnResizeMode: "onChange",
|
|
277
|
+
enableSorting: true,
|
|
278
|
+
enableColumnResizing: true,
|
|
279
|
+
globalFilterFn: "includesString",
|
|
280
|
+
...options,
|
|
281
|
+
features: tmDataGridFeatures,
|
|
282
|
+
columns: columns as TableOptions<TMDataGridFeatures, TData>["columns"],
|
|
283
|
+
initialState: {
|
|
284
|
+
...options.initialState,
|
|
285
|
+
...persistedState,
|
|
286
|
+
columnPinning: {
|
|
287
|
+
// The checkbox column is structurally pinned, so it is re-applied on
|
|
288
|
+
// top of anything restored from storage.
|
|
289
|
+
left: [
|
|
290
|
+
...(selectionEnabled && pinningEnabled ? [SELECT_COLUMN_ID] : []),
|
|
291
|
+
...(
|
|
292
|
+
persistedState.columnPinning?.left ??
|
|
293
|
+
options.initialState?.columnPinning?.left ??
|
|
294
|
+
[]
|
|
295
|
+
).filter((id) => id !== SELECT_COLUMN_ID),
|
|
296
|
+
],
|
|
297
|
+
right:
|
|
298
|
+
persistedState.columnPinning?.right ??
|
|
299
|
+
options.initialState?.columnPinning?.right ??
|
|
300
|
+
[],
|
|
301
|
+
},
|
|
302
|
+
pagination: {
|
|
303
|
+
pageIndex: 0,
|
|
304
|
+
pageSize: 25,
|
|
305
|
+
...options.initialState?.pagination,
|
|
306
|
+
...persistedState.pagination,
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Mirror every state change back to storage. Subscribing (rather than writing
|
|
312
|
+
// from an effect on a state snapshot) means nothing is missed, including
|
|
313
|
+
// changes made straight through the table API by the consumer.
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
if (!hasPersistenceKeys(persist)) return;
|
|
316
|
+
writePersistedState(table.store.state, persist);
|
|
317
|
+
const subscription = table.store.subscribe((state) => {
|
|
318
|
+
writePersistedState(state, persist);
|
|
319
|
+
});
|
|
320
|
+
return () => subscription.unsubscribe();
|
|
321
|
+
}, [persist, table]);
|
|
322
|
+
|
|
323
|
+
const ui = useCreateStore<TMDataGridUiState, TMDataGridUiActions>(
|
|
324
|
+
{
|
|
325
|
+
filterPanelOpen: false,
|
|
326
|
+
columnsPanelOpen: false,
|
|
327
|
+
filterPanelColumnId: null,
|
|
328
|
+
draggedColumnId: null,
|
|
329
|
+
},
|
|
330
|
+
({ setState }) => ({
|
|
331
|
+
openFilterPanel: (columnId = null) =>
|
|
332
|
+
setState((prev) => ({
|
|
333
|
+
...prev,
|
|
334
|
+
filterPanelOpen: true,
|
|
335
|
+
filterPanelColumnId: columnId,
|
|
336
|
+
})),
|
|
337
|
+
closeFilterPanel: () =>
|
|
338
|
+
setState((prev) => ({
|
|
339
|
+
...prev,
|
|
340
|
+
filterPanelOpen: false,
|
|
341
|
+
filterPanelColumnId: null,
|
|
342
|
+
})),
|
|
343
|
+
setColumnsPanelOpen: (open) =>
|
|
344
|
+
setState((prev) => ({ ...prev, columnsPanelOpen: open })),
|
|
345
|
+
toggleColumnsPanel: () =>
|
|
346
|
+
setState((prev) => ({ ...prev, columnsPanelOpen: !prev.columnsPanelOpen })),
|
|
347
|
+
startColumnDrag: (columnId) =>
|
|
348
|
+
setState((prev) => ({ ...prev, draggedColumnId: columnId })),
|
|
349
|
+
endColumnDrag: () =>
|
|
350
|
+
setState((prev) => ({ ...prev, draggedColumnId: null })),
|
|
351
|
+
}),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// Deliberately not memoized on `table`: the flags must re-derive whenever the
|
|
355
|
+
// caller passes different options, and `table` keeps the same identity when
|
|
356
|
+
// they do. See readFeatureFlags.
|
|
357
|
+
const features = readFeatureFlags({ ...options, enableColumnOrdering });
|
|
358
|
+
|
|
359
|
+
return { table, ui, features };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Opens the filter panel for a column, seeding an empty filter row when the
|
|
364
|
+
* column has none yet — mirrors "Filter" in the column header menu.
|
|
365
|
+
*/
|
|
366
|
+
export function openColumnFilter<TData extends RowData>(
|
|
367
|
+
api: TMDataGridApi<TData>,
|
|
368
|
+
columnId: string,
|
|
369
|
+
): void {
|
|
370
|
+
const column = api.table.getColumn(columnId);
|
|
371
|
+
if (column && column.getFilterValue() === undefined) {
|
|
372
|
+
column.setFilterValue({
|
|
373
|
+
operator: getDefaultOperator(column.columnDef.meta?.type ?? "string"),
|
|
374
|
+
value: "",
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
api.ui.actions.openFilterPanel(columnId);
|
|
378
|
+
}
|