@jielga/tmdatagrid 2.0.0-beta.9 → 2.0.1

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 (157) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1281 -768
  3. package/dist/index.js +4607 -3250
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/adding-rows.md +132 -0
  7. package/docs/anatomy.md +119 -0
  8. package/docs/card-view.md +108 -0
  9. package/docs/cell-selection.md +194 -0
  10. package/docs/column-layout.md +182 -0
  11. package/docs/column-menu.md +66 -0
  12. package/docs/columns.md +269 -0
  13. package/docs/components.md +311 -0
  14. package/docs/draft-store.md +242 -0
  15. package/docs/editing.md +303 -0
  16. package/docs/editors.md +250 -0
  17. package/docs/export.md +319 -0
  18. package/docs/filtering.md +362 -0
  19. package/docs/getting-started.md +123 -0
  20. package/docs/grouping.md +165 -0
  21. package/docs/loading-and-empty.md +92 -0
  22. package/docs/localization.md +79 -0
  23. package/docs/menu.md +143 -0
  24. package/docs/migrating-to-2.md +163 -0
  25. package/docs/pagination.md +144 -0
  26. package/docs/persistence.md +114 -0
  27. package/docs/portfolio-rebalancer.md +94 -0
  28. package/docs/query-builder.md +179 -0
  29. package/docs/quick-search.md +84 -0
  30. package/docs/row-details.md +115 -0
  31. package/docs/row-interaction.md +149 -0
  32. package/docs/row-pinning.md +132 -0
  33. package/docs/row-selection.md +136 -0
  34. package/docs/row-styling.md +133 -0
  35. package/docs/scrolling.md +112 -0
  36. package/docs/server-query.md +246 -0
  37. package/docs/server-side.md +206 -0
  38. package/docs/sorting.md +101 -0
  39. package/docs/styling.md +126 -0
  40. package/docs/summary-row.md +76 -0
  41. package/docs/testing.md +744 -0
  42. package/docs/toolbar.md +161 -0
  43. package/docs/use-tm-data-grid.md +361 -0
  44. package/package.json +22 -46
  45. package/skills/appearance/SKILL.md +72 -19
  46. package/skills/cell-selection/SKILL.md +49 -48
  47. package/skills/columns/SKILL.md +125 -70
  48. package/skills/columns/references/columns-api.md +59 -0
  49. package/skills/data/SKILL.md +112 -18
  50. package/skills/editing/SKILL.md +76 -42
  51. package/skills/editing/references/common-mistakes.md +77 -69
  52. package/skills/editing/references/editing-api.md +25 -20
  53. package/skills/editing/references/editors-and-validation.md +80 -18
  54. package/skills/filtering/SKILL.md +155 -41
  55. package/skills/getting-started/SKILL.md +116 -16
  56. package/skills/grouping/SKILL.md +31 -16
  57. package/skills/migrating-to-2/SKILL.md +244 -0
  58. package/skills/options/SKILL.md +24 -12
  59. package/skills/rows/SKILL.md +22 -18
  60. package/skills/rows/references/rows-api.md +10 -6
  61. package/skills/server-side/SKILL.md +170 -17
  62. package/skills/testing/SKILL.md +150 -32
  63. package/skills/testing-components/SKILL.md +230 -0
  64. package/skills/testing-editing/SKILL.md +240 -0
  65. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
  66. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +54 -8
  67. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  68. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  69. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +5 -51
  70. package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
  71. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +12 -59
  72. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +164 -92
  73. package/src/components/TMDataGridExportPicker.module.css +77 -0
  74. package/src/components/TMDataGridExportPicker.tsx +234 -0
  75. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  76. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  77. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
  78. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  79. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  80. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
  81. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +5 -69
  82. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
  83. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
  84. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  85. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  86. package/src/components/TMDataGridMenu.tsx +357 -0
  87. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +11 -48
  88. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +69 -56
  89. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +240 -138
  90. package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
  91. package/src/components/TMDataGridToolbar.tsx +181 -0
  92. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  93. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  94. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  95. package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
  96. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  97. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  98. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
  99. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  100. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  101. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  102. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  103. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  104. package/src/components/filters/controlLayout.ts +32 -0
  105. package/src/components/filters/filterControlFor.ts +65 -0
  106. package/src/components/generatedColumns.tsx +187 -0
  107. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  108. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  109. package/src/components/useHideableColumns.ts +52 -0
  110. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  111. package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
  112. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  113. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  114. package/src/core/controlledStateSync.ts +108 -0
  115. package/src/core/deletedRows.ts +34 -0
  116. package/src/core/dom.ts +74 -0
  117. package/src/{tmdatagrid/core → core}/editEngine.ts +1107 -460
  118. package/src/core/export.ts +704 -0
  119. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  120. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  121. package/src/core/filterSurface.ts +99 -0
  122. package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
  123. package/src/{tmdatagrid/core → core}/labels.ts +51 -6
  124. package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
  125. package/src/core/pageReset.ts +120 -0
  126. package/src/core/pagination.ts +81 -0
  127. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  128. package/src/{tmdatagrid/core → core}/summary.ts +20 -4
  129. package/src/{tmdatagrid/index.ts → index.ts} +69 -35
  130. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +428 -109
  131. package/src/useTMDataGridExport.ts +78 -0
  132. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  133. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  134. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
  135. package/src/tmdatagrid/core/cellExport.ts +0 -320
  136. /package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  141. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  142. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  143. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  144. /package/src/{tmdatagrid/core → core}/capabilities.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/editorFocus.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  151. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  152. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  153. /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
  154. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  155. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  156. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
  157. /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
