@smallwebco/tinypivot-react 1.0.49 → 1.0.51
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 +1615 -1512
- 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 +1604 -1501
- package/dist/index.js.map +1 -1
- package/package.json +39 -39
package/dist/index.cjs
CHANGED
|
@@ -29,10 +29,10 @@ __export(index_exports, {
|
|
|
29
29
|
enableDemoMode: () => enableDemoMode,
|
|
30
30
|
exportPivotToCSV: () => exportPivotToCSV,
|
|
31
31
|
exportToCSV: () => exportToCSV,
|
|
32
|
-
formatCellValue: () =>
|
|
32
|
+
formatCellValue: () => import_tinypivot_core2.formatCellValue,
|
|
33
33
|
formatSelectionForClipboard: () => formatSelectionForClipboard,
|
|
34
|
-
getAggregationLabel: () =>
|
|
35
|
-
getColumnUniqueValues: () =>
|
|
34
|
+
getAggregationLabel: () => import_tinypivot_core5.getAggregationLabel,
|
|
35
|
+
getColumnUniqueValues: () => import_tinypivot_core2.getColumnUniqueValues,
|
|
36
36
|
setLicenseKey: () => setLicenseKey,
|
|
37
37
|
useColumnResize: () => useColumnResize,
|
|
38
38
|
useExcelGrid: () => useExcelGrid,
|
|
@@ -44,591 +44,1058 @@ __export(index_exports, {
|
|
|
44
44
|
});
|
|
45
45
|
module.exports = __toCommonJS(index_exports);
|
|
46
46
|
|
|
47
|
-
// src/components/
|
|
48
|
-
var import_react10 = require("react");
|
|
49
|
-
var import_react_dom2 = require("react-dom");
|
|
50
|
-
|
|
51
|
-
// src/hooks/useExcelGrid.ts
|
|
52
|
-
var import_react = require("react");
|
|
53
|
-
var import_react_table = require("@tanstack/react-table");
|
|
47
|
+
// src/components/CalculatedFieldModal.tsx
|
|
54
48
|
var import_tinypivot_core = require("@smallwebco/tinypivot-core");
|
|
55
|
-
var
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
var import_react = require("react");
|
|
50
|
+
var import_react_dom = require("react-dom");
|
|
51
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
52
|
+
function CalculatedFieldModal({
|
|
53
|
+
show,
|
|
54
|
+
availableFields,
|
|
55
|
+
existingField,
|
|
56
|
+
onClose,
|
|
57
|
+
onSave
|
|
58
|
+
}) {
|
|
59
|
+
const [name, setName] = (0, import_react.useState)("");
|
|
60
|
+
const [formula, setFormula] = (0, import_react.useState)("");
|
|
61
|
+
const [formatAs, setFormatAs] = (0, import_react.useState)("number");
|
|
62
|
+
const [decimals, setDecimals] = (0, import_react.useState)(2);
|
|
63
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
64
|
+
(0, import_react.useEffect)(() => {
|
|
65
|
+
if (show) {
|
|
66
|
+
if (existingField) {
|
|
67
|
+
setName(existingField.name);
|
|
68
|
+
setFormula(existingField.formula);
|
|
69
|
+
setFormatAs(existingField.formatAs || "number");
|
|
70
|
+
setDecimals(existingField.decimals ?? 2);
|
|
71
|
+
} else {
|
|
72
|
+
setName("");
|
|
73
|
+
setFormula("");
|
|
74
|
+
setFormatAs("number");
|
|
75
|
+
setDecimals(2);
|
|
76
|
+
}
|
|
77
|
+
setError(null);
|
|
61
78
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return filterValue.includes(cellString);
|
|
73
|
-
}
|
|
74
|
-
return true;
|
|
75
|
-
};
|
|
76
|
-
function useExcelGrid(options) {
|
|
77
|
-
const { data, enableSorting = true, enableFiltering = true } = options;
|
|
78
|
-
const [sorting, setSorting] = (0, import_react.useState)([]);
|
|
79
|
-
const [columnFilters, setColumnFilters] = (0, import_react.useState)([]);
|
|
80
|
-
const [columnVisibility, setColumnVisibility] = (0, import_react.useState)({});
|
|
81
|
-
const [globalFilter, setGlobalFilter] = (0, import_react.useState)("");
|
|
82
|
-
const [columnStatsCache, setColumnStatsCache] = (0, import_react.useState)({});
|
|
83
|
-
const dataSignature = (0, import_react.useMemo)(
|
|
84
|
-
() => `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
85
|
-
[data]
|
|
86
|
-
);
|
|
87
|
-
const columnKeys = (0, import_react.useMemo)(() => {
|
|
88
|
-
if (data.length === 0) return [];
|
|
89
|
-
return Object.keys(data[0]);
|
|
90
|
-
}, [data]);
|
|
91
|
-
const getColumnStats = (0, import_react.useCallback)(
|
|
92
|
-
(columnKey) => {
|
|
93
|
-
const cacheKey = `${columnKey}-${dataSignature}`;
|
|
94
|
-
if (!columnStatsCache[cacheKey]) {
|
|
95
|
-
const stats = (0, import_tinypivot_core.getColumnUniqueValues)(data, columnKey);
|
|
96
|
-
setColumnStatsCache((prev) => ({ ...prev, [cacheKey]: stats }));
|
|
97
|
-
return stats;
|
|
79
|
+
}, [show, existingField]);
|
|
80
|
+
const validationError = (0, import_react.useMemo)(() => {
|
|
81
|
+
if (!formula.trim())
|
|
82
|
+
return null;
|
|
83
|
+
return (0, import_tinypivot_core.validateSimpleFormula)(formula, availableFields);
|
|
84
|
+
}, [formula, availableFields]);
|
|
85
|
+
const insertField = (0, import_react.useCallback)((field) => {
|
|
86
|
+
setFormula((prev) => {
|
|
87
|
+
if (prev.trim() && !prev.endsWith(" ")) {
|
|
88
|
+
return `${prev} ${field}`;
|
|
98
89
|
}
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
[data, columnStatsCache, dataSignature]
|
|
102
|
-
);
|
|
103
|
-
const clearStatsCache = (0, import_react.useCallback)(() => {
|
|
104
|
-
setColumnStatsCache({});
|
|
90
|
+
return prev + field;
|
|
91
|
+
});
|
|
105
92
|
}, []);
|
|
106
|
-
(0, import_react.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
id: key,
|
|
114
|
-
accessorKey: key,
|
|
115
|
-
header: key,
|
|
116
|
-
cell: (info) => (0, import_tinypivot_core.formatCellValue)(info.getValue(), stats.type),
|
|
117
|
-
filterFn: multiSelectFilter,
|
|
118
|
-
meta: {
|
|
119
|
-
type: stats.type,
|
|
120
|
-
uniqueCount: stats.uniqueValues.length
|
|
121
|
-
}
|
|
122
|
-
};
|
|
93
|
+
const insertOperator = (0, import_react.useCallback)((op) => {
|
|
94
|
+
setFormula((prev) => {
|
|
95
|
+
if (prev.trim() && !prev.endsWith(" ")) {
|
|
96
|
+
return `${prev} ${op} `;
|
|
97
|
+
}
|
|
98
|
+
return `${prev + op} `;
|
|
123
99
|
});
|
|
124
|
-
}, [
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
100
|
+
}, []);
|
|
101
|
+
const handleSave = (0, import_react.useCallback)(() => {
|
|
102
|
+
if (!name.trim()) {
|
|
103
|
+
setError("Name is required");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const validationResult = (0, import_tinypivot_core.validateSimpleFormula)(formula, availableFields);
|
|
107
|
+
if (validationResult) {
|
|
108
|
+
setError(validationResult);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const field = {
|
|
112
|
+
id: existingField?.id || `calc_${Date.now()}`,
|
|
113
|
+
name: name.trim(),
|
|
114
|
+
formula: formula.trim(),
|
|
115
|
+
formatAs,
|
|
116
|
+
decimals
|
|
117
|
+
};
|
|
118
|
+
onSave(field);
|
|
119
|
+
onClose();
|
|
120
|
+
}, [name, formula, formatAs, decimals, existingField, availableFields, onSave, onClose]);
|
|
121
|
+
const handleOverlayClick = (0, import_react.useCallback)((e) => {
|
|
122
|
+
if (e.target === e.currentTarget) {
|
|
123
|
+
onClose();
|
|
124
|
+
}
|
|
125
|
+
}, [onClose]);
|
|
126
|
+
if (!show)
|
|
127
|
+
return null;
|
|
128
|
+
const modalContent = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vpg-modal-overlay", onClick: handleOverlayClick, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-modal", children: [
|
|
129
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-modal-header", children: [
|
|
130
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h3", { children: [
|
|
131
|
+
existingField ? "Edit" : "Create",
|
|
132
|
+
" ",
|
|
133
|
+
"Calculated Field"
|
|
134
|
+
] }),
|
|
135
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "vpg-modal-close", onClick: onClose, children: "\xD7" })
|
|
136
|
+
] }),
|
|
137
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-modal-body", children: [
|
|
138
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-form-group", children: [
|
|
139
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "vpg-label", children: "Name" }),
|
|
140
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
141
|
+
"input",
|
|
142
|
+
{
|
|
143
|
+
type: "text",
|
|
144
|
+
className: "vpg-input",
|
|
145
|
+
placeholder: "e.g., Profit Margin %",
|
|
146
|
+
value: name,
|
|
147
|
+
onChange: (e) => setName(e.target.value)
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
] }),
|
|
151
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-form-group", children: [
|
|
152
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "vpg-label", children: "Formula" }),
|
|
153
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
154
|
+
"textarea",
|
|
155
|
+
{
|
|
156
|
+
className: "vpg-textarea",
|
|
157
|
+
placeholder: "e.g., revenue / units",
|
|
158
|
+
rows: 2,
|
|
159
|
+
value: formula,
|
|
160
|
+
onChange: (e) => setFormula(e.target.value)
|
|
161
|
+
}
|
|
162
|
+
),
|
|
163
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vpg-formula-hint", children: "Use field names with math operators: + - * / ( )" }),
|
|
164
|
+
validationError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vpg-error", children: validationError })
|
|
165
|
+
] }),
|
|
166
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-form-group", children: [
|
|
167
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "vpg-label-small", children: "Operators" }),
|
|
168
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-button-group", children: [
|
|
169
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("+"), children: "+" }),
|
|
170
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("-"), children: "\u2212" }),
|
|
171
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("*"), children: "\xD7" }),
|
|
172
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("/"), children: "\xF7" }),
|
|
173
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("("), children: "(" }),
|
|
174
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator(")"), children: ")" })
|
|
175
|
+
] })
|
|
176
|
+
] }),
|
|
177
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-form-group", children: [
|
|
178
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "vpg-label-small", children: "Insert Field" }),
|
|
179
|
+
availableFields.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vpg-button-group vpg-field-buttons", children: availableFields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
180
|
+
"button",
|
|
181
|
+
{
|
|
182
|
+
className: "vpg-insert-btn vpg-field-btn",
|
|
183
|
+
onClick: () => insertField(field),
|
|
184
|
+
children: field
|
|
185
|
+
},
|
|
186
|
+
field
|
|
187
|
+
)) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vpg-no-fields", children: "No numeric fields available" })
|
|
188
|
+
] }),
|
|
189
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-form-row", children: [
|
|
190
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-form-group vpg-form-group-half", children: [
|
|
191
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "vpg-label", children: "Format As" }),
|
|
192
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
193
|
+
"select",
|
|
194
|
+
{
|
|
195
|
+
className: "vpg-select",
|
|
196
|
+
value: formatAs,
|
|
197
|
+
onChange: (e) => setFormatAs(e.target.value),
|
|
198
|
+
children: [
|
|
199
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "number", children: "Number" }),
|
|
200
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "percent", children: "Percentage" }),
|
|
201
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "currency", children: "Currency ($)" })
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
] }),
|
|
206
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-form-group vpg-form-group-half", children: [
|
|
207
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "vpg-label", children: "Decimals" }),
|
|
208
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
209
|
+
"input",
|
|
210
|
+
{
|
|
211
|
+
type: "number",
|
|
212
|
+
className: "vpg-input",
|
|
213
|
+
min: 0,
|
|
214
|
+
max: 6,
|
|
215
|
+
value: decimals,
|
|
216
|
+
onChange: (e) => setDecimals(Number(e.target.value))
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
] })
|
|
220
|
+
] }),
|
|
221
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vpg-error vpg-error-box", children: error })
|
|
222
|
+
] }),
|
|
223
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-modal-footer", children: [
|
|
224
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "vpg-btn vpg-btn-secondary", onClick: onClose, children: "Cancel" }),
|
|
225
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { className: "vpg-btn vpg-btn-primary", onClick: handleSave, children: [
|
|
226
|
+
existingField ? "Update" : "Add",
|
|
227
|
+
" ",
|
|
228
|
+
"Field"
|
|
229
|
+
] })
|
|
230
|
+
] })
|
|
231
|
+
] }) });
|
|
232
|
+
if (typeof document === "undefined")
|
|
233
|
+
return null;
|
|
234
|
+
return (0, import_react_dom.createPortal)(modalContent, document.body);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/components/ColumnFilter.tsx
|
|
238
|
+
var import_react3 = require("react");
|
|
239
|
+
|
|
240
|
+
// src/components/NumericRangeFilter.tsx
|
|
241
|
+
var import_react2 = require("react");
|
|
242
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
243
|
+
function NumericRangeFilter({
|
|
244
|
+
dataMin,
|
|
245
|
+
dataMax,
|
|
246
|
+
currentRange,
|
|
247
|
+
onChange
|
|
248
|
+
}) {
|
|
249
|
+
const [localMin, setLocalMin] = (0, import_react2.useState)(currentRange?.min ?? null);
|
|
250
|
+
const [localMax, setLocalMax] = (0, import_react2.useState)(currentRange?.max ?? null);
|
|
251
|
+
const step = (0, import_react2.useMemo)(() => {
|
|
252
|
+
const range = dataMax - dataMin;
|
|
253
|
+
if (range === 0)
|
|
254
|
+
return 1;
|
|
255
|
+
if (range <= 1)
|
|
256
|
+
return 0.01;
|
|
257
|
+
if (range <= 10)
|
|
258
|
+
return 0.1;
|
|
259
|
+
if (range <= 100)
|
|
260
|
+
return 1;
|
|
261
|
+
if (range <= 1e3)
|
|
262
|
+
return 10;
|
|
263
|
+
return 10 ** (Math.floor(Math.log10(range)) - 2);
|
|
264
|
+
}, [dataMin, dataMax]);
|
|
265
|
+
const formatValue = (0, import_react2.useCallback)((val) => {
|
|
266
|
+
if (val === null)
|
|
267
|
+
return "";
|
|
268
|
+
if (Number.isInteger(val))
|
|
269
|
+
return val.toLocaleString();
|
|
270
|
+
return val.toLocaleString(void 0, { maximumFractionDigits: 2 });
|
|
271
|
+
}, []);
|
|
272
|
+
const isFilterActive = localMin !== null || localMax !== null;
|
|
273
|
+
const minPercent = (0, import_react2.useMemo)(() => {
|
|
274
|
+
if (localMin === null || dataMax === dataMin)
|
|
275
|
+
return 0;
|
|
276
|
+
return (localMin - dataMin) / (dataMax - dataMin) * 100;
|
|
277
|
+
}, [localMin, dataMin, dataMax]);
|
|
278
|
+
const maxPercent = (0, import_react2.useMemo)(() => {
|
|
279
|
+
if (localMax === null || dataMax === dataMin)
|
|
280
|
+
return 100;
|
|
281
|
+
return (localMax - dataMin) / (dataMax - dataMin) * 100;
|
|
282
|
+
}, [localMax, dataMin, dataMax]);
|
|
283
|
+
const handleMinSlider = (0, import_react2.useCallback)((event) => {
|
|
284
|
+
const value = Number.parseFloat(event.target.value);
|
|
285
|
+
setLocalMin(() => {
|
|
286
|
+
if (localMax !== null && value > localMax) {
|
|
287
|
+
return localMax;
|
|
159
288
|
}
|
|
160
|
-
return
|
|
161
|
-
column: f.id,
|
|
162
|
-
type: "values",
|
|
163
|
-
values: Array.isArray(filterValue) ? filterValue : [],
|
|
164
|
-
range: null
|
|
165
|
-
};
|
|
289
|
+
return value;
|
|
166
290
|
});
|
|
167
|
-
}, [
|
|
168
|
-
const
|
|
169
|
-
(
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
if (!filterValue) return false;
|
|
174
|
-
if ((0, import_tinypivot_core.isNumericRange)(filterValue)) {
|
|
175
|
-
return filterValue.min !== null || filterValue.max !== null;
|
|
291
|
+
}, [localMax]);
|
|
292
|
+
const handleMaxSlider = (0, import_react2.useCallback)((event) => {
|
|
293
|
+
const value = Number.parseFloat(event.target.value);
|
|
294
|
+
setLocalMax(() => {
|
|
295
|
+
if (localMin !== null && value < localMin) {
|
|
296
|
+
return localMin;
|
|
176
297
|
}
|
|
177
|
-
return
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
298
|
+
return value;
|
|
299
|
+
});
|
|
300
|
+
}, [localMin]);
|
|
301
|
+
const handleSliderChange = (0, import_react2.useCallback)(() => {
|
|
302
|
+
if (localMin === null && localMax === null) {
|
|
303
|
+
onChange(null);
|
|
304
|
+
} else {
|
|
305
|
+
onChange({ min: localMin, max: localMax });
|
|
306
|
+
}
|
|
307
|
+
}, [localMin, localMax, onChange]);
|
|
308
|
+
const handleMinInput = (0, import_react2.useCallback)((event) => {
|
|
309
|
+
const value = event.target.value === "" ? null : Number.parseFloat(event.target.value);
|
|
310
|
+
if (value !== null && !Number.isNaN(value)) {
|
|
311
|
+
setLocalMin(Math.max(dataMin, Math.min(value, localMax ?? dataMax)));
|
|
312
|
+
} else if (value === null) {
|
|
313
|
+
setLocalMin(null);
|
|
314
|
+
}
|
|
315
|
+
}, [dataMin, dataMax, localMax]);
|
|
316
|
+
const handleMaxInput = (0, import_react2.useCallback)((event) => {
|
|
317
|
+
const value = event.target.value === "" ? null : Number.parseFloat(event.target.value);
|
|
318
|
+
if (value !== null && !Number.isNaN(value)) {
|
|
319
|
+
setLocalMax(Math.min(dataMax, Math.max(value, localMin ?? dataMin)));
|
|
320
|
+
} else if (value === null) {
|
|
321
|
+
setLocalMax(null);
|
|
322
|
+
}
|
|
323
|
+
}, [dataMin, dataMax, localMin]);
|
|
324
|
+
const handleInputBlur = (0, import_react2.useCallback)(() => {
|
|
325
|
+
if (localMin === null && localMax === null) {
|
|
326
|
+
onChange(null);
|
|
327
|
+
} else {
|
|
328
|
+
onChange({ min: localMin, max: localMax });
|
|
329
|
+
}
|
|
330
|
+
}, [localMin, localMax, onChange]);
|
|
331
|
+
const clearFilter = (0, import_react2.useCallback)(() => {
|
|
332
|
+
setLocalMin(null);
|
|
333
|
+
setLocalMax(null);
|
|
334
|
+
onChange(null);
|
|
335
|
+
}, [onChange]);
|
|
336
|
+
const setFullRange = (0, import_react2.useCallback)(() => {
|
|
337
|
+
setLocalMin(dataMin);
|
|
338
|
+
setLocalMax(dataMax);
|
|
339
|
+
onChange({ min: dataMin, max: dataMax });
|
|
340
|
+
}, [dataMin, dataMax, onChange]);
|
|
341
|
+
(0, import_react2.useEffect)(() => {
|
|
342
|
+
setLocalMin(currentRange?.min ?? null);
|
|
343
|
+
setLocalMax(currentRange?.max ?? null);
|
|
344
|
+
}, [currentRange]);
|
|
345
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-range-filter", children: [
|
|
346
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-range-info", children: [
|
|
347
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "vpg-range-label", children: "Data range:" }),
|
|
348
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "vpg-range-bounds", children: [
|
|
349
|
+
formatValue(dataMin),
|
|
350
|
+
" ",
|
|
351
|
+
"\u2013",
|
|
352
|
+
formatValue(dataMax)
|
|
353
|
+
] })
|
|
354
|
+
] }),
|
|
355
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-slider-container", children: [
|
|
356
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "vpg-slider-track", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
357
|
+
"div",
|
|
358
|
+
{
|
|
359
|
+
className: "vpg-slider-fill",
|
|
360
|
+
style: {
|
|
361
|
+
left: `${minPercent}%`,
|
|
362
|
+
right: `${100 - maxPercent}%`
|
|
363
|
+
}
|
|
198
364
|
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
365
|
+
) }),
|
|
366
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
367
|
+
"input",
|
|
368
|
+
{
|
|
369
|
+
type: "range",
|
|
370
|
+
className: "vpg-slider vpg-slider-min",
|
|
371
|
+
min: dataMin,
|
|
372
|
+
max: dataMax,
|
|
373
|
+
step,
|
|
374
|
+
value: localMin ?? dataMin,
|
|
375
|
+
onChange: handleMinSlider,
|
|
376
|
+
onMouseUp: handleSliderChange,
|
|
377
|
+
onTouchEnd: handleSliderChange
|
|
378
|
+
}
|
|
379
|
+
),
|
|
380
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
381
|
+
"input",
|
|
382
|
+
{
|
|
383
|
+
type: "range",
|
|
384
|
+
className: "vpg-slider vpg-slider-max",
|
|
385
|
+
min: dataMin,
|
|
386
|
+
max: dataMax,
|
|
387
|
+
step,
|
|
388
|
+
value: localMax ?? dataMax,
|
|
389
|
+
onChange: handleMaxSlider,
|
|
390
|
+
onMouseUp: handleSliderChange,
|
|
391
|
+
onTouchEnd: handleSliderChange
|
|
392
|
+
}
|
|
393
|
+
)
|
|
394
|
+
] }),
|
|
395
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-range-inputs", children: [
|
|
396
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-input-group", children: [
|
|
397
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { className: "vpg-input-label", children: "Min" }),
|
|
398
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
399
|
+
"input",
|
|
400
|
+
{
|
|
401
|
+
type: "number",
|
|
402
|
+
className: "vpg-range-input",
|
|
403
|
+
placeholder: formatValue(dataMin),
|
|
404
|
+
value: localMin ?? "",
|
|
405
|
+
step,
|
|
406
|
+
onChange: handleMinInput,
|
|
407
|
+
onBlur: handleInputBlur
|
|
408
|
+
}
|
|
409
|
+
)
|
|
410
|
+
] }),
|
|
411
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "vpg-input-separator", children: "to" }),
|
|
412
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-input-group", children: [
|
|
413
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { className: "vpg-input-label", children: "Max" }),
|
|
414
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
415
|
+
"input",
|
|
416
|
+
{
|
|
417
|
+
type: "number",
|
|
418
|
+
className: "vpg-range-input",
|
|
419
|
+
placeholder: formatValue(dataMax),
|
|
420
|
+
value: localMax ?? "",
|
|
421
|
+
step,
|
|
422
|
+
onChange: handleMaxInput,
|
|
423
|
+
onBlur: handleInputBlur
|
|
424
|
+
}
|
|
425
|
+
)
|
|
426
|
+
] })
|
|
427
|
+
] }),
|
|
428
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-range-actions", children: [
|
|
429
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
430
|
+
"button",
|
|
431
|
+
{
|
|
432
|
+
className: "vpg-range-btn",
|
|
433
|
+
disabled: !isFilterActive,
|
|
434
|
+
onClick: clearFilter,
|
|
435
|
+
children: [
|
|
436
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
437
|
+
"path",
|
|
438
|
+
{
|
|
439
|
+
strokeLinecap: "round",
|
|
440
|
+
strokeLinejoin: "round",
|
|
441
|
+
strokeWidth: 2,
|
|
442
|
+
d: "M6 18L18 6M6 6l12 12"
|
|
443
|
+
}
|
|
444
|
+
) }),
|
|
445
|
+
"Clear"
|
|
446
|
+
]
|
|
447
|
+
}
|
|
448
|
+
),
|
|
449
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { className: "vpg-range-btn", onClick: setFullRange, children: [
|
|
450
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
451
|
+
"path",
|
|
452
|
+
{
|
|
453
|
+
strokeLinecap: "round",
|
|
454
|
+
strokeLinejoin: "round",
|
|
455
|
+
strokeWidth: 2,
|
|
456
|
+
d: "M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
|
|
457
|
+
}
|
|
458
|
+
) }),
|
|
459
|
+
"Full Range"
|
|
460
|
+
] })
|
|
461
|
+
] }),
|
|
462
|
+
isFilterActive && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-filter-summary", children: [
|
|
463
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
464
|
+
"path",
|
|
465
|
+
{
|
|
466
|
+
strokeLinecap: "round",
|
|
467
|
+
strokeLinejoin: "round",
|
|
468
|
+
strokeWidth: 2,
|
|
469
|
+
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"
|
|
470
|
+
}
|
|
471
|
+
) }),
|
|
472
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { children: [
|
|
473
|
+
"Showing values",
|
|
474
|
+
" ",
|
|
475
|
+
localMin !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("strong", { children: [
|
|
476
|
+
"\u2265",
|
|
477
|
+
formatValue(localMin)
|
|
478
|
+
] }),
|
|
479
|
+
localMin !== null && localMax !== null && " and ",
|
|
480
|
+
localMax !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("strong", { children: [
|
|
481
|
+
"\u2264",
|
|
482
|
+
formatValue(localMax)
|
|
483
|
+
] })
|
|
484
|
+
] })
|
|
485
|
+
] })
|
|
486
|
+
] });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/components/ColumnFilter.tsx
|
|
490
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
491
|
+
function ColumnFilter({
|
|
492
|
+
columnName,
|
|
493
|
+
stats,
|
|
494
|
+
selectedValues,
|
|
495
|
+
sortDirection,
|
|
496
|
+
numericRange,
|
|
497
|
+
onFilter,
|
|
498
|
+
onSort,
|
|
499
|
+
onClose,
|
|
500
|
+
onRangeFilter
|
|
501
|
+
}) {
|
|
502
|
+
const [searchQuery, setSearchQuery] = (0, import_react3.useState)("");
|
|
503
|
+
const [localSelected, setLocalSelected] = (0, import_react3.useState)(new Set(selectedValues));
|
|
504
|
+
const dropdownRef = (0, import_react3.useRef)(null);
|
|
505
|
+
const searchInputRef = (0, import_react3.useRef)(null);
|
|
506
|
+
const isNumericColumn = stats.type === "number" && stats.numericMin !== void 0 && stats.numericMax !== void 0;
|
|
507
|
+
const [filterMode, setFilterMode] = (0, import_react3.useState)(numericRange ? "range" : "values");
|
|
508
|
+
const [localRange, setLocalRange] = (0, import_react3.useState)(numericRange ?? null);
|
|
509
|
+
const hasBlankValues = stats.nullCount > 0;
|
|
510
|
+
const filteredValues = (0, import_react3.useMemo)(() => {
|
|
511
|
+
const values = stats.uniqueValues;
|
|
512
|
+
if (!searchQuery)
|
|
513
|
+
return values;
|
|
514
|
+
const query = searchQuery.toLowerCase();
|
|
515
|
+
return values.filter((v) => v.toLowerCase().includes(query));
|
|
516
|
+
}, [stats.uniqueValues, searchQuery]);
|
|
517
|
+
const allValues = (0, import_react3.useMemo)(() => {
|
|
518
|
+
const values = [...filteredValues];
|
|
519
|
+
if (hasBlankValues && (!searchQuery || "(blank)".includes(searchQuery.toLowerCase()))) {
|
|
520
|
+
values.unshift("(blank)");
|
|
521
|
+
}
|
|
522
|
+
return values;
|
|
523
|
+
}, [filteredValues, hasBlankValues, searchQuery]);
|
|
524
|
+
const _isAllSelected = (0, import_react3.useMemo)(
|
|
525
|
+
() => allValues.every((v) => localSelected.has(v)),
|
|
526
|
+
[allValues, localSelected]
|
|
228
527
|
);
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
} else if (!current.desc) {
|
|
235
|
-
return [{ id: columnId, desc: true }];
|
|
528
|
+
const toggleValue = (0, import_react3.useCallback)((value) => {
|
|
529
|
+
setLocalSelected((prev) => {
|
|
530
|
+
const next = new Set(prev);
|
|
531
|
+
if (next.has(value)) {
|
|
532
|
+
next.delete(value);
|
|
236
533
|
} else {
|
|
237
|
-
|
|
534
|
+
next.add(value);
|
|
238
535
|
}
|
|
536
|
+
return next;
|
|
239
537
|
});
|
|
240
538
|
}, []);
|
|
241
|
-
const
|
|
242
|
-
(
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
},
|
|
247
|
-
[sorting]
|
|
248
|
-
);
|
|
249
|
-
return {
|
|
250
|
-
// Table instance
|
|
251
|
-
table,
|
|
252
|
-
// State
|
|
253
|
-
sorting,
|
|
254
|
-
columnFilters,
|
|
255
|
-
columnVisibility,
|
|
256
|
-
globalFilter,
|
|
257
|
-
columnKeys,
|
|
258
|
-
setSorting,
|
|
259
|
-
setColumnFilters,
|
|
260
|
-
setGlobalFilter,
|
|
261
|
-
// Computed
|
|
262
|
-
filteredRowCount,
|
|
263
|
-
totalRowCount,
|
|
264
|
-
activeFilters,
|
|
265
|
-
// Methods
|
|
266
|
-
getColumnStats,
|
|
267
|
-
clearStatsCache,
|
|
268
|
-
hasActiveFilter,
|
|
269
|
-
setColumnFilter,
|
|
270
|
-
getColumnFilterValues,
|
|
271
|
-
clearAllFilters,
|
|
272
|
-
toggleSort,
|
|
273
|
-
getSortDirection,
|
|
274
|
-
// Numeric range filters
|
|
275
|
-
setNumericRangeFilter,
|
|
276
|
-
getNumericRangeFilter
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// src/hooks/usePivotTable.ts
|
|
281
|
-
var import_react3 = require("react");
|
|
282
|
-
var import_tinypivot_core3 = require("@smallwebco/tinypivot-core");
|
|
283
|
-
|
|
284
|
-
// src/hooks/useLicense.ts
|
|
285
|
-
var import_react2 = require("react");
|
|
286
|
-
var import_tinypivot_core2 = require("@smallwebco/tinypivot-core");
|
|
287
|
-
var globalLicenseInfo = (0, import_tinypivot_core2.getFreeLicenseInfo)();
|
|
288
|
-
var globalDemoMode = false;
|
|
289
|
-
var listeners = /* @__PURE__ */ new Set();
|
|
290
|
-
function notifyListeners() {
|
|
291
|
-
listeners.forEach((listener) => listener());
|
|
292
|
-
}
|
|
293
|
-
async function setLicenseKey(key) {
|
|
294
|
-
globalLicenseInfo = await (0, import_tinypivot_core2.validateLicenseKey)(key);
|
|
295
|
-
if (!globalLicenseInfo.isValid) {
|
|
296
|
-
console.warn("[TinyPivot] Invalid or expired license key. Running in free mode.");
|
|
297
|
-
} else if (globalLicenseInfo.type !== "free") {
|
|
298
|
-
console.info(`[TinyPivot] Pro license activated (${globalLicenseInfo.type})`);
|
|
299
|
-
}
|
|
300
|
-
notifyListeners();
|
|
301
|
-
}
|
|
302
|
-
async function enableDemoMode(secret) {
|
|
303
|
-
const demoLicense = await (0, import_tinypivot_core2.getDemoLicenseInfo)(secret);
|
|
304
|
-
if (!demoLicense) {
|
|
305
|
-
console.warn("[TinyPivot] Demo mode activation failed - invalid secret");
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
globalDemoMode = true;
|
|
309
|
-
globalLicenseInfo = demoLicense;
|
|
310
|
-
console.info("[TinyPivot] Demo mode enabled - all Pro features unlocked for evaluation");
|
|
311
|
-
notifyListeners();
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
314
|
-
function configureLicenseSecret(secret) {
|
|
315
|
-
(0, import_tinypivot_core2.configureLicenseSecret)(secret);
|
|
316
|
-
}
|
|
317
|
-
function useLicense() {
|
|
318
|
-
const [, forceUpdate] = (0, import_react2.useState)({});
|
|
319
|
-
(0, import_react2.useState)(() => {
|
|
320
|
-
const update = () => forceUpdate({});
|
|
321
|
-
listeners.add(update);
|
|
322
|
-
return () => listeners.delete(update);
|
|
323
|
-
});
|
|
324
|
-
const isDemo = globalDemoMode;
|
|
325
|
-
const licenseInfo = globalLicenseInfo;
|
|
326
|
-
const isPro = (0, import_react2.useMemo)(
|
|
327
|
-
() => globalDemoMode || (0, import_tinypivot_core2.isPro)(licenseInfo),
|
|
328
|
-
[licenseInfo]
|
|
329
|
-
);
|
|
330
|
-
const canUsePivot = (0, import_react2.useMemo)(
|
|
331
|
-
() => globalDemoMode || (0, import_tinypivot_core2.canUsePivot)(licenseInfo),
|
|
332
|
-
[licenseInfo]
|
|
333
|
-
);
|
|
334
|
-
const canUseAdvancedAggregations = (0, import_react2.useMemo)(
|
|
335
|
-
() => globalDemoMode || licenseInfo.features.advancedAggregations,
|
|
336
|
-
[licenseInfo]
|
|
337
|
-
);
|
|
338
|
-
const canUsePercentageMode = (0, import_react2.useMemo)(
|
|
339
|
-
() => globalDemoMode || licenseInfo.features.percentageMode,
|
|
340
|
-
[licenseInfo]
|
|
341
|
-
);
|
|
342
|
-
const showWatermark = (0, import_react2.useMemo)(
|
|
343
|
-
() => (0, import_tinypivot_core2.shouldShowWatermark)(licenseInfo, globalDemoMode),
|
|
344
|
-
[licenseInfo]
|
|
345
|
-
);
|
|
346
|
-
const requirePro = (0, import_react2.useCallback)((feature) => {
|
|
347
|
-
if (!isPro) {
|
|
348
|
-
(0, import_tinypivot_core2.logProRequired)(feature);
|
|
349
|
-
return false;
|
|
350
|
-
}
|
|
351
|
-
return true;
|
|
352
|
-
}, [isPro]);
|
|
353
|
-
return {
|
|
354
|
-
licenseInfo,
|
|
355
|
-
isDemo,
|
|
356
|
-
isPro,
|
|
357
|
-
canUsePivot,
|
|
358
|
-
canUseAdvancedAggregations,
|
|
359
|
-
canUsePercentageMode,
|
|
360
|
-
showWatermark,
|
|
361
|
-
requirePro
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// src/hooks/usePivotTable.ts
|
|
366
|
-
function usePivotTable(data) {
|
|
367
|
-
const { canUsePivot, requirePro } = useLicense();
|
|
368
|
-
const [rowFields, setRowFieldsState] = (0, import_react3.useState)([]);
|
|
369
|
-
const [columnFields, setColumnFieldsState] = (0, import_react3.useState)([]);
|
|
370
|
-
const [valueFields, setValueFields] = (0, import_react3.useState)([]);
|
|
371
|
-
const [showRowTotals, setShowRowTotals] = (0, import_react3.useState)(true);
|
|
372
|
-
const [showColumnTotals, setShowColumnTotals] = (0, import_react3.useState)(true);
|
|
373
|
-
const [calculatedFields, setCalculatedFields] = (0, import_react3.useState)(() => (0, import_tinypivot_core3.loadCalculatedFields)());
|
|
374
|
-
const [currentStorageKey, setCurrentStorageKey] = (0, import_react3.useState)(null);
|
|
375
|
-
const availableFields = (0, import_react3.useMemo)(() => {
|
|
376
|
-
return (0, import_tinypivot_core3.computeAvailableFields)(data);
|
|
377
|
-
}, [data]);
|
|
378
|
-
const unassignedFields = (0, import_react3.useMemo)(() => {
|
|
379
|
-
return (0, import_tinypivot_core3.getUnassignedFields)(availableFields, rowFields, columnFields, valueFields);
|
|
380
|
-
}, [availableFields, rowFields, columnFields, valueFields]);
|
|
381
|
-
const isConfigured = (0, import_react3.useMemo)(() => {
|
|
382
|
-
return (0, import_tinypivot_core3.isPivotConfigured)({
|
|
383
|
-
rowFields,
|
|
384
|
-
columnFields,
|
|
385
|
-
valueFields,
|
|
386
|
-
showRowTotals,
|
|
387
|
-
showColumnTotals
|
|
388
|
-
});
|
|
389
|
-
}, [rowFields, columnFields, valueFields, showRowTotals, showColumnTotals]);
|
|
390
|
-
const pivotResult = (0, import_react3.useMemo)(() => {
|
|
391
|
-
if (!isConfigured) return null;
|
|
392
|
-
if (!canUsePivot) return null;
|
|
393
|
-
return (0, import_tinypivot_core3.computePivotResult)(data, {
|
|
394
|
-
rowFields,
|
|
395
|
-
columnFields,
|
|
396
|
-
valueFields,
|
|
397
|
-
showRowTotals,
|
|
398
|
-
showColumnTotals,
|
|
399
|
-
calculatedFields
|
|
400
|
-
});
|
|
401
|
-
}, [data, isConfigured, canUsePivot, rowFields, columnFields, valueFields, showRowTotals, showColumnTotals, calculatedFields]);
|
|
402
|
-
(0, import_react3.useEffect)(() => {
|
|
403
|
-
if (data.length === 0) return;
|
|
404
|
-
const newKeys = Object.keys(data[0]);
|
|
405
|
-
const storageKey = (0, import_tinypivot_core3.generateStorageKey)(newKeys);
|
|
406
|
-
if (storageKey !== currentStorageKey) {
|
|
407
|
-
setCurrentStorageKey(storageKey);
|
|
408
|
-
const savedConfig = (0, import_tinypivot_core3.loadPivotConfig)(storageKey);
|
|
409
|
-
if (savedConfig && (0, import_tinypivot_core3.isConfigValidForFields)(savedConfig, newKeys)) {
|
|
410
|
-
setRowFieldsState(savedConfig.rowFields);
|
|
411
|
-
setColumnFieldsState(savedConfig.columnFields);
|
|
412
|
-
setValueFields(savedConfig.valueFields);
|
|
413
|
-
setShowRowTotals(savedConfig.showRowTotals);
|
|
414
|
-
setShowColumnTotals(savedConfig.showColumnTotals);
|
|
415
|
-
if (savedConfig.calculatedFields) {
|
|
416
|
-
setCalculatedFields(savedConfig.calculatedFields);
|
|
417
|
-
}
|
|
418
|
-
} else {
|
|
419
|
-
const currentConfig = {
|
|
420
|
-
rowFields,
|
|
421
|
-
columnFields,
|
|
422
|
-
valueFields,
|
|
423
|
-
showRowTotals,
|
|
424
|
-
showColumnTotals
|
|
425
|
-
};
|
|
426
|
-
if (!(0, import_tinypivot_core3.isConfigValidForFields)(currentConfig, newKeys)) {
|
|
427
|
-
setRowFieldsState([]);
|
|
428
|
-
setColumnFieldsState([]);
|
|
429
|
-
setValueFields([]);
|
|
430
|
-
}
|
|
539
|
+
const selectAll = (0, import_react3.useCallback)(() => {
|
|
540
|
+
setLocalSelected((prev) => {
|
|
541
|
+
const next = new Set(prev);
|
|
542
|
+
for (const value of allValues) {
|
|
543
|
+
next.add(value);
|
|
431
544
|
}
|
|
545
|
+
return next;
|
|
546
|
+
});
|
|
547
|
+
}, [allValues]);
|
|
548
|
+
const clearAll = (0, import_react3.useCallback)(() => {
|
|
549
|
+
setLocalSelected(/* @__PURE__ */ new Set());
|
|
550
|
+
}, []);
|
|
551
|
+
const applyFilter = (0, import_react3.useCallback)(() => {
|
|
552
|
+
if (localSelected.size === 0) {
|
|
553
|
+
onFilter([]);
|
|
554
|
+
} else {
|
|
555
|
+
onFilter(Array.from(localSelected));
|
|
432
556
|
}
|
|
433
|
-
|
|
557
|
+
onClose();
|
|
558
|
+
}, [localSelected, onFilter, onClose]);
|
|
559
|
+
const sortAscending = (0, import_react3.useCallback)(() => {
|
|
560
|
+
onSort(sortDirection === "asc" ? null : "asc");
|
|
561
|
+
}, [sortDirection, onSort]);
|
|
562
|
+
const sortDescending = (0, import_react3.useCallback)(() => {
|
|
563
|
+
onSort(sortDirection === "desc" ? null : "desc");
|
|
564
|
+
}, [sortDirection, onSort]);
|
|
565
|
+
const clearFilter = (0, import_react3.useCallback)(() => {
|
|
566
|
+
setLocalSelected(/* @__PURE__ */ new Set());
|
|
567
|
+
onFilter([]);
|
|
568
|
+
onClose();
|
|
569
|
+
}, [onFilter, onClose]);
|
|
570
|
+
const handleRangeChange = (0, import_react3.useCallback)((range) => {
|
|
571
|
+
setLocalRange(range);
|
|
572
|
+
}, []);
|
|
573
|
+
const applyRangeFilter = (0, import_react3.useCallback)(() => {
|
|
574
|
+
onRangeFilter?.(localRange);
|
|
575
|
+
onClose();
|
|
576
|
+
}, [localRange, onRangeFilter, onClose]);
|
|
577
|
+
const clearRangeFilter = (0, import_react3.useCallback)(() => {
|
|
578
|
+
setLocalRange(null);
|
|
579
|
+
onRangeFilter?.(null);
|
|
580
|
+
onClose();
|
|
581
|
+
}, [onRangeFilter, onClose]);
|
|
434
582
|
(0, import_react3.useEffect)(() => {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
columnFields,
|
|
439
|
-
valueFields,
|
|
440
|
-
showRowTotals,
|
|
441
|
-
showColumnTotals,
|
|
442
|
-
calculatedFields
|
|
443
|
-
};
|
|
444
|
-
(0, import_tinypivot_core3.savePivotConfig)(currentStorageKey, config);
|
|
445
|
-
}, [currentStorageKey, rowFields, columnFields, valueFields, showRowTotals, showColumnTotals, calculatedFields]);
|
|
446
|
-
const addRowField = (0, import_react3.useCallback)(
|
|
447
|
-
(field) => {
|
|
448
|
-
if (!rowFields.includes(field)) {
|
|
449
|
-
setRowFieldsState((prev) => [...prev, field]);
|
|
583
|
+
const handleClickOutside = (event) => {
|
|
584
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
585
|
+
onClose();
|
|
450
586
|
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
(field) => {
|
|
462
|
-
if (!columnFields.includes(field)) {
|
|
463
|
-
setColumnFieldsState((prev) => [...prev, field]);
|
|
587
|
+
};
|
|
588
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
589
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
590
|
+
}, [onClose]);
|
|
591
|
+
(0, import_react3.useEffect)(() => {
|
|
592
|
+
const handleKeydown = (event) => {
|
|
593
|
+
if (event.key === "Escape") {
|
|
594
|
+
onClose();
|
|
595
|
+
} else if (event.key === "Enter" && event.ctrlKey) {
|
|
596
|
+
applyFilter();
|
|
464
597
|
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const setColumnFields = (0, import_react3.useCallback)((fields) => {
|
|
472
|
-
setColumnFieldsState(fields);
|
|
598
|
+
};
|
|
599
|
+
document.addEventListener("keydown", handleKeydown);
|
|
600
|
+
return () => document.removeEventListener("keydown", handleKeydown);
|
|
601
|
+
}, [onClose, applyFilter]);
|
|
602
|
+
(0, import_react3.useEffect)(() => {
|
|
603
|
+
searchInputRef.current?.focus();
|
|
473
604
|
}, []);
|
|
474
|
-
|
|
475
|
-
(
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
605
|
+
(0, import_react3.useEffect)(() => {
|
|
606
|
+
setLocalSelected(new Set(selectedValues));
|
|
607
|
+
}, [selectedValues]);
|
|
608
|
+
(0, import_react3.useEffect)(() => {
|
|
609
|
+
setLocalRange(numericRange ?? null);
|
|
610
|
+
if (numericRange) {
|
|
611
|
+
setFilterMode("range");
|
|
612
|
+
}
|
|
613
|
+
}, [numericRange]);
|
|
614
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { ref: dropdownRef, className: "vpg-filter-dropdown", children: [
|
|
615
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-filter-header", children: [
|
|
616
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "vpg-filter-title", children: columnName }),
|
|
617
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "vpg-filter-count", children: [
|
|
618
|
+
stats.uniqueValues.length.toLocaleString(),
|
|
619
|
+
" ",
|
|
620
|
+
"unique"
|
|
621
|
+
] })
|
|
622
|
+
] }),
|
|
623
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-sort-controls", children: [
|
|
624
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
625
|
+
"button",
|
|
626
|
+
{
|
|
627
|
+
className: `vpg-sort-btn ${sortDirection === "asc" ? "active" : ""}`,
|
|
628
|
+
title: isNumericColumn ? "Sort Low to High" : "Sort A to Z",
|
|
629
|
+
onClick: sortAscending,
|
|
630
|
+
children: [
|
|
631
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
632
|
+
"path",
|
|
633
|
+
{
|
|
634
|
+
strokeLinecap: "round",
|
|
635
|
+
strokeLinejoin: "round",
|
|
636
|
+
strokeWidth: 2,
|
|
637
|
+
d: "M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"
|
|
638
|
+
}
|
|
639
|
+
) }),
|
|
640
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: isNumericColumn ? "1\u21929" : "A\u2192Z" })
|
|
641
|
+
]
|
|
482
642
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
643
|
+
),
|
|
644
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
645
|
+
"button",
|
|
646
|
+
{
|
|
647
|
+
className: `vpg-sort-btn ${sortDirection === "desc" ? "active" : ""}`,
|
|
648
|
+
title: isNumericColumn ? "Sort High to Low" : "Sort Z to A",
|
|
649
|
+
onClick: sortDescending,
|
|
650
|
+
children: [
|
|
651
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
652
|
+
"path",
|
|
653
|
+
{
|
|
654
|
+
strokeLinecap: "round",
|
|
655
|
+
strokeLinejoin: "round",
|
|
656
|
+
strokeWidth: 2,
|
|
657
|
+
d: "M3 4h13M3 8h9m-9 4h9m5-4v12m0 0l-4-4m4 4l4-4"
|
|
658
|
+
}
|
|
659
|
+
) }),
|
|
660
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: isNumericColumn ? "9\u21921" : "Z\u2192A" })
|
|
661
|
+
]
|
|
662
|
+
}
|
|
663
|
+
)
|
|
664
|
+
] }),
|
|
665
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "vpg-divider" }),
|
|
666
|
+
isNumericColumn && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-filter-tabs", children: [
|
|
667
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
668
|
+
"button",
|
|
669
|
+
{
|
|
670
|
+
className: `vpg-tab-btn ${filterMode === "values" ? "active" : ""}`,
|
|
671
|
+
onClick: () => setFilterMode("values"),
|
|
672
|
+
children: [
|
|
673
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
674
|
+
"path",
|
|
675
|
+
{
|
|
676
|
+
strokeLinecap: "round",
|
|
677
|
+
strokeLinejoin: "round",
|
|
678
|
+
strokeWidth: 2,
|
|
679
|
+
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"
|
|
680
|
+
}
|
|
681
|
+
) }),
|
|
682
|
+
"Values"
|
|
683
|
+
]
|
|
684
|
+
}
|
|
685
|
+
),
|
|
686
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
687
|
+
"button",
|
|
688
|
+
{
|
|
689
|
+
className: `vpg-tab-btn ${filterMode === "range" ? "active" : ""}`,
|
|
690
|
+
onClick: () => setFilterMode("range"),
|
|
691
|
+
children: [
|
|
692
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
693
|
+
"path",
|
|
694
|
+
{
|
|
695
|
+
strokeLinecap: "round",
|
|
696
|
+
strokeLinejoin: "round",
|
|
697
|
+
strokeWidth: 2,
|
|
698
|
+
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"
|
|
699
|
+
}
|
|
700
|
+
) }),
|
|
701
|
+
"Range"
|
|
702
|
+
]
|
|
703
|
+
}
|
|
704
|
+
)
|
|
705
|
+
] }),
|
|
706
|
+
(!isNumericColumn || filterMode === "values") && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
707
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-search-container", children: [
|
|
708
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "vpg-search-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
709
|
+
"path",
|
|
710
|
+
{
|
|
711
|
+
strokeLinecap: "round",
|
|
712
|
+
strokeLinejoin: "round",
|
|
713
|
+
strokeWidth: 2,
|
|
714
|
+
d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
502
715
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
716
|
+
) }),
|
|
717
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
718
|
+
"input",
|
|
719
|
+
{
|
|
720
|
+
ref: searchInputRef,
|
|
721
|
+
type: "text",
|
|
722
|
+
value: searchQuery,
|
|
723
|
+
onChange: (e) => setSearchQuery(e.target.value),
|
|
724
|
+
placeholder: "Search values...",
|
|
725
|
+
className: "vpg-search-input"
|
|
726
|
+
}
|
|
727
|
+
),
|
|
728
|
+
searchQuery && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-clear-search", onClick: () => setSearchQuery(""), children: "\xD7" })
|
|
729
|
+
] }),
|
|
730
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-bulk-actions", children: [
|
|
731
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { className: "vpg-bulk-btn", onClick: selectAll, children: [
|
|
732
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
733
|
+
"path",
|
|
734
|
+
{
|
|
735
|
+
strokeLinecap: "round",
|
|
736
|
+
strokeLinejoin: "round",
|
|
737
|
+
strokeWidth: 2,
|
|
738
|
+
d: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
739
|
+
}
|
|
740
|
+
) }),
|
|
741
|
+
"Select All"
|
|
742
|
+
] }),
|
|
743
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { className: "vpg-bulk-btn", onClick: clearAll, children: [
|
|
744
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
745
|
+
"path",
|
|
746
|
+
{
|
|
747
|
+
strokeLinecap: "round",
|
|
748
|
+
strokeLinejoin: "round",
|
|
749
|
+
strokeWidth: 2,
|
|
750
|
+
d: "M6 18L18 6M6 6l12 12"
|
|
751
|
+
}
|
|
752
|
+
) }),
|
|
753
|
+
"Clear All"
|
|
754
|
+
] })
|
|
755
|
+
] }),
|
|
756
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-values-list", children: [
|
|
757
|
+
allValues.map((value) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
758
|
+
"label",
|
|
759
|
+
{
|
|
760
|
+
className: `vpg-value-item ${localSelected.has(value) ? "selected" : ""}`,
|
|
761
|
+
children: [
|
|
762
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
763
|
+
"input",
|
|
764
|
+
{
|
|
765
|
+
type: "checkbox",
|
|
766
|
+
checked: localSelected.has(value),
|
|
767
|
+
onChange: () => toggleValue(value),
|
|
768
|
+
className: "vpg-value-checkbox"
|
|
769
|
+
}
|
|
770
|
+
),
|
|
771
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: `vpg-value-text ${value === "(blank)" ? "vpg-blank" : ""}`, children: value })
|
|
772
|
+
]
|
|
773
|
+
},
|
|
774
|
+
value
|
|
775
|
+
)),
|
|
776
|
+
allValues.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "vpg-no-results", children: "No matching values" })
|
|
777
|
+
] }),
|
|
778
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-filter-footer", children: [
|
|
779
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-btn-clear", onClick: clearFilter, children: "Clear Filter" }),
|
|
780
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-btn-apply", onClick: applyFilter, children: "Apply" })
|
|
781
|
+
] })
|
|
782
|
+
] }),
|
|
783
|
+
isNumericColumn && filterMode === "range" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
784
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
785
|
+
NumericRangeFilter,
|
|
786
|
+
{
|
|
787
|
+
dataMin: stats.numericMin,
|
|
788
|
+
dataMax: stats.numericMax,
|
|
789
|
+
currentRange: localRange,
|
|
790
|
+
onChange: handleRangeChange
|
|
791
|
+
}
|
|
792
|
+
),
|
|
793
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-filter-footer", children: [
|
|
794
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-btn-clear", onClick: clearRangeFilter, children: "Clear Filter" }),
|
|
795
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-btn-apply", onClick: applyRangeFilter, children: "Apply" })
|
|
796
|
+
] })
|
|
797
|
+
] })
|
|
798
|
+
] });
|
|
575
799
|
}
|
|
576
800
|
|
|
577
|
-
// src/
|
|
801
|
+
// src/components/DataGrid.tsx
|
|
802
|
+
var import_react10 = require("react");
|
|
803
|
+
var import_react_dom2 = require("react-dom");
|
|
804
|
+
|
|
805
|
+
// src/hooks/useExcelGrid.ts
|
|
806
|
+
var import_tinypivot_core2 = require("@smallwebco/tinypivot-core");
|
|
807
|
+
var import_react_table = require("@tanstack/react-table");
|
|
578
808
|
var import_react4 = require("react");
|
|
579
|
-
var
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
809
|
+
var multiSelectFilter = (row, columnId, filterValue) => {
|
|
810
|
+
if (!filterValue)
|
|
811
|
+
return true;
|
|
812
|
+
if ((0, import_tinypivot_core2.isNumericRange)(filterValue)) {
|
|
813
|
+
const cellValue = row.getValue(columnId);
|
|
814
|
+
if (cellValue === null || cellValue === void 0 || cellValue === "") {
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
const num = typeof cellValue === "number" ? cellValue : Number.parseFloat(String(cellValue));
|
|
818
|
+
if (Number.isNaN(num))
|
|
819
|
+
return false;
|
|
820
|
+
const { min, max } = filterValue;
|
|
821
|
+
if (min !== null && num < min)
|
|
822
|
+
return false;
|
|
823
|
+
if (max !== null && num > max)
|
|
824
|
+
return false;
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
if (Array.isArray(filterValue) && filterValue.length > 0) {
|
|
828
|
+
const cellValue = row.getValue(columnId);
|
|
829
|
+
const cellString = cellValue === null || cellValue === void 0 || cellValue === "" ? "(blank)" : String(cellValue);
|
|
830
|
+
return filterValue.includes(cellString);
|
|
831
|
+
}
|
|
832
|
+
return true;
|
|
833
|
+
};
|
|
834
|
+
function useExcelGrid(options) {
|
|
835
|
+
const { data, enableSorting = true, enableFiltering = true } = options;
|
|
836
|
+
const [sorting, setSorting] = (0, import_react4.useState)([]);
|
|
837
|
+
const [columnFilters, setColumnFilters] = (0, import_react4.useState)([]);
|
|
838
|
+
const [columnVisibility, setColumnVisibility] = (0, import_react4.useState)({});
|
|
839
|
+
const [globalFilter, setGlobalFilter] = (0, import_react4.useState)("");
|
|
840
|
+
const [columnStatsCache, setColumnStatsCache] = (0, import_react4.useState)({});
|
|
841
|
+
const dataSignature = (0, import_react4.useMemo)(
|
|
842
|
+
() => `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
843
|
+
[data]
|
|
598
844
|
);
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
return
|
|
603
|
-
}, [data
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
845
|
+
const columnKeys = (0, import_react4.useMemo)(() => {
|
|
846
|
+
if (data.length === 0)
|
|
847
|
+
return [];
|
|
848
|
+
return Object.keys(data[0]);
|
|
849
|
+
}, [data]);
|
|
850
|
+
const getColumnStats = (0, import_react4.useCallback)(
|
|
851
|
+
(columnKey) => {
|
|
852
|
+
const cacheKey = `${columnKey}-${dataSignature}`;
|
|
853
|
+
if (!columnStatsCache[cacheKey]) {
|
|
854
|
+
const stats = (0, import_tinypivot_core2.getColumnUniqueValues)(data, columnKey);
|
|
855
|
+
setColumnStatsCache((prev) => ({ ...prev, [cacheKey]: stats }));
|
|
856
|
+
return stats;
|
|
857
|
+
}
|
|
858
|
+
return columnStatsCache[cacheKey];
|
|
859
|
+
},
|
|
860
|
+
[data, columnStatsCache, dataSignature]
|
|
608
861
|
);
|
|
609
|
-
const
|
|
862
|
+
const clearStatsCache = (0, import_react4.useCallback)(() => {
|
|
863
|
+
setColumnStatsCache({});
|
|
864
|
+
}, []);
|
|
865
|
+
(0, import_react4.useEffect)(() => {
|
|
866
|
+
clearStatsCache();
|
|
867
|
+
}, [dataSignature, clearStatsCache]);
|
|
868
|
+
const columnDefs = (0, import_react4.useMemo)(() => {
|
|
869
|
+
return columnKeys.map((key) => {
|
|
870
|
+
const stats = getColumnStats(key);
|
|
871
|
+
return {
|
|
872
|
+
id: key,
|
|
873
|
+
accessorKey: key,
|
|
874
|
+
header: key,
|
|
875
|
+
cell: (info) => (0, import_tinypivot_core2.formatCellValue)(info.getValue(), stats.type),
|
|
876
|
+
filterFn: multiSelectFilter,
|
|
877
|
+
meta: {
|
|
878
|
+
type: stats.type,
|
|
879
|
+
uniqueCount: stats.uniqueValues.length
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
});
|
|
883
|
+
}, [columnKeys, getColumnStats]);
|
|
884
|
+
const table = (0, import_react_table.useReactTable)({
|
|
885
|
+
data,
|
|
886
|
+
columns: columnDefs,
|
|
887
|
+
state: {
|
|
888
|
+
sorting,
|
|
889
|
+
columnFilters,
|
|
890
|
+
columnVisibility,
|
|
891
|
+
globalFilter
|
|
892
|
+
},
|
|
893
|
+
onSortingChange: setSorting,
|
|
894
|
+
onColumnFiltersChange: setColumnFilters,
|
|
895
|
+
onColumnVisibilityChange: setColumnVisibility,
|
|
896
|
+
onGlobalFilterChange: setGlobalFilter,
|
|
897
|
+
getCoreRowModel: (0, import_react_table.getCoreRowModel)(),
|
|
898
|
+
getSortedRowModel: enableSorting ? (0, import_react_table.getSortedRowModel)() : void 0,
|
|
899
|
+
getFilteredRowModel: enableFiltering ? (0, import_react_table.getFilteredRowModel)() : void 0,
|
|
900
|
+
filterFns: {
|
|
901
|
+
multiSelect: multiSelectFilter
|
|
902
|
+
},
|
|
903
|
+
enableSorting,
|
|
904
|
+
enableFilters: enableFiltering
|
|
905
|
+
});
|
|
906
|
+
const filteredRowCount = table.getFilteredRowModel().rows.length;
|
|
907
|
+
const totalRowCount = data.length;
|
|
908
|
+
const activeFilters = (0, import_react4.useMemo)(() => {
|
|
909
|
+
return columnFilters.map((f) => {
|
|
910
|
+
const filterValue = f.value;
|
|
911
|
+
if (filterValue && (0, import_tinypivot_core2.isNumericRange)(filterValue)) {
|
|
912
|
+
return {
|
|
913
|
+
column: f.id,
|
|
914
|
+
type: "range",
|
|
915
|
+
range: filterValue,
|
|
916
|
+
values: []
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
column: f.id,
|
|
921
|
+
type: "values",
|
|
922
|
+
values: Array.isArray(filterValue) ? filterValue : [],
|
|
923
|
+
range: null
|
|
924
|
+
};
|
|
925
|
+
});
|
|
926
|
+
}, [columnFilters]);
|
|
927
|
+
const hasActiveFilter = (0, import_react4.useCallback)(
|
|
928
|
+
(columnId) => {
|
|
929
|
+
const column = table.getColumn(columnId);
|
|
930
|
+
if (!column)
|
|
931
|
+
return false;
|
|
932
|
+
const filterValue = column.getFilterValue();
|
|
933
|
+
if (!filterValue)
|
|
934
|
+
return false;
|
|
935
|
+
if ((0, import_tinypivot_core2.isNumericRange)(filterValue)) {
|
|
936
|
+
return filterValue.min !== null || filterValue.max !== null;
|
|
937
|
+
}
|
|
938
|
+
return Array.isArray(filterValue) && filterValue.length > 0;
|
|
939
|
+
},
|
|
940
|
+
[table]
|
|
941
|
+
);
|
|
942
|
+
const setColumnFilter = (0, import_react4.useCallback)(
|
|
943
|
+
(columnId, values) => {
|
|
944
|
+
const column = table.getColumn(columnId);
|
|
945
|
+
if (column) {
|
|
946
|
+
column.setFilterValue(values.length === 0 ? void 0 : values);
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
[table]
|
|
950
|
+
);
|
|
951
|
+
const setNumericRangeFilter = (0, import_react4.useCallback)(
|
|
952
|
+
(columnId, range) => {
|
|
953
|
+
const column = table.getColumn(columnId);
|
|
954
|
+
if (column) {
|
|
955
|
+
if (!range || range.min === null && range.max === null) {
|
|
956
|
+
column.setFilterValue(void 0);
|
|
957
|
+
} else {
|
|
958
|
+
column.setFilterValue(range);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
},
|
|
962
|
+
[table]
|
|
963
|
+
);
|
|
964
|
+
const getNumericRangeFilter = (0, import_react4.useCallback)(
|
|
965
|
+
(columnId) => {
|
|
966
|
+
const column = table.getColumn(columnId);
|
|
967
|
+
if (!column)
|
|
968
|
+
return null;
|
|
969
|
+
const filterValue = column.getFilterValue();
|
|
970
|
+
if (filterValue && (0, import_tinypivot_core2.isNumericRange)(filterValue)) {
|
|
971
|
+
return filterValue;
|
|
972
|
+
}
|
|
973
|
+
return null;
|
|
974
|
+
},
|
|
975
|
+
[table]
|
|
976
|
+
);
|
|
977
|
+
const clearAllFilters = (0, import_react4.useCallback)(() => {
|
|
978
|
+
table.resetColumnFilters();
|
|
979
|
+
setGlobalFilter("");
|
|
980
|
+
setColumnFilters([]);
|
|
981
|
+
}, [table]);
|
|
982
|
+
const getColumnFilterValues = (0, import_react4.useCallback)(
|
|
983
|
+
(columnId) => {
|
|
984
|
+
const column = table.getColumn(columnId);
|
|
985
|
+
if (!column)
|
|
986
|
+
return [];
|
|
987
|
+
const filterValue = column.getFilterValue();
|
|
988
|
+
return Array.isArray(filterValue) ? filterValue : [];
|
|
989
|
+
},
|
|
990
|
+
[table]
|
|
991
|
+
);
|
|
992
|
+
const toggleSort = (0, import_react4.useCallback)((columnId) => {
|
|
993
|
+
setSorting((prev) => {
|
|
994
|
+
const current = prev.find((s) => s.id === columnId);
|
|
995
|
+
if (!current) {
|
|
996
|
+
return [{ id: columnId, desc: false }];
|
|
997
|
+
} else if (!current.desc) {
|
|
998
|
+
return [{ id: columnId, desc: true }];
|
|
999
|
+
} else {
|
|
1000
|
+
return [];
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
}, []);
|
|
1004
|
+
const getSortDirection = (0, import_react4.useCallback)(
|
|
1005
|
+
(columnId) => {
|
|
1006
|
+
const sort = sorting.find((s) => s.id === columnId);
|
|
1007
|
+
if (!sort)
|
|
1008
|
+
return null;
|
|
1009
|
+
return sort.desc ? "desc" : "asc";
|
|
1010
|
+
},
|
|
1011
|
+
[sorting]
|
|
1012
|
+
);
|
|
1013
|
+
return {
|
|
1014
|
+
// Table instance
|
|
1015
|
+
table,
|
|
1016
|
+
// State
|
|
1017
|
+
sorting,
|
|
1018
|
+
columnFilters,
|
|
1019
|
+
columnVisibility,
|
|
1020
|
+
globalFilter,
|
|
1021
|
+
columnKeys,
|
|
1022
|
+
setSorting,
|
|
1023
|
+
setColumnFilters,
|
|
1024
|
+
setGlobalFilter,
|
|
1025
|
+
// Computed
|
|
1026
|
+
filteredRowCount,
|
|
1027
|
+
totalRowCount,
|
|
1028
|
+
activeFilters,
|
|
1029
|
+
// Methods
|
|
1030
|
+
getColumnStats,
|
|
1031
|
+
clearStatsCache,
|
|
1032
|
+
hasActiveFilter,
|
|
1033
|
+
setColumnFilter,
|
|
1034
|
+
getColumnFilterValues,
|
|
1035
|
+
clearAllFilters,
|
|
1036
|
+
toggleSort,
|
|
1037
|
+
getSortDirection,
|
|
1038
|
+
// Numeric range filters
|
|
1039
|
+
setNumericRangeFilter,
|
|
1040
|
+
getNumericRangeFilter
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// src/hooks/useGridFeatures.ts
|
|
1045
|
+
var import_tinypivot_core3 = require("@smallwebco/tinypivot-core");
|
|
1046
|
+
var import_react5 = require("react");
|
|
1047
|
+
function exportToCSV(data, columns, options) {
|
|
1048
|
+
(0, import_tinypivot_core3.exportToCSV)(data, columns, options);
|
|
1049
|
+
}
|
|
1050
|
+
function exportPivotToCSV(pivotData, rowFields, columnFields, valueFields, options) {
|
|
1051
|
+
(0, import_tinypivot_core3.exportPivotToCSV)(pivotData, rowFields, columnFields, valueFields, options);
|
|
1052
|
+
}
|
|
1053
|
+
function copyToClipboard(text, onSuccess, onError) {
|
|
1054
|
+
(0, import_tinypivot_core3.copyToClipboard)(text, onSuccess, onError);
|
|
1055
|
+
}
|
|
1056
|
+
function formatSelectionForClipboard(rows, columns, selectionBounds) {
|
|
1057
|
+
return (0, import_tinypivot_core3.formatSelectionForClipboard)(rows, columns, selectionBounds);
|
|
1058
|
+
}
|
|
1059
|
+
function usePagination(data, options = {}) {
|
|
1060
|
+
const [pageSize, setPageSize] = (0, import_react5.useState)(options.pageSize ?? 50);
|
|
1061
|
+
const [currentPage, setCurrentPage] = (0, import_react5.useState)(options.currentPage ?? 1);
|
|
1062
|
+
const totalPages = (0, import_react5.useMemo)(
|
|
1063
|
+
() => Math.max(1, Math.ceil(data.length / pageSize)),
|
|
1064
|
+
[data.length, pageSize]
|
|
1065
|
+
);
|
|
1066
|
+
const paginatedData = (0, import_react5.useMemo)(() => {
|
|
1067
|
+
const start = (currentPage - 1) * pageSize;
|
|
1068
|
+
const end = start + pageSize;
|
|
1069
|
+
return data.slice(start, end);
|
|
1070
|
+
}, [data, currentPage, pageSize]);
|
|
1071
|
+
const startIndex = (0, import_react5.useMemo)(() => (currentPage - 1) * pageSize + 1, [currentPage, pageSize]);
|
|
1072
|
+
const endIndex = (0, import_react5.useMemo)(
|
|
1073
|
+
() => Math.min(currentPage * pageSize, data.length),
|
|
1074
|
+
[currentPage, pageSize, data.length]
|
|
1075
|
+
);
|
|
1076
|
+
const goToPage = (0, import_react5.useCallback)(
|
|
610
1077
|
(page) => {
|
|
611
1078
|
setCurrentPage(Math.max(1, Math.min(page, totalPages)));
|
|
612
1079
|
},
|
|
613
1080
|
[totalPages]
|
|
614
1081
|
);
|
|
615
|
-
const nextPage = (0,
|
|
1082
|
+
const nextPage = (0, import_react5.useCallback)(() => {
|
|
616
1083
|
if (currentPage < totalPages) {
|
|
617
1084
|
setCurrentPage((prev) => prev + 1);
|
|
618
1085
|
}
|
|
619
1086
|
}, [currentPage, totalPages]);
|
|
620
|
-
const prevPage = (0,
|
|
1087
|
+
const prevPage = (0, import_react5.useCallback)(() => {
|
|
621
1088
|
if (currentPage > 1) {
|
|
622
1089
|
setCurrentPage((prev) => prev - 1);
|
|
623
1090
|
}
|
|
624
1091
|
}, [currentPage]);
|
|
625
|
-
const firstPage = (0,
|
|
1092
|
+
const firstPage = (0, import_react5.useCallback)(() => {
|
|
626
1093
|
setCurrentPage(1);
|
|
627
1094
|
}, []);
|
|
628
|
-
const lastPage = (0,
|
|
1095
|
+
const lastPage = (0, import_react5.useCallback)(() => {
|
|
629
1096
|
setCurrentPage(totalPages);
|
|
630
1097
|
}, [totalPages]);
|
|
631
|
-
const updatePageSize = (0,
|
|
1098
|
+
const updatePageSize = (0, import_react5.useCallback)((size) => {
|
|
632
1099
|
setPageSize(size);
|
|
633
1100
|
setCurrentPage(1);
|
|
634
1101
|
}, []);
|
|
@@ -644,920 +1111,484 @@ function usePagination(data, options = {}) {
|
|
|
644
1111
|
prevPage,
|
|
645
1112
|
firstPage,
|
|
646
1113
|
lastPage,
|
|
647
|
-
setPageSize: updatePageSize
|
|
648
|
-
};
|
|
649
|
-
}
|
|
650
|
-
function useGlobalSearch(data, columns) {
|
|
651
|
-
const [searchTerm, setSearchTerm] = (0,
|
|
652
|
-
const [caseSensitive, setCaseSensitive] = (0,
|
|
653
|
-
const filteredData = (0,
|
|
654
|
-
if (!searchTerm.trim()) {
|
|
655
|
-
return data;
|
|
656
|
-
}
|
|
657
|
-
const term = caseSensitive ? searchTerm.trim() : searchTerm.trim().toLowerCase();
|
|
658
|
-
return data.filter((row) => {
|
|
659
|
-
for (const col of columns) {
|
|
660
|
-
const value = row[col];
|
|
661
|
-
if (value === null || value === void 0)
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
}
|
|
667
|
-
return false;
|
|
668
|
-
});
|
|
669
|
-
}, [data, columns, searchTerm, caseSensitive]);
|
|
670
|
-
const clearSearch = (0, import_react4.useCallback)(() => {
|
|
671
|
-
setSearchTerm("");
|
|
672
|
-
}, []);
|
|
673
|
-
return {
|
|
674
|
-
searchTerm,
|
|
675
|
-
setSearchTerm,
|
|
676
|
-
caseSensitive,
|
|
677
|
-
setCaseSensitive,
|
|
678
|
-
filteredData,
|
|
679
|
-
clearSearch
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
function useRowSelection(data) {
|
|
683
|
-
const [selectedRowIndices, setSelectedRowIndices] = (0, import_react4.useState)(/* @__PURE__ */ new Set());
|
|
684
|
-
const selectedRows = (0, import_react4.useMemo)(() => {
|
|
685
|
-
return Array.from(selectedRowIndices).sort((a, b) => a - b).map((idx) => data[idx]).filter(Boolean);
|
|
686
|
-
}, [data, selectedRowIndices]);
|
|
687
|
-
const allSelected = (0, import_react4.useMemo)(() => {
|
|
688
|
-
return data.length > 0 && selectedRowIndices.size === data.length;
|
|
689
|
-
}, [data.length, selectedRowIndices.size]);
|
|
690
|
-
const someSelected = (0, import_react4.useMemo)(() => {
|
|
691
|
-
return selectedRowIndices.size > 0 && selectedRowIndices.size < data.length;
|
|
692
|
-
}, [data.length, selectedRowIndices.size]);
|
|
693
|
-
const toggleRow = (0, import_react4.useCallback)((index) => {
|
|
694
|
-
setSelectedRowIndices((prev) => {
|
|
695
|
-
const next = new Set(prev);
|
|
696
|
-
if (next.has(index)) {
|
|
697
|
-
next.delete(index);
|
|
698
|
-
} else {
|
|
699
|
-
next.add(index);
|
|
700
|
-
}
|
|
701
|
-
return next;
|
|
702
|
-
});
|
|
703
|
-
}, []);
|
|
704
|
-
const selectRow = (0, import_react4.useCallback)((index) => {
|
|
705
|
-
setSelectedRowIndices((prev) => /* @__PURE__ */ new Set([...prev, index]));
|
|
706
|
-
}, []);
|
|
707
|
-
const deselectRow = (0, import_react4.useCallback)((index) => {
|
|
708
|
-
setSelectedRowIndices((prev) => {
|
|
709
|
-
const next = new Set(prev);
|
|
710
|
-
next.delete(index);
|
|
711
|
-
return next;
|
|
712
|
-
});
|
|
713
|
-
}, []);
|
|
714
|
-
const selectAll = (0, import_react4.useCallback)(() => {
|
|
715
|
-
setSelectedRowIndices(new Set(data.map((_, idx) => idx)));
|
|
716
|
-
}, [data]);
|
|
717
|
-
const deselectAll = (0, import_react4.useCallback)(() => {
|
|
718
|
-
setSelectedRowIndices(/* @__PURE__ */ new Set());
|
|
719
|
-
}, []);
|
|
720
|
-
const toggleAll = (0, import_react4.useCallback)(() => {
|
|
721
|
-
if (allSelected) {
|
|
722
|
-
deselectAll();
|
|
723
|
-
} else {
|
|
724
|
-
selectAll();
|
|
725
|
-
}
|
|
726
|
-
}, [allSelected, selectAll, deselectAll]);
|
|
727
|
-
const isSelected = (0, import_react4.useCallback)(
|
|
728
|
-
(index) => {
|
|
729
|
-
return selectedRowIndices.has(index);
|
|
730
|
-
},
|
|
731
|
-
[selectedRowIndices]
|
|
732
|
-
);
|
|
733
|
-
const selectRange = (0, import_react4.useCallback)((startIndex, endIndex) => {
|
|
734
|
-
const min = Math.min(startIndex, endIndex);
|
|
735
|
-
const max = Math.max(startIndex, endIndex);
|
|
736
|
-
setSelectedRowIndices((prev) => {
|
|
737
|
-
const next = new Set(prev);
|
|
738
|
-
for (let i = min; i <= max; i++) {
|
|
739
|
-
next.add(i);
|
|
740
|
-
}
|
|
741
|
-
return next;
|
|
742
|
-
});
|
|
743
|
-
}, []);
|
|
744
|
-
return {
|
|
745
|
-
selectedRowIndices,
|
|
746
|
-
selectedRows,
|
|
747
|
-
allSelected,
|
|
748
|
-
someSelected,
|
|
749
|
-
toggleRow,
|
|
750
|
-
selectRow,
|
|
751
|
-
deselectRow,
|
|
752
|
-
selectAll,
|
|
753
|
-
deselectAll,
|
|
754
|
-
toggleAll,
|
|
755
|
-
isSelected,
|
|
756
|
-
selectRange
|
|
757
|
-
};
|
|
758
|
-
}
|
|
759
|
-
function useColumnResize(initialWidths, minWidth = 60, maxWidth = 600) {
|
|
760
|
-
const [columnWidths, setColumnWidths] = (0, import_react4.useState)({ ...initialWidths });
|
|
761
|
-
const [isResizing, setIsResizing] = (0, import_react4.useState)(false);
|
|
762
|
-
const [resizingColumn, setResizingColumn] = (0, import_react4.useState)(null);
|
|
763
|
-
const startResize = (0, import_react4.useCallback)(
|
|
764
|
-
(columnId, event) => {
|
|
765
|
-
setIsResizing(true);
|
|
766
|
-
setResizingColumn(columnId);
|
|
767
|
-
const startX = event.clientX;
|
|
768
|
-
const startWidth = columnWidths[columnId] || 150;
|
|
769
|
-
const handleMouseMove = (e) => {
|
|
770
|
-
const diff = e.clientX - startX;
|
|
771
|
-
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + diff));
|
|
772
|
-
setColumnWidths((prev) => ({
|
|
773
|
-
...prev,
|
|
774
|
-
[columnId]: newWidth
|
|
775
|
-
}));
|
|
776
|
-
};
|
|
777
|
-
const handleMouseUp = () => {
|
|
778
|
-
setIsResizing(false);
|
|
779
|
-
setResizingColumn(null);
|
|
780
|
-
document.removeEventListener("mousemove", handleMouseMove);
|
|
781
|
-
document.removeEventListener("mouseup", handleMouseUp);
|
|
782
|
-
};
|
|
783
|
-
document.addEventListener("mousemove", handleMouseMove);
|
|
784
|
-
document.addEventListener("mouseup", handleMouseUp);
|
|
785
|
-
},
|
|
786
|
-
[columnWidths, minWidth, maxWidth]
|
|
787
|
-
);
|
|
788
|
-
const resetColumnWidth = (0, import_react4.useCallback)(
|
|
789
|
-
(columnId) => {
|
|
790
|
-
if (initialWidths[columnId]) {
|
|
791
|
-
setColumnWidths((prev) => ({
|
|
792
|
-
...prev,
|
|
793
|
-
[columnId]: initialWidths[columnId]
|
|
794
|
-
}));
|
|
795
|
-
}
|
|
796
|
-
},
|
|
797
|
-
[initialWidths]
|
|
798
|
-
);
|
|
799
|
-
const resetAllWidths = (0, import_react4.useCallback)(() => {
|
|
800
|
-
setColumnWidths({ ...initialWidths });
|
|
801
|
-
}, [initialWidths]);
|
|
802
|
-
return {
|
|
803
|
-
columnWidths,
|
|
804
|
-
setColumnWidths,
|
|
805
|
-
isResizing,
|
|
806
|
-
resizingColumn,
|
|
807
|
-
startResize,
|
|
808
|
-
resetColumnWidth,
|
|
809
|
-
resetAllWidths
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// src/components/ColumnFilter.tsx
|
|
814
|
-
var import_react6 = require("react");
|
|
815
|
-
|
|
816
|
-
// src/components/NumericRangeFilter.tsx
|
|
817
|
-
var import_react5 = require("react");
|
|
818
|
-
var import_jsx_runtime = require("react/jsx-runtime");
|
|
819
|
-
function NumericRangeFilter({
|
|
820
|
-
dataMin,
|
|
821
|
-
dataMax,
|
|
822
|
-
currentRange,
|
|
823
|
-
onChange
|
|
824
|
-
}) {
|
|
825
|
-
const [localMin, setLocalMin] = (0, import_react5.useState)(currentRange?.min ?? null);
|
|
826
|
-
const [localMax, setLocalMax] = (0, import_react5.useState)(currentRange?.max ?? null);
|
|
827
|
-
const step = (0, import_react5.useMemo)(() => {
|
|
828
|
-
const range = dataMax - dataMin;
|
|
829
|
-
if (range === 0) return 1;
|
|
830
|
-
if (range <= 1) return 0.01;
|
|
831
|
-
if (range <= 10) return 0.1;
|
|
832
|
-
if (range <= 100) return 1;
|
|
833
|
-
if (range <= 1e3) return 10;
|
|
834
|
-
return Math.pow(10, Math.floor(Math.log10(range)) - 2);
|
|
835
|
-
}, [dataMin, dataMax]);
|
|
836
|
-
const formatValue = (0, import_react5.useCallback)((val) => {
|
|
837
|
-
if (val === null) return "";
|
|
838
|
-
if (Number.isInteger(val)) return val.toLocaleString();
|
|
839
|
-
return val.toLocaleString(void 0, { maximumFractionDigits: 2 });
|
|
840
|
-
}, []);
|
|
841
|
-
const isFilterActive = localMin !== null || localMax !== null;
|
|
842
|
-
const minPercent = (0, import_react5.useMemo)(() => {
|
|
843
|
-
if (localMin === null || dataMax === dataMin) return 0;
|
|
844
|
-
return (localMin - dataMin) / (dataMax - dataMin) * 100;
|
|
845
|
-
}, [localMin, dataMin, dataMax]);
|
|
846
|
-
const maxPercent = (0, import_react5.useMemo)(() => {
|
|
847
|
-
if (localMax === null || dataMax === dataMin) return 100;
|
|
848
|
-
return (localMax - dataMin) / (dataMax - dataMin) * 100;
|
|
849
|
-
}, [localMax, dataMin, dataMax]);
|
|
850
|
-
const handleMinSlider = (0, import_react5.useCallback)((event) => {
|
|
851
|
-
const value = Number.parseFloat(event.target.value);
|
|
852
|
-
setLocalMin((prev) => {
|
|
853
|
-
if (localMax !== null && value > localMax) {
|
|
854
|
-
return localMax;
|
|
855
|
-
}
|
|
856
|
-
return value;
|
|
857
|
-
});
|
|
858
|
-
}, [localMax]);
|
|
859
|
-
const handleMaxSlider = (0, import_react5.useCallback)((event) => {
|
|
860
|
-
const value = Number.parseFloat(event.target.value);
|
|
861
|
-
setLocalMax((prev) => {
|
|
862
|
-
if (localMin !== null && value < localMin) {
|
|
863
|
-
return localMin;
|
|
864
|
-
}
|
|
865
|
-
return value;
|
|
866
|
-
});
|
|
867
|
-
}, [localMin]);
|
|
868
|
-
const handleSliderChange = (0, import_react5.useCallback)(() => {
|
|
869
|
-
if (localMin === null && localMax === null) {
|
|
870
|
-
onChange(null);
|
|
871
|
-
} else {
|
|
872
|
-
onChange({ min: localMin, max: localMax });
|
|
873
|
-
}
|
|
874
|
-
}, [localMin, localMax, onChange]);
|
|
875
|
-
const handleMinInput = (0, import_react5.useCallback)((event) => {
|
|
876
|
-
const value = event.target.value === "" ? null : Number.parseFloat(event.target.value);
|
|
877
|
-
if (value !== null && !Number.isNaN(value)) {
|
|
878
|
-
setLocalMin(Math.max(dataMin, Math.min(value, localMax ?? dataMax)));
|
|
879
|
-
} else if (value === null) {
|
|
880
|
-
setLocalMin(null);
|
|
881
|
-
}
|
|
882
|
-
}, [dataMin, dataMax, localMax]);
|
|
883
|
-
const handleMaxInput = (0, import_react5.useCallback)((event) => {
|
|
884
|
-
const value = event.target.value === "" ? null : Number.parseFloat(event.target.value);
|
|
885
|
-
if (value !== null && !Number.isNaN(value)) {
|
|
886
|
-
setLocalMax(Math.min(dataMax, Math.max(value, localMin ?? dataMin)));
|
|
887
|
-
} else if (value === null) {
|
|
888
|
-
setLocalMax(null);
|
|
889
|
-
}
|
|
890
|
-
}, [dataMin, dataMax, localMin]);
|
|
891
|
-
const handleInputBlur = (0, import_react5.useCallback)(() => {
|
|
892
|
-
if (localMin === null && localMax === null) {
|
|
893
|
-
onChange(null);
|
|
894
|
-
} else {
|
|
895
|
-
onChange({ min: localMin, max: localMax });
|
|
896
|
-
}
|
|
897
|
-
}, [localMin, localMax, onChange]);
|
|
898
|
-
const clearFilter = (0, import_react5.useCallback)(() => {
|
|
899
|
-
setLocalMin(null);
|
|
900
|
-
setLocalMax(null);
|
|
901
|
-
onChange(null);
|
|
902
|
-
}, [onChange]);
|
|
903
|
-
const setFullRange = (0, import_react5.useCallback)(() => {
|
|
904
|
-
setLocalMin(dataMin);
|
|
905
|
-
setLocalMax(dataMax);
|
|
906
|
-
onChange({ min: dataMin, max: dataMax });
|
|
907
|
-
}, [dataMin, dataMax, onChange]);
|
|
908
|
-
(0, import_react5.useEffect)(() => {
|
|
909
|
-
setLocalMin(currentRange?.min ?? null);
|
|
910
|
-
setLocalMax(currentRange?.max ?? null);
|
|
911
|
-
}, [currentRange]);
|
|
912
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-range-filter", children: [
|
|
913
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-range-info", children: [
|
|
914
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "vpg-range-label", children: "Data range:" }),
|
|
915
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "vpg-range-bounds", children: [
|
|
916
|
-
formatValue(dataMin),
|
|
917
|
-
" \u2013 ",
|
|
918
|
-
formatValue(dataMax)
|
|
919
|
-
] })
|
|
920
|
-
] }),
|
|
921
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-slider-container", children: [
|
|
922
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vpg-slider-track", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
923
|
-
"div",
|
|
924
|
-
{
|
|
925
|
-
className: "vpg-slider-fill",
|
|
926
|
-
style: {
|
|
927
|
-
left: `${minPercent}%`,
|
|
928
|
-
right: `${100 - maxPercent}%`
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
) }),
|
|
932
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
933
|
-
"input",
|
|
934
|
-
{
|
|
935
|
-
type: "range",
|
|
936
|
-
className: "vpg-slider vpg-slider-min",
|
|
937
|
-
min: dataMin,
|
|
938
|
-
max: dataMax,
|
|
939
|
-
step,
|
|
940
|
-
value: localMin ?? dataMin,
|
|
941
|
-
onChange: handleMinSlider,
|
|
942
|
-
onMouseUp: handleSliderChange,
|
|
943
|
-
onTouchEnd: handleSliderChange
|
|
944
|
-
}
|
|
945
|
-
),
|
|
946
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
947
|
-
"input",
|
|
948
|
-
{
|
|
949
|
-
type: "range",
|
|
950
|
-
className: "vpg-slider vpg-slider-max",
|
|
951
|
-
min: dataMin,
|
|
952
|
-
max: dataMax,
|
|
953
|
-
step,
|
|
954
|
-
value: localMax ?? dataMax,
|
|
955
|
-
onChange: handleMaxSlider,
|
|
956
|
-
onMouseUp: handleSliderChange,
|
|
957
|
-
onTouchEnd: handleSliderChange
|
|
958
|
-
}
|
|
959
|
-
)
|
|
960
|
-
] }),
|
|
961
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-range-inputs", children: [
|
|
962
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-input-group", children: [
|
|
963
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "vpg-input-label", children: "Min" }),
|
|
964
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
965
|
-
"input",
|
|
966
|
-
{
|
|
967
|
-
type: "number",
|
|
968
|
-
className: "vpg-range-input",
|
|
969
|
-
placeholder: formatValue(dataMin),
|
|
970
|
-
value: localMin ?? "",
|
|
971
|
-
step,
|
|
972
|
-
onChange: handleMinInput,
|
|
973
|
-
onBlur: handleInputBlur
|
|
974
|
-
}
|
|
975
|
-
)
|
|
976
|
-
] }),
|
|
977
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "vpg-input-separator", children: "to" }),
|
|
978
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-input-group", children: [
|
|
979
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "vpg-input-label", children: "Max" }),
|
|
980
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
981
|
-
"input",
|
|
982
|
-
{
|
|
983
|
-
type: "number",
|
|
984
|
-
className: "vpg-range-input",
|
|
985
|
-
placeholder: formatValue(dataMax),
|
|
986
|
-
value: localMax ?? "",
|
|
987
|
-
step,
|
|
988
|
-
onChange: handleMaxInput,
|
|
989
|
-
onBlur: handleInputBlur
|
|
990
|
-
}
|
|
991
|
-
)
|
|
992
|
-
] })
|
|
993
|
-
] }),
|
|
994
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-range-actions", children: [
|
|
995
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
996
|
-
"button",
|
|
997
|
-
{
|
|
998
|
-
className: "vpg-range-btn",
|
|
999
|
-
disabled: !isFilterActive,
|
|
1000
|
-
onClick: clearFilter,
|
|
1001
|
-
children: [
|
|
1002
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1003
|
-
"path",
|
|
1004
|
-
{
|
|
1005
|
-
strokeLinecap: "round",
|
|
1006
|
-
strokeLinejoin: "round",
|
|
1007
|
-
strokeWidth: 2,
|
|
1008
|
-
d: "M6 18L18 6M6 6l12 12"
|
|
1009
|
-
}
|
|
1010
|
-
) }),
|
|
1011
|
-
"Clear"
|
|
1012
|
-
]
|
|
1013
|
-
}
|
|
1014
|
-
),
|
|
1015
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { className: "vpg-range-btn", onClick: setFullRange, children: [
|
|
1016
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1017
|
-
"path",
|
|
1018
|
-
{
|
|
1019
|
-
strokeLinecap: "round",
|
|
1020
|
-
strokeLinejoin: "round",
|
|
1021
|
-
strokeWidth: 2,
|
|
1022
|
-
d: "M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
|
|
1023
|
-
}
|
|
1024
|
-
) }),
|
|
1025
|
-
"Full Range"
|
|
1026
|
-
] })
|
|
1027
|
-
] }),
|
|
1028
|
-
isFilterActive && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vpg-filter-summary", children: [
|
|
1029
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className: "vpg-icon-xs", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1030
|
-
"path",
|
|
1031
|
-
{
|
|
1032
|
-
strokeLinecap: "round",
|
|
1033
|
-
strokeLinejoin: "round",
|
|
1034
|
-
strokeWidth: 2,
|
|
1035
|
-
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"
|
|
1036
|
-
}
|
|
1037
|
-
) }),
|
|
1038
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
|
|
1039
|
-
"Showing values",
|
|
1040
|
-
" ",
|
|
1041
|
-
localMin !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
|
|
1042
|
-
"\u2265 ",
|
|
1043
|
-
formatValue(localMin)
|
|
1044
|
-
] }),
|
|
1045
|
-
localMin !== null && localMax !== null && " and ",
|
|
1046
|
-
localMax !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
|
|
1047
|
-
"\u2264 ",
|
|
1048
|
-
formatValue(localMax)
|
|
1049
|
-
] })
|
|
1050
|
-
] })
|
|
1051
|
-
] })
|
|
1052
|
-
] });
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
// src/components/ColumnFilter.tsx
|
|
1056
|
-
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
1057
|
-
function ColumnFilter({
|
|
1058
|
-
columnName,
|
|
1059
|
-
stats,
|
|
1060
|
-
selectedValues,
|
|
1061
|
-
sortDirection,
|
|
1062
|
-
numericRange,
|
|
1063
|
-
onFilter,
|
|
1064
|
-
onSort,
|
|
1065
|
-
onClose,
|
|
1066
|
-
onRangeFilter
|
|
1067
|
-
}) {
|
|
1068
|
-
const [searchQuery, setSearchQuery] = (0, import_react6.useState)("");
|
|
1069
|
-
const [localSelected, setLocalSelected] = (0, import_react6.useState)(new Set(selectedValues));
|
|
1070
|
-
const dropdownRef = (0, import_react6.useRef)(null);
|
|
1071
|
-
const searchInputRef = (0, import_react6.useRef)(null);
|
|
1072
|
-
const isNumericColumn = stats.type === "number" && stats.numericMin !== void 0 && stats.numericMax !== void 0;
|
|
1073
|
-
const [filterMode, setFilterMode] = (0, import_react6.useState)(numericRange ? "range" : "values");
|
|
1074
|
-
const [localRange, setLocalRange] = (0, import_react6.useState)(numericRange ?? null);
|
|
1075
|
-
const hasBlankValues = stats.nullCount > 0;
|
|
1076
|
-
const filteredValues = (0, import_react6.useMemo)(() => {
|
|
1077
|
-
const values = stats.uniqueValues;
|
|
1078
|
-
if (!searchQuery) return values;
|
|
1079
|
-
const query = searchQuery.toLowerCase();
|
|
1080
|
-
return values.filter((v) => v.toLowerCase().includes(query));
|
|
1081
|
-
}, [stats.uniqueValues, searchQuery]);
|
|
1082
|
-
const allValues = (0, import_react6.useMemo)(() => {
|
|
1083
|
-
const values = [...filteredValues];
|
|
1084
|
-
if (hasBlankValues && (!searchQuery || "(blank)".includes(searchQuery.toLowerCase()))) {
|
|
1085
|
-
values.unshift("(blank)");
|
|
1086
|
-
}
|
|
1087
|
-
return values;
|
|
1088
|
-
}, [filteredValues, hasBlankValues, searchQuery]);
|
|
1089
|
-
const isAllSelected = (0, import_react6.useMemo)(
|
|
1090
|
-
() => allValues.every((v) => localSelected.has(v)),
|
|
1091
|
-
[allValues, localSelected]
|
|
1092
|
-
);
|
|
1093
|
-
const toggleValue = (0, import_react6.useCallback)((value) => {
|
|
1094
|
-
setLocalSelected((prev) => {
|
|
1095
|
-
const next = new Set(prev);
|
|
1096
|
-
if (next.has(value)) {
|
|
1097
|
-
next.delete(value);
|
|
1098
|
-
} else {
|
|
1099
|
-
next.add(value);
|
|
1100
|
-
}
|
|
1101
|
-
return next;
|
|
1102
|
-
});
|
|
1103
|
-
}, []);
|
|
1104
|
-
const selectAll = (0, import_react6.useCallback)(() => {
|
|
1105
|
-
setLocalSelected((prev) => {
|
|
1106
|
-
const next = new Set(prev);
|
|
1107
|
-
for (const value of allValues) {
|
|
1108
|
-
next.add(value);
|
|
1109
|
-
}
|
|
1110
|
-
return next;
|
|
1111
|
-
});
|
|
1112
|
-
}, [allValues]);
|
|
1113
|
-
const clearAll = (0, import_react6.useCallback)(() => {
|
|
1114
|
-
setLocalSelected(/* @__PURE__ */ new Set());
|
|
1115
|
-
}, []);
|
|
1116
|
-
const applyFilter = (0, import_react6.useCallback)(() => {
|
|
1117
|
-
if (localSelected.size === 0) {
|
|
1118
|
-
onFilter([]);
|
|
1119
|
-
} else {
|
|
1120
|
-
onFilter(Array.from(localSelected));
|
|
1121
|
-
}
|
|
1122
|
-
onClose();
|
|
1123
|
-
}, [localSelected, onFilter, onClose]);
|
|
1124
|
-
const sortAscending = (0, import_react6.useCallback)(() => {
|
|
1125
|
-
onSort(sortDirection === "asc" ? null : "asc");
|
|
1126
|
-
}, [sortDirection, onSort]);
|
|
1127
|
-
const sortDescending = (0, import_react6.useCallback)(() => {
|
|
1128
|
-
onSort(sortDirection === "desc" ? null : "desc");
|
|
1129
|
-
}, [sortDirection, onSort]);
|
|
1130
|
-
const clearFilter = (0, import_react6.useCallback)(() => {
|
|
1131
|
-
setLocalSelected(/* @__PURE__ */ new Set());
|
|
1132
|
-
onFilter([]);
|
|
1133
|
-
onClose();
|
|
1134
|
-
}, [onFilter, onClose]);
|
|
1135
|
-
const handleRangeChange = (0, import_react6.useCallback)((range) => {
|
|
1136
|
-
setLocalRange(range);
|
|
1137
|
-
}, []);
|
|
1138
|
-
const applyRangeFilter = (0, import_react6.useCallback)(() => {
|
|
1139
|
-
onRangeFilter?.(localRange);
|
|
1140
|
-
onClose();
|
|
1141
|
-
}, [localRange, onRangeFilter, onClose]);
|
|
1142
|
-
const clearRangeFilter = (0, import_react6.useCallback)(() => {
|
|
1143
|
-
setLocalRange(null);
|
|
1144
|
-
onRangeFilter?.(null);
|
|
1145
|
-
onClose();
|
|
1146
|
-
}, [onRangeFilter, onClose]);
|
|
1147
|
-
(0, import_react6.useEffect)(() => {
|
|
1148
|
-
const handleClickOutside = (event) => {
|
|
1149
|
-
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
1150
|
-
onClose();
|
|
1151
|
-
}
|
|
1152
|
-
};
|
|
1153
|
-
document.addEventListener("mousedown", handleClickOutside);
|
|
1154
|
-
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
1155
|
-
}, [onClose]);
|
|
1156
|
-
(0, import_react6.useEffect)(() => {
|
|
1157
|
-
const handleKeydown = (event) => {
|
|
1158
|
-
if (event.key === "Escape") {
|
|
1159
|
-
onClose();
|
|
1160
|
-
} else if (event.key === "Enter" && event.ctrlKey) {
|
|
1161
|
-
applyFilter();
|
|
1162
|
-
}
|
|
1163
|
-
};
|
|
1164
|
-
document.addEventListener("keydown", handleKeydown);
|
|
1165
|
-
return () => document.removeEventListener("keydown", handleKeydown);
|
|
1166
|
-
}, [onClose, applyFilter]);
|
|
1167
|
-
(0, import_react6.useEffect)(() => {
|
|
1168
|
-
searchInputRef.current?.focus();
|
|
1169
|
-
}, []);
|
|
1170
|
-
(0, import_react6.useEffect)(() => {
|
|
1171
|
-
setLocalSelected(new Set(selectedValues));
|
|
1172
|
-
}, [selectedValues]);
|
|
1173
|
-
(0, import_react6.useEffect)(() => {
|
|
1174
|
-
setLocalRange(numericRange ?? null);
|
|
1175
|
-
if (numericRange) {
|
|
1176
|
-
setFilterMode("range");
|
|
1177
|
-
}
|
|
1178
|
-
}, [numericRange]);
|
|
1179
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { ref: dropdownRef, className: "vpg-filter-dropdown", children: [
|
|
1180
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-filter-header", children: [
|
|
1181
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "vpg-filter-title", children: columnName }),
|
|
1182
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "vpg-filter-count", children: [
|
|
1183
|
-
stats.uniqueValues.length.toLocaleString(),
|
|
1184
|
-
" unique"
|
|
1185
|
-
] })
|
|
1186
|
-
] }),
|
|
1187
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-sort-controls", children: [
|
|
1188
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1189
|
-
"button",
|
|
1190
|
-
{
|
|
1191
|
-
className: `vpg-sort-btn ${sortDirection === "asc" ? "active" : ""}`,
|
|
1192
|
-
title: isNumericColumn ? "Sort Low to High" : "Sort A to Z",
|
|
1193
|
-
onClick: sortAscending,
|
|
1194
|
-
children: [
|
|
1195
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1196
|
-
"path",
|
|
1197
|
-
{
|
|
1198
|
-
strokeLinecap: "round",
|
|
1199
|
-
strokeLinejoin: "round",
|
|
1200
|
-
strokeWidth: 2,
|
|
1201
|
-
d: "M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"
|
|
1202
|
-
}
|
|
1203
|
-
) }),
|
|
1204
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: isNumericColumn ? "1\u21929" : "A\u2192Z" })
|
|
1205
|
-
]
|
|
1206
|
-
}
|
|
1207
|
-
),
|
|
1208
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1209
|
-
"button",
|
|
1210
|
-
{
|
|
1211
|
-
className: `vpg-sort-btn ${sortDirection === "desc" ? "active" : ""}`,
|
|
1212
|
-
title: isNumericColumn ? "Sort High to Low" : "Sort Z to A",
|
|
1213
|
-
onClick: sortDescending,
|
|
1214
|
-
children: [
|
|
1215
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1216
|
-
"path",
|
|
1217
|
-
{
|
|
1218
|
-
strokeLinecap: "round",
|
|
1219
|
-
strokeLinejoin: "round",
|
|
1220
|
-
strokeWidth: 2,
|
|
1221
|
-
d: "M3 4h13M3 8h9m-9 4h9m5-4v12m0 0l-4-4m4 4l4-4"
|
|
1222
|
-
}
|
|
1223
|
-
) }),
|
|
1224
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: isNumericColumn ? "9\u21921" : "Z\u2192A" })
|
|
1225
|
-
]
|
|
1226
|
-
}
|
|
1227
|
-
)
|
|
1228
|
-
] }),
|
|
1229
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "vpg-divider" }),
|
|
1230
|
-
isNumericColumn && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-filter-tabs", children: [
|
|
1231
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1232
|
-
"button",
|
|
1233
|
-
{
|
|
1234
|
-
className: `vpg-tab-btn ${filterMode === "values" ? "active" : ""}`,
|
|
1235
|
-
onClick: () => setFilterMode("values"),
|
|
1236
|
-
children: [
|
|
1237
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1238
|
-
"path",
|
|
1239
|
-
{
|
|
1240
|
-
strokeLinecap: "round",
|
|
1241
|
-
strokeLinejoin: "round",
|
|
1242
|
-
strokeWidth: 2,
|
|
1243
|
-
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"
|
|
1244
|
-
}
|
|
1245
|
-
) }),
|
|
1246
|
-
"Values"
|
|
1247
|
-
]
|
|
1248
|
-
}
|
|
1249
|
-
),
|
|
1250
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1251
|
-
"button",
|
|
1252
|
-
{
|
|
1253
|
-
className: `vpg-tab-btn ${filterMode === "range" ? "active" : ""}`,
|
|
1254
|
-
onClick: () => setFilterMode("range"),
|
|
1255
|
-
children: [
|
|
1256
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1257
|
-
"path",
|
|
1258
|
-
{
|
|
1259
|
-
strokeLinecap: "round",
|
|
1260
|
-
strokeLinejoin: "round",
|
|
1261
|
-
strokeWidth: 2,
|
|
1262
|
-
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"
|
|
1263
|
-
}
|
|
1264
|
-
) }),
|
|
1265
|
-
"Range"
|
|
1266
|
-
]
|
|
1267
|
-
}
|
|
1268
|
-
)
|
|
1269
|
-
] }),
|
|
1270
|
-
(!isNumericColumn || filterMode === "values") && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
1271
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-search-container", children: [
|
|
1272
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-search-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1273
|
-
"path",
|
|
1274
|
-
{
|
|
1275
|
-
strokeLinecap: "round",
|
|
1276
|
-
strokeLinejoin: "round",
|
|
1277
|
-
strokeWidth: 2,
|
|
1278
|
-
d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
1279
|
-
}
|
|
1280
|
-
) }),
|
|
1281
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1282
|
-
"input",
|
|
1283
|
-
{
|
|
1284
|
-
ref: searchInputRef,
|
|
1285
|
-
type: "text",
|
|
1286
|
-
value: searchQuery,
|
|
1287
|
-
onChange: (e) => setSearchQuery(e.target.value),
|
|
1288
|
-
placeholder: "Search values...",
|
|
1289
|
-
className: "vpg-search-input"
|
|
1290
|
-
}
|
|
1291
|
-
),
|
|
1292
|
-
searchQuery && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "vpg-clear-search", onClick: () => setSearchQuery(""), children: "\xD7" })
|
|
1293
|
-
] }),
|
|
1294
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-bulk-actions", children: [
|
|
1295
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { className: "vpg-bulk-btn", onClick: selectAll, children: [
|
|
1296
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1297
|
-
"path",
|
|
1298
|
-
{
|
|
1299
|
-
strokeLinecap: "round",
|
|
1300
|
-
strokeLinejoin: "round",
|
|
1301
|
-
strokeWidth: 2,
|
|
1302
|
-
d: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
1303
|
-
}
|
|
1304
|
-
) }),
|
|
1305
|
-
"Select All"
|
|
1306
|
-
] }),
|
|
1307
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { className: "vpg-bulk-btn", onClick: clearAll, children: [
|
|
1308
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: "vpg-icon-sm", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1309
|
-
"path",
|
|
1310
|
-
{
|
|
1311
|
-
strokeLinecap: "round",
|
|
1312
|
-
strokeLinejoin: "round",
|
|
1313
|
-
strokeWidth: 2,
|
|
1314
|
-
d: "M6 18L18 6M6 6l12 12"
|
|
1315
|
-
}
|
|
1316
|
-
) }),
|
|
1317
|
-
"Clear All"
|
|
1318
|
-
] })
|
|
1319
|
-
] }),
|
|
1320
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-values-list", children: [
|
|
1321
|
-
allValues.map((value) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1322
|
-
"label",
|
|
1323
|
-
{
|
|
1324
|
-
className: `vpg-value-item ${localSelected.has(value) ? "selected" : ""}`,
|
|
1325
|
-
children: [
|
|
1326
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1327
|
-
"input",
|
|
1328
|
-
{
|
|
1329
|
-
type: "checkbox",
|
|
1330
|
-
checked: localSelected.has(value),
|
|
1331
|
-
onChange: () => toggleValue(value),
|
|
1332
|
-
className: "vpg-value-checkbox"
|
|
1333
|
-
}
|
|
1334
|
-
),
|
|
1335
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: `vpg-value-text ${value === "(blank)" ? "vpg-blank" : ""}`, children: value })
|
|
1336
|
-
]
|
|
1337
|
-
},
|
|
1338
|
-
value
|
|
1339
|
-
)),
|
|
1340
|
-
allValues.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "vpg-no-results", children: "No matching values" })
|
|
1341
|
-
] }),
|
|
1342
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "vpg-filter-footer", children: [
|
|
1343
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "vpg-btn-clear", onClick: clearFilter, children: "Clear Filter" }),
|
|
1344
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "vpg-btn-apply", onClick: applyFilter, children: "Apply" })
|
|
1345
|
-
] })
|
|
1346
|
-
] }),
|
|
1347
|
-
isNumericColumn && filterMode === "range" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
1348
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1349
|
-
NumericRangeFilter,
|
|
1350
|
-
{
|
|
1351
|
-
dataMin: stats.numericMin,
|
|
1352
|
-
dataMax: stats.numericMax,
|
|
1353
|
-
currentRange: localRange,
|
|
1354
|
-
onChange: handleRangeChange
|
|
1114
|
+
setPageSize: updatePageSize
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
function useGlobalSearch(data, columns) {
|
|
1118
|
+
const [searchTerm, setSearchTerm] = (0, import_react5.useState)("");
|
|
1119
|
+
const [caseSensitive, setCaseSensitive] = (0, import_react5.useState)(false);
|
|
1120
|
+
const filteredData = (0, import_react5.useMemo)(() => {
|
|
1121
|
+
if (!searchTerm.trim()) {
|
|
1122
|
+
return data;
|
|
1123
|
+
}
|
|
1124
|
+
const term = caseSensitive ? searchTerm.trim() : searchTerm.trim().toLowerCase();
|
|
1125
|
+
return data.filter((row) => {
|
|
1126
|
+
for (const col of columns) {
|
|
1127
|
+
const value = row[col];
|
|
1128
|
+
if (value === null || value === void 0)
|
|
1129
|
+
continue;
|
|
1130
|
+
const strValue = caseSensitive ? String(value) : String(value).toLowerCase();
|
|
1131
|
+
if (strValue.includes(term)) {
|
|
1132
|
+
return true;
|
|
1355
1133
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
]
|
|
1134
|
+
}
|
|
1135
|
+
return false;
|
|
1136
|
+
});
|
|
1137
|
+
}, [data, columns, searchTerm, caseSensitive]);
|
|
1138
|
+
const clearSearch = (0, import_react5.useCallback)(() => {
|
|
1139
|
+
setSearchTerm("");
|
|
1140
|
+
}, []);
|
|
1141
|
+
return {
|
|
1142
|
+
searchTerm,
|
|
1143
|
+
setSearchTerm,
|
|
1144
|
+
caseSensitive,
|
|
1145
|
+
setCaseSensitive,
|
|
1146
|
+
filteredData,
|
|
1147
|
+
clearSearch
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
function useRowSelection(data) {
|
|
1151
|
+
const [selectedRowIndices, setSelectedRowIndices] = (0, import_react5.useState)(/* @__PURE__ */ new Set());
|
|
1152
|
+
const selectedRows = (0, import_react5.useMemo)(() => {
|
|
1153
|
+
return Array.from(selectedRowIndices).sort((a, b) => a - b).map((idx) => data[idx]).filter(Boolean);
|
|
1154
|
+
}, [data, selectedRowIndices]);
|
|
1155
|
+
const allSelected = (0, import_react5.useMemo)(() => {
|
|
1156
|
+
return data.length > 0 && selectedRowIndices.size === data.length;
|
|
1157
|
+
}, [data.length, selectedRowIndices.size]);
|
|
1158
|
+
const someSelected = (0, import_react5.useMemo)(() => {
|
|
1159
|
+
return selectedRowIndices.size > 0 && selectedRowIndices.size < data.length;
|
|
1160
|
+
}, [data.length, selectedRowIndices.size]);
|
|
1161
|
+
const toggleRow = (0, import_react5.useCallback)((index) => {
|
|
1162
|
+
setSelectedRowIndices((prev) => {
|
|
1163
|
+
const next = new Set(prev);
|
|
1164
|
+
if (next.has(index)) {
|
|
1165
|
+
next.delete(index);
|
|
1166
|
+
} else {
|
|
1167
|
+
next.add(index);
|
|
1168
|
+
}
|
|
1169
|
+
return next;
|
|
1170
|
+
});
|
|
1171
|
+
}, []);
|
|
1172
|
+
const selectRow = (0, import_react5.useCallback)((index) => {
|
|
1173
|
+
setSelectedRowIndices((prev) => /* @__PURE__ */ new Set([...prev, index]));
|
|
1174
|
+
}, []);
|
|
1175
|
+
const deselectRow = (0, import_react5.useCallback)((index) => {
|
|
1176
|
+
setSelectedRowIndices((prev) => {
|
|
1177
|
+
const next = new Set(prev);
|
|
1178
|
+
next.delete(index);
|
|
1179
|
+
return next;
|
|
1180
|
+
});
|
|
1181
|
+
}, []);
|
|
1182
|
+
const selectAll = (0, import_react5.useCallback)(() => {
|
|
1183
|
+
setSelectedRowIndices(new Set(data.map((_, idx) => idx)));
|
|
1184
|
+
}, [data]);
|
|
1185
|
+
const deselectAll = (0, import_react5.useCallback)(() => {
|
|
1186
|
+
setSelectedRowIndices(/* @__PURE__ */ new Set());
|
|
1187
|
+
}, []);
|
|
1188
|
+
const toggleAll = (0, import_react5.useCallback)(() => {
|
|
1189
|
+
if (allSelected) {
|
|
1190
|
+
deselectAll();
|
|
1191
|
+
} else {
|
|
1192
|
+
selectAll();
|
|
1193
|
+
}
|
|
1194
|
+
}, [allSelected, selectAll, deselectAll]);
|
|
1195
|
+
const isSelected = (0, import_react5.useCallback)(
|
|
1196
|
+
(index) => {
|
|
1197
|
+
return selectedRowIndices.has(index);
|
|
1198
|
+
},
|
|
1199
|
+
[selectedRowIndices]
|
|
1200
|
+
);
|
|
1201
|
+
const selectRange = (0, import_react5.useCallback)((startIndex, endIndex) => {
|
|
1202
|
+
const min = Math.min(startIndex, endIndex);
|
|
1203
|
+
const max = Math.max(startIndex, endIndex);
|
|
1204
|
+
setSelectedRowIndices((prev) => {
|
|
1205
|
+
const next = new Set(prev);
|
|
1206
|
+
for (let i = min; i <= max; i++) {
|
|
1207
|
+
next.add(i);
|
|
1208
|
+
}
|
|
1209
|
+
return next;
|
|
1210
|
+
});
|
|
1211
|
+
}, []);
|
|
1212
|
+
return {
|
|
1213
|
+
selectedRowIndices,
|
|
1214
|
+
selectedRows,
|
|
1215
|
+
allSelected,
|
|
1216
|
+
someSelected,
|
|
1217
|
+
toggleRow,
|
|
1218
|
+
selectRow,
|
|
1219
|
+
deselectRow,
|
|
1220
|
+
selectAll,
|
|
1221
|
+
deselectAll,
|
|
1222
|
+
toggleAll,
|
|
1223
|
+
isSelected,
|
|
1224
|
+
selectRange
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
function useColumnResize(initialWidths, minWidth = 60, maxWidth = 600) {
|
|
1228
|
+
const [columnWidths, setColumnWidths] = (0, import_react5.useState)({ ...initialWidths });
|
|
1229
|
+
const [isResizing, setIsResizing] = (0, import_react5.useState)(false);
|
|
1230
|
+
const [resizingColumn, setResizingColumn] = (0, import_react5.useState)(null);
|
|
1231
|
+
const startResize = (0, import_react5.useCallback)(
|
|
1232
|
+
(columnId, event) => {
|
|
1233
|
+
setIsResizing(true);
|
|
1234
|
+
setResizingColumn(columnId);
|
|
1235
|
+
const startX = event.clientX;
|
|
1236
|
+
const startWidth = columnWidths[columnId] || 150;
|
|
1237
|
+
const handleMouseMove = (e) => {
|
|
1238
|
+
const diff = e.clientX - startX;
|
|
1239
|
+
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + diff));
|
|
1240
|
+
setColumnWidths((prev) => ({
|
|
1241
|
+
...prev,
|
|
1242
|
+
[columnId]: newWidth
|
|
1243
|
+
}));
|
|
1244
|
+
};
|
|
1245
|
+
const handleMouseUp = () => {
|
|
1246
|
+
setIsResizing(false);
|
|
1247
|
+
setResizingColumn(null);
|
|
1248
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
1249
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
1250
|
+
};
|
|
1251
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
1252
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
1253
|
+
},
|
|
1254
|
+
[columnWidths, minWidth, maxWidth]
|
|
1255
|
+
);
|
|
1256
|
+
const resetColumnWidth = (0, import_react5.useCallback)(
|
|
1257
|
+
(columnId) => {
|
|
1258
|
+
if (initialWidths[columnId]) {
|
|
1259
|
+
setColumnWidths((prev) => ({
|
|
1260
|
+
...prev,
|
|
1261
|
+
[columnId]: initialWidths[columnId]
|
|
1262
|
+
}));
|
|
1263
|
+
}
|
|
1264
|
+
},
|
|
1265
|
+
[initialWidths]
|
|
1266
|
+
);
|
|
1267
|
+
const resetAllWidths = (0, import_react5.useCallback)(() => {
|
|
1268
|
+
setColumnWidths({ ...initialWidths });
|
|
1269
|
+
}, [initialWidths]);
|
|
1270
|
+
return {
|
|
1271
|
+
columnWidths,
|
|
1272
|
+
setColumnWidths,
|
|
1273
|
+
isResizing,
|
|
1274
|
+
resizingColumn,
|
|
1275
|
+
startResize,
|
|
1276
|
+
resetColumnWidth,
|
|
1277
|
+
resetAllWidths
|
|
1278
|
+
};
|
|
1363
1279
|
}
|
|
1364
1280
|
|
|
1365
|
-
// src/
|
|
1366
|
-
var
|
|
1367
|
-
var
|
|
1281
|
+
// src/hooks/useLicense.ts
|
|
1282
|
+
var import_tinypivot_core4 = require("@smallwebco/tinypivot-core");
|
|
1283
|
+
var import_react6 = require("react");
|
|
1284
|
+
var globalLicenseInfo = (0, import_tinypivot_core4.getFreeLicenseInfo)();
|
|
1285
|
+
var globalDemoMode = false;
|
|
1286
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
1287
|
+
function notifyListeners() {
|
|
1288
|
+
listeners.forEach((listener) => listener());
|
|
1289
|
+
}
|
|
1290
|
+
async function setLicenseKey(key) {
|
|
1291
|
+
globalLicenseInfo = await (0, import_tinypivot_core4.validateLicenseKey)(key);
|
|
1292
|
+
if (!globalLicenseInfo.isValid) {
|
|
1293
|
+
console.warn("[TinyPivot] Invalid or expired license key. Running in free mode.");
|
|
1294
|
+
} else if (globalLicenseInfo.type !== "free") {
|
|
1295
|
+
console.info(`[TinyPivot] Pro license activated (${globalLicenseInfo.type})`);
|
|
1296
|
+
}
|
|
1297
|
+
notifyListeners();
|
|
1298
|
+
}
|
|
1299
|
+
async function enableDemoMode(secret) {
|
|
1300
|
+
const demoLicense = await (0, import_tinypivot_core4.getDemoLicenseInfo)(secret);
|
|
1301
|
+
if (!demoLicense) {
|
|
1302
|
+
console.warn("[TinyPivot] Demo mode activation failed - invalid secret");
|
|
1303
|
+
return false;
|
|
1304
|
+
}
|
|
1305
|
+
globalDemoMode = true;
|
|
1306
|
+
globalLicenseInfo = demoLicense;
|
|
1307
|
+
console.info("[TinyPivot] Demo mode enabled - all Pro features unlocked for evaluation");
|
|
1308
|
+
notifyListeners();
|
|
1309
|
+
return true;
|
|
1310
|
+
}
|
|
1311
|
+
function configureLicenseSecret(secret) {
|
|
1312
|
+
(0, import_tinypivot_core4.configureLicenseSecret)(secret);
|
|
1313
|
+
}
|
|
1314
|
+
function useLicense() {
|
|
1315
|
+
const [, forceUpdate] = (0, import_react6.useState)({});
|
|
1316
|
+
(0, import_react6.useState)(() => {
|
|
1317
|
+
const update = () => forceUpdate({});
|
|
1318
|
+
listeners.add(update);
|
|
1319
|
+
return () => listeners.delete(update);
|
|
1320
|
+
});
|
|
1321
|
+
const isDemo = globalDemoMode;
|
|
1322
|
+
const licenseInfo = globalLicenseInfo;
|
|
1323
|
+
const isPro = (0, import_react6.useMemo)(
|
|
1324
|
+
() => globalDemoMode || (0, import_tinypivot_core4.isPro)(licenseInfo),
|
|
1325
|
+
[licenseInfo]
|
|
1326
|
+
);
|
|
1327
|
+
const canUsePivot = (0, import_react6.useMemo)(
|
|
1328
|
+
() => globalDemoMode || (0, import_tinypivot_core4.canUsePivot)(licenseInfo),
|
|
1329
|
+
[licenseInfo]
|
|
1330
|
+
);
|
|
1331
|
+
const canUseAdvancedAggregations = (0, import_react6.useMemo)(
|
|
1332
|
+
() => globalDemoMode || licenseInfo.features.advancedAggregations,
|
|
1333
|
+
[licenseInfo]
|
|
1334
|
+
);
|
|
1335
|
+
const canUsePercentageMode = (0, import_react6.useMemo)(
|
|
1336
|
+
() => globalDemoMode || licenseInfo.features.percentageMode,
|
|
1337
|
+
[licenseInfo]
|
|
1338
|
+
);
|
|
1339
|
+
const showWatermark = (0, import_react6.useMemo)(
|
|
1340
|
+
() => (0, import_tinypivot_core4.shouldShowWatermark)(licenseInfo, globalDemoMode),
|
|
1341
|
+
[licenseInfo]
|
|
1342
|
+
);
|
|
1343
|
+
const requirePro = (0, import_react6.useCallback)((feature) => {
|
|
1344
|
+
if (!isPro) {
|
|
1345
|
+
(0, import_tinypivot_core4.logProRequired)(feature);
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
1348
|
+
return true;
|
|
1349
|
+
}, [isPro]);
|
|
1350
|
+
return {
|
|
1351
|
+
licenseInfo,
|
|
1352
|
+
isDemo,
|
|
1353
|
+
isPro,
|
|
1354
|
+
canUsePivot,
|
|
1355
|
+
canUseAdvancedAggregations,
|
|
1356
|
+
canUsePercentageMode,
|
|
1357
|
+
showWatermark,
|
|
1358
|
+
requirePro
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1368
1361
|
|
|
1369
|
-
// src/
|
|
1370
|
-
var import_react7 = require("react");
|
|
1371
|
-
var import_react_dom = require("react-dom");
|
|
1362
|
+
// src/hooks/usePivotTable.ts
|
|
1372
1363
|
var import_tinypivot_core5 = require("@smallwebco/tinypivot-core");
|
|
1373
|
-
var
|
|
1374
|
-
function
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
const [
|
|
1382
|
-
const [
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
|
|
1364
|
+
var import_react7 = require("react");
|
|
1365
|
+
function usePivotTable(data) {
|
|
1366
|
+
const { canUsePivot, requirePro } = useLicense();
|
|
1367
|
+
const [rowFields, setRowFieldsState] = (0, import_react7.useState)([]);
|
|
1368
|
+
const [columnFields, setColumnFieldsState] = (0, import_react7.useState)([]);
|
|
1369
|
+
const [valueFields, setValueFields] = (0, import_react7.useState)([]);
|
|
1370
|
+
const [showRowTotals, setShowRowTotals] = (0, import_react7.useState)(true);
|
|
1371
|
+
const [showColumnTotals, setShowColumnTotals] = (0, import_react7.useState)(true);
|
|
1372
|
+
const [calculatedFields, setCalculatedFields] = (0, import_react7.useState)(() => (0, import_tinypivot_core5.loadCalculatedFields)());
|
|
1373
|
+
const [currentStorageKey, setCurrentStorageKey] = (0, import_react7.useState)(null);
|
|
1374
|
+
const availableFields = (0, import_react7.useMemo)(() => {
|
|
1375
|
+
return (0, import_tinypivot_core5.computeAvailableFields)(data);
|
|
1376
|
+
}, [data]);
|
|
1377
|
+
const unassignedFields = (0, import_react7.useMemo)(() => {
|
|
1378
|
+
return (0, import_tinypivot_core5.getUnassignedFields)(availableFields, rowFields, columnFields, valueFields);
|
|
1379
|
+
}, [availableFields, rowFields, columnFields, valueFields]);
|
|
1380
|
+
const isConfigured = (0, import_react7.useMemo)(() => {
|
|
1381
|
+
return (0, import_tinypivot_core5.isPivotConfigured)({
|
|
1382
|
+
rowFields,
|
|
1383
|
+
columnFields,
|
|
1384
|
+
valueFields,
|
|
1385
|
+
showRowTotals,
|
|
1386
|
+
showColumnTotals
|
|
1387
|
+
});
|
|
1388
|
+
}, [rowFields, columnFields, valueFields, showRowTotals, showColumnTotals]);
|
|
1389
|
+
const pivotResult = (0, import_react7.useMemo)(() => {
|
|
1390
|
+
if (!isConfigured)
|
|
1391
|
+
return null;
|
|
1392
|
+
if (!canUsePivot)
|
|
1393
|
+
return null;
|
|
1394
|
+
return (0, import_tinypivot_core5.computePivotResult)(data, {
|
|
1395
|
+
rowFields,
|
|
1396
|
+
columnFields,
|
|
1397
|
+
valueFields,
|
|
1398
|
+
showRowTotals,
|
|
1399
|
+
showColumnTotals,
|
|
1400
|
+
calculatedFields
|
|
1401
|
+
});
|
|
1402
|
+
}, [data, isConfigured, canUsePivot, rowFields, columnFields, valueFields, showRowTotals, showColumnTotals, calculatedFields]);
|
|
1386
1403
|
(0, import_react7.useEffect)(() => {
|
|
1387
|
-
if (
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1404
|
+
if (data.length === 0)
|
|
1405
|
+
return;
|
|
1406
|
+
const newKeys = Object.keys(data[0]);
|
|
1407
|
+
const storageKey = (0, import_tinypivot_core5.generateStorageKey)(newKeys);
|
|
1408
|
+
if (storageKey !== currentStorageKey) {
|
|
1409
|
+
setCurrentStorageKey(storageKey);
|
|
1410
|
+
const savedConfig = (0, import_tinypivot_core5.loadPivotConfig)(storageKey);
|
|
1411
|
+
if (savedConfig && (0, import_tinypivot_core5.isConfigValidForFields)(savedConfig, newKeys)) {
|
|
1412
|
+
setRowFieldsState(savedConfig.rowFields);
|
|
1413
|
+
setColumnFieldsState(savedConfig.columnFields);
|
|
1414
|
+
setValueFields(savedConfig.valueFields);
|
|
1415
|
+
setShowRowTotals(savedConfig.showRowTotals);
|
|
1416
|
+
setShowColumnTotals(savedConfig.showColumnTotals);
|
|
1417
|
+
if (savedConfig.calculatedFields) {
|
|
1418
|
+
setCalculatedFields(savedConfig.calculatedFields);
|
|
1419
|
+
}
|
|
1393
1420
|
} else {
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1421
|
+
const currentConfig = {
|
|
1422
|
+
rowFields,
|
|
1423
|
+
columnFields,
|
|
1424
|
+
valueFields,
|
|
1425
|
+
showRowTotals,
|
|
1426
|
+
showColumnTotals
|
|
1427
|
+
};
|
|
1428
|
+
if (!(0, import_tinypivot_core5.isConfigValidForFields)(currentConfig, newKeys)) {
|
|
1429
|
+
setRowFieldsState([]);
|
|
1430
|
+
setColumnFieldsState([]);
|
|
1431
|
+
setValueFields([]);
|
|
1432
|
+
}
|
|
1398
1433
|
}
|
|
1399
|
-
setError(null);
|
|
1400
1434
|
}
|
|
1401
|
-
}, [
|
|
1402
|
-
|
|
1403
|
-
if (!
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1435
|
+
}, [data]);
|
|
1436
|
+
(0, import_react7.useEffect)(() => {
|
|
1437
|
+
if (!currentStorageKey)
|
|
1438
|
+
return;
|
|
1439
|
+
const config = {
|
|
1440
|
+
rowFields,
|
|
1441
|
+
columnFields,
|
|
1442
|
+
valueFields,
|
|
1443
|
+
showRowTotals,
|
|
1444
|
+
showColumnTotals,
|
|
1445
|
+
calculatedFields
|
|
1446
|
+
};
|
|
1447
|
+
(0, import_tinypivot_core5.savePivotConfig)(currentStorageKey, config);
|
|
1448
|
+
}, [currentStorageKey, rowFields, columnFields, valueFields, showRowTotals, showColumnTotals, calculatedFields]);
|
|
1449
|
+
const addRowField = (0, import_react7.useCallback)(
|
|
1450
|
+
(field) => {
|
|
1451
|
+
if (!rowFields.includes(field)) {
|
|
1452
|
+
setRowFieldsState((prev) => [...prev, field]);
|
|
1410
1453
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1454
|
+
},
|
|
1455
|
+
[rowFields]
|
|
1456
|
+
);
|
|
1457
|
+
const removeRowField = (0, import_react7.useCallback)((field) => {
|
|
1458
|
+
setRowFieldsState((prev) => prev.filter((f) => f !== field));
|
|
1413
1459
|
}, []);
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1460
|
+
const setRowFields = (0, import_react7.useCallback)((fields) => {
|
|
1461
|
+
setRowFieldsState(fields);
|
|
1462
|
+
}, []);
|
|
1463
|
+
const addColumnField = (0, import_react7.useCallback)(
|
|
1464
|
+
(field) => {
|
|
1465
|
+
if (!columnFields.includes(field)) {
|
|
1466
|
+
setColumnFieldsState((prev) => [...prev, field]);
|
|
1467
|
+
}
|
|
1468
|
+
},
|
|
1469
|
+
[columnFields]
|
|
1470
|
+
);
|
|
1471
|
+
const removeColumnField = (0, import_react7.useCallback)((field) => {
|
|
1472
|
+
setColumnFieldsState((prev) => prev.filter((f) => f !== field));
|
|
1473
|
+
}, []);
|
|
1474
|
+
const setColumnFields = (0, import_react7.useCallback)((fields) => {
|
|
1475
|
+
setColumnFieldsState(fields);
|
|
1476
|
+
}, []);
|
|
1477
|
+
const addValueField = (0, import_react7.useCallback)(
|
|
1478
|
+
(field, aggregation = "sum") => {
|
|
1479
|
+
if (aggregation !== "sum" && !requirePro(`${aggregation} aggregation`)) {
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
setValueFields((prev) => {
|
|
1483
|
+
if (prev.some((v) => v.field === field && v.aggregation === aggregation)) {
|
|
1484
|
+
return prev;
|
|
1485
|
+
}
|
|
1486
|
+
return [...prev, { field, aggregation }];
|
|
1487
|
+
});
|
|
1488
|
+
},
|
|
1489
|
+
[requirePro]
|
|
1490
|
+
);
|
|
1491
|
+
const removeValueField = (0, import_react7.useCallback)((field, aggregation) => {
|
|
1492
|
+
setValueFields((prev) => {
|
|
1493
|
+
if (aggregation) {
|
|
1494
|
+
return prev.filter((v) => !(v.field === field && v.aggregation === aggregation));
|
|
1418
1495
|
}
|
|
1419
|
-
return prev
|
|
1496
|
+
return prev.filter((v) => v.field !== field);
|
|
1420
1497
|
});
|
|
1421
1498
|
}, []);
|
|
1422
|
-
const
|
|
1423
|
-
|
|
1424
|
-
|
|
1499
|
+
const updateValueFieldAggregation = (0, import_react7.useCallback)(
|
|
1500
|
+
(field, oldAgg, newAgg) => {
|
|
1501
|
+
setValueFields(
|
|
1502
|
+
(prev) => prev.map((v) => {
|
|
1503
|
+
if (v.field === field && v.aggregation === oldAgg) {
|
|
1504
|
+
return { ...v, aggregation: newAgg };
|
|
1505
|
+
}
|
|
1506
|
+
return v;
|
|
1507
|
+
})
|
|
1508
|
+
);
|
|
1509
|
+
},
|
|
1510
|
+
[]
|
|
1511
|
+
);
|
|
1512
|
+
const clearConfig = (0, import_react7.useCallback)(() => {
|
|
1513
|
+
setRowFieldsState([]);
|
|
1514
|
+
setColumnFieldsState([]);
|
|
1515
|
+
setValueFields([]);
|
|
1516
|
+
}, []);
|
|
1517
|
+
const autoSuggestConfig = (0, import_react7.useCallback)(() => {
|
|
1518
|
+
if (!requirePro("Pivot Table - Auto Suggest"))
|
|
1425
1519
|
return;
|
|
1426
|
-
|
|
1427
|
-
const validationResult = (0, import_tinypivot_core5.validateSimpleFormula)(formula, availableFields);
|
|
1428
|
-
if (validationResult) {
|
|
1429
|
-
setError(validationResult);
|
|
1520
|
+
if (availableFields.length === 0)
|
|
1430
1521
|
return;
|
|
1522
|
+
const categoricalFields = availableFields.filter((f) => !f.isNumeric && f.uniqueCount < 50);
|
|
1523
|
+
const numericFields = availableFields.filter((f) => f.isNumeric);
|
|
1524
|
+
if (categoricalFields.length > 0 && numericFields.length > 0) {
|
|
1525
|
+
setRowFieldsState([categoricalFields[0].field]);
|
|
1526
|
+
setValueFields([{ field: numericFields[0].field, aggregation: "sum" }]);
|
|
1431
1527
|
}
|
|
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
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
] }),
|
|
1485
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-form-group", children: [
|
|
1486
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { className: "vpg-label-small", children: "Operators" }),
|
|
1487
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-button-group", children: [
|
|
1488
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("+"), children: "+" }),
|
|
1489
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("-"), children: "\u2212" }),
|
|
1490
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("*"), children: "\xD7" }),
|
|
1491
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("/"), children: "\xF7" }),
|
|
1492
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator("("), children: "(" }),
|
|
1493
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-insert-btn vpg-op-btn", onClick: () => insertOperator(")"), children: ")" })
|
|
1494
|
-
] })
|
|
1495
|
-
] }),
|
|
1496
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-form-group", children: [
|
|
1497
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { className: "vpg-label-small", children: "Insert Field" }),
|
|
1498
|
-
availableFields.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "vpg-button-group vpg-field-buttons", children: availableFields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1499
|
-
"button",
|
|
1500
|
-
{
|
|
1501
|
-
className: "vpg-insert-btn vpg-field-btn",
|
|
1502
|
-
onClick: () => insertField(field),
|
|
1503
|
-
children: field
|
|
1504
|
-
},
|
|
1505
|
-
field
|
|
1506
|
-
)) }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "vpg-no-fields", children: "No numeric fields available" })
|
|
1507
|
-
] }),
|
|
1508
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-form-row", children: [
|
|
1509
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-form-group vpg-form-group-half", children: [
|
|
1510
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { className: "vpg-label", children: "Format As" }),
|
|
1511
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1512
|
-
"select",
|
|
1513
|
-
{
|
|
1514
|
-
className: "vpg-select",
|
|
1515
|
-
value: formatAs,
|
|
1516
|
-
onChange: (e) => setFormatAs(e.target.value),
|
|
1517
|
-
children: [
|
|
1518
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "number", children: "Number" }),
|
|
1519
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "percent", children: "Percentage" }),
|
|
1520
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "currency", children: "Currency ($)" })
|
|
1521
|
-
]
|
|
1522
|
-
}
|
|
1523
|
-
)
|
|
1524
|
-
] }),
|
|
1525
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-form-group vpg-form-group-half", children: [
|
|
1526
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { className: "vpg-label", children: "Decimals" }),
|
|
1527
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1528
|
-
"input",
|
|
1529
|
-
{
|
|
1530
|
-
type: "number",
|
|
1531
|
-
className: "vpg-input",
|
|
1532
|
-
min: 0,
|
|
1533
|
-
max: 6,
|
|
1534
|
-
value: decimals,
|
|
1535
|
-
onChange: (e) => setDecimals(Number(e.target.value))
|
|
1536
|
-
}
|
|
1537
|
-
)
|
|
1538
|
-
] })
|
|
1539
|
-
] }),
|
|
1540
|
-
error && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "vpg-error vpg-error-box", children: error })
|
|
1541
|
-
] }),
|
|
1542
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "vpg-modal-footer", children: [
|
|
1543
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "vpg-btn vpg-btn-secondary", onClick: onClose, children: "Cancel" }),
|
|
1544
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { className: "vpg-btn vpg-btn-primary", onClick: handleSave, children: [
|
|
1545
|
-
existingField ? "Update" : "Add",
|
|
1546
|
-
" Field"
|
|
1547
|
-
] })
|
|
1548
|
-
] })
|
|
1549
|
-
] }) });
|
|
1550
|
-
if (typeof document === "undefined") return null;
|
|
1551
|
-
return (0, import_react_dom.createPortal)(modalContent, document.body);
|
|
1528
|
+
}, [availableFields, requirePro]);
|
|
1529
|
+
const addCalculatedField = (0, import_react7.useCallback)((field) => {
|
|
1530
|
+
setCalculatedFields((prev) => {
|
|
1531
|
+
const existing = prev.findIndex((f) => f.id === field.id);
|
|
1532
|
+
let updated;
|
|
1533
|
+
if (existing >= 0) {
|
|
1534
|
+
updated = [...prev.slice(0, existing), field, ...prev.slice(existing + 1)];
|
|
1535
|
+
} else {
|
|
1536
|
+
updated = [...prev, field];
|
|
1537
|
+
}
|
|
1538
|
+
(0, import_tinypivot_core5.saveCalculatedFields)(updated);
|
|
1539
|
+
return updated;
|
|
1540
|
+
});
|
|
1541
|
+
}, []);
|
|
1542
|
+
const removeCalculatedField = (0, import_react7.useCallback)((id) => {
|
|
1543
|
+
setCalculatedFields((prev) => {
|
|
1544
|
+
const updated = prev.filter((f) => f.id !== id);
|
|
1545
|
+
(0, import_tinypivot_core5.saveCalculatedFields)(updated);
|
|
1546
|
+
return updated;
|
|
1547
|
+
});
|
|
1548
|
+
setValueFields((prev) => prev.filter((v) => v.field !== `calc:${id}`));
|
|
1549
|
+
}, []);
|
|
1550
|
+
return {
|
|
1551
|
+
// State
|
|
1552
|
+
rowFields,
|
|
1553
|
+
columnFields,
|
|
1554
|
+
valueFields,
|
|
1555
|
+
showRowTotals,
|
|
1556
|
+
showColumnTotals,
|
|
1557
|
+
calculatedFields,
|
|
1558
|
+
// Computed
|
|
1559
|
+
availableFields,
|
|
1560
|
+
unassignedFields,
|
|
1561
|
+
isConfigured,
|
|
1562
|
+
pivotResult,
|
|
1563
|
+
// Actions
|
|
1564
|
+
addRowField,
|
|
1565
|
+
removeRowField,
|
|
1566
|
+
addColumnField,
|
|
1567
|
+
removeColumnField,
|
|
1568
|
+
addValueField,
|
|
1569
|
+
removeValueField,
|
|
1570
|
+
updateValueFieldAggregation,
|
|
1571
|
+
clearConfig,
|
|
1572
|
+
setShowRowTotals,
|
|
1573
|
+
setShowColumnTotals,
|
|
1574
|
+
autoSuggestConfig,
|
|
1575
|
+
setRowFields,
|
|
1576
|
+
setColumnFields,
|
|
1577
|
+
addCalculatedField,
|
|
1578
|
+
removeCalculatedField
|
|
1579
|
+
};
|
|
1552
1580
|
}
|
|
1553
1581
|
|
|
1554
1582
|
// src/components/PivotConfig.tsx
|
|
1583
|
+
var import_tinypivot_core6 = require("@smallwebco/tinypivot-core");
|
|
1584
|
+
var import_react8 = require("react");
|
|
1555
1585
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1556
1586
|
function aggregationRequiresPro(agg) {
|
|
1557
1587
|
return agg !== "sum";
|
|
1558
1588
|
}
|
|
1559
1589
|
function getFieldIcon(type, isCalculated) {
|
|
1560
|
-
if (isCalculated)
|
|
1590
|
+
if (isCalculated)
|
|
1591
|
+
return "\u0192";
|
|
1561
1592
|
switch (type) {
|
|
1562
1593
|
case "number":
|
|
1563
1594
|
return "#";
|
|
@@ -1579,7 +1610,7 @@ function PivotConfig({
|
|
|
1579
1610
|
onShowRowTotalsChange,
|
|
1580
1611
|
onShowColumnTotalsChange,
|
|
1581
1612
|
onClearConfig,
|
|
1582
|
-
onAutoSuggest,
|
|
1613
|
+
onAutoSuggest: _onAutoSuggest,
|
|
1583
1614
|
onDragStart,
|
|
1584
1615
|
onDragEnd,
|
|
1585
1616
|
onUpdateAggregation,
|
|
@@ -1599,12 +1630,10 @@ function PivotConfig({
|
|
|
1599
1630
|
const isAggregationAvailable = (0, import_react8.useCallback)((agg) => {
|
|
1600
1631
|
return !aggregationRequiresPro(agg) || canUseAdvancedAggregations;
|
|
1601
1632
|
}, [canUseAdvancedAggregations]);
|
|
1602
|
-
const numericFieldNames = (0, import_react8.useMemo)(
|
|
1603
|
-
() => availableFields.filter((f) => f.isNumeric).map((f) => f.field),
|
|
1604
|
-
[availableFields]
|
|
1605
|
-
);
|
|
1633
|
+
const numericFieldNames = (0, import_react8.useMemo)(() => availableFields.filter((f) => f.isNumeric).map((f) => f.field), [availableFields]);
|
|
1606
1634
|
const calculatedFieldsAsStats = (0, import_react8.useMemo)(() => {
|
|
1607
|
-
if (!calculatedFields)
|
|
1635
|
+
if (!calculatedFields)
|
|
1636
|
+
return [];
|
|
1608
1637
|
return calculatedFields.map((calc) => ({
|
|
1609
1638
|
field: `calc:${calc.id}`,
|
|
1610
1639
|
type: "number",
|
|
@@ -1639,7 +1668,8 @@ function PivotConfig({
|
|
|
1639
1668
|
);
|
|
1640
1669
|
}, [allAvailableFields, rowFields, columnFields, valueFields]);
|
|
1641
1670
|
const filteredUnassignedFields = (0, import_react8.useMemo)(() => {
|
|
1642
|
-
if (!fieldSearch.trim())
|
|
1671
|
+
if (!fieldSearch.trim())
|
|
1672
|
+
return unassignedFields;
|
|
1643
1673
|
const search = fieldSearch.toLowerCase().trim();
|
|
1644
1674
|
return unassignedFields.filter((f) => {
|
|
1645
1675
|
const fieldName = f.field.toLowerCase();
|
|
@@ -1844,7 +1874,8 @@ function PivotConfig({
|
|
|
1844
1874
|
] }),
|
|
1845
1875
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "vpg-unassigned-section", children: [
|
|
1846
1876
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "vpg-section-header", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "vpg-section-label", children: [
|
|
1847
|
-
"Available
|
|
1877
|
+
"Available",
|
|
1878
|
+
" ",
|
|
1848
1879
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "vpg-count", children: unassignedFields.length })
|
|
1849
1880
|
] }) }),
|
|
1850
1881
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "vpg-field-search", children: [
|
|
@@ -1898,7 +1929,8 @@ function PivotConfig({
|
|
|
1898
1929
|
onClick: (e) => {
|
|
1899
1930
|
e.stopPropagation();
|
|
1900
1931
|
const calcField = calculatedFields?.find((c) => c.id === field.calcId);
|
|
1901
|
-
if (calcField)
|
|
1932
|
+
if (calcField)
|
|
1933
|
+
openCalcModal(calcField);
|
|
1902
1934
|
},
|
|
1903
1935
|
children: "\u270E"
|
|
1904
1936
|
}
|
|
@@ -1961,8 +1993,8 @@ function PivotConfig({
|
|
|
1961
1993
|
}
|
|
1962
1994
|
|
|
1963
1995
|
// src/components/PivotSkeleton.tsx
|
|
1964
|
-
var import_react9 = require("react");
|
|
1965
1996
|
var import_tinypivot_core7 = require("@smallwebco/tinypivot-core");
|
|
1997
|
+
var import_react9 = require("react");
|
|
1966
1998
|
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1967
1999
|
function PivotSkeleton({
|
|
1968
2000
|
rowFields,
|
|
@@ -2017,7 +2049,8 @@ function PivotSkeleton({
|
|
|
2017
2049
|
const [showCopyToast, setShowCopyToast] = (0, import_react9.useState)(false);
|
|
2018
2050
|
const [copyToastMessage, setCopyToastMessage] = (0, import_react9.useState)("");
|
|
2019
2051
|
const selectionBounds = (0, import_react9.useMemo)(() => {
|
|
2020
|
-
if (!selectionStart || !selectionEnd)
|
|
2052
|
+
if (!selectionStart || !selectionEnd)
|
|
2053
|
+
return null;
|
|
2021
2054
|
return {
|
|
2022
2055
|
minRow: Math.min(selectionStart.row, selectionEnd.row),
|
|
2023
2056
|
maxRow: Math.max(selectionStart.row, selectionEnd.row),
|
|
@@ -2063,7 +2096,8 @@ function PivotSkeleton({
|
|
|
2063
2096
|
return () => document.removeEventListener("mouseup", handleMouseUp);
|
|
2064
2097
|
}, []);
|
|
2065
2098
|
const sortedRowIndices = (0, import_react9.useMemo)(() => {
|
|
2066
|
-
if (!pivotResult)
|
|
2099
|
+
if (!pivotResult)
|
|
2100
|
+
return [];
|
|
2067
2101
|
const indices = pivotResult.rowHeaders.map((_, i) => i);
|
|
2068
2102
|
const headers = pivotResult.rowHeaders;
|
|
2069
2103
|
const data = pivotResult.data;
|
|
@@ -2077,9 +2111,12 @@ function PivotSkeleton({
|
|
|
2077
2111
|
const colIdx = sortTarget;
|
|
2078
2112
|
const aVal = data[a]?.[colIdx]?.value ?? null;
|
|
2079
2113
|
const bVal = data[b]?.[colIdx]?.value ?? null;
|
|
2080
|
-
if (aVal === null && bVal === null)
|
|
2081
|
-
|
|
2082
|
-
else if (
|
|
2114
|
+
if (aVal === null && bVal === null)
|
|
2115
|
+
cmp = 0;
|
|
2116
|
+
else if (aVal === null)
|
|
2117
|
+
cmp = 1;
|
|
2118
|
+
else if (bVal === null)
|
|
2119
|
+
cmp = -1;
|
|
2083
2120
|
else cmp = aVal - bVal;
|
|
2084
2121
|
}
|
|
2085
2122
|
return sortDirection === "asc" ? cmp : -cmp;
|
|
@@ -2087,12 +2124,14 @@ function PivotSkeleton({
|
|
|
2087
2124
|
return indices;
|
|
2088
2125
|
}, [pivotResult, sortTarget, sortDirection]);
|
|
2089
2126
|
const copySelectionToClipboard = (0, import_react9.useCallback)(() => {
|
|
2090
|
-
if (!selectionBounds || !pivotResult)
|
|
2127
|
+
if (!selectionBounds || !pivotResult)
|
|
2128
|
+
return;
|
|
2091
2129
|
const { minRow, maxRow, minCol, maxCol } = selectionBounds;
|
|
2092
2130
|
const lines = [];
|
|
2093
2131
|
for (let r = minRow; r <= maxRow; r++) {
|
|
2094
2132
|
const sortedIdx = sortedRowIndices[r];
|
|
2095
|
-
if (sortedIdx === void 0)
|
|
2133
|
+
if (sortedIdx === void 0)
|
|
2134
|
+
continue;
|
|
2096
2135
|
const rowValues = [];
|
|
2097
2136
|
for (let c = minCol; c <= maxCol; c++) {
|
|
2098
2137
|
const cell = pivotResult.data[sortedIdx]?.[c];
|
|
@@ -2112,7 +2151,8 @@ function PivotSkeleton({
|
|
|
2112
2151
|
}, [selectionBounds, pivotResult, sortedRowIndices]);
|
|
2113
2152
|
(0, import_react9.useEffect)(() => {
|
|
2114
2153
|
const handleKeydown = (event) => {
|
|
2115
|
-
if (!selectionBounds)
|
|
2154
|
+
if (!selectionBounds)
|
|
2155
|
+
return;
|
|
2116
2156
|
if ((event.ctrlKey || event.metaKey) && event.key === "c") {
|
|
2117
2157
|
event.preventDefault();
|
|
2118
2158
|
copySelectionToClipboard();
|
|
@@ -2128,13 +2168,15 @@ function PivotSkeleton({
|
|
|
2128
2168
|
return () => document.removeEventListener("keydown", handleKeydown);
|
|
2129
2169
|
}, [selectionBounds, copySelectionToClipboard]);
|
|
2130
2170
|
const selectionStats = (0, import_react9.useMemo)(() => {
|
|
2131
|
-
if (!selectionBounds || !pivotResult)
|
|
2171
|
+
if (!selectionBounds || !pivotResult)
|
|
2172
|
+
return null;
|
|
2132
2173
|
const { minRow, maxRow, minCol, maxCol } = selectionBounds;
|
|
2133
2174
|
const values = [];
|
|
2134
2175
|
let count = 0;
|
|
2135
2176
|
for (let r = minRow; r <= maxRow; r++) {
|
|
2136
2177
|
const sortedIdx = sortedRowIndices[r];
|
|
2137
|
-
if (sortedIdx === void 0)
|
|
2178
|
+
if (sortedIdx === void 0)
|
|
2179
|
+
continue;
|
|
2138
2180
|
for (let c = minCol; c <= maxCol; c++) {
|
|
2139
2181
|
const cell = pivotResult.data[sortedIdx]?.[c];
|
|
2140
2182
|
count++;
|
|
@@ -2143,7 +2185,8 @@ function PivotSkeleton({
|
|
|
2143
2185
|
}
|
|
2144
2186
|
}
|
|
2145
2187
|
}
|
|
2146
|
-
if (count <= 1)
|
|
2188
|
+
if (count <= 1)
|
|
2189
|
+
return null;
|
|
2147
2190
|
const sum = values.reduce((a, b) => a + b, 0);
|
|
2148
2191
|
const avg = values.length > 0 ? sum / values.length : 0;
|
|
2149
2192
|
return {
|
|
@@ -2154,8 +2197,10 @@ function PivotSkeleton({
|
|
|
2154
2197
|
};
|
|
2155
2198
|
}, [selectionBounds, pivotResult, sortedRowIndices]);
|
|
2156
2199
|
const formatStatValue = (0, import_react9.useCallback)((val) => {
|
|
2157
|
-
if (Math.abs(val) >= 1e6)
|
|
2158
|
-
|
|
2200
|
+
if (Math.abs(val) >= 1e6)
|
|
2201
|
+
return `${(val / 1e6).toFixed(2)}M`;
|
|
2202
|
+
if (Math.abs(val) >= 1e3)
|
|
2203
|
+
return `${(val / 1e3).toFixed(2)}K`;
|
|
2159
2204
|
return val.toFixed(2);
|
|
2160
2205
|
}, []);
|
|
2161
2206
|
const columnHeaderCells = (0, import_react9.useMemo)(() => {
|
|
@@ -2187,12 +2232,14 @@ function PivotSkeleton({
|
|
|
2187
2232
|
}, [pivotResult, valueFields]);
|
|
2188
2233
|
const hasActiveFilters = activeFilters && activeFilters.length > 0;
|
|
2189
2234
|
const filterSummary = (0, import_react9.useMemo)(() => {
|
|
2190
|
-
if (!activeFilters || activeFilters.length === 0)
|
|
2235
|
+
if (!activeFilters || activeFilters.length === 0)
|
|
2236
|
+
return "";
|
|
2191
2237
|
return activeFilters.map((f) => f.column).join(", ");
|
|
2192
2238
|
}, [activeFilters]);
|
|
2193
2239
|
const [showFilterTooltip, setShowFilterTooltip] = (0, import_react9.useState)(false);
|
|
2194
2240
|
const filterTooltipDetails = (0, import_react9.useMemo)(() => {
|
|
2195
|
-
if (!activeFilters || activeFilters.length === 0)
|
|
2241
|
+
if (!activeFilters || activeFilters.length === 0)
|
|
2242
|
+
return [];
|
|
2196
2243
|
return activeFilters.map((f) => {
|
|
2197
2244
|
if (f.isRange && f.displayText) {
|
|
2198
2245
|
return {
|
|
@@ -2234,10 +2281,13 @@ function PivotSkeleton({
|
|
|
2234
2281
|
setDragOverArea(null);
|
|
2235
2282
|
return;
|
|
2236
2283
|
}
|
|
2237
|
-
if (rowFields.includes(field))
|
|
2238
|
-
|
|
2284
|
+
if (rowFields.includes(field))
|
|
2285
|
+
onRemoveRowField(field);
|
|
2286
|
+
if (columnFields.includes(field))
|
|
2287
|
+
onRemoveColumnField(field);
|
|
2239
2288
|
const existingValue = valueFields.find((v) => v.field === field);
|
|
2240
|
-
if (existingValue)
|
|
2289
|
+
if (existingValue)
|
|
2290
|
+
onRemoveValueField(field, existingValue.aggregation);
|
|
2241
2291
|
switch (area) {
|
|
2242
2292
|
case "row":
|
|
2243
2293
|
onAddRowField(field);
|
|
@@ -2377,14 +2427,18 @@ function PivotSkeleton({
|
|
|
2377
2427
|
}
|
|
2378
2428
|
),
|
|
2379
2429
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-filter-text", children: [
|
|
2380
|
-
"Filtered:
|
|
2430
|
+
"Filtered:",
|
|
2431
|
+
" ",
|
|
2381
2432
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: filterSummary }),
|
|
2382
2433
|
filteredRowCount !== void 0 && totalRowCount !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-filter-count", children: [
|
|
2383
2434
|
"(",
|
|
2384
2435
|
filteredRowCount.toLocaleString(),
|
|
2385
|
-
"
|
|
2436
|
+
" ",
|
|
2437
|
+
"of",
|
|
2438
|
+
" ",
|
|
2386
2439
|
totalRowCount.toLocaleString(),
|
|
2387
|
-
"
|
|
2440
|
+
" ",
|
|
2441
|
+
"rows)"
|
|
2388
2442
|
] })
|
|
2389
2443
|
] }),
|
|
2390
2444
|
showFilterTooltip && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "vpg-filter-tooltip", children: [
|
|
@@ -2396,16 +2450,21 @@ function PivotSkeleton({
|
|
|
2396
2450
|
filter.remaining > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-tooltip-more", children: [
|
|
2397
2451
|
"+",
|
|
2398
2452
|
filter.remaining,
|
|
2399
|
-
"
|
|
2453
|
+
" ",
|
|
2454
|
+
"more"
|
|
2400
2455
|
] })
|
|
2401
2456
|
] }) })
|
|
2402
2457
|
] }, filter.column)),
|
|
2403
2458
|
filteredRowCount !== void 0 && totalRowCount !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "vpg-tooltip-summary", children: [
|
|
2404
|
-
"Showing
|
|
2459
|
+
"Showing",
|
|
2460
|
+
" ",
|
|
2405
2461
|
filteredRowCount.toLocaleString(),
|
|
2406
|
-
"
|
|
2462
|
+
" ",
|
|
2463
|
+
"of",
|
|
2464
|
+
" ",
|
|
2407
2465
|
totalRowCount.toLocaleString(),
|
|
2408
|
-
"
|
|
2466
|
+
" ",
|
|
2467
|
+
"rows"
|
|
2409
2468
|
] })
|
|
2410
2469
|
] })
|
|
2411
2470
|
]
|
|
@@ -2414,17 +2473,20 @@ function PivotSkeleton({
|
|
|
2414
2473
|
isConfigured && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "vpg-config-summary", children: [
|
|
2415
2474
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-summary-badge vpg-rows", children: [
|
|
2416
2475
|
rowFields.length,
|
|
2417
|
-
"
|
|
2476
|
+
" ",
|
|
2477
|
+
"row",
|
|
2418
2478
|
rowFields.length !== 1 ? "s" : ""
|
|
2419
2479
|
] }),
|
|
2420
2480
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-summary-badge vpg-cols", children: [
|
|
2421
2481
|
columnFields.length,
|
|
2422
|
-
"
|
|
2482
|
+
" ",
|
|
2483
|
+
"col",
|
|
2423
2484
|
columnFields.length !== 1 ? "s" : ""
|
|
2424
2485
|
] }),
|
|
2425
2486
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-summary-badge vpg-vals", children: [
|
|
2426
2487
|
valueFields.length,
|
|
2427
|
-
"
|
|
2488
|
+
" ",
|
|
2489
|
+
"val",
|
|
2428
2490
|
valueFields.length !== 1 ? "s" : ""
|
|
2429
2491
|
] })
|
|
2430
2492
|
] })
|
|
@@ -2595,15 +2657,21 @@ function PivotSkeleton({
|
|
|
2595
2657
|
}
|
|
2596
2658
|
),
|
|
2597
2659
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "vpg-placeholder-text", children: valueFields.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
|
|
2598
|
-
"Add a
|
|
2660
|
+
"Add a",
|
|
2661
|
+
" ",
|
|
2599
2662
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: "Values" }),
|
|
2600
|
-
"
|
|
2663
|
+
" ",
|
|
2664
|
+
"field to see your pivot table"
|
|
2601
2665
|
] }) : rowFields.length === 0 && columnFields.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
|
|
2602
|
-
"Add
|
|
2666
|
+
"Add",
|
|
2667
|
+
" ",
|
|
2603
2668
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: "Row" }),
|
|
2604
|
-
"
|
|
2669
|
+
" ",
|
|
2670
|
+
"or",
|
|
2671
|
+
" ",
|
|
2605
2672
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: "Column" }),
|
|
2606
|
-
"
|
|
2673
|
+
" ",
|
|
2674
|
+
"fields to group your data"
|
|
2607
2675
|
] }) : "Your pivot table will appear here" })
|
|
2608
2676
|
] }) }),
|
|
2609
2677
|
isConfigured && pivotResult && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "vpg-table-container", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("table", { className: "vpg-pivot-table", children: [
|
|
@@ -2681,9 +2749,11 @@ function PivotSkeleton({
|
|
|
2681
2749
|
isConfigured && pivotResult && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "vpg-skeleton-footer", children: [
|
|
2682
2750
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-footer-info", children: [
|
|
2683
2751
|
pivotResult.rowHeaders.length,
|
|
2684
|
-
"
|
|
2752
|
+
" ",
|
|
2753
|
+
"rows \xD7",
|
|
2685
2754
|
pivotResult.data[0]?.length || 0,
|
|
2686
|
-
"
|
|
2755
|
+
" ",
|
|
2756
|
+
"columns"
|
|
2687
2757
|
] }),
|
|
2688
2758
|
selectionStats && selectionStats.count > 1 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "vpg-selection-stats", children: [
|
|
2689
2759
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "vpg-stat", children: [
|
|
@@ -2836,12 +2906,15 @@ function DataGrid({
|
|
|
2836
2906
|
removeCalculatedField
|
|
2837
2907
|
} = usePivotTable(filteredDataForPivot);
|
|
2838
2908
|
const activeFilterInfo = (0, import_react10.useMemo)(() => {
|
|
2839
|
-
if (activeFilters.length === 0)
|
|
2909
|
+
if (activeFilters.length === 0)
|
|
2910
|
+
return null;
|
|
2840
2911
|
return activeFilters.map((f) => {
|
|
2841
2912
|
if (f.type === "range" && f.range) {
|
|
2842
2913
|
const parts = [];
|
|
2843
|
-
if (f.range.min !== null)
|
|
2844
|
-
|
|
2914
|
+
if (f.range.min !== null)
|
|
2915
|
+
parts.push(`\u2265 ${f.range.min}`);
|
|
2916
|
+
if (f.range.max !== null)
|
|
2917
|
+
parts.push(`\u2264 ${f.range.max}`);
|
|
2845
2918
|
return {
|
|
2846
2919
|
column: f.column,
|
|
2847
2920
|
valueCount: 1,
|
|
@@ -2866,7 +2939,8 @@ function DataGrid({
|
|
|
2866
2939
|
return rows.filter((row) => {
|
|
2867
2940
|
for (const col of columnKeys) {
|
|
2868
2941
|
const value = row.original[col];
|
|
2869
|
-
if (value === null || value === void 0)
|
|
2942
|
+
if (value === null || value === void 0)
|
|
2943
|
+
continue;
|
|
2870
2944
|
if (String(value).toLowerCase().includes(term)) {
|
|
2871
2945
|
return true;
|
|
2872
2946
|
}
|
|
@@ -2876,11 +2950,13 @@ function DataGrid({
|
|
|
2876
2950
|
}, [rows, globalSearchTerm, enableSearch, columnKeys]);
|
|
2877
2951
|
const totalSearchedRows = searchFilteredData.length;
|
|
2878
2952
|
const totalPages = (0, import_react10.useMemo)(() => {
|
|
2879
|
-
if (!enablePagination)
|
|
2953
|
+
if (!enablePagination)
|
|
2954
|
+
return 1;
|
|
2880
2955
|
return Math.max(1, Math.ceil(totalSearchedRows / pageSize));
|
|
2881
2956
|
}, [enablePagination, totalSearchedRows, pageSize]);
|
|
2882
2957
|
const paginatedRows = (0, import_react10.useMemo)(() => {
|
|
2883
|
-
if (!enablePagination)
|
|
2958
|
+
if (!enablePagination)
|
|
2959
|
+
return searchFilteredData;
|
|
2884
2960
|
const start = (currentPage - 1) * pageSize;
|
|
2885
2961
|
const end = start + pageSize;
|
|
2886
2962
|
return searchFilteredData.slice(start, end);
|
|
@@ -2889,7 +2965,8 @@ function DataGrid({
|
|
|
2889
2965
|
setCurrentPage(1);
|
|
2890
2966
|
}, [columnFilters, globalSearchTerm]);
|
|
2891
2967
|
const selectionBounds = (0, import_react10.useMemo)(() => {
|
|
2892
|
-
if (!selectionStart || !selectionEnd)
|
|
2968
|
+
if (!selectionStart || !selectionEnd)
|
|
2969
|
+
return null;
|
|
2893
2970
|
return {
|
|
2894
2971
|
minRow: Math.min(selectionStart.row, selectionEnd.row),
|
|
2895
2972
|
maxRow: Math.max(selectionStart.row, selectionEnd.row),
|
|
@@ -2898,16 +2975,19 @@ function DataGrid({
|
|
|
2898
2975
|
};
|
|
2899
2976
|
}, [selectionStart, selectionEnd]);
|
|
2900
2977
|
const selectionStats = (0, import_react10.useMemo)(() => {
|
|
2901
|
-
if (!selectionBounds)
|
|
2978
|
+
if (!selectionBounds)
|
|
2979
|
+
return null;
|
|
2902
2980
|
const { minRow, maxRow, minCol, maxCol } = selectionBounds;
|
|
2903
2981
|
const values = [];
|
|
2904
2982
|
let count = 0;
|
|
2905
2983
|
for (let r = minRow; r <= maxRow; r++) {
|
|
2906
2984
|
const row = rows[r];
|
|
2907
|
-
if (!row)
|
|
2985
|
+
if (!row)
|
|
2986
|
+
continue;
|
|
2908
2987
|
for (let c = minCol; c <= maxCol; c++) {
|
|
2909
2988
|
const colId = columnKeys[c];
|
|
2910
|
-
if (!colId)
|
|
2989
|
+
if (!colId)
|
|
2990
|
+
continue;
|
|
2911
2991
|
const value = row.original[colId];
|
|
2912
2992
|
count++;
|
|
2913
2993
|
if (value !== null && value !== void 0 && value !== "") {
|
|
@@ -2918,19 +2998,23 @@ function DataGrid({
|
|
|
2918
2998
|
}
|
|
2919
2999
|
}
|
|
2920
3000
|
}
|
|
2921
|
-
if (values.length === 0)
|
|
3001
|
+
if (values.length === 0)
|
|
3002
|
+
return { count, sum: null, avg: null, numericCount: 0 };
|
|
2922
3003
|
const sum = values.reduce((a, b) => a + b, 0);
|
|
2923
3004
|
const avg = sum / values.length;
|
|
2924
3005
|
return { count, sum, avg, numericCount: values.length };
|
|
2925
3006
|
}, [selectionBounds, rows, columnKeys]);
|
|
2926
3007
|
(0, import_react10.useEffect)(() => {
|
|
2927
|
-
if (typeof document === "undefined")
|
|
2928
|
-
|
|
3008
|
+
if (typeof document === "undefined")
|
|
3009
|
+
return;
|
|
3010
|
+
if (data.length === 0)
|
|
3011
|
+
return;
|
|
2929
3012
|
const widths = {};
|
|
2930
3013
|
const sampleSize = Math.min(100, data.length);
|
|
2931
3014
|
const canvas = document.createElement("canvas");
|
|
2932
3015
|
const ctx = canvas.getContext("2d");
|
|
2933
|
-
if (!ctx)
|
|
3016
|
+
if (!ctx)
|
|
3017
|
+
return;
|
|
2934
3018
|
ctx.font = "13px system-ui, -apple-system, sans-serif";
|
|
2935
3019
|
for (const key of columnKeys) {
|
|
2936
3020
|
let maxWidth = ctx.measureText(key).width + 56;
|
|
@@ -2946,7 +3030,8 @@ function DataGrid({
|
|
|
2946
3030
|
}, [data, columnKeys]);
|
|
2947
3031
|
const startColumnResize = (0, import_react10.useCallback)(
|
|
2948
3032
|
(columnId, event) => {
|
|
2949
|
-
if (!enableColumnResize)
|
|
3033
|
+
if (!enableColumnResize)
|
|
3034
|
+
return;
|
|
2950
3035
|
event.preventDefault();
|
|
2951
3036
|
event.stopPropagation();
|
|
2952
3037
|
setResizingColumnId(columnId);
|
|
@@ -2956,7 +3041,8 @@ function DataGrid({
|
|
|
2956
3041
|
[enableColumnResize, columnWidths]
|
|
2957
3042
|
);
|
|
2958
3043
|
(0, import_react10.useEffect)(() => {
|
|
2959
|
-
if (!resizingColumnId)
|
|
3044
|
+
if (!resizingColumnId)
|
|
3045
|
+
return;
|
|
2960
3046
|
const handleResizeMove = (event) => {
|
|
2961
3047
|
const diff = event.clientX - resizeStartX;
|
|
2962
3048
|
const newWidth = Math.max(MIN_COL_WIDTH, Math.min(MAX_COL_WIDTH, resizeStartWidth + diff));
|
|
@@ -2977,7 +3063,8 @@ function DataGrid({
|
|
|
2977
3063
|
}, [resizingColumnId, resizeStartX, resizeStartWidth]);
|
|
2978
3064
|
const startVerticalResize = (0, import_react10.useCallback)(
|
|
2979
3065
|
(event) => {
|
|
2980
|
-
if (!enableVerticalResize)
|
|
3066
|
+
if (!enableVerticalResize)
|
|
3067
|
+
return;
|
|
2981
3068
|
event.preventDefault();
|
|
2982
3069
|
setIsResizingVertically(true);
|
|
2983
3070
|
setVerticalResizeStartY(event.clientY);
|
|
@@ -2986,7 +3073,8 @@ function DataGrid({
|
|
|
2986
3073
|
[enableVerticalResize, gridHeight]
|
|
2987
3074
|
);
|
|
2988
3075
|
(0, import_react10.useEffect)(() => {
|
|
2989
|
-
if (!isResizingVertically)
|
|
3076
|
+
if (!isResizingVertically)
|
|
3077
|
+
return;
|
|
2990
3078
|
const handleVerticalResizeMove = (event) => {
|
|
2991
3079
|
const diff = event.clientY - verticalResizeStartY;
|
|
2992
3080
|
const newHeight = Math.max(minHeight, Math.min(maxHeight, verticalResizeStartHeight + diff));
|
|
@@ -3004,7 +3092,8 @@ function DataGrid({
|
|
|
3004
3092
|
}, [isResizingVertically, verticalResizeStartY, verticalResizeStartHeight, minHeight, maxHeight]);
|
|
3005
3093
|
const handleExport = (0, import_react10.useCallback)(() => {
|
|
3006
3094
|
if (viewMode === "pivot") {
|
|
3007
|
-
if (!pivotResult)
|
|
3095
|
+
if (!pivotResult)
|
|
3096
|
+
return;
|
|
3008
3097
|
const pivotFilename = exportFilename.replace(".csv", "-pivot.csv");
|
|
3009
3098
|
exportPivotToCSV(
|
|
3010
3099
|
{
|
|
@@ -3048,7 +3137,8 @@ function DataGrid({
|
|
|
3048
3137
|
onExport
|
|
3049
3138
|
]);
|
|
3050
3139
|
const copySelectionToClipboard = (0, import_react10.useCallback)(() => {
|
|
3051
|
-
if (!selectionBounds || !enableClipboard)
|
|
3140
|
+
if (!selectionBounds || !enableClipboard)
|
|
3141
|
+
return;
|
|
3052
3142
|
const text = formatSelectionForClipboard(
|
|
3053
3143
|
rows.map((r) => r.original),
|
|
3054
3144
|
columnKeys,
|
|
@@ -3205,7 +3295,8 @@ function DataGrid({
|
|
|
3205
3295
|
[selectionBounds, selectedCell]
|
|
3206
3296
|
);
|
|
3207
3297
|
const formatStatValue = (value) => {
|
|
3208
|
-
if (value === null)
|
|
3298
|
+
if (value === null)
|
|
3299
|
+
return "-";
|
|
3209
3300
|
if (Math.abs(value) >= 1e3) {
|
|
3210
3301
|
return value.toLocaleString("en-US", { maximumFractionDigits: 2 });
|
|
3211
3302
|
}
|
|
@@ -3216,12 +3307,15 @@ function DataGrid({
|
|
|
3216
3307
|
return !noFormatPatterns.test(columnId);
|
|
3217
3308
|
};
|
|
3218
3309
|
const formatCellValueDisplay = (value, columnId) => {
|
|
3219
|
-
if (value === null || value === void 0)
|
|
3220
|
-
|
|
3310
|
+
if (value === null || value === void 0)
|
|
3311
|
+
return "";
|
|
3312
|
+
if (value === "")
|
|
3313
|
+
return "";
|
|
3221
3314
|
const stats = getColumnStats(columnId);
|
|
3222
3315
|
if (stats.type === "number") {
|
|
3223
3316
|
const num = typeof value === "number" ? value : Number.parseFloat(String(value));
|
|
3224
|
-
if (Number.isNaN(num))
|
|
3317
|
+
if (Number.isNaN(num))
|
|
3318
|
+
return String(value);
|
|
3225
3319
|
if (shouldFormatNumber(columnId) && Math.abs(num) >= 1e3) {
|
|
3226
3320
|
return num.toLocaleString("en-US", { maximumFractionDigits: 2 });
|
|
3227
3321
|
}
|
|
@@ -3383,13 +3477,15 @@ function DataGrid({
|
|
|
3383
3477
|
) }),
|
|
3384
3478
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
|
|
3385
3479
|
activeFilterCount,
|
|
3386
|
-
"
|
|
3480
|
+
" ",
|
|
3481
|
+
"filter",
|
|
3387
3482
|
activeFilterCount > 1 ? "s" : ""
|
|
3388
3483
|
] })
|
|
3389
3484
|
] }),
|
|
3390
3485
|
globalSearchTerm && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "vpg-search-info", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
|
|
3391
3486
|
totalSearchedRows,
|
|
3392
|
-
"
|
|
3487
|
+
" ",
|
|
3488
|
+
"match",
|
|
3393
3489
|
totalSearchedRows !== 1 ? "es" : ""
|
|
3394
3490
|
] }) })
|
|
3395
3491
|
] }),
|
|
@@ -3410,7 +3506,8 @@ function DataGrid({
|
|
|
3410
3506
|
}
|
|
3411
3507
|
) }),
|
|
3412
3508
|
showPivotConfig ? "Hide" : "Show",
|
|
3413
|
-
"
|
|
3509
|
+
" ",
|
|
3510
|
+
"Config"
|
|
3414
3511
|
]
|
|
3415
3512
|
}
|
|
3416
3513
|
),
|
|
@@ -3532,7 +3629,7 @@ function DataGrid({
|
|
|
3532
3629
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "vpg-clear-link", onClick: clearAllFilters, children: "Clear all filters" })
|
|
3533
3630
|
] }),
|
|
3534
3631
|
!loading && filteredRowCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "vpg-table-wrapper", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("table", { className: "vpg-table", style: { minWidth: `${totalTableWidth}px` }, children: [
|
|
3535
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("tr", { children: columnKeys.map((colId
|
|
3632
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("tr", { children: columnKeys.map((colId) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
3536
3633
|
"th",
|
|
3537
3634
|
{
|
|
3538
3635
|
className: `vpg-header-cell ${hasActiveFilter(colId) ? "vpg-has-filter" : ""} ${getSortDirection(colId) !== null ? "vpg-is-sorted" : ""} ${activeFilterColumn === colId ? "vpg-is-active" : ""}`,
|
|
@@ -3661,7 +3758,7 @@ function DataGrid({
|
|
|
3661
3758
|
onShowColumnTotalsChange: setPivotShowColumnTotals,
|
|
3662
3759
|
onClearConfig: clearPivotConfig,
|
|
3663
3760
|
onAutoSuggest: autoSuggestConfig,
|
|
3664
|
-
onDragStart: (field,
|
|
3761
|
+
onDragStart: (field, _e) => setDraggingField(field),
|
|
3665
3762
|
onDragEnd: () => setDraggingField(null),
|
|
3666
3763
|
onUpdateAggregation: updateValueFieldAggregation,
|
|
3667
3764
|
onAddRowField: addRowField,
|
|
@@ -3713,11 +3810,13 @@ function DataGrid({
|
|
|
3713
3810
|
totalSearchedRows !== totalRowCount && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "vpg-filtered-note", children: [
|
|
3714
3811
|
"(",
|
|
3715
3812
|
totalRowCount.toLocaleString(),
|
|
3716
|
-
"
|
|
3813
|
+
" ",
|
|
3814
|
+
"total)"
|
|
3717
3815
|
] })
|
|
3718
3816
|
] }) : filteredRowCount === totalRowCount && totalSearchedRows === totalRowCount ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
|
|
3719
3817
|
totalRowCount.toLocaleString(),
|
|
3720
|
-
"
|
|
3818
|
+
" ",
|
|
3819
|
+
"records"
|
|
3721
3820
|
] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
|
|
3722
3821
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "vpg-filtered-count", children: totalSearchedRows.toLocaleString() }),
|
|
3723
3822
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "vpg-separator", children: "of" }),
|
|
@@ -3728,7 +3827,8 @@ function DataGrid({
|
|
|
3728
3827
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "vpg-separator", children: "\u2022" }),
|
|
3729
3828
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
|
|
3730
3829
|
totalRowCount.toLocaleString(),
|
|
3731
|
-
"
|
|
3830
|
+
" ",
|
|
3831
|
+
"source records"
|
|
3732
3832
|
] })
|
|
3733
3833
|
] }) }),
|
|
3734
3834
|
enablePagination && viewMode === "grid" && totalPages > 1 && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "vpg-pagination", children: [
|
|
@@ -3767,9 +3867,12 @@ function DataGrid({
|
|
|
3767
3867
|
}
|
|
3768
3868
|
),
|
|
3769
3869
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "vpg-page-info", children: [
|
|
3770
|
-
"Page
|
|
3870
|
+
"Page",
|
|
3871
|
+
" ",
|
|
3771
3872
|
currentPage,
|
|
3772
|
-
"
|
|
3873
|
+
" ",
|
|
3874
|
+
"of",
|
|
3875
|
+
" ",
|
|
3773
3876
|
totalPages
|
|
3774
3877
|
] }),
|
|
3775
3878
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|