@jielga/tmdatagrid 2.0.0-beta.11 → 2.0.0-beta.13
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/dist/index.d.ts +285 -22
- package/dist/index.js +2857 -2376
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -2
- package/skills/appearance/SKILL.md +1 -1
- package/skills/cell-selection/SKILL.md +1 -1
- package/skills/columns/SKILL.md +2 -2
- package/skills/data/SKILL.md +5 -2
- package/skills/editing/SKILL.md +1 -1
- package/skills/filtering/SKILL.md +112 -26
- package/skills/getting-started/SKILL.md +4 -4
- package/skills/grouping/SKILL.md +1 -1
- package/skills/options/SKILL.md +3 -1
- package/skills/rows/SKILL.md +1 -1
- package/skills/server-side/SKILL.md +44 -15
- package/skills/testing/SKILL.md +6 -3
- package/src/tmdatagrid/components/TMDataGrid.tsx +3 -0
- package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +1 -1
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +37 -18
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +138 -160
- package/src/tmdatagrid/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/tmdatagrid/components/TMDataGridFilterSurface.tsx +162 -0
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +28 -1
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +10 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +12 -3
- package/src/tmdatagrid/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/tmdatagrid/components/TMDataGridHeaderFilterRow.tsx +303 -0
- package/src/tmdatagrid/components/TMDataGridTable.module.css +9 -41
- package/src/tmdatagrid/components/TMDataGridTable.tsx +84 -15
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +37 -11
- package/src/tmdatagrid/components/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/tmdatagrid/components/filters/DgDateRangeFilter.tsx +22 -5
- package/src/tmdatagrid/components/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/tmdatagrid/components/filters/DgTriStateFilter.tsx +5 -1
- package/src/tmdatagrid/components/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/tmdatagrid/components/filters/controlLayout.ts +32 -0
- package/src/tmdatagrid/components/filters/filterControlFor.ts +65 -0
- package/src/tmdatagrid/components/sticky.module.css +44 -0
- package/src/tmdatagrid/core/columnOptions.ts +46 -0
- package/src/tmdatagrid/core/controlledStateSync.ts +108 -0
- package/src/tmdatagrid/core/editEngine.ts +68 -12
- package/src/tmdatagrid/core/filterControls.ts +28 -0
- package/src/tmdatagrid/core/filterOperators.ts +64 -1
- package/src/tmdatagrid/core/filterSurface.ts +99 -0
- package/src/tmdatagrid/core/labels.ts +7 -0
- package/src/tmdatagrid/core/labelsSv.ts +3 -0
- package/src/tmdatagrid/core/pageReset.ts +115 -0
- package/src/tmdatagrid/index.ts +13 -0
- package/src/tmdatagrid/useTMDataGrid.tsx +191 -11
|
@@ -62,6 +62,7 @@ export const TMDATAGRID_LABELS_SV: TMDataGridLabels = {
|
|
|
62
62
|
clearAllFilters: "Rensa alla",
|
|
63
63
|
closeFilters: "Stäng filter",
|
|
64
64
|
removeFilter: "Ta bort filter",
|
|
65
|
+
filterOperatorFor: (column) => `Filteroperator för ${column}`,
|
|
65
66
|
|
|
66
67
|
activeFilters: "Aktiva filter",
|
|
67
68
|
clearFilter: (column) => `Rensa filtret för ${column}`,
|
|
@@ -86,6 +87,8 @@ export const TMDATAGRID_LABELS_SV: TMDataGridLabels = {
|
|
|
86
87
|
|
|
87
88
|
rowsPerPage: "Rader per sida:",
|
|
88
89
|
pageRange: ({ from, to, total }) => `${from}–${to} av ${total}`,
|
|
90
|
+
pageNumber: ({ page, pageCount }) =>
|
|
91
|
+
pageCount < 0 ? `Sida ${page}` : `Sida ${page} av ${pageCount}`,
|
|
89
92
|
groupedAllRows: (total) => `Grupperad · alla ${total} rader`,
|
|
90
93
|
pagingSuspendedHint:
|
|
91
94
|
"Sidindelningen är avstängd medan raderna är grupperade: hela trädet renderas och virtualiseras. Avgruppera för att bläddra igen.",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
functionalUpdate,
|
|
3
|
+
makeStateUpdater,
|
|
4
|
+
type TableState,
|
|
5
|
+
type Updater,
|
|
6
|
+
} from "@tanstack/react-table";
|
|
7
|
+
import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
8
|
+
import { sameStateValue } from "./controlledState";
|
|
9
|
+
import { isFilterActive, isTMDataGridFilterValue } from "./filterOperators";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Sends a server-paged grid back to the first page when the query changes.
|
|
13
|
+
*
|
|
14
|
+
* Under `manualPagination` the grid does not own the result set, so a column
|
|
15
|
+
* filter, the quick search or a sort leaves `pageIndex` pointing into a page
|
|
16
|
+
* range that no longer exists: the next request asks the server for page 8 of
|
|
17
|
+
* a result set that now has three, and the consumer gets an empty grid with
|
|
18
|
+
* no error.
|
|
19
|
+
*
|
|
20
|
+
* TanStack's `autoResetPageIndex` does not cover it. It defaults to
|
|
21
|
+
* `!manualPagination`, so it is off exactly when this is needed, and it fires
|
|
22
|
+
* on a data change - which server-side is the response landing, one request
|
|
23
|
+
* too late.
|
|
24
|
+
*
|
|
25
|
+
* The reset rides on the slice's own change callback rather than on an effect
|
|
26
|
+
* over table state, so both writes land in the same render and the query goes
|
|
27
|
+
* out once. From an effect the request for the stale page would leave first.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** The state a server-side query is built from. */
|
|
31
|
+
export type TMDataGridQuerySlice =
|
|
32
|
+
| "columnFilters"
|
|
33
|
+
| "globalFilter"
|
|
34
|
+
| "sorting";
|
|
35
|
+
|
|
36
|
+
/** What the reset reads and writes - the table, narrowed to that. */
|
|
37
|
+
export type TMDataGridQueryTable = {
|
|
38
|
+
store: { state: TableState<TMDataGridFeatures> };
|
|
39
|
+
setPageIndex: (index: number) => void;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type QueryHandler = (updater: never) => void;
|
|
43
|
+
|
|
44
|
+
type ColumnFilters = TableState<TMDataGridFeatures>["columnFilters"];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The entries the server could answer differently. Only the grid's own
|
|
48
|
+
* `{ operator, value }` shape can be recognised as "still empty" - anything
|
|
49
|
+
* else, a custom filter control's raw value or state restored from a URL,
|
|
50
|
+
* narrows the grid as far as this module can know, so it always counts.
|
|
51
|
+
*/
|
|
52
|
+
function querySignificantFilters(filters: ColumnFilters): ColumnFilters {
|
|
53
|
+
return filters.filter(
|
|
54
|
+
(entry) =>
|
|
55
|
+
!isTMDataGridFilterValue(entry.value) || isFilterActive(entry.value),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether the server would answer the next state differently.
|
|
61
|
+
*
|
|
62
|
+
* A column filter with an empty value matches every row, so it is not part of
|
|
63
|
+
* the query: opening the filter panel seeds a row on the first filterable
|
|
64
|
+
* column, and the user then types into it. Neither moves the result set, and
|
|
65
|
+
* neither may throw away the page the user is on.
|
|
66
|
+
*/
|
|
67
|
+
function changesTheQuery(
|
|
68
|
+
slice: TMDataGridQuerySlice,
|
|
69
|
+
previous: unknown,
|
|
70
|
+
next: unknown,
|
|
71
|
+
): boolean {
|
|
72
|
+
if (slice === "columnFilters") {
|
|
73
|
+
return !sameStateValue(
|
|
74
|
+
querySignificantFilters(previous as ColumnFilters),
|
|
75
|
+
querySignificantFilters(next as ColumnFilters),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return !sameStateValue(previous, next);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Wraps one slice's change callback. `handler` is the consumer's, where they
|
|
83
|
+
* control the slice; without one the write is the same `makeStateUpdater`
|
|
84
|
+
* TanStack would have defaulted to, which this option replaces.
|
|
85
|
+
*/
|
|
86
|
+
export function withPageReset(
|
|
87
|
+
slice: TMDataGridQuerySlice,
|
|
88
|
+
handler: QueryHandler | undefined,
|
|
89
|
+
getTable: () => TMDataGridQueryTable,
|
|
90
|
+
): QueryHandler {
|
|
91
|
+
return (updater) => {
|
|
92
|
+
const table = getTable();
|
|
93
|
+
const previous = table.store.state[slice];
|
|
94
|
+
// Resolved here rather than read back off the table afterwards: where the
|
|
95
|
+
// consumer owns the slice the write is their `setState`, so the table
|
|
96
|
+
// still holds the old value when this returns. The updater therefore runs
|
|
97
|
+
// twice per change - once here, once in the write path - which is safe
|
|
98
|
+
// for every updater the grid and table-core produce, and a requirement on
|
|
99
|
+
// the consumer's: an updater has to be a pure derivation of the previous
|
|
100
|
+
// value. The decision is also best-effort under batching: two writes to
|
|
101
|
+
// one slice in the same tick both read the same `previous`, so the second
|
|
102
|
+
// decision is computed from a stale base. A wrong reset decision at
|
|
103
|
+
// worst, never a wrong write.
|
|
104
|
+
const next = functionalUpdate(updater as Updater<unknown>, previous);
|
|
105
|
+
|
|
106
|
+
(handler ?? (makeStateUpdater(slice, table as never) as QueryHandler))(
|
|
107
|
+
updater,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (!changesTheQuery(slice, previous, next)) return;
|
|
111
|
+
// Already on the first page: writing anyway would publish the store and
|
|
112
|
+
// re-render the grid for a value that did not move.
|
|
113
|
+
if (table.store.state.pagination.pageIndex !== 0) table.setPageIndex(0);
|
|
114
|
+
};
|
|
115
|
+
}
|
package/src/tmdatagrid/index.ts
CHANGED
|
@@ -45,11 +45,15 @@ export type {
|
|
|
45
45
|
TMDataGridMenuProps,
|
|
46
46
|
} from "./components/TMDataGridMenu";
|
|
47
47
|
export {
|
|
48
|
+
type TMDataGridColumnFilter,
|
|
48
49
|
type TMDataGridColumnType,
|
|
49
50
|
type TMDataGridFilterOperator,
|
|
50
51
|
type TMDataGridFilterValue,
|
|
52
|
+
type TMDataGridFilterValueShape,
|
|
53
|
+
activeColumnFilters,
|
|
51
54
|
emptyValueForOperator,
|
|
52
55
|
FILTER_OPERATOR_LABELS,
|
|
56
|
+
filterValueShape,
|
|
53
57
|
formatFilterLabel,
|
|
54
58
|
getDefaultOperator,
|
|
55
59
|
getOperatorsForType,
|
|
@@ -112,7 +116,16 @@ export type {
|
|
|
112
116
|
TMDataGridColumnFilterOptions,
|
|
113
117
|
TMDataGridFilterControlArgs,
|
|
114
118
|
TMDataGridFilterControlComponent,
|
|
119
|
+
TMDataGridFilterControlLayout,
|
|
120
|
+
TMDataGridFilterPanelLayout,
|
|
115
121
|
} from "./core/filterControls";
|
|
122
|
+
export type {
|
|
123
|
+
TMDataGridFiltersOptions,
|
|
124
|
+
TMDataGridFiltersSettings,
|
|
125
|
+
TMDataGridFilterSidebarSide,
|
|
126
|
+
TMDataGridFilterSurface,
|
|
127
|
+
} from "./core/filterSurface";
|
|
128
|
+
export type { TMDataGridFilterPanelProps } from "./components/TMDataGridFilterPanel";
|
|
116
129
|
export { TMDataGridFilterValueInput } from "./components/filters/TMDataGridFilterValueInput";
|
|
117
130
|
export { DgRangeSliderFilter } from "./components/filters/DgRangeSliderFilter";
|
|
118
131
|
export { DgDateRangeFilter } from "./components/filters/DgDateRangeFilter";
|
|
@@ -69,6 +69,11 @@ import {
|
|
|
69
69
|
} from "./core/filterOperators";
|
|
70
70
|
import { getColumnDefaultOperator, isControlColumn } from "./core/columnUtils";
|
|
71
71
|
import type { TMDataGridColumnFilterOptions } from "./core/filterControls";
|
|
72
|
+
import {
|
|
73
|
+
resolveFilterOptions,
|
|
74
|
+
type TMDataGridFiltersOptions,
|
|
75
|
+
type TMDataGridFiltersSettings,
|
|
76
|
+
} from "./core/filterSurface";
|
|
72
77
|
import {
|
|
73
78
|
createFuzzyRankedSortedRowModel,
|
|
74
79
|
fuzzyGlobalFilterFn,
|
|
@@ -101,6 +106,15 @@ import {
|
|
|
101
106
|
stabilizeControlledState,
|
|
102
107
|
withoutUndefinedSlices,
|
|
103
108
|
} from "./core/controlledState";
|
|
109
|
+
import {
|
|
110
|
+
beginControlledStateSync,
|
|
111
|
+
deferControlledStateSyncPublishes,
|
|
112
|
+
endControlledStateSync,
|
|
113
|
+
} from "./core/controlledStateSync";
|
|
114
|
+
import {
|
|
115
|
+
withPageReset,
|
|
116
|
+
type TMDataGridQueryTable,
|
|
117
|
+
} from "./core/pageReset";
|
|
104
118
|
import type { TMDataGridCellRange } from "./core/cellRange";
|
|
105
119
|
import {
|
|
106
120
|
createSelectColumn,
|
|
@@ -305,8 +319,20 @@ const EMPTY_IDS: ReadonlyArray<string> = [];
|
|
|
305
319
|
*/
|
|
306
320
|
export type TMDataGridUiState = {
|
|
307
321
|
filterPanelOpen: boolean;
|
|
308
|
-
/**
|
|
322
|
+
/**
|
|
323
|
+
* Column whose *panel* row should take the focus. Cleared once the row has
|
|
324
|
+
* taken it, so pointing at the same column twice focuses twice.
|
|
325
|
+
*/
|
|
309
326
|
filterPanelColumnId: string | null;
|
|
327
|
+
/**
|
|
328
|
+
* Column whose *header filter* control should take the focus, under
|
|
329
|
+
* `filters.inHeader`. Cleared once taken, like the one above.
|
|
330
|
+
*
|
|
331
|
+
* Its own slot rather than a second reader of `filterPanelColumnId`: a grid
|
|
332
|
+
* can have header filters and a panel at once, and two controls racing to
|
|
333
|
+
* answer one id means whichever mounted last wins the caret.
|
|
334
|
+
*/
|
|
335
|
+
headerFilterColumnId: string | null;
|
|
310
336
|
/**
|
|
311
337
|
* Column being dragged by its header, if any. Held here rather than read from
|
|
312
338
|
* `dataTransfer`, which browsers keep unreadable until the drop.
|
|
@@ -350,6 +376,18 @@ export type TMDataGridUiState = {
|
|
|
350
376
|
export type TMDataGridUiActions = {
|
|
351
377
|
openFilterPanel: (columnId?: string | null) => void;
|
|
352
378
|
closeFilterPanel: () => void;
|
|
379
|
+
/**
|
|
380
|
+
* Points at a column's row in the filter panel without opening anything.
|
|
381
|
+
* `openFilterPanel` does this as well as opening; this is the half a panel
|
|
382
|
+
* that is already showing needs.
|
|
383
|
+
*/
|
|
384
|
+
focusPanelFilter: (columnId: string | null) => void;
|
|
385
|
+
/**
|
|
386
|
+
* Points at a column's header filter control - what `openColumnFilter` does
|
|
387
|
+
* under `filters.inHeader`, where there is no panel to open. The header row
|
|
388
|
+
* scrolls the column into view and focuses it.
|
|
389
|
+
*/
|
|
390
|
+
focusHeaderFilter: (columnId: string | null) => void;
|
|
353
391
|
startColumnDrag: (columnId: string) => void;
|
|
354
392
|
endColumnDrag: () => void;
|
|
355
393
|
/**
|
|
@@ -401,6 +439,13 @@ export type TMDataGridApi<TData extends RowData> = {
|
|
|
401
439
|
edit: TMDataGridEditApi<TData>;
|
|
402
440
|
/** Table-level feature switches, re-read from options on every render. */
|
|
403
441
|
features: TMDataGridFeatureFlags;
|
|
442
|
+
/**
|
|
443
|
+
* Where the filter controls live, the `filters` option with its defaults
|
|
444
|
+
* filled in. On the api rather than in a component's props because the
|
|
445
|
+
* pills, the column menu and `openColumnFilter` all have to agree with the
|
|
446
|
+
* table about which surface is on.
|
|
447
|
+
*/
|
|
448
|
+
filters: TMDataGridFiltersSettings;
|
|
404
449
|
/** Every string the chrome renders, `labels` merged over the English defaults. */
|
|
405
450
|
labels: TMDataGridLabels;
|
|
406
451
|
/** The detail renderer, when row details are on. See `renderDetails`. */
|
|
@@ -690,12 +735,40 @@ export type UseTMDataGridOptions<TData extends RowData> = Omit<
|
|
|
690
735
|
* no extra flag.
|
|
691
736
|
*/
|
|
692
737
|
enablePagination?: boolean;
|
|
738
|
+
/**
|
|
739
|
+
* Sends the grid back to the first page whenever the query changes - a
|
|
740
|
+
* column filter, the quick search or the sort. Defaults to `true` under
|
|
741
|
+
* `manualPagination` and `false` otherwise, where TanStack's own
|
|
742
|
+
* `autoResetPageIndex` already does it.
|
|
743
|
+
*
|
|
744
|
+
* Server-side, `pageIndex` is a position in a result set the grid does not
|
|
745
|
+
* own: narrowing the query leaves it pointing past the last page, and the
|
|
746
|
+
* next request comes back empty. The reset is applied in the same event as
|
|
747
|
+
* the change, so one request goes out, for the first page of the new query.
|
|
748
|
+
*/
|
|
749
|
+
resetPageOnQueryChange?: boolean;
|
|
693
750
|
/**
|
|
694
751
|
* The row-number gutter: a generated lane, outermost left, numbering the
|
|
695
752
|
* rows of the current view - sorted, filtered, continuing across pages,
|
|
696
753
|
* with group rows unnumbered. Off by default.
|
|
697
754
|
*/
|
|
698
755
|
enableRowNumbers?: boolean;
|
|
756
|
+
/**
|
|
757
|
+
* Where the grid puts its filter controls - a popup over the rows, a sidebar
|
|
758
|
+
* beside them, controls in the header row, or nowhere at all so you place
|
|
759
|
+
* `TMDataGrid.FilterPanel` yourself.
|
|
760
|
+
*
|
|
761
|
+
* ```tsx
|
|
762
|
+
* useTMDataGrid({ data, columns, filters: { surface: "sidebar", inHeader: true } });
|
|
763
|
+
* ```
|
|
764
|
+
*
|
|
765
|
+
* Defaults to `{ surface: "popup" }` - the floating panel the grid has
|
|
766
|
+
* always shown. See {@link TMDataGridFiltersOptions}.
|
|
767
|
+
*
|
|
768
|
+
* Read field by field, so a literal is fine here - unlike `labels` or
|
|
769
|
+
* `persist`, this one does not have to be referentially stable.
|
|
770
|
+
*/
|
|
771
|
+
filters?: TMDataGridFiltersOptions;
|
|
699
772
|
/**
|
|
700
773
|
* How the quick search (`TMDataGrid.Search`) matches. `"fuzzy"` - the
|
|
701
774
|
* default - forgives typos and skipped characters, and while it is the
|
|
@@ -959,6 +1032,7 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
959
1032
|
labels: labelsOverride,
|
|
960
1033
|
enableColumnOrdering,
|
|
961
1034
|
enablePagination,
|
|
1035
|
+
resetPageOnQueryChange,
|
|
962
1036
|
enableRowNumbers,
|
|
963
1037
|
selectionMode,
|
|
964
1038
|
showSelectedBackground,
|
|
@@ -966,6 +1040,7 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
966
1040
|
onHighlightedRowChange,
|
|
967
1041
|
cellSelection,
|
|
968
1042
|
onFocusedCellChange,
|
|
1043
|
+
filters: filterOptions,
|
|
969
1044
|
editing,
|
|
970
1045
|
renderDetails,
|
|
971
1046
|
renderDetailsEstHeight = DEFAULT_DETAILS_EST_HEIGHT,
|
|
@@ -997,6 +1072,33 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
997
1072
|
// Resolved on the override's identity, so a module-scope dictionary costs one
|
|
998
1073
|
// merge for the lifetime of the grid.
|
|
999
1074
|
const labels = useMemo(() => mergeLabels(labelsOverride), [labelsOverride]);
|
|
1075
|
+
// Unpacked before the memo, so the api is keyed on the five fields rather
|
|
1076
|
+
// than on the object's identity - which is what lets `filters` be written as
|
|
1077
|
+
// a literal, the way it reads best, without republishing every render.
|
|
1078
|
+
const {
|
|
1079
|
+
surface: filterSurface,
|
|
1080
|
+
sidebarSide: filterSidebarSide,
|
|
1081
|
+
sidebarWidth: filterSidebarWidth,
|
|
1082
|
+
defaultOpen: filtersDefaultOpen,
|
|
1083
|
+
inHeader: filtersInHeader,
|
|
1084
|
+
} = filterOptions ?? {};
|
|
1085
|
+
const filters = useMemo(
|
|
1086
|
+
() =>
|
|
1087
|
+
resolveFilterOptions({
|
|
1088
|
+
surface: filterSurface,
|
|
1089
|
+
sidebarSide: filterSidebarSide,
|
|
1090
|
+
sidebarWidth: filterSidebarWidth,
|
|
1091
|
+
defaultOpen: filtersDefaultOpen,
|
|
1092
|
+
inHeader: filtersInHeader,
|
|
1093
|
+
}),
|
|
1094
|
+
[
|
|
1095
|
+
filterSurface,
|
|
1096
|
+
filterSidebarSide,
|
|
1097
|
+
filterSidebarWidth,
|
|
1098
|
+
filtersDefaultOpen,
|
|
1099
|
+
filtersInHeader,
|
|
1100
|
+
],
|
|
1101
|
+
);
|
|
1000
1102
|
|
|
1001
1103
|
const pinningEnabled = options.enableColumnPinning !== false;
|
|
1002
1104
|
const selectColumnEnabled = features.selectColumn;
|
|
@@ -1266,6 +1368,23 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1266
1368
|
[],
|
|
1267
1369
|
);
|
|
1268
1370
|
|
|
1371
|
+
// Filled immediately after the call below. The query-change wrappers close
|
|
1372
|
+
// over it rather than over `table`, since they are built as part of the
|
|
1373
|
+
// options the table is constructed from.
|
|
1374
|
+
const tableRef = useRef<TMDataGridTable<TData>>(null as never);
|
|
1375
|
+
// Server-side, a narrower query invalidates the page the grid is on - see
|
|
1376
|
+
// pageReset.ts. On by default only where the grid does not own the result
|
|
1377
|
+
// set; TanStack's `autoResetPageIndex` covers the client-side case.
|
|
1378
|
+
const resetPage = resetPageOnQueryChange ?? options.manualPagination === true;
|
|
1379
|
+
const getQueryTable = useCallback(
|
|
1380
|
+
() => tableRef.current as unknown as TMDataGridQueryTable,
|
|
1381
|
+
[],
|
|
1382
|
+
);
|
|
1383
|
+
|
|
1384
|
+
// The sync of `state` into the table's atoms happens inside this call, in
|
|
1385
|
+
// the render body, and publishing from there makes React warn about the
|
|
1386
|
+
// consumer's component - see controlledStateSync.ts.
|
|
1387
|
+
beginControlledStateSync();
|
|
1269
1388
|
const table = useTable({
|
|
1270
1389
|
// The grid paints a running drag itself, one style write per frame - see
|
|
1271
1390
|
// the resize preview in TMDataGridTable - and takes the width into state
|
|
@@ -1288,6 +1407,32 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1288
1407
|
// `"reorder"` to keep the column and have it moved to the front instead.
|
|
1289
1408
|
groupedColumnMode: "remove",
|
|
1290
1409
|
...options,
|
|
1410
|
+
// The query slices, wrapped so a change also takes the grid back to the
|
|
1411
|
+
// first page - see pageReset.ts. Spread conditionally: the keys carry a
|
|
1412
|
+
// `makeStateUpdater` default, and an explicit `undefined` would overwrite
|
|
1413
|
+
// it and leave the slice unwritable.
|
|
1414
|
+
...(resetPage
|
|
1415
|
+
? {
|
|
1416
|
+
onColumnFiltersChange: withPageReset(
|
|
1417
|
+
"columnFilters",
|
|
1418
|
+
options.onColumnFiltersChange as never,
|
|
1419
|
+
getQueryTable,
|
|
1420
|
+
) as TableOptions<
|
|
1421
|
+
TMDataGridFeatures,
|
|
1422
|
+
TData
|
|
1423
|
+
>["onColumnFiltersChange"],
|
|
1424
|
+
onGlobalFilterChange: withPageReset(
|
|
1425
|
+
"globalFilter",
|
|
1426
|
+
options.onGlobalFilterChange as never,
|
|
1427
|
+
getQueryTable,
|
|
1428
|
+
) as TableOptions<TMDataGridFeatures, TData>["onGlobalFilterChange"],
|
|
1429
|
+
onSortingChange: withPageReset(
|
|
1430
|
+
"sorting",
|
|
1431
|
+
options.onSortingChange as never,
|
|
1432
|
+
getQueryTable,
|
|
1433
|
+
) as TableOptions<TMDataGridFeatures, TData>["onSortingChange"],
|
|
1434
|
+
}
|
|
1435
|
+
: {}),
|
|
1291
1436
|
// The rows as shown - see `shown` above. The consumer's own array passes
|
|
1292
1437
|
// through untouched while nothing is committed.
|
|
1293
1438
|
data: shown.rows,
|
|
@@ -1367,6 +1512,9 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1367
1512
|
},
|
|
1368
1513
|
},
|
|
1369
1514
|
}, selectSettledState);
|
|
1515
|
+
endControlledStateSync();
|
|
1516
|
+
tableRef.current = table as unknown as TMDataGridTable<TData>;
|
|
1517
|
+
deferControlledStateSyncPublishes(table.store);
|
|
1370
1518
|
|
|
1371
1519
|
// The engine's view of this render - see the engine's creation above.
|
|
1372
1520
|
editContextRef.current = {
|
|
@@ -1553,8 +1701,11 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1553
1701
|
|
|
1554
1702
|
const ui = useCreateStore<TMDataGridUiState, TMDataGridUiActions>(
|
|
1555
1703
|
{
|
|
1556
|
-
|
|
1704
|
+
// `useCreateStore` builds the store once per mount, so `defaultOpen` is
|
|
1705
|
+
// read the way `initialState` is - a starting point, not a controller.
|
|
1706
|
+
filterPanelOpen: filters.defaultOpen,
|
|
1557
1707
|
filterPanelColumnId: null,
|
|
1708
|
+
headerFilterColumnId: null,
|
|
1558
1709
|
draggedColumnId: null,
|
|
1559
1710
|
// `useCreateStore` builds the store once per mount, so this is a genuine
|
|
1560
1711
|
// default rather than a value that would fight later clicks.
|
|
@@ -1576,6 +1727,10 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1576
1727
|
filterPanelOpen: false,
|
|
1577
1728
|
filterPanelColumnId: null,
|
|
1578
1729
|
})),
|
|
1730
|
+
focusPanelFilter: (columnId) =>
|
|
1731
|
+
setState((prev) => ({ ...prev, filterPanelColumnId: columnId })),
|
|
1732
|
+
focusHeaderFilter: (columnId) =>
|
|
1733
|
+
setState((prev) => ({ ...prev, headerFilterColumnId: columnId })),
|
|
1579
1734
|
startColumnDrag: (columnId) =>
|
|
1580
1735
|
setState((prev) => ({ ...prev, draggedColumnId: columnId })),
|
|
1581
1736
|
endColumnDrag: () =>
|
|
@@ -1701,6 +1856,7 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1701
1856
|
ui,
|
|
1702
1857
|
edit,
|
|
1703
1858
|
features,
|
|
1859
|
+
filters,
|
|
1704
1860
|
labels,
|
|
1705
1861
|
renderDetails,
|
|
1706
1862
|
renderDetailsEstHeight,
|
|
@@ -1712,20 +1868,44 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1712
1868
|
}
|
|
1713
1869
|
|
|
1714
1870
|
/**
|
|
1715
|
-
*
|
|
1716
|
-
*
|
|
1871
|
+
* Gives a column an empty filter of its default operator, unless it already
|
|
1872
|
+
* has one - which is what makes a surface open on a row rather than on
|
|
1873
|
+
* nothing.
|
|
1874
|
+
*
|
|
1875
|
+
* @internal Shared by `openColumnFilter` and the toolbar's filter button.
|
|
1876
|
+
*/
|
|
1877
|
+
export function seedColumnFilter<TData extends RowData>(
|
|
1878
|
+
api: TMDataGridApi<TData>,
|
|
1879
|
+
columnId: string,
|
|
1880
|
+
): void {
|
|
1881
|
+
const column = api.table.getColumn(columnId);
|
|
1882
|
+
if (column === undefined || column.getFilterValue() !== undefined) return;
|
|
1883
|
+
const operator = getColumnDefaultOperator(column);
|
|
1884
|
+
column.setFilterValue({ operator, value: emptyValueForOperator(operator) });
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
/**
|
|
1888
|
+
* Sends the user to a column's filter control, seeding an empty filter when
|
|
1889
|
+
* the column has none yet - what "Filter" in the column menu and a click on a
|
|
1890
|
+
* filter pill both do.
|
|
1891
|
+
*
|
|
1892
|
+
* Which control that is follows the grid's `filters` option. Under
|
|
1893
|
+
* `inHeader` it is the column's header control, which is already on screen, so
|
|
1894
|
+
* the call focuses it and leaves the popup or sidebar closed. Otherwise it is
|
|
1895
|
+
* the panel's row for that column, and the call opens the surface on it.
|
|
1717
1896
|
*/
|
|
1718
1897
|
export function openColumnFilter<TData extends RowData>(
|
|
1719
1898
|
api: TMDataGridApi<TData>,
|
|
1720
1899
|
columnId: string,
|
|
1721
1900
|
): void {
|
|
1722
|
-
|
|
1723
|
-
if (
|
|
1724
|
-
|
|
1725
|
-
column.
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1901
|
+
seedColumnFilter(api, columnId);
|
|
1902
|
+
if (api.filters.inHeader) {
|
|
1903
|
+
// The header control is always visible, so there is nothing to open -
|
|
1904
|
+
// only a column to point at. The header row watching this focuses it and
|
|
1905
|
+
// scrolls it into view. The panel, if one is also showing, is left alone:
|
|
1906
|
+
// it reads the other slot.
|
|
1907
|
+
api.ui.actions.focusHeaderFilter(columnId);
|
|
1908
|
+
return;
|
|
1729
1909
|
}
|
|
1730
1910
|
api.ui.actions.openFilterPanel(columnId);
|
|
1731
1911
|
}
|