@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.20

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 (150) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1664 -632
  3. package/dist/index.js +5226 -3223
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/anatomy.md +102 -0
  7. package/docs/cell-selection.md +154 -0
  8. package/docs/column-layout.md +204 -0
  9. package/docs/columns.md +262 -0
  10. package/docs/components.md +304 -0
  11. package/docs/editing.md +603 -0
  12. package/docs/editors.md +250 -0
  13. package/docs/export.md +326 -0
  14. package/docs/filtering.md +358 -0
  15. package/docs/getting-started.md +123 -0
  16. package/docs/grouping.md +165 -0
  17. package/docs/loading-and-empty.md +92 -0
  18. package/docs/localization.md +79 -0
  19. package/docs/menu.md +143 -0
  20. package/docs/pagination.md +144 -0
  21. package/docs/persistence.md +111 -0
  22. package/docs/portfolio-rebalancer.md +94 -0
  23. package/docs/query-builder.md +175 -0
  24. package/docs/quick-search.md +83 -0
  25. package/docs/row-details.md +113 -0
  26. package/docs/row-interaction.md +148 -0
  27. package/docs/row-pinning.md +132 -0
  28. package/docs/row-selection.md +134 -0
  29. package/docs/row-styling.md +133 -0
  30. package/docs/scrolling.md +111 -0
  31. package/docs/server-query.md +246 -0
  32. package/docs/server-side.md +206 -0
  33. package/docs/sorting.md +101 -0
  34. package/docs/styling.md +126 -0
  35. package/docs/summary-row.md +76 -0
  36. package/docs/testing.md +309 -0
  37. package/docs/toolbar.md +161 -0
  38. package/docs/use-tm-data-grid.md +361 -0
  39. package/package.json +21 -45
  40. package/skills/appearance/SKILL.md +70 -17
  41. package/skills/cell-selection/SKILL.md +70 -76
  42. package/skills/columns/SKILL.md +131 -32
  43. package/skills/data/SKILL.md +100 -23
  44. package/skills/editing/SKILL.md +217 -96
  45. package/skills/editing/references/common-mistakes.md +111 -24
  46. package/skills/editing/references/editing-api.md +63 -39
  47. package/skills/editing/references/editors-and-validation.md +77 -19
  48. package/skills/filtering/SKILL.md +148 -40
  49. package/skills/getting-started/SKILL.md +18 -16
  50. package/skills/grouping/SKILL.md +32 -15
  51. package/skills/options/SKILL.md +39 -9
  52. package/skills/rows/SKILL.md +22 -18
  53. package/skills/server-side/SKILL.md +170 -17
  54. package/skills/testing/SKILL.md +10 -7
  55. package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
  56. package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
  57. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
  58. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
  59. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  60. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  61. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
  62. package/src/components/TMDataGridDraftActions.tsx +307 -0
  63. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
  64. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
  65. package/src/components/TMDataGridExportPicker.module.css +77 -0
  66. package/src/components/TMDataGridExportPicker.tsx +234 -0
  67. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  68. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  69. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
  70. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  71. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  72. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
  73. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
  74. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
  75. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
  76. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  77. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  78. package/src/components/TMDataGridMenu.tsx +354 -0
  79. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
  80. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
  81. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
  82. package/src/components/TMDataGridToolbar.module.css +21 -0
  83. package/src/components/TMDataGridToolbar.tsx +181 -0
  84. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  85. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  86. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  87. package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
  88. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
  91. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  92. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  93. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  94. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  95. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  96. package/src/components/filters/controlLayout.ts +32 -0
  97. package/src/components/filters/filterControlFor.ts +65 -0
  98. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  99. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  100. package/src/components/useHideableColumns.ts +52 -0
  101. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  102. package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
  103. package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
  104. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  105. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  106. package/src/core/controlledState.ts +179 -0
  107. package/src/core/controlledStateSync.ts +108 -0
  108. package/src/core/deletedRows.ts +34 -0
  109. package/src/core/dom.ts +74 -0
  110. package/src/core/editEngine.ts +2476 -0
  111. package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
  112. package/src/core/export.ts +843 -0
  113. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  114. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  115. package/src/core/filterSurface.ts +99 -0
  116. package/src/{tmdatagrid/core → core}/labels.ts +66 -8
  117. package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
  118. package/src/core/pageReset.ts +120 -0
  119. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  120. package/src/core/resizePreview.ts +141 -0
  121. package/src/core/summary.ts +59 -0
  122. package/src/core/useSettledTableState.ts +36 -0
  123. package/src/{tmdatagrid/index.ts → index.ts} +75 -12
  124. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
  125. package/src/useTMDataGridExport.ts +78 -0
  126. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
  127. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  128. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  129. package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
  130. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
  131. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. package/src/tmdatagrid/core/editEngine.ts +0 -1006
  134. package/src/tmdatagrid/core/summary.ts +0 -35
  135. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  136. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  141. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/sizes.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,162 +0,0 @@
