@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.
- package/README.md +5 -212
- package/dist/index.d.ts +1664 -632
- package/dist/index.js +5226 -3223
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/anatomy.md +102 -0
- package/docs/cell-selection.md +154 -0
- package/docs/column-layout.md +204 -0
- package/docs/columns.md +262 -0
- package/docs/components.md +304 -0
- package/docs/editing.md +603 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +326 -0
- package/docs/filtering.md +358 -0
- package/docs/getting-started.md +123 -0
- package/docs/grouping.md +165 -0
- package/docs/loading-and-empty.md +92 -0
- package/docs/localization.md +79 -0
- package/docs/menu.md +143 -0
- package/docs/pagination.md +144 -0
- package/docs/persistence.md +111 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +175 -0
- package/docs/quick-search.md +83 -0
- package/docs/row-details.md +113 -0
- package/docs/row-interaction.md +148 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +134 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +111 -0
- package/docs/server-query.md +246 -0
- package/docs/server-side.md +206 -0
- package/docs/sorting.md +101 -0
- package/docs/styling.md +126 -0
- package/docs/summary-row.md +76 -0
- package/docs/testing.md +309 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +21 -45
- package/skills/appearance/SKILL.md +70 -17
- package/skills/cell-selection/SKILL.md +70 -76
- package/skills/columns/SKILL.md +131 -32
- package/skills/data/SKILL.md +100 -23
- package/skills/editing/SKILL.md +217 -96
- package/skills/editing/references/common-mistakes.md +111 -24
- package/skills/editing/references/editing-api.md +63 -39
- package/skills/editing/references/editors-and-validation.md +77 -19
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +18 -16
- package/skills/grouping/SKILL.md +32 -15
- package/skills/options/SKILL.md +39 -9
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +10 -7
- package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
- package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
- package/src/components/TMDataGridDraftActions.tsx +307 -0
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
- package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
- package/src/components/TMDataGridExportPicker.module.css +77 -0
- package/src/components/TMDataGridExportPicker.tsx +234 -0
- package/src/components/TMDataGridFilterPanel.module.css +54 -0
- package/src/components/TMDataGridFilterPanel.tsx +348 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +354 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
- package/src/components/TMDataGridToolbar.module.css +21 -0
- package/src/components/TMDataGridToolbar.tsx +181 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
- package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
- package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
- package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/components/filters/controlLayout.ts +32 -0
- package/src/components/filters/filterControlFor.ts +65 -0
- package/src/{tmdatagrid/components → components}/icons.ts +1 -0
- package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
- package/src/components/useHideableColumns.ts +52 -0
- package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
- package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
- package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledState.ts +179 -0
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/core/editEngine.ts +2476 -0
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- package/src/core/export.ts +843 -0
- package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
- package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
- package/src/core/filterSurface.ts +99 -0
- package/src/{tmdatagrid/core → core}/labels.ts +66 -8
- package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
- package/src/core/pageReset.ts +120 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/core/resizePreview.ts +141 -0
- package/src/core/summary.ts +59 -0
- package/src/core/useSettledTableState.ts +36 -0
- package/src/{tmdatagrid/index.ts → index.ts} +75 -12
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- package/src/tmdatagrid/core/editEngine.ts +0 -1006
- package/src/tmdatagrid/core/summary.ts +0 -35
- /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
- /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
- /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
- /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
- /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
- /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /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
|
-
}
|