@helix-x/datagrid-ui 0.1.0
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/LICENSE +15 -0
- package/README.md +141 -0
- package/dist/index.cjs +2351 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +502 -0
- package/dist/index.d.ts +502 -0
- package/dist/index.js +2307 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2351 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
// src/components/DataGrid.tsx
|
|
7
|
+
|
|
8
|
+
// src/core/values.ts
|
|
9
|
+
var DEFAULT_WIDTH = 150;
|
|
10
|
+
var DEFAULT_MIN_WIDTH = 60;
|
|
11
|
+
function readPath(row, path) {
|
|
12
|
+
if (row == null) return void 0;
|
|
13
|
+
if (!path.includes(".")) return row[path];
|
|
14
|
+
let current = row;
|
|
15
|
+
for (const segment of path.split(".")) {
|
|
16
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
17
|
+
current = current[segment];
|
|
18
|
+
}
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
function resolveValue(row, column) {
|
|
22
|
+
if (column.valueGetter) return column.valueGetter(row);
|
|
23
|
+
const { field } = column;
|
|
24
|
+
if (!field) return void 0;
|
|
25
|
+
const nested = readPath(row, field);
|
|
26
|
+
if (nested !== void 0) return nested;
|
|
27
|
+
const flat = row?.[field];
|
|
28
|
+
if (flat !== void 0) return flat;
|
|
29
|
+
for (const alias of column.fieldAliases ?? []) {
|
|
30
|
+
const value = readPath(row, alias) ?? row?.[alias];
|
|
31
|
+
if (value !== void 0) return value;
|
|
32
|
+
}
|
|
33
|
+
return void 0;
|
|
34
|
+
}
|
|
35
|
+
function formatValue(value, row, column) {
|
|
36
|
+
if (column.valueFormatter) return column.valueFormatter(value, row);
|
|
37
|
+
if (value == null) return "";
|
|
38
|
+
if (value instanceof Date) return value.toISOString().slice(0, 10);
|
|
39
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
40
|
+
return String(value);
|
|
41
|
+
}
|
|
42
|
+
function exportValue(row, column) {
|
|
43
|
+
if (column.exportValue) return column.exportValue(row);
|
|
44
|
+
return formatValue(resolveValue(row, column), row, column);
|
|
45
|
+
}
|
|
46
|
+
function headerText(column) {
|
|
47
|
+
if (column.headerName) return column.headerName;
|
|
48
|
+
if (typeof column.header === "string") return column.header;
|
|
49
|
+
if (typeof column.header === "number") return String(column.header);
|
|
50
|
+
return column.colId ?? column.field ?? "";
|
|
51
|
+
}
|
|
52
|
+
function columnId(column) {
|
|
53
|
+
const id = column.colId ?? column.field;
|
|
54
|
+
if (!id) {
|
|
55
|
+
throw new Error("@helix-x/datagrid-ui: every column needs a `colId` or a `field`.");
|
|
56
|
+
}
|
|
57
|
+
return id;
|
|
58
|
+
}
|
|
59
|
+
function resolveColumn(column) {
|
|
60
|
+
const minWidth = column.minWidth ?? DEFAULT_MIN_WIDTH;
|
|
61
|
+
return {
|
|
62
|
+
...column,
|
|
63
|
+
colId: columnId(column),
|
|
64
|
+
width: Math.max(column.width ?? DEFAULT_WIDTH, minWidth),
|
|
65
|
+
minWidth,
|
|
66
|
+
sortable: column.sortable ?? true,
|
|
67
|
+
resizable: column.resizable ?? true
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function isEditable(column, row) {
|
|
71
|
+
if (typeof column.editable === "function") return column.editable(row);
|
|
72
|
+
return column.editable === true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/core/useColumnState.ts
|
|
76
|
+
function defaultsFrom(columns) {
|
|
77
|
+
const order = [];
|
|
78
|
+
const hidden = [];
|
|
79
|
+
const widths = {};
|
|
80
|
+
const pinned = {};
|
|
81
|
+
for (const column of columns) {
|
|
82
|
+
const id = columnId(column);
|
|
83
|
+
order.push(id);
|
|
84
|
+
if (column.hide) hidden.push(id);
|
|
85
|
+
if (column.width != null) widths[id] = column.width;
|
|
86
|
+
if (column.pinned) pinned[id] = column.pinned;
|
|
87
|
+
}
|
|
88
|
+
return { order, hidden, widths, pinned };
|
|
89
|
+
}
|
|
90
|
+
function mergePersisted(defaults, persisted, knownIds) {
|
|
91
|
+
if (!persisted) return defaults;
|
|
92
|
+
const savedOrder = persisted.order.filter((id) => knownIds.has(id));
|
|
93
|
+
const savedSet = new Set(savedOrder);
|
|
94
|
+
const order = [...savedOrder];
|
|
95
|
+
defaults.order.forEach((id, index) => {
|
|
96
|
+
if (!savedSet.has(id)) order.splice(Math.min(index, order.length), 0, id);
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
order,
|
|
100
|
+
hidden: (persisted.hidden ?? defaults.hidden).filter((id) => knownIds.has(id)),
|
|
101
|
+
widths: { ...defaults.widths, ...persisted.widths ?? {} },
|
|
102
|
+
pinned: { ...defaults.pinned, ...persisted.pinned ?? {} }
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function useColumnState(columns, persisted, onChange, availableWidth) {
|
|
106
|
+
const resolved = react.useMemo(() => columns.map((c) => resolveColumn(c)), [columns]);
|
|
107
|
+
const byId = react.useMemo(() => {
|
|
108
|
+
const map = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const column of resolved) map.set(column.colId, column);
|
|
110
|
+
return map;
|
|
111
|
+
}, [resolved]);
|
|
112
|
+
const defaults = react.useMemo(() => defaultsFrom(columns), [columns]);
|
|
113
|
+
const [state, setState] = react.useState(
|
|
114
|
+
() => mergePersisted(defaults, persisted, new Set(defaults.order))
|
|
115
|
+
);
|
|
116
|
+
const update = react.useCallback(
|
|
117
|
+
(mutate) => {
|
|
118
|
+
setState((current) => {
|
|
119
|
+
const next = mutate(current);
|
|
120
|
+
onChange(next);
|
|
121
|
+
return next;
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
[onChange]
|
|
125
|
+
);
|
|
126
|
+
const hiddenSet = react.useMemo(() => new Set(state.hidden), [state.hidden]);
|
|
127
|
+
const allColumns = react.useMemo(
|
|
128
|
+
() => state.order.map((id) => byId.get(id)).filter((c) => c != null),
|
|
129
|
+
[state.order, byId]
|
|
130
|
+
);
|
|
131
|
+
const visibleColumns = react.useMemo(
|
|
132
|
+
() => allColumns.filter((column) => !hiddenSet.has(column.colId)).map(
|
|
133
|
+
(column) => state.widths[column.colId] != null || state.pinned[column.colId] != null ? {
|
|
134
|
+
...column,
|
|
135
|
+
width: Math.max(
|
|
136
|
+
state.widths[column.colId] ?? column.width,
|
|
137
|
+
column.minWidth
|
|
138
|
+
),
|
|
139
|
+
pinned: state.pinned[column.colId] ?? column.pinned
|
|
140
|
+
} : column
|
|
141
|
+
),
|
|
142
|
+
[allColumns, hiddenSet, state.widths, state.pinned]
|
|
143
|
+
);
|
|
144
|
+
const layout = react.useMemo(() => {
|
|
145
|
+
const flexTotal = visibleColumns.reduce((sum, c) => sum + (c.flex ?? 0), 0);
|
|
146
|
+
const fixedWidth = visibleColumns.reduce(
|
|
147
|
+
(sum, c) => sum + (c.flex ? 0 : c.width),
|
|
148
|
+
0
|
|
149
|
+
);
|
|
150
|
+
const spare = Math.max(0, availableWidth - fixedWidth);
|
|
151
|
+
const widthOf = (column) => {
|
|
152
|
+
if (!column.flex || flexTotal === 0) return column.width;
|
|
153
|
+
const share = Math.floor(spare * column.flex / flexTotal);
|
|
154
|
+
const capped = column.maxWidth ? Math.min(share, column.maxWidth) : share;
|
|
155
|
+
return Math.max(capped, column.minWidth);
|
|
156
|
+
};
|
|
157
|
+
const left = visibleColumns.filter((c) => c.pinned === "left");
|
|
158
|
+
const middle = visibleColumns.filter((c) => !c.pinned);
|
|
159
|
+
const right = visibleColumns.filter((c) => c.pinned === "right");
|
|
160
|
+
const items = [];
|
|
161
|
+
let offset = 0;
|
|
162
|
+
let leftSticky = 0;
|
|
163
|
+
for (const column of left) {
|
|
164
|
+
const width = widthOf(column);
|
|
165
|
+
items.push({
|
|
166
|
+
column,
|
|
167
|
+
colId: column.colId,
|
|
168
|
+
width,
|
|
169
|
+
left: offset,
|
|
170
|
+
pinned: "left",
|
|
171
|
+
stickyOffset: leftSticky
|
|
172
|
+
});
|
|
173
|
+
offset += width;
|
|
174
|
+
leftSticky += width;
|
|
175
|
+
}
|
|
176
|
+
for (const column of middle) {
|
|
177
|
+
const width = widthOf(column);
|
|
178
|
+
items.push({ column, colId: column.colId, width, left: offset, stickyOffset: 0 });
|
|
179
|
+
offset += width;
|
|
180
|
+
}
|
|
181
|
+
const rightWidths = right.map(widthOf);
|
|
182
|
+
let rightSticky = 0;
|
|
183
|
+
const rightOffsets = [];
|
|
184
|
+
for (let i = right.length - 1; i >= 0; i--) {
|
|
185
|
+
rightOffsets[i] = rightSticky;
|
|
186
|
+
rightSticky += rightWidths[i];
|
|
187
|
+
}
|
|
188
|
+
right.forEach((column, index) => {
|
|
189
|
+
const width = rightWidths[index];
|
|
190
|
+
items.push({
|
|
191
|
+
column,
|
|
192
|
+
colId: column.colId,
|
|
193
|
+
width,
|
|
194
|
+
left: offset,
|
|
195
|
+
pinned: "right",
|
|
196
|
+
stickyOffset: rightOffsets[index]
|
|
197
|
+
});
|
|
198
|
+
offset += width;
|
|
199
|
+
});
|
|
200
|
+
return {
|
|
201
|
+
items,
|
|
202
|
+
totalWidth: offset,
|
|
203
|
+
leftPinnedWidth: leftSticky,
|
|
204
|
+
rightPinnedWidth: rightSticky
|
|
205
|
+
};
|
|
206
|
+
}, [visibleColumns, availableWidth]);
|
|
207
|
+
const isHidden = react.useCallback((colId) => hiddenSet.has(colId), [hiddenSet]);
|
|
208
|
+
const setHidden = react.useCallback(
|
|
209
|
+
(colId, hidden) => {
|
|
210
|
+
update((current) => ({
|
|
211
|
+
...current,
|
|
212
|
+
hidden: hidden ? current.hidden.includes(colId) ? current.hidden : [...current.hidden, colId] : current.hidden.filter((id) => id !== colId)
|
|
213
|
+
}));
|
|
214
|
+
},
|
|
215
|
+
[update]
|
|
216
|
+
);
|
|
217
|
+
const setWidth = react.useCallback(
|
|
218
|
+
(colId, width) => {
|
|
219
|
+
update((current) => ({
|
|
220
|
+
...current,
|
|
221
|
+
widths: { ...current.widths, [colId]: Math.round(width) }
|
|
222
|
+
}));
|
|
223
|
+
},
|
|
224
|
+
[update]
|
|
225
|
+
);
|
|
226
|
+
const setPinned = react.useCallback(
|
|
227
|
+
(colId, pinned) => {
|
|
228
|
+
update((current) => {
|
|
229
|
+
const next = { ...current.pinned };
|
|
230
|
+
if (pinned) next[colId] = pinned;
|
|
231
|
+
else delete next[colId];
|
|
232
|
+
return { ...current, pinned: next };
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
[update]
|
|
236
|
+
);
|
|
237
|
+
const moveColumn = react.useCallback(
|
|
238
|
+
(colId, toIndex) => {
|
|
239
|
+
update((current) => {
|
|
240
|
+
const from = current.order.indexOf(colId);
|
|
241
|
+
if (from === -1 || from === toIndex) return current;
|
|
242
|
+
const order = [...current.order];
|
|
243
|
+
order.splice(from, 1);
|
|
244
|
+
order.splice(Math.max(0, Math.min(toIndex, order.length)), 0, colId);
|
|
245
|
+
return { ...current, order };
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
[update]
|
|
249
|
+
);
|
|
250
|
+
const reset = react.useCallback(() => {
|
|
251
|
+
update(() => defaults);
|
|
252
|
+
}, [update, defaults]);
|
|
253
|
+
return {
|
|
254
|
+
allColumns,
|
|
255
|
+
visibleColumns,
|
|
256
|
+
layout,
|
|
257
|
+
state,
|
|
258
|
+
isHidden,
|
|
259
|
+
setHidden,
|
|
260
|
+
setWidth,
|
|
261
|
+
setPinned,
|
|
262
|
+
moveColumn,
|
|
263
|
+
reset
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function setPath(row, path, value) {
|
|
267
|
+
if (!path.includes(".")) {
|
|
268
|
+
return { ...row, [path]: value };
|
|
269
|
+
}
|
|
270
|
+
const [head, ...rest] = path.split(".");
|
|
271
|
+
const current = row[head];
|
|
272
|
+
return {
|
|
273
|
+
...row,
|
|
274
|
+
[head]: setPath(
|
|
275
|
+
current ?? {},
|
|
276
|
+
rest.join("."),
|
|
277
|
+
value
|
|
278
|
+
)
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function useEditModel(onCommit) {
|
|
282
|
+
const [edit, setEdit] = react.useState(null);
|
|
283
|
+
const editRef = react.useRef(null);
|
|
284
|
+
const apply = react.useCallback(
|
|
285
|
+
(next) => {
|
|
286
|
+
editRef.current = next;
|
|
287
|
+
setEdit(next);
|
|
288
|
+
},
|
|
289
|
+
[]
|
|
290
|
+
);
|
|
291
|
+
const patch = react.useCallback((mutate) => {
|
|
292
|
+
const current = editRef.current;
|
|
293
|
+
if (!current) return;
|
|
294
|
+
const next = mutate(current);
|
|
295
|
+
editRef.current = next;
|
|
296
|
+
setEdit(next);
|
|
297
|
+
}, []);
|
|
298
|
+
const isEditing = react.useCallback(
|
|
299
|
+
(rowId) => edit?.rowId === rowId,
|
|
300
|
+
[edit]
|
|
301
|
+
);
|
|
302
|
+
const start = react.useCallback(
|
|
303
|
+
(rowId, row) => {
|
|
304
|
+
apply({ rowId, draft: row, original: row, errors: {}, isSaving: false });
|
|
305
|
+
},
|
|
306
|
+
[apply]
|
|
307
|
+
);
|
|
308
|
+
const setField = react.useCallback(
|
|
309
|
+
(field, value) => {
|
|
310
|
+
patch((current) => {
|
|
311
|
+
const errors = { ...current.errors };
|
|
312
|
+
delete errors[field];
|
|
313
|
+
return { ...current, draft: setPath(current.draft, field, value), errors };
|
|
314
|
+
});
|
|
315
|
+
},
|
|
316
|
+
[patch]
|
|
317
|
+
);
|
|
318
|
+
const cancel = react.useCallback(() => apply(null), [apply]);
|
|
319
|
+
const commit = react.useCallback(async () => {
|
|
320
|
+
const state = editRef.current;
|
|
321
|
+
if (!state || state.isSaving) return;
|
|
322
|
+
apply({ ...state, isSaving: true });
|
|
323
|
+
try {
|
|
324
|
+
const result = await onCommit(state.draft, state.original);
|
|
325
|
+
if (result.ok) {
|
|
326
|
+
apply(null);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
patch((current) => ({ ...current, errors: result.errors, isSaving: false }));
|
|
330
|
+
} catch (error) {
|
|
331
|
+
patch((current) => ({
|
|
332
|
+
...current,
|
|
333
|
+
isSaving: false,
|
|
334
|
+
errors: {
|
|
335
|
+
...current.errors,
|
|
336
|
+
__row__: error instanceof Error ? error.message : "Failed to save the row."
|
|
337
|
+
}
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
}, [onCommit, apply, patch]);
|
|
341
|
+
return { edit, isEditing, start, setField, cancel, commit };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/types.ts
|
|
345
|
+
var GRID_STATE_VERSION = 1;
|
|
346
|
+
|
|
347
|
+
// src/core/useGridState.ts
|
|
348
|
+
var PREFIX = "hxg:";
|
|
349
|
+
function read(storageKey) {
|
|
350
|
+
if (typeof window === "undefined") return void 0;
|
|
351
|
+
try {
|
|
352
|
+
const raw = window.localStorage.getItem(PREFIX + storageKey);
|
|
353
|
+
if (!raw) return void 0;
|
|
354
|
+
const parsed = JSON.parse(raw);
|
|
355
|
+
if (!parsed || parsed.v !== GRID_STATE_VERSION) return void 0;
|
|
356
|
+
return parsed;
|
|
357
|
+
} catch {
|
|
358
|
+
return void 0;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function write(storageKey, state) {
|
|
362
|
+
if (typeof window === "undefined") return;
|
|
363
|
+
try {
|
|
364
|
+
window.localStorage.setItem(PREFIX + storageKey, JSON.stringify(state));
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function useGridState(storageKey) {
|
|
369
|
+
const initialRef = react.useRef(
|
|
370
|
+
storageKey ? read(storageKey) : void 0
|
|
371
|
+
);
|
|
372
|
+
const [hasSavedState, setHasSavedState] = react.useState(() => initialRef.current != null);
|
|
373
|
+
const currentRef = react.useRef(
|
|
374
|
+
initialRef.current ?? {
|
|
375
|
+
v: GRID_STATE_VERSION,
|
|
376
|
+
columns: { order: [], hidden: [], widths: {}, pinned: {} },
|
|
377
|
+
sort: [],
|
|
378
|
+
filters: {},
|
|
379
|
+
pagination: { pageSize: 20 }
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
const persist = react.useCallback(
|
|
383
|
+
(patch) => {
|
|
384
|
+
if (!storageKey) return;
|
|
385
|
+
currentRef.current = { ...currentRef.current, ...patch, v: GRID_STATE_VERSION };
|
|
386
|
+
write(storageKey, currentRef.current);
|
|
387
|
+
setHasSavedState(true);
|
|
388
|
+
},
|
|
389
|
+
[storageKey]
|
|
390
|
+
);
|
|
391
|
+
const saveColumns = react.useCallback(
|
|
392
|
+
(columns) => persist({ columns }),
|
|
393
|
+
[persist]
|
|
394
|
+
);
|
|
395
|
+
const saveSort = react.useCallback((sort) => persist({ sort }), [persist]);
|
|
396
|
+
const saveFilters = react.useCallback(
|
|
397
|
+
(filters) => persist({ filters }),
|
|
398
|
+
[persist]
|
|
399
|
+
);
|
|
400
|
+
const savePageSize = react.useCallback(
|
|
401
|
+
(pageSize) => persist({ pagination: { pageSize } }),
|
|
402
|
+
[persist]
|
|
403
|
+
);
|
|
404
|
+
const clear = react.useCallback(() => {
|
|
405
|
+
if (!storageKey || typeof window === "undefined") return;
|
|
406
|
+
try {
|
|
407
|
+
window.localStorage.removeItem(PREFIX + storageKey);
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
setHasSavedState(false);
|
|
411
|
+
}, [storageKey]);
|
|
412
|
+
return {
|
|
413
|
+
initial: initialRef.current,
|
|
414
|
+
saveColumns,
|
|
415
|
+
saveSort,
|
|
416
|
+
saveFilters,
|
|
417
|
+
savePageSize,
|
|
418
|
+
clear,
|
|
419
|
+
hasSavedState
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function useSelectionModel(rows, getRowId, onSelectionChanged) {
|
|
423
|
+
const [selectedIds, setSelectedIds] = react.useState(() => /* @__PURE__ */ new Set());
|
|
424
|
+
const lastToggledIndexRef = react.useRef(null);
|
|
425
|
+
const rowsRef = react.useRef(rows);
|
|
426
|
+
rowsRef.current = rows;
|
|
427
|
+
const getRowIdRef = react.useRef(getRowId);
|
|
428
|
+
getRowIdRef.current = getRowId;
|
|
429
|
+
const commit = react.useCallback(
|
|
430
|
+
(next) => {
|
|
431
|
+
setSelectedIds(next);
|
|
432
|
+
onSelectionChanged?.([...next]);
|
|
433
|
+
},
|
|
434
|
+
[onSelectionChanged]
|
|
435
|
+
);
|
|
436
|
+
const isSelected = react.useCallback((id) => selectedIds.has(id), [selectedIds]);
|
|
437
|
+
const visibleIds = react.useMemo(() => rows.map(getRowId), [rows, getRowId]);
|
|
438
|
+
const allVisibleSelected = visibleIds.length > 0 && visibleIds.every((id) => selectedIds.has(id));
|
|
439
|
+
const someVisibleSelected = !allVisibleSelected && visibleIds.some((id) => selectedIds.has(id));
|
|
440
|
+
const toggleRow = react.useCallback(
|
|
441
|
+
(id, index, shiftKey) => {
|
|
442
|
+
const next = new Set(selectedIds);
|
|
443
|
+
const anchor = lastToggledIndexRef.current;
|
|
444
|
+
if (shiftKey && anchor != null && anchor !== index) {
|
|
445
|
+
const [from, to] = anchor < index ? [anchor, index] : [index, anchor];
|
|
446
|
+
const shouldSelect = !next.has(id);
|
|
447
|
+
for (let i = from; i <= to; i++) {
|
|
448
|
+
const rowId = getRowIdRef.current(rowsRef.current[i]);
|
|
449
|
+
if (rowId == null) continue;
|
|
450
|
+
if (shouldSelect) next.add(rowId);
|
|
451
|
+
else next.delete(rowId);
|
|
452
|
+
}
|
|
453
|
+
} else if (next.has(id)) {
|
|
454
|
+
next.delete(id);
|
|
455
|
+
} else {
|
|
456
|
+
next.add(id);
|
|
457
|
+
}
|
|
458
|
+
lastToggledIndexRef.current = index;
|
|
459
|
+
commit(next);
|
|
460
|
+
},
|
|
461
|
+
[selectedIds, commit]
|
|
462
|
+
);
|
|
463
|
+
const toggleAllVisible = react.useCallback(() => {
|
|
464
|
+
const next = new Set(selectedIds);
|
|
465
|
+
if (allVisibleSelected) {
|
|
466
|
+
for (const id of visibleIds) next.delete(id);
|
|
467
|
+
} else {
|
|
468
|
+
for (const id of visibleIds) next.add(id);
|
|
469
|
+
}
|
|
470
|
+
lastToggledIndexRef.current = null;
|
|
471
|
+
commit(next);
|
|
472
|
+
}, [selectedIds, allVisibleSelected, visibleIds, commit]);
|
|
473
|
+
const selectAllVisible = react.useCallback(() => {
|
|
474
|
+
const next = new Set(selectedIds);
|
|
475
|
+
for (const id of visibleIds) next.add(id);
|
|
476
|
+
commit(next);
|
|
477
|
+
}, [selectedIds, visibleIds, commit]);
|
|
478
|
+
const clear = react.useCallback(() => {
|
|
479
|
+
lastToggledIndexRef.current = null;
|
|
480
|
+
commit(/* @__PURE__ */ new Set());
|
|
481
|
+
}, [commit]);
|
|
482
|
+
const getSelectedRows = react.useCallback(
|
|
483
|
+
() => rowsRef.current.filter((row) => selectedIds.has(getRowIdRef.current(row))),
|
|
484
|
+
[selectedIds]
|
|
485
|
+
);
|
|
486
|
+
return {
|
|
487
|
+
selectedIds,
|
|
488
|
+
isSelected,
|
|
489
|
+
allVisibleSelected,
|
|
490
|
+
someVisibleSelected,
|
|
491
|
+
toggleRow,
|
|
492
|
+
toggleAllVisible,
|
|
493
|
+
clear,
|
|
494
|
+
selectAllVisible,
|
|
495
|
+
getSelectedRows
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function buildRequest(page, pageSize, sortModel, filterModel) {
|
|
499
|
+
return {
|
|
500
|
+
startRow: page * pageSize,
|
|
501
|
+
endRow: page * pageSize + pageSize,
|
|
502
|
+
sortModel,
|
|
503
|
+
filterModel,
|
|
504
|
+
// Sent empty for wire compatibility with grid endpoints that expect them.
|
|
505
|
+
rowGroupCols: [],
|
|
506
|
+
valueCols: [],
|
|
507
|
+
pivotCols: [],
|
|
508
|
+
pivotMode: false,
|
|
509
|
+
groupKeys: []
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function useServerDataSource({
|
|
513
|
+
dataSource,
|
|
514
|
+
pageSize,
|
|
515
|
+
page,
|
|
516
|
+
sortModel,
|
|
517
|
+
filterModel,
|
|
518
|
+
maxCachedBlocks = 3,
|
|
519
|
+
onError
|
|
520
|
+
}) {
|
|
521
|
+
const [rows, setRows] = react.useState([]);
|
|
522
|
+
const [totalRows, setTotalRows] = react.useState(0);
|
|
523
|
+
const [isLoading, setIsLoading] = react.useState(false);
|
|
524
|
+
const [error, setError] = react.useState(null);
|
|
525
|
+
const [refreshToken, setRefreshToken] = react.useState(0);
|
|
526
|
+
const cacheRef = react.useRef(/* @__PURE__ */ new Map());
|
|
527
|
+
const requestIdRef = react.useRef(0);
|
|
528
|
+
const abortRef = react.useRef(null);
|
|
529
|
+
const queryKey = react.useMemo(
|
|
530
|
+
() => JSON.stringify({ pageSize, sortModel, filterModel }),
|
|
531
|
+
[pageSize, sortModel, filterModel]
|
|
532
|
+
);
|
|
533
|
+
react.useEffect(() => {
|
|
534
|
+
cacheRef.current.clear();
|
|
535
|
+
}, [queryKey]);
|
|
536
|
+
const onErrorRef = react.useRef(onError);
|
|
537
|
+
react.useEffect(() => {
|
|
538
|
+
onErrorRef.current = onError;
|
|
539
|
+
}, [onError]);
|
|
540
|
+
const dataSourceRef = react.useRef(dataSource);
|
|
541
|
+
react.useEffect(() => {
|
|
542
|
+
dataSourceRef.current = dataSource;
|
|
543
|
+
}, [dataSource]);
|
|
544
|
+
react.useEffect(() => {
|
|
545
|
+
const blockKey = `${queryKey}::${page}`;
|
|
546
|
+
const cached = cacheRef.current.get(blockKey);
|
|
547
|
+
if (cached) {
|
|
548
|
+
setRows(cached.rows);
|
|
549
|
+
setTotalRows(cached.lastRow);
|
|
550
|
+
setError(null);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
abortRef.current?.abort();
|
|
554
|
+
const controller = new AbortController();
|
|
555
|
+
abortRef.current = controller;
|
|
556
|
+
const requestId = ++requestIdRef.current;
|
|
557
|
+
setIsLoading(true);
|
|
558
|
+
setError(null);
|
|
559
|
+
const request = buildRequest(page, pageSize, sortModel, filterModel);
|
|
560
|
+
dataSourceRef.current.getRows(request, controller.signal).then((response) => {
|
|
561
|
+
if (requestId !== requestIdRef.current) return;
|
|
562
|
+
const lastRow = response.lastRow ?? response.rows.length;
|
|
563
|
+
cacheRef.current.set(blockKey, { rows: response.rows, lastRow });
|
|
564
|
+
while (cacheRef.current.size > maxCachedBlocks) {
|
|
565
|
+
const oldest = cacheRef.current.keys().next().value;
|
|
566
|
+
if (oldest === void 0) break;
|
|
567
|
+
cacheRef.current.delete(oldest);
|
|
568
|
+
}
|
|
569
|
+
setRows(response.rows);
|
|
570
|
+
setTotalRows(lastRow);
|
|
571
|
+
setIsLoading(false);
|
|
572
|
+
}).catch((err) => {
|
|
573
|
+
if (controller.signal.aborted) return;
|
|
574
|
+
if (requestId !== requestIdRef.current) return;
|
|
575
|
+
setError(err);
|
|
576
|
+
setIsLoading(false);
|
|
577
|
+
onErrorRef.current?.(err);
|
|
578
|
+
});
|
|
579
|
+
return () => {
|
|
580
|
+
controller.abort();
|
|
581
|
+
};
|
|
582
|
+
}, [queryKey, page, pageSize, sortModel, filterModel, refreshToken, maxCachedBlocks]);
|
|
583
|
+
const refresh = react.useCallback((options) => {
|
|
584
|
+
if (options?.purge !== false) cacheRef.current.clear();
|
|
585
|
+
setRefreshToken((token) => token + 1);
|
|
586
|
+
}, []);
|
|
587
|
+
const patchRows = react.useCallback(
|
|
588
|
+
(updated, getRowId) => {
|
|
589
|
+
if (updated.length === 0) return;
|
|
590
|
+
const byId = new Map(updated.map((row) => [getRowId(row), row]));
|
|
591
|
+
const merge = (list) => {
|
|
592
|
+
let changed = false;
|
|
593
|
+
const next = list.map((row) => {
|
|
594
|
+
const replacement = byId.get(getRowId(row));
|
|
595
|
+
if (!replacement) return row;
|
|
596
|
+
changed = true;
|
|
597
|
+
return replacement;
|
|
598
|
+
});
|
|
599
|
+
return changed ? next : list;
|
|
600
|
+
};
|
|
601
|
+
setRows(merge);
|
|
602
|
+
for (const [key, block] of cacheRef.current) {
|
|
603
|
+
cacheRef.current.set(key, { ...block, rows: merge(block.rows) });
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
[]
|
|
607
|
+
);
|
|
608
|
+
return { rows, totalRows, isLoading, error, refresh, patchRows };
|
|
609
|
+
}
|
|
610
|
+
function useVirtualRows({
|
|
611
|
+
rowCount,
|
|
612
|
+
rowHeight,
|
|
613
|
+
overscan = 6
|
|
614
|
+
}) {
|
|
615
|
+
const [viewportHeight, setViewportHeightState] = react.useState(0);
|
|
616
|
+
const [window2, setWindow] = react.useState({ startIndex: 0, endIndex: 0 });
|
|
617
|
+
const scrollTopRef = react.useRef(0);
|
|
618
|
+
const frameRef = react.useRef(null);
|
|
619
|
+
const compute = react.useCallback(
|
|
620
|
+
(scrollTop, height) => {
|
|
621
|
+
if (height <= 0 || rowCount === 0) {
|
|
622
|
+
return { startIndex: 0, endIndex: 0 };
|
|
623
|
+
}
|
|
624
|
+
const first = Math.floor(scrollTop / rowHeight);
|
|
625
|
+
const visible = Math.ceil(height / rowHeight);
|
|
626
|
+
return {
|
|
627
|
+
startIndex: Math.max(0, first - overscan),
|
|
628
|
+
endIndex: Math.min(rowCount, first + visible + overscan)
|
|
629
|
+
};
|
|
630
|
+
},
|
|
631
|
+
[rowCount, rowHeight, overscan]
|
|
632
|
+
);
|
|
633
|
+
const apply = react.useCallback(
|
|
634
|
+
(scrollTop, height) => {
|
|
635
|
+
const next = compute(scrollTop, height);
|
|
636
|
+
setWindow(
|
|
637
|
+
(current) => current.startIndex === next.startIndex && current.endIndex === next.endIndex ? current : next
|
|
638
|
+
);
|
|
639
|
+
},
|
|
640
|
+
[compute]
|
|
641
|
+
);
|
|
642
|
+
const onScroll = react.useCallback(
|
|
643
|
+
(event) => {
|
|
644
|
+
scrollTopRef.current = event.currentTarget.scrollTop;
|
|
645
|
+
if (frameRef.current != null) return;
|
|
646
|
+
frameRef.current = requestAnimationFrame(() => {
|
|
647
|
+
frameRef.current = null;
|
|
648
|
+
apply(scrollTopRef.current, viewportHeight);
|
|
649
|
+
});
|
|
650
|
+
},
|
|
651
|
+
[apply, viewportHeight]
|
|
652
|
+
);
|
|
653
|
+
const setViewportHeight = react.useCallback((height) => {
|
|
654
|
+
setViewportHeightState((current) => current === height ? current : height);
|
|
655
|
+
}, []);
|
|
656
|
+
react.useEffect(() => {
|
|
657
|
+
apply(scrollTopRef.current, viewportHeight);
|
|
658
|
+
}, [apply, viewportHeight]);
|
|
659
|
+
react.useEffect(
|
|
660
|
+
() => () => {
|
|
661
|
+
if (frameRef.current != null) cancelAnimationFrame(frameRef.current);
|
|
662
|
+
},
|
|
663
|
+
[]
|
|
664
|
+
);
|
|
665
|
+
return {
|
|
666
|
+
window: window2,
|
|
667
|
+
totalHeight: rowCount * rowHeight,
|
|
668
|
+
onScroll,
|
|
669
|
+
setViewportHeight,
|
|
670
|
+
scrollTopRef
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/core/exporters.ts
|
|
675
|
+
function escapeCsv(value, separator) {
|
|
676
|
+
if (value === "") return "";
|
|
677
|
+
const needsQuotes = value.includes(separator) || value.includes('"') || value.includes("\n") || value.includes("\r");
|
|
678
|
+
return needsQuotes ? `"${value.replace(/"/g, '""')}"` : value;
|
|
679
|
+
}
|
|
680
|
+
function toDelimited(rows, columns, separator) {
|
|
681
|
+
const exportable = columns.filter((column) => !column.suppressExport);
|
|
682
|
+
const lines = [
|
|
683
|
+
exportable.map((c) => escapeCsv(headerText(c), separator)).join(separator)
|
|
684
|
+
];
|
|
685
|
+
for (const row of rows) {
|
|
686
|
+
lines.push(
|
|
687
|
+
exportable.map((column) => escapeCsv(exportValue(row, column), separator)).join(separator)
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
return lines.join("\r\n");
|
|
691
|
+
}
|
|
692
|
+
function toCsv(rows, columns, separator = ",") {
|
|
693
|
+
return toDelimited(rows, columns, separator);
|
|
694
|
+
}
|
|
695
|
+
function toTsv(rows, columns) {
|
|
696
|
+
return toDelimited(rows, columns, " ");
|
|
697
|
+
}
|
|
698
|
+
function downloadCsv(content, fileName) {
|
|
699
|
+
const blob = new Blob(["\uFEFF" + content], {
|
|
700
|
+
type: "text/csv;charset=utf-8;"
|
|
701
|
+
});
|
|
702
|
+
const url = URL.createObjectURL(blob);
|
|
703
|
+
const anchor = document.createElement("a");
|
|
704
|
+
anchor.href = url;
|
|
705
|
+
anchor.download = fileName.endsWith(".csv") ? fileName : `${fileName}.csv`;
|
|
706
|
+
document.body.appendChild(anchor);
|
|
707
|
+
anchor.click();
|
|
708
|
+
document.body.removeChild(anchor);
|
|
709
|
+
URL.revokeObjectURL(url);
|
|
710
|
+
}
|
|
711
|
+
async function copyToClipboard(text) {
|
|
712
|
+
if (navigator?.clipboard?.writeText) {
|
|
713
|
+
await navigator.clipboard.writeText(text);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const textarea = document.createElement("textarea");
|
|
717
|
+
textarea.value = text;
|
|
718
|
+
textarea.style.position = "fixed";
|
|
719
|
+
textarea.style.opacity = "0";
|
|
720
|
+
document.body.appendChild(textarea);
|
|
721
|
+
textarea.select();
|
|
722
|
+
document.execCommand("copy");
|
|
723
|
+
document.body.removeChild(textarea);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// src/core/filterModel.ts
|
|
727
|
+
var TEXT_FILTER_TYPES = [
|
|
728
|
+
"contains",
|
|
729
|
+
"notContains",
|
|
730
|
+
"equals",
|
|
731
|
+
"notEqual",
|
|
732
|
+
"startsWith",
|
|
733
|
+
"endsWith",
|
|
734
|
+
"blank",
|
|
735
|
+
"notBlank"
|
|
736
|
+
];
|
|
737
|
+
var NUMBER_FILTER_TYPES = [
|
|
738
|
+
"equals",
|
|
739
|
+
"notEqual",
|
|
740
|
+
"lessThan",
|
|
741
|
+
"lessThanOrEqual",
|
|
742
|
+
"greaterThan",
|
|
743
|
+
"greaterThanOrEqual",
|
|
744
|
+
"inRange",
|
|
745
|
+
"blank",
|
|
746
|
+
"notBlank"
|
|
747
|
+
];
|
|
748
|
+
var DATE_FILTER_TYPES = [
|
|
749
|
+
"equals",
|
|
750
|
+
"notEqual",
|
|
751
|
+
"before",
|
|
752
|
+
"after",
|
|
753
|
+
"inRange",
|
|
754
|
+
"blank",
|
|
755
|
+
"notBlank"
|
|
756
|
+
];
|
|
757
|
+
var FILTER_TYPE_LABELS = {
|
|
758
|
+
contains: "Contains",
|
|
759
|
+
notContains: "Does not contain",
|
|
760
|
+
equals: "Equals",
|
|
761
|
+
notEqual: "Not equal",
|
|
762
|
+
startsWith: "Starts with",
|
|
763
|
+
endsWith: "Ends with",
|
|
764
|
+
blank: "Is empty",
|
|
765
|
+
notBlank: "Is not empty",
|
|
766
|
+
lessThan: "Less than",
|
|
767
|
+
lessThanOrEqual: "Less than or equal",
|
|
768
|
+
greaterThan: "Greater than",
|
|
769
|
+
greaterThanOrEqual: "Greater than or equal",
|
|
770
|
+
inRange: "Between",
|
|
771
|
+
before: "Before",
|
|
772
|
+
after: "After"
|
|
773
|
+
};
|
|
774
|
+
function isUnaryFilter(type) {
|
|
775
|
+
return type === "blank" || type === "notBlank";
|
|
776
|
+
}
|
|
777
|
+
function isRangeFilter(type) {
|
|
778
|
+
return type === "inRange";
|
|
779
|
+
}
|
|
780
|
+
function defaultFilterType(kind) {
|
|
781
|
+
switch (kind) {
|
|
782
|
+
case "text":
|
|
783
|
+
return "contains";
|
|
784
|
+
case "number":
|
|
785
|
+
return "equals";
|
|
786
|
+
case "date":
|
|
787
|
+
return "equals";
|
|
788
|
+
case "set":
|
|
789
|
+
return "set";
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
function buildTextFilter(type, filter) {
|
|
793
|
+
if (!isUnaryFilter(type) && filter.trim() === "") return null;
|
|
794
|
+
return isUnaryFilter(type) ? { filterType: "text", type } : { filterType: "text", type, filter };
|
|
795
|
+
}
|
|
796
|
+
function buildNumberFilter(type, filter, filterTo) {
|
|
797
|
+
if (isUnaryFilter(type)) return { filterType: "number", type };
|
|
798
|
+
const from = Number(filter);
|
|
799
|
+
if (filter.trim() === "" || Number.isNaN(from)) return null;
|
|
800
|
+
if (isRangeFilter(type)) {
|
|
801
|
+
const to = Number(filterTo);
|
|
802
|
+
if (!filterTo || filterTo.trim() === "" || Number.isNaN(to)) return null;
|
|
803
|
+
return { filterType: "number", type, filter: from, filterTo: to };
|
|
804
|
+
}
|
|
805
|
+
return { filterType: "number", type, filter: from };
|
|
806
|
+
}
|
|
807
|
+
function buildDateFilter(type, dateFrom, dateTo) {
|
|
808
|
+
if (isUnaryFilter(type)) return { filterType: "date", type };
|
|
809
|
+
if (!dateFrom) return null;
|
|
810
|
+
if (isRangeFilter(type)) {
|
|
811
|
+
if (!dateTo) return null;
|
|
812
|
+
return { filterType: "date", type, dateFrom, dateTo };
|
|
813
|
+
}
|
|
814
|
+
return { filterType: "date", type, dateFrom };
|
|
815
|
+
}
|
|
816
|
+
function buildSetFilter(values) {
|
|
817
|
+
if (values.length === 0) return null;
|
|
818
|
+
return { filterType: "set", values };
|
|
819
|
+
}
|
|
820
|
+
function withFilter(model, colId, filter) {
|
|
821
|
+
const next = { ...model };
|
|
822
|
+
if (filter === null) {
|
|
823
|
+
delete next[colId];
|
|
824
|
+
} else {
|
|
825
|
+
next[colId] = filter;
|
|
826
|
+
}
|
|
827
|
+
return next;
|
|
828
|
+
}
|
|
829
|
+
function describeFilter(filter) {
|
|
830
|
+
switch (filter.filterType) {
|
|
831
|
+
case "set":
|
|
832
|
+
return filter.values.length === 1 ? filter.values[0] : `${filter.values.length} selected`;
|
|
833
|
+
case "text":
|
|
834
|
+
return isUnaryFilter(filter.type) ? FILTER_TYPE_LABELS[filter.type] : String(filter.filter ?? "");
|
|
835
|
+
case "number":
|
|
836
|
+
if (isUnaryFilter(filter.type)) return FILTER_TYPE_LABELS[filter.type];
|
|
837
|
+
return isRangeFilter(filter.type) ? `${filter.filter} - ${filter.filterTo}` : `${FILTER_TYPE_LABELS[filter.type]} ${filter.filter}`;
|
|
838
|
+
case "date":
|
|
839
|
+
if (isUnaryFilter(filter.type)) return FILTER_TYPE_LABELS[filter.type];
|
|
840
|
+
return isRangeFilter(filter.type) ? `${filter.dateFrom} - ${filter.dateTo}` : `${FILTER_TYPE_LABELS[filter.type]} ${filter.dateFrom}`;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function ColumnsPanel({
|
|
844
|
+
columns,
|
|
845
|
+
isHidden,
|
|
846
|
+
onToggle,
|
|
847
|
+
onMove,
|
|
848
|
+
onPin,
|
|
849
|
+
onReset,
|
|
850
|
+
onClose
|
|
851
|
+
}) {
|
|
852
|
+
const [search, setSearch] = react.useState("");
|
|
853
|
+
const filtered = react.useMemo(() => {
|
|
854
|
+
const needle = search.trim().toLowerCase();
|
|
855
|
+
if (!needle) return columns;
|
|
856
|
+
return columns.filter(
|
|
857
|
+
(column) => headerText(column).toLowerCase().includes(needle)
|
|
858
|
+
);
|
|
859
|
+
}, [columns, search]);
|
|
860
|
+
const visibleCount = columns.filter((c) => !isHidden(c.colId)).length;
|
|
861
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full w-60 shrink-0 flex-col gap-2 border-r border-gray-200 bg-white p-2 dark:border-gray-700 dark:bg-gray-900", children: [
|
|
862
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
863
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-semibold uppercase tracking-wide text-gray-600 dark:text-gray-300", children: "Columns" }),
|
|
864
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
865
|
+
"button",
|
|
866
|
+
{
|
|
867
|
+
type: "button",
|
|
868
|
+
"aria-label": "Close columns panel",
|
|
869
|
+
className: "rounded px-1.5 text-sm text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/5",
|
|
870
|
+
onClick: onClose,
|
|
871
|
+
children: "\xD7"
|
|
872
|
+
}
|
|
873
|
+
)
|
|
874
|
+
] }),
|
|
875
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
876
|
+
"input",
|
|
877
|
+
{
|
|
878
|
+
type: "search",
|
|
879
|
+
className: "h-7 w-full rounded border border-gray-300 bg-white px-2 text-xs text-gray-800 outline-none focus:border-brand-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100",
|
|
880
|
+
placeholder: "Search columns...",
|
|
881
|
+
value: search,
|
|
882
|
+
onChange: (event) => setSearch(event.target.value)
|
|
883
|
+
}
|
|
884
|
+
),
|
|
885
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-[11px] text-gray-500 dark:text-gray-400", children: [
|
|
886
|
+
visibleCount,
|
|
887
|
+
" of ",
|
|
888
|
+
columns.length,
|
|
889
|
+
" shown"
|
|
890
|
+
] }),
|
|
891
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: filtered.map((column) => {
|
|
892
|
+
const hidden = isHidden(column.colId);
|
|
893
|
+
const index = columns.findIndex((c) => c.colId === column.colId);
|
|
894
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
895
|
+
"div",
|
|
896
|
+
{
|
|
897
|
+
className: "group flex items-center gap-1 rounded px-1 py-1 hover:bg-gray-100 dark:hover:bg-white/5",
|
|
898
|
+
draggable: true,
|
|
899
|
+
onDragStart: (event) => event.dataTransfer.setData("text/plain", column.colId),
|
|
900
|
+
onDragOver: (event) => event.preventDefault(),
|
|
901
|
+
onDrop: (event) => {
|
|
902
|
+
event.preventDefault();
|
|
903
|
+
const dragged = event.dataTransfer.getData("text/plain");
|
|
904
|
+
if (dragged && dragged !== column.colId) onMove(dragged, index);
|
|
905
|
+
},
|
|
906
|
+
children: [
|
|
907
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cursor-grab text-[10px] text-gray-400", "aria-hidden": true, children: "\u283F" }),
|
|
908
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex min-w-0 flex-1 cursor-pointer items-center gap-1.5 text-xs text-gray-700 dark:text-gray-200", children: [
|
|
909
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
910
|
+
"input",
|
|
911
|
+
{
|
|
912
|
+
type: "checkbox",
|
|
913
|
+
className: "h-3.5 w-3.5 accent-brand-500",
|
|
914
|
+
checked: !hidden,
|
|
915
|
+
onChange: () => onToggle(column.colId, !hidden)
|
|
916
|
+
}
|
|
917
|
+
),
|
|
918
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: headerText(column) })
|
|
919
|
+
] }),
|
|
920
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
921
|
+
"button",
|
|
922
|
+
{
|
|
923
|
+
type: "button",
|
|
924
|
+
className: `shrink-0 rounded px-1 text-[10px] ${column.pinned ? "text-brand-500 dark:text-brand-400" : "text-gray-400 opacity-0 group-hover:opacity-100"}`,
|
|
925
|
+
title: column.pinned ? `Unpin (${column.pinned})` : "Pin left",
|
|
926
|
+
onClick: () => onPin(column.colId, column.pinned ? void 0 : "left"),
|
|
927
|
+
children: "\u{1F4CC}"
|
|
928
|
+
}
|
|
929
|
+
)
|
|
930
|
+
]
|
|
931
|
+
},
|
|
932
|
+
column.colId
|
|
933
|
+
);
|
|
934
|
+
}) }),
|
|
935
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
936
|
+
"button",
|
|
937
|
+
{
|
|
938
|
+
type: "button",
|
|
939
|
+
className: "rounded border border-error-300 px-2 py-1 text-xs font-medium text-error-600 hover:bg-error-50 dark:border-error-700 dark:text-error-400 dark:hover:bg-error-500/10",
|
|
940
|
+
onClick: onReset,
|
|
941
|
+
children: "Reset to defaults"
|
|
942
|
+
}
|
|
943
|
+
)
|
|
944
|
+
] });
|
|
945
|
+
}
|
|
946
|
+
var SELECTION_COLUMN_WIDTH = 40;
|
|
947
|
+
function SelectionCell({
|
|
948
|
+
checked,
|
|
949
|
+
indeterminate,
|
|
950
|
+
onToggle,
|
|
951
|
+
isHeader,
|
|
952
|
+
label
|
|
953
|
+
}) {
|
|
954
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
955
|
+
"div",
|
|
956
|
+
{
|
|
957
|
+
className: `sticky left-0 z-[3] flex h-full items-center justify-center border-r border-gray-200 dark:border-gray-700 ${isHeader ? "bg-gray-100 dark:bg-gray-800" : "bg-inherit"}`,
|
|
958
|
+
style: { width: SELECTION_COLUMN_WIDTH, minWidth: SELECTION_COLUMN_WIDTH },
|
|
959
|
+
onClick: (event) => event.stopPropagation(),
|
|
960
|
+
onDoubleClick: (event) => event.stopPropagation(),
|
|
961
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
962
|
+
"input",
|
|
963
|
+
{
|
|
964
|
+
type: "checkbox",
|
|
965
|
+
className: "h-3.5 w-3.5 cursor-pointer accent-brand-500",
|
|
966
|
+
checked,
|
|
967
|
+
"aria-label": label ?? (isHeader ? "Select all rows" : "Select row"),
|
|
968
|
+
ref: (node) => {
|
|
969
|
+
if (node) node.indeterminate = indeterminate === true;
|
|
970
|
+
},
|
|
971
|
+
onChange: () => void 0,
|
|
972
|
+
onClick: (event) => onToggle(event.shiftKey)
|
|
973
|
+
}
|
|
974
|
+
)
|
|
975
|
+
}
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
var CELL_INPUT_CLASS = "h-6 w-full rounded border border-gray-300 bg-white px-1.5 text-[11px] text-gray-800 outline-none focus:border-brand-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100";
|
|
979
|
+
function FloatingCell({
|
|
980
|
+
colId,
|
|
981
|
+
kind,
|
|
982
|
+
filter,
|
|
983
|
+
onFilterChange
|
|
984
|
+
}) {
|
|
985
|
+
const isFreeText = kind === "text";
|
|
986
|
+
const [draft, setDraft] = react.useState(
|
|
987
|
+
filter?.filterType === "text" ? filter.filter ?? "" : ""
|
|
988
|
+
);
|
|
989
|
+
react.useEffect(() => {
|
|
990
|
+
if (!isFreeText) return;
|
|
991
|
+
setDraft(filter?.filterType === "text" ? filter.filter ?? "" : "");
|
|
992
|
+
}, [filter, isFreeText]);
|
|
993
|
+
if (!isFreeText) {
|
|
994
|
+
const summary = filter ? describeFilter(filter) : "";
|
|
995
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
996
|
+
"div",
|
|
997
|
+
{
|
|
998
|
+
className: "flex h-6 items-center truncate rounded border border-dashed border-gray-300 px-1.5 text-[11px] text-gray-500 dark:border-gray-600 dark:text-gray-400",
|
|
999
|
+
title: summary,
|
|
1000
|
+
children: summary || /* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-50", children: "--" })
|
|
1001
|
+
}
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1005
|
+
"input",
|
|
1006
|
+
{
|
|
1007
|
+
type: "search",
|
|
1008
|
+
className: CELL_INPUT_CLASS,
|
|
1009
|
+
value: draft,
|
|
1010
|
+
"aria-label": `Filter ${colId}`,
|
|
1011
|
+
onChange: (event) => {
|
|
1012
|
+
const next = event.target.value;
|
|
1013
|
+
setDraft(next);
|
|
1014
|
+
onFilterChange(colId, buildTextFilter("contains", next));
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
function FloatingFilterRow({
|
|
1020
|
+
layout,
|
|
1021
|
+
filterModel,
|
|
1022
|
+
height,
|
|
1023
|
+
selectable,
|
|
1024
|
+
onFilterChange
|
|
1025
|
+
}) {
|
|
1026
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1027
|
+
"div",
|
|
1028
|
+
{
|
|
1029
|
+
role: "row",
|
|
1030
|
+
className: "sticky z-10 flex border-b border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800/60",
|
|
1031
|
+
style: {
|
|
1032
|
+
height,
|
|
1033
|
+
width: layout.totalWidth + (selectable ? SELECTION_COLUMN_WIDTH : 0)
|
|
1034
|
+
},
|
|
1035
|
+
children: [
|
|
1036
|
+
selectable && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1037
|
+
"div",
|
|
1038
|
+
{
|
|
1039
|
+
className: "sticky left-0 z-[3] h-full border-r border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800",
|
|
1040
|
+
style: { width: SELECTION_COLUMN_WIDTH, minWidth: SELECTION_COLUMN_WIDTH }
|
|
1041
|
+
}
|
|
1042
|
+
),
|
|
1043
|
+
layout.items.map(({ column, width, pinned, stickyOffset }) => {
|
|
1044
|
+
const style = {
|
|
1045
|
+
width,
|
|
1046
|
+
minWidth: width,
|
|
1047
|
+
maxWidth: width,
|
|
1048
|
+
...pinned === "left" ? { position: "sticky", left: stickyOffset, zIndex: 3 } : pinned === "right" ? { position: "sticky", right: stickyOffset, zIndex: 3 } : null
|
|
1049
|
+
};
|
|
1050
|
+
const showInput = column.filter && !column.filterParams?.suppressFloatingFilter;
|
|
1051
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1052
|
+
"div",
|
|
1053
|
+
{
|
|
1054
|
+
className: `flex h-full items-center border-r border-gray-200 px-1 dark:border-gray-700 ${pinned ? "bg-gray-50 dark:bg-gray-800" : ""}`,
|
|
1055
|
+
style,
|
|
1056
|
+
children: showInput && column.filter && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1057
|
+
FloatingCell,
|
|
1058
|
+
{
|
|
1059
|
+
colId: column.colId,
|
|
1060
|
+
kind: column.filter,
|
|
1061
|
+
filter: filterModel[column.colId],
|
|
1062
|
+
onFilterChange
|
|
1063
|
+
}
|
|
1064
|
+
)
|
|
1065
|
+
},
|
|
1066
|
+
column.colId
|
|
1067
|
+
);
|
|
1068
|
+
})
|
|
1069
|
+
]
|
|
1070
|
+
}
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
var FIELD_CLASS = "h-8 w-full rounded border border-gray-300 bg-white px-2 text-xs text-gray-800 outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100";
|
|
1074
|
+
var BUTTON_CLASS = "rounded px-2.5 py-1 text-xs font-medium transition-colors disabled:opacity-50";
|
|
1075
|
+
function SetFilterBody({
|
|
1076
|
+
value,
|
|
1077
|
+
setValues,
|
|
1078
|
+
onApply
|
|
1079
|
+
}) {
|
|
1080
|
+
const [options, setOptions] = react.useState(
|
|
1081
|
+
Array.isArray(setValues) ? setValues : []
|
|
1082
|
+
);
|
|
1083
|
+
const [isLoading, setIsLoading] = react.useState(typeof setValues === "function");
|
|
1084
|
+
const [search, setSearch] = react.useState("");
|
|
1085
|
+
const [selected, setSelected] = react.useState(
|
|
1086
|
+
() => new Set(value?.filterType === "set" ? value.values : [])
|
|
1087
|
+
);
|
|
1088
|
+
react.useEffect(() => {
|
|
1089
|
+
if (typeof setValues !== "function") {
|
|
1090
|
+
setOptions(Array.isArray(setValues) ? setValues : []);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
let cancelled = false;
|
|
1094
|
+
setIsLoading(true);
|
|
1095
|
+
setValues().then((loaded) => {
|
|
1096
|
+
if (!cancelled) setOptions(loaded);
|
|
1097
|
+
}).catch(() => {
|
|
1098
|
+
if (!cancelled) setOptions([]);
|
|
1099
|
+
}).finally(() => {
|
|
1100
|
+
if (!cancelled) setIsLoading(false);
|
|
1101
|
+
});
|
|
1102
|
+
return () => {
|
|
1103
|
+
cancelled = true;
|
|
1104
|
+
};
|
|
1105
|
+
}, [setValues]);
|
|
1106
|
+
const filtered = react.useMemo(() => {
|
|
1107
|
+
const needle = search.trim().toLowerCase();
|
|
1108
|
+
return needle ? options.filter((o) => o.toLowerCase().includes(needle)) : options;
|
|
1109
|
+
}, [options, search]);
|
|
1110
|
+
const toggle = (option) => {
|
|
1111
|
+
setSelected((current) => {
|
|
1112
|
+
const next = new Set(current);
|
|
1113
|
+
if (next.has(option)) next.delete(option);
|
|
1114
|
+
else next.add(option);
|
|
1115
|
+
return next;
|
|
1116
|
+
});
|
|
1117
|
+
};
|
|
1118
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1119
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1120
|
+
"input",
|
|
1121
|
+
{
|
|
1122
|
+
type: "search",
|
|
1123
|
+
className: FIELD_CLASS,
|
|
1124
|
+
placeholder: "Search values...",
|
|
1125
|
+
value: search,
|
|
1126
|
+
onChange: (event) => setSearch(event.target.value)
|
|
1127
|
+
}
|
|
1128
|
+
),
|
|
1129
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-h-52 overflow-y-auto rounded border border-gray-200 dark:border-gray-700", children: [
|
|
1130
|
+
isLoading && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "p-2 text-xs text-gray-500 dark:text-gray-400", children: "Loading..." }),
|
|
1131
|
+
!isLoading && filtered.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "p-2 text-xs text-gray-500 dark:text-gray-400", children: "No values." }),
|
|
1132
|
+
filtered.map((option) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1133
|
+
"label",
|
|
1134
|
+
{
|
|
1135
|
+
className: "flex cursor-pointer items-center gap-2 px-2 py-1 text-xs text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-white/5",
|
|
1136
|
+
children: [
|
|
1137
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1138
|
+
"input",
|
|
1139
|
+
{
|
|
1140
|
+
type: "checkbox",
|
|
1141
|
+
className: "h-3.5 w-3.5 accent-brand-500",
|
|
1142
|
+
checked: selected.has(option),
|
|
1143
|
+
onChange: () => toggle(option)
|
|
1144
|
+
}
|
|
1145
|
+
),
|
|
1146
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: option })
|
|
1147
|
+
]
|
|
1148
|
+
},
|
|
1149
|
+
option
|
|
1150
|
+
))
|
|
1151
|
+
] }),
|
|
1152
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between gap-2", children: [
|
|
1153
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1154
|
+
"button",
|
|
1155
|
+
{
|
|
1156
|
+
type: "button",
|
|
1157
|
+
className: `${BUTTON_CLASS} text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5`,
|
|
1158
|
+
onClick: () => {
|
|
1159
|
+
setSelected(/* @__PURE__ */ new Set());
|
|
1160
|
+
onApply(null);
|
|
1161
|
+
},
|
|
1162
|
+
children: "Clear"
|
|
1163
|
+
}
|
|
1164
|
+
),
|
|
1165
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1166
|
+
"button",
|
|
1167
|
+
{
|
|
1168
|
+
type: "button",
|
|
1169
|
+
className: `${BUTTON_CLASS} bg-brand-500 text-white hover:bg-brand-600`,
|
|
1170
|
+
onClick: () => onApply(buildSetFilter([...selected])),
|
|
1171
|
+
children: "Apply"
|
|
1172
|
+
}
|
|
1173
|
+
)
|
|
1174
|
+
] })
|
|
1175
|
+
] });
|
|
1176
|
+
}
|
|
1177
|
+
function FilterPopover({
|
|
1178
|
+
kind,
|
|
1179
|
+
value,
|
|
1180
|
+
setValues,
|
|
1181
|
+
onApply,
|
|
1182
|
+
onClose
|
|
1183
|
+
}) {
|
|
1184
|
+
const containerRef = react.useRef(null);
|
|
1185
|
+
const types = kind === "text" ? TEXT_FILTER_TYPES : kind === "number" ? NUMBER_FILTER_TYPES : DATE_FILTER_TYPES;
|
|
1186
|
+
const [type, setType] = react.useState(() => {
|
|
1187
|
+
if (value && value.filterType !== "set") return value.type;
|
|
1188
|
+
return types[0];
|
|
1189
|
+
});
|
|
1190
|
+
const [operand, setOperand] = react.useState(() => {
|
|
1191
|
+
if (!value) return "";
|
|
1192
|
+
if (value.filterType === "text") return value.filter ?? "";
|
|
1193
|
+
if (value.filterType === "number") return value.filter?.toString() ?? "";
|
|
1194
|
+
if (value.filterType === "date") return value.dateFrom ?? "";
|
|
1195
|
+
return "";
|
|
1196
|
+
});
|
|
1197
|
+
const [operandTo, setOperandTo] = react.useState(() => {
|
|
1198
|
+
if (!value) return "";
|
|
1199
|
+
if (value.filterType === "number") return value.filterTo?.toString() ?? "";
|
|
1200
|
+
if (value.filterType === "date") return value.dateTo ?? "";
|
|
1201
|
+
return "";
|
|
1202
|
+
});
|
|
1203
|
+
react.useEffect(() => {
|
|
1204
|
+
const onPointerDown = (event) => {
|
|
1205
|
+
if (!containerRef.current?.contains(event.target)) onClose();
|
|
1206
|
+
};
|
|
1207
|
+
const onKeyDown = (event) => {
|
|
1208
|
+
if (event.key === "Escape") onClose();
|
|
1209
|
+
};
|
|
1210
|
+
document.addEventListener("mousedown", onPointerDown);
|
|
1211
|
+
document.addEventListener("keydown", onKeyDown);
|
|
1212
|
+
return () => {
|
|
1213
|
+
document.removeEventListener("mousedown", onPointerDown);
|
|
1214
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
1215
|
+
};
|
|
1216
|
+
}, [onClose]);
|
|
1217
|
+
const apply = () => {
|
|
1218
|
+
if (kind === "text") {
|
|
1219
|
+
onApply(buildTextFilter(type, operand));
|
|
1220
|
+
} else if (kind === "number") {
|
|
1221
|
+
onApply(buildNumberFilter(type, operand, operandTo));
|
|
1222
|
+
} else {
|
|
1223
|
+
onApply(buildDateFilter(type, operand, operandTo));
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
const inputType = kind === "date" ? "date" : kind === "number" ? "number" : "text";
|
|
1227
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1228
|
+
"div",
|
|
1229
|
+
{
|
|
1230
|
+
ref: containerRef,
|
|
1231
|
+
className: "absolute right-0 top-full z-30 mt-1 flex w-60 flex-col gap-2 rounded-md border border-gray-200 bg-white p-2 shadow-theme-lg dark:border-gray-700 dark:bg-gray-900",
|
|
1232
|
+
onClick: (event) => event.stopPropagation(),
|
|
1233
|
+
children: kind === "set" ? /* @__PURE__ */ jsxRuntime.jsx(SetFilterBody, { value, setValues, onApply }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1234
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1235
|
+
"select",
|
|
1236
|
+
{
|
|
1237
|
+
className: FIELD_CLASS,
|
|
1238
|
+
value: type,
|
|
1239
|
+
onChange: (event) => setType(event.target.value),
|
|
1240
|
+
children: types.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option, children: FILTER_TYPE_LABELS[option] ?? option }, option))
|
|
1241
|
+
}
|
|
1242
|
+
),
|
|
1243
|
+
!isUnaryFilter(type) && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1244
|
+
"input",
|
|
1245
|
+
{
|
|
1246
|
+
type: inputType,
|
|
1247
|
+
className: FIELD_CLASS,
|
|
1248
|
+
value: operand,
|
|
1249
|
+
autoFocus: true,
|
|
1250
|
+
placeholder: "Value",
|
|
1251
|
+
onChange: (event) => setOperand(event.target.value),
|
|
1252
|
+
onKeyDown: (event) => {
|
|
1253
|
+
if (event.key === "Enter") apply();
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
),
|
|
1257
|
+
isRangeFilter(type) && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1258
|
+
"input",
|
|
1259
|
+
{
|
|
1260
|
+
type: inputType,
|
|
1261
|
+
className: FIELD_CLASS,
|
|
1262
|
+
value: operandTo,
|
|
1263
|
+
placeholder: "To",
|
|
1264
|
+
onChange: (event) => setOperandTo(event.target.value),
|
|
1265
|
+
onKeyDown: (event) => {
|
|
1266
|
+
if (event.key === "Enter") apply();
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
),
|
|
1270
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between gap-2", children: [
|
|
1271
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1272
|
+
"button",
|
|
1273
|
+
{
|
|
1274
|
+
type: "button",
|
|
1275
|
+
className: `${BUTTON_CLASS} text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5`,
|
|
1276
|
+
onClick: () => {
|
|
1277
|
+
setOperand("");
|
|
1278
|
+
setOperandTo("");
|
|
1279
|
+
onApply(null);
|
|
1280
|
+
},
|
|
1281
|
+
children: "Clear"
|
|
1282
|
+
}
|
|
1283
|
+
),
|
|
1284
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1285
|
+
"button",
|
|
1286
|
+
{
|
|
1287
|
+
type: "button",
|
|
1288
|
+
className: `${BUTTON_CLASS} bg-brand-500 text-white hover:bg-brand-600`,
|
|
1289
|
+
onClick: apply,
|
|
1290
|
+
children: "Apply"
|
|
1291
|
+
}
|
|
1292
|
+
)
|
|
1293
|
+
] })
|
|
1294
|
+
] })
|
|
1295
|
+
}
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
function SortIndicator({
|
|
1299
|
+
direction,
|
|
1300
|
+
index,
|
|
1301
|
+
showIndex
|
|
1302
|
+
}) {
|
|
1303
|
+
if (!direction) {
|
|
1304
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-0 transition-opacity group-hover:opacity-40", "aria-hidden": true, children: "\u2191" });
|
|
1305
|
+
}
|
|
1306
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center text-brand-500 dark:text-brand-400", "aria-hidden": true, children: [
|
|
1307
|
+
direction === "asc" ? "\u2191" : "\u2193",
|
|
1308
|
+
showIndex && /* @__PURE__ */ jsxRuntime.jsx("sub", { className: "ml-0.5 text-[9px]", children: index + 1 })
|
|
1309
|
+
] });
|
|
1310
|
+
}
|
|
1311
|
+
function GridHeader({
|
|
1312
|
+
layout,
|
|
1313
|
+
sortModel,
|
|
1314
|
+
filterModel,
|
|
1315
|
+
headerHeight,
|
|
1316
|
+
selectable,
|
|
1317
|
+
allSelected,
|
|
1318
|
+
someSelected,
|
|
1319
|
+
onToggleAll,
|
|
1320
|
+
onSort,
|
|
1321
|
+
onFilterChange,
|
|
1322
|
+
onResize,
|
|
1323
|
+
onMove,
|
|
1324
|
+
onPin
|
|
1325
|
+
}) {
|
|
1326
|
+
const [openFilter, setOpenFilter] = react.useState(null);
|
|
1327
|
+
const [openMenu, setOpenMenu] = react.useState(null);
|
|
1328
|
+
const [dragColId, setDragColId] = react.useState(null);
|
|
1329
|
+
const resizeRef = react.useRef(
|
|
1330
|
+
null
|
|
1331
|
+
);
|
|
1332
|
+
const startResize = react.useCallback(
|
|
1333
|
+
(event, item) => {
|
|
1334
|
+
event.preventDefault();
|
|
1335
|
+
event.stopPropagation();
|
|
1336
|
+
resizeRef.current = {
|
|
1337
|
+
colId: item.colId,
|
|
1338
|
+
startX: event.clientX,
|
|
1339
|
+
startWidth: item.width
|
|
1340
|
+
};
|
|
1341
|
+
event.target.setPointerCapture(event.pointerId);
|
|
1342
|
+
},
|
|
1343
|
+
[]
|
|
1344
|
+
);
|
|
1345
|
+
const onResizeMove = react.useCallback(
|
|
1346
|
+
(event) => {
|
|
1347
|
+
const active = resizeRef.current;
|
|
1348
|
+
if (!active) return;
|
|
1349
|
+
const delta = event.clientX - active.startX;
|
|
1350
|
+
onResize(active.colId, Math.max(40, active.startWidth + delta));
|
|
1351
|
+
},
|
|
1352
|
+
[onResize]
|
|
1353
|
+
);
|
|
1354
|
+
const endResize = react.useCallback((event) => {
|
|
1355
|
+
if (!resizeRef.current) return;
|
|
1356
|
+
resizeRef.current = null;
|
|
1357
|
+
event.target.releasePointerCapture(event.pointerId);
|
|
1358
|
+
}, []);
|
|
1359
|
+
const sortIndexOf = (colId) => sortModel.findIndex((entry) => entry.colId === colId);
|
|
1360
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1361
|
+
"div",
|
|
1362
|
+
{
|
|
1363
|
+
role: "row",
|
|
1364
|
+
className: "sticky top-0 z-10 flex border-b border-gray-300 bg-gray-100 dark:border-gray-600 dark:bg-gray-800",
|
|
1365
|
+
style: {
|
|
1366
|
+
height: headerHeight,
|
|
1367
|
+
width: layout.totalWidth + (selectable ? SELECTION_COLUMN_WIDTH : 0)
|
|
1368
|
+
},
|
|
1369
|
+
children: [
|
|
1370
|
+
selectable && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1371
|
+
SelectionCell,
|
|
1372
|
+
{
|
|
1373
|
+
isHeader: true,
|
|
1374
|
+
checked: allSelected,
|
|
1375
|
+
indeterminate: someSelected,
|
|
1376
|
+
onToggle: onToggleAll,
|
|
1377
|
+
label: "Select all rows on this page"
|
|
1378
|
+
}
|
|
1379
|
+
),
|
|
1380
|
+
layout.items.map((item, index) => {
|
|
1381
|
+
const { column, width, pinned, stickyOffset } = item;
|
|
1382
|
+
const sortIndex = sortIndexOf(column.colId);
|
|
1383
|
+
const direction = sortIndex === -1 ? void 0 : sortModel[sortIndex].sort;
|
|
1384
|
+
const activeFilter = filterModel[column.colId];
|
|
1385
|
+
const filterKind = column.filter;
|
|
1386
|
+
const style = {
|
|
1387
|
+
width,
|
|
1388
|
+
minWidth: width,
|
|
1389
|
+
maxWidth: width,
|
|
1390
|
+
...pinned === "left" ? { position: "sticky", left: stickyOffset, zIndex: 3 } : pinned === "right" ? { position: "sticky", right: stickyOffset, zIndex: 3 } : null
|
|
1391
|
+
};
|
|
1392
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1393
|
+
"div",
|
|
1394
|
+
{
|
|
1395
|
+
role: "columnheader",
|
|
1396
|
+
"aria-sort": direction === "asc" ? "ascending" : direction === "desc" ? "descending" : "none",
|
|
1397
|
+
"aria-colindex": index + 1,
|
|
1398
|
+
className: `group relative flex h-full items-center border-r border-gray-300 bg-gray-100 px-2 dark:border-gray-600 dark:bg-gray-800 ${dragColId === column.colId ? "opacity-40" : ""} ${column.headerClassName ?? ""}`,
|
|
1399
|
+
style,
|
|
1400
|
+
draggable: !column.lockPosition,
|
|
1401
|
+
onDragStart: (event) => {
|
|
1402
|
+
setDragColId(column.colId);
|
|
1403
|
+
event.dataTransfer.effectAllowed = "move";
|
|
1404
|
+
},
|
|
1405
|
+
onDragOver: (event) => {
|
|
1406
|
+
if (dragColId && dragColId !== column.colId) event.preventDefault();
|
|
1407
|
+
},
|
|
1408
|
+
onDrop: (event) => {
|
|
1409
|
+
event.preventDefault();
|
|
1410
|
+
if (dragColId && dragColId !== column.colId) onMove(dragColId, index);
|
|
1411
|
+
setDragColId(null);
|
|
1412
|
+
},
|
|
1413
|
+
onDragEnd: () => setDragColId(null),
|
|
1414
|
+
children: [
|
|
1415
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1416
|
+
"button",
|
|
1417
|
+
{
|
|
1418
|
+
type: "button",
|
|
1419
|
+
className: "flex min-w-0 flex-1 cursor-pointer items-center gap-1 text-left text-xs font-semibold uppercase tracking-wide text-gray-600 dark:text-gray-300",
|
|
1420
|
+
disabled: !column.sortable,
|
|
1421
|
+
onClick: (event) => column.sortable && onSort(column.colId, event.shiftKey),
|
|
1422
|
+
title: column.sortable ? "Sort (shift-click to add)" : void 0,
|
|
1423
|
+
children: [
|
|
1424
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: column.header }),
|
|
1425
|
+
column.sortable && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1426
|
+
SortIndicator,
|
|
1427
|
+
{
|
|
1428
|
+
direction,
|
|
1429
|
+
index: sortIndex,
|
|
1430
|
+
showIndex: sortModel.length > 1
|
|
1431
|
+
}
|
|
1432
|
+
)
|
|
1433
|
+
]
|
|
1434
|
+
}
|
|
1435
|
+
),
|
|
1436
|
+
filterKind && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1437
|
+
"button",
|
|
1438
|
+
{
|
|
1439
|
+
type: "button",
|
|
1440
|
+
"aria-label": `Filter ${column.colId}`,
|
|
1441
|
+
className: `ml-1 shrink-0 rounded px-1 text-[10px] leading-none ${activeFilter ? "text-brand-500 dark:text-brand-400" : "text-gray-400 opacity-0 group-hover:opacity-100"}`,
|
|
1442
|
+
title: activeFilter ? describeFilter(activeFilter) : "Filter",
|
|
1443
|
+
onClick: (event) => {
|
|
1444
|
+
event.stopPropagation();
|
|
1445
|
+
setOpenMenu(null);
|
|
1446
|
+
setOpenFilter(
|
|
1447
|
+
(current) => current === column.colId ? null : column.colId
|
|
1448
|
+
);
|
|
1449
|
+
},
|
|
1450
|
+
children: "\u25BC"
|
|
1451
|
+
}
|
|
1452
|
+
),
|
|
1453
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1454
|
+
"button",
|
|
1455
|
+
{
|
|
1456
|
+
type: "button",
|
|
1457
|
+
"aria-label": `Options for ${column.colId}`,
|
|
1458
|
+
className: "ml-0.5 shrink-0 rounded px-1 text-[10px] leading-none text-gray-400 opacity-0 group-hover:opacity-100",
|
|
1459
|
+
onClick: (event) => {
|
|
1460
|
+
event.stopPropagation();
|
|
1461
|
+
setOpenFilter(null);
|
|
1462
|
+
setOpenMenu((current) => current === column.colId ? null : column.colId);
|
|
1463
|
+
},
|
|
1464
|
+
children: "\u22EE"
|
|
1465
|
+
}
|
|
1466
|
+
),
|
|
1467
|
+
openFilter === column.colId && filterKind && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1468
|
+
FilterPopover,
|
|
1469
|
+
{
|
|
1470
|
+
kind: filterKind,
|
|
1471
|
+
value: activeFilter,
|
|
1472
|
+
setValues: column.filterParams?.values,
|
|
1473
|
+
onApply: (filter) => {
|
|
1474
|
+
onFilterChange(column.colId, filter);
|
|
1475
|
+
setOpenFilter(null);
|
|
1476
|
+
},
|
|
1477
|
+
onClose: () => setOpenFilter(null)
|
|
1478
|
+
}
|
|
1479
|
+
),
|
|
1480
|
+
openMenu === column.colId && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1481
|
+
"div",
|
|
1482
|
+
{
|
|
1483
|
+
className: "absolute right-0 top-full z-30 mt-1 w-36 rounded-md border border-gray-200 bg-white py-1 shadow-theme-lg dark:border-gray-700 dark:bg-gray-900",
|
|
1484
|
+
onMouseLeave: () => setOpenMenu(null),
|
|
1485
|
+
children: ["left", "right"].map((side) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1486
|
+
"button",
|
|
1487
|
+
{
|
|
1488
|
+
type: "button",
|
|
1489
|
+
className: "block w-full px-3 py-1 text-left text-xs text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-white/5",
|
|
1490
|
+
onClick: () => {
|
|
1491
|
+
onPin(column.colId, pinned === side ? void 0 : side);
|
|
1492
|
+
setOpenMenu(null);
|
|
1493
|
+
},
|
|
1494
|
+
children: pinned === side ? `Unpin ${side}` : `Pin ${side}`
|
|
1495
|
+
},
|
|
1496
|
+
side
|
|
1497
|
+
))
|
|
1498
|
+
}
|
|
1499
|
+
),
|
|
1500
|
+
column.resizable && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1501
|
+
"div",
|
|
1502
|
+
{
|
|
1503
|
+
role: "separator",
|
|
1504
|
+
"aria-orientation": "vertical",
|
|
1505
|
+
className: "absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-brand-400",
|
|
1506
|
+
onPointerDown: (event) => startResize(event, item),
|
|
1507
|
+
onPointerMove: onResizeMove,
|
|
1508
|
+
onPointerUp: endResize,
|
|
1509
|
+
onPointerCancel: endResize,
|
|
1510
|
+
onClick: (event) => event.stopPropagation()
|
|
1511
|
+
}
|
|
1512
|
+
)
|
|
1513
|
+
]
|
|
1514
|
+
},
|
|
1515
|
+
column.colId
|
|
1516
|
+
);
|
|
1517
|
+
})
|
|
1518
|
+
]
|
|
1519
|
+
}
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
function GridOverlay({ kind, message }) {
|
|
1523
|
+
if (kind === "loading") {
|
|
1524
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-none absolute inset-0 z-[5] flex items-start justify-center bg-white/60 pt-10 dark:bg-gray-900/60", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 rounded-md border border-gray-200 bg-white px-3 py-1.5 text-xs text-gray-600 shadow-theme-sm dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300", children: [
|
|
1525
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-3 w-3 animate-spin rounded-full border-2 border-brand-400 border-t-transparent" }),
|
|
1526
|
+
message ?? "Loading..."
|
|
1527
|
+
] }) });
|
|
1528
|
+
}
|
|
1529
|
+
if (kind === "error") {
|
|
1530
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 z-[5] flex items-start justify-center bg-white/80 pt-10 dark:bg-gray-900/80", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-w-md rounded-md border border-error-300 bg-error-50 px-3 py-2 text-xs text-error-700 dark:border-error-700 dark:bg-error-500/10 dark:text-error-400", children: message ?? "Something went wrong loading this grid." }) });
|
|
1531
|
+
}
|
|
1532
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 z-[5] flex items-start justify-center pt-10", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: message ?? "No records found" }) });
|
|
1533
|
+
}
|
|
1534
|
+
var BUTTON_CLASS2 = "rounded border border-gray-300 px-2 py-0.5 text-xs text-gray-700 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-white/5";
|
|
1535
|
+
function GridPagination({
|
|
1536
|
+
page,
|
|
1537
|
+
pageSize,
|
|
1538
|
+
totalRows,
|
|
1539
|
+
pageSizeOptions,
|
|
1540
|
+
isLoading,
|
|
1541
|
+
onPageChange,
|
|
1542
|
+
onPageSizeChange
|
|
1543
|
+
}) {
|
|
1544
|
+
const pageCount = Math.max(1, Math.ceil(totalRows / pageSize));
|
|
1545
|
+
const first = totalRows === 0 ? 0 : page * pageSize + 1;
|
|
1546
|
+
const last = Math.min(totalRows, (page + 1) * pageSize);
|
|
1547
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 border-t border-gray-200 bg-gray-50 px-2 py-1.5 dark:border-gray-700 dark:bg-gray-800/60", children: [
|
|
1548
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1549
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "text-xs text-gray-600 dark:text-gray-300", children: [
|
|
1550
|
+
"Rows",
|
|
1551
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1552
|
+
"select",
|
|
1553
|
+
{
|
|
1554
|
+
className: "ml-1 rounded border border-gray-300 bg-white px-1 py-0.5 text-xs dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100",
|
|
1555
|
+
value: pageSize,
|
|
1556
|
+
onChange: (event) => onPageSizeChange(Number(event.target.value)),
|
|
1557
|
+
children: pageSizeOptions.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option, children: option }, option))
|
|
1558
|
+
}
|
|
1559
|
+
)
|
|
1560
|
+
] }),
|
|
1561
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-600 dark:text-gray-300", children: isLoading ? "Loading..." : `${first}-${last} of ${totalRows}` })
|
|
1562
|
+
] }),
|
|
1563
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
1564
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1565
|
+
"button",
|
|
1566
|
+
{
|
|
1567
|
+
type: "button",
|
|
1568
|
+
className: BUTTON_CLASS2,
|
|
1569
|
+
disabled: page === 0,
|
|
1570
|
+
onClick: () => onPageChange(0),
|
|
1571
|
+
children: "\xAB First"
|
|
1572
|
+
}
|
|
1573
|
+
),
|
|
1574
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1575
|
+
"button",
|
|
1576
|
+
{
|
|
1577
|
+
type: "button",
|
|
1578
|
+
className: BUTTON_CLASS2,
|
|
1579
|
+
disabled: page === 0,
|
|
1580
|
+
onClick: () => onPageChange(page - 1),
|
|
1581
|
+
children: "\u2039 Prev"
|
|
1582
|
+
}
|
|
1583
|
+
),
|
|
1584
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "px-1 text-xs text-gray-600 dark:text-gray-300", children: [
|
|
1585
|
+
"Page ",
|
|
1586
|
+
page + 1,
|
|
1587
|
+
" of ",
|
|
1588
|
+
pageCount
|
|
1589
|
+
] }),
|
|
1590
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1591
|
+
"button",
|
|
1592
|
+
{
|
|
1593
|
+
type: "button",
|
|
1594
|
+
className: BUTTON_CLASS2,
|
|
1595
|
+
disabled: page + 1 >= pageCount,
|
|
1596
|
+
onClick: () => onPageChange(page + 1),
|
|
1597
|
+
children: "Next \u203A"
|
|
1598
|
+
}
|
|
1599
|
+
),
|
|
1600
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1601
|
+
"button",
|
|
1602
|
+
{
|
|
1603
|
+
type: "button",
|
|
1604
|
+
className: BUTTON_CLASS2,
|
|
1605
|
+
disabled: page + 1 >= pageCount,
|
|
1606
|
+
onClick: () => onPageChange(pageCount - 1),
|
|
1607
|
+
children: "Last \xBB"
|
|
1608
|
+
}
|
|
1609
|
+
)
|
|
1610
|
+
] })
|
|
1611
|
+
] });
|
|
1612
|
+
}
|
|
1613
|
+
var INPUT_CLASS = "h-7 w-full rounded border px-1.5 text-xs outline-none border-gray-300 bg-white text-gray-800 focus:border-brand-500 focus:ring-1 focus:ring-brand-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100";
|
|
1614
|
+
var ERROR_CLASS = "!border-error-500 focus:!border-error-500 focus:!ring-error-500";
|
|
1615
|
+
function className(error) {
|
|
1616
|
+
return error ? `${INPUT_CLASS} ${ERROR_CLASS}` : INPUT_CLASS;
|
|
1617
|
+
}
|
|
1618
|
+
function keyHandler(params) {
|
|
1619
|
+
return (event) => {
|
|
1620
|
+
if (event.key === "Enter") {
|
|
1621
|
+
event.preventDefault();
|
|
1622
|
+
event.stopPropagation();
|
|
1623
|
+
params.onCommit();
|
|
1624
|
+
} else if (event.key === "Escape") {
|
|
1625
|
+
event.preventDefault();
|
|
1626
|
+
event.stopPropagation();
|
|
1627
|
+
params.onCancel();
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
function TextEditor({
|
|
1632
|
+
value,
|
|
1633
|
+
onChange,
|
|
1634
|
+
onCommit,
|
|
1635
|
+
onCancel,
|
|
1636
|
+
error,
|
|
1637
|
+
autoFocus,
|
|
1638
|
+
column
|
|
1639
|
+
}) {
|
|
1640
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1641
|
+
"input",
|
|
1642
|
+
{
|
|
1643
|
+
type: "text",
|
|
1644
|
+
className: className(error),
|
|
1645
|
+
value: value == null ? "" : String(value),
|
|
1646
|
+
autoFocus,
|
|
1647
|
+
placeholder: column.editorParams?.placeholder,
|
|
1648
|
+
title: error,
|
|
1649
|
+
"aria-invalid": error ? true : void 0,
|
|
1650
|
+
onChange: (event) => onChange(event.target.value),
|
|
1651
|
+
onKeyDown: keyHandler({ onCommit, onCancel })
|
|
1652
|
+
}
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
function NumberEditor({
|
|
1656
|
+
value,
|
|
1657
|
+
onChange,
|
|
1658
|
+
onCommit,
|
|
1659
|
+
onCancel,
|
|
1660
|
+
error,
|
|
1661
|
+
autoFocus
|
|
1662
|
+
}) {
|
|
1663
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1664
|
+
"input",
|
|
1665
|
+
{
|
|
1666
|
+
type: "number",
|
|
1667
|
+
className: className(error),
|
|
1668
|
+
value: value == null ? "" : String(value),
|
|
1669
|
+
autoFocus,
|
|
1670
|
+
title: error,
|
|
1671
|
+
"aria-invalid": error ? true : void 0,
|
|
1672
|
+
onChange: (event) => onChange(event.target.value === "" ? null : Number(event.target.value)),
|
|
1673
|
+
onKeyDown: keyHandler({ onCommit, onCancel })
|
|
1674
|
+
}
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
function DateEditor({
|
|
1678
|
+
value,
|
|
1679
|
+
onChange,
|
|
1680
|
+
onCommit,
|
|
1681
|
+
onCancel,
|
|
1682
|
+
error,
|
|
1683
|
+
autoFocus
|
|
1684
|
+
}) {
|
|
1685
|
+
const asInputValue = (() => {
|
|
1686
|
+
if (value == null || value === "") return "";
|
|
1687
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
1688
|
+
return Number.isNaN(date.getTime()) ? "" : date.toISOString().slice(0, 10);
|
|
1689
|
+
})();
|
|
1690
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1691
|
+
"input",
|
|
1692
|
+
{
|
|
1693
|
+
type: "date",
|
|
1694
|
+
className: className(error),
|
|
1695
|
+
value: asInputValue,
|
|
1696
|
+
autoFocus,
|
|
1697
|
+
title: error,
|
|
1698
|
+
"aria-invalid": error ? true : void 0,
|
|
1699
|
+
onChange: (event) => onChange(event.target.value === "" ? null : event.target.value),
|
|
1700
|
+
onKeyDown: keyHandler({ onCommit, onCancel })
|
|
1701
|
+
}
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
function SelectEditor({
|
|
1705
|
+
value,
|
|
1706
|
+
onChange,
|
|
1707
|
+
onCommit,
|
|
1708
|
+
onCancel,
|
|
1709
|
+
error,
|
|
1710
|
+
autoFocus,
|
|
1711
|
+
column
|
|
1712
|
+
}) {
|
|
1713
|
+
const options = column.editorParams?.options ?? [];
|
|
1714
|
+
const selectedIndex = options.findIndex((option) => option.value === value);
|
|
1715
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1716
|
+
"select",
|
|
1717
|
+
{
|
|
1718
|
+
className: className(error),
|
|
1719
|
+
value: selectedIndex === -1 ? "" : String(selectedIndex),
|
|
1720
|
+
autoFocus,
|
|
1721
|
+
title: error,
|
|
1722
|
+
"aria-invalid": error ? true : void 0,
|
|
1723
|
+
onChange: (event) => {
|
|
1724
|
+
const index = Number(event.target.value);
|
|
1725
|
+
onChange(Number.isNaN(index) ? null : options[index]?.value ?? null);
|
|
1726
|
+
},
|
|
1727
|
+
onKeyDown: keyHandler({ onCommit, onCancel }),
|
|
1728
|
+
children: [
|
|
1729
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "--" }),
|
|
1730
|
+
options.map((option, index) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: index, children: option.label }, `${String(option.value)}-${index}`))
|
|
1731
|
+
]
|
|
1732
|
+
}
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
function CheckboxEditor({
|
|
1736
|
+
value,
|
|
1737
|
+
onChange,
|
|
1738
|
+
onCommit,
|
|
1739
|
+
onCancel,
|
|
1740
|
+
autoFocus
|
|
1741
|
+
}) {
|
|
1742
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1743
|
+
"input",
|
|
1744
|
+
{
|
|
1745
|
+
type: "checkbox",
|
|
1746
|
+
className: "h-4 w-4 accent-brand-500",
|
|
1747
|
+
checked: value === true,
|
|
1748
|
+
autoFocus,
|
|
1749
|
+
onChange: (event) => onChange(event.target.checked),
|
|
1750
|
+
onKeyDown: keyHandler({ onCommit, onCancel })
|
|
1751
|
+
}
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// src/components/editors/registry.ts
|
|
1756
|
+
var BUILTIN_EDITORS = {
|
|
1757
|
+
text: TextEditor,
|
|
1758
|
+
number: NumberEditor,
|
|
1759
|
+
date: DateEditor,
|
|
1760
|
+
select: SelectEditor,
|
|
1761
|
+
checkbox: CheckboxEditor
|
|
1762
|
+
};
|
|
1763
|
+
var ALIGN_CLASS = {
|
|
1764
|
+
left: "justify-start text-left",
|
|
1765
|
+
center: "justify-center text-center",
|
|
1766
|
+
right: "justify-end text-right"
|
|
1767
|
+
};
|
|
1768
|
+
function GridCellInner({
|
|
1769
|
+
item,
|
|
1770
|
+
row,
|
|
1771
|
+
rowIndex,
|
|
1772
|
+
context,
|
|
1773
|
+
api,
|
|
1774
|
+
isRowEditing,
|
|
1775
|
+
draft,
|
|
1776
|
+
errors,
|
|
1777
|
+
isFirstEditable,
|
|
1778
|
+
onFieldChange,
|
|
1779
|
+
onCommit,
|
|
1780
|
+
onCancel
|
|
1781
|
+
}) {
|
|
1782
|
+
const { column, width, pinned, stickyOffset } = item;
|
|
1783
|
+
const style = {
|
|
1784
|
+
width,
|
|
1785
|
+
minWidth: width,
|
|
1786
|
+
maxWidth: width,
|
|
1787
|
+
...pinned === "left" ? { position: "sticky", left: stickyOffset, zIndex: 2 } : pinned === "right" ? { position: "sticky", right: stickyOffset, zIndex: 2 } : null
|
|
1788
|
+
};
|
|
1789
|
+
const custom = typeof column.cellClassName === "function" ? column.cellClassName(row) : column.cellClassName;
|
|
1790
|
+
const base = "flex h-full items-center gap-1 border-r border-gray-200 px-2 text-xs text-gray-700 dark:border-gray-700 dark:text-gray-200 " + (pinned ? "bg-white dark:bg-gray-900 " : "");
|
|
1791
|
+
const alignment = ALIGN_CLASS[column.align ?? "left"];
|
|
1792
|
+
const editing = isRowEditing && draft != null && isEditable(column, row);
|
|
1793
|
+
if (editing) {
|
|
1794
|
+
const field = column.field ?? column.colId;
|
|
1795
|
+
const error = errors[field];
|
|
1796
|
+
const editorValue = resolveValue(draft, column);
|
|
1797
|
+
const Editor = typeof column.editor === "function" ? column.editor : BUILTIN_EDITORS[column.editor ?? "text"];
|
|
1798
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1799
|
+
"div",
|
|
1800
|
+
{
|
|
1801
|
+
role: "gridcell",
|
|
1802
|
+
className: `${base} ${alignment} ${custom ?? ""}`,
|
|
1803
|
+
style,
|
|
1804
|
+
"data-col-id": column.colId,
|
|
1805
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1806
|
+
Editor,
|
|
1807
|
+
{
|
|
1808
|
+
value: editorValue,
|
|
1809
|
+
row: draft,
|
|
1810
|
+
column,
|
|
1811
|
+
context,
|
|
1812
|
+
error,
|
|
1813
|
+
autoFocus: isFirstEditable,
|
|
1814
|
+
onChange: (value2) => onFieldChange(field, value2),
|
|
1815
|
+
onCommit,
|
|
1816
|
+
onCancel
|
|
1817
|
+
}
|
|
1818
|
+
)
|
|
1819
|
+
}
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
const value = resolveValue(row, column);
|
|
1823
|
+
const formatted = formatValue(value, row, column);
|
|
1824
|
+
const content = column.cellRenderer ? column.cellRenderer({
|
|
1825
|
+
row,
|
|
1826
|
+
rowIndex,
|
|
1827
|
+
value,
|
|
1828
|
+
formatted,
|
|
1829
|
+
column,
|
|
1830
|
+
context,
|
|
1831
|
+
api
|
|
1832
|
+
}) : formatted;
|
|
1833
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1834
|
+
"div",
|
|
1835
|
+
{
|
|
1836
|
+
role: "gridcell",
|
|
1837
|
+
className: `${base} ${alignment} ${custom ?? ""}`,
|
|
1838
|
+
style,
|
|
1839
|
+
"data-col-id": column.colId,
|
|
1840
|
+
title: column.cellRenderer ? void 0 : formatted || void 0,
|
|
1841
|
+
children: column.cellRenderer ? content : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: formatted })
|
|
1842
|
+
}
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
var GridCell = react.memo(GridCellInner);
|
|
1846
|
+
function GridRowInner({
|
|
1847
|
+
row,
|
|
1848
|
+
rowId,
|
|
1849
|
+
rowIndex,
|
|
1850
|
+
top,
|
|
1851
|
+
height,
|
|
1852
|
+
layout,
|
|
1853
|
+
context,
|
|
1854
|
+
api,
|
|
1855
|
+
selectable,
|
|
1856
|
+
isSelected,
|
|
1857
|
+
isRowEditing,
|
|
1858
|
+
draft,
|
|
1859
|
+
errors,
|
|
1860
|
+
isSaving,
|
|
1861
|
+
onToggleSelect,
|
|
1862
|
+
onFieldChange,
|
|
1863
|
+
onCommit,
|
|
1864
|
+
onCancel,
|
|
1865
|
+
onRowDoubleClick,
|
|
1866
|
+
onFilesDropped
|
|
1867
|
+
}) {
|
|
1868
|
+
const [isDropTarget, setIsDropTarget] = react.useState(false);
|
|
1869
|
+
const rowError = errors.__row__;
|
|
1870
|
+
const firstEditableColId = isRowEditing ? layout.items.find((item) => isEditable(item.column, row))?.colId : void 0;
|
|
1871
|
+
const background = isRowEditing ? "bg-brand-25 dark:bg-brand-500/10" : isSelected ? "bg-brand-50 dark:bg-brand-500/15" : rowIndex % 2 === 1 ? "bg-gray-50/60 dark:bg-white/[0.02]" : "bg-white dark:bg-gray-900";
|
|
1872
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1873
|
+
"div",
|
|
1874
|
+
{
|
|
1875
|
+
role: "row",
|
|
1876
|
+
"aria-rowindex": rowIndex + 1,
|
|
1877
|
+
"aria-selected": selectable ? isSelected : void 0,
|
|
1878
|
+
className: `absolute left-0 flex border-b border-gray-200 dark:border-gray-700 ${background} hover:bg-brand-25 dark:hover:bg-white/[0.04] ${isSaving ? "opacity-60" : ""} ${isDropTarget ? "outline-2 -outline-offset-2 outline-dashed outline-brand-500" : ""}`,
|
|
1879
|
+
style: { top, height, width: layout.totalWidth + (selectable ? 40 : 0) },
|
|
1880
|
+
onDoubleClick: () => onRowDoubleClick(rowId, row),
|
|
1881
|
+
title: rowError,
|
|
1882
|
+
onDragOver: onFilesDropped ? (event) => {
|
|
1883
|
+
if (!event.dataTransfer.types.includes("Files")) return;
|
|
1884
|
+
event.preventDefault();
|
|
1885
|
+
event.dataTransfer.dropEffect = "copy";
|
|
1886
|
+
if (!isDropTarget) setIsDropTarget(true);
|
|
1887
|
+
} : void 0,
|
|
1888
|
+
onDragLeave: onFilesDropped ? () => setIsDropTarget(false) : void 0,
|
|
1889
|
+
onDrop: onFilesDropped ? (event) => {
|
|
1890
|
+
if (!event.dataTransfer.types.includes("Files")) return;
|
|
1891
|
+
event.preventDefault();
|
|
1892
|
+
setIsDropTarget(false);
|
|
1893
|
+
const files = Array.from(event.dataTransfer.files);
|
|
1894
|
+
if (files.length > 0) onFilesDropped(row, files);
|
|
1895
|
+
} : void 0,
|
|
1896
|
+
children: [
|
|
1897
|
+
selectable && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1898
|
+
SelectionCell,
|
|
1899
|
+
{
|
|
1900
|
+
checked: isSelected,
|
|
1901
|
+
onToggle: (shiftKey) => onToggleSelect(rowId, rowIndex, shiftKey)
|
|
1902
|
+
}
|
|
1903
|
+
),
|
|
1904
|
+
layout.items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1905
|
+
GridCell,
|
|
1906
|
+
{
|
|
1907
|
+
item,
|
|
1908
|
+
row,
|
|
1909
|
+
rowIndex,
|
|
1910
|
+
context,
|
|
1911
|
+
api,
|
|
1912
|
+
isRowEditing,
|
|
1913
|
+
draft,
|
|
1914
|
+
errors,
|
|
1915
|
+
isFirstEditable: item.colId === firstEditableColId,
|
|
1916
|
+
onFieldChange,
|
|
1917
|
+
onCommit,
|
|
1918
|
+
onCancel
|
|
1919
|
+
},
|
|
1920
|
+
item.colId
|
|
1921
|
+
))
|
|
1922
|
+
]
|
|
1923
|
+
}
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
var GridRow = react.memo(GridRowInner);
|
|
1927
|
+
var DEFAULT_PAGE_SIZES = [10, 20, 50, 100];
|
|
1928
|
+
function DataGrid({
|
|
1929
|
+
columns,
|
|
1930
|
+
dataSource,
|
|
1931
|
+
getRowId,
|
|
1932
|
+
context,
|
|
1933
|
+
storageKey,
|
|
1934
|
+
rowHeight = 36,
|
|
1935
|
+
headerHeight = 36,
|
|
1936
|
+
floatingFilter = true,
|
|
1937
|
+
selectable = true,
|
|
1938
|
+
defaultPageSize = 20,
|
|
1939
|
+
pageSizeOptions = DEFAULT_PAGE_SIZES,
|
|
1940
|
+
onRowCommit,
|
|
1941
|
+
onSelectionChanged,
|
|
1942
|
+
onRowFilesDropped,
|
|
1943
|
+
onError,
|
|
1944
|
+
toolbar,
|
|
1945
|
+
emptyMessage,
|
|
1946
|
+
exportFileName = "export",
|
|
1947
|
+
className: className2 = "",
|
|
1948
|
+
height = "70vh",
|
|
1949
|
+
apiRef
|
|
1950
|
+
}) {
|
|
1951
|
+
const persisted = useGridState(storageKey);
|
|
1952
|
+
const [sortModel, setSortModel] = react.useState(
|
|
1953
|
+
() => persisted.initial?.sort ?? []
|
|
1954
|
+
);
|
|
1955
|
+
const [filterModel, setFilterModel] = react.useState(
|
|
1956
|
+
() => persisted.initial?.filters ?? {}
|
|
1957
|
+
);
|
|
1958
|
+
const [pageSize, setPageSize] = react.useState(
|
|
1959
|
+
() => persisted.initial?.pagination.pageSize ?? defaultPageSize
|
|
1960
|
+
);
|
|
1961
|
+
const [page, setPage] = react.useState(0);
|
|
1962
|
+
const [showColumnsPanel, setShowColumnsPanel] = react.useState(false);
|
|
1963
|
+
const viewportRef = react.useRef(null);
|
|
1964
|
+
const [viewportWidth, setViewportWidth] = react.useState(0);
|
|
1965
|
+
const floatingFilterHeight = floatingFilter ? 28 : 0;
|
|
1966
|
+
react.useLayoutEffect(() => {
|
|
1967
|
+
const element = viewportRef.current;
|
|
1968
|
+
if (!element) return;
|
|
1969
|
+
const observer = new ResizeObserver((entries) => {
|
|
1970
|
+
const rect = entries[0]?.contentRect;
|
|
1971
|
+
if (!rect) return;
|
|
1972
|
+
setViewportWidth(rect.width);
|
|
1973
|
+
setViewportHeightRef.current(
|
|
1974
|
+
Math.max(0, rect.height - headerHeight - floatingFilterHeight)
|
|
1975
|
+
);
|
|
1976
|
+
});
|
|
1977
|
+
observer.observe(element);
|
|
1978
|
+
return () => observer.disconnect();
|
|
1979
|
+
}, [headerHeight, floatingFilterHeight]);
|
|
1980
|
+
const columnState = useColumnState(
|
|
1981
|
+
columns,
|
|
1982
|
+
persisted.initial?.columns,
|
|
1983
|
+
persisted.saveColumns,
|
|
1984
|
+
viewportWidth - (selectable ? 40 : 0)
|
|
1985
|
+
);
|
|
1986
|
+
const { rows, totalRows, isLoading, error, refresh, patchRows } = useServerDataSource(
|
|
1987
|
+
{ dataSource, pageSize, page, sortModel, filterModel, onError }
|
|
1988
|
+
);
|
|
1989
|
+
react.useEffect(() => {
|
|
1990
|
+
if (isLoading || totalRows === 0) return;
|
|
1991
|
+
const pageCount = Math.max(1, Math.ceil(totalRows / pageSize));
|
|
1992
|
+
if (page > pageCount - 1) setPage(pageCount - 1);
|
|
1993
|
+
}, [totalRows, pageSize, page, isLoading]);
|
|
1994
|
+
const virtual = useVirtualRows({ rowCount: rows.length, rowHeight });
|
|
1995
|
+
const setViewportHeightRef = react.useRef(virtual.setViewportHeight);
|
|
1996
|
+
setViewportHeightRef.current = virtual.setViewportHeight;
|
|
1997
|
+
const selection = useSelectionModel(rows, getRowId, onSelectionChanged);
|
|
1998
|
+
const noopCommit = react.useCallback(() => ({ ok: true }), []);
|
|
1999
|
+
const editing = useEditModel(onRowCommit ?? noopCommit);
|
|
2000
|
+
const applySort = react.useCallback(
|
|
2001
|
+
(next) => {
|
|
2002
|
+
setSortModel(next);
|
|
2003
|
+
persisted.saveSort(next);
|
|
2004
|
+
setPage(0);
|
|
2005
|
+
},
|
|
2006
|
+
[persisted]
|
|
2007
|
+
);
|
|
2008
|
+
const applyFilters = react.useCallback(
|
|
2009
|
+
(next) => {
|
|
2010
|
+
setFilterModel(next);
|
|
2011
|
+
persisted.saveFilters(next);
|
|
2012
|
+
setPage(0);
|
|
2013
|
+
},
|
|
2014
|
+
[persisted]
|
|
2015
|
+
);
|
|
2016
|
+
const onSort = react.useCallback(
|
|
2017
|
+
(colId, additive) => {
|
|
2018
|
+
const existing = sortModel.find((entry) => entry.colId === colId);
|
|
2019
|
+
const others = sortModel.filter((entry) => entry.colId !== colId);
|
|
2020
|
+
const next = !existing ? [...additive ? sortModel : [], { colId, sort: "asc" }] : existing.sort === "asc" ? [...additive ? others : [], { colId, sort: "desc" }] : additive ? others : [];
|
|
2021
|
+
applySort(next);
|
|
2022
|
+
},
|
|
2023
|
+
[sortModel, applySort]
|
|
2024
|
+
);
|
|
2025
|
+
const onFilterChange = react.useCallback(
|
|
2026
|
+
(colId, filter) => {
|
|
2027
|
+
applyFilters(withFilter(filterModel, colId, filter));
|
|
2028
|
+
},
|
|
2029
|
+
[filterModel, applyFilters]
|
|
2030
|
+
);
|
|
2031
|
+
const onPageSizeChange = react.useCallback(
|
|
2032
|
+
(next) => {
|
|
2033
|
+
setPageSize(next);
|
|
2034
|
+
persisted.savePageSize(next);
|
|
2035
|
+
setPage(0);
|
|
2036
|
+
},
|
|
2037
|
+
[persisted]
|
|
2038
|
+
);
|
|
2039
|
+
const latest = react.useRef({
|
|
2040
|
+
rows,
|
|
2041
|
+
selection,
|
|
2042
|
+
columnState,
|
|
2043
|
+
filterModel,
|
|
2044
|
+
sortModel,
|
|
2045
|
+
editing,
|
|
2046
|
+
getRowId
|
|
2047
|
+
});
|
|
2048
|
+
latest.current = {
|
|
2049
|
+
rows,
|
|
2050
|
+
selection,
|
|
2051
|
+
columnState,
|
|
2052
|
+
filterModel,
|
|
2053
|
+
sortModel,
|
|
2054
|
+
editing,
|
|
2055
|
+
getRowId
|
|
2056
|
+
};
|
|
2057
|
+
const api = react.useMemo(
|
|
2058
|
+
() => ({
|
|
2059
|
+
refresh: (options) => refresh(options),
|
|
2060
|
+
getDisplayedRows: () => latest.current.rows,
|
|
2061
|
+
getSelectedRows: () => latest.current.selection.getSelectedRows(),
|
|
2062
|
+
getSelectedIds: () => [...latest.current.selection.selectedIds],
|
|
2063
|
+
clearSelection: () => latest.current.selection.clear(),
|
|
2064
|
+
selectAll: () => latest.current.selection.selectAllVisible(),
|
|
2065
|
+
exportCsv: (options) => {
|
|
2066
|
+
const source = options?.onlySelected ? latest.current.selection.getSelectedRows() : latest.current.rows;
|
|
2067
|
+
const csv = toCsv(
|
|
2068
|
+
source,
|
|
2069
|
+
latest.current.columnState.visibleColumns,
|
|
2070
|
+
options?.separator ?? ","
|
|
2071
|
+
);
|
|
2072
|
+
downloadCsv(csv, options?.fileName ?? exportFileName);
|
|
2073
|
+
},
|
|
2074
|
+
copySelectionToClipboard: async () => {
|
|
2075
|
+
const selected = latest.current.selection.getSelectedRows();
|
|
2076
|
+
const source = selected.length > 0 ? selected : latest.current.rows;
|
|
2077
|
+
await copyToClipboard(
|
|
2078
|
+
toTsv(source, latest.current.columnState.visibleColumns)
|
|
2079
|
+
);
|
|
2080
|
+
},
|
|
2081
|
+
getFilterModel: () => latest.current.filterModel,
|
|
2082
|
+
setFilterModel: applyFilters,
|
|
2083
|
+
getSortModel: () => latest.current.sortModel,
|
|
2084
|
+
setSortModel: applySort,
|
|
2085
|
+
resetColumns: () => {
|
|
2086
|
+
latest.current.columnState.reset();
|
|
2087
|
+
persisted.clear();
|
|
2088
|
+
},
|
|
2089
|
+
updateRows: (updated) => patchRows(updated, latest.current.getRowId),
|
|
2090
|
+
startEditing: (rowId) => {
|
|
2091
|
+
const row = latest.current.rows.find(
|
|
2092
|
+
(candidate) => latest.current.getRowId(candidate) === rowId
|
|
2093
|
+
);
|
|
2094
|
+
if (row) latest.current.editing.start(rowId, row);
|
|
2095
|
+
},
|
|
2096
|
+
stopEditing: () => latest.current.editing.cancel()
|
|
2097
|
+
}),
|
|
2098
|
+
[refresh, applyFilters, applySort, patchRows, persisted, exportFileName]
|
|
2099
|
+
);
|
|
2100
|
+
react.useImperativeHandle(apiRef, () => api, [api]);
|
|
2101
|
+
const onRowDoubleClick = react.useCallback(
|
|
2102
|
+
(rowId, row) => {
|
|
2103
|
+
if (onRowCommit) editing.start(rowId, row);
|
|
2104
|
+
},
|
|
2105
|
+
[onRowCommit, editing]
|
|
2106
|
+
);
|
|
2107
|
+
const editingRowId = editing.edit?.rowId ?? null;
|
|
2108
|
+
const cancelEditRef = react.useRef(editing.cancel);
|
|
2109
|
+
cancelEditRef.current = editing.cancel;
|
|
2110
|
+
react.useEffect(() => {
|
|
2111
|
+
if (editingRowId == null) return;
|
|
2112
|
+
const onPointerDown = (event) => {
|
|
2113
|
+
const target = event.target;
|
|
2114
|
+
if (!target.closest('[data-hxg-editing-row="true"]')) cancelEditRef.current();
|
|
2115
|
+
};
|
|
2116
|
+
document.addEventListener("mousedown", onPointerDown);
|
|
2117
|
+
return () => document.removeEventListener("mousedown", onPointerDown);
|
|
2118
|
+
}, [editingRowId]);
|
|
2119
|
+
const emptyContext = react.useMemo(() => ({}), []);
|
|
2120
|
+
const cellContext = context ?? emptyContext;
|
|
2121
|
+
const { startIndex, endIndex } = virtual.window;
|
|
2122
|
+
const visibleRows = rows.slice(startIndex, endIndex);
|
|
2123
|
+
const showEmpty = !isLoading && !error && rows.length === 0;
|
|
2124
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2125
|
+
"div",
|
|
2126
|
+
{
|
|
2127
|
+
className: `flex flex-col overflow-hidden rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900 ${className2}`,
|
|
2128
|
+
children: [
|
|
2129
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2 border-b border-gray-200 bg-gray-50 px-2 py-1.5 dark:border-gray-700 dark:bg-gray-800/60", children: [
|
|
2130
|
+
toolbar?.(api),
|
|
2131
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-auto flex items-center gap-1", children: [
|
|
2132
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2133
|
+
"button",
|
|
2134
|
+
{
|
|
2135
|
+
type: "button",
|
|
2136
|
+
className: "rounded border border-gray-300 px-2 py-0.5 text-xs text-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-white/5",
|
|
2137
|
+
onClick: () => setShowColumnsPanel((current) => !current),
|
|
2138
|
+
children: "Columns"
|
|
2139
|
+
}
|
|
2140
|
+
),
|
|
2141
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2142
|
+
"button",
|
|
2143
|
+
{
|
|
2144
|
+
type: "button",
|
|
2145
|
+
className: "rounded border border-gray-300 px-2 py-0.5 text-xs text-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-white/5",
|
|
2146
|
+
onClick: () => api.exportCsv(),
|
|
2147
|
+
children: "Export CSV"
|
|
2148
|
+
}
|
|
2149
|
+
)
|
|
2150
|
+
] })
|
|
2151
|
+
] }),
|
|
2152
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1", children: [
|
|
2153
|
+
showColumnsPanel && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2154
|
+
ColumnsPanel,
|
|
2155
|
+
{
|
|
2156
|
+
columns: columnState.allColumns,
|
|
2157
|
+
isHidden: columnState.isHidden,
|
|
2158
|
+
onToggle: columnState.setHidden,
|
|
2159
|
+
onMove: columnState.moveColumn,
|
|
2160
|
+
onPin: columnState.setPinned,
|
|
2161
|
+
onReset: () => {
|
|
2162
|
+
columnState.reset();
|
|
2163
|
+
persisted.clear();
|
|
2164
|
+
},
|
|
2165
|
+
onClose: () => setShowColumnsPanel(false)
|
|
2166
|
+
}
|
|
2167
|
+
),
|
|
2168
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2169
|
+
"div",
|
|
2170
|
+
{
|
|
2171
|
+
ref: viewportRef,
|
|
2172
|
+
role: "grid",
|
|
2173
|
+
"aria-rowcount": totalRows,
|
|
2174
|
+
"aria-colcount": columnState.layout.items.length,
|
|
2175
|
+
"aria-busy": isLoading,
|
|
2176
|
+
className: "relative min-w-0 flex-1 overflow-auto",
|
|
2177
|
+
style: { height },
|
|
2178
|
+
onScroll: virtual.onScroll,
|
|
2179
|
+
children: [
|
|
2180
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2181
|
+
GridHeader,
|
|
2182
|
+
{
|
|
2183
|
+
layout: columnState.layout,
|
|
2184
|
+
sortModel,
|
|
2185
|
+
filterModel,
|
|
2186
|
+
headerHeight,
|
|
2187
|
+
selectable,
|
|
2188
|
+
allSelected: selection.allVisibleSelected,
|
|
2189
|
+
someSelected: selection.someVisibleSelected,
|
|
2190
|
+
onToggleAll: selection.toggleAllVisible,
|
|
2191
|
+
onSort,
|
|
2192
|
+
onFilterChange,
|
|
2193
|
+
onResize: columnState.setWidth,
|
|
2194
|
+
onMove: columnState.moveColumn,
|
|
2195
|
+
onPin: columnState.setPinned
|
|
2196
|
+
}
|
|
2197
|
+
),
|
|
2198
|
+
floatingFilter && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { top: headerHeight, position: "sticky", zIndex: 9 }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2199
|
+
FloatingFilterRow,
|
|
2200
|
+
{
|
|
2201
|
+
layout: columnState.layout,
|
|
2202
|
+
filterModel,
|
|
2203
|
+
height: floatingFilterHeight,
|
|
2204
|
+
selectable,
|
|
2205
|
+
onFilterChange
|
|
2206
|
+
}
|
|
2207
|
+
) }),
|
|
2208
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2209
|
+
"div",
|
|
2210
|
+
{
|
|
2211
|
+
className: "relative",
|
|
2212
|
+
style: {
|
|
2213
|
+
height: virtual.totalHeight,
|
|
2214
|
+
width: columnState.layout.totalWidth + (selectable ? 40 : 0)
|
|
2215
|
+
},
|
|
2216
|
+
children: visibleRows.map((row, offset) => {
|
|
2217
|
+
const rowIndex = startIndex + offset;
|
|
2218
|
+
const rowId = getRowId(row);
|
|
2219
|
+
const isRowEditing = editing.edit?.rowId === rowId;
|
|
2220
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2221
|
+
"div",
|
|
2222
|
+
{
|
|
2223
|
+
"data-hxg-editing-row": isRowEditing ? "true" : void 0,
|
|
2224
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2225
|
+
GridRow,
|
|
2226
|
+
{
|
|
2227
|
+
row,
|
|
2228
|
+
rowId,
|
|
2229
|
+
rowIndex,
|
|
2230
|
+
top: rowIndex * rowHeight,
|
|
2231
|
+
height: rowHeight,
|
|
2232
|
+
layout: columnState.layout,
|
|
2233
|
+
context: cellContext,
|
|
2234
|
+
api,
|
|
2235
|
+
selectable,
|
|
2236
|
+
isSelected: selection.isSelected(rowId),
|
|
2237
|
+
isRowEditing,
|
|
2238
|
+
draft: isRowEditing ? editing.edit?.draft ?? null : null,
|
|
2239
|
+
errors: isRowEditing ? editing.edit?.errors ?? {} : {},
|
|
2240
|
+
isSaving: isRowEditing && editing.edit?.isSaving === true,
|
|
2241
|
+
onToggleSelect: selection.toggleRow,
|
|
2242
|
+
onFieldChange: editing.setField,
|
|
2243
|
+
onCommit: editing.commit,
|
|
2244
|
+
onCancel: editing.cancel,
|
|
2245
|
+
onRowDoubleClick,
|
|
2246
|
+
onFilesDropped: onRowFilesDropped
|
|
2247
|
+
}
|
|
2248
|
+
)
|
|
2249
|
+
},
|
|
2250
|
+
rowId
|
|
2251
|
+
);
|
|
2252
|
+
})
|
|
2253
|
+
}
|
|
2254
|
+
),
|
|
2255
|
+
isLoading && /* @__PURE__ */ jsxRuntime.jsx(GridOverlay, { kind: "loading" }),
|
|
2256
|
+
error != null && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2257
|
+
GridOverlay,
|
|
2258
|
+
{
|
|
2259
|
+
kind: "error",
|
|
2260
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2261
|
+
}
|
|
2262
|
+
),
|
|
2263
|
+
showEmpty && /* @__PURE__ */ jsxRuntime.jsx(GridOverlay, { kind: "empty", message: emptyMessage })
|
|
2264
|
+
]
|
|
2265
|
+
}
|
|
2266
|
+
)
|
|
2267
|
+
] }),
|
|
2268
|
+
editing.edit && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-2 border-t border-brand-200 bg-brand-25 px-2 py-1.5 dark:border-brand-500/30 dark:bg-brand-500/10", children: [
|
|
2269
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "mr-auto text-xs text-gray-600 dark:text-gray-300", children: editing.edit.errors.__row__ ?? "Editing row \u2014 Enter to save, Escape to cancel." }),
|
|
2270
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2271
|
+
"button",
|
|
2272
|
+
{
|
|
2273
|
+
type: "button",
|
|
2274
|
+
className: "rounded border border-gray-300 px-2 py-0.5 text-xs text-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-200",
|
|
2275
|
+
onClick: editing.cancel,
|
|
2276
|
+
children: "Cancel"
|
|
2277
|
+
}
|
|
2278
|
+
),
|
|
2279
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2280
|
+
"button",
|
|
2281
|
+
{
|
|
2282
|
+
type: "button",
|
|
2283
|
+
className: "rounded bg-brand-500 px-2.5 py-0.5 text-xs font-medium text-white hover:bg-brand-600 disabled:opacity-50",
|
|
2284
|
+
disabled: editing.edit.isSaving,
|
|
2285
|
+
onClick: editing.commit,
|
|
2286
|
+
children: editing.edit.isSaving ? "Saving..." : "Save"
|
|
2287
|
+
}
|
|
2288
|
+
)
|
|
2289
|
+
] }),
|
|
2290
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2291
|
+
GridPagination,
|
|
2292
|
+
{
|
|
2293
|
+
page,
|
|
2294
|
+
pageSize,
|
|
2295
|
+
totalRows,
|
|
2296
|
+
pageSizeOptions,
|
|
2297
|
+
isLoading,
|
|
2298
|
+
onPageChange: setPage,
|
|
2299
|
+
onPageSizeChange
|
|
2300
|
+
}
|
|
2301
|
+
)
|
|
2302
|
+
]
|
|
2303
|
+
}
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
exports.BUILTIN_EDITORS = BUILTIN_EDITORS;
|
|
2308
|
+
exports.CheckboxEditor = CheckboxEditor;
|
|
2309
|
+
exports.ColumnsPanel = ColumnsPanel;
|
|
2310
|
+
exports.DATE_FILTER_TYPES = DATE_FILTER_TYPES;
|
|
2311
|
+
exports.DataGrid = DataGrid;
|
|
2312
|
+
exports.DateEditor = DateEditor;
|
|
2313
|
+
exports.FILTER_TYPE_LABELS = FILTER_TYPE_LABELS;
|
|
2314
|
+
exports.FilterPopover = FilterPopover;
|
|
2315
|
+
exports.GRID_STATE_VERSION = GRID_STATE_VERSION;
|
|
2316
|
+
exports.GridOverlay = GridOverlay;
|
|
2317
|
+
exports.GridPagination = GridPagination;
|
|
2318
|
+
exports.NUMBER_FILTER_TYPES = NUMBER_FILTER_TYPES;
|
|
2319
|
+
exports.NumberEditor = NumberEditor;
|
|
2320
|
+
exports.SelectEditor = SelectEditor;
|
|
2321
|
+
exports.TEXT_FILTER_TYPES = TEXT_FILTER_TYPES;
|
|
2322
|
+
exports.TextEditor = TextEditor;
|
|
2323
|
+
exports.buildDateFilter = buildDateFilter;
|
|
2324
|
+
exports.buildNumberFilter = buildNumberFilter;
|
|
2325
|
+
exports.buildSetFilter = buildSetFilter;
|
|
2326
|
+
exports.buildTextFilter = buildTextFilter;
|
|
2327
|
+
exports.columnId = columnId;
|
|
2328
|
+
exports.copyToClipboard = copyToClipboard;
|
|
2329
|
+
exports.defaultFilterType = defaultFilterType;
|
|
2330
|
+
exports.describeFilter = describeFilter;
|
|
2331
|
+
exports.downloadCsv = downloadCsv;
|
|
2332
|
+
exports.exportValue = exportValue;
|
|
2333
|
+
exports.formatValue = formatValue;
|
|
2334
|
+
exports.headerText = headerText;
|
|
2335
|
+
exports.isEditable = isEditable;
|
|
2336
|
+
exports.isRangeFilter = isRangeFilter;
|
|
2337
|
+
exports.isUnaryFilter = isUnaryFilter;
|
|
2338
|
+
exports.resolveColumn = resolveColumn;
|
|
2339
|
+
exports.resolveValue = resolveValue;
|
|
2340
|
+
exports.toCsv = toCsv;
|
|
2341
|
+
exports.toDelimited = toDelimited;
|
|
2342
|
+
exports.toTsv = toTsv;
|
|
2343
|
+
exports.useColumnState = useColumnState;
|
|
2344
|
+
exports.useEditModel = useEditModel;
|
|
2345
|
+
exports.useGridState = useGridState;
|
|
2346
|
+
exports.useSelectionModel = useSelectionModel;
|
|
2347
|
+
exports.useServerDataSource = useServerDataSource;
|
|
2348
|
+
exports.useVirtualRows = useVirtualRows;
|
|
2349
|
+
exports.withFilter = withFilter;
|
|
2350
|
+
//# sourceMappingURL=index.cjs.map
|
|
2351
|
+
//# sourceMappingURL=index.cjs.map
|