1
- import { Button, Group } from "@mantine/core";
2
- import { useSelector } from "@tanstack/react-store";
3
- import type { ReactNode } from "react";
4
- import { useTMDataGridContext } from "../TMDataGridContext";
5
-
6
- /**
7
- * How many rows are waiting to be saved: dirty edits, entry rows (an add is
8
- * pending whether or not it was touched), and deletion marks.
9
- */
10
- function usePendingCount(): number {
11
- const { edit } = useTMDataGridContext();
12
- return useSelector(
13
- edit.store,
14
- (state) =>
15
- state.openRowIds.filter(
16
- (rowId) =>
17
- state.newRows.some((newRow) => newRow.tempId === rowId) ||
18
- (state.rows[rowId]?.dirtyFields.length ?? 0) > 0,
19
- ).length + state.deletedRowIds.length,
20
- );
21
- }
22
-
23
- /*
24
- * Module scope, like the pagination controls and for the same reason: a
25
- * component defined inside the render body is a new type every render, so
26
- * React would remount the buttons underneath an interaction.
27
- */
28
-
29
- function EditSaveButton() {
30
- const { edit, labels } = useTMDataGridContext();
31
- const pendingCount = usePendingCount();
32
- const isSubmitting = useSelector(edit.store, (state) =>
33
- state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
34
- );
35
-
36
- return (
37
- <Button
38
- size="compact-sm"
39
- disabled={pendingCount === 0}
40
- loading={isSubmitting}
41
- data-dg-part="save-all"
42
- onClick={() => void edit.submitAll()}
43
- >
44
- {labels.saveAllEdits(pendingCount)}
45
- </Button>
46
- );
47
- }
48
-
49
- function EditDiscardButton() {
50
- const { edit, labels } = useTMDataGridContext();
51
- const pendingCount = usePendingCount();
52
-
53
- return (
54
- <Button
55
- variant="subtle"
56
- color="gray"
57
- size="compact-sm"
58
- disabled={pendingCount === 0}
59
- data-dg-part="discard-all"
60
- onClick={() => edit.cancelAll()}
61
- >
62
- {labels.discardAllEdits}
63
- </Button>
64
- );
65
- }
66
-
67
- const EDIT_ACTIONS_CONTROLS: TMDataGridEditActionsControls = {
68
- Save: EditSaveButton,
69
- Discard: EditDiscardButton,
70
- };
71
-
72
- /** What the edit chrome is showing. */
73
- export type TMDataGridEditActionsState = {
74
- /** Rows with unsaved work: dirty edits, entry rows, and deletion marks. */
75
- pendingCount: number;
76
- /** Whether a submit is in flight. */
77
- isSubmitting: boolean;
78
- };
79
-
80
- /** What the edit chrome can do. */
81
- export type TMDataGridEditActionsActions = {
82
- /** Commits every open row. Resolves `false` when a row stayed open. */
83
- save: () => Promise<boolean>;
84
- /** Drops every draft. */
85
- discard: () => void;
86
- };
87
-
88
- /** The pre-bound pieces of the built-in edit chrome. */
89
- export type TMDataGridEditActionsControls = {
90
- /** Save, with the pending count, disabled while nothing is pending. */
91
- Save: () => ReactNode;
92
- /** Discard, disabled while nothing is pending. */
93
- Discard: () => ReactNode;
94
- };
95
-
96
- /** What {@link TMDataGridEditActionsProps.renderActions} is handed. */
97
- export type TMDataGridEditActionsSlotArgs = {
98
- state: TMDataGridEditActionsState;
99
- actions: TMDataGridEditActionsActions;
100
- Controls: TMDataGridEditActionsControls;
101
- };
102
-
103
- export type TMDataGridEditActionsProps = {
104
- /**
105
- * Replaces the built-in Save/Discard pair, and is handed the pieces of it.
106
- *
107
- * ```tsx
108
- * <TMDataGrid.EditActions
109
- * renderActions={({ state, Controls }) => (
110
- * <Group>
111
- * {state.pendingCount > 0 && <Badge>{state.pendingCount}</Badge>}
112
- * <Controls.Save />
113
- * <Controls.Discard />
114
- * </Group>
115
- * )}
116
- * />
117
- * ```
118
- */
119
- renderActions?: (args: TMDataGridEditActionsSlotArgs) => ReactNode;
120
- };
121
-
122
- /**
123
- * Draft mode's toolbar chrome: Save with the dirty-row count, and Discard.
124
- * Both read the edit store, so they grey out while nothing is dirty and the
125
- * Save spins while a submit is in flight. Works under any `editing.mode` - a
126
- * cellConfirm grid accumulating drafts can offer the same pair - and renders
127
- * nothing while editing is off.
128
- *
129
- * ```tsx
130
- * <TMDataGrid.Toolbar>
131
- * <TMDataGrid.SummaryCount />
132
- * <TMDataGrid.Spacer />
133
- * <TMDataGrid.EditActions />
134
- * </TMDataGrid.Toolbar>
135
- * ```
136
- */
137
- export function TMDataGridEditActions({
138
- renderActions,
139
- }: TMDataGridEditActionsProps = {}) {
140
- const { edit, features } = useTMDataGridContext();
141
- const pendingCount = usePendingCount();
142
- const isSubmitting = useSelector(edit.store, (state) =>
143
- state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
144
- );
145
-
146
- if (!features.editing) return null;
147
-
148
- if (renderActions) {
149
- return renderActions({
150
- state: { pendingCount, isSubmitting },
151
- actions: { save: () => edit.submitAll(), discard: () => edit.cancelAll() },
152
- Controls: EDIT_ACTIONS_CONTROLS,
153
- });
154
- }
155
-
156
- return (
157
- <Group gap="xs" wrap="nowrap">
158
- <EditSaveButton />
159
- <EditDiscardButton />
160
- </Group>
161
- );
162
- }
@@ -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,12 +0,0 @@
1
- .toolbar {
2
- display: flex;
3
- align-items: center;
4
- gap: var(--mantine-spacing-xs);
5
- padding: 6px var(--mantine-spacing-xs);
6
- min-height: 42px;
7
- flex-shrink: 0;
8
- }
9
-
10
- .toolbarSpacer {
11
- flex: 1;
12
- }