@wordrhyme/auto-crud 0.5.0 → 1.0.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/dist/index.cjs +1507 -688
- package/dist/index.d.cts +312 -100
- package/dist/index.d.ts +312 -100
- package/dist/index.js +1499 -691
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { forwardRef, useCallback, useImperativeHandle, useMemo, useReducer, useRef
|
|
3
|
-
import { ArrowDownUp, BadgeCheck, CalendarIcon, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, ChevronsLeft, ChevronsRight, ChevronsUpDown, CommandIcon, Download, Ellipsis, EyeOff, FileSpreadsheetIcon, GripVertical, ListFilter, ListFilterIcon, PlusCircle, Settings2, Text, Trash2, X, XCircle } from "lucide-react";
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useReducer, useRef } from "react";
|
|
3
|
+
import { ArrowDownUp, BadgeCheck, CalendarIcon, Check, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, ChevronsLeft, ChevronsRight, ChevronsUpDown, CommandIcon, Download, Ellipsis, EyeOff, FileDown, FileSpreadsheetIcon, GripVertical, ListFilter, ListFilterIcon, Loader2, PlusCircle, Settings2, Text, Trash2, Upload, X, XCircle } from "lucide-react";
|
|
4
4
|
import { flexRender, getCoreRowModel, getFacetedMinMaxValues, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
|
|
5
5
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, Calendar, Checkbox, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, Input, Label, Popover, PopoverContent, PopoverTrigger, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Separator, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, Slider, Switch } from "@pixpilot/shadcn";
|
|
6
6
|
import { clsx } from "clsx";
|
|
7
7
|
import { twMerge } from "tailwind-merge";
|
|
8
8
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { customAlphabet } from "nanoid";
|
|
9
10
|
import { DndContext, DragOverlay, KeyboardSensor, MouseSensor, TouchSensor, closestCenter, closestCorners, defaultDropAnimationSideEffects, useSensor, useSensors } from "@dnd-kit/core";
|
|
10
11
|
import { restrictToHorizontalAxis, restrictToParentElement, restrictToVerticalAxis } from "@dnd-kit/modifiers";
|
|
11
12
|
import { SortableContext, arrayMove, horizontalListSortingStrategy, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
12
13
|
import { CSS } from "@dnd-kit/utilities";
|
|
13
14
|
import { Slot } from "@radix-ui/react-slot";
|
|
14
15
|
import * as ReactDOM from "react-dom";
|
|
15
|
-
import { customAlphabet } from "nanoid";
|
|
16
16
|
import { useDirection } from "@radix-ui/react-direction";
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
import { createForm } from "@formily/core";
|
|
@@ -432,6 +432,104 @@ function DataTable({ table, actionBar, children, className,...props }) {
|
|
|
432
432
|
});
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/lib/id.ts
|
|
437
|
+
const prefixes = {};
|
|
438
|
+
function generateId(prefixOrOptions, inputOptions = {}) {
|
|
439
|
+
const finalOptions = typeof prefixOrOptions === "object" ? prefixOrOptions : inputOptions;
|
|
440
|
+
const prefix = typeof prefixOrOptions === "object" ? void 0 : prefixOrOptions;
|
|
441
|
+
const { length = 12, separator = "_" } = finalOptions;
|
|
442
|
+
const id = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", length)();
|
|
443
|
+
return prefix && prefix in prefixes ? `${prefixes[prefix]}${separator}${id}` : id;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
//#endregion
|
|
447
|
+
//#region src/lib/readable-filters.ts
|
|
448
|
+
const OP_SUFFIX = "__op";
|
|
449
|
+
const INDEX_SEPARATOR = "__";
|
|
450
|
+
const RANGE_VARIANTS = new Set(["range", "dateRange"]);
|
|
451
|
+
function getColumnId(column) {
|
|
452
|
+
if ("id" in column && column.id) return String(column.id);
|
|
453
|
+
if ("accessorKey" in column && column.accessorKey) return String(column.accessorKey);
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
function getColumnVariant(column) {
|
|
457
|
+
if ("columnDef" in column) return column.columnDef.meta?.variant;
|
|
458
|
+
return column.meta?.variant;
|
|
459
|
+
}
|
|
460
|
+
function splitKey(key) {
|
|
461
|
+
const match = key.match(/^(.*?)(?:__(\d+))?$/);
|
|
462
|
+
if (!match) return { base: key };
|
|
463
|
+
const index = match[2] ? Number(match[2]) : void 0;
|
|
464
|
+
return {
|
|
465
|
+
base: match[1] ?? key,
|
|
466
|
+
index
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function isFilterKey(key, columnIds) {
|
|
470
|
+
const { base } = splitKey(key.endsWith(OP_SUFFIX) ? key.slice(0, -4) : key);
|
|
471
|
+
return columnIds.has(base);
|
|
472
|
+
}
|
|
473
|
+
function parseValueByVariant(value, variant) {
|
|
474
|
+
if (variant === "multiSelect") return value ? value.split(",").filter(Boolean) : [];
|
|
475
|
+
if (RANGE_VARIANTS.has(variant)) {
|
|
476
|
+
const parts = value.split(",");
|
|
477
|
+
return [parts[0] ?? "", parts[1] ?? ""];
|
|
478
|
+
}
|
|
479
|
+
if (variant === "select") return value.split(",").filter(Boolean)[0] ?? "";
|
|
480
|
+
return value;
|
|
481
|
+
}
|
|
482
|
+
function serializeValue(value, variant) {
|
|
483
|
+
if (Array.isArray(value)) {
|
|
484
|
+
if (RANGE_VARIANTS.has(variant)) return [value[0] ?? "", value[1] ?? ""].join(",");
|
|
485
|
+
return value.join(",");
|
|
486
|
+
}
|
|
487
|
+
return value;
|
|
488
|
+
}
|
|
489
|
+
function parseReadableFilters(params, columns) {
|
|
490
|
+
const columnsById = /* @__PURE__ */ new Map();
|
|
491
|
+
for (const column of columns) {
|
|
492
|
+
const id = getColumnId(column);
|
|
493
|
+
if (id) columnsById.set(id, column);
|
|
494
|
+
}
|
|
495
|
+
const filters = [];
|
|
496
|
+
for (const [key, rawValue] of params.entries()) {
|
|
497
|
+
if (key.endsWith(OP_SUFFIX)) continue;
|
|
498
|
+
const { base } = splitKey(key);
|
|
499
|
+
const column = columnsById.get(base);
|
|
500
|
+
if (!column) continue;
|
|
501
|
+
const variant = getColumnVariant(column) ?? "text";
|
|
502
|
+
const opKey = `${key}${OP_SUFFIX}`;
|
|
503
|
+
const operator = params.get(opKey) ?? getDefaultFilterOperator(variant);
|
|
504
|
+
const parsedValue = operator === "isEmpty" || operator === "isNotEmpty" ? "" : parseValueByVariant(rawValue, variant);
|
|
505
|
+
filters.push({
|
|
506
|
+
id: base,
|
|
507
|
+
value: parsedValue,
|
|
508
|
+
variant,
|
|
509
|
+
operator,
|
|
510
|
+
filterId: key || generateId({ length: 8 })
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
return filters;
|
|
514
|
+
}
|
|
515
|
+
function applyReadableFilters(params, columns, filters) {
|
|
516
|
+
const columnIds = new Set(columns.map((column) => getColumnId(column)).filter(Boolean));
|
|
517
|
+
for (const key of Array.from(params.keys())) if (isFilterKey(key, columnIds)) params.delete(key);
|
|
518
|
+
const counters = /* @__PURE__ */ new Map();
|
|
519
|
+
for (const filter of filters) {
|
|
520
|
+
const columnId = String(filter.id);
|
|
521
|
+
const count = (counters.get(columnId) ?? 0) + 1;
|
|
522
|
+
counters.set(columnId, count);
|
|
523
|
+
const key = count === 1 ? columnId : `${columnId}${INDEX_SEPARATOR}${count}`;
|
|
524
|
+
const variant = filter.variant ?? "text";
|
|
525
|
+
const value = serializeValue(filter.value, variant);
|
|
526
|
+
params.set(key, filter.operator === "isEmpty" || filter.operator === "isNotEmpty" ? "" : value);
|
|
527
|
+
const defaultOperator = getDefaultFilterOperator(variant);
|
|
528
|
+
if (filter.operator !== defaultOperator) params.set(`${key}${OP_SUFFIX}`, filter.operator);
|
|
529
|
+
}
|
|
530
|
+
return params;
|
|
531
|
+
}
|
|
532
|
+
|
|
435
533
|
//#endregion
|
|
436
534
|
//#region src/hooks/use-url-state.ts
|
|
437
535
|
const parseAsInteger = {
|
|
@@ -682,6 +780,54 @@ function useQueryStates(parsers, options = {}) {
|
|
|
682
780
|
}, [setValues])];
|
|
683
781
|
}
|
|
684
782
|
|
|
783
|
+
//#endregion
|
|
784
|
+
//#region src/hooks/use-readable-filters.ts
|
|
785
|
+
const URL_CHANGE_EVENT = "urlchange";
|
|
786
|
+
function useReadableFilters(columns, options = {}) {
|
|
787
|
+
const { history = "replace", scroll = false } = options;
|
|
788
|
+
const columnSnapshot = React.useMemo(() => columns, [columns]);
|
|
789
|
+
const lastWrittenUrlRef = React.useRef("");
|
|
790
|
+
const filtersRef = React.useRef(null);
|
|
791
|
+
const [filters, setFilters] = React.useState(() => {
|
|
792
|
+
const initial = parseReadableFilters(getUrlParams(), columnSnapshot);
|
|
793
|
+
filtersRef.current = initial;
|
|
794
|
+
return initial;
|
|
795
|
+
});
|
|
796
|
+
React.useEffect(() => {
|
|
797
|
+
const handleUrlChange = () => {
|
|
798
|
+
if (window.location.search === lastWrittenUrlRef.current) return;
|
|
799
|
+
const next = parseReadableFilters(getUrlParams(), columnSnapshot);
|
|
800
|
+
filtersRef.current = next;
|
|
801
|
+
setFilters(next);
|
|
802
|
+
};
|
|
803
|
+
window.addEventListener("popstate", handleUrlChange);
|
|
804
|
+
window.addEventListener(URL_CHANGE_EVENT, handleUrlChange);
|
|
805
|
+
return () => {
|
|
806
|
+
window.removeEventListener("popstate", handleUrlChange);
|
|
807
|
+
window.removeEventListener(URL_CHANGE_EVENT, handleUrlChange);
|
|
808
|
+
};
|
|
809
|
+
}, [columnSnapshot]);
|
|
810
|
+
const writeToUrl = React.useCallback((next) => {
|
|
811
|
+
const params = getUrlParams();
|
|
812
|
+
applyReadableFilters(params, columnSnapshot, next);
|
|
813
|
+
setSearchParams(params, {
|
|
814
|
+
history,
|
|
815
|
+
scroll
|
|
816
|
+
});
|
|
817
|
+
lastWrittenUrlRef.current = `?${params.toString()}`;
|
|
818
|
+
}, [
|
|
819
|
+
columnSnapshot,
|
|
820
|
+
history,
|
|
821
|
+
scroll
|
|
822
|
+
]);
|
|
823
|
+
return [filters, React.useCallback((updater) => {
|
|
824
|
+
const next = typeof updater === "function" ? updater(filtersRef.current) : updater;
|
|
825
|
+
filtersRef.current = next;
|
|
826
|
+
setFilters(next);
|
|
827
|
+
writeToUrl(next);
|
|
828
|
+
}, [writeToUrl])];
|
|
829
|
+
}
|
|
830
|
+
|
|
685
831
|
//#endregion
|
|
686
832
|
//#region src/components/data-table/data-table-range-filter.tsx
|
|
687
833
|
function DataTableRangeFilter({ filter, column, inputId, onFilterUpdate, className,...props }) {
|
|
@@ -1250,204 +1396,47 @@ function formatDate(date, opts = {}) {
|
|
|
1250
1396
|
}
|
|
1251
1397
|
|
|
1252
1398
|
//#endregion
|
|
1253
|
-
//#region src/
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
const id =
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
return [parts[0] ?? "", parts[1] ?? ""];
|
|
1295
|
-
}
|
|
1296
|
-
if (variant === "select") return value.split(",").filter(Boolean)[0] ?? "";
|
|
1297
|
-
return value;
|
|
1298
|
-
}
|
|
1299
|
-
function serializeValue(value, variant) {
|
|
1300
|
-
if (Array.isArray(value)) {
|
|
1301
|
-
if (RANGE_VARIANTS.has(variant)) return [value[0] ?? "", value[1] ?? ""].join(",");
|
|
1302
|
-
return value.join(",");
|
|
1303
|
-
}
|
|
1304
|
-
return value;
|
|
1305
|
-
}
|
|
1306
|
-
function parseReadableFilters(params, columns) {
|
|
1307
|
-
const columnsById = /* @__PURE__ */ new Map();
|
|
1308
|
-
for (const column of columns) {
|
|
1309
|
-
const id = getColumnId(column);
|
|
1310
|
-
if (id) columnsById.set(id, column);
|
|
1311
|
-
}
|
|
1312
|
-
const filters = [];
|
|
1313
|
-
for (const [key, rawValue] of params.entries()) {
|
|
1314
|
-
if (key.endsWith(OP_SUFFIX)) continue;
|
|
1315
|
-
const { base } = splitKey(key);
|
|
1316
|
-
const column = columnsById.get(base);
|
|
1317
|
-
if (!column) continue;
|
|
1318
|
-
const variant = getColumnVariant(column) ?? "text";
|
|
1319
|
-
const opKey = `${key}${OP_SUFFIX}`;
|
|
1320
|
-
const operator = params.get(opKey) ?? getDefaultFilterOperator(variant);
|
|
1321
|
-
if (!rawValue && operator !== "isEmpty" && operator !== "isNotEmpty") continue;
|
|
1322
|
-
const parsedValue = operator === "isEmpty" || operator === "isNotEmpty" ? "" : parseValueByVariant(rawValue, variant);
|
|
1323
|
-
filters.push({
|
|
1324
|
-
id: base,
|
|
1325
|
-
value: parsedValue,
|
|
1326
|
-
variant,
|
|
1327
|
-
operator,
|
|
1328
|
-
filterId: key || generateId({ length: 8 })
|
|
1329
|
-
});
|
|
1330
|
-
}
|
|
1331
|
-
return filters;
|
|
1332
|
-
}
|
|
1333
|
-
function applyReadableFilters(params, columns, filters) {
|
|
1334
|
-
const columnIds = new Set(columns.map((column) => getColumnId(column)).filter(Boolean));
|
|
1335
|
-
for (const key of Array.from(params.keys())) if (isFilterKey(key, columnIds)) params.delete(key);
|
|
1336
|
-
const counters = /* @__PURE__ */ new Map();
|
|
1337
|
-
for (const filter of filters) {
|
|
1338
|
-
const columnId = String(filter.id);
|
|
1339
|
-
const count = (counters.get(columnId) ?? 0) + 1;
|
|
1340
|
-
counters.set(columnId, count);
|
|
1341
|
-
const key = count === 1 ? columnId : `${columnId}${INDEX_SEPARATOR}${count}`;
|
|
1342
|
-
const variant = filter.variant ?? "text";
|
|
1343
|
-
const value = serializeValue(filter.value, variant);
|
|
1344
|
-
params.set(key, filter.operator === "isEmpty" || filter.operator === "isNotEmpty" ? "" : value);
|
|
1345
|
-
const defaultOperator = getDefaultFilterOperator(variant);
|
|
1346
|
-
if (filter.operator !== defaultOperator) params.set(`${key}${OP_SUFFIX}`, filter.operator);
|
|
1347
|
-
}
|
|
1348
|
-
return params;
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
//#endregion
|
|
1352
|
-
//#region src/hooks/use-readable-filters.ts
|
|
1353
|
-
const URL_CHANGE_EVENT = "urlchange";
|
|
1354
|
-
function useReadableFilters(columns, options = {}) {
|
|
1355
|
-
const { debounceMs = 300, history = "replace", scroll = false } = options;
|
|
1356
|
-
const columnSnapshot = React.useMemo(() => columns, [columns]);
|
|
1357
|
-
const skipWriteRef = React.useRef(true);
|
|
1358
|
-
const [filters, setFilters] = React.useState(() => parseReadableFilters(getUrlParams(), columnSnapshot));
|
|
1359
|
-
React.useEffect(() => {
|
|
1360
|
-
const handleUrlChange = () => {
|
|
1361
|
-
skipWriteRef.current = true;
|
|
1362
|
-
setFilters(parseReadableFilters(getUrlParams(), columnSnapshot));
|
|
1363
|
-
};
|
|
1364
|
-
window.addEventListener("popstate", handleUrlChange);
|
|
1365
|
-
window.addEventListener(URL_CHANGE_EVENT, handleUrlChange);
|
|
1366
|
-
return () => {
|
|
1367
|
-
window.removeEventListener("popstate", handleUrlChange);
|
|
1368
|
-
window.removeEventListener(URL_CHANGE_EVENT, handleUrlChange);
|
|
1369
|
-
};
|
|
1370
|
-
}, [columnSnapshot]);
|
|
1371
|
-
const writeFilters = React.useCallback((next) => {
|
|
1372
|
-
const params = getUrlParams();
|
|
1373
|
-
applyReadableFilters(params, columnSnapshot, next);
|
|
1374
|
-
setSearchParams(params, {
|
|
1375
|
-
history,
|
|
1376
|
-
scroll
|
|
1377
|
-
});
|
|
1378
|
-
}, [
|
|
1379
|
-
columnSnapshot,
|
|
1380
|
-
history,
|
|
1381
|
-
scroll
|
|
1382
|
-
]);
|
|
1383
|
-
const updateFilters = React.useCallback((updater) => {
|
|
1384
|
-
setFilters((prev) => {
|
|
1385
|
-
return typeof updater === "function" ? updater(prev) : updater;
|
|
1386
|
-
});
|
|
1387
|
-
}, []);
|
|
1388
|
-
const debouncedWrite = useDebouncedCallback(writeFilters, debounceMs);
|
|
1389
|
-
React.useEffect(() => {
|
|
1390
|
-
if (skipWriteRef.current) {
|
|
1391
|
-
skipWriteRef.current = false;
|
|
1392
|
-
return;
|
|
1393
|
-
}
|
|
1394
|
-
if (debounceMs > 0) debouncedWrite(filters);
|
|
1395
|
-
else writeFilters(filters);
|
|
1396
|
-
}, [
|
|
1397
|
-
filters,
|
|
1398
|
-
debounceMs,
|
|
1399
|
-
debouncedWrite,
|
|
1400
|
-
writeFilters
|
|
1401
|
-
]);
|
|
1402
|
-
return [filters, updateFilters];
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
//#endregion
|
|
1406
|
-
//#region src/components/data-table/data-table-filter-list.tsx
|
|
1407
|
-
const DEBOUNCE_MS$2 = 300;
|
|
1408
|
-
const THROTTLE_MS$2 = 50;
|
|
1409
|
-
const FILTER_SHORTCUT_KEY$1 = "f";
|
|
1410
|
-
const REMOVE_FILTER_SHORTCUTS$1 = ["backspace", "delete"];
|
|
1411
|
-
function DataTableFilterList({ table, debounceMs = DEBOUNCE_MS$2, throttleMs = THROTTLE_MS$2, shallow = true, disabled,...props }) {
|
|
1412
|
-
const id = React.useId();
|
|
1413
|
-
const labelId = React.useId();
|
|
1414
|
-
const descriptionId = React.useId();
|
|
1415
|
-
const [open, setOpen] = React.useState(false);
|
|
1416
|
-
const addButtonRef = React.useRef(null);
|
|
1417
|
-
const columns = React.useMemo(() => {
|
|
1418
|
-
return table.getAllColumns().filter((column) => column.columnDef.enableColumnFilter);
|
|
1419
|
-
}, [table]);
|
|
1420
|
-
const queryStateOptions = table.options.meta?.queryStateOptions;
|
|
1421
|
-
const [filters, setFilters] = useReadableFilters(columns, {
|
|
1422
|
-
...queryStateOptions,
|
|
1423
|
-
debounceMs: 0
|
|
1424
|
-
});
|
|
1425
|
-
const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);
|
|
1426
|
-
const [joinOperator, setJoinOperator] = useQueryState(table.options.meta?.queryKeys?.joinOperator ?? "", parseAsStringEnum(["and", "or"]).withDefault("and"));
|
|
1427
|
-
const onFilterAdd = React.useCallback(() => {
|
|
1428
|
-
const column = columns[0];
|
|
1429
|
-
if (!column) return;
|
|
1430
|
-
debouncedSetFilters([...filters, {
|
|
1431
|
-
id: column.id,
|
|
1432
|
-
value: "",
|
|
1433
|
-
variant: column.columnDef.meta?.variant ?? "text",
|
|
1434
|
-
operator: getDefaultFilterOperator(column.columnDef.meta?.variant ?? "text"),
|
|
1435
|
-
filterId: generateId({ length: 8 })
|
|
1436
|
-
}]);
|
|
1437
|
-
}, [
|
|
1438
|
-
columns,
|
|
1439
|
-
filters,
|
|
1440
|
-
debouncedSetFilters
|
|
1441
|
-
]);
|
|
1442
|
-
const onFilterUpdate = React.useCallback((filterId, updates) => {
|
|
1443
|
-
debouncedSetFilters((prevFilters) => {
|
|
1444
|
-
return prevFilters.map((filter) => {
|
|
1445
|
-
if (filter.filterId === filterId) return {
|
|
1446
|
-
...filter,
|
|
1447
|
-
...updates
|
|
1448
|
-
};
|
|
1449
|
-
return filter;
|
|
1450
|
-
});
|
|
1399
|
+
//#region src/components/data-table/data-table-filter-list.tsx
|
|
1400
|
+
const DEBOUNCE_MS$2 = 300;
|
|
1401
|
+
const THROTTLE_MS$2 = 50;
|
|
1402
|
+
const FILTER_SHORTCUT_KEY$1 = "f";
|
|
1403
|
+
const REMOVE_FILTER_SHORTCUTS$1 = ["backspace", "delete"];
|
|
1404
|
+
function DataTableFilterList({ table, debounceMs = DEBOUNCE_MS$2, throttleMs = THROTTLE_MS$2, shallow = true, disabled,...props }) {
|
|
1405
|
+
const id = React.useId();
|
|
1406
|
+
const labelId = React.useId();
|
|
1407
|
+
const descriptionId = React.useId();
|
|
1408
|
+
const [open, setOpen] = React.useState(false);
|
|
1409
|
+
const addButtonRef = React.useRef(null);
|
|
1410
|
+
const columns = React.useMemo(() => {
|
|
1411
|
+
return table.getAllColumns().filter((column) => column.columnDef.enableColumnFilter);
|
|
1412
|
+
}, [table]);
|
|
1413
|
+
const [filters, setFilters] = useReadableFilters(columns, { debounceMs });
|
|
1414
|
+
const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);
|
|
1415
|
+
const [joinOperator, setJoinOperator] = useQueryState(table.options.meta?.queryKeys?.joinOperator ?? "", parseAsStringEnum(["and", "or"]).withDefault("and"));
|
|
1416
|
+
const onFilterAdd = React.useCallback(() => {
|
|
1417
|
+
const column = columns[0];
|
|
1418
|
+
if (!column) return;
|
|
1419
|
+
debouncedSetFilters([...filters, {
|
|
1420
|
+
id: column.id,
|
|
1421
|
+
value: "",
|
|
1422
|
+
variant: column.columnDef.meta?.variant ?? "text",
|
|
1423
|
+
operator: getDefaultFilterOperator(column.columnDef.meta?.variant ?? "text"),
|
|
1424
|
+
filterId: generateId({ length: 8 })
|
|
1425
|
+
}]);
|
|
1426
|
+
}, [
|
|
1427
|
+
columns,
|
|
1428
|
+
filters,
|
|
1429
|
+
debouncedSetFilters
|
|
1430
|
+
]);
|
|
1431
|
+
const onFilterUpdate = React.useCallback((filterId, updates) => {
|
|
1432
|
+
debouncedSetFilters((prevFilters) => {
|
|
1433
|
+
return prevFilters.map((filter) => {
|
|
1434
|
+
if (filter.filterId === filterId) return {
|
|
1435
|
+
...filter,
|
|
1436
|
+
...updates
|
|
1437
|
+
};
|
|
1438
|
+
return filter;
|
|
1439
|
+
});
|
|
1451
1440
|
});
|
|
1452
1441
|
}, [debouncedSetFilters]);
|
|
1453
1442
|
const onFilterRemove = React.useCallback((filterId) => {
|
|
@@ -1457,7 +1446,7 @@ function DataTableFilterList({ table, debounceMs = DEBOUNCE_MS$2, throttleMs = T
|
|
|
1457
1446
|
});
|
|
1458
1447
|
}, [filters, setFilters]);
|
|
1459
1448
|
const onFiltersReset = React.useCallback(() => {
|
|
1460
|
-
setFilters(
|
|
1449
|
+
setFilters([]);
|
|
1461
1450
|
setJoinOperator("and");
|
|
1462
1451
|
}, [setFilters, setJoinOperator]);
|
|
1463
1452
|
React.useEffect(() => {
|
|
@@ -1921,11 +1910,7 @@ function DataTableFilterMenu({ table, debounceMs = DEBOUNCE_MS$1, throttleMs = T
|
|
|
1921
1910
|
setSelectedColumn(null);
|
|
1922
1911
|
}
|
|
1923
1912
|
}, [inputValue, selectedColumn]);
|
|
1924
|
-
const
|
|
1925
|
-
const [filters, setFilters] = useReadableFilters(columns, {
|
|
1926
|
-
...queryStateOptions,
|
|
1927
|
-
debounceMs: 0
|
|
1928
|
-
});
|
|
1913
|
+
const [filters, setFilters] = useReadableFilters(columns, { debounceMs });
|
|
1929
1914
|
const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);
|
|
1930
1915
|
const onFilterAdd = React.useCallback((column, value) => {
|
|
1931
1916
|
if (!value.trim() && column.columnDef.meta?.variant !== "boolean") return;
|
|
@@ -2642,7 +2627,7 @@ function DataTableViewOptions({ table, disabled,...props }) {
|
|
|
2642
2627
|
role: "combobox",
|
|
2643
2628
|
variant: "outline",
|
|
2644
2629
|
size: "sm",
|
|
2645
|
-
className: "ml-auto
|
|
2630
|
+
className: "ml-auto flex h-8 font-normal",
|
|
2646
2631
|
disabled,
|
|
2647
2632
|
children: [/* @__PURE__ */ jsx(Settings2, { className: "text-muted-foreground" }), "View"]
|
|
2648
2633
|
})
|
|
@@ -2707,51 +2692,142 @@ function AutoTableSimpleFilters({ table, shallow = false, filters: externalFilte
|
|
|
2707
2692
|
setFilters
|
|
2708
2693
|
]);
|
|
2709
2694
|
const hasFilters = filters.length > 0;
|
|
2695
|
+
const filterColumns = React.useMemo(() => columns.filter((col) => col.columnDef.meta?.variant && col.columnDef.enableColumnFilter !== false), [columns]);
|
|
2696
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
2697
|
+
const [visibleCount, setVisibleCount] = React.useState(null);
|
|
2698
|
+
const containerRef = React.useRef(null);
|
|
2699
|
+
const calcVisibleCount = React.useCallback(() => {
|
|
2700
|
+
const el = containerRef.current;
|
|
2701
|
+
if (!el) return;
|
|
2702
|
+
const items = el.querySelectorAll("[data-filter-item]");
|
|
2703
|
+
if (items.length === 0) return;
|
|
2704
|
+
const restoreList = [];
|
|
2705
|
+
el.querySelectorAll(".hidden").forEach((node) => {
|
|
2706
|
+
const htmlNode = node;
|
|
2707
|
+
htmlNode.classList.remove("hidden");
|
|
2708
|
+
restoreList.push(htmlNode);
|
|
2709
|
+
});
|
|
2710
|
+
el.style.flex = "1 1 0%";
|
|
2711
|
+
el.offsetHeight;
|
|
2712
|
+
const firstTop = items[0].offsetTop;
|
|
2713
|
+
let count = 0;
|
|
2714
|
+
for (const item of Array.from(items)) {
|
|
2715
|
+
if (item.offsetTop > firstTop) break;
|
|
2716
|
+
count++;
|
|
2717
|
+
}
|
|
2718
|
+
const btn = el.querySelector("[data-filter-toggle]");
|
|
2719
|
+
if (btn && count < items.length && count > 0) {
|
|
2720
|
+
if (btn.offsetTop > firstTop) count--;
|
|
2721
|
+
}
|
|
2722
|
+
el.style.flex = "";
|
|
2723
|
+
restoreList.forEach((node) => node.classList.add("hidden"));
|
|
2724
|
+
setVisibleCount(count >= items.length ? null : count);
|
|
2725
|
+
}, []);
|
|
2726
|
+
React.useLayoutEffect(() => {
|
|
2727
|
+
if (!mounted) return;
|
|
2728
|
+
calcVisibleCount();
|
|
2729
|
+
}, [
|
|
2730
|
+
mounted,
|
|
2731
|
+
filterColumns.length,
|
|
2732
|
+
calcVisibleCount
|
|
2733
|
+
]);
|
|
2734
|
+
React.useEffect(() => {
|
|
2735
|
+
const parent = containerRef.current?.closest("[data-filter-parent]");
|
|
2736
|
+
if (!mounted || !parent) return;
|
|
2737
|
+
let lastWidth = parent.offsetWidth;
|
|
2738
|
+
let rafId = 0;
|
|
2739
|
+
const ro = new ResizeObserver(() => {
|
|
2740
|
+
const newWidth = parent.offsetWidth;
|
|
2741
|
+
if (newWidth === lastWidth) return;
|
|
2742
|
+
lastWidth = newWidth;
|
|
2743
|
+
cancelAnimationFrame(rafId);
|
|
2744
|
+
rafId = requestAnimationFrame(calcVisibleCount);
|
|
2745
|
+
});
|
|
2746
|
+
ro.observe(parent);
|
|
2747
|
+
return () => {
|
|
2748
|
+
cancelAnimationFrame(rafId);
|
|
2749
|
+
ro.disconnect();
|
|
2750
|
+
};
|
|
2751
|
+
}, [
|
|
2752
|
+
mounted,
|
|
2753
|
+
filterColumns.length,
|
|
2754
|
+
calcVisibleCount
|
|
2755
|
+
]);
|
|
2710
2756
|
if (!mounted) return null;
|
|
2711
2757
|
return /* @__PURE__ */ jsxs("div", {
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2758
|
+
ref: containerRef,
|
|
2759
|
+
className: "flex flex-wrap items-center gap-2 min-w-0",
|
|
2760
|
+
children: [
|
|
2761
|
+
filterColumns.map((column, index) => {
|
|
2762
|
+
const meta = column.columnDef.meta;
|
|
2763
|
+
const value = getFilterValue(column.id);
|
|
2764
|
+
const isHidden = !expanded && visibleCount !== null && index >= visibleCount;
|
|
2765
|
+
switch (meta.variant) {
|
|
2766
|
+
case "text": return /* @__PURE__ */ jsx("div", {
|
|
2767
|
+
className: isHidden ? "hidden" : "",
|
|
2768
|
+
"data-filter-item": true,
|
|
2769
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
2770
|
+
placeholder: meta.placeholder ?? meta.label ?? column.id,
|
|
2771
|
+
value: typeof value === "string" ? value : "",
|
|
2772
|
+
onChange: (e) => updateFilter(column.id, e.target.value || void 0),
|
|
2773
|
+
className: "h-8 w-36 shrink-0"
|
|
2774
|
+
})
|
|
2775
|
+
}, column.id);
|
|
2776
|
+
case "select":
|
|
2777
|
+
case "multiSelect": return /* @__PURE__ */ jsx("div", {
|
|
2778
|
+
className: isHidden ? "hidden" : "",
|
|
2779
|
+
"data-filter-item": true,
|
|
2780
|
+
children: /* @__PURE__ */ jsx(SimpleFacetedFilter, {
|
|
2781
|
+
title: meta.label ?? column.id,
|
|
2782
|
+
options: meta.options ?? [],
|
|
2783
|
+
multiple: meta.variant === "multiSelect",
|
|
2784
|
+
value: Array.isArray(value) ? value : value ? [value] : [],
|
|
2785
|
+
onChange: (v) => updateFilter(column.id, v.length ? v : void 0)
|
|
2786
|
+
})
|
|
2787
|
+
}, column.id);
|
|
2788
|
+
case "range": return /* @__PURE__ */ jsx("div", {
|
|
2789
|
+
className: isHidden ? "hidden" : "",
|
|
2790
|
+
"data-filter-item": true,
|
|
2791
|
+
children: /* @__PURE__ */ jsx(SimpleSliderFilter, {
|
|
2792
|
+
title: meta.label ?? column.id,
|
|
2793
|
+
range: meta.range,
|
|
2794
|
+
unit: meta.unit,
|
|
2795
|
+
value: Array.isArray(value) ? value.map(Number) : void 0,
|
|
2796
|
+
onChange: (v) => updateFilter(column.id, v ? v.map(String) : void 0)
|
|
2797
|
+
})
|
|
2798
|
+
}, column.id);
|
|
2799
|
+
case "date":
|
|
2800
|
+
case "dateRange": return /* @__PURE__ */ jsx("div", {
|
|
2801
|
+
className: isHidden ? "hidden" : "",
|
|
2802
|
+
"data-filter-item": true,
|
|
2803
|
+
children: /* @__PURE__ */ jsx(SimpleDateFilter, {
|
|
2804
|
+
title: meta.label ?? column.id,
|
|
2805
|
+
multiple: meta.variant === "dateRange",
|
|
2806
|
+
value,
|
|
2807
|
+
onChange: (v) => updateFilter(column.id, v)
|
|
2808
|
+
})
|
|
2809
|
+
}, column.id);
|
|
2810
|
+
default: return null;
|
|
2811
|
+
}
|
|
2812
|
+
}),
|
|
2813
|
+
/* @__PURE__ */ jsx(Button, {
|
|
2814
|
+
"data-filter-toggle": true,
|
|
2815
|
+
variant: "ghost",
|
|
2816
|
+
size: "icon",
|
|
2817
|
+
className: cn("h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground", visibleCount === null && "hidden"),
|
|
2818
|
+
onClick: () => setExpanded((prev) => !prev),
|
|
2819
|
+
"aria-label": expanded ? "收起筛选" : "展开更多筛选",
|
|
2820
|
+
"aria-expanded": expanded,
|
|
2821
|
+
children: expanded ? /* @__PURE__ */ jsx(ChevronUp, { className: "size-4" }) : /* @__PURE__ */ jsx(ChevronDown, { className: "size-4" })
|
|
2822
|
+
}),
|
|
2823
|
+
hasFilters && /* @__PURE__ */ jsxs(Button, {
|
|
2824
|
+
variant: "outline",
|
|
2825
|
+
size: "sm",
|
|
2826
|
+
className: "h-8 border-dashed shrink-0",
|
|
2827
|
+
onClick: () => setFilters([]),
|
|
2828
|
+
children: [/* @__PURE__ */ jsx(X, { className: "size-4" }), "Reset"]
|
|
2829
|
+
})
|
|
2830
|
+
]
|
|
2755
2831
|
});
|
|
2756
2832
|
}
|
|
2757
2833
|
function SimpleFacetedFilter({ title, options, multiple, value, onChange }) {
|
|
@@ -3473,356 +3549,48 @@ function ActionBarSeparator(props) {
|
|
|
3473
3549
|
}
|
|
3474
3550
|
|
|
3475
3551
|
//#endregion
|
|
3476
|
-
//#region src/
|
|
3477
|
-
function
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
const cellValue = row.getValue(header);
|
|
3482
|
-
return typeof cellValue === "string" ? `"${cellValue.replace(/"/g, "\"\"")}"` : cellValue;
|
|
3483
|
-
}).join(","))].join("\n");
|
|
3484
|
-
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
|
3485
|
-
const url = URL.createObjectURL(blob);
|
|
3486
|
-
const link = document.createElement("a");
|
|
3487
|
-
link.setAttribute("href", url);
|
|
3488
|
-
link.setAttribute("download", `${filename}.csv`);
|
|
3489
|
-
link.style.visibility = "hidden";
|
|
3490
|
-
document.body.appendChild(link);
|
|
3491
|
-
link.click();
|
|
3492
|
-
document.body.removeChild(link);
|
|
3493
|
-
}
|
|
3494
|
-
|
|
3495
|
-
//#endregion
|
|
3496
|
-
//#region src/components/auto-crud/auto-table-action-bar.tsx
|
|
3497
|
-
function AutoTableActionBar({ table, onDeleteSelected, onUpdateSelected, batchUpdateFields, enableExport = true, enableDelete = true, extraActions }) {
|
|
3498
|
-
const rows = table.getFilteredSelectedRowModel().rows;
|
|
3499
|
-
const onOpenChange = React.useCallback((open) => {
|
|
3500
|
-
if (!open) table.toggleAllRowsSelected(false);
|
|
3501
|
-
}, [table]);
|
|
3502
|
-
const onExport = React.useCallback(() => {
|
|
3503
|
-
exportTableToCSV(table, {
|
|
3504
|
-
excludeColumns: ["select", "actions"],
|
|
3505
|
-
onlySelected: true
|
|
3506
|
-
});
|
|
3507
|
-
}, [table]);
|
|
3508
|
-
const onDelete = React.useCallback(() => {
|
|
3509
|
-
if (onDeleteSelected) onDeleteSelected(rows.map((row) => row.original));
|
|
3510
|
-
}, [rows, onDeleteSelected]);
|
|
3511
|
-
const handleBatchUpdate = React.useCallback((field, value) => {
|
|
3512
|
-
if (onUpdateSelected) {
|
|
3513
|
-
onUpdateSelected(rows.map((row) => row.original), { [field]: value });
|
|
3514
|
-
table.toggleAllRowsSelected(false);
|
|
3515
|
-
}
|
|
3516
|
-
}, [
|
|
3517
|
-
rows,
|
|
3518
|
-
onUpdateSelected,
|
|
3519
|
-
table
|
|
3520
|
-
]);
|
|
3521
|
-
return /* @__PURE__ */ jsxs(ActionBar, {
|
|
3522
|
-
open: rows.length > 0,
|
|
3523
|
-
onOpenChange,
|
|
3524
|
-
children: [
|
|
3525
|
-
/* @__PURE__ */ jsxs(ActionBarSelection, { children: [
|
|
3526
|
-
/* @__PURE__ */ jsx("span", {
|
|
3527
|
-
className: "font-medium",
|
|
3528
|
-
children: rows.length
|
|
3529
|
-
}),
|
|
3530
|
-
/* @__PURE__ */ jsx("span", { children: "selected" }),
|
|
3531
|
-
/* @__PURE__ */ jsx(ActionBarSeparator, {}),
|
|
3532
|
-
/* @__PURE__ */ jsx(ActionBarClose, { children: /* @__PURE__ */ jsx(X, {}) })
|
|
3533
|
-
] }),
|
|
3534
|
-
/* @__PURE__ */ jsx(ActionBarSeparator, {}),
|
|
3535
|
-
/* @__PURE__ */ jsxs(ActionBarGroup, { children: [
|
|
3536
|
-
batchUpdateFields?.map((fieldConfig) => /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
3537
|
-
asChild: true,
|
|
3538
|
-
children: /* @__PURE__ */ jsxs(Button, {
|
|
3539
|
-
variant: "ghost",
|
|
3540
|
-
size: "sm",
|
|
3541
|
-
className: "h-7 gap-1 px-2",
|
|
3542
|
-
children: [fieldConfig.label, /* @__PURE__ */ jsx(ChevronDown, { className: "size-3.5" })]
|
|
3543
|
-
})
|
|
3544
|
-
}), /* @__PURE__ */ jsx(DropdownMenuContent, {
|
|
3545
|
-
align: "start",
|
|
3546
|
-
children: fieldConfig.options.map((option) => /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
3547
|
-
onClick: () => handleBatchUpdate(fieldConfig.field, option.value),
|
|
3548
|
-
children: option.label
|
|
3549
|
-
}, option.value))
|
|
3550
|
-
})] }, fieldConfig.field)),
|
|
3551
|
-
extraActions,
|
|
3552
|
-
enableExport && /* @__PURE__ */ jsxs(ActionBarItem, {
|
|
3553
|
-
onClick: onExport,
|
|
3554
|
-
children: [/* @__PURE__ */ jsx(Download, {}), "Export"]
|
|
3555
|
-
}),
|
|
3556
|
-
enableDelete && onDeleteSelected && /* @__PURE__ */ jsxs(ActionBarItem, {
|
|
3557
|
-
variant: "destructive",
|
|
3558
|
-
onClick: onDelete,
|
|
3559
|
-
children: [/* @__PURE__ */ jsx(Trash2, {}), "Delete"]
|
|
3560
|
-
})
|
|
3561
|
-
] })
|
|
3562
|
-
]
|
|
3552
|
+
//#region src/components/data-table/data-table-column-header.tsx
|
|
3553
|
+
function DataTableColumnHeader({ column, label, className,...props }) {
|
|
3554
|
+
if (!column.getCanSort() && !column.getCanHide()) return /* @__PURE__ */ jsx("div", {
|
|
3555
|
+
className: cn(className),
|
|
3556
|
+
children: label
|
|
3563
3557
|
});
|
|
3558
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsxs(DropdownMenuTrigger, {
|
|
3559
|
+
className: cn("-ml-1.5 flex h-8 items-center gap-1.5 rounded-md px-2 py-1.5 hover:bg-accent focus:outline-none focus:ring-1 focus:ring-ring data-[state=open]:bg-accent [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground", className),
|
|
3560
|
+
...props,
|
|
3561
|
+
children: [label, column.getCanSort() && (column.getIsSorted() === "desc" ? /* @__PURE__ */ jsx(ChevronDown, {}) : column.getIsSorted() === "asc" ? /* @__PURE__ */ jsx(ChevronUp, {}) : /* @__PURE__ */ jsx(ChevronsUpDown, {}))]
|
|
3562
|
+
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
3563
|
+
align: "start",
|
|
3564
|
+
className: "w-28",
|
|
3565
|
+
children: [column.getCanSort() && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3566
|
+
/* @__PURE__ */ jsxs(DropdownMenuCheckboxItem, {
|
|
3567
|
+
className: "relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground",
|
|
3568
|
+
checked: column.getIsSorted() === "asc",
|
|
3569
|
+
onClick: () => column.toggleSorting(false),
|
|
3570
|
+
children: [/* @__PURE__ */ jsx(ChevronUp, {}), "Asc"]
|
|
3571
|
+
}),
|
|
3572
|
+
/* @__PURE__ */ jsxs(DropdownMenuCheckboxItem, {
|
|
3573
|
+
className: "relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground",
|
|
3574
|
+
checked: column.getIsSorted() === "desc",
|
|
3575
|
+
onClick: () => column.toggleSorting(true),
|
|
3576
|
+
children: [/* @__PURE__ */ jsx(ChevronDown, {}), "Desc"]
|
|
3577
|
+
}),
|
|
3578
|
+
column.getIsSorted() && /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
3579
|
+
className: "pl-2 [&_svg]:text-muted-foreground",
|
|
3580
|
+
onClick: () => column.clearSorting(),
|
|
3581
|
+
children: [/* @__PURE__ */ jsx(X, {}), "Reset"]
|
|
3582
|
+
})
|
|
3583
|
+
] }), column.getCanHide() && /* @__PURE__ */ jsxs(DropdownMenuCheckboxItem, {
|
|
3584
|
+
className: "relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground",
|
|
3585
|
+
checked: !column.getIsVisible(),
|
|
3586
|
+
onClick: () => column.toggleVisibility(false),
|
|
3587
|
+
children: [/* @__PURE__ */ jsx(EyeOff, {}), "Hide"]
|
|
3588
|
+
})]
|
|
3589
|
+
})] });
|
|
3564
3590
|
}
|
|
3565
3591
|
|
|
3566
3592
|
//#endregion
|
|
3567
|
-
//#region src/lib/
|
|
3568
|
-
/**
|
|
3569
|
-
* 创建自定义 Parser(替代 nuqs 的 createParser)
|
|
3570
|
-
*/
|
|
3571
|
-
function createParser(config) {
|
|
3572
|
-
return {
|
|
3573
|
-
parse: config.parse,
|
|
3574
|
-
serialize: (value) => value === null ? "" : config.serialize(value),
|
|
3575
|
-
withDefault: (defaultValue) => ({
|
|
3576
|
-
parse: (value) => {
|
|
3577
|
-
const parsed = config.parse(value);
|
|
3578
|
-
return parsed === null ? defaultValue : parsed;
|
|
3579
|
-
},
|
|
3580
|
-
serialize: config.serialize,
|
|
3581
|
-
defaultValue
|
|
3582
|
-
})
|
|
3583
|
-
};
|
|
3584
|
-
}
|
|
3585
|
-
const sortingItemSchema = z.object({
|
|
3586
|
-
id: z.string(),
|
|
3587
|
-
desc: z.boolean()
|
|
3588
|
-
});
|
|
3589
|
-
const getSortingStateParser = (columnIds) => {
|
|
3590
|
-
const validKeys = columnIds ? columnIds instanceof Set ? columnIds : new Set(columnIds) : null;
|
|
3591
|
-
return createParser({
|
|
3592
|
-
parse: (value) => {
|
|
3593
|
-
try {
|
|
3594
|
-
const parsed = JSON.parse(value);
|
|
3595
|
-
const result = z.array(sortingItemSchema).safeParse(parsed);
|
|
3596
|
-
if (!result.success) return null;
|
|
3597
|
-
if (validKeys && result.data.some((item) => !validKeys.has(item.id))) return null;
|
|
3598
|
-
return result.data;
|
|
3599
|
-
} catch {
|
|
3600
|
-
return null;
|
|
3601
|
-
}
|
|
3602
|
-
},
|
|
3603
|
-
serialize: (value) => JSON.stringify(value)
|
|
3604
|
-
});
|
|
3605
|
-
};
|
|
3606
|
-
const filterItemSchema = z.object({
|
|
3607
|
-
id: z.string(),
|
|
3608
|
-
value: z.union([z.string(), z.array(z.string())]),
|
|
3609
|
-
variant: z.enum(dataTableConfig.filterVariants),
|
|
3610
|
-
operator: z.enum(dataTableConfig.operators),
|
|
3611
|
-
filterId: z.string()
|
|
3612
|
-
});
|
|
3613
|
-
|
|
3614
|
-
//#endregion
|
|
3615
|
-
//#region src/hooks/use-data-table.ts
|
|
3616
|
-
const PAGE_KEY = "page";
|
|
3617
|
-
const PER_PAGE_KEY = "perPage";
|
|
3618
|
-
const SORT_KEY = "sort";
|
|
3619
|
-
const FILTERS_KEY = "filters";
|
|
3620
|
-
const JOIN_OPERATOR_KEY = "joinOperator";
|
|
3621
|
-
const ARRAY_SEPARATOR = ",";
|
|
3622
|
-
const DEBOUNCE_MS = 300;
|
|
3623
|
-
const THROTTLE_MS = 50;
|
|
3624
|
-
function useDataTable(props) {
|
|
3625
|
-
const { columns, pageCount = -1, initialState, queryKeys, history = "replace", debounceMs = DEBOUNCE_MS, throttleMs = THROTTLE_MS, clearOnDefault = false, enableAdvancedFilter = false, scroll = false, shallow = true, startTransition,...tableProps } = props;
|
|
3626
|
-
const pageKey = queryKeys?.page ?? PAGE_KEY;
|
|
3627
|
-
const perPageKey = queryKeys?.perPage ?? PER_PAGE_KEY;
|
|
3628
|
-
const sortKey = queryKeys?.sort ?? SORT_KEY;
|
|
3629
|
-
const filtersKey = queryKeys?.filters ?? FILTERS_KEY;
|
|
3630
|
-
const joinOperatorKey = queryKeys?.joinOperator ?? JOIN_OPERATOR_KEY;
|
|
3631
|
-
const queryStateOptions = React.useMemo(() => ({
|
|
3632
|
-
history,
|
|
3633
|
-
scroll,
|
|
3634
|
-
shallow,
|
|
3635
|
-
throttleMs,
|
|
3636
|
-
debounceMs,
|
|
3637
|
-
clearOnDefault,
|
|
3638
|
-
startTransition
|
|
3639
|
-
}), [
|
|
3640
|
-
history,
|
|
3641
|
-
scroll,
|
|
3642
|
-
shallow,
|
|
3643
|
-
throttleMs,
|
|
3644
|
-
debounceMs,
|
|
3645
|
-
clearOnDefault,
|
|
3646
|
-
startTransition
|
|
3647
|
-
]);
|
|
3648
|
-
const [rowSelection, setRowSelection] = React.useState(initialState?.rowSelection ?? {});
|
|
3649
|
-
const [columnVisibility, setColumnVisibility] = React.useState(initialState?.columnVisibility ?? {});
|
|
3650
|
-
const [page, setPage] = useQueryState(pageKey, parseAsInteger.withDefault(1));
|
|
3651
|
-
const [perPage, setPerPage] = useQueryState(perPageKey, parseAsInteger.withDefault(initialState?.pagination?.pageSize ?? 10));
|
|
3652
|
-
const pagination = React.useMemo(() => {
|
|
3653
|
-
return {
|
|
3654
|
-
pageIndex: page - 1,
|
|
3655
|
-
pageSize: perPage
|
|
3656
|
-
};
|
|
3657
|
-
}, [page, perPage]);
|
|
3658
|
-
const onPaginationChange = React.useCallback((updaterOrValue) => {
|
|
3659
|
-
if (typeof updaterOrValue === "function") {
|
|
3660
|
-
const newPagination = updaterOrValue(pagination);
|
|
3661
|
-
setPage(newPagination.pageIndex + 1);
|
|
3662
|
-
setPerPage(newPagination.pageSize);
|
|
3663
|
-
} else {
|
|
3664
|
-
setPage(updaterOrValue.pageIndex + 1);
|
|
3665
|
-
setPerPage(updaterOrValue.pageSize);
|
|
3666
|
-
}
|
|
3667
|
-
}, [
|
|
3668
|
-
pagination,
|
|
3669
|
-
setPage,
|
|
3670
|
-
setPerPage
|
|
3671
|
-
]);
|
|
3672
|
-
const [sorting, setSorting] = useQueryState(sortKey, getSortingStateParser(React.useMemo(() => {
|
|
3673
|
-
return new Set(columns.map((column) => column.id).filter(Boolean));
|
|
3674
|
-
}, [columns])).withDefault(initialState?.sorting ?? []));
|
|
3675
|
-
const onSortingChange = React.useCallback((updaterOrValue) => {
|
|
3676
|
-
if (typeof updaterOrValue === "function") setSorting(updaterOrValue(sorting));
|
|
3677
|
-
else setSorting(updaterOrValue);
|
|
3678
|
-
}, [sorting, setSorting]);
|
|
3679
|
-
const filterableColumns = React.useMemo(() => {
|
|
3680
|
-
if (enableAdvancedFilter) return [];
|
|
3681
|
-
return columns.filter((column) => column.enableColumnFilter);
|
|
3682
|
-
}, [columns, enableAdvancedFilter]);
|
|
3683
|
-
const [filterValues, setFilterValues] = useQueryStates(React.useMemo(() => {
|
|
3684
|
-
if (enableAdvancedFilter) return {};
|
|
3685
|
-
return filterableColumns.reduce((acc, column) => {
|
|
3686
|
-
if (column.meta?.options) acc[column.id ?? ""] = parseAsArrayOf(parseAsString, ARRAY_SEPARATOR).withDefault([]);
|
|
3687
|
-
else acc[column.id ?? ""] = parseAsString.withDefault("");
|
|
3688
|
-
return acc;
|
|
3689
|
-
}, {});
|
|
3690
|
-
}, [filterableColumns, enableAdvancedFilter]));
|
|
3691
|
-
const debouncedSetFilterValues = useDebouncedCallback((values) => {
|
|
3692
|
-
setPage(1);
|
|
3693
|
-
setFilterValues(values);
|
|
3694
|
-
}, debounceMs);
|
|
3695
|
-
const initialColumnFilters = React.useMemo(() => {
|
|
3696
|
-
if (enableAdvancedFilter) return [];
|
|
3697
|
-
return Object.entries(filterValues).reduce((filters, [key, value]) => {
|
|
3698
|
-
if (value !== null) {
|
|
3699
|
-
const processedValue = Array.isArray(value) ? value : typeof value === "string" && /[^a-zA-Z0-9]/.test(value) ? value.split(/[^a-zA-Z0-9]+/).filter(Boolean) : [value];
|
|
3700
|
-
filters.push({
|
|
3701
|
-
id: key,
|
|
3702
|
-
value: processedValue
|
|
3703
|
-
});
|
|
3704
|
-
}
|
|
3705
|
-
return filters;
|
|
3706
|
-
}, []);
|
|
3707
|
-
}, [filterValues, enableAdvancedFilter]);
|
|
3708
|
-
const [columnFilters, setColumnFilters] = React.useState(initialColumnFilters);
|
|
3709
|
-
const onColumnFiltersChange = React.useCallback((updaterOrValue) => {
|
|
3710
|
-
if (enableAdvancedFilter) return;
|
|
3711
|
-
setColumnFilters((prev) => {
|
|
3712
|
-
const next = typeof updaterOrValue === "function" ? updaterOrValue(prev) : updaterOrValue;
|
|
3713
|
-
const filterUpdates = next.reduce((acc, filter) => {
|
|
3714
|
-
if (filterableColumns.find((column) => column.id === filter.id)) acc[filter.id] = filter.value;
|
|
3715
|
-
return acc;
|
|
3716
|
-
}, {});
|
|
3717
|
-
for (const prevFilter of prev) if (!next.some((filter) => filter.id === prevFilter.id)) filterUpdates[prevFilter.id] = null;
|
|
3718
|
-
debouncedSetFilterValues(filterUpdates);
|
|
3719
|
-
return next;
|
|
3720
|
-
});
|
|
3721
|
-
}, [
|
|
3722
|
-
debouncedSetFilterValues,
|
|
3723
|
-
filterableColumns,
|
|
3724
|
-
enableAdvancedFilter
|
|
3725
|
-
]);
|
|
3726
|
-
const table = useReactTable({
|
|
3727
|
-
...tableProps,
|
|
3728
|
-
columns,
|
|
3729
|
-
initialState,
|
|
3730
|
-
pageCount,
|
|
3731
|
-
state: {
|
|
3732
|
-
pagination,
|
|
3733
|
-
sorting,
|
|
3734
|
-
columnVisibility,
|
|
3735
|
-
rowSelection,
|
|
3736
|
-
columnFilters
|
|
3737
|
-
},
|
|
3738
|
-
defaultColumn: {
|
|
3739
|
-
...tableProps.defaultColumn,
|
|
3740
|
-
enableColumnFilter: false
|
|
3741
|
-
},
|
|
3742
|
-
enableRowSelection: true,
|
|
3743
|
-
onRowSelectionChange: setRowSelection,
|
|
3744
|
-
onPaginationChange,
|
|
3745
|
-
onSortingChange,
|
|
3746
|
-
onColumnFiltersChange,
|
|
3747
|
-
onColumnVisibilityChange: setColumnVisibility,
|
|
3748
|
-
getCoreRowModel: getCoreRowModel(),
|
|
3749
|
-
getFilteredRowModel: getFilteredRowModel(),
|
|
3750
|
-
getPaginationRowModel: getPaginationRowModel(),
|
|
3751
|
-
getSortedRowModel: getSortedRowModel(),
|
|
3752
|
-
getFacetedRowModel: getFacetedRowModel(),
|
|
3753
|
-
getFacetedUniqueValues: getFacetedUniqueValues(),
|
|
3754
|
-
getFacetedMinMaxValues: getFacetedMinMaxValues(),
|
|
3755
|
-
manualPagination: true,
|
|
3756
|
-
manualSorting: true,
|
|
3757
|
-
manualFiltering: true,
|
|
3758
|
-
meta: {
|
|
3759
|
-
...tableProps.meta,
|
|
3760
|
-
queryKeys: {
|
|
3761
|
-
page: pageKey,
|
|
3762
|
-
perPage: perPageKey,
|
|
3763
|
-
sort: sortKey,
|
|
3764
|
-
filters: filtersKey,
|
|
3765
|
-
joinOperator: joinOperatorKey
|
|
3766
|
-
},
|
|
3767
|
-
queryStateOptions
|
|
3768
|
-
}
|
|
3769
|
-
});
|
|
3770
|
-
return React.useMemo(() => ({
|
|
3771
|
-
table,
|
|
3772
|
-
shallow,
|
|
3773
|
-
debounceMs,
|
|
3774
|
-
throttleMs
|
|
3775
|
-
}), [
|
|
3776
|
-
table,
|
|
3777
|
-
shallow,
|
|
3778
|
-
debounceMs,
|
|
3779
|
-
throttleMs
|
|
3780
|
-
]);
|
|
3781
|
-
}
|
|
3782
|
-
|
|
3783
|
-
//#endregion
|
|
3784
|
-
//#region src/components/data-table/data-table-column-header.tsx
|
|
3785
|
-
function DataTableColumnHeader({ column, label, className,...props }) {
|
|
3786
|
-
if (!column.getCanSort() && !column.getCanHide()) return /* @__PURE__ */ jsx("div", {
|
|
3787
|
-
className: cn(className),
|
|
3788
|
-
children: label
|
|
3789
|
-
});
|
|
3790
|
-
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsxs(DropdownMenuTrigger, {
|
|
3791
|
-
className: cn("-ml-1.5 flex h-8 items-center gap-1.5 rounded-md px-2 py-1.5 hover:bg-accent focus:outline-none focus:ring-1 focus:ring-ring data-[state=open]:bg-accent [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground", className),
|
|
3792
|
-
...props,
|
|
3793
|
-
children: [label, column.getCanSort() && (column.getIsSorted() === "desc" ? /* @__PURE__ */ jsx(ChevronDown, {}) : column.getIsSorted() === "asc" ? /* @__PURE__ */ jsx(ChevronUp, {}) : /* @__PURE__ */ jsx(ChevronsUpDown, {}))]
|
|
3794
|
-
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
3795
|
-
align: "start",
|
|
3796
|
-
className: "w-28",
|
|
3797
|
-
children: [column.getCanSort() && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3798
|
-
/* @__PURE__ */ jsxs(DropdownMenuCheckboxItem, {
|
|
3799
|
-
className: "relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground",
|
|
3800
|
-
checked: column.getIsSorted() === "asc",
|
|
3801
|
-
onClick: () => column.toggleSorting(false),
|
|
3802
|
-
children: [/* @__PURE__ */ jsx(ChevronUp, {}), "Asc"]
|
|
3803
|
-
}),
|
|
3804
|
-
/* @__PURE__ */ jsxs(DropdownMenuCheckboxItem, {
|
|
3805
|
-
className: "relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground",
|
|
3806
|
-
checked: column.getIsSorted() === "desc",
|
|
3807
|
-
onClick: () => column.toggleSorting(true),
|
|
3808
|
-
children: [/* @__PURE__ */ jsx(ChevronDown, {}), "Desc"]
|
|
3809
|
-
}),
|
|
3810
|
-
column.getIsSorted() && /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
3811
|
-
className: "pl-2 [&_svg]:text-muted-foreground",
|
|
3812
|
-
onClick: () => column.clearSorting(),
|
|
3813
|
-
children: [/* @__PURE__ */ jsx(X, {}), "Reset"]
|
|
3814
|
-
})
|
|
3815
|
-
] }), column.getCanHide() && /* @__PURE__ */ jsxs(DropdownMenuCheckboxItem, {
|
|
3816
|
-
className: "relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground",
|
|
3817
|
-
checked: !column.getIsVisible(),
|
|
3818
|
-
onClick: () => column.toggleVisibility(false),
|
|
3819
|
-
children: [/* @__PURE__ */ jsx(EyeOff, {}), "Hide"]
|
|
3820
|
-
})]
|
|
3821
|
-
})] });
|
|
3822
|
-
}
|
|
3823
|
-
|
|
3824
|
-
//#endregion
|
|
3825
|
-
//#region src/lib/humanize.ts
|
|
3593
|
+
//#region src/lib/humanize.ts
|
|
3826
3594
|
/**
|
|
3827
3595
|
* 将字符串转为人类可读格式
|
|
3828
3596
|
*
|
|
@@ -4119,73 +3887,578 @@ function createTableSchema(schema, options) {
|
|
|
4119
3887
|
meta,
|
|
4120
3888
|
...restOverride
|
|
4121
3889
|
});
|
|
4122
|
-
}
|
|
4123
|
-
return columns;
|
|
4124
|
-
}
|
|
4125
|
-
/**
|
|
4126
|
-
* 创建选择列
|
|
4127
|
-
*/
|
|
4128
|
-
function createSelectColumn() {
|
|
4129
|
-
return {
|
|
4130
|
-
id: "select",
|
|
4131
|
-
header: ({ table }) => /* @__PURE__ */ jsx(Checkbox, {
|
|
4132
|
-
"aria-label": "Select all",
|
|
4133
|
-
className: "translate-y-0.5",
|
|
4134
|
-
checked: table.getIsAllPageRowsSelected() || table.getIsSomePageRowsSelected() && "indeterminate",
|
|
4135
|
-
onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value)
|
|
4136
|
-
}),
|
|
4137
|
-
cell: ({ row }) => /* @__PURE__ */ jsx(Checkbox, {
|
|
4138
|
-
"aria-label": "Select row",
|
|
4139
|
-
className: "translate-y-0.5",
|
|
4140
|
-
checked: row.getIsSelected(),
|
|
4141
|
-
onCheckedChange: (value) => row.toggleSelected(!!value)
|
|
4142
|
-
}),
|
|
4143
|
-
enableHiding: false,
|
|
4144
|
-
enableSorting: false,
|
|
4145
|
-
size: 40
|
|
4146
|
-
};
|
|
4147
|
-
}
|
|
4148
|
-
/**
|
|
4149
|
-
* 创建操作列
|
|
4150
|
-
*/
|
|
4151
|
-
function createActionsColumn(config) {
|
|
4152
|
-
const { onEdit, onDelete, onView, onCopy } = config;
|
|
4153
|
-
return {
|
|
4154
|
-
id: "actions",
|
|
4155
|
-
cell: ({ row }) => /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
4156
|
-
asChild: true,
|
|
4157
|
-
children: /* @__PURE__ */ jsx(Button, {
|
|
4158
|
-
"aria-label": "Open menu",
|
|
4159
|
-
variant: "ghost",
|
|
4160
|
-
className: "flex size-8 p-0 data-[state=open]:bg-muted",
|
|
4161
|
-
children: /* @__PURE__ */ jsx(Ellipsis, { className: "size-4" })
|
|
4162
|
-
})
|
|
4163
|
-
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
4164
|
-
align: "end",
|
|
4165
|
-
className: "w-40",
|
|
4166
|
-
children: [
|
|
4167
|
-
onView && /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
4168
|
-
onClick: () => onView(row.original),
|
|
4169
|
-
children: "查看"
|
|
4170
|
-
}),
|
|
4171
|
-
onEdit && /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
4172
|
-
onClick: () => onEdit(row.original),
|
|
4173
|
-
children: "编辑"
|
|
4174
|
-
}),
|
|
4175
|
-
onCopy && /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
4176
|
-
onClick: () => onCopy(row.original),
|
|
4177
|
-
children: "复制"
|
|
4178
|
-
}),
|
|
4179
|
-
(onView || onEdit || onCopy) && onDelete && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
4180
|
-
onDelete && /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
4181
|
-
className: "text-destructive",
|
|
4182
|
-
onClick: () => onDelete(row.original),
|
|
4183
|
-
children: "删除"
|
|
4184
|
-
})
|
|
4185
|
-
]
|
|
4186
|
-
})] }),
|
|
4187
|
-
size: 40
|
|
4188
|
-
};
|
|
3890
|
+
}
|
|
3891
|
+
return columns;
|
|
3892
|
+
}
|
|
3893
|
+
/**
|
|
3894
|
+
* 创建选择列
|
|
3895
|
+
*/
|
|
3896
|
+
function createSelectColumn() {
|
|
3897
|
+
return {
|
|
3898
|
+
id: "select",
|
|
3899
|
+
header: ({ table }) => /* @__PURE__ */ jsx(Checkbox, {
|
|
3900
|
+
"aria-label": "Select all",
|
|
3901
|
+
className: "translate-y-0.5",
|
|
3902
|
+
checked: table.getIsAllPageRowsSelected() || table.getIsSomePageRowsSelected() && "indeterminate",
|
|
3903
|
+
onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value)
|
|
3904
|
+
}),
|
|
3905
|
+
cell: ({ row }) => /* @__PURE__ */ jsx(Checkbox, {
|
|
3906
|
+
"aria-label": "Select row",
|
|
3907
|
+
className: "translate-y-0.5",
|
|
3908
|
+
checked: row.getIsSelected(),
|
|
3909
|
+
onCheckedChange: (value) => row.toggleSelected(!!value)
|
|
3910
|
+
}),
|
|
3911
|
+
enableHiding: false,
|
|
3912
|
+
enableSorting: false,
|
|
3913
|
+
size: 40
|
|
3914
|
+
};
|
|
3915
|
+
}
|
|
3916
|
+
/**
|
|
3917
|
+
* 创建操作列
|
|
3918
|
+
*/
|
|
3919
|
+
function createActionsColumn(config) {
|
|
3920
|
+
const { onEdit, onDelete, onView, onCopy } = config;
|
|
3921
|
+
return {
|
|
3922
|
+
id: "actions",
|
|
3923
|
+
cell: ({ row }) => /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
3924
|
+
asChild: true,
|
|
3925
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
3926
|
+
"aria-label": "Open menu",
|
|
3927
|
+
variant: "ghost",
|
|
3928
|
+
className: "flex size-8 p-0 data-[state=open]:bg-muted",
|
|
3929
|
+
children: /* @__PURE__ */ jsx(Ellipsis, { className: "size-4" })
|
|
3930
|
+
})
|
|
3931
|
+
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
3932
|
+
align: "end",
|
|
3933
|
+
className: "w-40",
|
|
3934
|
+
children: [
|
|
3935
|
+
onView && /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
3936
|
+
onClick: () => onView(row.original),
|
|
3937
|
+
children: "查看"
|
|
3938
|
+
}),
|
|
3939
|
+
onEdit && /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
3940
|
+
onClick: () => onEdit(row.original),
|
|
3941
|
+
children: "编辑"
|
|
3942
|
+
}),
|
|
3943
|
+
onCopy && /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
3944
|
+
onClick: () => onCopy(row.original),
|
|
3945
|
+
children: "复制"
|
|
3946
|
+
}),
|
|
3947
|
+
(onView || onEdit || onCopy) && onDelete && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
3948
|
+
onDelete && /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
3949
|
+
className: "text-destructive",
|
|
3950
|
+
onClick: () => onDelete(row.original),
|
|
3951
|
+
children: "删除"
|
|
3952
|
+
})
|
|
3953
|
+
]
|
|
3954
|
+
})] }),
|
|
3955
|
+
size: 40
|
|
3956
|
+
};
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
//#endregion
|
|
3960
|
+
//#region src/lib/import.ts
|
|
3961
|
+
/**
|
|
3962
|
+
* 解析 CSV 字符串为对象数组
|
|
3963
|
+
* 支持:引号包裹、逗号转义、换行符、空值
|
|
3964
|
+
*/
|
|
3965
|
+
function parseCSV(content) {
|
|
3966
|
+
const lines = parseCSVLines(content);
|
|
3967
|
+
if (lines.length === 0) return {
|
|
3968
|
+
headers: [],
|
|
3969
|
+
rows: []
|
|
3970
|
+
};
|
|
3971
|
+
const headers = lines[0];
|
|
3972
|
+
const rows = [];
|
|
3973
|
+
for (let i = 1; i < lines.length; i++) {
|
|
3974
|
+
const line = lines[i];
|
|
3975
|
+
if (line.length === 1 && line[0] === "") continue;
|
|
3976
|
+
const row = {};
|
|
3977
|
+
for (let j = 0; j < headers.length; j++) {
|
|
3978
|
+
const header = headers[j];
|
|
3979
|
+
row[header] = line[j] ?? "";
|
|
3980
|
+
}
|
|
3981
|
+
rows.push(row);
|
|
3982
|
+
}
|
|
3983
|
+
return {
|
|
3984
|
+
headers,
|
|
3985
|
+
rows
|
|
3986
|
+
};
|
|
3987
|
+
}
|
|
3988
|
+
/**
|
|
3989
|
+
* 将 CSV 文本解析为二维数组(处理引号转义)
|
|
3990
|
+
*/
|
|
3991
|
+
function parseCSVLines(text) {
|
|
3992
|
+
const result = [];
|
|
3993
|
+
let current = "";
|
|
3994
|
+
let inQuotes = false;
|
|
3995
|
+
let row = [];
|
|
3996
|
+
for (let i = 0; i < text.length; i++) {
|
|
3997
|
+
const char = text[i];
|
|
3998
|
+
const next = text[i + 1];
|
|
3999
|
+
if (inQuotes) if (char === "\"" && next === "\"") {
|
|
4000
|
+
current += "\"";
|
|
4001
|
+
i++;
|
|
4002
|
+
} else if (char === "\"") inQuotes = false;
|
|
4003
|
+
else current += char;
|
|
4004
|
+
else if (char === "\"") inQuotes = true;
|
|
4005
|
+
else if (char === ",") {
|
|
4006
|
+
row.push(current);
|
|
4007
|
+
current = "";
|
|
4008
|
+
} else if (char === "\r" && next === "\n") {
|
|
4009
|
+
row.push(current);
|
|
4010
|
+
current = "";
|
|
4011
|
+
result.push(row);
|
|
4012
|
+
row = [];
|
|
4013
|
+
i++;
|
|
4014
|
+
} else if (char === "\n") {
|
|
4015
|
+
row.push(current);
|
|
4016
|
+
current = "";
|
|
4017
|
+
result.push(row);
|
|
4018
|
+
row = [];
|
|
4019
|
+
} else current += char;
|
|
4020
|
+
}
|
|
4021
|
+
if (current !== "" || row.length > 0) {
|
|
4022
|
+
row.push(current);
|
|
4023
|
+
result.push(row);
|
|
4024
|
+
}
|
|
4025
|
+
return result;
|
|
4026
|
+
}
|
|
4027
|
+
/**
|
|
4028
|
+
* 解析 JSON 字符串为对象数组
|
|
4029
|
+
* 支持 JSON 数组或 { data: [...] } 格式
|
|
4030
|
+
*/
|
|
4031
|
+
function parseJSON(content) {
|
|
4032
|
+
const parsed = JSON.parse(content);
|
|
4033
|
+
if (Array.isArray(parsed)) return parsed;
|
|
4034
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.data)) return parsed.data;
|
|
4035
|
+
throw new Error("Invalid JSON format: expected an array or { data: [...] } object");
|
|
4036
|
+
}
|
|
4037
|
+
/**
|
|
4038
|
+
* 统一解析入口:根据文件扩展名自动选择解析器
|
|
4039
|
+
*/
|
|
4040
|
+
async function parseImportFile(file) {
|
|
4041
|
+
const content = await file.text();
|
|
4042
|
+
if (file.name.split(".").pop()?.toLowerCase() === "json") {
|
|
4043
|
+
const rows$1 = parseJSON(content);
|
|
4044
|
+
return {
|
|
4045
|
+
headers: rows$1.length > 0 ? Object.keys(rows$1[0]) : [],
|
|
4046
|
+
rows: rows$1,
|
|
4047
|
+
format: "json"
|
|
4048
|
+
};
|
|
4049
|
+
}
|
|
4050
|
+
const { headers, rows } = parseCSV(content);
|
|
4051
|
+
return {
|
|
4052
|
+
headers,
|
|
4053
|
+
rows,
|
|
4054
|
+
format: "csv"
|
|
4055
|
+
};
|
|
4056
|
+
}
|
|
4057
|
+
/**
|
|
4058
|
+
* 生成 CSV 模板(仅包含表头,用于下载空模板)
|
|
4059
|
+
*/
|
|
4060
|
+
function generateCSVTemplate(headers) {
|
|
4061
|
+
return headers.map((h) => escapeCSVField(h)).join(",") + "\n";
|
|
4062
|
+
}
|
|
4063
|
+
/**
|
|
4064
|
+
* 将数据数组转换为 CSV 字符串
|
|
4065
|
+
*/
|
|
4066
|
+
function dataToCSV(data, opts = {}) {
|
|
4067
|
+
if (data.length === 0) return "";
|
|
4068
|
+
const allHeaders = Object.keys(data[0]);
|
|
4069
|
+
const headers = (opts.headers ?? allHeaders).filter((h) => !opts.excludeColumns?.includes(h));
|
|
4070
|
+
return [headers.map((h) => escapeCSVField(h)).join(","), ...data.map((row) => headers.map((h) => escapeCSVField(String(row[h] ?? ""))).join(","))].join("\n");
|
|
4071
|
+
}
|
|
4072
|
+
function escapeCSVField(field) {
|
|
4073
|
+
if (field.includes(",") || field.includes("\"") || field.includes("\n") || field.includes("\r")) return `"${field.replace(/"/g, "\"\"")}"`;
|
|
4074
|
+
return field;
|
|
4075
|
+
}
|
|
4076
|
+
/**
|
|
4077
|
+
* 根据 Zod Schema 将 CSV 解析出的 string 值转换为正确类型
|
|
4078
|
+
*
|
|
4079
|
+
* CSV 解析后所有值都是 string,需要根据 schema 字段类型进行转换:
|
|
4080
|
+
* - number: parseFloat
|
|
4081
|
+
* - boolean: "true"/"1"/"yes" → true, 其他 → false
|
|
4082
|
+
* - date: new Date(value)
|
|
4083
|
+
* - 空字符串 → undefined(让 schema 默认值生效)
|
|
4084
|
+
*/
|
|
4085
|
+
function coerceRowValues(rows, schema) {
|
|
4086
|
+
const shape = schema.shape;
|
|
4087
|
+
const fieldTypes = /* @__PURE__ */ new Map();
|
|
4088
|
+
for (const [key, fieldSchema] of Object.entries(shape)) {
|
|
4089
|
+
const parsed = parseZodField(fieldSchema);
|
|
4090
|
+
fieldTypes.set(key, parsed.type);
|
|
4091
|
+
}
|
|
4092
|
+
return rows.map((row) => {
|
|
4093
|
+
const coerced = {};
|
|
4094
|
+
for (const [key, value] of Object.entries(row)) {
|
|
4095
|
+
const type = fieldTypes.get(key);
|
|
4096
|
+
if (!type) {
|
|
4097
|
+
coerced[key] = value;
|
|
4098
|
+
continue;
|
|
4099
|
+
}
|
|
4100
|
+
if (value === "" || value === null || value === void 0) {
|
|
4101
|
+
coerced[key] = void 0;
|
|
4102
|
+
continue;
|
|
4103
|
+
}
|
|
4104
|
+
if (typeof value !== "string") {
|
|
4105
|
+
coerced[key] = value;
|
|
4106
|
+
continue;
|
|
4107
|
+
}
|
|
4108
|
+
switch (type) {
|
|
4109
|
+
case "number": {
|
|
4110
|
+
const num = parseFloat(value);
|
|
4111
|
+
coerced[key] = isNaN(num) ? void 0 : num;
|
|
4112
|
+
break;
|
|
4113
|
+
}
|
|
4114
|
+
case "boolean": {
|
|
4115
|
+
const lower = value.toLowerCase().trim();
|
|
4116
|
+
coerced[key] = lower === "true" || lower === "1" || lower === "yes";
|
|
4117
|
+
break;
|
|
4118
|
+
}
|
|
4119
|
+
case "date": {
|
|
4120
|
+
const date = new Date(value);
|
|
4121
|
+
coerced[key] = isNaN(date.getTime()) ? void 0 : date;
|
|
4122
|
+
break;
|
|
4123
|
+
}
|
|
4124
|
+
default: coerced[key] = value;
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
return coerced;
|
|
4128
|
+
});
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
//#endregion
|
|
4132
|
+
//#region src/lib/export.ts
|
|
4133
|
+
function exportTableToCSV(table, opts = {}) {
|
|
4134
|
+
const { filename = "table", excludeColumns = [], onlySelected = false } = opts;
|
|
4135
|
+
const headers = table.getAllLeafColumns().map((column) => column.id).filter((id) => !excludeColumns.includes(id));
|
|
4136
|
+
downloadCSV([headers.join(","), ...(onlySelected ? table.getFilteredSelectedRowModel().rows : table.getRowModel().rows).map((row) => headers.map((header) => {
|
|
4137
|
+
const cellValue = row.getValue(header);
|
|
4138
|
+
return typeof cellValue === "string" ? `"${cellValue.replace(/"/g, "\"\"")}"` : cellValue;
|
|
4139
|
+
}).join(","))].join("\n"), filename);
|
|
4140
|
+
}
|
|
4141
|
+
/**
|
|
4142
|
+
* 导出原始数据数组为 CSV(用于服务端全量导出)
|
|
4143
|
+
*/
|
|
4144
|
+
function exportAllToCSV(data, opts = {}) {
|
|
4145
|
+
const { filename = "export",...csvOpts } = opts;
|
|
4146
|
+
downloadCSV(dataToCSV(data, csvOpts), filename);
|
|
4147
|
+
}
|
|
4148
|
+
/**
|
|
4149
|
+
* 下载 CSV 模板文件
|
|
4150
|
+
*/
|
|
4151
|
+
function downloadCSVTemplate(headers, filename = "template") {
|
|
4152
|
+
downloadCSV(headers.map((h) => {
|
|
4153
|
+
if (h.includes(",") || h.includes("\"") || h.includes("\n")) return `"${h.replace(/"/g, "\"\"")}"`;
|
|
4154
|
+
return h;
|
|
4155
|
+
}).join(",") + "\n", filename);
|
|
4156
|
+
}
|
|
4157
|
+
function downloadCSV(csvContent, filename) {
|
|
4158
|
+
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
|
4159
|
+
const url = URL.createObjectURL(blob);
|
|
4160
|
+
const link = document.createElement("a");
|
|
4161
|
+
link.setAttribute("href", url);
|
|
4162
|
+
link.setAttribute("download", `${filename}.csv`);
|
|
4163
|
+
link.style.visibility = "hidden";
|
|
4164
|
+
document.body.appendChild(link);
|
|
4165
|
+
link.click();
|
|
4166
|
+
document.body.removeChild(link);
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
//#endregion
|
|
4170
|
+
//#region src/components/auto-crud/auto-table-action-bar.tsx
|
|
4171
|
+
function AutoTableActionBar({ table, onDeleteSelected, onUpdateSelected, batchUpdateFields, enableExport = true, enableDelete = true, extraActions }) {
|
|
4172
|
+
const rows = table.getFilteredSelectedRowModel().rows;
|
|
4173
|
+
const onOpenChange = React.useCallback((open) => {
|
|
4174
|
+
if (!open) table.toggleAllRowsSelected(false);
|
|
4175
|
+
}, [table]);
|
|
4176
|
+
const onExport = React.useCallback(() => {
|
|
4177
|
+
exportTableToCSV(table, {
|
|
4178
|
+
excludeColumns: ["select", "actions"],
|
|
4179
|
+
onlySelected: true
|
|
4180
|
+
});
|
|
4181
|
+
}, [table]);
|
|
4182
|
+
const onDelete = React.useCallback(() => {
|
|
4183
|
+
if (onDeleteSelected) onDeleteSelected(rows.map((row) => row.original));
|
|
4184
|
+
}, [rows, onDeleteSelected]);
|
|
4185
|
+
const handleBatchUpdate = React.useCallback((field, value) => {
|
|
4186
|
+
if (onUpdateSelected) {
|
|
4187
|
+
onUpdateSelected(rows.map((row) => row.original), { [field]: value });
|
|
4188
|
+
table.toggleAllRowsSelected(false);
|
|
4189
|
+
}
|
|
4190
|
+
}, [
|
|
4191
|
+
rows,
|
|
4192
|
+
onUpdateSelected,
|
|
4193
|
+
table
|
|
4194
|
+
]);
|
|
4195
|
+
return /* @__PURE__ */ jsxs(ActionBar, {
|
|
4196
|
+
open: rows.length > 0,
|
|
4197
|
+
onOpenChange,
|
|
4198
|
+
children: [
|
|
4199
|
+
/* @__PURE__ */ jsxs(ActionBarSelection, { children: [
|
|
4200
|
+
/* @__PURE__ */ jsx("span", {
|
|
4201
|
+
className: "font-medium",
|
|
4202
|
+
children: rows.length
|
|
4203
|
+
}),
|
|
4204
|
+
/* @__PURE__ */ jsx("span", { children: "selected" }),
|
|
4205
|
+
/* @__PURE__ */ jsx(ActionBarSeparator, {}),
|
|
4206
|
+
/* @__PURE__ */ jsx(ActionBarClose, { children: /* @__PURE__ */ jsx(X, {}) })
|
|
4207
|
+
] }),
|
|
4208
|
+
/* @__PURE__ */ jsx(ActionBarSeparator, {}),
|
|
4209
|
+
/* @__PURE__ */ jsxs(ActionBarGroup, { children: [
|
|
4210
|
+
batchUpdateFields?.map((fieldConfig) => /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
4211
|
+
asChild: true,
|
|
4212
|
+
children: /* @__PURE__ */ jsxs(Button, {
|
|
4213
|
+
variant: "ghost",
|
|
4214
|
+
size: "sm",
|
|
4215
|
+
className: "h-7 gap-1 px-2",
|
|
4216
|
+
children: [fieldConfig.label, /* @__PURE__ */ jsx(ChevronDown, { className: "size-3.5" })]
|
|
4217
|
+
})
|
|
4218
|
+
}), /* @__PURE__ */ jsx(DropdownMenuContent, {
|
|
4219
|
+
align: "start",
|
|
4220
|
+
children: fieldConfig.options.map((option) => /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
4221
|
+
onClick: () => handleBatchUpdate(fieldConfig.field, option.value),
|
|
4222
|
+
children: option.label
|
|
4223
|
+
}, option.value))
|
|
4224
|
+
})] }, fieldConfig.field)),
|
|
4225
|
+
extraActions,
|
|
4226
|
+
enableExport && /* @__PURE__ */ jsxs(ActionBarItem, {
|
|
4227
|
+
onClick: onExport,
|
|
4228
|
+
children: [/* @__PURE__ */ jsx(Download, {}), "Export"]
|
|
4229
|
+
}),
|
|
4230
|
+
enableDelete && onDeleteSelected && /* @__PURE__ */ jsxs(ActionBarItem, {
|
|
4231
|
+
variant: "destructive",
|
|
4232
|
+
onClick: onDelete,
|
|
4233
|
+
children: [/* @__PURE__ */ jsx(Trash2, {}), "Delete"]
|
|
4234
|
+
})
|
|
4235
|
+
] })
|
|
4236
|
+
]
|
|
4237
|
+
});
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
//#endregion
|
|
4241
|
+
//#region src/lib/parsers.ts
|
|
4242
|
+
/**
|
|
4243
|
+
* 创建自定义 Parser(替代 nuqs 的 createParser)
|
|
4244
|
+
*/
|
|
4245
|
+
function createParser(config) {
|
|
4246
|
+
return {
|
|
4247
|
+
parse: config.parse,
|
|
4248
|
+
serialize: (value) => value === null ? "" : config.serialize(value),
|
|
4249
|
+
withDefault: (defaultValue) => ({
|
|
4250
|
+
parse: (value) => {
|
|
4251
|
+
const parsed = config.parse(value);
|
|
4252
|
+
return parsed === null ? defaultValue : parsed;
|
|
4253
|
+
},
|
|
4254
|
+
serialize: config.serialize,
|
|
4255
|
+
defaultValue
|
|
4256
|
+
})
|
|
4257
|
+
};
|
|
4258
|
+
}
|
|
4259
|
+
const getSortingStateParser = (columnIds) => {
|
|
4260
|
+
const validKeys = columnIds ? columnIds instanceof Set ? columnIds : new Set(columnIds) : null;
|
|
4261
|
+
return createParser({
|
|
4262
|
+
parse: (value) => {
|
|
4263
|
+
try {
|
|
4264
|
+
if (!value || value.trim() === "") return [];
|
|
4265
|
+
const items = value.split(",").map((item) => {
|
|
4266
|
+
const lastDot = item.lastIndexOf(".");
|
|
4267
|
+
if (lastDot === -1) return null;
|
|
4268
|
+
const id = item.slice(0, lastDot);
|
|
4269
|
+
const dir = item.slice(lastDot + 1);
|
|
4270
|
+
if (!id || dir !== "asc" && dir !== "desc") return null;
|
|
4271
|
+
return {
|
|
4272
|
+
id,
|
|
4273
|
+
desc: dir === "desc"
|
|
4274
|
+
};
|
|
4275
|
+
});
|
|
4276
|
+
if (items.some((item) => item === null)) return null;
|
|
4277
|
+
const result = items;
|
|
4278
|
+
if (validKeys && result.some((item) => !validKeys.has(item.id))) return null;
|
|
4279
|
+
return result;
|
|
4280
|
+
} catch {
|
|
4281
|
+
return null;
|
|
4282
|
+
}
|
|
4283
|
+
},
|
|
4284
|
+
serialize: (value) => value.map((item) => `${item.id}.${item.desc ? "desc" : "asc"}`).join(",")
|
|
4285
|
+
});
|
|
4286
|
+
};
|
|
4287
|
+
const filterItemSchema = z.object({
|
|
4288
|
+
id: z.string(),
|
|
4289
|
+
value: z.union([z.string(), z.array(z.string())]),
|
|
4290
|
+
variant: z.enum(dataTableConfig.filterVariants),
|
|
4291
|
+
operator: z.enum(dataTableConfig.operators),
|
|
4292
|
+
filterId: z.string()
|
|
4293
|
+
});
|
|
4294
|
+
|
|
4295
|
+
//#endregion
|
|
4296
|
+
//#region src/hooks/use-data-table.ts
|
|
4297
|
+
const PAGE_KEY = "page";
|
|
4298
|
+
const PER_PAGE_KEY = "perPage";
|
|
4299
|
+
const SORT_KEY = "sort";
|
|
4300
|
+
const FILTERS_KEY = "filters";
|
|
4301
|
+
const JOIN_OPERATOR_KEY = "joinOperator";
|
|
4302
|
+
const ARRAY_SEPARATOR = ",";
|
|
4303
|
+
const DEBOUNCE_MS = 300;
|
|
4304
|
+
const THROTTLE_MS = 50;
|
|
4305
|
+
function useDataTable(props) {
|
|
4306
|
+
const { columns, pageCount = -1, initialState, queryKeys, history = "replace", debounceMs = DEBOUNCE_MS, throttleMs = THROTTLE_MS, clearOnDefault = false, enableAdvancedFilter = false, scroll = false, shallow = true, startTransition,...tableProps } = props;
|
|
4307
|
+
const pageKey = queryKeys?.page ?? PAGE_KEY;
|
|
4308
|
+
const perPageKey = queryKeys?.perPage ?? PER_PAGE_KEY;
|
|
4309
|
+
const sortKey = queryKeys?.sort ?? SORT_KEY;
|
|
4310
|
+
const filtersKey = queryKeys?.filters ?? FILTERS_KEY;
|
|
4311
|
+
const joinOperatorKey = queryKeys?.joinOperator ?? JOIN_OPERATOR_KEY;
|
|
4312
|
+
const queryStateOptions = React.useMemo(() => ({
|
|
4313
|
+
history,
|
|
4314
|
+
scroll,
|
|
4315
|
+
shallow,
|
|
4316
|
+
throttleMs,
|
|
4317
|
+
debounceMs,
|
|
4318
|
+
clearOnDefault,
|
|
4319
|
+
startTransition
|
|
4320
|
+
}), [
|
|
4321
|
+
history,
|
|
4322
|
+
scroll,
|
|
4323
|
+
shallow,
|
|
4324
|
+
throttleMs,
|
|
4325
|
+
debounceMs,
|
|
4326
|
+
clearOnDefault,
|
|
4327
|
+
startTransition
|
|
4328
|
+
]);
|
|
4329
|
+
const [rowSelection, setRowSelection] = React.useState(initialState?.rowSelection ?? {});
|
|
4330
|
+
const [columnVisibility, setColumnVisibility] = React.useState(initialState?.columnVisibility ?? {});
|
|
4331
|
+
const [page, setPage] = useQueryState(pageKey, parseAsInteger.withDefault(1));
|
|
4332
|
+
const [perPage, setPerPage] = useQueryState(perPageKey, parseAsInteger.withDefault(initialState?.pagination?.pageSize ?? 10));
|
|
4333
|
+
const pagination = React.useMemo(() => {
|
|
4334
|
+
return {
|
|
4335
|
+
pageIndex: page - 1,
|
|
4336
|
+
pageSize: perPage
|
|
4337
|
+
};
|
|
4338
|
+
}, [page, perPage]);
|
|
4339
|
+
const onPaginationChange = React.useCallback((updaterOrValue) => {
|
|
4340
|
+
if (typeof updaterOrValue === "function") {
|
|
4341
|
+
const newPagination = updaterOrValue(pagination);
|
|
4342
|
+
setPage(newPagination.pageIndex + 1);
|
|
4343
|
+
setPerPage(newPagination.pageSize);
|
|
4344
|
+
} else {
|
|
4345
|
+
setPage(updaterOrValue.pageIndex + 1);
|
|
4346
|
+
setPerPage(updaterOrValue.pageSize);
|
|
4347
|
+
}
|
|
4348
|
+
}, [
|
|
4349
|
+
pagination,
|
|
4350
|
+
setPage,
|
|
4351
|
+
setPerPage
|
|
4352
|
+
]);
|
|
4353
|
+
const [sorting, setSorting] = useQueryState(sortKey, getSortingStateParser(React.useMemo(() => {
|
|
4354
|
+
return new Set(columns.map((column) => column.id).filter(Boolean));
|
|
4355
|
+
}, [columns])).withDefault(initialState?.sorting ?? []));
|
|
4356
|
+
const onSortingChange = React.useCallback((updaterOrValue) => {
|
|
4357
|
+
if (typeof updaterOrValue === "function") setSorting(updaterOrValue(sorting));
|
|
4358
|
+
else setSorting(updaterOrValue);
|
|
4359
|
+
}, [sorting, setSorting]);
|
|
4360
|
+
const filterableColumns = React.useMemo(() => {
|
|
4361
|
+
if (enableAdvancedFilter) return [];
|
|
4362
|
+
return columns.filter((column) => column.enableColumnFilter);
|
|
4363
|
+
}, [columns, enableAdvancedFilter]);
|
|
4364
|
+
const [filterValues, setFilterValues] = useQueryStates(React.useMemo(() => {
|
|
4365
|
+
if (enableAdvancedFilter) return {};
|
|
4366
|
+
return filterableColumns.reduce((acc, column) => {
|
|
4367
|
+
if (column.meta?.options) acc[column.id ?? ""] = parseAsArrayOf(parseAsString, ARRAY_SEPARATOR).withDefault([]);
|
|
4368
|
+
else acc[column.id ?? ""] = parseAsString.withDefault("");
|
|
4369
|
+
return acc;
|
|
4370
|
+
}, {});
|
|
4371
|
+
}, [filterableColumns, enableAdvancedFilter]));
|
|
4372
|
+
const debouncedSetFilterValues = useDebouncedCallback((values) => {
|
|
4373
|
+
setPage(1);
|
|
4374
|
+
setFilterValues(values);
|
|
4375
|
+
}, debounceMs);
|
|
4376
|
+
const initialColumnFilters = React.useMemo(() => {
|
|
4377
|
+
if (enableAdvancedFilter) return [];
|
|
4378
|
+
return Object.entries(filterValues).reduce((filters, [key, value]) => {
|
|
4379
|
+
if (value !== null) {
|
|
4380
|
+
const processedValue = Array.isArray(value) ? value : typeof value === "string" && /[^a-zA-Z0-9]/.test(value) ? value.split(/[^a-zA-Z0-9]+/).filter(Boolean) : [value];
|
|
4381
|
+
filters.push({
|
|
4382
|
+
id: key,
|
|
4383
|
+
value: processedValue
|
|
4384
|
+
});
|
|
4385
|
+
}
|
|
4386
|
+
return filters;
|
|
4387
|
+
}, []);
|
|
4388
|
+
}, [filterValues, enableAdvancedFilter]);
|
|
4389
|
+
const [columnFilters, setColumnFilters] = React.useState(initialColumnFilters);
|
|
4390
|
+
const onColumnFiltersChange = React.useCallback((updaterOrValue) => {
|
|
4391
|
+
if (enableAdvancedFilter) return;
|
|
4392
|
+
setColumnFilters((prev) => {
|
|
4393
|
+
const next = typeof updaterOrValue === "function" ? updaterOrValue(prev) : updaterOrValue;
|
|
4394
|
+
const filterUpdates = next.reduce((acc, filter) => {
|
|
4395
|
+
if (filterableColumns.find((column) => column.id === filter.id)) acc[filter.id] = filter.value;
|
|
4396
|
+
return acc;
|
|
4397
|
+
}, {});
|
|
4398
|
+
for (const prevFilter of prev) if (!next.some((filter) => filter.id === prevFilter.id)) filterUpdates[prevFilter.id] = null;
|
|
4399
|
+
debouncedSetFilterValues(filterUpdates);
|
|
4400
|
+
return next;
|
|
4401
|
+
});
|
|
4402
|
+
}, [
|
|
4403
|
+
debouncedSetFilterValues,
|
|
4404
|
+
filterableColumns,
|
|
4405
|
+
enableAdvancedFilter
|
|
4406
|
+
]);
|
|
4407
|
+
const table = useReactTable({
|
|
4408
|
+
...tableProps,
|
|
4409
|
+
columns,
|
|
4410
|
+
initialState,
|
|
4411
|
+
pageCount,
|
|
4412
|
+
state: {
|
|
4413
|
+
pagination,
|
|
4414
|
+
sorting,
|
|
4415
|
+
columnVisibility,
|
|
4416
|
+
rowSelection,
|
|
4417
|
+
columnFilters
|
|
4418
|
+
},
|
|
4419
|
+
defaultColumn: {
|
|
4420
|
+
...tableProps.defaultColumn,
|
|
4421
|
+
enableColumnFilter: false
|
|
4422
|
+
},
|
|
4423
|
+
enableRowSelection: true,
|
|
4424
|
+
onRowSelectionChange: setRowSelection,
|
|
4425
|
+
onPaginationChange,
|
|
4426
|
+
onSortingChange,
|
|
4427
|
+
onColumnFiltersChange,
|
|
4428
|
+
onColumnVisibilityChange: setColumnVisibility,
|
|
4429
|
+
getCoreRowModel: getCoreRowModel(),
|
|
4430
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
4431
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
4432
|
+
getSortedRowModel: getSortedRowModel(),
|
|
4433
|
+
getFacetedRowModel: getFacetedRowModel(),
|
|
4434
|
+
getFacetedUniqueValues: getFacetedUniqueValues(),
|
|
4435
|
+
getFacetedMinMaxValues: getFacetedMinMaxValues(),
|
|
4436
|
+
manualPagination: true,
|
|
4437
|
+
manualSorting: true,
|
|
4438
|
+
manualFiltering: true,
|
|
4439
|
+
meta: {
|
|
4440
|
+
...tableProps.meta,
|
|
4441
|
+
queryKeys: {
|
|
4442
|
+
page: pageKey,
|
|
4443
|
+
perPage: perPageKey,
|
|
4444
|
+
sort: sortKey,
|
|
4445
|
+
filters: filtersKey,
|
|
4446
|
+
joinOperator: joinOperatorKey
|
|
4447
|
+
},
|
|
4448
|
+
queryStateOptions
|
|
4449
|
+
}
|
|
4450
|
+
});
|
|
4451
|
+
return React.useMemo(() => ({
|
|
4452
|
+
table,
|
|
4453
|
+
shallow,
|
|
4454
|
+
debounceMs,
|
|
4455
|
+
throttleMs
|
|
4456
|
+
}), [
|
|
4457
|
+
table,
|
|
4458
|
+
shallow,
|
|
4459
|
+
debounceMs,
|
|
4460
|
+
throttleMs
|
|
4461
|
+
]);
|
|
4189
4462
|
}
|
|
4190
4463
|
|
|
4191
4464
|
//#endregion
|
|
@@ -4208,13 +4481,13 @@ const filterModeConfig = {
|
|
|
4208
4481
|
tooltip: "Linear-style command filters"
|
|
4209
4482
|
}
|
|
4210
4483
|
};
|
|
4211
|
-
function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection = true, exclude, actions, pinnedColumns, filterMode = "simple", onDeleteSelected, onUpdateSelected, batchUpdateFields, actionBarExtra, initialSorting, enableExport = true }) {
|
|
4484
|
+
function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection = true, exclude, actions, pinnedColumns, filterMode = "simple", onDeleteSelected, onUpdateSelected, batchUpdateFields, actionBarExtra, initialSorting, enableExport = true, onSelectedCountChange, getSelectedRows }) {
|
|
4212
4485
|
const modes = filterMode ? Array.isArray(filterMode) ? filterMode : [filterMode] : [
|
|
4213
4486
|
"simple",
|
|
4214
4487
|
"advanced",
|
|
4215
4488
|
"command"
|
|
4216
4489
|
];
|
|
4217
|
-
const [currentMode, setCurrentMode] =
|
|
4490
|
+
const [currentMode, setCurrentMode] = useQueryState("filterMode", parseAsStringEnum(modes).withDefault(modes[0] ?? "simple"));
|
|
4218
4491
|
const showToggle = modes.length > 1;
|
|
4219
4492
|
const { table, shallow, debounceMs, throttleMs } = useDataTable({
|
|
4220
4493
|
data,
|
|
@@ -4245,6 +4518,17 @@ function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection
|
|
|
4245
4518
|
shallow: false,
|
|
4246
4519
|
clearOnDefault: true
|
|
4247
4520
|
});
|
|
4521
|
+
const getSelectedRowsFn = useCallback(() => table.getFilteredSelectedRowModel().rows.map((row) => row.original), [table]);
|
|
4522
|
+
useEffect(() => {
|
|
4523
|
+
if (getSelectedRows) getSelectedRows.current = getSelectedRowsFn;
|
|
4524
|
+
return () => {
|
|
4525
|
+
if (getSelectedRows) getSelectedRows.current = null;
|
|
4526
|
+
};
|
|
4527
|
+
}, [getSelectedRows, getSelectedRowsFn]);
|
|
4528
|
+
const selectedRowCount = table.getFilteredSelectedRowModel().rows.length;
|
|
4529
|
+
useEffect(() => {
|
|
4530
|
+
onSelectedCountChange?.(selectedRowCount);
|
|
4531
|
+
}, [selectedRowCount, onSelectedCountChange]);
|
|
4248
4532
|
const FilterModeSelect = showToggle ? /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
4249
4533
|
asChild: true,
|
|
4250
4534
|
children: /* @__PURE__ */ jsx(Button, {
|
|
@@ -4298,7 +4582,8 @@ function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection
|
|
|
4298
4582
|
/* @__PURE__ */ jsxs("div", {
|
|
4299
4583
|
className: "flex w-full items-start justify-between gap-2 p-1",
|
|
4300
4584
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
4301
|
-
className: "flex flex-1
|
|
4585
|
+
className: "flex flex-1 items-start gap-2 min-h-[40px]",
|
|
4586
|
+
"data-filter-parent": true,
|
|
4302
4587
|
children: [currentMode !== "simple" && /* @__PURE__ */ jsx(DataTableSortList, {
|
|
4303
4588
|
table,
|
|
4304
4589
|
align: "start"
|
|
@@ -4576,19 +4861,342 @@ function CrudFormModal({ open, onOpenChange, mode, schema, initialValues, onSubm
|
|
|
4576
4861
|
});
|
|
4577
4862
|
}
|
|
4578
4863
|
|
|
4864
|
+
//#endregion
|
|
4865
|
+
//#region src/components/auto-crud/import-dialog.tsx
|
|
4866
|
+
function ImportDialog({ open, onOpenChange, onImport, columns = [], title = "导入数据" }) {
|
|
4867
|
+
const [step, setStep] = React.useState("upload");
|
|
4868
|
+
const [parsedData, setParsedData] = React.useState(null);
|
|
4869
|
+
const [error, setError] = React.useState(null);
|
|
4870
|
+
const [result, setResult] = React.useState(null);
|
|
4871
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
4872
|
+
const fileInputRef = React.useRef(null);
|
|
4873
|
+
const reset = React.useCallback(() => {
|
|
4874
|
+
setStep("upload");
|
|
4875
|
+
setParsedData(null);
|
|
4876
|
+
setError(null);
|
|
4877
|
+
setResult(null);
|
|
4878
|
+
setIsDragging(false);
|
|
4879
|
+
}, []);
|
|
4880
|
+
const handleOpenChange = React.useCallback((value) => {
|
|
4881
|
+
if (!value) reset();
|
|
4882
|
+
onOpenChange(value);
|
|
4883
|
+
}, [onOpenChange, reset]);
|
|
4884
|
+
const handleFile = React.useCallback(async (file) => {
|
|
4885
|
+
setError(null);
|
|
4886
|
+
try {
|
|
4887
|
+
const data = await parseImportFile(file);
|
|
4888
|
+
if (data.rows.length === 0) {
|
|
4889
|
+
setError("文件中没有数据行");
|
|
4890
|
+
return;
|
|
4891
|
+
}
|
|
4892
|
+
setParsedData(data);
|
|
4893
|
+
setStep("preview");
|
|
4894
|
+
} catch (e) {
|
|
4895
|
+
setError(e instanceof Error ? e.message : "文件解析失败");
|
|
4896
|
+
}
|
|
4897
|
+
}, []);
|
|
4898
|
+
const handleDrop = React.useCallback((e) => {
|
|
4899
|
+
e.preventDefault();
|
|
4900
|
+
setIsDragging(false);
|
|
4901
|
+
const file = e.dataTransfer.files[0];
|
|
4902
|
+
if (file) handleFile(file);
|
|
4903
|
+
}, [handleFile]);
|
|
4904
|
+
const handleFileInput = React.useCallback((e) => {
|
|
4905
|
+
const file = e.target.files?.[0];
|
|
4906
|
+
if (file) handleFile(file);
|
|
4907
|
+
}, [handleFile]);
|
|
4908
|
+
const handleImport = React.useCallback(async () => {
|
|
4909
|
+
if (!parsedData) return;
|
|
4910
|
+
setStep("importing");
|
|
4911
|
+
try {
|
|
4912
|
+
setResult(await onImport(parsedData.rows));
|
|
4913
|
+
setStep("result");
|
|
4914
|
+
} catch (e) {
|
|
4915
|
+
setError(e instanceof Error ? e.message : "导入失败");
|
|
4916
|
+
setStep("preview");
|
|
4917
|
+
}
|
|
4918
|
+
}, [parsedData, onImport]);
|
|
4919
|
+
const handleDownloadTemplate = React.useCallback(() => {
|
|
4920
|
+
if (columns.length === 0) return;
|
|
4921
|
+
const csv = generateCSVTemplate(columns);
|
|
4922
|
+
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
|
4923
|
+
const url = URL.createObjectURL(blob);
|
|
4924
|
+
const link = document.createElement("a");
|
|
4925
|
+
link.href = url;
|
|
4926
|
+
link.download = "import-template.csv";
|
|
4927
|
+
link.click();
|
|
4928
|
+
URL.revokeObjectURL(url);
|
|
4929
|
+
}, [columns]);
|
|
4930
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
4931
|
+
open,
|
|
4932
|
+
onOpenChange: handleOpenChange,
|
|
4933
|
+
children: /* @__PURE__ */ jsxs(DialogContent, {
|
|
4934
|
+
className: "sm:max-w-[600px]",
|
|
4935
|
+
children: [/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsx(DialogTitle, { children: title }), /* @__PURE__ */ jsxs(DialogDescription, { children: [
|
|
4936
|
+
step === "upload" && "上传 CSV 或 JSON 文件以批量导入数据",
|
|
4937
|
+
step === "preview" && `已解析 ${parsedData?.rows.length ?? 0} 条数据,确认后开始导入`,
|
|
4938
|
+
step === "importing" && "正在导入数据...",
|
|
4939
|
+
step === "result" && "导入完成"
|
|
4940
|
+
] })] }), /* @__PURE__ */ jsxs("div", {
|
|
4941
|
+
className: "space-y-4",
|
|
4942
|
+
children: [
|
|
4943
|
+
step === "upload" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
4944
|
+
className: `border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${isDragging ? "border-primary bg-primary/5" : "border-muted-foreground/25 hover:border-muted-foreground/50"}`,
|
|
4945
|
+
onDragOver: (e) => {
|
|
4946
|
+
e.preventDefault();
|
|
4947
|
+
setIsDragging(true);
|
|
4948
|
+
},
|
|
4949
|
+
onDragLeave: () => setIsDragging(false),
|
|
4950
|
+
onDrop: handleDrop,
|
|
4951
|
+
onClick: () => fileInputRef.current?.click(),
|
|
4952
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
4953
|
+
className: "flex flex-col items-center gap-2",
|
|
4954
|
+
children: [
|
|
4955
|
+
/* @__PURE__ */ jsx("svg", {
|
|
4956
|
+
className: "h-10 w-10 text-muted-foreground",
|
|
4957
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
4958
|
+
fill: "none",
|
|
4959
|
+
viewBox: "0 0 24 24",
|
|
4960
|
+
strokeWidth: 1.5,
|
|
4961
|
+
stroke: "currentColor",
|
|
4962
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
4963
|
+
strokeLinecap: "round",
|
|
4964
|
+
strokeLinejoin: "round",
|
|
4965
|
+
d: "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
|
|
4966
|
+
})
|
|
4967
|
+
}),
|
|
4968
|
+
/* @__PURE__ */ jsx("p", {
|
|
4969
|
+
className: "text-sm text-muted-foreground",
|
|
4970
|
+
children: "拖拽文件到此处,或点击选择文件"
|
|
4971
|
+
}),
|
|
4972
|
+
/* @__PURE__ */ jsx("p", {
|
|
4973
|
+
className: "text-xs text-muted-foreground/60",
|
|
4974
|
+
children: "支持 CSV、JSON 格式"
|
|
4975
|
+
})
|
|
4976
|
+
]
|
|
4977
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
4978
|
+
ref: fileInputRef,
|
|
4979
|
+
type: "file",
|
|
4980
|
+
accept: ".csv,.json",
|
|
4981
|
+
className: "hidden",
|
|
4982
|
+
onChange: handleFileInput
|
|
4983
|
+
})]
|
|
4984
|
+
}), columns.length > 0 && /* @__PURE__ */ jsx(Button, {
|
|
4985
|
+
variant: "outline",
|
|
4986
|
+
size: "sm",
|
|
4987
|
+
className: "w-full",
|
|
4988
|
+
onClick: handleDownloadTemplate,
|
|
4989
|
+
children: "下载 CSV 模板"
|
|
4990
|
+
})] }),
|
|
4991
|
+
step === "preview" && parsedData && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4992
|
+
/* @__PURE__ */ jsx("div", {
|
|
4993
|
+
className: "rounded-md border",
|
|
4994
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
4995
|
+
className: "max-h-[300px] overflow-auto",
|
|
4996
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
4997
|
+
className: "w-full text-sm",
|
|
4998
|
+
children: [/* @__PURE__ */ jsx("thead", {
|
|
4999
|
+
className: "sticky top-0 bg-muted",
|
|
5000
|
+
children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
5001
|
+
/* @__PURE__ */ jsx("th", {
|
|
5002
|
+
className: "px-3 py-2 text-left font-medium text-muted-foreground",
|
|
5003
|
+
children: "#"
|
|
5004
|
+
}),
|
|
5005
|
+
parsedData.headers.slice(0, 6).map((h) => /* @__PURE__ */ jsx("th", {
|
|
5006
|
+
className: "px-3 py-2 text-left font-medium text-muted-foreground",
|
|
5007
|
+
children: h
|
|
5008
|
+
}, h)),
|
|
5009
|
+
parsedData.headers.length > 6 && /* @__PURE__ */ jsxs("th", {
|
|
5010
|
+
className: "px-3 py-2 text-left font-medium text-muted-foreground",
|
|
5011
|
+
children: [
|
|
5012
|
+
"+",
|
|
5013
|
+
parsedData.headers.length - 6,
|
|
5014
|
+
" 列"
|
|
5015
|
+
]
|
|
5016
|
+
})
|
|
5017
|
+
] })
|
|
5018
|
+
}), /* @__PURE__ */ jsx("tbody", { children: parsedData.rows.slice(0, 10).map((row, i) => /* @__PURE__ */ jsxs("tr", {
|
|
5019
|
+
className: "border-t",
|
|
5020
|
+
children: [
|
|
5021
|
+
/* @__PURE__ */ jsx("td", {
|
|
5022
|
+
className: "px-3 py-2 text-muted-foreground",
|
|
5023
|
+
children: i + 1
|
|
5024
|
+
}),
|
|
5025
|
+
parsedData.headers.slice(0, 6).map((h) => /* @__PURE__ */ jsx("td", {
|
|
5026
|
+
className: "px-3 py-2 max-w-[150px] truncate",
|
|
5027
|
+
children: String(row[h] ?? "")
|
|
5028
|
+
}, h)),
|
|
5029
|
+
parsedData.headers.length > 6 && /* @__PURE__ */ jsx("td", {
|
|
5030
|
+
className: "px-3 py-2 text-muted-foreground",
|
|
5031
|
+
children: "..."
|
|
5032
|
+
})
|
|
5033
|
+
]
|
|
5034
|
+
}, i)) })]
|
|
5035
|
+
})
|
|
5036
|
+
})
|
|
5037
|
+
}),
|
|
5038
|
+
/* @__PURE__ */ jsxs("p", {
|
|
5039
|
+
className: "text-sm text-muted-foreground",
|
|
5040
|
+
children: [
|
|
5041
|
+
"共 ",
|
|
5042
|
+
parsedData.rows.length,
|
|
5043
|
+
" 条数据",
|
|
5044
|
+
parsedData.rows.length > 10 && "(预览前 10 条)",
|
|
5045
|
+
",",
|
|
5046
|
+
parsedData.headers.length,
|
|
5047
|
+
" 个字段 ,格式: ",
|
|
5048
|
+
parsedData.format.toUpperCase()
|
|
5049
|
+
]
|
|
5050
|
+
}),
|
|
5051
|
+
/* @__PURE__ */ jsxs("div", {
|
|
5052
|
+
className: "flex gap-2 justify-end",
|
|
5053
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
5054
|
+
variant: "outline",
|
|
5055
|
+
onClick: reset,
|
|
5056
|
+
children: "重新选择"
|
|
5057
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
5058
|
+
onClick: handleImport,
|
|
5059
|
+
children: "确认导入"
|
|
5060
|
+
})]
|
|
5061
|
+
})
|
|
5062
|
+
] }),
|
|
5063
|
+
step === "importing" && /* @__PURE__ */ jsxs("div", {
|
|
5064
|
+
className: "flex flex-col items-center gap-4 py-8",
|
|
5065
|
+
children: [/* @__PURE__ */ jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" }), /* @__PURE__ */ jsxs("p", {
|
|
5066
|
+
className: "text-sm text-muted-foreground",
|
|
5067
|
+
children: [
|
|
5068
|
+
"正在导入 ",
|
|
5069
|
+
parsedData?.rows.length ?? 0,
|
|
5070
|
+
" 条数据..."
|
|
5071
|
+
]
|
|
5072
|
+
})]
|
|
5073
|
+
}),
|
|
5074
|
+
step === "result" && result && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
5075
|
+
className: "space-y-3",
|
|
5076
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
5077
|
+
className: "flex gap-4",
|
|
5078
|
+
children: [
|
|
5079
|
+
/* @__PURE__ */ jsxs("div", {
|
|
5080
|
+
className: "flex-1 rounded-lg bg-green-50 dark:bg-green-950/30 p-3 text-center",
|
|
5081
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
5082
|
+
className: "text-2xl font-semibold text-green-600",
|
|
5083
|
+
children: result.success
|
|
5084
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
5085
|
+
className: "text-xs text-green-600/80",
|
|
5086
|
+
children: "新建"
|
|
5087
|
+
})]
|
|
5088
|
+
}),
|
|
5089
|
+
(result.updated ?? 0) > 0 && /* @__PURE__ */ jsxs("div", {
|
|
5090
|
+
className: "flex-1 rounded-lg bg-blue-50 dark:bg-blue-950/30 p-3 text-center",
|
|
5091
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
5092
|
+
className: "text-2xl font-semibold text-blue-600",
|
|
5093
|
+
children: result.updated
|
|
5094
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
5095
|
+
className: "text-xs text-blue-600/80",
|
|
5096
|
+
children: "更新"
|
|
5097
|
+
})]
|
|
5098
|
+
}),
|
|
5099
|
+
result.skipped > 0 && /* @__PURE__ */ jsxs("div", {
|
|
5100
|
+
className: "flex-1 rounded-lg bg-yellow-50 dark:bg-yellow-950/30 p-3 text-center",
|
|
5101
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
5102
|
+
className: "text-2xl font-semibold text-yellow-600",
|
|
5103
|
+
children: result.skipped
|
|
5104
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
5105
|
+
className: "text-xs text-yellow-600/80",
|
|
5106
|
+
children: "跳过"
|
|
5107
|
+
})]
|
|
5108
|
+
}),
|
|
5109
|
+
result.failed.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
5110
|
+
className: "flex-1 rounded-lg bg-red-50 dark:bg-red-950/30 p-3 text-center",
|
|
5111
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
5112
|
+
className: "text-2xl font-semibold text-red-600",
|
|
5113
|
+
children: result.failed.length
|
|
5114
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
5115
|
+
className: "text-xs text-red-600/80",
|
|
5116
|
+
children: "验证失败"
|
|
5117
|
+
})]
|
|
5118
|
+
})
|
|
5119
|
+
]
|
|
5120
|
+
}), result.failed.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
5121
|
+
className: "rounded-md border border-red-200 dark:border-red-900",
|
|
5122
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
5123
|
+
className: "max-h-[200px] overflow-auto",
|
|
5124
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
5125
|
+
className: "w-full text-sm",
|
|
5126
|
+
children: [/* @__PURE__ */ jsx("thead", {
|
|
5127
|
+
className: "sticky top-0 bg-red-50 dark:bg-red-950/30",
|
|
5128
|
+
children: /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("th", {
|
|
5129
|
+
className: "px-3 py-2 text-left font-medium text-red-600",
|
|
5130
|
+
children: "行号"
|
|
5131
|
+
}), /* @__PURE__ */ jsx("th", {
|
|
5132
|
+
className: "px-3 py-2 text-left font-medium text-red-600",
|
|
5133
|
+
children: "错误"
|
|
5134
|
+
})] })
|
|
5135
|
+
}), /* @__PURE__ */ jsx("tbody", { children: result.failed.map((f) => /* @__PURE__ */ jsxs("tr", {
|
|
5136
|
+
className: "border-t border-red-100 dark:border-red-900",
|
|
5137
|
+
children: [/* @__PURE__ */ jsx("td", {
|
|
5138
|
+
className: "px-3 py-2 text-red-600",
|
|
5139
|
+
children: f.row + 1
|
|
5140
|
+
}), /* @__PURE__ */ jsx("td", {
|
|
5141
|
+
className: "px-3 py-2 text-red-600/80",
|
|
5142
|
+
children: f.errors.join("; ")
|
|
5143
|
+
})]
|
|
5144
|
+
}, f.row)) })]
|
|
5145
|
+
})
|
|
5146
|
+
})
|
|
5147
|
+
})]
|
|
5148
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
5149
|
+
className: "flex gap-2 justify-end",
|
|
5150
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
5151
|
+
variant: "outline",
|
|
5152
|
+
onClick: () => handleOpenChange(false),
|
|
5153
|
+
children: "关闭"
|
|
5154
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
5155
|
+
variant: "outline",
|
|
5156
|
+
onClick: reset,
|
|
5157
|
+
children: "继续导入"
|
|
5158
|
+
})]
|
|
5159
|
+
})] }),
|
|
5160
|
+
error && /* @__PURE__ */ jsx("div", {
|
|
5161
|
+
className: "rounded-md bg-destructive/10 p-3 text-sm text-destructive",
|
|
5162
|
+
children: error
|
|
5163
|
+
})
|
|
5164
|
+
]
|
|
5165
|
+
})]
|
|
5166
|
+
})
|
|
5167
|
+
});
|
|
5168
|
+
}
|
|
5169
|
+
|
|
4579
5170
|
//#endregion
|
|
4580
5171
|
//#region src/components/auto-crud/auto-crud-table.tsx
|
|
4581
5172
|
/**
|
|
4582
5173
|
* 从统一配置生成表格 overrides
|
|
5174
|
+
* 当 filter 配置存在时,合并到 meta 中(不影响列隐藏)
|
|
4583
5175
|
*/
|
|
4584
5176
|
function buildTableOverrides(fields, legacyOverrides) {
|
|
4585
5177
|
const result = { ...legacyOverrides };
|
|
4586
|
-
if (fields) for (const [key, config] of Object.entries(fields))
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
5178
|
+
if (fields) for (const [key, config] of Object.entries(fields)) {
|
|
5179
|
+
let filterMeta;
|
|
5180
|
+
if (config.filter && config.filter.enabled !== false && !config.filter.hidden) {
|
|
5181
|
+
const { enabled: _, hidden: __,...filterProps } = config.filter;
|
|
5182
|
+
if (Object.keys(filterProps).length > 0) filterMeta = filterProps;
|
|
5183
|
+
}
|
|
5184
|
+
result[key] = {
|
|
5185
|
+
...result[key],
|
|
5186
|
+
...config.label && { label: config.label },
|
|
5187
|
+
...config.hidden && { hidden: true },
|
|
5188
|
+
...config.table,
|
|
5189
|
+
...filterMeta && { meta: {
|
|
5190
|
+
...result[key]?.meta ?? {},
|
|
5191
|
+
...config.table?.meta ?? {},
|
|
5192
|
+
...filterMeta
|
|
5193
|
+
} }
|
|
5194
|
+
};
|
|
5195
|
+
if (config.filter?.enabled === false || config.filter?.hidden === true) result[key] = {
|
|
5196
|
+
...result[key],
|
|
5197
|
+
enableColumnFilter: false
|
|
5198
|
+
};
|
|
5199
|
+
}
|
|
4592
5200
|
return result;
|
|
4593
5201
|
}
|
|
4594
5202
|
/**
|
|
@@ -4735,9 +5343,52 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
|
|
|
4735
5343
|
create: permissions?.can?.create ?? true,
|
|
4736
5344
|
update: permissions?.can?.update ?? true,
|
|
4737
5345
|
delete: permissions?.can?.delete ?? true,
|
|
4738
|
-
export: permissions?.can?.export ?? true
|
|
5346
|
+
export: permissions?.can?.export ?? true,
|
|
5347
|
+
import: permissions?.can?.import ?? true
|
|
4739
5348
|
};
|
|
4740
5349
|
const denyFields = permissions?.deny;
|
|
5350
|
+
const canImport = can.import && !!resource.handlers.import;
|
|
5351
|
+
const canExport = can.export;
|
|
5352
|
+
const [importOpen, setImportOpen] = React.useState(false);
|
|
5353
|
+
const [selectedCount, setSelectedCount] = React.useState(0);
|
|
5354
|
+
const [exporting, setExporting] = React.useState(false);
|
|
5355
|
+
const getSelectedRowsRef = React.useRef(null);
|
|
5356
|
+
const importColumns = React.useMemo(() => {
|
|
5357
|
+
const shape = schema.shape;
|
|
5358
|
+
const denySet = new Set(denyFields ?? []);
|
|
5359
|
+
return Object.keys(shape).filter((key) => !denySet.has(key));
|
|
5360
|
+
}, [schema, denyFields]);
|
|
5361
|
+
const handleExport = React.useCallback(async (mode) => {
|
|
5362
|
+
const filename = title ?? "export";
|
|
5363
|
+
const excludeColumns = denyFields;
|
|
5364
|
+
if (mode === "selected") {
|
|
5365
|
+
const selectedRows = getSelectedRowsRef.current?.();
|
|
5366
|
+
if (!selectedRows || selectedRows.length === 0) return;
|
|
5367
|
+
exportAllToCSV(selectedRows, {
|
|
5368
|
+
filename,
|
|
5369
|
+
excludeColumns
|
|
5370
|
+
});
|
|
5371
|
+
} else {
|
|
5372
|
+
if (!resource.handlers.export) return;
|
|
5373
|
+
exportAllToCSV(await resource.handlers.export(), {
|
|
5374
|
+
filename,
|
|
5375
|
+
excludeColumns
|
|
5376
|
+
});
|
|
5377
|
+
}
|
|
5378
|
+
}, [
|
|
5379
|
+
resource.handlers.export,
|
|
5380
|
+
title,
|
|
5381
|
+
denyFields
|
|
5382
|
+
]);
|
|
5383
|
+
const handleExportClick = React.useCallback(async () => {
|
|
5384
|
+
const mode = selectedCount > 0 ? "selected" : "filtered";
|
|
5385
|
+
setExporting(true);
|
|
5386
|
+
try {
|
|
5387
|
+
await handleExport(mode);
|
|
5388
|
+
} finally {
|
|
5389
|
+
setExporting(false);
|
|
5390
|
+
}
|
|
5391
|
+
}, [selectedCount, handleExport]);
|
|
4741
5392
|
const tableOverrides = buildTableOverrides(fields, tableConfig?.overrides);
|
|
4742
5393
|
const formOverrides = buildFormOverrides(fields, formConfig?.overrides, denyFields);
|
|
4743
5394
|
const hiddenColumns = buildHiddenColumns(fields, tableConfig?.hidden, denyFields);
|
|
@@ -4762,10 +5413,26 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
|
|
|
4762
5413
|
children: slots?.toolbarStart
|
|
4763
5414
|
}), /* @__PURE__ */ jsxs("div", {
|
|
4764
5415
|
className: "flex items-center gap-2",
|
|
4765
|
-
children: [
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
5416
|
+
children: [
|
|
5417
|
+
slots?.toolbarEnd,
|
|
5418
|
+
canImport && /* @__PURE__ */ jsxs(Button, {
|
|
5419
|
+
variant: "outline",
|
|
5420
|
+
size: "sm",
|
|
5421
|
+
onClick: () => setImportOpen(true),
|
|
5422
|
+
children: [/* @__PURE__ */ jsx(Download, { className: "mr-2 h-4 w-4" }), "导入"]
|
|
5423
|
+
}),
|
|
5424
|
+
canExport && /* @__PURE__ */ jsxs(Button, {
|
|
5425
|
+
variant: "outline",
|
|
5426
|
+
size: "sm",
|
|
5427
|
+
onClick: handleExportClick,
|
|
5428
|
+
disabled: exporting || selectedCount === 0 && !resource.handlers.export,
|
|
5429
|
+
children: [exporting ? /* @__PURE__ */ jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Upload, { className: "mr-2 h-4 w-4" }), selectedCount > 0 ? `导出(${selectedCount})` : "导出"]
|
|
5430
|
+
}),
|
|
5431
|
+
can.create && /* @__PURE__ */ jsx(Button, {
|
|
5432
|
+
onClick: resource.handlers.openCreate,
|
|
5433
|
+
children: "新建"
|
|
5434
|
+
})
|
|
5435
|
+
]
|
|
4769
5436
|
})]
|
|
4770
5437
|
}),
|
|
4771
5438
|
/* @__PURE__ */ jsx(AutoTable, {
|
|
@@ -4784,8 +5451,10 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
|
|
|
4784
5451
|
onDeleteSelected: can.delete ? resource.handlers.deleteMany : void 0,
|
|
4785
5452
|
onUpdateSelected: can.update ? resource.handlers.updateMany : void 0,
|
|
4786
5453
|
batchUpdateFields: can.update ? batchFields : void 0,
|
|
4787
|
-
enableExport:
|
|
4788
|
-
initialSorting: tableConfig?.defaultSort
|
|
5454
|
+
enableExport: false,
|
|
5455
|
+
initialSorting: tableConfig?.defaultSort,
|
|
5456
|
+
onSelectedCountChange: setSelectedCount,
|
|
5457
|
+
getSelectedRows: getSelectedRowsRef
|
|
4789
5458
|
}),
|
|
4790
5459
|
/* @__PURE__ */ jsx(CrudFormModal, {
|
|
4791
5460
|
open: resource.modal.createOpen || resource.modal.editOpen,
|
|
@@ -4818,11 +5487,90 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
|
|
|
4818
5487
|
disabled: resource.mutations.isDeleting,
|
|
4819
5488
|
children: resource.mutations.isDeleting ? "删除中..." : "确认删除"
|
|
4820
5489
|
})] })] })
|
|
5490
|
+
}),
|
|
5491
|
+
canImport && resource.handlers.import && /* @__PURE__ */ jsx(ImportDialog, {
|
|
5492
|
+
open: importOpen,
|
|
5493
|
+
onOpenChange: setImportOpen,
|
|
5494
|
+
onImport: resource.handlers.import,
|
|
5495
|
+
columns: importColumns
|
|
4821
5496
|
})
|
|
4822
5497
|
]
|
|
4823
5498
|
});
|
|
4824
5499
|
}
|
|
4825
5500
|
|
|
5501
|
+
//#endregion
|
|
5502
|
+
//#region src/components/auto-crud/export-dialog.tsx
|
|
5503
|
+
function ExportDialog({ open, onOpenChange, selectedCount, onExport, canExportFiltered = false }) {
|
|
5504
|
+
const [exporting, setExporting] = React.useState(null);
|
|
5505
|
+
const [done, setDone] = React.useState(false);
|
|
5506
|
+
const reset = React.useCallback(() => {
|
|
5507
|
+
setExporting(null);
|
|
5508
|
+
setDone(false);
|
|
5509
|
+
}, []);
|
|
5510
|
+
const handleOpenChange = React.useCallback((value) => {
|
|
5511
|
+
if (!value) reset();
|
|
5512
|
+
onOpenChange(value);
|
|
5513
|
+
}, [onOpenChange, reset]);
|
|
5514
|
+
const handleExport = React.useCallback(async (mode) => {
|
|
5515
|
+
setExporting(mode);
|
|
5516
|
+
try {
|
|
5517
|
+
await onExport(mode);
|
|
5518
|
+
setDone(true);
|
|
5519
|
+
setTimeout(() => handleOpenChange(false), 800);
|
|
5520
|
+
} catch {
|
|
5521
|
+
setExporting(null);
|
|
5522
|
+
}
|
|
5523
|
+
}, [onExport, handleOpenChange]);
|
|
5524
|
+
const hasSelected = selectedCount > 0;
|
|
5525
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
5526
|
+
open,
|
|
5527
|
+
onOpenChange: handleOpenChange,
|
|
5528
|
+
children: /* @__PURE__ */ jsxs(DialogContent, {
|
|
5529
|
+
className: "sm:max-w-[400px]",
|
|
5530
|
+
children: [/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsx(DialogTitle, { children: "导出数据" }), /* @__PURE__ */ jsx(DialogDescription, { children: "选择导出范围" })] }), done ? /* @__PURE__ */ jsxs("div", {
|
|
5531
|
+
className: "flex flex-col items-center gap-3 py-6",
|
|
5532
|
+
children: [/* @__PURE__ */ jsx(CheckCircle2, { className: "h-10 w-10 text-green-500" }), /* @__PURE__ */ jsx("p", {
|
|
5533
|
+
className: "text-sm text-muted-foreground",
|
|
5534
|
+
children: "导出成功"
|
|
5535
|
+
})]
|
|
5536
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
5537
|
+
className: "grid gap-3 py-2",
|
|
5538
|
+
children: [/* @__PURE__ */ jsxs(Button, {
|
|
5539
|
+
variant: "outline",
|
|
5540
|
+
className: "h-auto justify-start gap-3 px-4 py-3",
|
|
5541
|
+
disabled: !hasSelected || exporting !== null,
|
|
5542
|
+
onClick: () => handleExport("selected"),
|
|
5543
|
+
children: [exporting === "selected" ? /* @__PURE__ */ jsx(Loader2, { className: "h-5 w-5 shrink-0 animate-spin" }) : /* @__PURE__ */ jsx(FileDown, { className: "h-5 w-5 shrink-0" }), /* @__PURE__ */ jsxs("div", {
|
|
5544
|
+
className: "flex flex-col items-start",
|
|
5545
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
5546
|
+
className: "font-medium",
|
|
5547
|
+
children: ["导出选中", hasSelected ? ` (${selectedCount} 条)` : ""]
|
|
5548
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
5549
|
+
className: "text-xs text-muted-foreground",
|
|
5550
|
+
children: hasSelected ? "导出当前勾选的数据行" : "请先在表格中勾选数据行"
|
|
5551
|
+
})]
|
|
5552
|
+
})]
|
|
5553
|
+
}), canExportFiltered && /* @__PURE__ */ jsxs(Button, {
|
|
5554
|
+
variant: "outline",
|
|
5555
|
+
className: "h-auto justify-start gap-3 px-4 py-3",
|
|
5556
|
+
disabled: exporting !== null,
|
|
5557
|
+
onClick: () => handleExport("filtered"),
|
|
5558
|
+
children: [exporting === "filtered" ? /* @__PURE__ */ jsx(Loader2, { className: "h-5 w-5 shrink-0 animate-spin" }) : /* @__PURE__ */ jsx(FileDown, { className: "h-5 w-5 shrink-0" }), /* @__PURE__ */ jsxs("div", {
|
|
5559
|
+
className: "flex flex-col items-start",
|
|
5560
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
5561
|
+
className: "font-medium",
|
|
5562
|
+
children: "导出筛选结果"
|
|
5563
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
5564
|
+
className: "text-xs text-muted-foreground",
|
|
5565
|
+
children: "导出当前筛选条件下的所有数据"
|
|
5566
|
+
})]
|
|
5567
|
+
})]
|
|
5568
|
+
})]
|
|
5569
|
+
})]
|
|
5570
|
+
})
|
|
5571
|
+
});
|
|
5572
|
+
}
|
|
5573
|
+
|
|
4826
5574
|
//#endregion
|
|
4827
5575
|
//#region src/components/data-table/data-table-advanced-toolbar.tsx
|
|
4828
5576
|
function DataTableAdvancedToolbar({ table, children, className,...props }) {
|
|
@@ -5494,15 +6242,44 @@ function modalReducer(state, action) {
|
|
|
5494
6242
|
/**
|
|
5495
6243
|
* Hook 主函数
|
|
5496
6244
|
*/
|
|
5497
|
-
function useAutoCrudResource({ router,
|
|
5498
|
-
const { idKey = "id", defaultVariant = "dialog", hooks, toast: toastAdapter } = options;
|
|
6245
|
+
function useAutoCrudResource({ router, schema, query: queryTransform, options = {} }) {
|
|
6246
|
+
const { idKey = "id", defaultVariant = "dialog", hooks, toast: toastAdapter, exportFetcher } = options;
|
|
5499
6247
|
const toast$1 = useMemo(() => toastAdapter === false ? noopToastAdapter : toastAdapter ?? defaultToastAdapter, [toastAdapter]);
|
|
6248
|
+
const columns = useMemo(() => createTableSchema(schema), [schema]);
|
|
6249
|
+
const [urlParams] = useQueryStates({
|
|
6250
|
+
page: parseAsInteger.withDefault(1),
|
|
6251
|
+
perPage: parseAsInteger.withDefault(10),
|
|
6252
|
+
sort: getSortingStateParser().withDefault([]),
|
|
6253
|
+
joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and")
|
|
6254
|
+
}, { shallow: false });
|
|
6255
|
+
const [filters] = useReadableFilters(columns, { shallow: false });
|
|
6256
|
+
const queryInput = useMemo(() => {
|
|
6257
|
+
const autoParams = {
|
|
6258
|
+
page: urlParams.page,
|
|
6259
|
+
perPage: urlParams.perPage,
|
|
6260
|
+
sort: urlParams.sort,
|
|
6261
|
+
filters,
|
|
6262
|
+
joinOperator: urlParams.joinOperator
|
|
6263
|
+
};
|
|
6264
|
+
return queryTransform ? queryTransform(autoParams) : autoParams;
|
|
6265
|
+
}, [
|
|
6266
|
+
urlParams,
|
|
6267
|
+
filters,
|
|
6268
|
+
queryTransform
|
|
6269
|
+
]);
|
|
5500
6270
|
const listQuery = router.list.useQuery(queryInput, { placeholderData: keepPreviousData });
|
|
5501
6271
|
const createMutation = router.create.useMutation();
|
|
5502
6272
|
const updateMutation = router.update.useMutation();
|
|
5503
6273
|
const deleteMutation = router.delete.useMutation();
|
|
5504
6274
|
const deleteManyMutation = router.deleteMany?.useMutation();
|
|
5505
6275
|
const updateManyMutation = router.updateMany?.useMutation();
|
|
6276
|
+
const importMutation = router.import?.useMutation();
|
|
6277
|
+
const exportInput = useMemo(() => {
|
|
6278
|
+
if (!queryInput) return {};
|
|
6279
|
+
const { page: _p, perPage: _pp,...rest } = queryInput;
|
|
6280
|
+
return rest;
|
|
6281
|
+
}, [queryInput]);
|
|
6282
|
+
const exportQuery = router.export?.useQuery(exportInput, { enabled: false });
|
|
5506
6283
|
const [modal, dispatch] = useReducer(modalReducer, {
|
|
5507
6284
|
createOpen: false,
|
|
5508
6285
|
editOpen: false,
|
|
@@ -5714,6 +6491,33 @@ function useAutoCrudResource({ router, queryInput, options = {} }) {
|
|
|
5714
6491
|
payload: row
|
|
5715
6492
|
});
|
|
5716
6493
|
}, []);
|
|
6494
|
+
const handleImport = useCallback(async (rows) => {
|
|
6495
|
+
if (!importMutation) throw new Error("Import not supported: router has no import procedure");
|
|
6496
|
+
const coercedRows = coerceRowValues(rows, schema);
|
|
6497
|
+
const result = await importMutation.mutateAsync({
|
|
6498
|
+
rows: coercedRows,
|
|
6499
|
+
onConflict: "upsert"
|
|
6500
|
+
});
|
|
6501
|
+
listQuery.refetch();
|
|
6502
|
+
return result;
|
|
6503
|
+
}, [
|
|
6504
|
+
importMutation,
|
|
6505
|
+
listQuery,
|
|
6506
|
+
schema
|
|
6507
|
+
]);
|
|
6508
|
+
const handleExport = useCallback(async () => {
|
|
6509
|
+
if (exportFetcher) {
|
|
6510
|
+
const { page: _p, perPage: _pp,...exportParams } = queryInput ?? {};
|
|
6511
|
+
return (await exportFetcher(exportParams))?.data ?? [];
|
|
6512
|
+
}
|
|
6513
|
+
if (exportQuery) return (await exportQuery.refetch())?.data?.data ?? [];
|
|
6514
|
+
throw new Error("Export not supported");
|
|
6515
|
+
}, [
|
|
6516
|
+
exportFetcher,
|
|
6517
|
+
exportQuery,
|
|
6518
|
+
queryInput
|
|
6519
|
+
]);
|
|
6520
|
+
const canExport = !!(exportFetcher || exportQuery);
|
|
5717
6521
|
return {
|
|
5718
6522
|
tableData: {
|
|
5719
6523
|
data: listQuery.data?.data ?? [],
|
|
@@ -5725,7 +6529,8 @@ function useAutoCrudResource({ router, queryInput, options = {} }) {
|
|
|
5725
6529
|
mutations: {
|
|
5726
6530
|
isCreating: createMutation.isPending,
|
|
5727
6531
|
isUpdating: updateMutation.isPending,
|
|
5728
|
-
isDeleting: deleteMutation.isPending
|
|
6532
|
+
isDeleting: deleteMutation.isPending,
|
|
6533
|
+
isImporting: importMutation?.isPending ?? false
|
|
5729
6534
|
},
|
|
5730
6535
|
handlers: {
|
|
5731
6536
|
openCreate,
|
|
@@ -5739,7 +6544,9 @@ function useAutoCrudResource({ router, queryInput, options = {} }) {
|
|
|
5739
6544
|
confirmDelete,
|
|
5740
6545
|
deleteMany,
|
|
5741
6546
|
updateMany,
|
|
5742
|
-
setVariant
|
|
6547
|
+
setVariant,
|
|
6548
|
+
import: importMutation ? handleImport : null,
|
|
6549
|
+
export: canExport ? handleExport : null
|
|
5743
6550
|
}
|
|
5744
6551
|
};
|
|
5745
6552
|
}
|
|
@@ -5997,12 +6804,12 @@ function createMemoryDataSource(initialData = []) {
|
|
|
5997
6804
|
});
|
|
5998
6805
|
});
|
|
5999
6806
|
if (params.sort && params.sort.length > 0) {
|
|
6000
|
-
const
|
|
6807
|
+
const sortItem = params.sort[0];
|
|
6001
6808
|
filtered.sort((a, b) => {
|
|
6002
|
-
const aVal = a[id];
|
|
6003
|
-
const bVal = b[id];
|
|
6809
|
+
const aVal = a[sortItem.id];
|
|
6810
|
+
const bVal = b[sortItem.id];
|
|
6004
6811
|
const result = aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
|
|
6005
|
-
return desc ? -result : result;
|
|
6812
|
+
return sortItem.desc ? -result : result;
|
|
6006
6813
|
});
|
|
6007
6814
|
}
|
|
6008
6815
|
const start = (params.page - 1) * params.perPage;
|
|
@@ -6030,11 +6837,12 @@ function createMemoryDataSource(initialData = []) {
|
|
|
6030
6837
|
async update(id, updateData) {
|
|
6031
6838
|
const index = data.findIndex((item) => item.id === id);
|
|
6032
6839
|
if (index === -1) throw new Error(`Item with id ${id} not found`);
|
|
6033
|
-
|
|
6840
|
+
const updated = {
|
|
6034
6841
|
...data[index],
|
|
6035
6842
|
...updateData
|
|
6036
6843
|
};
|
|
6037
|
-
|
|
6844
|
+
data[index] = updated;
|
|
6845
|
+
return updated;
|
|
6038
6846
|
},
|
|
6039
6847
|
async delete(id) {
|
|
6040
6848
|
const index = data.findIndex((item) => item.id === id);
|
|
@@ -6048,4 +6856,4 @@ function createMemoryDataSource(initialData = []) {
|
|
|
6048
6856
|
}
|
|
6049
6857
|
|
|
6050
6858
|
//#endregion
|
|
6051
|
-
export { AutoCrudTable, AutoForm, AutoTable, AutoTableActionBar, AutoTableSimpleFilters, CrudFormModal, DataTable, DataTableAdvancedToolbar, DataTableColumnHeader, DataTableFacetedFilter, DataTablePagination, DataTableToolbar, DataTableViewOptions, SchemaAdapter, cn, createActionsColumn, createEditFormSchema, createFormSchema, createMemoryDataSource, createSelectColumn, createTRPCDataSource, createTableSchema, formatDate, getUrlParams, humanize, noopToastAdapter, parseAsArrayOf, parseAsInteger, parseAsString, parseAsStringEnum, parseZodField, setSearchParams, useAutoCrudResource, useDataTable, useQueryState, useQueryStates, useReadableFilters, useUrlState, useUrlStates };
|
|
6859
|
+
export { AutoCrudTable, AutoForm, AutoTable, AutoTableActionBar, AutoTableSimpleFilters, CrudFormModal, DataTable, DataTableAdvancedToolbar, DataTableColumnHeader, DataTableFacetedFilter, DataTablePagination, DataTableToolbar, DataTableViewOptions, ExportDialog, ImportDialog, SchemaAdapter, cn, coerceRowValues, createActionsColumn, createEditFormSchema, createFormSchema, createMemoryDataSource, createSelectColumn, createTRPCDataSource, createTableSchema, dataToCSV, downloadCSVTemplate, exportAllToCSV, exportTableToCSV, formatDate, generateCSVTemplate, getUrlParams, humanize, noopToastAdapter, parseAsArrayOf, parseAsInteger, parseAsString, parseAsStringEnum, parseCSV, parseImportFile, parseJSON, parseZodField, setSearchParams, useAutoCrudResource, useDataTable, useQueryState, useQueryStates, useReadableFilters, useUrlState, useUrlStates };
|