@smallwebco/tinypivot-react 1.0.48 → 1.0.50
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 +0 -1
- package/dist/index.cjs +1638 -1509
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -77
- package/dist/index.d.ts +77 -77
- package/dist/index.js +1638 -1509
- package/dist/index.js.map +1 -1
- package/package.json +39 -39
package/dist/index.js
CHANGED
|
@@ -1,1296 +1,701 @@
|
|
|
1
|
-
// src/components/
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (cellValue === null || cellValue === void 0 || cellValue === "") {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
const num = typeof cellValue === "number" ? cellValue : Number.parseFloat(String(cellValue));
|
|
22
|
-
if (Number.isNaN(num)) return false;
|
|
23
|
-
const { min, max } = filterValue;
|
|
24
|
-
if (min !== null && num < min) return false;
|
|
25
|
-
if (max !== null && num > max) return false;
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
if (Array.isArray(filterValue) && filterValue.length > 0) {
|
|
29
|
-
const cellValue = row.getValue(columnId);
|
|
30
|
-
const cellString = cellValue === null || cellValue === void 0 || cellValue === "" ? "(blank)" : String(cellValue);
|
|
31
|
-
return filterValue.includes(cellString);
|
|
32
|
-
}
|
|
33
|
-
return true;
|
|
34
|
-
};
|
|
35
|
-
function useExcelGrid(options) {
|
|
36
|
-
const { data, enableSorting = true, enableFiltering = true } = options;
|
|
37
|
-
const [sorting, setSorting] = useState([]);
|
|
38
|
-
const [columnFilters, setColumnFilters] = useState([]);
|
|
39
|
-
const [columnVisibility, setColumnVisibility] = useState({});
|
|
40
|
-
const [globalFilter, setGlobalFilter] = useState("");
|
|
41
|
-
const [columnStatsCache, setColumnStatsCache] = useState({});
|
|
42
|
-
const dataSignature = useMemo(
|
|
43
|
-
() => `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
44
|
-
[data]
|
|
45
|
-
);
|
|
46
|
-
const columnKeys = useMemo(() => {
|
|
47
|
-
if (data.length === 0) return [];
|
|
48
|
-
return Object.keys(data[0]);
|
|
49
|
-
}, [data]);
|
|
50
|
-
const getColumnStats = useCallback(
|
|
51
|
-
(columnKey) => {
|
|
52
|
-
const cacheKey = `${columnKey}-${dataSignature}`;
|
|
53
|
-
if (!columnStatsCache[cacheKey]) {
|
|
54
|
-
const stats = getColumnUniqueValues(data, columnKey);
|
|
55
|
-
setColumnStatsCache((prev) => ({ ...prev, [cacheKey]: stats }));
|
|
56
|
-
return stats;
|
|
57
|
-
}
|
|
58
|
-
return columnStatsCache[cacheKey];
|
|
59
|
-
},
|
|
60
|
-
[data, columnStatsCache, dataSignature]
|
|
61
|
-
);
|
|
62
|
-
const clearStatsCache = useCallback(() => {
|
|
63
|
-
setColumnStatsCache({});
|
|
64
|
-
}, []);
|
|
1
|
+
// src/components/CalculatedFieldModal.tsx
|
|
2
|
+
import { validateSimpleFormula } from "@smallwebco/tinypivot-core";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
function CalculatedFieldModal({
|
|
7
|
+
show,
|
|
8
|
+
availableFields,
|
|
9
|
+
existingField,
|
|
10
|
+
onClose,
|
|
11
|
+
onSave
|
|
12
|
+
}) {
|
|
13
|
+
const [name, setName] = useState("");
|
|
14
|
+
const [formula, setFormula] = useState("");
|
|
15
|
+
const [formatAs, setFormatAs] = useState("number");
|
|
16
|
+
const [decimals, setDecimals] = useState(2);
|
|
17
|
+
const [error, setError] = useState(null);
|
|
65
18
|
useEffect(() => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
meta: {
|
|
78
|
-
type: stats.type,
|
|
79
|
-
uniqueCount: stats.uniqueValues.length
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
});
|
|
83
|
-
}, [columnKeys, getColumnStats]);
|
|
84
|
-
const table = useReactTable({
|
|
85
|
-
data,
|
|
86
|
-
columns: columnDefs,
|
|
87
|
-
state: {
|
|
88
|
-
sorting,
|
|
89
|
-
columnFilters,
|
|
90
|
-
columnVisibility,
|
|
91
|
-
globalFilter
|
|
92
|
-
},
|
|
93
|
-
onSortingChange: setSorting,
|
|
94
|
-
onColumnFiltersChange: setColumnFilters,
|
|
95
|
-
onColumnVisibilityChange: setColumnVisibility,
|
|
96
|
-
onGlobalFilterChange: setGlobalFilter,
|
|
97
|
-
getCoreRowModel: getCoreRowModel(),
|
|
98
|
-
getSortedRowModel: enableSorting ? getSortedRowModel() : void 0,
|
|
99
|
-
getFilteredRowModel: enableFiltering ? getFilteredRowModel() : void 0,
|
|
100
|
-
filterFns: {
|
|
101
|
-
multiSelect: multiSelectFilter
|
|
102
|
-
},
|
|
103
|
-
enableSorting,
|
|
104
|
-
enableFilters: enableFiltering
|
|
105
|
-
});
|
|
106
|
-
const filteredRowCount = table.getFilteredRowModel().rows.length;
|
|
107
|
-
const totalRowCount = data.length;
|
|
108
|
-
const activeFilters = useMemo(() => {
|
|
109
|
-
return columnFilters.map((f) => {
|
|
110
|
-
const filterValue = f.value;
|
|
111
|
-
if (filterValue && isNumericRange(filterValue)) {
|
|
112
|
-
return {
|
|
113
|
-
column: f.id,
|
|
114
|
-
type: "range",
|
|
115
|
-
range: filterValue,
|
|
116
|
-
values: []
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
return {
|
|
120
|
-
column: f.id,
|
|
121
|
-
type: "values",
|
|
122
|
-
values: Array.isArray(filterValue) ? filterValue : [],
|
|
123
|
-
range: null
|
|
124
|
-
};
|
|
125
|
-
});
|
|
126
|
-
}, [columnFilters]);
|
|
127
|
-
const hasActiveFilter = useCallback(
|
|
128
|
-
(columnId) => {
|
|
129
|
-
const column = table.getColumn(columnId);
|
|
130
|
-
if (!column) return false;
|
|
131
|
-
const filterValue = column.getFilterValue();
|
|
132
|
-
if (!filterValue) return false;
|
|
133
|
-
if (isNumericRange(filterValue)) {
|
|
134
|
-
return filterValue.min !== null || filterValue.max !== null;
|
|
135
|
-
}
|
|
136
|
-
return Array.isArray(filterValue) && filterValue.length > 0;
|
|
137
|
-
},
|
|
138
|
-
[table]
|
|
139
|
-
);
|
|
140
|
-
const setColumnFilter = useCallback(
|
|
141
|
-
(columnId, values) => {
|
|
142
|
-
const column = table.getColumn(columnId);
|
|
143
|
-
if (column) {
|
|
144
|
-
column.setFilterValue(values.length === 0 ? void 0 : values);
|
|
145
|
-
}
|
|
146
|
-
},
|
|
147
|
-
[table]
|
|
148
|
-
);
|
|
149
|
-
const setNumericRangeFilter = useCallback(
|
|
150
|
-
(columnId, range) => {
|
|
151
|
-
const column = table.getColumn(columnId);
|
|
152
|
-
if (column) {
|
|
153
|
-
if (!range || range.min === null && range.max === null) {
|
|
154
|
-
column.setFilterValue(void 0);
|
|
155
|
-
} else {
|
|
156
|
-
column.setFilterValue(range);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
[table]
|
|
161
|
-
);
|
|
162
|
-
const getNumericRangeFilter = useCallback(
|
|
163
|
-
(columnId) => {
|
|
164
|
-
const column = table.getColumn(columnId);
|
|
165
|
-
if (!column) return null;
|
|
166
|
-
const filterValue = column.getFilterValue();
|
|
167
|
-
if (filterValue && isNumericRange(filterValue)) {
|
|
168
|
-
return filterValue;
|
|
19
|
+
if (show) {
|
|
20
|
+
if (existingField) {
|
|
21
|
+
setName(existingField.name);
|
|
22
|
+
setFormula(existingField.formula);
|
|
23
|
+
setFormatAs(existingField.formatAs || "number");
|
|
24
|
+
setDecimals(existingField.decimals ?? 2);
|
|
25
|
+
} else {
|
|
26
|
+
setName("");
|
|
27
|
+
setFormula("");
|
|
28
|
+
setFormatAs("number");
|
|
29
|
+
setDecimals(2);
|
|
169
30
|
}
|
|
31
|
+
setError(null);
|
|
32
|
+
}
|
|
33
|
+
}, [show, existingField]);
|
|
34
|
+
const validationError = useMemo(() => {
|
|
35
|
+
if (!formula.trim())
|
|
170
36
|
return null;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
setColumnFilters([]);
|
|
178
|
-
}, [table]);
|
|
179
|
-
const getColumnFilterValues = useCallback(
|
|
180
|
-
(columnId) => {
|
|
181
|
-
const column = table.getColumn(columnId);
|
|
182
|
-
if (!column) return [];
|
|
183
|
-
const filterValue = column.getFilterValue();
|
|
184
|
-
return Array.isArray(filterValue) ? filterValue : [];
|
|
185
|
-
},
|
|
186
|
-
[table]
|
|
187
|
-
);
|
|
188
|
-
const toggleSort = useCallback((columnId) => {
|
|
189
|
-
setSorting((prev) => {
|
|
190
|
-
const current = prev.find((s) => s.id === columnId);
|
|
191
|
-
if (!current) {
|
|
192
|
-
return [{ id: columnId, desc: false }];
|
|
193
|
-
} else if (!current.desc) {
|
|
194
|
-
return [{ id: columnId, desc: true }];
|
|
195
|
-
} else {
|
|
196
|
-
return [];
|
|
37
|
+
return validateSimpleFormula(formula, availableFields);
|
|
38
|
+
}, [formula, availableFields]);
|
|
39
|
+
const insertField = useCallback((field) => {
|
|
40
|
+
setFormula((prev) => {
|
|
41
|
+
if (prev.trim() && !prev.endsWith(" ")) {
|
|
42
|
+
return `${prev} ${field}`;
|
|
197
43
|
}
|
|
44
|
+
return prev + field;
|
|
198
45
|
});
|
|
199
46
|
}, []);
|
|
200
|
-
const
|
|
201
|
-
(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
sorting,
|
|
213
|
-
columnFilters,
|
|
214
|
-
columnVisibility,
|
|
215
|
-
globalFilter,
|
|
216
|
-
columnKeys,
|
|
217
|
-
setSorting,
|
|
218
|
-
setColumnFilters,
|
|
219
|
-
setGlobalFilter,
|
|
220
|
-
// Computed
|
|
221
|
-
filteredRowCount,
|
|
222
|
-
totalRowCount,
|
|
223
|
-
activeFilters,
|
|
224
|
-
// Methods
|
|
225
|
-
getColumnStats,
|
|
226
|
-
clearStatsCache,
|
|
227
|
-
hasActiveFilter,
|
|
228
|
-
setColumnFilter,
|
|
229
|
-
getColumnFilterValues,
|
|
230
|
-
clearAllFilters,
|
|
231
|
-
toggleSort,
|
|
232
|
-
getSortDirection,
|
|
233
|
-
// Numeric range filters
|
|
234
|
-
setNumericRangeFilter,
|
|
235
|
-
getNumericRangeFilter
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// src/hooks/usePivotTable.ts
|
|
240
|
-
import { useState as useState3, useMemo as useMemo3, useEffect as useEffect2, useCallback as useCallback3 } from "react";
|
|
241
|
-
import {
|
|
242
|
-
computeAvailableFields,
|
|
243
|
-
getUnassignedFields,
|
|
244
|
-
isPivotConfigured,
|
|
245
|
-
computePivotResult,
|
|
246
|
-
generateStorageKey,
|
|
247
|
-
savePivotConfig,
|
|
248
|
-
loadPivotConfig,
|
|
249
|
-
isConfigValidForFields,
|
|
250
|
-
getAggregationLabel,
|
|
251
|
-
loadCalculatedFields,
|
|
252
|
-
saveCalculatedFields
|
|
253
|
-
} from "@smallwebco/tinypivot-core";
|
|
254
|
-
|
|
255
|
-
// src/hooks/useLicense.ts
|
|
256
|
-
import { useState as useState2, useCallback as useCallback2, useMemo as useMemo2 } from "react";
|
|
257
|
-
import {
|
|
258
|
-
validateLicenseKey,
|
|
259
|
-
configureLicenseSecret as coreConfigureLicenseSecret,
|
|
260
|
-
getDemoLicenseInfo,
|
|
261
|
-
getFreeLicenseInfo,
|
|
262
|
-
canUsePivot as coreCanUsePivot,
|
|
263
|
-
isPro as coreIsPro,
|
|
264
|
-
shouldShowWatermark as coreShouldShowWatermark,
|
|
265
|
-
logProRequired
|
|
266
|
-
} from "@smallwebco/tinypivot-core";
|
|
267
|
-
var globalLicenseInfo = getFreeLicenseInfo();
|
|
268
|
-
var globalDemoMode = false;
|
|
269
|
-
var listeners = /* @__PURE__ */ new Set();
|
|
270
|
-
function notifyListeners() {
|
|
271
|
-
listeners.forEach((listener) => listener());
|
|
272
|
-
}
|
|
273
|
-
async function setLicenseKey(key) {
|
|
274
|
-
globalLicenseInfo = await validateLicenseKey(key);
|
|
275
|
-
if (!globalLicenseInfo.isValid) {
|
|
276
|
-
console.warn("[TinyPivot] Invalid or expired license key. Running in free mode.");
|
|
277
|
-
} else if (globalLicenseInfo.type !== "free") {
|
|
278
|
-
console.info(`[TinyPivot] Pro license activated (${globalLicenseInfo.type})`);
|
|
279
|
-
}
|
|
280
|
-
notifyListeners();
|
|
281
|
-
}
|
|
282
|
-
async function enableDemoMode(secret) {
|
|
283
|
-
const demoLicense = await getDemoLicenseInfo(secret);
|
|
284
|
-
if (!demoLicense) {
|
|
285
|
-
console.warn("[TinyPivot] Demo mode activation failed - invalid secret");
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
globalDemoMode = true;
|
|
289
|
-
globalLicenseInfo = demoLicense;
|
|
290
|
-
console.info("[TinyPivot] Demo mode enabled - all Pro features unlocked for evaluation");
|
|
291
|
-
notifyListeners();
|
|
292
|
-
return true;
|
|
293
|
-
}
|
|
294
|
-
function configureLicenseSecret(secret) {
|
|
295
|
-
coreConfigureLicenseSecret(secret);
|
|
296
|
-
}
|
|
297
|
-
function useLicense() {
|
|
298
|
-
const [, forceUpdate] = useState2({});
|
|
299
|
-
useState2(() => {
|
|
300
|
-
const update = () => forceUpdate({});
|
|
301
|
-
listeners.add(update);
|
|
302
|
-
return () => listeners.delete(update);
|
|
303
|
-
});
|
|
304
|
-
const isDemo = globalDemoMode;
|
|
305
|
-
const licenseInfo = globalLicenseInfo;
|
|
306
|
-
const isPro = useMemo2(
|
|
307
|
-
() => globalDemoMode || coreIsPro(licenseInfo),
|
|
308
|
-
[licenseInfo]
|
|
309
|
-
);
|
|
310
|
-
const canUsePivot = useMemo2(
|
|
311
|
-
() => globalDemoMode || coreCanUsePivot(licenseInfo),
|
|
312
|
-
[licenseInfo]
|
|
313
|
-
);
|
|
314
|
-
const canUseAdvancedAggregations = useMemo2(
|
|
315
|
-
() => globalDemoMode || licenseInfo.features.advancedAggregations,
|
|
316
|
-
[licenseInfo]
|
|
317
|
-
);
|
|
318
|
-
const canUsePercentageMode = useMemo2(
|
|
319
|
-
() => globalDemoMode || licenseInfo.features.percentageMode,
|
|
320
|
-
[licenseInfo]
|
|
321
|
-
);
|
|
322
|
-
const showWatermark = useMemo2(
|
|
323
|
-
() => coreShouldShowWatermark(licenseInfo, globalDemoMode),
|
|
324
|
-
[licenseInfo]
|
|
325
|
-
);
|
|
326
|
-
const requirePro = useCallback2((feature) => {
|
|
327
|
-
if (!isPro) {
|
|
328
|
-
logProRequired(feature);
|
|
329
|
-
return false;
|
|
47
|
+
const insertOperator = useCallback((op) => {
|
|
48
|
+
setFormula((prev) => {
|
|
49
|
+
if (prev.trim() && !prev.endsWith(" ")) {
|
|
50
|
+
return `${prev} ${op} `;
|
|
51
|
+
}
|
|
52
|
+
return `${prev + op} `;
|
|
53
|
+
});
|
|
54
|
+
}, []);
|
|
55
|
+
const handleSave = useCallback(() => {
|
|
56
|
+
if (!name.trim()) {
|
|
57
|
+
setError("Name is required");
|
|
58
|
+
return;
|
|
330
59
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
60
|
+
const validationResult = validateSimpleFormula(formula, availableFields);
|
|
61
|
+
if (validationResult) {
|
|
62
|
+
setError(validationResult);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const field = {
|
|
66
|
+
id: existingField?.id || `calc_${Date.now()}`,
|
|
67
|
+
name: name.trim(),
|
|
68
|
+
formula: formula.trim(),
|
|
69
|
+
formatAs,
|
|
70
|
+
decimals
|
|
71
|
+
};
|
|
72
|
+
onSave(field);
|
|
73
|
+
onClose();
|
|
74
|
+
}, [name, formula, formatAs, decimals, existingField, availableFields, onSave, onClose]);
|
|
75
|
+
const handleOverlayClick = useCallback((e) => {
|
|
76
|
+
if (e.target === e.currentTarget) {
|
|
77
|
+
onClose();
|
|
78
|
+
}
|
|
79
|
+
}, [onClose]);
|
|
80
|
+
if (!show)
|
|
81
|
+
return null;
|
|
82
|
+
const modalContent = /* @__PURE__ */ jsx("div", { className: "vpg-modal-overlay", onClick: handleOverlayClick, children: /* @__PURE__ */ jsxs("div", { className: "vpg-modal", children: [
|
|
83
|
+
/* @__PURE__ */ jsxs("div", { className: "vpg-modal-header", children: [
|
|
84
|
+
/* @__PURE__ */ jsxs("h3", { children: [
|
|
85
|
+
existingField ? "Edit" : "Create",
|
|
86
|
+
" ",
|
|
87
|
+
"Calculated Field"
|
|
88
|
+
] }),
|
|
89
|
+
/* @__PURE__ */ jsx("button", { className: "vpg-modal-close", onClick: onClose, children: "\xD7" })
|
|
90
|
+
] }),
|
|
91
|
+
/* @__PURE__ */ jsxs("div", { className: "vpg-modal-body", children: [
|
|
92
|
+
/* @__PURE__ */ jsxs("div", { className: "vpg-form-group", children: [
|
|
93
|
+
/* @__PURE__ */ jsx("label", { className: "vpg-label", children: "Name" }),
|
|
94
|
+
/* @__PURE__ */ jsx(
|
|
95
|
+
"input",
|
|
96
|
+
{
|
|
97
|
+
type: "text",
|
|
98
|
+
className: "vpg-input",
|
|
99
|
+
placeholder: "e.g., Profit Margin %",
|
|
100
|
+
value: name,
|
|
101
|
+
onChange: (e) => setName(e.target.value)
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
] }),
|
|
105
|
+
/* @__PURE__ */ jsxs("div", { className: "vpg-form-group", children: [
|
|
106
|
+
/* @__PURE__ */ jsx("label", { className: "vpg-label", children: "Formula" }),
|
|
107
|
+
/* @__PURE__ */ jsx(
|
|
108
|
+
"textarea",
|
|
109
|
+
{
|
|
110
|
+
className: "vpg-textarea",
|
|
111
|
+
placeholder: "e.g., revenue / units",
|
|
112
|
+
rows: 2,
|
|
113
|
+
value: formula,
|
|
114
|
+
onChange: (e) => setFormula(e.target.value)
|
|
115
|
+
}
|
|
116
|
+
),
|
|
117
|
+
/* @__PURE__ */ jsx("div", { className: "vpg-formula-hint", children: "Use field names with math operators: + - * / ( )" }),
|
|
118
|
+
validationError && /* @__PURE__ */ jsx("div", { className: "vpg-error", children: validationError })
|
|
119
|
+
] }),
|
|
120
|
+
/* @__PURE__ */ jsxs("div", { className: "vpg-form-group", children: [
|
|
121
|
+
/* @__PURE__ */ jsx("label", { className: "vpg-label-small", children: "Operators" }),
|
|
122
|
+
/* @__PURE__ */ jsxs("div", { className: "vpg-button-group", children: [
|
|
123
|
+
/* @__PURE__ */ jsx("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("+"), children: "+" }),
|
|
124
|
+
/* @__PURE__ */ jsx("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("-"), children: "\u2212" }),
|
|
125
|
+
/* @__PURE__ */ jsx("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("*"), children: "\xD7" }),
|
|
126
|
+
/* @__PURE__ */ jsx("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("/"), children: "\xF7" }),
|
|
127
|
+
/* @__PURE__ */ jsx("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("("), children: "(" }),
|
|
128
|
+
/* @__PURE__ */ jsx("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator(")"), children: ")" })
|
|
129
|
+
] })
|
|
130
|
+
] }),
|
|
131
|
+
/* @__PURE__ */ jsxs("div", { className: "vpg-form-group", children: [
|
|
132
|
+
/* @__PURE__ */ jsx("label", { className: "vpg-label-small", children: "Insert Field" }),
|
|
133
|
+
availableFields.length > 0 ? /* @__PURE__ */ jsx("div", { className: "vpg-button-group vpg-field-buttons", children: availableFields.map((field) => /* @__PURE__ */ jsx(
|
|
134
|
+
"button",
|
|
135
|
+
{
|
|
136
|
+
className: "vpg-insert-btn vpg-field-btn",
|
|
137
|
+
onClick: () => insertField(field),
|
|
138
|
+
children: field
|
|
139
|
+
},
|
|
140
|
+
field
|
|
141
|
+
)) }) : /* @__PURE__ */ jsx("div", { className: "vpg-no-fields", children: "No numeric fields available" })
|
|
142
|
+
] }),
|
|
143
|
+
/* @__PURE__ */ jsxs("div", { className: "vpg-form-row", children: [
|
|
144
|
+
/* @__PURE__ */ jsxs("div", { className: "vpg-form-group vpg-form-group-half", children: [
|
|
145
|
+
/* @__PURE__ */ jsx("label", { className: "vpg-label", children: "Format As" }),
|
|
146
|
+
/* @__PURE__ */ jsxs(
|
|
147
|
+
"select",
|
|
148
|
+
{
|
|
149
|
+
className: "vpg-select",
|
|
150
|
+
value: formatAs,
|
|
151
|
+
onChange: (e) => setFormatAs(e.target.value),
|
|
152
|
+
children: [
|
|
153
|
+
/* @__PURE__ */ jsx("option", { value: "number", children: "Number" }),
|
|
154
|
+
/* @__PURE__ */ jsx("option", { value: "percent", children: "Percentage" }),
|
|
155
|
+
/* @__PURE__ */ jsx("option", { value: "currency", children: "Currency ($)" })
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
] }),
|
|
160
|
+
/* @__PURE__ */ jsxs("div", { className: "vpg-form-group vpg-form-group-half", children: [
|
|
161
|
+
/* @__PURE__ */ jsx("label", { className: "vpg-label", children: "Decimals" }),
|
|
162
|
+
/* @__PURE__ */ jsx(
|
|
163
|
+
"input",
|
|
164
|
+
{
|
|
165
|
+
type: "number",
|
|
166
|
+
className: "vpg-input",
|
|
167
|
+
min: 0,
|
|
168
|
+
max: 6,
|
|
169
|
+
value: decimals,
|
|
170
|
+
onChange: (e) => setDecimals(Number(e.target.value))
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
] })
|
|
174
|
+
] }),
|
|
175
|
+
error && /* @__PURE__ */ jsx("div", { className: "vpg-error vpg-error-box", children: error })
|
|
176
|
+
] }),
|
|
177
|
+
/* @__PURE__ */ jsxs("div", { className: "vpg-modal-footer", children: [
|
|
178
|
+
/* @__PURE__ */ jsx("button", { className: "vpg-btn vpg-btn-secondary", onClick: onClose, children: "Cancel" }),
|
|
179
|
+
/* @__PURE__ */ jsxs("button", { className: "vpg-btn vpg-btn-primary", onClick: handleSave, children: [
|
|
180
|
+
existingField ? "Update" : "Add",
|
|
181
|
+
" ",
|
|
182
|
+
"Field"
|
|
183
|
+
] })
|
|
184
|
+
] })
|
|
185
|
+
] }) });
|
|
186
|
+
if (typeof document === "undefined")
|
|
187
|
+
return null;
|
|
188
|
+
return createPortal(modalContent, document.body);
|
|
343
189
|
}
|
|
344
190
|
|
|
345
|
-
// src/
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
191
|
+
// src/components/ColumnFilter.tsx
|
|
192
|
+
import { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo3, useRef, useState as useState3 } from "react";
|
|
193
|
+
|
|
194
|
+
// src/components/NumericRangeFilter.tsx
|
|
195
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react";
|
|
196
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
197
|
+
function NumericRangeFilter({
|
|
198
|
+
dataMin,
|
|
199
|
+
dataMax,
|
|
200
|
+
currentRange,
|
|
201
|
+
onChange
|
|
202
|
+
}) {
|
|
203
|
+
const [localMin, setLocalMin] = useState2(currentRange?.min ?? null);
|
|
204
|
+
const [localMax, setLocalMax] = useState2(currentRange?.max ?? null);
|
|
205
|
+
const step = useMemo2(() => {
|
|
206
|
+
const range = dataMax - dataMin;
|
|
207
|
+
if (range === 0)
|
|
208
|
+
return 1;
|
|
209
|
+
if (range <= 1)
|
|
210
|
+
return 0.01;
|
|
211
|
+
if (range <= 10)
|
|
212
|
+
return 0.1;
|
|
213
|
+
if (range <= 100)
|
|
214
|
+
return 1;
|
|
215
|
+
if (range <= 1e3)
|
|
216
|
+
return 10;
|
|
217
|
+
return 10 ** (Math.floor(Math.log10(range)) - 2);
|
|
218
|
+
}, [dataMin, dataMax]);
|
|
219
|
+
const formatValue = useCallback2((val) => {
|
|
220
|
+
if (val === null)
|
|
221
|
+
return "";
|
|
222
|
+
if (Number.isInteger(val))
|
|
223
|
+
return val.toLocaleString();
|
|
224
|
+
return val.toLocaleString(void 0, { maximumFractionDigits: 2 });
|
|
225
|
+
}, []);
|
|
226
|
+
const isFilterActive = localMin !== null || localMax !== null;
|
|
227
|
+
const minPercent = useMemo2(() => {
|
|
228
|
+
if (localMin === null || dataMax === dataMin)
|
|
229
|
+
return 0;
|
|
230
|
+
return (localMin - dataMin) / (dataMax - dataMin) * 100;
|
|
231
|
+
}, [localMin, dataMin, dataMax]);
|
|
232
|
+
const maxPercent = useMemo2(() => {
|
|
233
|
+
if (localMax === null || dataMax === dataMin)
|
|
234
|
+
return 100;
|
|
235
|
+
return (localMax - dataMin) / (dataMax - dataMin) * 100;
|
|
236
|
+
}, [localMax, dataMin, dataMax]);
|
|
237
|
+
const handleMinSlider = useCallback2((event) => {
|
|
238
|
+
const value = Number.parseFloat(event.target.value);
|
|
239
|
+
setLocalMin(() => {
|
|
240
|
+
if (localMax !== null && value > localMax) {
|
|
241
|
+
return localMax;
|
|
242
|
+
}
|
|
243
|
+
return value;
|
|
380
244
|
});
|
|
381
|
-
}, [
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
setCurrentStorageKey(storageKey);
|
|
388
|
-
const savedConfig = loadPivotConfig(storageKey);
|
|
389
|
-
if (savedConfig && isConfigValidForFields(savedConfig, newKeys)) {
|
|
390
|
-
setRowFieldsState(savedConfig.rowFields);
|
|
391
|
-
setColumnFieldsState(savedConfig.columnFields);
|
|
392
|
-
setValueFields(savedConfig.valueFields);
|
|
393
|
-
setShowRowTotals(savedConfig.showRowTotals);
|
|
394
|
-
setShowColumnTotals(savedConfig.showColumnTotals);
|
|
395
|
-
if (savedConfig.calculatedFields) {
|
|
396
|
-
setCalculatedFields(savedConfig.calculatedFields);
|
|
397
|
-
}
|
|
398
|
-
} else {
|
|
399
|
-
const currentConfig = {
|
|
400
|
-
rowFields,
|
|
401
|
-
columnFields,
|
|
402
|
-
valueFields,
|
|
403
|
-
showRowTotals,
|
|
404
|
-
showColumnTotals
|
|
405
|
-
};
|
|
406
|
-
if (!isConfigValidForFields(currentConfig, newKeys)) {
|
|
407
|
-
setRowFieldsState([]);
|
|
408
|
-
setColumnFieldsState([]);
|
|
409
|
-
setValueFields([]);
|
|
410
|
-
}
|
|
245
|
+
}, [localMax]);
|
|
246
|
+
const handleMaxSlider = useCallback2((event) => {
|
|
247
|
+
const value = Number.parseFloat(event.target.value);
|
|
248
|
+
setLocalMax(() => {
|
|
249
|
+
if (localMin !== null && value < localMin) {
|
|
250
|
+
return localMin;
|
|
411
251
|
}
|
|
252
|
+
return value;
|
|
253
|
+
});
|
|
254
|
+
}, [localMin]);
|
|
255
|
+
const handleSliderChange = useCallback2(() => {
|
|
256
|
+
if (localMin === null && localMax === null) {
|
|
257
|
+
onChange(null);
|
|
258
|
+
} else {
|
|
259
|
+
onChange({ min: localMin, max: localMax });
|
|
412
260
|
}
|
|
413
|
-
}, [
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}, []);
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
)
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}, []);
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
261
|
+
}, [localMin, localMax, onChange]);
|
|
262
|
+
const handleMinInput = useCallback2((event) => {
|
|
263
|
+
const value = event.target.value === "" ? null : Number.parseFloat(event.target.value);
|
|
264
|
+
if (value !== null && !Number.isNaN(value)) {
|
|
265
|
+
setLocalMin(Math.max(dataMin, Math.min(value, localMax ?? dataMax)));
|
|
266
|
+
} else if (value === null) {
|
|
267
|
+
setLocalMin(null);
|
|
268
|
+
}
|
|
269
|
+
}, [dataMin, dataMax, localMax]);
|
|
270
|
+
const handleMaxInput = useCallback2((event) => {
|
|
271
|
+
const value = event.target.value === "" ? null : Number.parseFloat(event.target.value);
|
|
272
|
+
if (value !== null && !Number.isNaN(value)) {
|
|
273
|
+
setLocalMax(Math.min(dataMax, Math.max(value, localMin ?? dataMin)));
|
|
274
|
+
} else if (value === null) {
|
|
275
|
+
setLocalMax(null);
|
|
276
|
+
}
|
|
277
|
+
}, [dataMin, dataMax, localMin]);
|
|
278
|
+
const handleInputBlur = useCallback2(() => {
|
|
279
|
+
if (localMin === null && localMax === null) {
|
|
280
|
+
onChange(null);
|
|
281
|
+
} else {
|
|
282
|
+
onChange({ min: localMin, max: localMax });
|
|
283
|
+
}
|
|
284
|
+
}, [localMin, localMax, onChange]);
|
|
285
|
+
const clearFilter = useCallback2(() => {
|
|
286
|
+
setLocalMin(null);
|
|
287
|
+
setLocalMax(null);
|
|
288
|
+
onChange(null);
|
|
289
|
+
}, [onChange]);
|
|
290
|
+
const setFullRange = useCallback2(() => {
|
|
291
|
+
setLocalMin(dataMin);
|
|
292
|
+
setLocalMax(dataMax);
|
|
293
|
+
onChange({ min: dataMin, max: dataMax });
|
|
294
|
+
}, [dataMin, dataMax, onChange]);
|
|
295
|
+
useEffect2(() => {
|
|
296
|
+
setLocalMin(currentRange?.min ?? null);
|
|
297
|
+
setLocalMax(currentRange?.max ?? null);
|
|
298
|
+
}, [currentRange]);
|
|
299
|
+
return /* @__PURE__ */ jsxs2("div", { className: "vpg-range-filter", children: [
|
|
300
|
+
/* @__PURE__ */ jsxs2("div", { className: "vpg-range-info", children: [
|
|
301
|
+
/* @__PURE__ */ jsx2("span", { className: "vpg-range-label", children: "Data range:" }),
|
|
302
|
+
/* @__PURE__ */ jsxs2("span", { className: "vpg-range-bounds", children: [
|
|
303
|
+
formatValue(dataMin),
|
|
304
|
+
" ",
|
|
305
|
+
"\u2013",
|
|
306
|
+
formatValue(dataMax)
|
|
307
|
+
] })
|
|
308
|
+
] }),
|
|
309
|
+
/* @__PURE__ */ jsxs2("div", { className: "vpg-slider-container", children: [
|
|
310
|
+
/* @__PURE__ */ jsx2("div", { className: "vpg-slider-track", children: /* @__PURE__ */ jsx2(
|
|
311
|
+
"div",
|
|
312
|
+
{
|
|
313
|
+
className: "vpg-slider-fill",
|
|
314
|
+
style: {
|
|
315
|
+
left: `${minPercent}%`,
|
|
316
|
+
right: `${100 - maxPercent}%`
|
|
317
|
+
}
|
|
462
318
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
319
|
+
) }),
|
|
320
|
+
/* @__PURE__ */ jsx2(
|
|
321
|
+
"input",
|
|
322
|
+
{
|
|
323
|
+
type: "range",
|
|
324
|
+
className: "vpg-slider vpg-slider-min",
|
|
325
|
+
min: dataMin,
|
|
326
|
+
max: dataMax,
|
|
327
|
+
step,
|
|
328
|
+
value: localMin ?? dataMin,
|
|
329
|
+
onChange: handleMinSlider,
|
|
330
|
+
onMouseUp: handleSliderChange,
|
|
331
|
+
onTouchEnd: handleSliderChange
|
|
332
|
+
}
|
|
333
|
+
),
|
|
334
|
+
/* @__PURE__ */ jsx2(
|
|
335
|
+
"input",
|
|
336
|
+
{
|
|
337
|
+
type: "range",
|
|
338
|
+
className: "vpg-slider vpg-slider-max",
|
|
339
|
+
min: dataMin,
|
|
340
|
+
max: dataMax,
|
|
341
|
+
step,
|
|
342
|
+
value: localMax ?? dataMax,
|
|
343
|
+
onChange: handleMaxSlider,
|
|
344
|
+
onMouseUp: handleSliderChange,
|
|
345
|
+
onTouchEnd: handleSliderChange
|
|
346
|
+
}
|
|
347
|
+
)
|
|
348
|
+
] }),
|
|
349
|
+
/* @__PURE__ */ jsxs2("div", { className: "vpg-range-inputs", children: [
|
|
350
|
+
/* @__PURE__ */ jsxs2("div", { className: "vpg-input-group", children: [
|
|
351
|
+
/* @__PURE__ */ jsx2("label", { className: "vpg-input-label", children: "Min" }),
|
|
352
|
+
/* @__PURE__ */ jsx2(
|
|
353
|
+
"input",
|
|
354
|
+
{
|
|
355
|
+
type: "number",
|
|
356
|
+
className: "vpg-range-input",
|
|
357
|
+
placeholder: formatValue(dataMin),
|
|
358
|
+
value: localMin ?? "",
|
|
359
|
+
step,
|
|
360
|
+
onChange: handleMinInput,
|
|
361
|
+
onBlur: handleInputBlur
|
|
482
362
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
363
|
+
)
|
|
364
|
+
] }),
|
|
365
|
+
/* @__PURE__ */ jsx2("span", { className: "vpg-input-separator", children: "to" }),
|
|
366
|
+
/* @__PURE__ */ jsxs2("div", { className: "vpg-input-group", children: [
|
|
367
|
+
/* @__PURE__ */ jsx2("label", { className: "vpg-input-label", children: "Max" }),
|
|
368
|
+
/* @__PURE__ */ jsx2(
|
|
369
|
+
"input",
|
|
370
|
+
{
|
|
371
|
+
type: "number",
|
|
372
|
+
className: "vpg-range-input",
|
|
373
|
+
placeholder: formatValue(dataMax),
|
|
374
|
+
value: localMax ?? "",
|
|
375
|
+
step,
|
|
376
|
+
onChange: handleMaxInput,
|
|
377
|
+
onBlur: handleInputBlur
|
|
378
|
+
}
|
|
379
|
+
)
|
|
380
|
+
] })
|
|
381
|
+
] }),
|
|
382
|
+
/* @__PURE__ */ jsxs2("div", { className: "vpg-range-actions", children: [
|
|
383
|
+
/* @__PURE__ */ jsxs2(
|
|
384
|
+
"button",
|
|
385
|
+
{
|
|
386
|
+
className: "vpg-range-btn",
|
|
387
|
+
disabled: !isFilterActive,
|
|
388
|
+
onClick: clearFilter,
|
|
389
|
+
children: [
|
|
390
|
+
/* @__PURE__ */ jsx2("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
|
|
391
|
+
"path",
|
|
392
|
+
{
|
|
393
|
+
strokeLinecap: "round",
|
|
394
|
+
strokeLinejoin: "round",
|
|
395
|
+
strokeWidth: 2,
|
|
396
|
+
d: "M6 18L18 6M6 6l12 12"
|
|
397
|
+
}
|
|
398
|
+
) }),
|
|
399
|
+
"Clear"
|
|
400
|
+
]
|
|
401
|
+
}
|
|
402
|
+
),
|
|
403
|
+
/* @__PURE__ */ jsxs2("button", { className: "vpg-range-btn", onClick: setFullRange, children: [
|
|
404
|
+
/* @__PURE__ */ jsx2("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
|
|
405
|
+
"path",
|
|
406
|
+
{
|
|
407
|
+
strokeLinecap: "round",
|
|
408
|
+
strokeLinejoin: "round",
|
|
409
|
+
strokeWidth: 2,
|
|
410
|
+
d: "M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
|
|
411
|
+
}
|
|
412
|
+
) }),
|
|
413
|
+
"Full Range"
|
|
414
|
+
] })
|
|
415
|
+
] }),
|
|
416
|
+
isFilterActive && /* @__PURE__ */ jsxs2("div", { className: "vpg-filter-summary", children: [
|
|
417
|
+
/* @__PURE__ */ jsx2("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
|
|
418
|
+
"path",
|
|
419
|
+
{
|
|
420
|
+
strokeLinecap: "round",
|
|
421
|
+
strokeLinejoin: "round",
|
|
422
|
+
strokeWidth: 2,
|
|
423
|
+
d: "M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
|
|
424
|
+
}
|
|
425
|
+
) }),
|
|
426
|
+
/* @__PURE__ */ jsxs2("span", { children: [
|
|
427
|
+
"Showing values",
|
|
428
|
+
" ",
|
|
429
|
+
localMin !== null && /* @__PURE__ */ jsxs2("strong", { children: [
|
|
430
|
+
"\u2265",
|
|
431
|
+
formatValue(localMin)
|
|
432
|
+
] }),
|
|
433
|
+
localMin !== null && localMax !== null && " and ",
|
|
434
|
+
localMax !== null && /* @__PURE__ */ jsxs2("strong", { children: [
|
|
435
|
+
"\u2264",
|
|
436
|
+
formatValue(localMax)
|
|
437
|
+
] })
|
|
438
|
+
] })
|
|
439
|
+
] })
|
|
440
|
+
] });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/components/ColumnFilter.tsx
|
|
444
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
445
|
+
function ColumnFilter({
|
|
446
|
+
columnName,
|
|
447
|
+
stats,
|
|
448
|
+
selectedValues,
|
|
449
|
+
sortDirection,
|
|
450
|
+
numericRange,
|
|
451
|
+
onFilter,
|
|
452
|
+
onSort,
|
|
453
|
+
onClose,
|
|
454
|
+
onRangeFilter
|
|
455
|
+
}) {
|
|
456
|
+
const [searchQuery, setSearchQuery] = useState3("");
|
|
457
|
+
const [localSelected, setLocalSelected] = useState3(new Set(selectedValues));
|
|
458
|
+
const dropdownRef = useRef(null);
|
|
459
|
+
const searchInputRef = useRef(null);
|
|
460
|
+
const isNumericColumn = stats.type === "number" && stats.numericMin !== void 0 && stats.numericMax !== void 0;
|
|
461
|
+
const [filterMode, setFilterMode] = useState3(numericRange ? "range" : "values");
|
|
462
|
+
const [localRange, setLocalRange] = useState3(numericRange ?? null);
|
|
463
|
+
const hasBlankValues = stats.nullCount > 0;
|
|
464
|
+
const filteredValues = useMemo3(() => {
|
|
465
|
+
const values = stats.uniqueValues;
|
|
466
|
+
if (!searchQuery)
|
|
467
|
+
return values;
|
|
468
|
+
const query = searchQuery.toLowerCase();
|
|
469
|
+
return values.filter((v) => v.toLowerCase().includes(query));
|
|
470
|
+
}, [stats.uniqueValues, searchQuery]);
|
|
471
|
+
const allValues = useMemo3(() => {
|
|
472
|
+
const values = [...filteredValues];
|
|
473
|
+
if (hasBlankValues && (!searchQuery || "(blank)".includes(searchQuery.toLowerCase()))) {
|
|
474
|
+
values.unshift("(blank)");
|
|
502
475
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
476
|
+
return values;
|
|
477
|
+
}, [filteredValues, hasBlankValues, searchQuery]);
|
|
478
|
+
const _isAllSelected = useMemo3(
|
|
479
|
+
() => allValues.every((v) => localSelected.has(v)),
|
|
480
|
+
[allValues, localSelected]
|
|
481
|
+
);
|
|
482
|
+
const toggleValue = useCallback3((value) => {
|
|
483
|
+
setLocalSelected((prev) => {
|
|
484
|
+
const next = new Set(prev);
|
|
485
|
+
if (next.has(value)) {
|
|
486
|
+
next.delete(value);
|
|
510
487
|
} else {
|
|
511
|
-
|
|
488
|
+
next.add(value);
|
|
512
489
|
}
|
|
513
|
-
|
|
514
|
-
return updated;
|
|
515
|
-
});
|
|
516
|
-
}, []);
|
|
517
|
-
const removeCalculatedField = useCallback3((id) => {
|
|
518
|
-
setCalculatedFields((prev) => {
|
|
519
|
-
const updated = prev.filter((f) => f.id !== id);
|
|
520
|
-
saveCalculatedFields(updated);
|
|
521
|
-
return updated;
|
|
490
|
+
return next;
|
|
522
491
|
});
|
|
523
|
-
setValueFields((prev) => prev.filter((v) => v.field !== `calc:${id}`));
|
|
524
492
|
}, []);
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
rowFields,
|
|
528
|
-
columnFields,
|
|
529
|
-
valueFields,
|
|
530
|
-
showRowTotals,
|
|
531
|
-
showColumnTotals,
|
|
532
|
-
calculatedFields,
|
|
533
|
-
// Computed
|
|
534
|
-
availableFields,
|
|
535
|
-
unassignedFields,
|
|
536
|
-
isConfigured,
|
|
537
|
-
pivotResult,
|
|
538
|
-
// Actions
|
|
539
|
-
addRowField,
|
|
540
|
-
removeRowField,
|
|
541
|
-
addColumnField,
|
|
542
|
-
removeColumnField,
|
|
543
|
-
addValueField,
|
|
544
|
-
removeValueField,
|
|
545
|
-
updateValueFieldAggregation,
|
|
546
|
-
clearConfig,
|
|
547
|
-
setShowRowTotals,
|
|
548
|
-
setShowColumnTotals,
|
|
549
|
-
autoSuggestConfig,
|
|
550
|
-
setRowFields,
|
|
551
|
-
setColumnFields,
|
|
552
|
-
addCalculatedField,
|
|
553
|
-
removeCalculatedField
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// src/hooks/useGridFeatures.ts
|
|
558
|
-
import { useState as useState4, useMemo as useMemo4, useCallback as useCallback4 } from "react";
|
|
559
|
-
import {
|
|
560
|
-
exportToCSV as coreExportToCSV,
|
|
561
|
-
exportPivotToCSV as coreExportPivotToCSV,
|
|
562
|
-
copyToClipboard as coreCopyToClipboard,
|
|
563
|
-
formatSelectionForClipboard as coreFormatSelection
|
|
564
|
-
} from "@smallwebco/tinypivot-core";
|
|
565
|
-
function exportToCSV(data, columns, options) {
|
|
566
|
-
coreExportToCSV(data, columns, options);
|
|
567
|
-
}
|
|
568
|
-
function exportPivotToCSV(pivotData, rowFields, columnFields, valueFields, options) {
|
|
569
|
-
coreExportPivotToCSV(pivotData, rowFields, columnFields, valueFields, options);
|
|
570
|
-
}
|
|
571
|
-
function copyToClipboard(text, onSuccess, onError) {
|
|
572
|
-
coreCopyToClipboard(text, onSuccess, onError);
|
|
573
|
-
}
|
|
574
|
-
function formatSelectionForClipboard(rows, columns, selectionBounds) {
|
|
575
|
-
return coreFormatSelection(rows, columns, selectionBounds);
|
|
576
|
-
}
|
|
577
|
-
function usePagination(data, options = {}) {
|
|
578
|
-
const [pageSize, setPageSize] = useState4(options.pageSize ?? 50);
|
|
579
|
-
const [currentPage, setCurrentPage] = useState4(options.currentPage ?? 1);
|
|
580
|
-
const totalPages = useMemo4(
|
|
581
|
-
() => Math.max(1, Math.ceil(data.length / pageSize)),
|
|
582
|
-
[data.length, pageSize]
|
|
583
|
-
);
|
|
584
|
-
const paginatedData = useMemo4(() => {
|
|
585
|
-
const start = (currentPage - 1) * pageSize;
|
|
586
|
-
const end = start + pageSize;
|
|
587
|
-
return data.slice(start, end);
|
|
588
|
-
}, [data, currentPage, pageSize]);
|
|
589
|
-
const startIndex = useMemo4(() => (currentPage - 1) * pageSize + 1, [currentPage, pageSize]);
|
|
590
|
-
const endIndex = useMemo4(
|
|
591
|
-
() => Math.min(currentPage * pageSize, data.length),
|
|
592
|
-
[currentPage, pageSize, data.length]
|
|
593
|
-
);
|
|
594
|
-
const goToPage = useCallback4(
|
|
595
|
-
(page) => {
|
|
596
|
-
setCurrentPage(Math.max(1, Math.min(page, totalPages)));
|
|
597
|
-
},
|
|
598
|
-
[totalPages]
|
|
599
|
-
);
|
|
600
|
-
const nextPage = useCallback4(() => {
|
|
601
|
-
if (currentPage < totalPages) {
|
|
602
|
-
setCurrentPage((prev) => prev + 1);
|
|
603
|
-
}
|
|
604
|
-
}, [currentPage, totalPages]);
|
|
605
|
-
const prevPage = useCallback4(() => {
|
|
606
|
-
if (currentPage > 1) {
|
|
607
|
-
setCurrentPage((prev) => prev - 1);
|
|
608
|
-
}
|
|
609
|
-
}, [currentPage]);
|
|
610
|
-
const firstPage = useCallback4(() => {
|
|
611
|
-
setCurrentPage(1);
|
|
612
|
-
}, []);
|
|
613
|
-
const lastPage = useCallback4(() => {
|
|
614
|
-
setCurrentPage(totalPages);
|
|
615
|
-
}, [totalPages]);
|
|
616
|
-
const updatePageSize = useCallback4((size) => {
|
|
617
|
-
setPageSize(size);
|
|
618
|
-
setCurrentPage(1);
|
|
619
|
-
}, []);
|
|
620
|
-
return {
|
|
621
|
-
pageSize,
|
|
622
|
-
currentPage,
|
|
623
|
-
totalPages,
|
|
624
|
-
paginatedData,
|
|
625
|
-
startIndex,
|
|
626
|
-
endIndex,
|
|
627
|
-
goToPage,
|
|
628
|
-
nextPage,
|
|
629
|
-
prevPage,
|
|
630
|
-
firstPage,
|
|
631
|
-
lastPage,
|
|
632
|
-
setPageSize: updatePageSize
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
function useGlobalSearch(data, columns) {
|
|
636
|
-
const [searchTerm, setSearchTerm] = useState4("");
|
|
637
|
-
const [caseSensitive, setCaseSensitive] = useState4(false);
|
|
638
|
-
const filteredData = useMemo4(() => {
|
|
639
|
-
if (!searchTerm.trim()) {
|
|
640
|
-
return data;
|
|
641
|
-
}
|
|
642
|
-
const term = caseSensitive ? searchTerm.trim() : searchTerm.trim().toLowerCase();
|
|
643
|
-
return data.filter((row) => {
|
|
644
|
-
for (const col of columns) {
|
|
645
|
-
const value = row[col];
|
|
646
|
-
if (value === null || value === void 0) continue;
|
|
647
|
-
const strValue = caseSensitive ? String(value) : String(value).toLowerCase();
|
|
648
|
-
if (strValue.includes(term)) {
|
|
649
|
-
return true;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
return false;
|
|
653
|
-
});
|
|
654
|
-
}, [data, columns, searchTerm, caseSensitive]);
|
|
655
|
-
const clearSearch = useCallback4(() => {
|
|
656
|
-
setSearchTerm("");
|
|
657
|
-
}, []);
|
|
658
|
-
return {
|
|
659
|
-
searchTerm,
|
|
660
|
-
setSearchTerm,
|
|
661
|
-
caseSensitive,
|
|
662
|
-
setCaseSensitive,
|
|
663
|
-
filteredData,
|
|
664
|
-
clearSearch
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
function useRowSelection(data) {
|
|
668
|
-
const [selectedRowIndices, setSelectedRowIndices] = useState4(/* @__PURE__ */ new Set());
|
|
669
|
-
const selectedRows = useMemo4(() => {
|
|
670
|
-
return Array.from(selectedRowIndices).sort((a, b) => a - b).map((idx) => data[idx]).filter(Boolean);
|
|
671
|
-
}, [data, selectedRowIndices]);
|
|
672
|
-
const allSelected = useMemo4(() => {
|
|
673
|
-
return data.length > 0 && selectedRowIndices.size === data.length;
|
|
674
|
-
}, [data.length, selectedRowIndices.size]);
|
|
675
|
-
const someSelected = useMemo4(() => {
|
|
676
|
-
return selectedRowIndices.size > 0 && selectedRowIndices.size < data.length;
|
|
677
|
-
}, [data.length, selectedRowIndices.size]);
|
|
678
|
-
const toggleRow = useCallback4((index) => {
|
|
679
|
-
setSelectedRowIndices((prev) => {
|
|
493
|
+
const selectAll = useCallback3(() => {
|
|
494
|
+
setLocalSelected((prev) => {
|
|
680
495
|
const next = new Set(prev);
|
|
681
|
-
|
|
682
|
-
next.
|
|
683
|
-
} else {
|
|
684
|
-
next.add(index);
|
|
496
|
+
for (const value of allValues) {
|
|
497
|
+
next.add(value);
|
|
685
498
|
}
|
|
686
499
|
return next;
|
|
687
500
|
});
|
|
501
|
+
}, [allValues]);
|
|
502
|
+
const clearAll = useCallback3(() => {
|
|
503
|
+
setLocalSelected(/* @__PURE__ */ new Set());
|
|
688
504
|
}, []);
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const deselectRow = useCallback4((index) => {
|
|
693
|
-
setSelectedRowIndices((prev) => {
|
|
694
|
-
const next = new Set(prev);
|
|
695
|
-
next.delete(index);
|
|
696
|
-
return next;
|
|
697
|
-
});
|
|
698
|
-
}, []);
|
|
699
|
-
const selectAll = useCallback4(() => {
|
|
700
|
-
setSelectedRowIndices(new Set(data.map((_, idx) => idx)));
|
|
701
|
-
}, [data]);
|
|
702
|
-
const deselectAll = useCallback4(() => {
|
|
703
|
-
setSelectedRowIndices(/* @__PURE__ */ new Set());
|
|
704
|
-
}, []);
|
|
705
|
-
const toggleAll = useCallback4(() => {
|
|
706
|
-
if (allSelected) {
|
|
707
|
-
deselectAll();
|
|
505
|
+
const applyFilter = useCallback3(() => {
|
|
506
|
+
if (localSelected.size === 0) {
|
|
507
|
+
onFilter([]);
|
|
708
508
|
} else {
|
|
709
|
-
|
|
509
|
+
onFilter(Array.from(localSelected));
|
|
710
510
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
return next;
|
|
727
|
-
});
|
|
511
|
+
onClose();
|
|
512
|
+
}, [localSelected, onFilter, onClose]);
|
|
513
|
+
const sortAscending = useCallback3(() => {
|
|
514
|
+
onSort(sortDirection === "asc" ? null : "asc");
|
|
515
|
+
}, [sortDirection, onSort]);
|
|
516
|
+
const sortDescending = useCallback3(() => {
|
|
517
|
+
onSort(sortDirection === "desc" ? null : "desc");
|
|
518
|
+
}, [sortDirection, onSort]);
|
|
519
|
+
const clearFilter = useCallback3(() => {
|
|
520
|
+
setLocalSelected(/* @__PURE__ */ new Set());
|
|
521
|
+
onFilter([]);
|
|
522
|
+
onClose();
|
|
523
|
+
}, [onFilter, onClose]);
|
|
524
|
+
const handleRangeChange = useCallback3((range) => {
|
|
525
|
+
setLocalRange(range);
|
|
728
526
|
}, []);
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
};
|
|
743
|
-
}
|
|
744
|
-
function useColumnResize(initialWidths, minWidth = 60, maxWidth = 600) {
|
|
745
|
-
const [columnWidths, setColumnWidths] = useState4({ ...initialWidths });
|
|
746
|
-
const [isResizing, setIsResizing] = useState4(false);
|
|
747
|
-
const [resizingColumn, setResizingColumn] = useState4(null);
|
|
748
|
-
const startResize = useCallback4(
|
|
749
|
-
(columnId, event) => {
|
|
750
|
-
setIsResizing(true);
|
|
751
|
-
setResizingColumn(columnId);
|
|
752
|
-
const startX = event.clientX;
|
|
753
|
-
const startWidth = columnWidths[columnId] || 150;
|
|
754
|
-
const handleMouseMove = (e) => {
|
|
755
|
-
const diff = e.clientX - startX;
|
|
756
|
-
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + diff));
|
|
757
|
-
setColumnWidths((prev) => ({
|
|
758
|
-
...prev,
|
|
759
|
-
[columnId]: newWidth
|
|
760
|
-
}));
|
|
761
|
-
};
|
|
762
|
-
const handleMouseUp = () => {
|
|
763
|
-
setIsResizing(false);
|
|
764
|
-
setResizingColumn(null);
|
|
765
|
-
document.removeEventListener("mousemove", handleMouseMove);
|
|
766
|
-
document.removeEventListener("mouseup", handleMouseUp);
|
|
767
|
-
};
|
|
768
|
-
document.addEventListener("mousemove", handleMouseMove);
|
|
769
|
-
document.addEventListener("mouseup", handleMouseUp);
|
|
770
|
-
},
|
|
771
|
-
[columnWidths, minWidth, maxWidth]
|
|
772
|
-
);
|
|
773
|
-
const resetColumnWidth = useCallback4(
|
|
774
|
-
(columnId) => {
|
|
775
|
-
if (initialWidths[columnId]) {
|
|
776
|
-
setColumnWidths((prev) => ({
|
|
777
|
-
...prev,
|
|
778
|
-
[columnId]: initialWidths[columnId]
|
|
779
|
-
}));
|
|
527
|
+
const applyRangeFilter = useCallback3(() => {
|
|
528
|
+
onRangeFilter?.(localRange);
|
|
529
|
+
onClose();
|
|
530
|
+
}, [localRange, onRangeFilter, onClose]);
|
|
531
|
+
const clearRangeFilter = useCallback3(() => {
|
|
532
|
+
setLocalRange(null);
|
|
533
|
+
onRangeFilter?.(null);
|
|
534
|
+
onClose();
|
|
535
|
+
}, [onRangeFilter, onClose]);
|
|
536
|
+
useEffect3(() => {
|
|
537
|
+
const handleClickOutside = (event) => {
|
|
538
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
539
|
+
onClose();
|
|
780
540
|
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
resizingColumn,
|
|
792
|
-
startResize,
|
|
793
|
-
resetColumnWidth,
|
|
794
|
-
resetAllWidths
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// src/components/ColumnFilter.tsx
|
|
799
|
-
import { useState as useState6, useEffect as useEffect4, useRef, useCallback as useCallback6, useMemo as useMemo6 } from "react";
|
|
800
|
-
|
|
801
|
-
// src/components/NumericRangeFilter.tsx
|
|
802
|
-
import { useState as useState5, useCallback as useCallback5, useMemo as useMemo5, useEffect as useEffect3 } from "react";
|
|
803
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
804
|
-
function NumericRangeFilter({
|
|
805
|
-
dataMin,
|
|
806
|
-
dataMax,
|
|
807
|
-
currentRange,
|
|
808
|
-
onChange
|
|
809
|
-
}) {
|
|
810
|
-
const [localMin, setLocalMin] = useState5(currentRange?.min ?? null);
|
|
811
|
-
const [localMax, setLocalMax] = useState5(currentRange?.max ?? null);
|
|
812
|
-
const step = useMemo5(() => {
|
|
813
|
-
const range = dataMax - dataMin;
|
|
814
|
-
if (range === 0) return 1;
|
|
815
|
-
if (range <= 1) return 0.01;
|
|
816
|
-
if (range <= 10) return 0.1;
|
|
817
|
-
if (range <= 100) return 1;
|
|
818
|
-
if (range <= 1e3) return 10;
|
|
819
|
-
return Math.pow(10, Math.floor(Math.log10(range)) - 2);
|
|
820
|
-
}, [dataMin, dataMax]);
|
|
821
|
-
const formatValue = useCallback5((val) => {
|
|
822
|
-
if (val === null) return "";
|
|
823
|
-
if (Number.isInteger(val)) return val.toLocaleString();
|
|
824
|
-
return val.toLocaleString(void 0, { maximumFractionDigits: 2 });
|
|
825
|
-
}, []);
|
|
826
|
-
const isFilterActive = localMin !== null || localMax !== null;
|
|
827
|
-
const minPercent = useMemo5(() => {
|
|
828
|
-
if (localMin === null || dataMax === dataMin) return 0;
|
|
829
|
-
return (localMin - dataMin) / (dataMax - dataMin) * 100;
|
|
830
|
-
}, [localMin, dataMin, dataMax]);
|
|
831
|
-
const maxPercent = useMemo5(() => {
|
|
832
|
-
if (localMax === null || dataMax === dataMin) return 100;
|
|
833
|
-
return (localMax - dataMin) / (dataMax - dataMin) * 100;
|
|
834
|
-
}, [localMax, dataMin, dataMax]);
|
|
835
|
-
const handleMinSlider = useCallback5((event) => {
|
|
836
|
-
const value = Number.parseFloat(event.target.value);
|
|
837
|
-
setLocalMin((prev) => {
|
|
838
|
-
if (localMax !== null && value > localMax) {
|
|
839
|
-
return localMax;
|
|
840
|
-
}
|
|
841
|
-
return value;
|
|
842
|
-
});
|
|
843
|
-
}, [localMax]);
|
|
844
|
-
const handleMaxSlider = useCallback5((event) => {
|
|
845
|
-
const value = Number.parseFloat(event.target.value);
|
|
846
|
-
setLocalMax((prev) => {
|
|
847
|
-
if (localMin !== null && value < localMin) {
|
|
848
|
-
return localMin;
|
|
541
|
+
};
|
|
542
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
543
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
544
|
+
}, [onClose]);
|
|
545
|
+
useEffect3(() => {
|
|
546
|
+
const handleKeydown = (event) => {
|
|
547
|
+
if (event.key === "Escape") {
|
|
548
|
+
onClose();
|
|
549
|
+
} else if (event.key === "Enter" && event.ctrlKey) {
|
|
550
|
+
applyFilter();
|
|
849
551
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
if (localMin === null && localMax === null) {
|
|
855
|
-
onChange(null);
|
|
856
|
-
} else {
|
|
857
|
-
onChange({ min: localMin, max: localMax });
|
|
858
|
-
}
|
|
859
|
-
}, [localMin, localMax, onChange]);
|
|
860
|
-
const handleMinInput = useCallback5((event) => {
|
|
861
|
-
const value = event.target.value === "" ? null : Number.parseFloat(event.target.value);
|
|
862
|
-
if (value !== null && !Number.isNaN(value)) {
|
|
863
|
-
setLocalMin(Math.max(dataMin, Math.min(value, localMax ?? dataMax)));
|
|
864
|
-
} else if (value === null) {
|
|
865
|
-
setLocalMin(null);
|
|
866
|
-
}
|
|
867
|
-
}, [dataMin, dataMax, localMax]);
|
|
868
|
-
const handleMaxInput = useCallback5((event) => {
|
|
869
|
-
const value = event.target.value === "" ? null : Number.parseFloat(event.target.value);
|
|
870
|
-
if (value !== null && !Number.isNaN(value)) {
|
|
871
|
-
setLocalMax(Math.min(dataMax, Math.max(value, localMin ?? dataMin)));
|
|
872
|
-
} else if (value === null) {
|
|
873
|
-
setLocalMax(null);
|
|
874
|
-
}
|
|
875
|
-
}, [dataMin, dataMax, localMin]);
|
|
876
|
-
const handleInputBlur = useCallback5(() => {
|
|
877
|
-
if (localMin === null && localMax === null) {
|
|
878
|
-
onChange(null);
|
|
879
|
-
} else {
|
|
880
|
-
onChange({ min: localMin, max: localMax });
|
|
881
|
-
}
|
|
882
|
-
}, [localMin, localMax, onChange]);
|
|
883
|
-
const clearFilter = useCallback5(() => {
|
|
884
|
-
setLocalMin(null);
|
|
885
|
-
setLocalMax(null);
|
|
886
|
-
onChange(null);
|
|
887
|
-
}, [onChange]);
|
|
888
|
-
const setFullRange = useCallback5(() => {
|
|
889
|
-
setLocalMin(dataMin);
|
|
890
|
-
setLocalMax(dataMax);
|
|
891
|
-
onChange({ min: dataMin, max: dataMax });
|
|
892
|
-
}, [dataMin, dataMax, onChange]);
|
|
552
|
+
};
|
|
553
|
+
document.addEventListener("keydown", handleKeydown);
|
|
554
|
+
return () => document.removeEventListener("keydown", handleKeydown);
|
|
555
|
+
}, [onClose, applyFilter]);
|
|
893
556
|
useEffect3(() => {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
557
|
+
searchInputRef.current?.focus();
|
|
558
|
+
}, []);
|
|
559
|
+
useEffect3(() => {
|
|
560
|
+
setLocalSelected(new Set(selectedValues));
|
|
561
|
+
}, [selectedValues]);
|
|
562
|
+
useEffect3(() => {
|
|
563
|
+
setLocalRange(numericRange ?? null);
|
|
564
|
+
if (numericRange) {
|
|
565
|
+
setFilterMode("range");
|
|
566
|
+
}
|
|
567
|
+
}, [numericRange]);
|
|
568
|
+
return /* @__PURE__ */ jsxs3("div", { ref: dropdownRef, className: "vpg-filter-dropdown", children: [
|
|
569
|
+
/* @__PURE__ */ jsxs3("div", { className: "vpg-filter-header", children: [
|
|
570
|
+
/* @__PURE__ */ jsx3("span", { className: "vpg-filter-title", children: columnName }),
|
|
571
|
+
/* @__PURE__ */ jsxs3("span", { className: "vpg-filter-count", children: [
|
|
572
|
+
stats.uniqueValues.length.toLocaleString(),
|
|
573
|
+
" ",
|
|
574
|
+
"unique"
|
|
904
575
|
] })
|
|
905
576
|
] }),
|
|
906
|
-
/* @__PURE__ */
|
|
907
|
-
/* @__PURE__ */
|
|
908
|
-
"
|
|
909
|
-
{
|
|
910
|
-
className: "vpg-slider-fill",
|
|
911
|
-
style: {
|
|
912
|
-
left: `${minPercent}%`,
|
|
913
|
-
right: `${100 - maxPercent}%`
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
) }),
|
|
917
|
-
/* @__PURE__ */ jsx(
|
|
918
|
-
"input",
|
|
577
|
+
/* @__PURE__ */ jsxs3("div", { className: "vpg-sort-controls", children: [
|
|
578
|
+
/* @__PURE__ */ jsxs3(
|
|
579
|
+
"button",
|
|
919
580
|
{
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
581
|
+
className: `vpg-sort-btn ${sortDirection === "asc" ? "active" : ""}`,
|
|
582
|
+
title: isNumericColumn ? "Sort Low to High" : "Sort A to Z",
|
|
583
|
+
onClick: sortAscending,
|
|
584
|
+
children: [
|
|
585
|
+
/* @__PURE__ */ jsx3("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3(
|
|
586
|
+
"path",
|
|
587
|
+
{
|
|
588
|
+
strokeLinecap: "round",
|
|
589
|
+
strokeLinejoin: "round",
|
|
590
|
+
strokeWidth: 2,
|
|
591
|
+
d: "M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"
|
|
592
|
+
}
|
|
593
|
+
) }),
|
|
594
|
+
/* @__PURE__ */ jsx3("span", { children: isNumericColumn ? "1\u21929" : "A\u2192Z" })
|
|
595
|
+
]
|
|
929
596
|
}
|
|
930
597
|
),
|
|
931
|
-
/* @__PURE__ */
|
|
932
|
-
"
|
|
598
|
+
/* @__PURE__ */ jsxs3(
|
|
599
|
+
"button",
|
|
933
600
|
{
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
601
|
+
className: `vpg-sort-btn ${sortDirection === "desc" ? "active" : ""}`,
|
|
602
|
+
title: isNumericColumn ? "Sort High to Low" : "Sort Z to A",
|
|
603
|
+
onClick: sortDescending,
|
|
604
|
+
children: [
|
|
605
|
+
/* @__PURE__ */ jsx3("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3(
|
|
606
|
+
"path",
|
|
607
|
+
{
|
|
608
|
+
strokeLinecap: "round",
|
|
609
|
+
strokeLinejoin: "round",
|
|
610
|
+
strokeWidth: 2,
|
|
611
|
+
d: "M3 4h13M3 8h9m-9 4h9m5-4v12m0 0l-4-4m4 4l4-4"
|
|
612
|
+
}
|
|
613
|
+
) }),
|
|
614
|
+
/* @__PURE__ */ jsx3("span", { children: isNumericColumn ? "9\u21921" : "Z\u2192A" })
|
|
615
|
+
]
|
|
943
616
|
}
|
|
944
617
|
)
|
|
945
618
|
] }),
|
|
946
|
-
/* @__PURE__ */
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
/* @__PURE__ */ jsx(
|
|
950
|
-
"input",
|
|
951
|
-
{
|
|
952
|
-
type: "number",
|
|
953
|
-
className: "vpg-range-input",
|
|
954
|
-
placeholder: formatValue(dataMin),
|
|
955
|
-
value: localMin ?? "",
|
|
956
|
-
step,
|
|
957
|
-
onChange: handleMinInput,
|
|
958
|
-
onBlur: handleInputBlur
|
|
959
|
-
}
|
|
960
|
-
)
|
|
961
|
-
] }),
|
|
962
|
-
/* @__PURE__ */ jsx("span", { className: "vpg-input-separator", children: "to" }),
|
|
963
|
-
/* @__PURE__ */ jsxs("div", { className: "vpg-input-group", children: [
|
|
964
|
-
/* @__PURE__ */ jsx("label", { className: "vpg-input-label", children: "Max" }),
|
|
965
|
-
/* @__PURE__ */ jsx(
|
|
966
|
-
"input",
|
|
967
|
-
{
|
|
968
|
-
type: "number",
|
|
969
|
-
className: "vpg-range-input",
|
|
970
|
-
placeholder: formatValue(dataMax),
|
|
971
|
-
value: localMax ?? "",
|
|
972
|
-
step,
|
|
973
|
-
onChange: handleMaxInput,
|
|
974
|
-
onBlur: handleInputBlur
|
|
975
|
-
}
|
|
976
|
-
)
|
|
977
|
-
] })
|
|
978
|
-
] }),
|
|
979
|
-
/* @__PURE__ */ jsxs("div", { className: "vpg-range-actions", children: [
|
|
980
|
-
/* @__PURE__ */ jsxs(
|
|
619
|
+
/* @__PURE__ */ jsx3("div", { className: "vpg-divider" }),
|
|
620
|
+
isNumericColumn && /* @__PURE__ */ jsxs3("div", { className: "vpg-filter-tabs", children: [
|
|
621
|
+
/* @__PURE__ */ jsxs3(
|
|
981
622
|
"button",
|
|
982
623
|
{
|
|
983
|
-
className:
|
|
984
|
-
|
|
985
|
-
onClick: clearFilter,
|
|
624
|
+
className: `vpg-tab-btn ${filterMode === "values" ? "active" : ""}`,
|
|
625
|
+
onClick: () => setFilterMode("values"),
|
|
986
626
|
children: [
|
|
987
|
-
/* @__PURE__ */
|
|
627
|
+
/* @__PURE__ */ jsx3("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3(
|
|
988
628
|
"path",
|
|
989
629
|
{
|
|
990
630
|
strokeLinecap: "round",
|
|
991
631
|
strokeLinejoin: "round",
|
|
992
632
|
strokeWidth: 2,
|
|
993
|
-
d: "
|
|
633
|
+
d: "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
|
|
994
634
|
}
|
|
995
635
|
) }),
|
|
996
|
-
"
|
|
636
|
+
"Values"
|
|
997
637
|
]
|
|
998
638
|
}
|
|
999
639
|
),
|
|
1000
|
-
/* @__PURE__ */
|
|
1001
|
-
|
|
640
|
+
/* @__PURE__ */ jsxs3(
|
|
641
|
+
"button",
|
|
642
|
+
{
|
|
643
|
+
className: `vpg-tab-btn ${filterMode === "range" ? "active" : ""}`,
|
|
644
|
+
onClick: () => setFilterMode("range"),
|
|
645
|
+
children: [
|
|
646
|
+
/* @__PURE__ */ jsx3("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3(
|
|
647
|
+
"path",
|
|
648
|
+
{
|
|
649
|
+
strokeLinecap: "round",
|
|
650
|
+
strokeLinejoin: "round",
|
|
651
|
+
strokeWidth: 2,
|
|
652
|
+
d: "M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
|
|
653
|
+
}
|
|
654
|
+
) }),
|
|
655
|
+
"Range"
|
|
656
|
+
]
|
|
657
|
+
}
|
|
658
|
+
)
|
|
659
|
+
] }),
|
|
660
|
+
(!isNumericColumn || filterMode === "values") && /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
661
|
+
/* @__PURE__ */ jsxs3("div", { className: "vpg-search-container", children: [
|
|
662
|
+
/* @__PURE__ */ jsx3("svg", { className: "vpg-search-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3(
|
|
1002
663
|
"path",
|
|
1003
664
|
{
|
|
1004
665
|
strokeLinecap: "round",
|
|
1005
666
|
strokeLinejoin: "round",
|
|
1006
667
|
strokeWidth: 2,
|
|
1007
|
-
d: "
|
|
668
|
+
d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
1008
669
|
}
|
|
1009
670
|
) }),
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
/* @__PURE__ */
|
|
1024
|
-
"
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
671
|
+
/* @__PURE__ */ jsx3(
|
|
672
|
+
"input",
|
|
673
|
+
{
|
|
674
|
+
ref: searchInputRef,
|
|
675
|
+
type: "text",
|
|
676
|
+
value: searchQuery,
|
|
677
|
+
onChange: (e) => setSearchQuery(e.target.value),
|
|
678
|
+
placeholder: "Search values...",
|
|
679
|
+
className: "vpg-search-input"
|
|
680
|
+
}
|
|
681
|
+
),
|
|
682
|
+
searchQuery && /* @__PURE__ */ jsx3("button", { className: "vpg-clear-search", onClick: () => setSearchQuery(""), children: "\xD7" })
|
|
683
|
+
] }),
|
|
684
|
+
/* @__PURE__ */ jsxs3("div", { className: "vpg-bulk-actions", children: [
|
|
685
|
+
/* @__PURE__ */ jsxs3("button", { className: "vpg-bulk-btn", onClick: selectAll, children: [
|
|
686
|
+
/* @__PURE__ */ jsx3("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3(
|
|
687
|
+
"path",
|
|
688
|
+
{
|
|
689
|
+
strokeLinecap: "round",
|
|
690
|
+
strokeLinejoin: "round",
|
|
691
|
+
strokeWidth: 2,
|
|
692
|
+
d: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
693
|
+
}
|
|
694
|
+
) }),
|
|
695
|
+
"Select All"
|
|
1029
696
|
] }),
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
"\u2264 ",
|
|
1033
|
-
formatValue(localMax)
|
|
1034
|
-
] })
|
|
1035
|
-
] })
|
|
1036
|
-
] })
|
|
1037
|
-
] });
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
// src/components/ColumnFilter.tsx
|
|
1041
|
-
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1042
|
-
function ColumnFilter({
|
|
1043
|
-
columnName,
|
|
1044
|
-
stats,
|
|
1045
|
-
selectedValues,
|
|
1046
|
-
sortDirection,
|
|
1047
|
-
numericRange,
|
|
1048
|
-
onFilter,
|
|
1049
|
-
onSort,
|
|
1050
|
-
onClose,
|
|
1051
|
-
onRangeFilter
|
|
1052
|
-
}) {
|
|
1053
|
-
const [searchQuery, setSearchQuery] = useState6("");
|
|
1054
|
-
const [localSelected, setLocalSelected] = useState6(new Set(selectedValues));
|
|
1055
|
-
const dropdownRef = useRef(null);
|
|
1056
|
-
const searchInputRef = useRef(null);
|
|
1057
|
-
const isNumericColumn = stats.type === "number" && stats.numericMin !== void 0 && stats.numericMax !== void 0;
|
|
1058
|
-
const [filterMode, setFilterMode] = useState6(numericRange ? "range" : "values");
|
|
1059
|
-
const [localRange, setLocalRange] = useState6(numericRange ?? null);
|
|
1060
|
-
const hasBlankValues = stats.nullCount > 0;
|
|
1061
|
-
const filteredValues = useMemo6(() => {
|
|
1062
|
-
const values = stats.uniqueValues;
|
|
1063
|
-
if (!searchQuery) return values;
|
|
1064
|
-
const query = searchQuery.toLowerCase();
|
|
1065
|
-
return values.filter((v) => v.toLowerCase().includes(query));
|
|
1066
|
-
}, [stats.uniqueValues, searchQuery]);
|
|
1067
|
-
const allValues = useMemo6(() => {
|
|
1068
|
-
const values = [...filteredValues];
|
|
1069
|
-
if (hasBlankValues && (!searchQuery || "(blank)".includes(searchQuery.toLowerCase()))) {
|
|
1070
|
-
values.unshift("(blank)");
|
|
1071
|
-
}
|
|
1072
|
-
return values;
|
|
1073
|
-
}, [filteredValues, hasBlankValues, searchQuery]);
|
|
1074
|
-
const isAllSelected = useMemo6(
|
|
1075
|
-
() => allValues.every((v) => localSelected.has(v)),
|
|
1076
|
-
[allValues, localSelected]
|
|
1077
|
-
);
|
|
1078
|
-
const toggleValue = useCallback6((value) => {
|
|
1079
|
-
setLocalSelected((prev) => {
|
|
1080
|
-
const next = new Set(prev);
|
|
1081
|
-
if (next.has(value)) {
|
|
1082
|
-
next.delete(value);
|
|
1083
|
-
} else {
|
|
1084
|
-
next.add(value);
|
|
1085
|
-
}
|
|
1086
|
-
return next;
|
|
1087
|
-
});
|
|
1088
|
-
}, []);
|
|
1089
|
-
const selectAll = useCallback6(() => {
|
|
1090
|
-
setLocalSelected((prev) => {
|
|
1091
|
-
const next = new Set(prev);
|
|
1092
|
-
for (const value of allValues) {
|
|
1093
|
-
next.add(value);
|
|
1094
|
-
}
|
|
1095
|
-
return next;
|
|
1096
|
-
});
|
|
1097
|
-
}, [allValues]);
|
|
1098
|
-
const clearAll = useCallback6(() => {
|
|
1099
|
-
setLocalSelected(/* @__PURE__ */ new Set());
|
|
1100
|
-
}, []);
|
|
1101
|
-
const applyFilter = useCallback6(() => {
|
|
1102
|
-
if (localSelected.size === 0) {
|
|
1103
|
-
onFilter([]);
|
|
1104
|
-
} else {
|
|
1105
|
-
onFilter(Array.from(localSelected));
|
|
1106
|
-
}
|
|
1107
|
-
onClose();
|
|
1108
|
-
}, [localSelected, onFilter, onClose]);
|
|
1109
|
-
const sortAscending = useCallback6(() => {
|
|
1110
|
-
onSort(sortDirection === "asc" ? null : "asc");
|
|
1111
|
-
}, [sortDirection, onSort]);
|
|
1112
|
-
const sortDescending = useCallback6(() => {
|
|
1113
|
-
onSort(sortDirection === "desc" ? null : "desc");
|
|
1114
|
-
}, [sortDirection, onSort]);
|
|
1115
|
-
const clearFilter = useCallback6(() => {
|
|
1116
|
-
setLocalSelected(/* @__PURE__ */ new Set());
|
|
1117
|
-
onFilter([]);
|
|
1118
|
-
onClose();
|
|
1119
|
-
}, [onFilter, onClose]);
|
|
1120
|
-
const handleRangeChange = useCallback6((range) => {
|
|
1121
|
-
setLocalRange(range);
|
|
1122
|
-
}, []);
|
|
1123
|
-
const applyRangeFilter = useCallback6(() => {
|
|
1124
|
-
onRangeFilter?.(localRange);
|
|
1125
|
-
onClose();
|
|
1126
|
-
}, [localRange, onRangeFilter, onClose]);
|
|
1127
|
-
const clearRangeFilter = useCallback6(() => {
|
|
1128
|
-
setLocalRange(null);
|
|
1129
|
-
onRangeFilter?.(null);
|
|
1130
|
-
onClose();
|
|
1131
|
-
}, [onRangeFilter, onClose]);
|
|
1132
|
-
useEffect4(() => {
|
|
1133
|
-
const handleClickOutside = (event) => {
|
|
1134
|
-
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
1135
|
-
onClose();
|
|
1136
|
-
}
|
|
1137
|
-
};
|
|
1138
|
-
document.addEventListener("mousedown", handleClickOutside);
|
|
1139
|
-
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
1140
|
-
}, [onClose]);
|
|
1141
|
-
useEffect4(() => {
|
|
1142
|
-
const handleKeydown = (event) => {
|
|
1143
|
-
if (event.key === "Escape") {
|
|
1144
|
-
onClose();
|
|
1145
|
-
} else if (event.key === "Enter" && event.ctrlKey) {
|
|
1146
|
-
applyFilter();
|
|
1147
|
-
}
|
|
1148
|
-
};
|
|
1149
|
-
document.addEventListener("keydown", handleKeydown);
|
|
1150
|
-
return () => document.removeEventListener("keydown", handleKeydown);
|
|
1151
|
-
}, [onClose, applyFilter]);
|
|
1152
|
-
useEffect4(() => {
|
|
1153
|
-
searchInputRef.current?.focus();
|
|
1154
|
-
}, []);
|
|
1155
|
-
useEffect4(() => {
|
|
1156
|
-
setLocalSelected(new Set(selectedValues));
|
|
1157
|
-
}, [selectedValues]);
|
|
1158
|
-
useEffect4(() => {
|
|
1159
|
-
setLocalRange(numericRange ?? null);
|
|
1160
|
-
if (numericRange) {
|
|
1161
|
-
setFilterMode("range");
|
|
1162
|
-
}
|
|
1163
|
-
}, [numericRange]);
|
|
1164
|
-
return /* @__PURE__ */ jsxs2("div", { ref: dropdownRef, className: "vpg-filter-dropdown", children: [
|
|
1165
|
-
/* @__PURE__ */ jsxs2("div", { className: "vpg-filter-header", children: [
|
|
1166
|
-
/* @__PURE__ */ jsx2("span", { className: "vpg-filter-title", children: columnName }),
|
|
1167
|
-
/* @__PURE__ */ jsxs2("span", { className: "vpg-filter-count", children: [
|
|
1168
|
-
stats.uniqueValues.length.toLocaleString(),
|
|
1169
|
-
" unique"
|
|
1170
|
-
] })
|
|
1171
|
-
] }),
|
|
1172
|
-
/* @__PURE__ */ jsxs2("div", { className: "vpg-sort-controls", children: [
|
|
1173
|
-
/* @__PURE__ */ jsxs2(
|
|
1174
|
-
"button",
|
|
1175
|
-
{
|
|
1176
|
-
className: `vpg-sort-btn ${sortDirection === "asc" ? "active" : ""}`,
|
|
1177
|
-
title: isNumericColumn ? "Sort Low to High" : "Sort A to Z",
|
|
1178
|
-
onClick: sortAscending,
|
|
1179
|
-
children: [
|
|
1180
|
-
/* @__PURE__ */ jsx2("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
|
|
1181
|
-
"path",
|
|
1182
|
-
{
|
|
1183
|
-
strokeLinecap: "round",
|
|
1184
|
-
strokeLinejoin: "round",
|
|
1185
|
-
strokeWidth: 2,
|
|
1186
|
-
d: "M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"
|
|
1187
|
-
}
|
|
1188
|
-
) }),
|
|
1189
|
-
/* @__PURE__ */ jsx2("span", { children: isNumericColumn ? "1\u21929" : "A\u2192Z" })
|
|
1190
|
-
]
|
|
1191
|
-
}
|
|
1192
|
-
),
|
|
1193
|
-
/* @__PURE__ */ jsxs2(
|
|
1194
|
-
"button",
|
|
1195
|
-
{
|
|
1196
|
-
className: `vpg-sort-btn ${sortDirection === "desc" ? "active" : ""}`,
|
|
1197
|
-
title: isNumericColumn ? "Sort High to Low" : "Sort Z to A",
|
|
1198
|
-
onClick: sortDescending,
|
|
1199
|
-
children: [
|
|
1200
|
-
/* @__PURE__ */ jsx2("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
|
|
1201
|
-
"path",
|
|
1202
|
-
{
|
|
1203
|
-
strokeLinecap: "round",
|
|
1204
|
-
strokeLinejoin: "round",
|
|
1205
|
-
strokeWidth: 2,
|
|
1206
|
-
d: "M3 4h13M3 8h9m-9 4h9m5-4v12m0 0l-4-4m4 4l4-4"
|
|
1207
|
-
}
|
|
1208
|
-
) }),
|
|
1209
|
-
/* @__PURE__ */ jsx2("span", { children: isNumericColumn ? "9\u21921" : "Z\u2192A" })
|
|
1210
|
-
]
|
|
1211
|
-
}
|
|
1212
|
-
)
|
|
1213
|
-
] }),
|
|
1214
|
-
/* @__PURE__ */ jsx2("div", { className: "vpg-divider" }),
|
|
1215
|
-
isNumericColumn && /* @__PURE__ */ jsxs2("div", { className: "vpg-filter-tabs", children: [
|
|
1216
|
-
/* @__PURE__ */ jsxs2(
|
|
1217
|
-
"button",
|
|
1218
|
-
{
|
|
1219
|
-
className: `vpg-tab-btn ${filterMode === "values" ? "active" : ""}`,
|
|
1220
|
-
onClick: () => setFilterMode("values"),
|
|
1221
|
-
children: [
|
|
1222
|
-
/* @__PURE__ */ jsx2("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
|
|
1223
|
-
"path",
|
|
1224
|
-
{
|
|
1225
|
-
strokeLinecap: "round",
|
|
1226
|
-
strokeLinejoin: "round",
|
|
1227
|
-
strokeWidth: 2,
|
|
1228
|
-
d: "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
|
|
1229
|
-
}
|
|
1230
|
-
) }),
|
|
1231
|
-
"Values"
|
|
1232
|
-
]
|
|
1233
|
-
}
|
|
1234
|
-
),
|
|
1235
|
-
/* @__PURE__ */ jsxs2(
|
|
1236
|
-
"button",
|
|
1237
|
-
{
|
|
1238
|
-
className: `vpg-tab-btn ${filterMode === "range" ? "active" : ""}`,
|
|
1239
|
-
onClick: () => setFilterMode("range"),
|
|
1240
|
-
children: [
|
|
1241
|
-
/* @__PURE__ */ jsx2("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
|
|
1242
|
-
"path",
|
|
1243
|
-
{
|
|
1244
|
-
strokeLinecap: "round",
|
|
1245
|
-
strokeLinejoin: "round",
|
|
1246
|
-
strokeWidth: 2,
|
|
1247
|
-
d: "M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
|
|
1248
|
-
}
|
|
1249
|
-
) }),
|
|
1250
|
-
"Range"
|
|
1251
|
-
]
|
|
1252
|
-
}
|
|
1253
|
-
)
|
|
1254
|
-
] }),
|
|
1255
|
-
(!isNumericColumn || filterMode === "values") && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1256
|
-
/* @__PURE__ */ jsxs2("div", { className: "vpg-search-container", children: [
|
|
1257
|
-
/* @__PURE__ */ jsx2("svg", { className: "vpg-search-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
|
|
1258
|
-
"path",
|
|
1259
|
-
{
|
|
1260
|
-
strokeLinecap: "round",
|
|
1261
|
-
strokeLinejoin: "round",
|
|
1262
|
-
strokeWidth: 2,
|
|
1263
|
-
d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
1264
|
-
}
|
|
1265
|
-
) }),
|
|
1266
|
-
/* @__PURE__ */ jsx2(
|
|
1267
|
-
"input",
|
|
1268
|
-
{
|
|
1269
|
-
ref: searchInputRef,
|
|
1270
|
-
type: "text",
|
|
1271
|
-
value: searchQuery,
|
|
1272
|
-
onChange: (e) => setSearchQuery(e.target.value),
|
|
1273
|
-
placeholder: "Search values...",
|
|
1274
|
-
className: "vpg-search-input"
|
|
1275
|
-
}
|
|
1276
|
-
),
|
|
1277
|
-
searchQuery && /* @__PURE__ */ jsx2("button", { className: "vpg-clear-search", onClick: () => setSearchQuery(""), children: "\xD7" })
|
|
1278
|
-
] }),
|
|
1279
|
-
/* @__PURE__ */ jsxs2("div", { className: "vpg-bulk-actions", children: [
|
|
1280
|
-
/* @__PURE__ */ jsxs2("button", { className: "vpg-bulk-btn", onClick: selectAll, children: [
|
|
1281
|
-
/* @__PURE__ */ jsx2("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
|
|
1282
|
-
"path",
|
|
1283
|
-
{
|
|
1284
|
-
strokeLinecap: "round",
|
|
1285
|
-
strokeLinejoin: "round",
|
|
1286
|
-
strokeWidth: 2,
|
|
1287
|
-
d: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
1288
|
-
}
|
|
1289
|
-
) }),
|
|
1290
|
-
"Select All"
|
|
1291
|
-
] }),
|
|
1292
|
-
/* @__PURE__ */ jsxs2("button", { className: "vpg-bulk-btn", onClick: clearAll, children: [
|
|
1293
|
-
/* @__PURE__ */ jsx2("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx2(
|
|
697
|
+
/* @__PURE__ */ jsxs3("button", { className: "vpg-bulk-btn", onClick: clearAll, children: [
|
|
698
|
+
/* @__PURE__ */ jsx3("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3(
|
|
1294
699
|
"path",
|
|
1295
700
|
{
|
|
1296
701
|
strokeLinecap: "round",
|
|
@@ -1302,13 +707,13 @@ function ColumnFilter({
|
|
|
1302
707
|
"Clear All"
|
|
1303
708
|
] })
|
|
1304
709
|
] }),
|
|
1305
|
-
/* @__PURE__ */
|
|
1306
|
-
allValues.map((value) => /* @__PURE__ */
|
|
710
|
+
/* @__PURE__ */ jsxs3("div", { className: "vpg-values-list", children: [
|
|
711
|
+
allValues.map((value) => /* @__PURE__ */ jsxs3(
|
|
1307
712
|
"label",
|
|
1308
713
|
{
|
|
1309
714
|
className: `vpg-value-item ${localSelected.has(value) ? "selected" : ""}`,
|
|
1310
715
|
children: [
|
|
1311
|
-
/* @__PURE__ */
|
|
716
|
+
/* @__PURE__ */ jsx3(
|
|
1312
717
|
"input",
|
|
1313
718
|
{
|
|
1314
719
|
type: "checkbox",
|
|
@@ -1317,20 +722,20 @@ function ColumnFilter({
|
|
|
1317
722
|
className: "vpg-value-checkbox"
|
|
1318
723
|
}
|
|
1319
724
|
),
|
|
1320
|
-
/* @__PURE__ */
|
|
725
|
+
/* @__PURE__ */ jsx3("span", { className: `vpg-value-text ${value === "(blank)" ? "vpg-blank" : ""}`, children: value })
|
|
1321
726
|
]
|
|
1322
727
|
},
|
|
1323
728
|
value
|
|
1324
729
|
)),
|
|
1325
|
-
allValues.length === 0 && /* @__PURE__ */
|
|
730
|
+
allValues.length === 0 && /* @__PURE__ */ jsx3("div", { className: "vpg-no-results", children: "No matching values" })
|
|
1326
731
|
] }),
|
|
1327
|
-
/* @__PURE__ */
|
|
1328
|
-
/* @__PURE__ */
|
|
1329
|
-
/* @__PURE__ */
|
|
732
|
+
/* @__PURE__ */ jsxs3("div", { className: "vpg-filter-footer", children: [
|
|
733
|
+
/* @__PURE__ */ jsx3("button", { className: "vpg-btn-clear", onClick: clearFilter, children: "Clear Filter" }),
|
|
734
|
+
/* @__PURE__ */ jsx3("button", { className: "vpg-btn-apply", onClick: applyFilter, children: "Apply" })
|
|
1330
735
|
] })
|
|
1331
736
|
] }),
|
|
1332
|
-
isNumericColumn && filterMode === "range" && /* @__PURE__ */
|
|
1333
|
-
/* @__PURE__ */
|
|
737
|
+
isNumericColumn && filterMode === "range" && /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
738
|
+
/* @__PURE__ */ jsx3(
|
|
1334
739
|
NumericRangeFilter,
|
|
1335
740
|
{
|
|
1336
741
|
dataMin: stats.numericMin,
|
|
@@ -1339,210 +744,836 @@ function ColumnFilter({
|
|
|
1339
744
|
onChange: handleRangeChange
|
|
1340
745
|
}
|
|
1341
746
|
),
|
|
1342
|
-
/* @__PURE__ */
|
|
1343
|
-
/* @__PURE__ */
|
|
1344
|
-
/* @__PURE__ */
|
|
747
|
+
/* @__PURE__ */ jsxs3("div", { className: "vpg-filter-footer", children: [
|
|
748
|
+
/* @__PURE__ */ jsx3("button", { className: "vpg-btn-clear", onClick: clearRangeFilter, children: "Clear Filter" }),
|
|
749
|
+
/* @__PURE__ */ jsx3("button", { className: "vpg-btn-apply", onClick: applyRangeFilter, children: "Apply" })
|
|
1345
750
|
] })
|
|
1346
751
|
] })
|
|
1347
752
|
] });
|
|
1348
753
|
}
|
|
1349
754
|
|
|
1350
|
-
// src/components/
|
|
1351
|
-
import {
|
|
1352
|
-
import {
|
|
755
|
+
// src/components/DataGrid.tsx
|
|
756
|
+
import { useCallback as useCallback10, useEffect as useEffect7, useMemo as useMemo10, useRef as useRef2, useState as useState10 } from "react";
|
|
757
|
+
import { createPortal as createPortal2 } from "react-dom";
|
|
1353
758
|
|
|
1354
|
-
// src/
|
|
1355
|
-
import {
|
|
1356
|
-
import {
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
if (
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
759
|
+
// src/hooks/useExcelGrid.ts
|
|
760
|
+
import { formatCellValue, getColumnUniqueValues, isNumericRange } from "@smallwebco/tinypivot-core";
|
|
761
|
+
import {
|
|
762
|
+
getCoreRowModel,
|
|
763
|
+
getFilteredRowModel,
|
|
764
|
+
getSortedRowModel,
|
|
765
|
+
useReactTable
|
|
766
|
+
} from "@tanstack/react-table";
|
|
767
|
+
import { useCallback as useCallback4, useEffect as useEffect4, useMemo as useMemo4, useState as useState4 } from "react";
|
|
768
|
+
var multiSelectFilter = (row, columnId, filterValue) => {
|
|
769
|
+
if (!filterValue)
|
|
770
|
+
return true;
|
|
771
|
+
if (isNumericRange(filterValue)) {
|
|
772
|
+
const cellValue = row.getValue(columnId);
|
|
773
|
+
if (cellValue === null || cellValue === void 0 || cellValue === "") {
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
const num = typeof cellValue === "number" ? cellValue : Number.parseFloat(String(cellValue));
|
|
777
|
+
if (Number.isNaN(num))
|
|
778
|
+
return false;
|
|
779
|
+
const { min, max } = filterValue;
|
|
780
|
+
if (min !== null && num < min)
|
|
781
|
+
return false;
|
|
782
|
+
if (max !== null && num > max)
|
|
783
|
+
return false;
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
if (Array.isArray(filterValue) && filterValue.length > 0) {
|
|
787
|
+
const cellValue = row.getValue(columnId);
|
|
788
|
+
const cellString = cellValue === null || cellValue === void 0 || cellValue === "" ? "(blank)" : String(cellValue);
|
|
789
|
+
return filterValue.includes(cellString);
|
|
790
|
+
}
|
|
791
|
+
return true;
|
|
792
|
+
};
|
|
793
|
+
function useExcelGrid(options) {
|
|
794
|
+
const { data, enableSorting = true, enableFiltering = true } = options;
|
|
795
|
+
const [sorting, setSorting] = useState4([]);
|
|
796
|
+
const [columnFilters, setColumnFilters] = useState4([]);
|
|
797
|
+
const [columnVisibility, setColumnVisibility] = useState4({});
|
|
798
|
+
const [globalFilter, setGlobalFilter] = useState4("");
|
|
799
|
+
const [columnStatsCache, setColumnStatsCache] = useState4({});
|
|
800
|
+
const dataSignature = useMemo4(
|
|
801
|
+
() => `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
802
|
+
[data]
|
|
803
|
+
);
|
|
804
|
+
const columnKeys = useMemo4(() => {
|
|
805
|
+
if (data.length === 0)
|
|
806
|
+
return [];
|
|
807
|
+
return Object.keys(data[0]);
|
|
808
|
+
}, [data]);
|
|
809
|
+
const getColumnStats = useCallback4(
|
|
810
|
+
(columnKey) => {
|
|
811
|
+
const cacheKey = `${columnKey}-${dataSignature}`;
|
|
812
|
+
if (!columnStatsCache[cacheKey]) {
|
|
813
|
+
const stats = getColumnUniqueValues(data, columnKey);
|
|
814
|
+
setColumnStatsCache((prev) => ({ ...prev, [cacheKey]: stats }));
|
|
815
|
+
return stats;
|
|
816
|
+
}
|
|
817
|
+
return columnStatsCache[cacheKey];
|
|
818
|
+
},
|
|
819
|
+
[data, columnStatsCache, dataSignature]
|
|
820
|
+
);
|
|
821
|
+
const clearStatsCache = useCallback4(() => {
|
|
822
|
+
setColumnStatsCache({});
|
|
823
|
+
}, []);
|
|
824
|
+
useEffect4(() => {
|
|
825
|
+
clearStatsCache();
|
|
826
|
+
}, [dataSignature, clearStatsCache]);
|
|
827
|
+
const columnDefs = useMemo4(() => {
|
|
828
|
+
return columnKeys.map((key) => {
|
|
829
|
+
const stats = getColumnStats(key);
|
|
830
|
+
return {
|
|
831
|
+
id: key,
|
|
832
|
+
accessorKey: key,
|
|
833
|
+
header: key,
|
|
834
|
+
cell: (info) => formatCellValue(info.getValue(), stats.type),
|
|
835
|
+
filterFn: multiSelectFilter,
|
|
836
|
+
meta: {
|
|
837
|
+
type: stats.type,
|
|
838
|
+
uniqueCount: stats.uniqueValues.length
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
});
|
|
842
|
+
}, [columnKeys, getColumnStats]);
|
|
843
|
+
const table = useReactTable({
|
|
844
|
+
data,
|
|
845
|
+
columns: columnDefs,
|
|
846
|
+
state: {
|
|
847
|
+
sorting,
|
|
848
|
+
columnFilters,
|
|
849
|
+
columnVisibility,
|
|
850
|
+
globalFilter
|
|
851
|
+
},
|
|
852
|
+
onSortingChange: setSorting,
|
|
853
|
+
onColumnFiltersChange: setColumnFilters,
|
|
854
|
+
onColumnVisibilityChange: setColumnVisibility,
|
|
855
|
+
onGlobalFilterChange: setGlobalFilter,
|
|
856
|
+
getCoreRowModel: getCoreRowModel(),
|
|
857
|
+
getSortedRowModel: enableSorting ? getSortedRowModel() : void 0,
|
|
858
|
+
getFilteredRowModel: enableFiltering ? getFilteredRowModel() : void 0,
|
|
859
|
+
filterFns: {
|
|
860
|
+
multiSelect: multiSelectFilter
|
|
861
|
+
},
|
|
862
|
+
enableSorting,
|
|
863
|
+
enableFilters: enableFiltering
|
|
864
|
+
});
|
|
865
|
+
const filteredRowCount = table.getFilteredRowModel().rows.length;
|
|
866
|
+
const totalRowCount = data.length;
|
|
867
|
+
const activeFilters = useMemo4(() => {
|
|
868
|
+
return columnFilters.map((f) => {
|
|
869
|
+
const filterValue = f.value;
|
|
870
|
+
if (filterValue && isNumericRange(filterValue)) {
|
|
871
|
+
return {
|
|
872
|
+
column: f.id,
|
|
873
|
+
type: "range",
|
|
874
|
+
range: filterValue,
|
|
875
|
+
values: []
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
column: f.id,
|
|
880
|
+
type: "values",
|
|
881
|
+
values: Array.isArray(filterValue) ? filterValue : [],
|
|
882
|
+
range: null
|
|
883
|
+
};
|
|
884
|
+
});
|
|
885
|
+
}, [columnFilters]);
|
|
886
|
+
const hasActiveFilter = useCallback4(
|
|
887
|
+
(columnId) => {
|
|
888
|
+
const column = table.getColumn(columnId);
|
|
889
|
+
if (!column)
|
|
890
|
+
return false;
|
|
891
|
+
const filterValue = column.getFilterValue();
|
|
892
|
+
if (!filterValue)
|
|
893
|
+
return false;
|
|
894
|
+
if (isNumericRange(filterValue)) {
|
|
895
|
+
return filterValue.min !== null || filterValue.max !== null;
|
|
896
|
+
}
|
|
897
|
+
return Array.isArray(filterValue) && filterValue.length > 0;
|
|
898
|
+
},
|
|
899
|
+
[table]
|
|
900
|
+
);
|
|
901
|
+
const setColumnFilter = useCallback4(
|
|
902
|
+
(columnId, values) => {
|
|
903
|
+
const column = table.getColumn(columnId);
|
|
904
|
+
if (column) {
|
|
905
|
+
column.setFilterValue(values.length === 0 ? void 0 : values);
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
[table]
|
|
909
|
+
);
|
|
910
|
+
const setNumericRangeFilter = useCallback4(
|
|
911
|
+
(columnId, range) => {
|
|
912
|
+
const column = table.getColumn(columnId);
|
|
913
|
+
if (column) {
|
|
914
|
+
if (!range || range.min === null && range.max === null) {
|
|
915
|
+
column.setFilterValue(void 0);
|
|
916
|
+
} else {
|
|
917
|
+
column.setFilterValue(range);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
[table]
|
|
922
|
+
);
|
|
923
|
+
const getNumericRangeFilter = useCallback4(
|
|
924
|
+
(columnId) => {
|
|
925
|
+
const column = table.getColumn(columnId);
|
|
926
|
+
if (!column)
|
|
927
|
+
return null;
|
|
928
|
+
const filterValue = column.getFilterValue();
|
|
929
|
+
if (filterValue && isNumericRange(filterValue)) {
|
|
930
|
+
return filterValue;
|
|
931
|
+
}
|
|
932
|
+
return null;
|
|
933
|
+
},
|
|
934
|
+
[table]
|
|
935
|
+
);
|
|
936
|
+
const clearAllFilters = useCallback4(() => {
|
|
937
|
+
table.resetColumnFilters();
|
|
938
|
+
setGlobalFilter("");
|
|
939
|
+
setColumnFilters([]);
|
|
940
|
+
}, [table]);
|
|
941
|
+
const getColumnFilterValues = useCallback4(
|
|
942
|
+
(columnId) => {
|
|
943
|
+
const column = table.getColumn(columnId);
|
|
944
|
+
if (!column)
|
|
945
|
+
return [];
|
|
946
|
+
const filterValue = column.getFilterValue();
|
|
947
|
+
return Array.isArray(filterValue) ? filterValue : [];
|
|
948
|
+
},
|
|
949
|
+
[table]
|
|
950
|
+
);
|
|
951
|
+
const toggleSort = useCallback4((columnId) => {
|
|
952
|
+
setSorting((prev) => {
|
|
953
|
+
const current = prev.find((s) => s.id === columnId);
|
|
954
|
+
if (!current) {
|
|
955
|
+
return [{ id: columnId, desc: false }];
|
|
956
|
+
} else if (!current.desc) {
|
|
957
|
+
return [{ id: columnId, desc: true }];
|
|
958
|
+
} else {
|
|
959
|
+
return [];
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
}, []);
|
|
963
|
+
const getSortDirection = useCallback4(
|
|
964
|
+
(columnId) => {
|
|
965
|
+
const sort = sorting.find((s) => s.id === columnId);
|
|
966
|
+
if (!sort)
|
|
967
|
+
return null;
|
|
968
|
+
return sort.desc ? "desc" : "asc";
|
|
969
|
+
},
|
|
970
|
+
[sorting]
|
|
971
|
+
);
|
|
972
|
+
return {
|
|
973
|
+
// Table instance
|
|
974
|
+
table,
|
|
975
|
+
// State
|
|
976
|
+
sorting,
|
|
977
|
+
columnFilters,
|
|
978
|
+
columnVisibility,
|
|
979
|
+
globalFilter,
|
|
980
|
+
columnKeys,
|
|
981
|
+
setSorting,
|
|
982
|
+
setColumnFilters,
|
|
983
|
+
setGlobalFilter,
|
|
984
|
+
// Computed
|
|
985
|
+
filteredRowCount,
|
|
986
|
+
totalRowCount,
|
|
987
|
+
activeFilters,
|
|
988
|
+
// Methods
|
|
989
|
+
getColumnStats,
|
|
990
|
+
clearStatsCache,
|
|
991
|
+
hasActiveFilter,
|
|
992
|
+
setColumnFilter,
|
|
993
|
+
getColumnFilterValues,
|
|
994
|
+
clearAllFilters,
|
|
995
|
+
toggleSort,
|
|
996
|
+
getSortDirection,
|
|
997
|
+
// Numeric range filters
|
|
998
|
+
setNumericRangeFilter,
|
|
999
|
+
getNumericRangeFilter
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// src/hooks/useGridFeatures.ts
|
|
1004
|
+
import {
|
|
1005
|
+
copyToClipboard as coreCopyToClipboard,
|
|
1006
|
+
exportPivotToCSV as coreExportPivotToCSV,
|
|
1007
|
+
exportToCSV as coreExportToCSV,
|
|
1008
|
+
formatSelectionForClipboard as coreFormatSelection
|
|
1009
|
+
} from "@smallwebco/tinypivot-core";
|
|
1010
|
+
import { useCallback as useCallback5, useMemo as useMemo5, useState as useState5 } from "react";
|
|
1011
|
+
function exportToCSV(data, columns, options) {
|
|
1012
|
+
coreExportToCSV(data, columns, options);
|
|
1013
|
+
}
|
|
1014
|
+
function exportPivotToCSV(pivotData, rowFields, columnFields, valueFields, options) {
|
|
1015
|
+
coreExportPivotToCSV(pivotData, rowFields, columnFields, valueFields, options);
|
|
1016
|
+
}
|
|
1017
|
+
function copyToClipboard(text, onSuccess, onError) {
|
|
1018
|
+
coreCopyToClipboard(text, onSuccess, onError);
|
|
1019
|
+
}
|
|
1020
|
+
function formatSelectionForClipboard(rows, columns, selectionBounds) {
|
|
1021
|
+
return coreFormatSelection(rows, columns, selectionBounds);
|
|
1022
|
+
}
|
|
1023
|
+
function usePagination(data, options = {}) {
|
|
1024
|
+
const [pageSize, setPageSize] = useState5(options.pageSize ?? 50);
|
|
1025
|
+
const [currentPage, setCurrentPage] = useState5(options.currentPage ?? 1);
|
|
1026
|
+
const totalPages = useMemo5(
|
|
1027
|
+
() => Math.max(1, Math.ceil(data.length / pageSize)),
|
|
1028
|
+
[data.length, pageSize]
|
|
1029
|
+
);
|
|
1030
|
+
const paginatedData = useMemo5(() => {
|
|
1031
|
+
const start = (currentPage - 1) * pageSize;
|
|
1032
|
+
const end = start + pageSize;
|
|
1033
|
+
return data.slice(start, end);
|
|
1034
|
+
}, [data, currentPage, pageSize]);
|
|
1035
|
+
const startIndex = useMemo5(() => (currentPage - 1) * pageSize + 1, [currentPage, pageSize]);
|
|
1036
|
+
const endIndex = useMemo5(
|
|
1037
|
+
() => Math.min(currentPage * pageSize, data.length),
|
|
1038
|
+
[currentPage, pageSize, data.length]
|
|
1039
|
+
);
|
|
1040
|
+
const goToPage = useCallback5(
|
|
1041
|
+
(page) => {
|
|
1042
|
+
setCurrentPage(Math.max(1, Math.min(page, totalPages)));
|
|
1043
|
+
},
|
|
1044
|
+
[totalPages]
|
|
1045
|
+
);
|
|
1046
|
+
const nextPage = useCallback5(() => {
|
|
1047
|
+
if (currentPage < totalPages) {
|
|
1048
|
+
setCurrentPage((prev) => prev + 1);
|
|
1049
|
+
}
|
|
1050
|
+
}, [currentPage, totalPages]);
|
|
1051
|
+
const prevPage = useCallback5(() => {
|
|
1052
|
+
if (currentPage > 1) {
|
|
1053
|
+
setCurrentPage((prev) => prev - 1);
|
|
1054
|
+
}
|
|
1055
|
+
}, [currentPage]);
|
|
1056
|
+
const firstPage = useCallback5(() => {
|
|
1057
|
+
setCurrentPage(1);
|
|
1058
|
+
}, []);
|
|
1059
|
+
const lastPage = useCallback5(() => {
|
|
1060
|
+
setCurrentPage(totalPages);
|
|
1061
|
+
}, [totalPages]);
|
|
1062
|
+
const updatePageSize = useCallback5((size) => {
|
|
1063
|
+
setPageSize(size);
|
|
1064
|
+
setCurrentPage(1);
|
|
1065
|
+
}, []);
|
|
1066
|
+
return {
|
|
1067
|
+
pageSize,
|
|
1068
|
+
currentPage,
|
|
1069
|
+
totalPages,
|
|
1070
|
+
paginatedData,
|
|
1071
|
+
startIndex,
|
|
1072
|
+
endIndex,
|
|
1073
|
+
goToPage,
|
|
1074
|
+
nextPage,
|
|
1075
|
+
prevPage,
|
|
1076
|
+
firstPage,
|
|
1077
|
+
lastPage,
|
|
1078
|
+
setPageSize: updatePageSize
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
function useGlobalSearch(data, columns) {
|
|
1082
|
+
const [searchTerm, setSearchTerm] = useState5("");
|
|
1083
|
+
const [caseSensitive, setCaseSensitive] = useState5(false);
|
|
1084
|
+
const filteredData = useMemo5(() => {
|
|
1085
|
+
if (!searchTerm.trim()) {
|
|
1086
|
+
return data;
|
|
1087
|
+
}
|
|
1088
|
+
const term = caseSensitive ? searchTerm.trim() : searchTerm.trim().toLowerCase();
|
|
1089
|
+
return data.filter((row) => {
|
|
1090
|
+
for (const col of columns) {
|
|
1091
|
+
const value = row[col];
|
|
1092
|
+
if (value === null || value === void 0)
|
|
1093
|
+
continue;
|
|
1094
|
+
const strValue = caseSensitive ? String(value) : String(value).toLowerCase();
|
|
1095
|
+
if (strValue.includes(term)) {
|
|
1096
|
+
return true;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return false;
|
|
1100
|
+
});
|
|
1101
|
+
}, [data, columns, searchTerm, caseSensitive]);
|
|
1102
|
+
const clearSearch = useCallback5(() => {
|
|
1103
|
+
setSearchTerm("");
|
|
1104
|
+
}, []);
|
|
1105
|
+
return {
|
|
1106
|
+
searchTerm,
|
|
1107
|
+
setSearchTerm,
|
|
1108
|
+
caseSensitive,
|
|
1109
|
+
setCaseSensitive,
|
|
1110
|
+
filteredData,
|
|
1111
|
+
clearSearch
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
function useRowSelection(data) {
|
|
1115
|
+
const [selectedRowIndices, setSelectedRowIndices] = useState5(/* @__PURE__ */ new Set());
|
|
1116
|
+
const selectedRows = useMemo5(() => {
|
|
1117
|
+
return Array.from(selectedRowIndices).sort((a, b) => a - b).map((idx) => data[idx]).filter(Boolean);
|
|
1118
|
+
}, [data, selectedRowIndices]);
|
|
1119
|
+
const allSelected = useMemo5(() => {
|
|
1120
|
+
return data.length > 0 && selectedRowIndices.size === data.length;
|
|
1121
|
+
}, [data.length, selectedRowIndices.size]);
|
|
1122
|
+
const someSelected = useMemo5(() => {
|
|
1123
|
+
return selectedRowIndices.size > 0 && selectedRowIndices.size < data.length;
|
|
1124
|
+
}, [data.length, selectedRowIndices.size]);
|
|
1125
|
+
const toggleRow = useCallback5((index) => {
|
|
1126
|
+
setSelectedRowIndices((prev) => {
|
|
1127
|
+
const next = new Set(prev);
|
|
1128
|
+
if (next.has(index)) {
|
|
1129
|
+
next.delete(index);
|
|
1130
|
+
} else {
|
|
1131
|
+
next.add(index);
|
|
1132
|
+
}
|
|
1133
|
+
return next;
|
|
1134
|
+
});
|
|
1135
|
+
}, []);
|
|
1136
|
+
const selectRow = useCallback5((index) => {
|
|
1137
|
+
setSelectedRowIndices((prev) => /* @__PURE__ */ new Set([...prev, index]));
|
|
1138
|
+
}, []);
|
|
1139
|
+
const deselectRow = useCallback5((index) => {
|
|
1140
|
+
setSelectedRowIndices((prev) => {
|
|
1141
|
+
const next = new Set(prev);
|
|
1142
|
+
next.delete(index);
|
|
1143
|
+
return next;
|
|
1144
|
+
});
|
|
1145
|
+
}, []);
|
|
1146
|
+
const selectAll = useCallback5(() => {
|
|
1147
|
+
setSelectedRowIndices(new Set(data.map((_, idx) => idx)));
|
|
1148
|
+
}, [data]);
|
|
1149
|
+
const deselectAll = useCallback5(() => {
|
|
1150
|
+
setSelectedRowIndices(/* @__PURE__ */ new Set());
|
|
1151
|
+
}, []);
|
|
1152
|
+
const toggleAll = useCallback5(() => {
|
|
1153
|
+
if (allSelected) {
|
|
1154
|
+
deselectAll();
|
|
1155
|
+
} else {
|
|
1156
|
+
selectAll();
|
|
1157
|
+
}
|
|
1158
|
+
}, [allSelected, selectAll, deselectAll]);
|
|
1159
|
+
const isSelected = useCallback5(
|
|
1160
|
+
(index) => {
|
|
1161
|
+
return selectedRowIndices.has(index);
|
|
1162
|
+
},
|
|
1163
|
+
[selectedRowIndices]
|
|
1164
|
+
);
|
|
1165
|
+
const selectRange = useCallback5((startIndex, endIndex) => {
|
|
1166
|
+
const min = Math.min(startIndex, endIndex);
|
|
1167
|
+
const max = Math.max(startIndex, endIndex);
|
|
1168
|
+
setSelectedRowIndices((prev) => {
|
|
1169
|
+
const next = new Set(prev);
|
|
1170
|
+
for (let i = min; i <= max; i++) {
|
|
1171
|
+
next.add(i);
|
|
1172
|
+
}
|
|
1173
|
+
return next;
|
|
1174
|
+
});
|
|
1175
|
+
}, []);
|
|
1176
|
+
return {
|
|
1177
|
+
selectedRowIndices,
|
|
1178
|
+
selectedRows,
|
|
1179
|
+
allSelected,
|
|
1180
|
+
someSelected,
|
|
1181
|
+
toggleRow,
|
|
1182
|
+
selectRow,
|
|
1183
|
+
deselectRow,
|
|
1184
|
+
selectAll,
|
|
1185
|
+
deselectAll,
|
|
1186
|
+
toggleAll,
|
|
1187
|
+
isSelected,
|
|
1188
|
+
selectRange
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
function useColumnResize(initialWidths, minWidth = 60, maxWidth = 600) {
|
|
1192
|
+
const [columnWidths, setColumnWidths] = useState5({ ...initialWidths });
|
|
1193
|
+
const [isResizing, setIsResizing] = useState5(false);
|
|
1194
|
+
const [resizingColumn, setResizingColumn] = useState5(null);
|
|
1195
|
+
const startResize = useCallback5(
|
|
1196
|
+
(columnId, event) => {
|
|
1197
|
+
setIsResizing(true);
|
|
1198
|
+
setResizingColumn(columnId);
|
|
1199
|
+
const startX = event.clientX;
|
|
1200
|
+
const startWidth = columnWidths[columnId] || 150;
|
|
1201
|
+
const handleMouseMove = (e) => {
|
|
1202
|
+
const diff = e.clientX - startX;
|
|
1203
|
+
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + diff));
|
|
1204
|
+
setColumnWidths((prev) => ({
|
|
1205
|
+
...prev,
|
|
1206
|
+
[columnId]: newWidth
|
|
1207
|
+
}));
|
|
1208
|
+
};
|
|
1209
|
+
const handleMouseUp = () => {
|
|
1210
|
+
setIsResizing(false);
|
|
1211
|
+
setResizingColumn(null);
|
|
1212
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
1213
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
1214
|
+
};
|
|
1215
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
1216
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
1217
|
+
},
|
|
1218
|
+
[columnWidths, minWidth, maxWidth]
|
|
1219
|
+
);
|
|
1220
|
+
const resetColumnWidth = useCallback5(
|
|
1221
|
+
(columnId) => {
|
|
1222
|
+
if (initialWidths[columnId]) {
|
|
1223
|
+
setColumnWidths((prev) => ({
|
|
1224
|
+
...prev,
|
|
1225
|
+
[columnId]: initialWidths[columnId]
|
|
1226
|
+
}));
|
|
1227
|
+
}
|
|
1228
|
+
},
|
|
1229
|
+
[initialWidths]
|
|
1230
|
+
);
|
|
1231
|
+
const resetAllWidths = useCallback5(() => {
|
|
1232
|
+
setColumnWidths({ ...initialWidths });
|
|
1233
|
+
}, [initialWidths]);
|
|
1234
|
+
return {
|
|
1235
|
+
columnWidths,
|
|
1236
|
+
setColumnWidths,
|
|
1237
|
+
isResizing,
|
|
1238
|
+
resizingColumn,
|
|
1239
|
+
startResize,
|
|
1240
|
+
resetColumnWidth,
|
|
1241
|
+
resetAllWidths
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// src/hooks/useLicense.ts
|
|
1246
|
+
import {
|
|
1247
|
+
canUsePivot as coreCanUsePivot,
|
|
1248
|
+
configureLicenseSecret as coreConfigureLicenseSecret,
|
|
1249
|
+
isPro as coreIsPro,
|
|
1250
|
+
shouldShowWatermark as coreShouldShowWatermark,
|
|
1251
|
+
getDemoLicenseInfo,
|
|
1252
|
+
getFreeLicenseInfo,
|
|
1253
|
+
logProRequired,
|
|
1254
|
+
validateLicenseKey
|
|
1255
|
+
} from "@smallwebco/tinypivot-core";
|
|
1256
|
+
import { useCallback as useCallback6, useMemo as useMemo6, useState as useState6 } from "react";
|
|
1257
|
+
var globalLicenseInfo = getFreeLicenseInfo();
|
|
1258
|
+
var globalDemoMode = false;
|
|
1259
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
1260
|
+
function notifyListeners() {
|
|
1261
|
+
listeners.forEach((listener) => listener());
|
|
1262
|
+
}
|
|
1263
|
+
async function setLicenseKey(key) {
|
|
1264
|
+
globalLicenseInfo = await validateLicenseKey(key);
|
|
1265
|
+
if (!globalLicenseInfo.isValid) {
|
|
1266
|
+
console.warn("[TinyPivot] Invalid or expired license key. Running in free mode.");
|
|
1267
|
+
} else if (globalLicenseInfo.type !== "free") {
|
|
1268
|
+
console.info(`[TinyPivot] Pro license activated (${globalLicenseInfo.type})`);
|
|
1269
|
+
}
|
|
1270
|
+
notifyListeners();
|
|
1271
|
+
}
|
|
1272
|
+
async function enableDemoMode(secret) {
|
|
1273
|
+
const demoLicense = await getDemoLicenseInfo(secret);
|
|
1274
|
+
if (!demoLicense) {
|
|
1275
|
+
console.warn("[TinyPivot] Demo mode activation failed - invalid secret");
|
|
1276
|
+
return false;
|
|
1277
|
+
}
|
|
1278
|
+
globalDemoMode = true;
|
|
1279
|
+
globalLicenseInfo = demoLicense;
|
|
1280
|
+
console.info("[TinyPivot] Demo mode enabled - all Pro features unlocked for evaluation");
|
|
1281
|
+
notifyListeners();
|
|
1282
|
+
return true;
|
|
1283
|
+
}
|
|
1284
|
+
function configureLicenseSecret(secret) {
|
|
1285
|
+
coreConfigureLicenseSecret(secret);
|
|
1286
|
+
}
|
|
1287
|
+
function useLicense() {
|
|
1288
|
+
const [, forceUpdate] = useState6({});
|
|
1289
|
+
useState6(() => {
|
|
1290
|
+
const update = () => forceUpdate({});
|
|
1291
|
+
listeners.add(update);
|
|
1292
|
+
return () => listeners.delete(update);
|
|
1293
|
+
});
|
|
1294
|
+
const isDemo = globalDemoMode;
|
|
1295
|
+
const licenseInfo = globalLicenseInfo;
|
|
1296
|
+
const isPro = useMemo6(
|
|
1297
|
+
() => globalDemoMode || coreIsPro(licenseInfo),
|
|
1298
|
+
[licenseInfo]
|
|
1299
|
+
);
|
|
1300
|
+
const canUsePivot = useMemo6(
|
|
1301
|
+
() => globalDemoMode || coreCanUsePivot(licenseInfo),
|
|
1302
|
+
[licenseInfo]
|
|
1303
|
+
);
|
|
1304
|
+
const canUseAdvancedAggregations = useMemo6(
|
|
1305
|
+
() => globalDemoMode || licenseInfo.features.advancedAggregations,
|
|
1306
|
+
[licenseInfo]
|
|
1307
|
+
);
|
|
1308
|
+
const canUsePercentageMode = useMemo6(
|
|
1309
|
+
() => globalDemoMode || licenseInfo.features.percentageMode,
|
|
1310
|
+
[licenseInfo]
|
|
1311
|
+
);
|
|
1312
|
+
const showWatermark = useMemo6(
|
|
1313
|
+
() => coreShouldShowWatermark(licenseInfo, globalDemoMode),
|
|
1314
|
+
[licenseInfo]
|
|
1315
|
+
);
|
|
1316
|
+
const requirePro = useCallback6((feature) => {
|
|
1317
|
+
if (!isPro) {
|
|
1318
|
+
logProRequired(feature);
|
|
1319
|
+
return false;
|
|
1320
|
+
}
|
|
1321
|
+
return true;
|
|
1322
|
+
}, [isPro]);
|
|
1323
|
+
return {
|
|
1324
|
+
licenseInfo,
|
|
1325
|
+
isDemo,
|
|
1326
|
+
isPro,
|
|
1327
|
+
canUsePivot,
|
|
1328
|
+
canUseAdvancedAggregations,
|
|
1329
|
+
canUsePercentageMode,
|
|
1330
|
+
showWatermark,
|
|
1331
|
+
requirePro
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// src/hooks/usePivotTable.ts
|
|
1336
|
+
import {
|
|
1337
|
+
computeAvailableFields,
|
|
1338
|
+
computePivotResult,
|
|
1339
|
+
generateStorageKey,
|
|
1340
|
+
getAggregationLabel,
|
|
1341
|
+
getUnassignedFields,
|
|
1342
|
+
isConfigValidForFields,
|
|
1343
|
+
isPivotConfigured,
|
|
1344
|
+
loadCalculatedFields,
|
|
1345
|
+
loadPivotConfig,
|
|
1346
|
+
saveCalculatedFields,
|
|
1347
|
+
savePivotConfig
|
|
1348
|
+
} from "@smallwebco/tinypivot-core";
|
|
1349
|
+
import { useCallback as useCallback7, useEffect as useEffect5, useMemo as useMemo7, useState as useState7 } from "react";
|
|
1350
|
+
function usePivotTable(data) {
|
|
1351
|
+
const { canUsePivot, requirePro } = useLicense();
|
|
1352
|
+
const [rowFields, setRowFieldsState] = useState7([]);
|
|
1353
|
+
const [columnFields, setColumnFieldsState] = useState7([]);
|
|
1354
|
+
const [valueFields, setValueFields] = useState7([]);
|
|
1355
|
+
const [showRowTotals, setShowRowTotals] = useState7(true);
|
|
1356
|
+
const [showColumnTotals, setShowColumnTotals] = useState7(true);
|
|
1357
|
+
const [calculatedFields, setCalculatedFields] = useState7(() => loadCalculatedFields());
|
|
1358
|
+
const [currentStorageKey, setCurrentStorageKey] = useState7(null);
|
|
1359
|
+
const availableFields = useMemo7(() => {
|
|
1360
|
+
return computeAvailableFields(data);
|
|
1361
|
+
}, [data]);
|
|
1362
|
+
const unassignedFields = useMemo7(() => {
|
|
1363
|
+
return getUnassignedFields(availableFields, rowFields, columnFields, valueFields);
|
|
1364
|
+
}, [availableFields, rowFields, columnFields, valueFields]);
|
|
1365
|
+
const isConfigured = useMemo7(() => {
|
|
1366
|
+
return isPivotConfigured({
|
|
1367
|
+
rowFields,
|
|
1368
|
+
columnFields,
|
|
1369
|
+
valueFields,
|
|
1370
|
+
showRowTotals,
|
|
1371
|
+
showColumnTotals
|
|
1372
|
+
});
|
|
1373
|
+
}, [rowFields, columnFields, valueFields, showRowTotals, showColumnTotals]);
|
|
1374
|
+
const pivotResult = useMemo7(() => {
|
|
1375
|
+
if (!isConfigured)
|
|
1376
|
+
return null;
|
|
1377
|
+
if (!canUsePivot)
|
|
1378
|
+
return null;
|
|
1379
|
+
return computePivotResult(data, {
|
|
1380
|
+
rowFields,
|
|
1381
|
+
columnFields,
|
|
1382
|
+
valueFields,
|
|
1383
|
+
showRowTotals,
|
|
1384
|
+
showColumnTotals,
|
|
1385
|
+
calculatedFields
|
|
1386
|
+
});
|
|
1387
|
+
}, [data, isConfigured, canUsePivot, rowFields, columnFields, valueFields, showRowTotals, showColumnTotals, calculatedFields]);
|
|
1388
|
+
useEffect5(() => {
|
|
1389
|
+
if (data.length === 0)
|
|
1390
|
+
return;
|
|
1391
|
+
const newKeys = Object.keys(data[0]);
|
|
1392
|
+
const storageKey = generateStorageKey(newKeys);
|
|
1393
|
+
if (storageKey !== currentStorageKey) {
|
|
1394
|
+
setCurrentStorageKey(storageKey);
|
|
1395
|
+
const savedConfig = loadPivotConfig(storageKey);
|
|
1396
|
+
if (savedConfig && isConfigValidForFields(savedConfig, newKeys)) {
|
|
1397
|
+
setRowFieldsState(savedConfig.rowFields);
|
|
1398
|
+
setColumnFieldsState(savedConfig.columnFields);
|
|
1399
|
+
setValueFields(savedConfig.valueFields);
|
|
1400
|
+
setShowRowTotals(savedConfig.showRowTotals);
|
|
1401
|
+
setShowColumnTotals(savedConfig.showColumnTotals);
|
|
1402
|
+
if (savedConfig.calculatedFields) {
|
|
1403
|
+
setCalculatedFields(savedConfig.calculatedFields);
|
|
1404
|
+
}
|
|
1405
|
+
} else {
|
|
1406
|
+
const currentConfig = {
|
|
1407
|
+
rowFields,
|
|
1408
|
+
columnFields,
|
|
1409
|
+
valueFields,
|
|
1410
|
+
showRowTotals,
|
|
1411
|
+
showColumnTotals
|
|
1412
|
+
};
|
|
1413
|
+
if (!isConfigValidForFields(currentConfig, newKeys)) {
|
|
1414
|
+
setRowFieldsState([]);
|
|
1415
|
+
setColumnFieldsState([]);
|
|
1416
|
+
setValueFields([]);
|
|
1417
|
+
}
|
|
1383
1418
|
}
|
|
1384
|
-
setError(null);
|
|
1385
1419
|
}
|
|
1386
|
-
}, [
|
|
1387
|
-
|
|
1388
|
-
if (!
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1420
|
+
}, [data]);
|
|
1421
|
+
useEffect5(() => {
|
|
1422
|
+
if (!currentStorageKey)
|
|
1423
|
+
return;
|
|
1424
|
+
const config = {
|
|
1425
|
+
rowFields,
|
|
1426
|
+
columnFields,
|
|
1427
|
+
valueFields,
|
|
1428
|
+
showRowTotals,
|
|
1429
|
+
showColumnTotals,
|
|
1430
|
+
calculatedFields
|
|
1431
|
+
};
|
|
1432
|
+
savePivotConfig(currentStorageKey, config);
|
|
1433
|
+
}, [currentStorageKey, rowFields, columnFields, valueFields, showRowTotals, showColumnTotals, calculatedFields]);
|
|
1434
|
+
const addRowField = useCallback7(
|
|
1435
|
+
(field) => {
|
|
1436
|
+
if (!rowFields.includes(field)) {
|
|
1437
|
+
setRowFieldsState((prev) => [...prev, field]);
|
|
1395
1438
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1439
|
+
},
|
|
1440
|
+
[rowFields]
|
|
1441
|
+
);
|
|
1442
|
+
const removeRowField = useCallback7((field) => {
|
|
1443
|
+
setRowFieldsState((prev) => prev.filter((f) => f !== field));
|
|
1398
1444
|
}, []);
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1445
|
+
const setRowFields = useCallback7((fields) => {
|
|
1446
|
+
setRowFieldsState(fields);
|
|
1447
|
+
}, []);
|
|
1448
|
+
const addColumnField = useCallback7(
|
|
1449
|
+
(field) => {
|
|
1450
|
+
if (!columnFields.includes(field)) {
|
|
1451
|
+
setColumnFieldsState((prev) => [...prev, field]);
|
|
1452
|
+
}
|
|
1453
|
+
},
|
|
1454
|
+
[columnFields]
|
|
1455
|
+
);
|
|
1456
|
+
const removeColumnField = useCallback7((field) => {
|
|
1457
|
+
setColumnFieldsState((prev) => prev.filter((f) => f !== field));
|
|
1458
|
+
}, []);
|
|
1459
|
+
const setColumnFields = useCallback7((fields) => {
|
|
1460
|
+
setColumnFieldsState(fields);
|
|
1461
|
+
}, []);
|
|
1462
|
+
const addValueField = useCallback7(
|
|
1463
|
+
(field, aggregation = "sum") => {
|
|
1464
|
+
if (aggregation !== "sum" && !requirePro(`${aggregation} aggregation`)) {
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
setValueFields((prev) => {
|
|
1468
|
+
if (prev.some((v) => v.field === field && v.aggregation === aggregation)) {
|
|
1469
|
+
return prev;
|
|
1470
|
+
}
|
|
1471
|
+
return [...prev, { field, aggregation }];
|
|
1472
|
+
});
|
|
1473
|
+
},
|
|
1474
|
+
[requirePro]
|
|
1475
|
+
);
|
|
1476
|
+
const removeValueField = useCallback7((field, aggregation) => {
|
|
1477
|
+
setValueFields((prev) => {
|
|
1478
|
+
if (aggregation) {
|
|
1479
|
+
return prev.filter((v) => !(v.field === field && v.aggregation === aggregation));
|
|
1403
1480
|
}
|
|
1404
|
-
return prev
|
|
1481
|
+
return prev.filter((v) => v.field !== field);
|
|
1405
1482
|
});
|
|
1406
1483
|
}, []);
|
|
1407
|
-
const
|
|
1408
|
-
|
|
1409
|
-
|
|
1484
|
+
const updateValueFieldAggregation = useCallback7(
|
|
1485
|
+
(field, oldAgg, newAgg) => {
|
|
1486
|
+
setValueFields(
|
|
1487
|
+
(prev) => prev.map((v) => {
|
|
1488
|
+
if (v.field === field && v.aggregation === oldAgg) {
|
|
1489
|
+
return { ...v, aggregation: newAgg };
|
|
1490
|
+
}
|
|
1491
|
+
return v;
|
|
1492
|
+
})
|
|
1493
|
+
);
|
|
1494
|
+
},
|
|
1495
|
+
[]
|
|
1496
|
+
);
|
|
1497
|
+
const clearConfig = useCallback7(() => {
|
|
1498
|
+
setRowFieldsState([]);
|
|
1499
|
+
setColumnFieldsState([]);
|
|
1500
|
+
setValueFields([]);
|
|
1501
|
+
}, []);
|
|
1502
|
+
const autoSuggestConfig = useCallback7(() => {
|
|
1503
|
+
if (!requirePro("Pivot Table - Auto Suggest"))
|
|
1410
1504
|
return;
|
|
1411
|
-
|
|
1412
|
-
const validationResult = validateSimpleFormula(formula, availableFields);
|
|
1413
|
-
if (validationResult) {
|
|
1414
|
-
setError(validationResult);
|
|
1505
|
+
if (availableFields.length === 0)
|
|
1415
1506
|
return;
|
|
1507
|
+
const categoricalFields = availableFields.filter((f) => !f.isNumeric && f.uniqueCount < 50);
|
|
1508
|
+
const numericFields = availableFields.filter((f) => f.isNumeric);
|
|
1509
|
+
if (categoricalFields.length > 0 && numericFields.length > 0) {
|
|
1510
|
+
setRowFieldsState([categoricalFields[0].field]);
|
|
1511
|
+
setValueFields([{ field: numericFields[0].field, aggregation: "sum" }]);
|
|
1416
1512
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
] }),
|
|
1470
|
-
/* @__PURE__ */ jsxs3("div", { className: "vpg-form-group", children: [
|
|
1471
|
-
/* @__PURE__ */ jsx3("label", { className: "vpg-label-small", children: "Operators" }),
|
|
1472
|
-
/* @__PURE__ */ jsxs3("div", { className: "vpg-button-group", children: [
|
|
1473
|
-
/* @__PURE__ */ jsx3("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("+"), children: "+" }),
|
|
1474
|
-
/* @__PURE__ */ jsx3("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("-"), children: "\u2212" }),
|
|
1475
|
-
/* @__PURE__ */ jsx3("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("*"), children: "\xD7" }),
|
|
1476
|
-
/* @__PURE__ */ jsx3("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("/"), children: "\xF7" }),
|
|
1477
|
-
/* @__PURE__ */ jsx3("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("("), children: "(" }),
|
|
1478
|
-
/* @__PURE__ */ jsx3("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator(")"), children: ")" })
|
|
1479
|
-
] })
|
|
1480
|
-
] }),
|
|
1481
|
-
/* @__PURE__ */ jsxs3("div", { className: "vpg-form-group", children: [
|
|
1482
|
-
/* @__PURE__ */ jsx3("label", { className: "vpg-label-small", children: "Insert Field" }),
|
|
1483
|
-
availableFields.length > 0 ? /* @__PURE__ */ jsx3("div", { className: "vpg-button-group vpg-field-buttons", children: availableFields.map((field) => /* @__PURE__ */ jsx3(
|
|
1484
|
-
"button",
|
|
1485
|
-
{
|
|
1486
|
-
className: "vpg-insert-btn vpg-field-btn",
|
|
1487
|
-
onClick: () => insertField(field),
|
|
1488
|
-
children: field
|
|
1489
|
-
},
|
|
1490
|
-
field
|
|
1491
|
-
)) }) : /* @__PURE__ */ jsx3("div", { className: "vpg-no-fields", children: "No numeric fields available" })
|
|
1492
|
-
] }),
|
|
1493
|
-
/* @__PURE__ */ jsxs3("div", { className: "vpg-form-row", children: [
|
|
1494
|
-
/* @__PURE__ */ jsxs3("div", { className: "vpg-form-group vpg-form-group-half", children: [
|
|
1495
|
-
/* @__PURE__ */ jsx3("label", { className: "vpg-label", children: "Format As" }),
|
|
1496
|
-
/* @__PURE__ */ jsxs3(
|
|
1497
|
-
"select",
|
|
1498
|
-
{
|
|
1499
|
-
className: "vpg-select",
|
|
1500
|
-
value: formatAs,
|
|
1501
|
-
onChange: (e) => setFormatAs(e.target.value),
|
|
1502
|
-
children: [
|
|
1503
|
-
/* @__PURE__ */ jsx3("option", { value: "number", children: "Number" }),
|
|
1504
|
-
/* @__PURE__ */ jsx3("option", { value: "percent", children: "Percentage" }),
|
|
1505
|
-
/* @__PURE__ */ jsx3("option", { value: "currency", children: "Currency ($)" })
|
|
1506
|
-
]
|
|
1507
|
-
}
|
|
1508
|
-
)
|
|
1509
|
-
] }),
|
|
1510
|
-
/* @__PURE__ */ jsxs3("div", { className: "vpg-form-group vpg-form-group-half", children: [
|
|
1511
|
-
/* @__PURE__ */ jsx3("label", { className: "vpg-label", children: "Decimals" }),
|
|
1512
|
-
/* @__PURE__ */ jsx3(
|
|
1513
|
-
"input",
|
|
1514
|
-
{
|
|
1515
|
-
type: "number",
|
|
1516
|
-
className: "vpg-input",
|
|
1517
|
-
min: 0,
|
|
1518
|
-
max: 6,
|
|
1519
|
-
value: decimals,
|
|
1520
|
-
onChange: (e) => setDecimals(Number(e.target.value))
|
|
1521
|
-
}
|
|
1522
|
-
)
|
|
1523
|
-
] })
|
|
1524
|
-
] }),
|
|
1525
|
-
error && /* @__PURE__ */ jsx3("div", { className: "vpg-error vpg-error-box", children: error })
|
|
1526
|
-
] }),
|
|
1527
|
-
/* @__PURE__ */ jsxs3("div", { className: "vpg-modal-footer", children: [
|
|
1528
|
-
/* @__PURE__ */ jsx3("button", { className: "vpg-btn vpg-btn-secondary", onClick: onClose, children: "Cancel" }),
|
|
1529
|
-
/* @__PURE__ */ jsxs3("button", { className: "vpg-btn vpg-btn-primary", onClick: handleSave, children: [
|
|
1530
|
-
existingField ? "Update" : "Add",
|
|
1531
|
-
" Field"
|
|
1532
|
-
] })
|
|
1533
|
-
] })
|
|
1534
|
-
] }) });
|
|
1535
|
-
if (typeof document === "undefined") return null;
|
|
1536
|
-
return createPortal(modalContent, document.body);
|
|
1513
|
+
}, [availableFields, requirePro]);
|
|
1514
|
+
const addCalculatedField = useCallback7((field) => {
|
|
1515
|
+
setCalculatedFields((prev) => {
|
|
1516
|
+
const existing = prev.findIndex((f) => f.id === field.id);
|
|
1517
|
+
let updated;
|
|
1518
|
+
if (existing >= 0) {
|
|
1519
|
+
updated = [...prev.slice(0, existing), field, ...prev.slice(existing + 1)];
|
|
1520
|
+
} else {
|
|
1521
|
+
updated = [...prev, field];
|
|
1522
|
+
}
|
|
1523
|
+
saveCalculatedFields(updated);
|
|
1524
|
+
return updated;
|
|
1525
|
+
});
|
|
1526
|
+
}, []);
|
|
1527
|
+
const removeCalculatedField = useCallback7((id) => {
|
|
1528
|
+
setCalculatedFields((prev) => {
|
|
1529
|
+
const updated = prev.filter((f) => f.id !== id);
|
|
1530
|
+
saveCalculatedFields(updated);
|
|
1531
|
+
return updated;
|
|
1532
|
+
});
|
|
1533
|
+
setValueFields((prev) => prev.filter((v) => v.field !== `calc:${id}`));
|
|
1534
|
+
}, []);
|
|
1535
|
+
return {
|
|
1536
|
+
// State
|
|
1537
|
+
rowFields,
|
|
1538
|
+
columnFields,
|
|
1539
|
+
valueFields,
|
|
1540
|
+
showRowTotals,
|
|
1541
|
+
showColumnTotals,
|
|
1542
|
+
calculatedFields,
|
|
1543
|
+
// Computed
|
|
1544
|
+
availableFields,
|
|
1545
|
+
unassignedFields,
|
|
1546
|
+
isConfigured,
|
|
1547
|
+
pivotResult,
|
|
1548
|
+
// Actions
|
|
1549
|
+
addRowField,
|
|
1550
|
+
removeRowField,
|
|
1551
|
+
addColumnField,
|
|
1552
|
+
removeColumnField,
|
|
1553
|
+
addValueField,
|
|
1554
|
+
removeValueField,
|
|
1555
|
+
updateValueFieldAggregation,
|
|
1556
|
+
clearConfig,
|
|
1557
|
+
setShowRowTotals,
|
|
1558
|
+
setShowColumnTotals,
|
|
1559
|
+
autoSuggestConfig,
|
|
1560
|
+
setRowFields,
|
|
1561
|
+
setColumnFields,
|
|
1562
|
+
addCalculatedField,
|
|
1563
|
+
removeCalculatedField
|
|
1564
|
+
};
|
|
1537
1565
|
}
|
|
1538
1566
|
|
|
1539
1567
|
// src/components/PivotConfig.tsx
|
|
1568
|
+
import { AGGREGATION_OPTIONS, getAggregationSymbol } from "@smallwebco/tinypivot-core";
|
|
1569
|
+
import { useCallback as useCallback8, useMemo as useMemo8, useState as useState8 } from "react";
|
|
1540
1570
|
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1541
1571
|
function aggregationRequiresPro(agg) {
|
|
1542
1572
|
return agg !== "sum";
|
|
1543
1573
|
}
|
|
1544
1574
|
function getFieldIcon(type, isCalculated) {
|
|
1545
|
-
if (isCalculated)
|
|
1575
|
+
if (isCalculated)
|
|
1576
|
+
return "\u0192";
|
|
1546
1577
|
switch (type) {
|
|
1547
1578
|
case "number":
|
|
1548
1579
|
return "#";
|
|
@@ -1564,7 +1595,7 @@ function PivotConfig({
|
|
|
1564
1595
|
onShowRowTotalsChange,
|
|
1565
1596
|
onShowColumnTotalsChange,
|
|
1566
1597
|
onClearConfig,
|
|
1567
|
-
onAutoSuggest,
|
|
1598
|
+
onAutoSuggest: _onAutoSuggest,
|
|
1568
1599
|
onDragStart,
|
|
1569
1600
|
onDragEnd,
|
|
1570
1601
|
onUpdateAggregation,
|
|
@@ -1584,12 +1615,10 @@ function PivotConfig({
|
|
|
1584
1615
|
const isAggregationAvailable = useCallback8((agg) => {
|
|
1585
1616
|
return !aggregationRequiresPro(agg) || canUseAdvancedAggregations;
|
|
1586
1617
|
}, [canUseAdvancedAggregations]);
|
|
1587
|
-
const numericFieldNames = useMemo8(
|
|
1588
|
-
() => availableFields.filter((f) => f.isNumeric).map((f) => f.field),
|
|
1589
|
-
[availableFields]
|
|
1590
|
-
);
|
|
1618
|
+
const numericFieldNames = useMemo8(() => availableFields.filter((f) => f.isNumeric).map((f) => f.field), [availableFields]);
|
|
1591
1619
|
const calculatedFieldsAsStats = useMemo8(() => {
|
|
1592
|
-
if (!calculatedFields)
|
|
1620
|
+
if (!calculatedFields)
|
|
1621
|
+
return [];
|
|
1593
1622
|
return calculatedFields.map((calc) => ({
|
|
1594
1623
|
field: `calc:${calc.id}`,
|
|
1595
1624
|
type: "number",
|
|
@@ -1624,7 +1653,8 @@ function PivotConfig({
|
|
|
1624
1653
|
);
|
|
1625
1654
|
}, [allAvailableFields, rowFields, columnFields, valueFields]);
|
|
1626
1655
|
const filteredUnassignedFields = useMemo8(() => {
|
|
1627
|
-
if (!fieldSearch.trim())
|
|
1656
|
+
if (!fieldSearch.trim())
|
|
1657
|
+
return unassignedFields;
|
|
1628
1658
|
const search = fieldSearch.toLowerCase().trim();
|
|
1629
1659
|
return unassignedFields.filter((f) => {
|
|
1630
1660
|
const fieldName = f.field.toLowerCase();
|
|
@@ -1829,7 +1859,8 @@ function PivotConfig({
|
|
|
1829
1859
|
] }),
|
|
1830
1860
|
/* @__PURE__ */ jsxs4("div", { className: "vpg-unassigned-section", children: [
|
|
1831
1861
|
/* @__PURE__ */ jsx4("div", { className: "vpg-section-header", children: /* @__PURE__ */ jsxs4("div", { className: "vpg-section-label", children: [
|
|
1832
|
-
"Available
|
|
1862
|
+
"Available",
|
|
1863
|
+
" ",
|
|
1833
1864
|
/* @__PURE__ */ jsx4("span", { className: "vpg-count", children: unassignedFields.length })
|
|
1834
1865
|
] }) }),
|
|
1835
1866
|
/* @__PURE__ */ jsxs4("div", { className: "vpg-field-search", children: [
|
|
@@ -1883,7 +1914,8 @@ function PivotConfig({
|
|
|
1883
1914
|
onClick: (e) => {
|
|
1884
1915
|
e.stopPropagation();
|
|
1885
1916
|
const calcField = calculatedFields?.find((c) => c.id === field.calcId);
|
|
1886
|
-
if (calcField)
|
|
1917
|
+
if (calcField)
|
|
1918
|
+
openCalcModal(calcField);
|
|
1887
1919
|
},
|
|
1888
1920
|
children: "\u270E"
|
|
1889
1921
|
}
|
|
@@ -1946,8 +1978,8 @@ function PivotConfig({
|
|
|
1946
1978
|
}
|
|
1947
1979
|
|
|
1948
1980
|
// src/components/PivotSkeleton.tsx
|
|
1949
|
-
import { useState as useState9, useMemo as useMemo9, useCallback as useCallback9, useEffect as useEffect6 } from "react";
|
|
1950
1981
|
import { getAggregationLabel as getAggregationLabel2, getAggregationSymbol as getAggregationSymbol2 } from "@smallwebco/tinypivot-core";
|
|
1982
|
+
import { useCallback as useCallback9, useEffect as useEffect6, useMemo as useMemo9, useState as useState9 } from "react";
|
|
1951
1983
|
import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1952
1984
|
function PivotSkeleton({
|
|
1953
1985
|
rowFields,
|
|
@@ -2002,7 +2034,8 @@ function PivotSkeleton({
|
|
|
2002
2034
|
const [showCopyToast, setShowCopyToast] = useState9(false);
|
|
2003
2035
|
const [copyToastMessage, setCopyToastMessage] = useState9("");
|
|
2004
2036
|
const selectionBounds = useMemo9(() => {
|
|
2005
|
-
if (!selectionStart || !selectionEnd)
|
|
2037
|
+
if (!selectionStart || !selectionEnd)
|
|
2038
|
+
return null;
|
|
2006
2039
|
return {
|
|
2007
2040
|
minRow: Math.min(selectionStart.row, selectionEnd.row),
|
|
2008
2041
|
maxRow: Math.max(selectionStart.row, selectionEnd.row),
|
|
@@ -2048,7 +2081,8 @@ function PivotSkeleton({
|
|
|
2048
2081
|
return () => document.removeEventListener("mouseup", handleMouseUp);
|
|
2049
2082
|
}, []);
|
|
2050
2083
|
const sortedRowIndices = useMemo9(() => {
|
|
2051
|
-
if (!pivotResult)
|
|
2084
|
+
if (!pivotResult)
|
|
2085
|
+
return [];
|
|
2052
2086
|
const indices = pivotResult.rowHeaders.map((_, i) => i);
|
|
2053
2087
|
const headers = pivotResult.rowHeaders;
|
|
2054
2088
|
const data = pivotResult.data;
|
|
@@ -2062,9 +2096,12 @@ function PivotSkeleton({
|
|
|
2062
2096
|
const colIdx = sortTarget;
|
|
2063
2097
|
const aVal = data[a]?.[colIdx]?.value ?? null;
|
|
2064
2098
|
const bVal = data[b]?.[colIdx]?.value ?? null;
|
|
2065
|
-
if (aVal === null && bVal === null)
|
|
2066
|
-
|
|
2067
|
-
else if (
|
|
2099
|
+
if (aVal === null && bVal === null)
|
|
2100
|
+
cmp = 0;
|
|
2101
|
+
else if (aVal === null)
|
|
2102
|
+
cmp = 1;
|
|
2103
|
+
else if (bVal === null)
|
|
2104
|
+
cmp = -1;
|
|
2068
2105
|
else cmp = aVal - bVal;
|
|
2069
2106
|
}
|
|
2070
2107
|
return sortDirection === "asc" ? cmp : -cmp;
|
|
@@ -2072,12 +2109,14 @@ function PivotSkeleton({
|
|
|
2072
2109
|
return indices;
|
|
2073
2110
|
}, [pivotResult, sortTarget, sortDirection]);
|
|
2074
2111
|
const copySelectionToClipboard = useCallback9(() => {
|
|
2075
|
-
if (!selectionBounds || !pivotResult)
|
|
2112
|
+
if (!selectionBounds || !pivotResult)
|
|
2113
|
+
return;
|
|
2076
2114
|
const { minRow, maxRow, minCol, maxCol } = selectionBounds;
|
|
2077
2115
|
const lines = [];
|
|
2078
2116
|
for (let r = minRow; r <= maxRow; r++) {
|
|
2079
2117
|
const sortedIdx = sortedRowIndices[r];
|
|
2080
|
-
if (sortedIdx === void 0)
|
|
2118
|
+
if (sortedIdx === void 0)
|
|
2119
|
+
continue;
|
|
2081
2120
|
const rowValues = [];
|
|
2082
2121
|
for (let c = minCol; c <= maxCol; c++) {
|
|
2083
2122
|
const cell = pivotResult.data[sortedIdx]?.[c];
|
|
@@ -2097,7 +2136,8 @@ function PivotSkeleton({
|
|
|
2097
2136
|
}, [selectionBounds, pivotResult, sortedRowIndices]);
|
|
2098
2137
|
useEffect6(() => {
|
|
2099
2138
|
const handleKeydown = (event) => {
|
|
2100
|
-
if (!selectionBounds)
|
|
2139
|
+
if (!selectionBounds)
|
|
2140
|
+
return;
|
|
2101
2141
|
if ((event.ctrlKey || event.metaKey) && event.key === "c") {
|
|
2102
2142
|
event.preventDefault();
|
|
2103
2143
|
copySelectionToClipboard();
|
|
@@ -2113,13 +2153,15 @@ function PivotSkeleton({
|
|
|
2113
2153
|
return () => document.removeEventListener("keydown", handleKeydown);
|
|
2114
2154
|
}, [selectionBounds, copySelectionToClipboard]);
|
|
2115
2155
|
const selectionStats = useMemo9(() => {
|
|
2116
|
-
if (!selectionBounds || !pivotResult)
|
|
2156
|
+
if (!selectionBounds || !pivotResult)
|
|
2157
|
+
return null;
|
|
2117
2158
|
const { minRow, maxRow, minCol, maxCol } = selectionBounds;
|
|
2118
2159
|
const values = [];
|
|
2119
2160
|
let count = 0;
|
|
2120
2161
|
for (let r = minRow; r <= maxRow; r++) {
|
|
2121
2162
|
const sortedIdx = sortedRowIndices[r];
|
|
2122
|
-
if (sortedIdx === void 0)
|
|
2163
|
+
if (sortedIdx === void 0)
|
|
2164
|
+
continue;
|
|
2123
2165
|
for (let c = minCol; c <= maxCol; c++) {
|
|
2124
2166
|
const cell = pivotResult.data[sortedIdx]?.[c];
|
|
2125
2167
|
count++;
|
|
@@ -2128,7 +2170,8 @@ function PivotSkeleton({
|
|
|
2128
2170
|
}
|
|
2129
2171
|
}
|
|
2130
2172
|
}
|
|
2131
|
-
if (count <= 1)
|
|
2173
|
+
if (count <= 1)
|
|
2174
|
+
return null;
|
|
2132
2175
|
const sum = values.reduce((a, b) => a + b, 0);
|
|
2133
2176
|
const avg = values.length > 0 ? sum / values.length : 0;
|
|
2134
2177
|
return {
|
|
@@ -2139,8 +2182,10 @@ function PivotSkeleton({
|
|
|
2139
2182
|
};
|
|
2140
2183
|
}, [selectionBounds, pivotResult, sortedRowIndices]);
|
|
2141
2184
|
const formatStatValue = useCallback9((val) => {
|
|
2142
|
-
if (Math.abs(val) >= 1e6)
|
|
2143
|
-
|
|
2185
|
+
if (Math.abs(val) >= 1e6)
|
|
2186
|
+
return `${(val / 1e6).toFixed(2)}M`;
|
|
2187
|
+
if (Math.abs(val) >= 1e3)
|
|
2188
|
+
return `${(val / 1e3).toFixed(2)}K`;
|
|
2144
2189
|
return val.toFixed(2);
|
|
2145
2190
|
}, []);
|
|
2146
2191
|
const columnHeaderCells = useMemo9(() => {
|
|
@@ -2172,12 +2217,14 @@ function PivotSkeleton({
|
|
|
2172
2217
|
}, [pivotResult, valueFields]);
|
|
2173
2218
|
const hasActiveFilters = activeFilters && activeFilters.length > 0;
|
|
2174
2219
|
const filterSummary = useMemo9(() => {
|
|
2175
|
-
if (!activeFilters || activeFilters.length === 0)
|
|
2220
|
+
if (!activeFilters || activeFilters.length === 0)
|
|
2221
|
+
return "";
|
|
2176
2222
|
return activeFilters.map((f) => f.column).join(", ");
|
|
2177
2223
|
}, [activeFilters]);
|
|
2178
2224
|
const [showFilterTooltip, setShowFilterTooltip] = useState9(false);
|
|
2179
2225
|
const filterTooltipDetails = useMemo9(() => {
|
|
2180
|
-
if (!activeFilters || activeFilters.length === 0)
|
|
2226
|
+
if (!activeFilters || activeFilters.length === 0)
|
|
2227
|
+
return [];
|
|
2181
2228
|
return activeFilters.map((f) => {
|
|
2182
2229
|
if (f.isRange && f.displayText) {
|
|
2183
2230
|
return {
|
|
@@ -2219,10 +2266,13 @@ function PivotSkeleton({
|
|
|
2219
2266
|
setDragOverArea(null);
|
|
2220
2267
|
return;
|
|
2221
2268
|
}
|
|
2222
|
-
if (rowFields.includes(field))
|
|
2223
|
-
|
|
2269
|
+
if (rowFields.includes(field))
|
|
2270
|
+
onRemoveRowField(field);
|
|
2271
|
+
if (columnFields.includes(field))
|
|
2272
|
+
onRemoveColumnField(field);
|
|
2224
2273
|
const existingValue = valueFields.find((v) => v.field === field);
|
|
2225
|
-
if (existingValue)
|
|
2274
|
+
if (existingValue)
|
|
2275
|
+
onRemoveValueField(field, existingValue.aggregation);
|
|
2226
2276
|
switch (area) {
|
|
2227
2277
|
case "row":
|
|
2228
2278
|
onAddRowField(field);
|
|
@@ -2305,6 +2355,14 @@ function PivotSkeleton({
|
|
|
2305
2355
|
[reorderDropTarget]
|
|
2306
2356
|
);
|
|
2307
2357
|
const currentFontSize = fontSize;
|
|
2358
|
+
const rowHeaderWidth = 180;
|
|
2359
|
+
const rowHeaderColWidth = useMemo9(() => {
|
|
2360
|
+
const numCols = Math.max(rowFields.length, 1);
|
|
2361
|
+
return Math.max(rowHeaderWidth / numCols, 80);
|
|
2362
|
+
}, [rowFields.length]);
|
|
2363
|
+
const getRowHeaderLeftOffset = useCallback9((fieldIdx) => {
|
|
2364
|
+
return fieldIdx * rowHeaderColWidth;
|
|
2365
|
+
}, [rowHeaderColWidth]);
|
|
2308
2366
|
return /* @__PURE__ */ jsxs5(
|
|
2309
2367
|
"div",
|
|
2310
2368
|
{
|
|
@@ -2354,14 +2412,18 @@ function PivotSkeleton({
|
|
|
2354
2412
|
}
|
|
2355
2413
|
),
|
|
2356
2414
|
/* @__PURE__ */ jsxs5("span", { className: "vpg-filter-text", children: [
|
|
2357
|
-
"Filtered:
|
|
2415
|
+
"Filtered:",
|
|
2416
|
+
" ",
|
|
2358
2417
|
/* @__PURE__ */ jsx5("strong", { children: filterSummary }),
|
|
2359
2418
|
filteredRowCount !== void 0 && totalRowCount !== void 0 && /* @__PURE__ */ jsxs5("span", { className: "vpg-filter-count", children: [
|
|
2360
2419
|
"(",
|
|
2361
2420
|
filteredRowCount.toLocaleString(),
|
|
2362
|
-
"
|
|
2421
|
+
" ",
|
|
2422
|
+
"of",
|
|
2423
|
+
" ",
|
|
2363
2424
|
totalRowCount.toLocaleString(),
|
|
2364
|
-
"
|
|
2425
|
+
" ",
|
|
2426
|
+
"rows)"
|
|
2365
2427
|
] })
|
|
2366
2428
|
] }),
|
|
2367
2429
|
showFilterTooltip && /* @__PURE__ */ jsxs5("div", { className: "vpg-filter-tooltip", children: [
|
|
@@ -2373,16 +2435,21 @@ function PivotSkeleton({
|
|
|
2373
2435
|
filter.remaining > 0 && /* @__PURE__ */ jsxs5("span", { className: "vpg-tooltip-more", children: [
|
|
2374
2436
|
"+",
|
|
2375
2437
|
filter.remaining,
|
|
2376
|
-
"
|
|
2438
|
+
" ",
|
|
2439
|
+
"more"
|
|
2377
2440
|
] })
|
|
2378
2441
|
] }) })
|
|
2379
2442
|
] }, filter.column)),
|
|
2380
2443
|
filteredRowCount !== void 0 && totalRowCount !== void 0 && /* @__PURE__ */ jsxs5("div", { className: "vpg-tooltip-summary", children: [
|
|
2381
|
-
"Showing
|
|
2444
|
+
"Showing",
|
|
2445
|
+
" ",
|
|
2382
2446
|
filteredRowCount.toLocaleString(),
|
|
2383
|
-
"
|
|
2447
|
+
" ",
|
|
2448
|
+
"of",
|
|
2449
|
+
" ",
|
|
2384
2450
|
totalRowCount.toLocaleString(),
|
|
2385
|
-
"
|
|
2451
|
+
" ",
|
|
2452
|
+
"rows"
|
|
2386
2453
|
] })
|
|
2387
2454
|
] })
|
|
2388
2455
|
]
|
|
@@ -2391,17 +2458,20 @@ function PivotSkeleton({
|
|
|
2391
2458
|
isConfigured && /* @__PURE__ */ jsxs5("div", { className: "vpg-config-summary", children: [
|
|
2392
2459
|
/* @__PURE__ */ jsxs5("span", { className: "vpg-summary-badge vpg-rows", children: [
|
|
2393
2460
|
rowFields.length,
|
|
2394
|
-
"
|
|
2461
|
+
" ",
|
|
2462
|
+
"row",
|
|
2395
2463
|
rowFields.length !== 1 ? "s" : ""
|
|
2396
2464
|
] }),
|
|
2397
2465
|
/* @__PURE__ */ jsxs5("span", { className: "vpg-summary-badge vpg-cols", children: [
|
|
2398
2466
|
columnFields.length,
|
|
2399
|
-
"
|
|
2467
|
+
" ",
|
|
2468
|
+
"col",
|
|
2400
2469
|
columnFields.length !== 1 ? "s" : ""
|
|
2401
2470
|
] }),
|
|
2402
2471
|
/* @__PURE__ */ jsxs5("span", { className: "vpg-summary-badge vpg-vals", children: [
|
|
2403
2472
|
valueFields.length,
|
|
2404
|
-
"
|
|
2473
|
+
" ",
|
|
2474
|
+
"val",
|
|
2405
2475
|
valueFields.length !== 1 ? "s" : ""
|
|
2406
2476
|
] })
|
|
2407
2477
|
] })
|
|
@@ -2572,31 +2642,39 @@ function PivotSkeleton({
|
|
|
2572
2642
|
}
|
|
2573
2643
|
),
|
|
2574
2644
|
/* @__PURE__ */ jsx5("span", { className: "vpg-placeholder-text", children: valueFields.length === 0 ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
|
|
2575
|
-
"Add a
|
|
2645
|
+
"Add a",
|
|
2646
|
+
" ",
|
|
2576
2647
|
/* @__PURE__ */ jsx5("strong", { children: "Values" }),
|
|
2577
|
-
"
|
|
2648
|
+
" ",
|
|
2649
|
+
"field to see your pivot table"
|
|
2578
2650
|
] }) : rowFields.length === 0 && columnFields.length === 0 ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
|
|
2579
|
-
"Add
|
|
2651
|
+
"Add",
|
|
2652
|
+
" ",
|
|
2580
2653
|
/* @__PURE__ */ jsx5("strong", { children: "Row" }),
|
|
2581
|
-
"
|
|
2654
|
+
" ",
|
|
2655
|
+
"or",
|
|
2656
|
+
" ",
|
|
2582
2657
|
/* @__PURE__ */ jsx5("strong", { children: "Column" }),
|
|
2583
|
-
"
|
|
2658
|
+
" ",
|
|
2659
|
+
"fields to group your data"
|
|
2584
2660
|
] }) : "Your pivot table will appear here" })
|
|
2585
2661
|
] }) }),
|
|
2586
2662
|
isConfigured && pivotResult && /* @__PURE__ */ jsx5("div", { className: "vpg-table-container", children: /* @__PURE__ */ jsxs5("table", { className: "vpg-pivot-table", children: [
|
|
2587
2663
|
/* @__PURE__ */ jsx5("thead", { children: columnHeaderCells.map((headerRow, levelIdx) => /* @__PURE__ */ jsxs5("tr", { className: "vpg-column-header-row", children: [
|
|
2588
|
-
levelIdx === 0 && /* @__PURE__ */ jsx5(
|
|
2664
|
+
levelIdx === 0 && (rowFields.length > 0 ? rowFields : ["Rows"]).map((field, fieldIdx) => /* @__PURE__ */ jsx5(
|
|
2589
2665
|
"th",
|
|
2590
2666
|
{
|
|
2591
2667
|
className: "vpg-row-header-label",
|
|
2592
2668
|
rowSpan: columnHeaderCells.length,
|
|
2669
|
+
style: { width: `${rowHeaderColWidth}px`, minWidth: "80px", left: `${getRowHeaderLeftOffset(fieldIdx)}px` },
|
|
2593
2670
|
onClick: () => toggleSort("row"),
|
|
2594
2671
|
children: /* @__PURE__ */ jsxs5("div", { className: "vpg-header-content", children: [
|
|
2595
|
-
/* @__PURE__ */ jsx5("span", { children:
|
|
2596
|
-
/* @__PURE__ */ jsx5("span", { className: `vpg-sort-indicator ${sortTarget === "row" ? "active" : ""}`, children: sortTarget === "row" ? sortDirection === "asc" ? "\u2191" : "\u2193" : "\u21C5" })
|
|
2672
|
+
/* @__PURE__ */ jsx5("span", { children: field }),
|
|
2673
|
+
(fieldIdx === rowFields.length - 1 || rowFields.length === 0) && /* @__PURE__ */ jsx5("span", { className: `vpg-sort-indicator ${sortTarget === "row" ? "active" : ""}`, children: sortTarget === "row" ? sortDirection === "asc" ? "\u2191" : "\u2193" : "\u21C5" })
|
|
2597
2674
|
] })
|
|
2598
|
-
}
|
|
2599
|
-
|
|
2675
|
+
},
|
|
2676
|
+
`row-header-${fieldIdx}`
|
|
2677
|
+
)),
|
|
2600
2678
|
headerRow.map((cell, idx) => /* @__PURE__ */ jsx5(
|
|
2601
2679
|
"th",
|
|
2602
2680
|
{
|
|
@@ -2614,7 +2692,15 @@ function PivotSkeleton({
|
|
|
2614
2692
|
] }, `header-${levelIdx}`)) }),
|
|
2615
2693
|
/* @__PURE__ */ jsxs5("tbody", { children: [
|
|
2616
2694
|
sortedRowIndices.map((sortedIdx) => /* @__PURE__ */ jsxs5("tr", { className: "vpg-data-row", children: [
|
|
2617
|
-
|
|
2695
|
+
pivotResult.rowHeaders[sortedIdx].map((val, idx) => /* @__PURE__ */ jsx5(
|
|
2696
|
+
"th",
|
|
2697
|
+
{
|
|
2698
|
+
className: "vpg-row-header-cell",
|
|
2699
|
+
style: { width: `${rowHeaderColWidth}px`, minWidth: "80px", left: `${getRowHeaderLeftOffset(idx)}px` },
|
|
2700
|
+
children: val
|
|
2701
|
+
},
|
|
2702
|
+
`row-${sortedIdx}-${idx}`
|
|
2703
|
+
)),
|
|
2618
2704
|
pivotResult.data[sortedIdx].map((cell, colIdx) => {
|
|
2619
2705
|
const displayRowIdx = sortedRowIndices.indexOf(sortedIdx);
|
|
2620
2706
|
return /* @__PURE__ */ jsx5(
|
|
@@ -2631,7 +2717,15 @@ function PivotSkeleton({
|
|
|
2631
2717
|
pivotResult.rowTotals[sortedIdx] && /* @__PURE__ */ jsx5("td", { className: "vpg-data-cell vpg-total-cell", children: pivotResult.rowTotals[sortedIdx].formattedValue })
|
|
2632
2718
|
] }, sortedIdx)),
|
|
2633
2719
|
pivotResult.columnTotals.length > 0 && /* @__PURE__ */ jsxs5("tr", { className: "vpg-totals-row", children: [
|
|
2634
|
-
/* @__PURE__ */ jsx5(
|
|
2720
|
+
/* @__PURE__ */ jsx5(
|
|
2721
|
+
"th",
|
|
2722
|
+
{
|
|
2723
|
+
className: "vpg-row-header-cell vpg-total-label",
|
|
2724
|
+
colSpan: Math.max(rowFields.length, 1),
|
|
2725
|
+
style: { width: `${rowHeaderWidth}px` },
|
|
2726
|
+
children: "Total"
|
|
2727
|
+
}
|
|
2728
|
+
),
|
|
2635
2729
|
pivotResult.columnTotals.map((cell, colIdx) => /* @__PURE__ */ jsx5("td", { className: "vpg-data-cell vpg-total-cell", children: cell.formattedValue }, colIdx)),
|
|
2636
2730
|
pivotResult.rowTotals.length > 0 && /* @__PURE__ */ jsx5("td", { className: "vpg-data-cell vpg-grand-total-cell", children: pivotResult.grandTotal.formattedValue })
|
|
2637
2731
|
] })
|
|
@@ -2640,9 +2734,11 @@ function PivotSkeleton({
|
|
|
2640
2734
|
isConfigured && pivotResult && /* @__PURE__ */ jsxs5("div", { className: "vpg-skeleton-footer", children: [
|
|
2641
2735
|
/* @__PURE__ */ jsxs5("span", { className: "vpg-footer-info", children: [
|
|
2642
2736
|
pivotResult.rowHeaders.length,
|
|
2643
|
-
"
|
|
2737
|
+
" ",
|
|
2738
|
+
"rows \xD7",
|
|
2644
2739
|
pivotResult.data[0]?.length || 0,
|
|
2645
|
-
"
|
|
2740
|
+
" ",
|
|
2741
|
+
"columns"
|
|
2646
2742
|
] }),
|
|
2647
2743
|
selectionStats && selectionStats.count > 1 && /* @__PURE__ */ jsxs5("div", { className: "vpg-selection-stats", children: [
|
|
2648
2744
|
/* @__PURE__ */ jsxs5("span", { className: "vpg-stat", children: [
|
|
@@ -2795,12 +2891,15 @@ function DataGrid({
|
|
|
2795
2891
|
removeCalculatedField
|
|
2796
2892
|
} = usePivotTable(filteredDataForPivot);
|
|
2797
2893
|
const activeFilterInfo = useMemo10(() => {
|
|
2798
|
-
if (activeFilters.length === 0)
|
|
2894
|
+
if (activeFilters.length === 0)
|
|
2895
|
+
return null;
|
|
2799
2896
|
return activeFilters.map((f) => {
|
|
2800
2897
|
if (f.type === "range" && f.range) {
|
|
2801
2898
|
const parts = [];
|
|
2802
|
-
if (f.range.min !== null)
|
|
2803
|
-
|
|
2899
|
+
if (f.range.min !== null)
|
|
2900
|
+
parts.push(`\u2265 ${f.range.min}`);
|
|
2901
|
+
if (f.range.max !== null)
|
|
2902
|
+
parts.push(`\u2264 ${f.range.max}`);
|
|
2804
2903
|
return {
|
|
2805
2904
|
column: f.column,
|
|
2806
2905
|
valueCount: 1,
|
|
@@ -2825,7 +2924,8 @@ function DataGrid({
|
|
|
2825
2924
|
return rows.filter((row) => {
|
|
2826
2925
|
for (const col of columnKeys) {
|
|
2827
2926
|
const value = row.original[col];
|
|
2828
|
-
if (value === null || value === void 0)
|
|
2927
|
+
if (value === null || value === void 0)
|
|
2928
|
+
continue;
|
|
2829
2929
|
if (String(value).toLowerCase().includes(term)) {
|
|
2830
2930
|
return true;
|
|
2831
2931
|
}
|
|
@@ -2835,11 +2935,13 @@ function DataGrid({
|
|
|
2835
2935
|
}, [rows, globalSearchTerm, enableSearch, columnKeys]);
|
|
2836
2936
|
const totalSearchedRows = searchFilteredData.length;
|
|
2837
2937
|
const totalPages = useMemo10(() => {
|
|
2838
|
-
if (!enablePagination)
|
|
2938
|
+
if (!enablePagination)
|
|
2939
|
+
return 1;
|
|
2839
2940
|
return Math.max(1, Math.ceil(totalSearchedRows / pageSize));
|
|
2840
2941
|
}, [enablePagination, totalSearchedRows, pageSize]);
|
|
2841
2942
|
const paginatedRows = useMemo10(() => {
|
|
2842
|
-
if (!enablePagination)
|
|
2943
|
+
if (!enablePagination)
|
|
2944
|
+
return searchFilteredData;
|
|
2843
2945
|
const start = (currentPage - 1) * pageSize;
|
|
2844
2946
|
const end = start + pageSize;
|
|
2845
2947
|
return searchFilteredData.slice(start, end);
|
|
@@ -2848,7 +2950,8 @@ function DataGrid({
|
|
|
2848
2950
|
setCurrentPage(1);
|
|
2849
2951
|
}, [columnFilters, globalSearchTerm]);
|
|
2850
2952
|
const selectionBounds = useMemo10(() => {
|
|
2851
|
-
if (!selectionStart || !selectionEnd)
|
|
2953
|
+
if (!selectionStart || !selectionEnd)
|
|
2954
|
+
return null;
|
|
2852
2955
|
return {
|
|
2853
2956
|
minRow: Math.min(selectionStart.row, selectionEnd.row),
|
|
2854
2957
|
maxRow: Math.max(selectionStart.row, selectionEnd.row),
|
|
@@ -2857,16 +2960,19 @@ function DataGrid({
|
|
|
2857
2960
|
};
|
|
2858
2961
|
}, [selectionStart, selectionEnd]);
|
|
2859
2962
|
const selectionStats = useMemo10(() => {
|
|
2860
|
-
if (!selectionBounds)
|
|
2963
|
+
if (!selectionBounds)
|
|
2964
|
+
return null;
|
|
2861
2965
|
const { minRow, maxRow, minCol, maxCol } = selectionBounds;
|
|
2862
2966
|
const values = [];
|
|
2863
2967
|
let count = 0;
|
|
2864
2968
|
for (let r = minRow; r <= maxRow; r++) {
|
|
2865
2969
|
const row = rows[r];
|
|
2866
|
-
if (!row)
|
|
2970
|
+
if (!row)
|
|
2971
|
+
continue;
|
|
2867
2972
|
for (let c = minCol; c <= maxCol; c++) {
|
|
2868
2973
|
const colId = columnKeys[c];
|
|
2869
|
-
if (!colId)
|
|
2974
|
+
if (!colId)
|
|
2975
|
+
continue;
|
|
2870
2976
|
const value = row.original[colId];
|
|
2871
2977
|
count++;
|
|
2872
2978
|
if (value !== null && value !== void 0 && value !== "") {
|
|
@@ -2877,19 +2983,23 @@ function DataGrid({
|
|
|
2877
2983
|
}
|
|
2878
2984
|
}
|
|
2879
2985
|
}
|
|
2880
|
-
if (values.length === 0)
|
|
2986
|
+
if (values.length === 0)
|
|
2987
|
+
return { count, sum: null, avg: null, numericCount: 0 };
|
|
2881
2988
|
const sum = values.reduce((a, b) => a + b, 0);
|
|
2882
2989
|
const avg = sum / values.length;
|
|
2883
2990
|
return { count, sum, avg, numericCount: values.length };
|
|
2884
2991
|
}, [selectionBounds, rows, columnKeys]);
|
|
2885
2992
|
useEffect7(() => {
|
|
2886
|
-
if (typeof document === "undefined")
|
|
2887
|
-
|
|
2993
|
+
if (typeof document === "undefined")
|
|
2994
|
+
return;
|
|
2995
|
+
if (data.length === 0)
|
|
2996
|
+
return;
|
|
2888
2997
|
const widths = {};
|
|
2889
2998
|
const sampleSize = Math.min(100, data.length);
|
|
2890
2999
|
const canvas = document.createElement("canvas");
|
|
2891
3000
|
const ctx = canvas.getContext("2d");
|
|
2892
|
-
if (!ctx)
|
|
3001
|
+
if (!ctx)
|
|
3002
|
+
return;
|
|
2893
3003
|
ctx.font = "13px system-ui, -apple-system, sans-serif";
|
|
2894
3004
|
for (const key of columnKeys) {
|
|
2895
3005
|
let maxWidth = ctx.measureText(key).width + 56;
|
|
@@ -2905,7 +3015,8 @@ function DataGrid({
|
|
|
2905
3015
|
}, [data, columnKeys]);
|
|
2906
3016
|
const startColumnResize = useCallback10(
|
|
2907
3017
|
(columnId, event) => {
|
|
2908
|
-
if (!enableColumnResize)
|
|
3018
|
+
if (!enableColumnResize)
|
|
3019
|
+
return;
|
|
2909
3020
|
event.preventDefault();
|
|
2910
3021
|
event.stopPropagation();
|
|
2911
3022
|
setResizingColumnId(columnId);
|
|
@@ -2915,7 +3026,8 @@ function DataGrid({
|
|
|
2915
3026
|
[enableColumnResize, columnWidths]
|
|
2916
3027
|
);
|
|
2917
3028
|
useEffect7(() => {
|
|
2918
|
-
if (!resizingColumnId)
|
|
3029
|
+
if (!resizingColumnId)
|
|
3030
|
+
return;
|
|
2919
3031
|
const handleResizeMove = (event) => {
|
|
2920
3032
|
const diff = event.clientX - resizeStartX;
|
|
2921
3033
|
const newWidth = Math.max(MIN_COL_WIDTH, Math.min(MAX_COL_WIDTH, resizeStartWidth + diff));
|
|
@@ -2936,7 +3048,8 @@ function DataGrid({
|
|
|
2936
3048
|
}, [resizingColumnId, resizeStartX, resizeStartWidth]);
|
|
2937
3049
|
const startVerticalResize = useCallback10(
|
|
2938
3050
|
(event) => {
|
|
2939
|
-
if (!enableVerticalResize)
|
|
3051
|
+
if (!enableVerticalResize)
|
|
3052
|
+
return;
|
|
2940
3053
|
event.preventDefault();
|
|
2941
3054
|
setIsResizingVertically(true);
|
|
2942
3055
|
setVerticalResizeStartY(event.clientY);
|
|
@@ -2945,7 +3058,8 @@ function DataGrid({
|
|
|
2945
3058
|
[enableVerticalResize, gridHeight]
|
|
2946
3059
|
);
|
|
2947
3060
|
useEffect7(() => {
|
|
2948
|
-
if (!isResizingVertically)
|
|
3061
|
+
if (!isResizingVertically)
|
|
3062
|
+
return;
|
|
2949
3063
|
const handleVerticalResizeMove = (event) => {
|
|
2950
3064
|
const diff = event.clientY - verticalResizeStartY;
|
|
2951
3065
|
const newHeight = Math.max(minHeight, Math.min(maxHeight, verticalResizeStartHeight + diff));
|
|
@@ -2963,7 +3077,8 @@ function DataGrid({
|
|
|
2963
3077
|
}, [isResizingVertically, verticalResizeStartY, verticalResizeStartHeight, minHeight, maxHeight]);
|
|
2964
3078
|
const handleExport = useCallback10(() => {
|
|
2965
3079
|
if (viewMode === "pivot") {
|
|
2966
|
-
if (!pivotResult)
|
|
3080
|
+
if (!pivotResult)
|
|
3081
|
+
return;
|
|
2967
3082
|
const pivotFilename = exportFilename.replace(".csv", "-pivot.csv");
|
|
2968
3083
|
exportPivotToCSV(
|
|
2969
3084
|
{
|
|
@@ -3007,7 +3122,8 @@ function DataGrid({
|
|
|
3007
3122
|
onExport
|
|
3008
3123
|
]);
|
|
3009
3124
|
const copySelectionToClipboard = useCallback10(() => {
|
|
3010
|
-
if (!selectionBounds || !enableClipboard)
|
|
3125
|
+
if (!selectionBounds || !enableClipboard)
|
|
3126
|
+
return;
|
|
3011
3127
|
const text = formatSelectionForClipboard(
|
|
3012
3128
|
rows.map((r) => r.original),
|
|
3013
3129
|
columnKeys,
|
|
@@ -3164,7 +3280,8 @@ function DataGrid({
|
|
|
3164
3280
|
[selectionBounds, selectedCell]
|
|
3165
3281
|
);
|
|
3166
3282
|
const formatStatValue = (value) => {
|
|
3167
|
-
if (value === null)
|
|
3283
|
+
if (value === null)
|
|
3284
|
+
return "-";
|
|
3168
3285
|
if (Math.abs(value) >= 1e3) {
|
|
3169
3286
|
return value.toLocaleString("en-US", { maximumFractionDigits: 2 });
|
|
3170
3287
|
}
|
|
@@ -3175,12 +3292,15 @@ function DataGrid({
|
|
|
3175
3292
|
return !noFormatPatterns.test(columnId);
|
|
3176
3293
|
};
|
|
3177
3294
|
const formatCellValueDisplay = (value, columnId) => {
|
|
3178
|
-
if (value === null || value === void 0)
|
|
3179
|
-
|
|
3295
|
+
if (value === null || value === void 0)
|
|
3296
|
+
return "";
|
|
3297
|
+
if (value === "")
|
|
3298
|
+
return "";
|
|
3180
3299
|
const stats = getColumnStats(columnId);
|
|
3181
3300
|
if (stats.type === "number") {
|
|
3182
3301
|
const num = typeof value === "number" ? value : Number.parseFloat(String(value));
|
|
3183
|
-
if (Number.isNaN(num))
|
|
3302
|
+
if (Number.isNaN(num))
|
|
3303
|
+
return String(value);
|
|
3184
3304
|
if (shouldFormatNumber(columnId) && Math.abs(num) >= 1e3) {
|
|
3185
3305
|
return num.toLocaleString("en-US", { maximumFractionDigits: 2 });
|
|
3186
3306
|
}
|
|
@@ -3342,13 +3462,15 @@ function DataGrid({
|
|
|
3342
3462
|
) }),
|
|
3343
3463
|
/* @__PURE__ */ jsxs6("span", { children: [
|
|
3344
3464
|
activeFilterCount,
|
|
3345
|
-
"
|
|
3465
|
+
" ",
|
|
3466
|
+
"filter",
|
|
3346
3467
|
activeFilterCount > 1 ? "s" : ""
|
|
3347
3468
|
] })
|
|
3348
3469
|
] }),
|
|
3349
3470
|
globalSearchTerm && /* @__PURE__ */ jsx6("div", { className: "vpg-search-info", children: /* @__PURE__ */ jsxs6("span", { children: [
|
|
3350
3471
|
totalSearchedRows,
|
|
3351
|
-
"
|
|
3472
|
+
" ",
|
|
3473
|
+
"match",
|
|
3352
3474
|
totalSearchedRows !== 1 ? "es" : ""
|
|
3353
3475
|
] }) })
|
|
3354
3476
|
] }),
|
|
@@ -3369,7 +3491,8 @@ function DataGrid({
|
|
|
3369
3491
|
}
|
|
3370
3492
|
) }),
|
|
3371
3493
|
showPivotConfig ? "Hide" : "Show",
|
|
3372
|
-
"
|
|
3494
|
+
" ",
|
|
3495
|
+
"Config"
|
|
3373
3496
|
]
|
|
3374
3497
|
}
|
|
3375
3498
|
),
|
|
@@ -3491,7 +3614,7 @@ function DataGrid({
|
|
|
3491
3614
|
/* @__PURE__ */ jsx6("button", { className: "vpg-clear-link", onClick: clearAllFilters, children: "Clear all filters" })
|
|
3492
3615
|
] }),
|
|
3493
3616
|
!loading && filteredRowCount > 0 && /* @__PURE__ */ jsx6("div", { className: "vpg-table-wrapper", children: /* @__PURE__ */ jsxs6("table", { className: "vpg-table", style: { minWidth: `${totalTableWidth}px` }, children: [
|
|
3494
|
-
/* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsx6("tr", { children: columnKeys.map((colId
|
|
3617
|
+
/* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsx6("tr", { children: columnKeys.map((colId) => /* @__PURE__ */ jsxs6(
|
|
3495
3618
|
"th",
|
|
3496
3619
|
{
|
|
3497
3620
|
className: `vpg-header-cell ${hasActiveFilter(colId) ? "vpg-has-filter" : ""} ${getSortDirection(colId) !== null ? "vpg-is-sorted" : ""} ${activeFilterColumn === colId ? "vpg-is-active" : ""}`,
|
|
@@ -3620,7 +3743,7 @@ function DataGrid({
|
|
|
3620
3743
|
onShowColumnTotalsChange: setPivotShowColumnTotals,
|
|
3621
3744
|
onClearConfig: clearPivotConfig,
|
|
3622
3745
|
onAutoSuggest: autoSuggestConfig,
|
|
3623
|
-
onDragStart: (field,
|
|
3746
|
+
onDragStart: (field, _e) => setDraggingField(field),
|
|
3624
3747
|
onDragEnd: () => setDraggingField(null),
|
|
3625
3748
|
onUpdateAggregation: updateValueFieldAggregation,
|
|
3626
3749
|
onAddRowField: addRowField,
|
|
@@ -3672,11 +3795,13 @@ function DataGrid({
|
|
|
3672
3795
|
totalSearchedRows !== totalRowCount && /* @__PURE__ */ jsxs6("span", { className: "vpg-filtered-note", children: [
|
|
3673
3796
|
"(",
|
|
3674
3797
|
totalRowCount.toLocaleString(),
|
|
3675
|
-
"
|
|
3798
|
+
" ",
|
|
3799
|
+
"total)"
|
|
3676
3800
|
] })
|
|
3677
3801
|
] }) : filteredRowCount === totalRowCount && totalSearchedRows === totalRowCount ? /* @__PURE__ */ jsxs6("span", { children: [
|
|
3678
3802
|
totalRowCount.toLocaleString(),
|
|
3679
|
-
"
|
|
3803
|
+
" ",
|
|
3804
|
+
"records"
|
|
3680
3805
|
] }) : /* @__PURE__ */ jsxs6(Fragment4, { children: [
|
|
3681
3806
|
/* @__PURE__ */ jsx6("span", { className: "vpg-filtered-count", children: totalSearchedRows.toLocaleString() }),
|
|
3682
3807
|
/* @__PURE__ */ jsx6("span", { className: "vpg-separator", children: "of" }),
|
|
@@ -3687,7 +3812,8 @@ function DataGrid({
|
|
|
3687
3812
|
/* @__PURE__ */ jsx6("span", { className: "vpg-separator", children: "\u2022" }),
|
|
3688
3813
|
/* @__PURE__ */ jsxs6("span", { children: [
|
|
3689
3814
|
totalRowCount.toLocaleString(),
|
|
3690
|
-
"
|
|
3815
|
+
" ",
|
|
3816
|
+
"source records"
|
|
3691
3817
|
] })
|
|
3692
3818
|
] }) }),
|
|
3693
3819
|
enablePagination && viewMode === "grid" && totalPages > 1 && /* @__PURE__ */ jsxs6("div", { className: "vpg-pagination", children: [
|
|
@@ -3726,9 +3852,12 @@ function DataGrid({
|
|
|
3726
3852
|
}
|
|
3727
3853
|
),
|
|
3728
3854
|
/* @__PURE__ */ jsxs6("span", { className: "vpg-page-info", children: [
|
|
3729
|
-
"Page
|
|
3855
|
+
"Page",
|
|
3856
|
+
" ",
|
|
3730
3857
|
currentPage,
|
|
3731
|
-
"
|
|
3858
|
+
" ",
|
|
3859
|
+
"of",
|
|
3860
|
+
" ",
|
|
3732
3861
|
totalPages
|
|
3733
3862
|
] }),
|
|
3734
3863
|
/* @__PURE__ */ jsx6(
|