@@ -0,0 +1,78 @@
1
+ import { useSelector } from "@tanstack/react-store";
2
+ import { useTMDataGridContext } from "./TMDataGridContext";
3
+ import {
4
+ buildExportData,
5
+ countSelectedExportRows,
6
+ resolveExportOptions,
7
+ writeExportFile,
8
+ type TMDataGridExportOptions,
9
+ } from "./core/export";
10
+
11
+ /** What {@link useTMDataGridExport} returns. */
12
+ export type TMDataGridExportApi = {
13
+ /**
14
+ * Downloads every filtered and sorted row, all pages. `overrides` are folded
15
+ * over the hook's options for this one call - the column picker's chosen
16
+ * columns, for instance.
17
+ */
18
+ exportAll: (overrides?: TMDataGridExportOptions) => Promise<void>;
19
+ /**
20
+ * Downloads the selected rows of the current view, in grid order. Resolves
21
+ * without a download when none is selected.
22
+ */
23
+ exportSelected: (overrides?: TMDataGridExportOptions) => Promise<void>;
24
+ /** How many rows `exportSelected` would write. Subscribes to the selection. */
25
+ selectedCount: number;
26
+ /**
27
+ * Whether row selection is on at all - `false` under
28
+ * `selectionMode: "highlight"` or `enableRowSelection: false`, where a
29
+ * "selected rows" control has nothing to offer.
30
+ */
31
+ canExportSelected: boolean;
32
+ };
33
+
34
+ /**
35
+ * The export as click handlers, for a button of your own anywhere inside the
36
+ * grid. The `TMDataGrid.Menu.Export*` items are this hook behind a
37
+ * `Menu.Item`.
38
+ *
39
+ * ```tsx
40
+ * function ExportButton() {
41
+ * const { exportAll } = useTMDataGridExport();
42
+ * return <Button onClick={() => void exportAll()}>Export</Button>;
43
+ * }
44
+ * ```
45
+ *
46
+ * `overrides` are folded over the grid's `exportOptions` for this caller:
47
+ * another format, another file name, another set of columns.
48
+ */
49
+ export function useTMDataGridExport(
50
+ overrides?: TMDataGridExportOptions,
51
+ ): TMDataGridExportApi {
52
+ const { table, features, exportOptions } = useTMDataGridContext();
53
+ const selectedCount = useSelector(table.store, () =>
54
+ countSelectedExportRows(table),
55
+ );
56
+
57
+ const run = async (
58
+ rows: "all" | "selected",
59
+ callOverrides?: TMDataGridExportOptions,
60
+ ) => {
61
+ const resolved = resolveExportOptions(
62
+ exportOptions,
63
+ overrides,
64
+ callOverrides,
65
+ );
66
+ const data = buildExportData({ table, rows, columns: resolved.columns });
67
+ if (data.columnIds.length === 0) return;
68
+ if (rows === "selected" && data.rows.length === 0) return;
69
+ await writeExportFile(data, resolved);
70
+ };
71
+
72
+ return {
73
+ exportAll: (callOverrides) => run("all", callOverrides),
74
+ exportSelected: (callOverrides) => run("selected", callOverrides),
75
+ selectedCount,
76
+ canExportSelected: features.rowSelection,
77
+ };
78
+ }
@@ -1,35 +0,0 @@
1
- /* Floats over the first body rows, anchored to `.tableWrapper`. Sits just
2
- below the header row, so its offset follows the size scale rather than a
3
- fixed pixel count. */
4
- .filterPanel {
5
- position: absolute;
6
- top: calc(var(--dg-header-height) + 2px);
7
- left: var(--mantine-spacing-md);
8
- z-index: 5;
9
- padding: var(--mantine-spacing-sm);
10
- border-radius: var(--mantine-radius-md);
11
- border: 1px solid var(--mantine-color-default-border);
12
- background-color: var(--mantine-color-body);
13
- box-shadow: var(--mantine-shadow-md);
14
- }
15
-
16
- .filterRow {
17
- display: flex;
18
- align-items: flex-end;
19
- gap: var(--mantine-spacing-xs);
20
- }
21
-
22
- .filterPanelHeader {
23
- display: flex;
24
- align-items: center;
25
- justify-content: space-between;
26
- gap: var(--mantine-spacing-md);
27
- padding-bottom: var(--mantine-spacing-xs);
28
- }
29
-
30
- .filterPanelFooter {
31
- display: flex;
32
- align-items: center;
33
- justify-content: space-between;
34
- gap: var(--mantine-spacing-md);
35
- }
@@ -1,354 +0,0 @@
1
- import {
2
- ActionIcon,
3
- Button,
4
- Select,
5
- Stack,
6
- Text,
7
- TextInput,
8
- } from "@mantine/core";
9
- import { useSelector } from "@tanstack/react-store";
10
- import { useEffect, useRef } from "react";
11
- import classes from "./TMDataGridFilterPanel.module.css";
12
- import { useTMDataGridContext } from "../TMDataGridContext";
13
- import { resolveColumnOptions } from "../core/columnOptions";
14
- import {
15
- getColumnDefaultOperator,
16
- getColumnFilterControl,
17
- getColumnLabel,
18
- getColumnType,
19
- } from "../core/columnUtils";
20
- import {
21
- type TMDataGridFilterOperator,
22
- type TMDataGridFilterValue,
23
- emptyValueForOperator,
24
- getDefaultOperator,
25
- getOperatorsForType,
26
- isTMDataGridFilterValue,
27
- operatorNeedsValue,
28
- operatorTakesArrayValue,
29
- operatorTakesRangeValue,
30
- } from "../core/filterOperators";
31
- import { CloseIcon } from "./icons";
32
- import { TMDataGridFilterValueInput } from "./filters/TMDataGridFilterValueInput";
33
-
34
- const FALLBACK_FILTER: TMDataGridFilterValue = { operator: "contains", value: "" };
35
-
36
- function asFilterValue(value: unknown): TMDataGridFilterValue {
37
- return isTMDataGridFilterValue(value) ? value : FALLBACK_FILTER;
38
- }
39
-
40
- /**
41
- * The three value shapes an operator can take. A typed value survives an
42
- * operator or column change only within its shape - a set is not a range,
43
- * even though both are arrays.
44
- */
45
- function valueShape(
46
- operator: TMDataGridFilterOperator,
47
- ): "scalar" | "set" | "range" {
48
- if (operatorTakesArrayValue(operator)) return "set";
49
- if (operatorTakesRangeValue(operator)) return "range";
50
- return "scalar";
51
- }
52
-
53
- /**
54
- * The MUI-style filter surface: one row per active column filter, each row a
55
- * column / operator / value triple. It only ever reads and writes the table's
56
- * `columnFilters` state, so a `manualFiltering` grid gets the same panel for
57
- * free - the state is forwarded to the server instead of a row model.
58
- */
59
- export function TMDataGridFilterPanel() {
60
- const { table, ui, labels, controlSize } = useTMDataGridContext();
61
- const opened = useSelector(ui, (state) => state.filterPanelOpen);
62
- const columnFilters = useSelector(
63
- table.store,
64
- (state) => state.columnFilters,
65
- );
66
- const panelRef = useRef<HTMLDivElement>(null);
67
-
68
- // Clicking away hides the panel, the way any floating surface behaves. On
69
- // pointerdown rather than click, so a press that starts outside dismisses it
70
- // even when the pointer is released somewhere else.
71
- useEffect(() => {
72
- if (!opened) return;
73
-
74
- const handlePointerDown = (event: PointerEvent) => {
75
- const target = event.target;
76
- if (!(target instanceof Element)) return;
77
- if (panelRef.current?.contains(target)) return;
78
- // The toolbar button toggles the panel itself. Closing from here first
79
- // would leave its click reopening what the user meant to close.
80
- if (target.closest('[data-dg-part="filter-button"]')) return;
81
- ui.actions.closeFilterPanel();
82
- };
83
-
84
- document.addEventListener("pointerdown", handlePointerDown);
85
- return () => document.removeEventListener("pointerdown", handlePointerDown);
86
- }, [opened, ui]);
87
-
88
- if (!opened) return null;
89
-
90
- const filterableColumns = table
91
- .getAllLeafColumns()
92
- .filter((column) => column.getCanFilter());
93
-
94
- const columnOptions = filterableColumns.map((column) => ({
95
- value: column.id,
96
- label: getColumnLabel(column),
97
- }));
98
-
99
- function removeFilter(columnId: string) {
100
- const remaining = columnFilters.filter((filter) => filter.id !== columnId);
101
- table.setColumnFilters(remaining);
102
- if (remaining.length === 0) ui.actions.closeFilterPanel();
103
- }
104
-
105
- function changeFilterColumn(fromColumnId: string, toColumnId: string) {
106
- const current = asFilterValue(
107
- columnFilters.find((filter) => filter.id === fromColumnId)?.value,
108
- );
109
- const target = table.getColumn(toColumnId);
110
- const operator = target
111
- ? getColumnDefaultOperator(target)
112
- : getDefaultOperator("string");
113
- // The typed value only survives the move while it still fits the new
114
- // operator - a text needle has no meaning to an `isAnyOf` set, or a set
115
- // to a text input.
116
- const value =
117
- valueShape(operator) === valueShape(current.operator)
118
- ? current.value
119
- : emptyValueForOperator(operator);
120
- table.setColumnFilters(
121
- columnFilters
122
- .filter((filter) => filter.id !== toColumnId)
123
- .map((filter) =>
124
- filter.id === fromColumnId
125
- ? {
126
- id: toColumnId,
127
- value: { operator, value } satisfies TMDataGridFilterValue,
128
- }
129
- : filter,
130
- ),
131
- );
132
- ui.actions.openFilterPanel(toColumnId);
133
- }
134
-
135
- function patchFilter(columnId: string, patch: Partial<TMDataGridFilterValue>) {
136
- table.setColumnFilters(
137
- columnFilters.map((filter) =>
138
- filter.id === columnId
139
- ? { ...filter, value: { ...asFilterValue(filter.value), ...patch } }
140
- : filter,
141
- ),
142
- );
143
- }
144
-
145
- function changeOperator(columnId: string, operator: TMDataGridFilterOperator) {
146
- const current = asFilterValue(
147
- columnFilters.find((filter) => filter.id === columnId)?.value,
148
- );
149
- // Same arity rule as changing the column: keep the value across operators
150
- // of the same shape, reset it across any shape boundary.
151
- const value =
152
- valueShape(operator) === valueShape(current.operator)
153
- ? current.value
154
- : emptyValueForOperator(operator);
155
- patchFilter(columnId, { operator, value });
156
- }
157
-
158
- /** Clears every filter, including half-typed ones the pills never showed. */
159
- function clearAllFilters() {
160
- table.setColumnFilters([]);
161
- // Same exit as removing the last filter row by hand: an empty panel has
162
- // nothing to show but its own buttons.
163
- ui.actions.closeFilterPanel();
164
- }
165
-
166
- function addFilter() {
167
- const nextColumn = filterableColumns.find(
168
- (column) => !columnFilters.some((filter) => filter.id === column.id),
169
- );
170
- if (!nextColumn) return;
171
- const operator = getColumnDefaultOperator(nextColumn);
172
- table.setColumnFilters([
173
- ...columnFilters,
174
- {
175
- id: nextColumn.id,
176
- value: {
177
- operator,
178
- value: emptyValueForOperator(operator),
179
- } satisfies TMDataGridFilterValue,
180
- },
181
- ]);
182
- }
183
-
184
- const canAddFilter = filterableColumns.some(
185
- (column) => !columnFilters.some((filter) => filter.id === column.id),
186
- );
187
-
188
- return (
189
- <div
190
- ref={panelRef}
191
- role="group"
192
- aria-label={labels.filters}
193
- data-dg-part="filter-panel"
194
- className={classes.filterPanel}
195
- // Escape is what closes a floating surface, and every control that can
196
- // hold focus in here sits inside this element.
197
- onKeyDown={(event) => {
198
- if (event.key !== "Escape") return;
199
- event.stopPropagation();
200
- ui.actions.closeFilterPanel();
201
- }}
202
- >
203
- <div className={classes.filterPanelHeader}>
204
- <Text span size={controlSize} fw={600}>
205
- {labels.filters}
206
- </Text>
207
- <ActionIcon
208
- variant="subtle"
209
- color="gray"
210
- size="sm"
211
- aria-label={labels.closeFilters}
212
- data-dg-part="filter-panel-close"
213
- onClick={ui.actions.closeFilterPanel}
214
- >
215
- <CloseIcon size={16} stroke={1.6} />
216
- </ActionIcon>
217
- </div>
218
-
219
- <Stack gap="xs">
220
- {columnFilters.map((filter) => {
221
- const column = table.getColumn(filter.id);
222
- const value = asFilterValue(filter.value);
223
- const type = column ? getColumnType(column) : "string";
224
- const needsValue = operatorNeedsValue(value.operator);
225
- const scalarValue = typeof value.value === "string" ? value.value : "";
226
- // Pre-resolved only where options mean something out of the box -
227
- // a declared set, or a select-shaped column's faceted values. A
228
- // custom control wanting faceted values elsewhere resolves them
229
- // itself; resolving here would build the faceted index for every
230
- // filtered column.
231
- const options =
232
- column &&
233
- (column.columnDef.meta?.options !== undefined ||
234
- type === "select" ||
235
- type === "multiSelect")
236
- ? resolveColumnOptions({ table, column, fallback: "faceted" })
237
- : [];
238
- const ValueControl =
239
- (column === undefined ? undefined : getColumnFilterControl(column)) ??
240
- TMDataGridFilterValueInput;
241
-
242
- return (
243
- <div
244
- key={filter.id}
245
- // The row is the handle: a `meta.filter.control` renders whatever
246
- // it likes in the value slot, so the one thing a test can always
247
- // count on is which row it is in.
248
- data-dg-part="filter-row"
249
- data-column-id={filter.id}
250
- className={classes.filterRow}
251
- >
252
- <ActionIcon
253
- variant="subtle"
254
- color="gray"
255
- size="lg"
256
- aria-label={labels.removeFilter}
257
- data-dg-part="filter-remove"
258
- onClick={() => removeFilter(filter.id)}
259
- >
260
- <CloseIcon size={18} stroke={1.6} />
261
- </ActionIcon>
262
-
263
- <Select
264
- label={labels.filterColumn}
265
- size={controlSize}
266
- w={160}
267
- allowDeselect={false}
268
- // Both dropdowns render inside the panel: a portalled one is
269
- // outside it in the DOM, and picking an option would read as a
270
- // click away and close the panel under the user.
271
- comboboxProps={{ withinPortal: false }}
272
- data-dg-part="filter-column"
273
- data={columnOptions}
274
- value={filter.id}
275
- onChange={(next) => next && changeFilterColumn(filter.id, next)}
276
- />
277
-
278
- <Select
279
- label={labels.filterOperator}
280
- size={controlSize}
281
- w={170}
282
- allowDeselect={false}
283
- comboboxProps={{ withinPortal: false }}
284
- data-dg-part="filter-operator"
285
- data={getOperatorsForType(type).map((operator) => ({
286
- value: operator,
287
- label: labels.operators[operator],
288
- }))}
289
- value={value.operator}
290
- onChange={(next) =>
291
- next &&
292
- changeOperator(filter.id, next as TMDataGridFilterOperator)
293
- }
294
- />
295
-
296
- {column ? (
297
- // The value slot: `meta.filter.control` if the column declares
298
- // one, the built-in shape-by-operator input otherwise - both
299
- // through the same value-only contract.
300
- <ValueControl
301
- column={column}
302
- table={table}
303
- operator={value.operator}
304
- value={value.value}
305
- onChange={(next) => patchFilter(filter.id, { value: next })}
306
- options={options}
307
- size={controlSize}
308
- labels={labels}
309
- />
310
- ) : (
311
- // The column is gone from the definition; the row survives
312
- // only to be re-pointed or removed.
313
- <TextInput
314
- label={labels.filterValue}
315
- size={controlSize}
316
- w={180}
317
- data-dg-part="filter-value"
318
- disabled={!needsValue}
319
- placeholder={needsValue ? labels.filterValuePlaceholder : ""}
320
- value={needsValue ? scalarValue : ""}
321
- onChange={(event) =>
322
- patchFilter(filter.id, { value: event.currentTarget.value })
323
- }
324
- />
325
- )}
326
- </div>
327
- );
328
- })}
329
-
330
- <div className={classes.filterPanelFooter}>
331
- <Button
332
- variant="subtle"
333
- size="compact-sm"
334
- disabled={!canAddFilter}
335
- data-dg-part="filter-add"
336
- onClick={addFilter}
337
- >
338
- {labels.addFilter}
339
- </Button>
340
- <Button
341
- variant="subtle"
342
- color="gray"
343
- size="compact-sm"
344
- disabled={columnFilters.length === 0}
345
- data-dg-part="filter-clear-all"
346
- onClick={clearAllFilters}
347
- >
348
- {labels.clearAllFilters}
349
- </Button>
350
- </div>
351
- </Stack>
352
- </div>
353
- );
354
- }
@@ -1,161 +0,0 @@
1
- import { ActionIcon, Loader, Popover, Text, Tooltip } from "@mantine/core";
2
- import { useSelector } from "@tanstack/react-store";
3
- import type { ReactNode } from "react";
4
- import classes from "./TMDataGridToolbar.module.css";
5
- import { TMDataGridColumnsPanel } from "./TMDataGridColumnsPanel";
6
- import { useTMDataGridContext } from "../TMDataGridContext";
7
- import { getGridCapabilities } from "../core/capabilities";
8
- import { useSettledTableState } from "../core/useSettledTableState";
9
- import { isFilterActive } from "../core/filterOperators";
10
- import { BurgerIcon, FilterIcon } from "./icons";
11
- import { openColumnFilter } from "../useTMDataGrid";
12
-
13
- /** Row above the grid. Compose it from the pieces below, or anything else. */
14
- export function TMDataGridToolbar({ children }: { children?: ReactNode }) {
15
- return (
16
- <div data-dg-part="toolbar" className={classes.toolbar}>
17
- {children}
18
- </div>
19
- );
20
- }
21
-
22
- /** Pushes the following toolbar items to the right. */
23
- export function TMDataGridToolbarSpacer() {
24
- return <div className={classes.toolbarSpacer} />;
25
- }
26
-
27
- /**
28
- * A small spinner shown while `meta.loading` is true, and nothing otherwise.
29
- *
30
- * The body only shows its loading state while the grid is *empty* - a
31
- * server-driven grid refetching with rows on screen keeps showing them, which
32
- * is right, but leaves nothing saying a fetch is running. This is that signal,
33
- * and the consumer decides where it sits by placing it in the toolbar:
34
- *
35
- * ```tsx
36
- * <TMDataGrid.Toolbar>
37
- * <TMDataGrid.SummaryCount />
38
- * <TMDataGrid.Spacer />
39
- * <TMDataGrid.LoadingIndicator />
40
- * <TMDataGrid.FilterButton />
41
- * </TMDataGrid.Toolbar>
42
- * ```
43
- */
44
- export function TMDataGridLoadingIndicator() {
45
- const { table, labels } = useTMDataGridContext();
46
- if (table.options.meta?.loading !== true) return null;
47
- return <Loader size="xs" data-dg-part="loading" aria-label={labels.loading} />;
48
- }
49
-
50
- /**
51
- * Visible rows over total rows. On a server-driven grid, set
52
- * `meta.totalRowCount` for the denominator - the client never sees every row.
53
- */
54
- export function TMDataGridSummaryCount({ children }: { children?: ReactNode }) {
55
- const { table, controlSize } = useTMDataGridContext();
56
- useSettledTableState(table.store);
57
-
58
- const shown = table.getRowCount();
59
- const total =
60
- table.options.meta?.totalRowCount ??
61
- table.getPreFilteredRowModel().rows.length;
62
-
63
- return (
64
- <Text
65
- span
66
- size={controlSize}
67
- c="dimmed"
68
- className={classes.summaryCount}
69
- data-dg-part="summary-count"
70
- >
71
- {children !== undefined ? children : `${shown} / ${total}`}
72
- </Text>
73
- );
74
- }
75
-
76
- /**
77
- * Burger menu in the grid's top-right corner - opens "Manage columns".
78
- * Renders nothing when no column can be hidden (`enableHiding: false`).
79
- */
80
- export function TMDataGridColumnsButton() {
81
- const { table, ui, features, labels, controlSize } = useTMDataGridContext();
82
- const opened = useSelector(ui, (state) => state.columnsPanelOpen);
83
-
84
- if (!getGridCapabilities(table, features).canHideAny) return null;
85
-
86
- return (
87
- <Popover
88
- opened={opened}
89
- onChange={ui.actions.setColumnsPanelOpen}
90
- position="bottom-end"
91
- shadow="md"
92
- radius="md"
93
- withinPortal
94
- trapFocus
95
- >
96
- <Popover.Target>
97
- <Tooltip label={labels.manageColumns} openDelay={400}>
98
- <ActionIcon
99
- variant="subtle"
100
- color="gray"
101
- size={controlSize}
102
- aria-label={labels.manageColumns}
103
- data-dg-part="columns-button"
104
- onClick={ui.actions.toggleColumnsPanel}
105
- >
106
- <BurgerIcon size={18} stroke={1.6} />
107
- </ActionIcon>
108
- </Tooltip>
109
- </Popover.Target>
110
- <Popover.Dropdown p={0}>
111
- <TMDataGridColumnsPanel />
112
- </Popover.Dropdown>
113
- </Popover>
114
- );
115
- }
116
-
117
- /**
118
- * Toggles the filter panel, seeding a filter row on the first filterable column.
119
- * Renders nothing when no column can be filtered (`enableColumnFilters: false`).
120
- */
121
- export function TMDataGridFilterButton() {
122
- const api = useTMDataGridContext();
123
- const { table, ui, features, labels, controlSize } = api;
124
- const opened = useSelector(ui, (state) => state.filterPanelOpen);
125
- const columnFilters = useSelector(
126
- table.store,
127
- (state) => state.columnFilters,
128
- );
129
- const activeCount = columnFilters.filter((filter) =>
130
- isFilterActive(filter.value),
131
- ).length;
132
-
133
- if (!getGridCapabilities(table, features).canFilterAny) return null;
134
-
135
- return (
136
- <Tooltip label={labels.filters} openDelay={400}>
137
- <ActionIcon
138
- variant={opened || activeCount > 0 ? "light" : "subtle"}
139
- color={activeCount > 0 ? undefined : "gray"}
140
- size={controlSize}
141
- aria-label={labels.filters}
142
- // Also what the panel's click-away handler tests for, so that a click
143
- // here reads as a toggle rather than as a click outside.
144
- data-dg-part="filter-button"
145
- onClick={() => {
146
- if (opened) {
147
- ui.actions.closeFilterPanel();
148
- return;
149
- }
150
- const firstFilterable = table
151
- .getAllLeafColumns()
152
- .find((column) => column.getCanFilter());
153
- if (firstFilterable) openColumnFilter(api, firstFilterable.id);
154
- else ui.actions.openFilterPanel();
155
- }}
156
- >
157
- <FilterIcon size={16} stroke={1.6} />
158
- </ActionIcon>
159
- </Tooltip>
160
- );
161
- }