@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.cjs
CHANGED
|
@@ -35,6 +35,8 @@ let tailwind_merge = require("tailwind-merge");
|
|
|
35
35
|
tailwind_merge = __toESM(tailwind_merge);
|
|
36
36
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
37
37
|
react_jsx_runtime = __toESM(react_jsx_runtime);
|
|
38
|
+
let nanoid = require("nanoid");
|
|
39
|
+
nanoid = __toESM(nanoid);
|
|
38
40
|
let __dnd_kit_core = require("@dnd-kit/core");
|
|
39
41
|
__dnd_kit_core = __toESM(__dnd_kit_core);
|
|
40
42
|
let __dnd_kit_modifiers = require("@dnd-kit/modifiers");
|
|
@@ -47,8 +49,6 @@ let __radix_ui_react_slot = require("@radix-ui/react-slot");
|
|
|
47
49
|
__radix_ui_react_slot = __toESM(__radix_ui_react_slot);
|
|
48
50
|
let react_dom = require("react-dom");
|
|
49
51
|
react_dom = __toESM(react_dom);
|
|
50
|
-
let nanoid = require("nanoid");
|
|
51
|
-
nanoid = __toESM(nanoid);
|
|
52
52
|
let __radix_ui_react_direction = require("@radix-ui/react-direction");
|
|
53
53
|
__radix_ui_react_direction = __toESM(__radix_ui_react_direction);
|
|
54
54
|
let zod = require("zod");
|
|
@@ -474,6 +474,104 @@ function DataTable({ table, actionBar, children, className,...props }) {
|
|
|
474
474
|
});
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
+
//#endregion
|
|
478
|
+
//#region src/lib/id.ts
|
|
479
|
+
const prefixes = {};
|
|
480
|
+
function generateId(prefixOrOptions, inputOptions = {}) {
|
|
481
|
+
const finalOptions = typeof prefixOrOptions === "object" ? prefixOrOptions : inputOptions;
|
|
482
|
+
const prefix = typeof prefixOrOptions === "object" ? void 0 : prefixOrOptions;
|
|
483
|
+
const { length = 12, separator = "_" } = finalOptions;
|
|
484
|
+
const id = (0, nanoid.customAlphabet)("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", length)();
|
|
485
|
+
return prefix && prefix in prefixes ? `${prefixes[prefix]}${separator}${id}` : id;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
//#endregion
|
|
489
|
+
//#region src/lib/readable-filters.ts
|
|
490
|
+
const OP_SUFFIX = "__op";
|
|
491
|
+
const INDEX_SEPARATOR = "__";
|
|
492
|
+
const RANGE_VARIANTS = new Set(["range", "dateRange"]);
|
|
493
|
+
function getColumnId(column) {
|
|
494
|
+
if ("id" in column && column.id) return String(column.id);
|
|
495
|
+
if ("accessorKey" in column && column.accessorKey) return String(column.accessorKey);
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
function getColumnVariant(column) {
|
|
499
|
+
if ("columnDef" in column) return column.columnDef.meta?.variant;
|
|
500
|
+
return column.meta?.variant;
|
|
501
|
+
}
|
|
502
|
+
function splitKey(key) {
|
|
503
|
+
const match = key.match(/^(.*?)(?:__(\d+))?$/);
|
|
504
|
+
if (!match) return { base: key };
|
|
505
|
+
const index = match[2] ? Number(match[2]) : void 0;
|
|
506
|
+
return {
|
|
507
|
+
base: match[1] ?? key,
|
|
508
|
+
index
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function isFilterKey(key, columnIds) {
|
|
512
|
+
const { base } = splitKey(key.endsWith(OP_SUFFIX) ? key.slice(0, -4) : key);
|
|
513
|
+
return columnIds.has(base);
|
|
514
|
+
}
|
|
515
|
+
function parseValueByVariant(value, variant) {
|
|
516
|
+
if (variant === "multiSelect") return value ? value.split(",").filter(Boolean) : [];
|
|
517
|
+
if (RANGE_VARIANTS.has(variant)) {
|
|
518
|
+
const parts = value.split(",");
|
|
519
|
+
return [parts[0] ?? "", parts[1] ?? ""];
|
|
520
|
+
}
|
|
521
|
+
if (variant === "select") return value.split(",").filter(Boolean)[0] ?? "";
|
|
522
|
+
return value;
|
|
523
|
+
}
|
|
524
|
+
function serializeValue(value, variant) {
|
|
525
|
+
if (Array.isArray(value)) {
|
|
526
|
+
if (RANGE_VARIANTS.has(variant)) return [value[0] ?? "", value[1] ?? ""].join(",");
|
|
527
|
+
return value.join(",");
|
|
528
|
+
}
|
|
529
|
+
return value;
|
|
530
|
+
}
|
|
531
|
+
function parseReadableFilters(params, columns) {
|
|
532
|
+
const columnsById = /* @__PURE__ */ new Map();
|
|
533
|
+
for (const column of columns) {
|
|
534
|
+
const id = getColumnId(column);
|
|
535
|
+
if (id) columnsById.set(id, column);
|
|
536
|
+
}
|
|
537
|
+
const filters = [];
|
|
538
|
+
for (const [key, rawValue] of params.entries()) {
|
|
539
|
+
if (key.endsWith(OP_SUFFIX)) continue;
|
|
540
|
+
const { base } = splitKey(key);
|
|
541
|
+
const column = columnsById.get(base);
|
|
542
|
+
if (!column) continue;
|
|
543
|
+
const variant = getColumnVariant(column) ?? "text";
|
|
544
|
+
const opKey = `${key}${OP_SUFFIX}`;
|
|
545
|
+
const operator = params.get(opKey) ?? getDefaultFilterOperator(variant);
|
|
546
|
+
const parsedValue = operator === "isEmpty" || operator === "isNotEmpty" ? "" : parseValueByVariant(rawValue, variant);
|
|
547
|
+
filters.push({
|
|
548
|
+
id: base,
|
|
549
|
+
value: parsedValue,
|
|
550
|
+
variant,
|
|
551
|
+
operator,
|
|
552
|
+
filterId: key || generateId({ length: 8 })
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
return filters;
|
|
556
|
+
}
|
|
557
|
+
function applyReadableFilters(params, columns, filters) {
|
|
558
|
+
const columnIds = new Set(columns.map((column) => getColumnId(column)).filter(Boolean));
|
|
559
|
+
for (const key of Array.from(params.keys())) if (isFilterKey(key, columnIds)) params.delete(key);
|
|
560
|
+
const counters = /* @__PURE__ */ new Map();
|
|
561
|
+
for (const filter of filters) {
|
|
562
|
+
const columnId = String(filter.id);
|
|
563
|
+
const count = (counters.get(columnId) ?? 0) + 1;
|
|
564
|
+
counters.set(columnId, count);
|
|
565
|
+
const key = count === 1 ? columnId : `${columnId}${INDEX_SEPARATOR}${count}`;
|
|
566
|
+
const variant = filter.variant ?? "text";
|
|
567
|
+
const value = serializeValue(filter.value, variant);
|
|
568
|
+
params.set(key, filter.operator === "isEmpty" || filter.operator === "isNotEmpty" ? "" : value);
|
|
569
|
+
const defaultOperator = getDefaultFilterOperator(variant);
|
|
570
|
+
if (filter.operator !== defaultOperator) params.set(`${key}${OP_SUFFIX}`, filter.operator);
|
|
571
|
+
}
|
|
572
|
+
return params;
|
|
573
|
+
}
|
|
574
|
+
|
|
477
575
|
//#endregion
|
|
478
576
|
//#region src/hooks/use-url-state.ts
|
|
479
577
|
const parseAsInteger = {
|
|
@@ -724,6 +822,54 @@ function useQueryStates(parsers, options = {}) {
|
|
|
724
822
|
}, [setValues])];
|
|
725
823
|
}
|
|
726
824
|
|
|
825
|
+
//#endregion
|
|
826
|
+
//#region src/hooks/use-readable-filters.ts
|
|
827
|
+
const URL_CHANGE_EVENT = "urlchange";
|
|
828
|
+
function useReadableFilters(columns, options = {}) {
|
|
829
|
+
const { history = "replace", scroll = false } = options;
|
|
830
|
+
const columnSnapshot = react.useMemo(() => columns, [columns]);
|
|
831
|
+
const lastWrittenUrlRef = react.useRef("");
|
|
832
|
+
const filtersRef = react.useRef(null);
|
|
833
|
+
const [filters, setFilters] = react.useState(() => {
|
|
834
|
+
const initial = parseReadableFilters(getUrlParams(), columnSnapshot);
|
|
835
|
+
filtersRef.current = initial;
|
|
836
|
+
return initial;
|
|
837
|
+
});
|
|
838
|
+
react.useEffect(() => {
|
|
839
|
+
const handleUrlChange = () => {
|
|
840
|
+
if (window.location.search === lastWrittenUrlRef.current) return;
|
|
841
|
+
const next = parseReadableFilters(getUrlParams(), columnSnapshot);
|
|
842
|
+
filtersRef.current = next;
|
|
843
|
+
setFilters(next);
|
|
844
|
+
};
|
|
845
|
+
window.addEventListener("popstate", handleUrlChange);
|
|
846
|
+
window.addEventListener(URL_CHANGE_EVENT, handleUrlChange);
|
|
847
|
+
return () => {
|
|
848
|
+
window.removeEventListener("popstate", handleUrlChange);
|
|
849
|
+
window.removeEventListener(URL_CHANGE_EVENT, handleUrlChange);
|
|
850
|
+
};
|
|
851
|
+
}, [columnSnapshot]);
|
|
852
|
+
const writeToUrl = react.useCallback((next) => {
|
|
853
|
+
const params = getUrlParams();
|
|
854
|
+
applyReadableFilters(params, columnSnapshot, next);
|
|
855
|
+
setSearchParams(params, {
|
|
856
|
+
history,
|
|
857
|
+
scroll
|
|
858
|
+
});
|
|
859
|
+
lastWrittenUrlRef.current = `?${params.toString()}`;
|
|
860
|
+
}, [
|
|
861
|
+
columnSnapshot,
|
|
862
|
+
history,
|
|
863
|
+
scroll
|
|
864
|
+
]);
|
|
865
|
+
return [filters, react.useCallback((updater) => {
|
|
866
|
+
const next = typeof updater === "function" ? updater(filtersRef.current) : updater;
|
|
867
|
+
filtersRef.current = next;
|
|
868
|
+
setFilters(next);
|
|
869
|
+
writeToUrl(next);
|
|
870
|
+
}, [writeToUrl])];
|
|
871
|
+
}
|
|
872
|
+
|
|
727
873
|
//#endregion
|
|
728
874
|
//#region src/components/data-table/data-table-range-filter.tsx
|
|
729
875
|
function DataTableRangeFilter({ filter, column, inputId, onFilterUpdate, className,...props }) {
|
|
@@ -1292,204 +1438,47 @@ function formatDate(date, opts = {}) {
|
|
|
1292
1438
|
}
|
|
1293
1439
|
|
|
1294
1440
|
//#endregion
|
|
1295
|
-
//#region src/
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
const id =
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
const
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
return [parts[0] ?? "", parts[1] ?? ""];
|
|
1337
|
-
}
|
|
1338
|
-
if (variant === "select") return value.split(",").filter(Boolean)[0] ?? "";
|
|
1339
|
-
return value;
|
|
1340
|
-
}
|
|
1341
|
-
function serializeValue(value, variant) {
|
|
1342
|
-
if (Array.isArray(value)) {
|
|
1343
|
-
if (RANGE_VARIANTS.has(variant)) return [value[0] ?? "", value[1] ?? ""].join(",");
|
|
1344
|
-
return value.join(",");
|
|
1345
|
-
}
|
|
1346
|
-
return value;
|
|
1347
|
-
}
|
|
1348
|
-
function parseReadableFilters(params, columns) {
|
|
1349
|
-
const columnsById = /* @__PURE__ */ new Map();
|
|
1350
|
-
for (const column of columns) {
|
|
1351
|
-
const id = getColumnId(column);
|
|
1352
|
-
if (id) columnsById.set(id, column);
|
|
1353
|
-
}
|
|
1354
|
-
const filters = [];
|
|
1355
|
-
for (const [key, rawValue] of params.entries()) {
|
|
1356
|
-
if (key.endsWith(OP_SUFFIX)) continue;
|
|
1357
|
-
const { base } = splitKey(key);
|
|
1358
|
-
const column = columnsById.get(base);
|
|
1359
|
-
if (!column) continue;
|
|
1360
|
-
const variant = getColumnVariant(column) ?? "text";
|
|
1361
|
-
const opKey = `${key}${OP_SUFFIX}`;
|
|
1362
|
-
const operator = params.get(opKey) ?? getDefaultFilterOperator(variant);
|
|
1363
|
-
if (!rawValue && operator !== "isEmpty" && operator !== "isNotEmpty") continue;
|
|
1364
|
-
const parsedValue = operator === "isEmpty" || operator === "isNotEmpty" ? "" : parseValueByVariant(rawValue, variant);
|
|
1365
|
-
filters.push({
|
|
1366
|
-
id: base,
|
|
1367
|
-
value: parsedValue,
|
|
1368
|
-
variant,
|
|
1369
|
-
operator,
|
|
1370
|
-
filterId: key || generateId({ length: 8 })
|
|
1371
|
-
});
|
|
1372
|
-
}
|
|
1373
|
-
return filters;
|
|
1374
|
-
}
|
|
1375
|
-
function applyReadableFilters(params, columns, filters) {
|
|
1376
|
-
const columnIds = new Set(columns.map((column) => getColumnId(column)).filter(Boolean));
|
|
1377
|
-
for (const key of Array.from(params.keys())) if (isFilterKey(key, columnIds)) params.delete(key);
|
|
1378
|
-
const counters = /* @__PURE__ */ new Map();
|
|
1379
|
-
for (const filter of filters) {
|
|
1380
|
-
const columnId = String(filter.id);
|
|
1381
|
-
const count = (counters.get(columnId) ?? 0) + 1;
|
|
1382
|
-
counters.set(columnId, count);
|
|
1383
|
-
const key = count === 1 ? columnId : `${columnId}${INDEX_SEPARATOR}${count}`;
|
|
1384
|
-
const variant = filter.variant ?? "text";
|
|
1385
|
-
const value = serializeValue(filter.value, variant);
|
|
1386
|
-
params.set(key, filter.operator === "isEmpty" || filter.operator === "isNotEmpty" ? "" : value);
|
|
1387
|
-
const defaultOperator = getDefaultFilterOperator(variant);
|
|
1388
|
-
if (filter.operator !== defaultOperator) params.set(`${key}${OP_SUFFIX}`, filter.operator);
|
|
1389
|
-
}
|
|
1390
|
-
return params;
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
//#endregion
|
|
1394
|
-
//#region src/hooks/use-readable-filters.ts
|
|
1395
|
-
const URL_CHANGE_EVENT = "urlchange";
|
|
1396
|
-
function useReadableFilters(columns, options = {}) {
|
|
1397
|
-
const { debounceMs = 300, history = "replace", scroll = false } = options;
|
|
1398
|
-
const columnSnapshot = react.useMemo(() => columns, [columns]);
|
|
1399
|
-
const skipWriteRef = react.useRef(true);
|
|
1400
|
-
const [filters, setFilters] = react.useState(() => parseReadableFilters(getUrlParams(), columnSnapshot));
|
|
1401
|
-
react.useEffect(() => {
|
|
1402
|
-
const handleUrlChange = () => {
|
|
1403
|
-
skipWriteRef.current = true;
|
|
1404
|
-
setFilters(parseReadableFilters(getUrlParams(), columnSnapshot));
|
|
1405
|
-
};
|
|
1406
|
-
window.addEventListener("popstate", handleUrlChange);
|
|
1407
|
-
window.addEventListener(URL_CHANGE_EVENT, handleUrlChange);
|
|
1408
|
-
return () => {
|
|
1409
|
-
window.removeEventListener("popstate", handleUrlChange);
|
|
1410
|
-
window.removeEventListener(URL_CHANGE_EVENT, handleUrlChange);
|
|
1411
|
-
};
|
|
1412
|
-
}, [columnSnapshot]);
|
|
1413
|
-
const writeFilters = react.useCallback((next) => {
|
|
1414
|
-
const params = getUrlParams();
|
|
1415
|
-
applyReadableFilters(params, columnSnapshot, next);
|
|
1416
|
-
setSearchParams(params, {
|
|
1417
|
-
history,
|
|
1418
|
-
scroll
|
|
1419
|
-
});
|
|
1420
|
-
}, [
|
|
1421
|
-
columnSnapshot,
|
|
1422
|
-
history,
|
|
1423
|
-
scroll
|
|
1424
|
-
]);
|
|
1425
|
-
const updateFilters = react.useCallback((updater) => {
|
|
1426
|
-
setFilters((prev) => {
|
|
1427
|
-
return typeof updater === "function" ? updater(prev) : updater;
|
|
1428
|
-
});
|
|
1429
|
-
}, []);
|
|
1430
|
-
const debouncedWrite = useDebouncedCallback(writeFilters, debounceMs);
|
|
1431
|
-
react.useEffect(() => {
|
|
1432
|
-
if (skipWriteRef.current) {
|
|
1433
|
-
skipWriteRef.current = false;
|
|
1434
|
-
return;
|
|
1435
|
-
}
|
|
1436
|
-
if (debounceMs > 0) debouncedWrite(filters);
|
|
1437
|
-
else writeFilters(filters);
|
|
1438
|
-
}, [
|
|
1439
|
-
filters,
|
|
1440
|
-
debounceMs,
|
|
1441
|
-
debouncedWrite,
|
|
1442
|
-
writeFilters
|
|
1443
|
-
]);
|
|
1444
|
-
return [filters, updateFilters];
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
//#endregion
|
|
1448
|
-
//#region src/components/data-table/data-table-filter-list.tsx
|
|
1449
|
-
const DEBOUNCE_MS$2 = 300;
|
|
1450
|
-
const THROTTLE_MS$2 = 50;
|
|
1451
|
-
const FILTER_SHORTCUT_KEY$1 = "f";
|
|
1452
|
-
const REMOVE_FILTER_SHORTCUTS$1 = ["backspace", "delete"];
|
|
1453
|
-
function DataTableFilterList({ table, debounceMs = DEBOUNCE_MS$2, throttleMs = THROTTLE_MS$2, shallow = true, disabled,...props }) {
|
|
1454
|
-
const id = react.useId();
|
|
1455
|
-
const labelId = react.useId();
|
|
1456
|
-
const descriptionId = react.useId();
|
|
1457
|
-
const [open, setOpen] = react.useState(false);
|
|
1458
|
-
const addButtonRef = react.useRef(null);
|
|
1459
|
-
const columns = react.useMemo(() => {
|
|
1460
|
-
return table.getAllColumns().filter((column) => column.columnDef.enableColumnFilter);
|
|
1461
|
-
}, [table]);
|
|
1462
|
-
const queryStateOptions = table.options.meta?.queryStateOptions;
|
|
1463
|
-
const [filters, setFilters] = useReadableFilters(columns, {
|
|
1464
|
-
...queryStateOptions,
|
|
1465
|
-
debounceMs: 0
|
|
1466
|
-
});
|
|
1467
|
-
const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);
|
|
1468
|
-
const [joinOperator, setJoinOperator] = useQueryState(table.options.meta?.queryKeys?.joinOperator ?? "", parseAsStringEnum(["and", "or"]).withDefault("and"));
|
|
1469
|
-
const onFilterAdd = react.useCallback(() => {
|
|
1470
|
-
const column = columns[0];
|
|
1471
|
-
if (!column) return;
|
|
1472
|
-
debouncedSetFilters([...filters, {
|
|
1473
|
-
id: column.id,
|
|
1474
|
-
value: "",
|
|
1475
|
-
variant: column.columnDef.meta?.variant ?? "text",
|
|
1476
|
-
operator: getDefaultFilterOperator(column.columnDef.meta?.variant ?? "text"),
|
|
1477
|
-
filterId: generateId({ length: 8 })
|
|
1478
|
-
}]);
|
|
1479
|
-
}, [
|
|
1480
|
-
columns,
|
|
1481
|
-
filters,
|
|
1482
|
-
debouncedSetFilters
|
|
1483
|
-
]);
|
|
1484
|
-
const onFilterUpdate = react.useCallback((filterId, updates) => {
|
|
1485
|
-
debouncedSetFilters((prevFilters) => {
|
|
1486
|
-
return prevFilters.map((filter) => {
|
|
1487
|
-
if (filter.filterId === filterId) return {
|
|
1488
|
-
...filter,
|
|
1489
|
-
...updates
|
|
1490
|
-
};
|
|
1491
|
-
return filter;
|
|
1492
|
-
});
|
|
1441
|
+
//#region src/components/data-table/data-table-filter-list.tsx
|
|
1442
|
+
const DEBOUNCE_MS$2 = 300;
|
|
1443
|
+
const THROTTLE_MS$2 = 50;
|
|
1444
|
+
const FILTER_SHORTCUT_KEY$1 = "f";
|
|
1445
|
+
const REMOVE_FILTER_SHORTCUTS$1 = ["backspace", "delete"];
|
|
1446
|
+
function DataTableFilterList({ table, debounceMs = DEBOUNCE_MS$2, throttleMs = THROTTLE_MS$2, shallow = true, disabled,...props }) {
|
|
1447
|
+
const id = react.useId();
|
|
1448
|
+
const labelId = react.useId();
|
|
1449
|
+
const descriptionId = react.useId();
|
|
1450
|
+
const [open, setOpen] = react.useState(false);
|
|
1451
|
+
const addButtonRef = react.useRef(null);
|
|
1452
|
+
const columns = react.useMemo(() => {
|
|
1453
|
+
return table.getAllColumns().filter((column) => column.columnDef.enableColumnFilter);
|
|
1454
|
+
}, [table]);
|
|
1455
|
+
const [filters, setFilters] = useReadableFilters(columns, { debounceMs });
|
|
1456
|
+
const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);
|
|
1457
|
+
const [joinOperator, setJoinOperator] = useQueryState(table.options.meta?.queryKeys?.joinOperator ?? "", parseAsStringEnum(["and", "or"]).withDefault("and"));
|
|
1458
|
+
const onFilterAdd = react.useCallback(() => {
|
|
1459
|
+
const column = columns[0];
|
|
1460
|
+
if (!column) return;
|
|
1461
|
+
debouncedSetFilters([...filters, {
|
|
1462
|
+
id: column.id,
|
|
1463
|
+
value: "",
|
|
1464
|
+
variant: column.columnDef.meta?.variant ?? "text",
|
|
1465
|
+
operator: getDefaultFilterOperator(column.columnDef.meta?.variant ?? "text"),
|
|
1466
|
+
filterId: generateId({ length: 8 })
|
|
1467
|
+
}]);
|
|
1468
|
+
}, [
|
|
1469
|
+
columns,
|
|
1470
|
+
filters,
|
|
1471
|
+
debouncedSetFilters
|
|
1472
|
+
]);
|
|
1473
|
+
const onFilterUpdate = react.useCallback((filterId, updates) => {
|
|
1474
|
+
debouncedSetFilters((prevFilters) => {
|
|
1475
|
+
return prevFilters.map((filter) => {
|
|
1476
|
+
if (filter.filterId === filterId) return {
|
|
1477
|
+
...filter,
|
|
1478
|
+
...updates
|
|
1479
|
+
};
|
|
1480
|
+
return filter;
|
|
1481
|
+
});
|
|
1493
1482
|
});
|
|
1494
1483
|
}, [debouncedSetFilters]);
|
|
1495
1484
|
const onFilterRemove = react.useCallback((filterId) => {
|
|
@@ -1499,7 +1488,7 @@ function DataTableFilterList({ table, debounceMs = DEBOUNCE_MS$2, throttleMs = T
|
|
|
1499
1488
|
});
|
|
1500
1489
|
}, [filters, setFilters]);
|
|
1501
1490
|
const onFiltersReset = react.useCallback(() => {
|
|
1502
|
-
setFilters(
|
|
1491
|
+
setFilters([]);
|
|
1503
1492
|
setJoinOperator("and");
|
|
1504
1493
|
}, [setFilters, setJoinOperator]);
|
|
1505
1494
|
react.useEffect(() => {
|
|
@@ -1963,11 +1952,7 @@ function DataTableFilterMenu({ table, debounceMs = DEBOUNCE_MS$1, throttleMs = T
|
|
|
1963
1952
|
setSelectedColumn(null);
|
|
1964
1953
|
}
|
|
1965
1954
|
}, [inputValue, selectedColumn]);
|
|
1966
|
-
const
|
|
1967
|
-
const [filters, setFilters] = useReadableFilters(columns, {
|
|
1968
|
-
...queryStateOptions,
|
|
1969
|
-
debounceMs: 0
|
|
1970
|
-
});
|
|
1955
|
+
const [filters, setFilters] = useReadableFilters(columns, { debounceMs });
|
|
1971
1956
|
const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);
|
|
1972
1957
|
const onFilterAdd = react.useCallback((column, value) => {
|
|
1973
1958
|
if (!value.trim() && column.columnDef.meta?.variant !== "boolean") return;
|
|
@@ -2684,7 +2669,7 @@ function DataTableViewOptions({ table, disabled,...props }) {
|
|
|
2684
2669
|
role: "combobox",
|
|
2685
2670
|
variant: "outline",
|
|
2686
2671
|
size: "sm",
|
|
2687
|
-
className: "ml-auto
|
|
2672
|
+
className: "ml-auto flex h-8 font-normal",
|
|
2688
2673
|
disabled,
|
|
2689
2674
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Settings2, { className: "text-muted-foreground" }), "View"]
|
|
2690
2675
|
})
|
|
@@ -2749,51 +2734,142 @@ function AutoTableSimpleFilters({ table, shallow = false, filters: externalFilte
|
|
|
2749
2734
|
setFilters
|
|
2750
2735
|
]);
|
|
2751
2736
|
const hasFilters = filters.length > 0;
|
|
2737
|
+
const filterColumns = react.useMemo(() => columns.filter((col) => col.columnDef.meta?.variant && col.columnDef.enableColumnFilter !== false), [columns]);
|
|
2738
|
+
const [expanded, setExpanded] = react.useState(false);
|
|
2739
|
+
const [visibleCount, setVisibleCount] = react.useState(null);
|
|
2740
|
+
const containerRef = react.useRef(null);
|
|
2741
|
+
const calcVisibleCount = react.useCallback(() => {
|
|
2742
|
+
const el = containerRef.current;
|
|
2743
|
+
if (!el) return;
|
|
2744
|
+
const items = el.querySelectorAll("[data-filter-item]");
|
|
2745
|
+
if (items.length === 0) return;
|
|
2746
|
+
const restoreList = [];
|
|
2747
|
+
el.querySelectorAll(".hidden").forEach((node) => {
|
|
2748
|
+
const htmlNode = node;
|
|
2749
|
+
htmlNode.classList.remove("hidden");
|
|
2750
|
+
restoreList.push(htmlNode);
|
|
2751
|
+
});
|
|
2752
|
+
el.style.flex = "1 1 0%";
|
|
2753
|
+
el.offsetHeight;
|
|
2754
|
+
const firstTop = items[0].offsetTop;
|
|
2755
|
+
let count = 0;
|
|
2756
|
+
for (const item of Array.from(items)) {
|
|
2757
|
+
if (item.offsetTop > firstTop) break;
|
|
2758
|
+
count++;
|
|
2759
|
+
}
|
|
2760
|
+
const btn = el.querySelector("[data-filter-toggle]");
|
|
2761
|
+
if (btn && count < items.length && count > 0) {
|
|
2762
|
+
if (btn.offsetTop > firstTop) count--;
|
|
2763
|
+
}
|
|
2764
|
+
el.style.flex = "";
|
|
2765
|
+
restoreList.forEach((node) => node.classList.add("hidden"));
|
|
2766
|
+
setVisibleCount(count >= items.length ? null : count);
|
|
2767
|
+
}, []);
|
|
2768
|
+
react.useLayoutEffect(() => {
|
|
2769
|
+
if (!mounted) return;
|
|
2770
|
+
calcVisibleCount();
|
|
2771
|
+
}, [
|
|
2772
|
+
mounted,
|
|
2773
|
+
filterColumns.length,
|
|
2774
|
+
calcVisibleCount
|
|
2775
|
+
]);
|
|
2776
|
+
react.useEffect(() => {
|
|
2777
|
+
const parent = containerRef.current?.closest("[data-filter-parent]");
|
|
2778
|
+
if (!mounted || !parent) return;
|
|
2779
|
+
let lastWidth = parent.offsetWidth;
|
|
2780
|
+
let rafId = 0;
|
|
2781
|
+
const ro = new ResizeObserver(() => {
|
|
2782
|
+
const newWidth = parent.offsetWidth;
|
|
2783
|
+
if (newWidth === lastWidth) return;
|
|
2784
|
+
lastWidth = newWidth;
|
|
2785
|
+
cancelAnimationFrame(rafId);
|
|
2786
|
+
rafId = requestAnimationFrame(calcVisibleCount);
|
|
2787
|
+
});
|
|
2788
|
+
ro.observe(parent);
|
|
2789
|
+
return () => {
|
|
2790
|
+
cancelAnimationFrame(rafId);
|
|
2791
|
+
ro.disconnect();
|
|
2792
|
+
};
|
|
2793
|
+
}, [
|
|
2794
|
+
mounted,
|
|
2795
|
+
filterColumns.length,
|
|
2796
|
+
calcVisibleCount
|
|
2797
|
+
]);
|
|
2752
2798
|
if (!mounted) return null;
|
|
2753
2799
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2800
|
+
ref: containerRef,
|
|
2801
|
+
className: "flex flex-wrap items-center gap-2 min-w-0",
|
|
2802
|
+
children: [
|
|
2803
|
+
filterColumns.map((column, index) => {
|
|
2804
|
+
const meta = column.columnDef.meta;
|
|
2805
|
+
const value = getFilterValue(column.id);
|
|
2806
|
+
const isHidden = !expanded && visibleCount !== null && index >= visibleCount;
|
|
2807
|
+
switch (meta.variant) {
|
|
2808
|
+
case "text": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2809
|
+
className: isHidden ? "hidden" : "",
|
|
2810
|
+
"data-filter-item": true,
|
|
2811
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Input, {
|
|
2812
|
+
placeholder: meta.placeholder ?? meta.label ?? column.id,
|
|
2813
|
+
value: typeof value === "string" ? value : "",
|
|
2814
|
+
onChange: (e) => updateFilter(column.id, e.target.value || void 0),
|
|
2815
|
+
className: "h-8 w-36 shrink-0"
|
|
2816
|
+
})
|
|
2817
|
+
}, column.id);
|
|
2818
|
+
case "select":
|
|
2819
|
+
case "multiSelect": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2820
|
+
className: isHidden ? "hidden" : "",
|
|
2821
|
+
"data-filter-item": true,
|
|
2822
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SimpleFacetedFilter, {
|
|
2823
|
+
title: meta.label ?? column.id,
|
|
2824
|
+
options: meta.options ?? [],
|
|
2825
|
+
multiple: meta.variant === "multiSelect",
|
|
2826
|
+
value: Array.isArray(value) ? value : value ? [value] : [],
|
|
2827
|
+
onChange: (v) => updateFilter(column.id, v.length ? v : void 0)
|
|
2828
|
+
})
|
|
2829
|
+
}, column.id);
|
|
2830
|
+
case "range": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2831
|
+
className: isHidden ? "hidden" : "",
|
|
2832
|
+
"data-filter-item": true,
|
|
2833
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SimpleSliderFilter, {
|
|
2834
|
+
title: meta.label ?? column.id,
|
|
2835
|
+
range: meta.range,
|
|
2836
|
+
unit: meta.unit,
|
|
2837
|
+
value: Array.isArray(value) ? value.map(Number) : void 0,
|
|
2838
|
+
onChange: (v) => updateFilter(column.id, v ? v.map(String) : void 0)
|
|
2839
|
+
})
|
|
2840
|
+
}, column.id);
|
|
2841
|
+
case "date":
|
|
2842
|
+
case "dateRange": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2843
|
+
className: isHidden ? "hidden" : "",
|
|
2844
|
+
"data-filter-item": true,
|
|
2845
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SimpleDateFilter, {
|
|
2846
|
+
title: meta.label ?? column.id,
|
|
2847
|
+
multiple: meta.variant === "dateRange",
|
|
2848
|
+
value,
|
|
2849
|
+
onChange: (v) => updateFilter(column.id, v)
|
|
2850
|
+
})
|
|
2851
|
+
}, column.id);
|
|
2852
|
+
default: return null;
|
|
2853
|
+
}
|
|
2854
|
+
}),
|
|
2855
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
2856
|
+
"data-filter-toggle": true,
|
|
2857
|
+
variant: "ghost",
|
|
2858
|
+
size: "icon",
|
|
2859
|
+
className: cn("h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground", visibleCount === null && "hidden"),
|
|
2860
|
+
onClick: () => setExpanded((prev) => !prev),
|
|
2861
|
+
"aria-label": expanded ? "收起筛选" : "展开更多筛选",
|
|
2862
|
+
"aria-expanded": expanded,
|
|
2863
|
+
children: expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronUp, { className: "size-4" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "size-4" })
|
|
2864
|
+
}),
|
|
2865
|
+
hasFilters && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.Button, {
|
|
2866
|
+
variant: "outline",
|
|
2867
|
+
size: "sm",
|
|
2868
|
+
className: "h-8 border-dashed shrink-0",
|
|
2869
|
+
onClick: () => setFilters([]),
|
|
2870
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { className: "size-4" }), "Reset"]
|
|
2871
|
+
})
|
|
2872
|
+
]
|
|
2797
2873
|
});
|
|
2798
2874
|
}
|
|
2799
2875
|
function SimpleFacetedFilter({ title, options, multiple, value, onChange }) {
|
|
@@ -3515,352 +3591,44 @@ function ActionBarSeparator(props) {
|
|
|
3515
3591
|
}
|
|
3516
3592
|
|
|
3517
3593
|
//#endregion
|
|
3518
|
-
//#region src/
|
|
3519
|
-
function
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
const cellValue = row.getValue(header);
|
|
3524
|
-
return typeof cellValue === "string" ? `"${cellValue.replace(/"/g, "\"\"")}"` : cellValue;
|
|
3525
|
-
}).join(","))].join("\n");
|
|
3526
|
-
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
|
3527
|
-
const url = URL.createObjectURL(blob);
|
|
3528
|
-
const link = document.createElement("a");
|
|
3529
|
-
link.setAttribute("href", url);
|
|
3530
|
-
link.setAttribute("download", `${filename}.csv`);
|
|
3531
|
-
link.style.visibility = "hidden";
|
|
3532
|
-
document.body.appendChild(link);
|
|
3533
|
-
link.click();
|
|
3534
|
-
document.body.removeChild(link);
|
|
3535
|
-
}
|
|
3536
|
-
|
|
3537
|
-
//#endregion
|
|
3538
|
-
//#region src/components/auto-crud/auto-table-action-bar.tsx
|
|
3539
|
-
function AutoTableActionBar({ table, onDeleteSelected, onUpdateSelected, batchUpdateFields, enableExport = true, enableDelete = true, extraActions }) {
|
|
3540
|
-
const rows = table.getFilteredSelectedRowModel().rows;
|
|
3541
|
-
const onOpenChange = react.useCallback((open) => {
|
|
3542
|
-
if (!open) table.toggleAllRowsSelected(false);
|
|
3543
|
-
}, [table]);
|
|
3544
|
-
const onExport = react.useCallback(() => {
|
|
3545
|
-
exportTableToCSV(table, {
|
|
3546
|
-
excludeColumns: ["select", "actions"],
|
|
3547
|
-
onlySelected: true
|
|
3548
|
-
});
|
|
3549
|
-
}, [table]);
|
|
3550
|
-
const onDelete = react.useCallback(() => {
|
|
3551
|
-
if (onDeleteSelected) onDeleteSelected(rows.map((row) => row.original));
|
|
3552
|
-
}, [rows, onDeleteSelected]);
|
|
3553
|
-
const handleBatchUpdate = react.useCallback((field, value) => {
|
|
3554
|
-
if (onUpdateSelected) {
|
|
3555
|
-
onUpdateSelected(rows.map((row) => row.original), { [field]: value });
|
|
3556
|
-
table.toggleAllRowsSelected(false);
|
|
3557
|
-
}
|
|
3558
|
-
}, [
|
|
3559
|
-
rows,
|
|
3560
|
-
onUpdateSelected,
|
|
3561
|
-
table
|
|
3562
|
-
]);
|
|
3563
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ActionBar, {
|
|
3564
|
-
open: rows.length > 0,
|
|
3565
|
-
onOpenChange,
|
|
3566
|
-
children: [
|
|
3567
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ActionBarSelection, { children: [
|
|
3568
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3569
|
-
className: "font-medium",
|
|
3570
|
-
children: rows.length
|
|
3571
|
-
}),
|
|
3572
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "selected" }),
|
|
3573
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionBarSeparator, {}),
|
|
3574
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionBarClose, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, {}) })
|
|
3575
|
-
] }),
|
|
3576
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionBarSeparator, {}),
|
|
3577
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ActionBarGroup, { children: [
|
|
3578
|
-
batchUpdateFields?.map((fieldConfig) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuTrigger, {
|
|
3579
|
-
asChild: true,
|
|
3580
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.Button, {
|
|
3581
|
-
variant: "ghost",
|
|
3582
|
-
size: "sm",
|
|
3583
|
-
className: "h-7 gap-1 px-2",
|
|
3584
|
-
children: [fieldConfig.label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "size-3.5" })]
|
|
3585
|
-
})
|
|
3586
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuContent, {
|
|
3587
|
-
align: "start",
|
|
3588
|
-
children: fieldConfig.options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
3589
|
-
onClick: () => handleBatchUpdate(fieldConfig.field, option.value),
|
|
3590
|
-
children: option.label
|
|
3591
|
-
}, option.value))
|
|
3592
|
-
})] }, fieldConfig.field)),
|
|
3593
|
-
extraActions,
|
|
3594
|
-
enableExport && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ActionBarItem, {
|
|
3595
|
-
onClick: onExport,
|
|
3596
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Download, {}), "Export"]
|
|
3597
|
-
}),
|
|
3598
|
-
enableDelete && onDeleteSelected && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ActionBarItem, {
|
|
3599
|
-
variant: "destructive",
|
|
3600
|
-
onClick: onDelete,
|
|
3601
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Trash2, {}), "Delete"]
|
|
3602
|
-
})
|
|
3603
|
-
] })
|
|
3604
|
-
]
|
|
3594
|
+
//#region src/components/data-table/data-table-column-header.tsx
|
|
3595
|
+
function DataTableColumnHeader({ column, label, className,...props }) {
|
|
3596
|
+
if (!column.getCanSort() && !column.getCanHide()) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3597
|
+
className: cn(className),
|
|
3598
|
+
children: label
|
|
3605
3599
|
});
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
})
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
if (!result.success) return null;
|
|
3639
|
-
if (validKeys && result.data.some((item) => !validKeys.has(item.id))) return null;
|
|
3640
|
-
return result.data;
|
|
3641
|
-
} catch {
|
|
3642
|
-
return null;
|
|
3643
|
-
}
|
|
3644
|
-
},
|
|
3645
|
-
serialize: (value) => JSON.stringify(value)
|
|
3646
|
-
});
|
|
3647
|
-
};
|
|
3648
|
-
const filterItemSchema = zod.z.object({
|
|
3649
|
-
id: zod.z.string(),
|
|
3650
|
-
value: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]),
|
|
3651
|
-
variant: zod.z.enum(dataTableConfig.filterVariants),
|
|
3652
|
-
operator: zod.z.enum(dataTableConfig.operators),
|
|
3653
|
-
filterId: zod.z.string()
|
|
3654
|
-
});
|
|
3655
|
-
|
|
3656
|
-
//#endregion
|
|
3657
|
-
//#region src/hooks/use-data-table.ts
|
|
3658
|
-
const PAGE_KEY = "page";
|
|
3659
|
-
const PER_PAGE_KEY = "perPage";
|
|
3660
|
-
const SORT_KEY = "sort";
|
|
3661
|
-
const FILTERS_KEY = "filters";
|
|
3662
|
-
const JOIN_OPERATOR_KEY = "joinOperator";
|
|
3663
|
-
const ARRAY_SEPARATOR = ",";
|
|
3664
|
-
const DEBOUNCE_MS = 300;
|
|
3665
|
-
const THROTTLE_MS = 50;
|
|
3666
|
-
function useDataTable(props) {
|
|
3667
|
-
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;
|
|
3668
|
-
const pageKey = queryKeys?.page ?? PAGE_KEY;
|
|
3669
|
-
const perPageKey = queryKeys?.perPage ?? PER_PAGE_KEY;
|
|
3670
|
-
const sortKey = queryKeys?.sort ?? SORT_KEY;
|
|
3671
|
-
const filtersKey = queryKeys?.filters ?? FILTERS_KEY;
|
|
3672
|
-
const joinOperatorKey = queryKeys?.joinOperator ?? JOIN_OPERATOR_KEY;
|
|
3673
|
-
const queryStateOptions = react.useMemo(() => ({
|
|
3674
|
-
history,
|
|
3675
|
-
scroll,
|
|
3676
|
-
shallow,
|
|
3677
|
-
throttleMs,
|
|
3678
|
-
debounceMs,
|
|
3679
|
-
clearOnDefault,
|
|
3680
|
-
startTransition
|
|
3681
|
-
}), [
|
|
3682
|
-
history,
|
|
3683
|
-
scroll,
|
|
3684
|
-
shallow,
|
|
3685
|
-
throttleMs,
|
|
3686
|
-
debounceMs,
|
|
3687
|
-
clearOnDefault,
|
|
3688
|
-
startTransition
|
|
3689
|
-
]);
|
|
3690
|
-
const [rowSelection, setRowSelection] = react.useState(initialState?.rowSelection ?? {});
|
|
3691
|
-
const [columnVisibility, setColumnVisibility] = react.useState(initialState?.columnVisibility ?? {});
|
|
3692
|
-
const [page, setPage] = useQueryState(pageKey, parseAsInteger.withDefault(1));
|
|
3693
|
-
const [perPage, setPerPage] = useQueryState(perPageKey, parseAsInteger.withDefault(initialState?.pagination?.pageSize ?? 10));
|
|
3694
|
-
const pagination = react.useMemo(() => {
|
|
3695
|
-
return {
|
|
3696
|
-
pageIndex: page - 1,
|
|
3697
|
-
pageSize: perPage
|
|
3698
|
-
};
|
|
3699
|
-
}, [page, perPage]);
|
|
3700
|
-
const onPaginationChange = react.useCallback((updaterOrValue) => {
|
|
3701
|
-
if (typeof updaterOrValue === "function") {
|
|
3702
|
-
const newPagination = updaterOrValue(pagination);
|
|
3703
|
-
setPage(newPagination.pageIndex + 1);
|
|
3704
|
-
setPerPage(newPagination.pageSize);
|
|
3705
|
-
} else {
|
|
3706
|
-
setPage(updaterOrValue.pageIndex + 1);
|
|
3707
|
-
setPerPage(updaterOrValue.pageSize);
|
|
3708
|
-
}
|
|
3709
|
-
}, [
|
|
3710
|
-
pagination,
|
|
3711
|
-
setPage,
|
|
3712
|
-
setPerPage
|
|
3713
|
-
]);
|
|
3714
|
-
const [sorting, setSorting] = useQueryState(sortKey, getSortingStateParser(react.useMemo(() => {
|
|
3715
|
-
return new Set(columns.map((column) => column.id).filter(Boolean));
|
|
3716
|
-
}, [columns])).withDefault(initialState?.sorting ?? []));
|
|
3717
|
-
const onSortingChange = react.useCallback((updaterOrValue) => {
|
|
3718
|
-
if (typeof updaterOrValue === "function") setSorting(updaterOrValue(sorting));
|
|
3719
|
-
else setSorting(updaterOrValue);
|
|
3720
|
-
}, [sorting, setSorting]);
|
|
3721
|
-
const filterableColumns = react.useMemo(() => {
|
|
3722
|
-
if (enableAdvancedFilter) return [];
|
|
3723
|
-
return columns.filter((column) => column.enableColumnFilter);
|
|
3724
|
-
}, [columns, enableAdvancedFilter]);
|
|
3725
|
-
const [filterValues, setFilterValues] = useQueryStates(react.useMemo(() => {
|
|
3726
|
-
if (enableAdvancedFilter) return {};
|
|
3727
|
-
return filterableColumns.reduce((acc, column) => {
|
|
3728
|
-
if (column.meta?.options) acc[column.id ?? ""] = parseAsArrayOf(parseAsString, ARRAY_SEPARATOR).withDefault([]);
|
|
3729
|
-
else acc[column.id ?? ""] = parseAsString.withDefault("");
|
|
3730
|
-
return acc;
|
|
3731
|
-
}, {});
|
|
3732
|
-
}, [filterableColumns, enableAdvancedFilter]));
|
|
3733
|
-
const debouncedSetFilterValues = useDebouncedCallback((values) => {
|
|
3734
|
-
setPage(1);
|
|
3735
|
-
setFilterValues(values);
|
|
3736
|
-
}, debounceMs);
|
|
3737
|
-
const initialColumnFilters = react.useMemo(() => {
|
|
3738
|
-
if (enableAdvancedFilter) return [];
|
|
3739
|
-
return Object.entries(filterValues).reduce((filters, [key, value]) => {
|
|
3740
|
-
if (value !== null) {
|
|
3741
|
-
const processedValue = Array.isArray(value) ? value : typeof value === "string" && /[^a-zA-Z0-9]/.test(value) ? value.split(/[^a-zA-Z0-9]+/).filter(Boolean) : [value];
|
|
3742
|
-
filters.push({
|
|
3743
|
-
id: key,
|
|
3744
|
-
value: processedValue
|
|
3745
|
-
});
|
|
3746
|
-
}
|
|
3747
|
-
return filters;
|
|
3748
|
-
}, []);
|
|
3749
|
-
}, [filterValues, enableAdvancedFilter]);
|
|
3750
|
-
const [columnFilters, setColumnFilters] = react.useState(initialColumnFilters);
|
|
3751
|
-
const onColumnFiltersChange = react.useCallback((updaterOrValue) => {
|
|
3752
|
-
if (enableAdvancedFilter) return;
|
|
3753
|
-
setColumnFilters((prev) => {
|
|
3754
|
-
const next = typeof updaterOrValue === "function" ? updaterOrValue(prev) : updaterOrValue;
|
|
3755
|
-
const filterUpdates = next.reduce((acc, filter) => {
|
|
3756
|
-
if (filterableColumns.find((column) => column.id === filter.id)) acc[filter.id] = filter.value;
|
|
3757
|
-
return acc;
|
|
3758
|
-
}, {});
|
|
3759
|
-
for (const prevFilter of prev) if (!next.some((filter) => filter.id === prevFilter.id)) filterUpdates[prevFilter.id] = null;
|
|
3760
|
-
debouncedSetFilterValues(filterUpdates);
|
|
3761
|
-
return next;
|
|
3762
|
-
});
|
|
3763
|
-
}, [
|
|
3764
|
-
debouncedSetFilterValues,
|
|
3765
|
-
filterableColumns,
|
|
3766
|
-
enableAdvancedFilter
|
|
3767
|
-
]);
|
|
3768
|
-
const table = (0, __tanstack_react_table.useReactTable)({
|
|
3769
|
-
...tableProps,
|
|
3770
|
-
columns,
|
|
3771
|
-
initialState,
|
|
3772
|
-
pageCount,
|
|
3773
|
-
state: {
|
|
3774
|
-
pagination,
|
|
3775
|
-
sorting,
|
|
3776
|
-
columnVisibility,
|
|
3777
|
-
rowSelection,
|
|
3778
|
-
columnFilters
|
|
3779
|
-
},
|
|
3780
|
-
defaultColumn: {
|
|
3781
|
-
...tableProps.defaultColumn,
|
|
3782
|
-
enableColumnFilter: false
|
|
3783
|
-
},
|
|
3784
|
-
enableRowSelection: true,
|
|
3785
|
-
onRowSelectionChange: setRowSelection,
|
|
3786
|
-
onPaginationChange,
|
|
3787
|
-
onSortingChange,
|
|
3788
|
-
onColumnFiltersChange,
|
|
3789
|
-
onColumnVisibilityChange: setColumnVisibility,
|
|
3790
|
-
getCoreRowModel: (0, __tanstack_react_table.getCoreRowModel)(),
|
|
3791
|
-
getFilteredRowModel: (0, __tanstack_react_table.getFilteredRowModel)(),
|
|
3792
|
-
getPaginationRowModel: (0, __tanstack_react_table.getPaginationRowModel)(),
|
|
3793
|
-
getSortedRowModel: (0, __tanstack_react_table.getSortedRowModel)(),
|
|
3794
|
-
getFacetedRowModel: (0, __tanstack_react_table.getFacetedRowModel)(),
|
|
3795
|
-
getFacetedUniqueValues: (0, __tanstack_react_table.getFacetedUniqueValues)(),
|
|
3796
|
-
getFacetedMinMaxValues: (0, __tanstack_react_table.getFacetedMinMaxValues)(),
|
|
3797
|
-
manualPagination: true,
|
|
3798
|
-
manualSorting: true,
|
|
3799
|
-
manualFiltering: true,
|
|
3800
|
-
meta: {
|
|
3801
|
-
...tableProps.meta,
|
|
3802
|
-
queryKeys: {
|
|
3803
|
-
page: pageKey,
|
|
3804
|
-
perPage: perPageKey,
|
|
3805
|
-
sort: sortKey,
|
|
3806
|
-
filters: filtersKey,
|
|
3807
|
-
joinOperator: joinOperatorKey
|
|
3808
|
-
},
|
|
3809
|
-
queryStateOptions
|
|
3810
|
-
}
|
|
3811
|
-
});
|
|
3812
|
-
return react.useMemo(() => ({
|
|
3813
|
-
table,
|
|
3814
|
-
shallow,
|
|
3815
|
-
debounceMs,
|
|
3816
|
-
throttleMs
|
|
3817
|
-
}), [
|
|
3818
|
-
table,
|
|
3819
|
-
shallow,
|
|
3820
|
-
debounceMs,
|
|
3821
|
-
throttleMs
|
|
3822
|
-
]);
|
|
3823
|
-
}
|
|
3824
|
-
|
|
3825
|
-
//#endregion
|
|
3826
|
-
//#region src/components/data-table/data-table-column-header.tsx
|
|
3827
|
-
function DataTableColumnHeader({ column, label, className,...props }) {
|
|
3828
|
-
if (!column.getCanSort() && !column.getCanHide()) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3829
|
-
className: cn(className),
|
|
3830
|
-
children: label
|
|
3831
|
-
});
|
|
3832
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuTrigger, {
|
|
3833
|
-
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),
|
|
3834
|
-
...props,
|
|
3835
|
-
children: [label, column.getCanSort() && (column.getIsSorted() === "desc" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, {}) : column.getIsSorted() === "asc" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronUp, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronsUpDown, {}))]
|
|
3836
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuContent, {
|
|
3837
|
-
align: "start",
|
|
3838
|
-
className: "w-28",
|
|
3839
|
-
children: [column.getCanSort() && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
3840
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuCheckboxItem, {
|
|
3841
|
-
className: "relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground",
|
|
3842
|
-
checked: column.getIsSorted() === "asc",
|
|
3843
|
-
onClick: () => column.toggleSorting(false),
|
|
3844
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronUp, {}), "Asc"]
|
|
3845
|
-
}),
|
|
3846
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuCheckboxItem, {
|
|
3847
|
-
className: "relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground",
|
|
3848
|
-
checked: column.getIsSorted() === "desc",
|
|
3849
|
-
onClick: () => column.toggleSorting(true),
|
|
3850
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, {}), "Desc"]
|
|
3851
|
-
}),
|
|
3852
|
-
column.getIsSorted() && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
3853
|
-
className: "pl-2 [&_svg]:text-muted-foreground",
|
|
3854
|
-
onClick: () => column.clearSorting(),
|
|
3855
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, {}), "Reset"]
|
|
3856
|
-
})
|
|
3857
|
-
] }), column.getCanHide() && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuCheckboxItem, {
|
|
3858
|
-
className: "relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground",
|
|
3859
|
-
checked: !column.getIsVisible(),
|
|
3860
|
-
onClick: () => column.toggleVisibility(false),
|
|
3861
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.EyeOff, {}), "Hide"]
|
|
3862
|
-
})]
|
|
3863
|
-
})] });
|
|
3600
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuTrigger, {
|
|
3601
|
+
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),
|
|
3602
|
+
...props,
|
|
3603
|
+
children: [label, column.getCanSort() && (column.getIsSorted() === "desc" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, {}) : column.getIsSorted() === "asc" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronUp, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronsUpDown, {}))]
|
|
3604
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuContent, {
|
|
3605
|
+
align: "start",
|
|
3606
|
+
className: "w-28",
|
|
3607
|
+
children: [column.getCanSort() && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
3608
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuCheckboxItem, {
|
|
3609
|
+
className: "relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground",
|
|
3610
|
+
checked: column.getIsSorted() === "asc",
|
|
3611
|
+
onClick: () => column.toggleSorting(false),
|
|
3612
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronUp, {}), "Asc"]
|
|
3613
|
+
}),
|
|
3614
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuCheckboxItem, {
|
|
3615
|
+
className: "relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground",
|
|
3616
|
+
checked: column.getIsSorted() === "desc",
|
|
3617
|
+
onClick: () => column.toggleSorting(true),
|
|
3618
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, {}), "Desc"]
|
|
3619
|
+
}),
|
|
3620
|
+
column.getIsSorted() && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
3621
|
+
className: "pl-2 [&_svg]:text-muted-foreground",
|
|
3622
|
+
onClick: () => column.clearSorting(),
|
|
3623
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, {}), "Reset"]
|
|
3624
|
+
})
|
|
3625
|
+
] }), column.getCanHide() && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuCheckboxItem, {
|
|
3626
|
+
className: "relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground",
|
|
3627
|
+
checked: !column.getIsVisible(),
|
|
3628
|
+
onClick: () => column.toggleVisibility(false),
|
|
3629
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.EyeOff, {}), "Hide"]
|
|
3630
|
+
})]
|
|
3631
|
+
})] });
|
|
3864
3632
|
}
|
|
3865
3633
|
|
|
3866
3634
|
//#endregion
|
|
@@ -4161,73 +3929,578 @@ function createTableSchema(schema, options) {
|
|
|
4161
3929
|
meta,
|
|
4162
3930
|
...restOverride
|
|
4163
3931
|
});
|
|
4164
|
-
}
|
|
4165
|
-
return columns;
|
|
4166
|
-
}
|
|
4167
|
-
/**
|
|
4168
|
-
* 创建选择列
|
|
4169
|
-
*/
|
|
4170
|
-
function createSelectColumn() {
|
|
4171
|
-
return {
|
|
4172
|
-
id: "select",
|
|
4173
|
-
header: ({ table }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Checkbox, {
|
|
4174
|
-
"aria-label": "Select all",
|
|
4175
|
-
className: "translate-y-0.5",
|
|
4176
|
-
checked: table.getIsAllPageRowsSelected() || table.getIsSomePageRowsSelected() && "indeterminate",
|
|
4177
|
-
onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value)
|
|
4178
|
-
}),
|
|
4179
|
-
cell: ({ row }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Checkbox, {
|
|
4180
|
-
"aria-label": "Select row",
|
|
4181
|
-
className: "translate-y-0.5",
|
|
4182
|
-
checked: row.getIsSelected(),
|
|
4183
|
-
onCheckedChange: (value) => row.toggleSelected(!!value)
|
|
4184
|
-
}),
|
|
4185
|
-
enableHiding: false,
|
|
4186
|
-
enableSorting: false,
|
|
4187
|
-
size: 40
|
|
4188
|
-
};
|
|
4189
|
-
}
|
|
4190
|
-
/**
|
|
4191
|
-
* 创建操作列
|
|
4192
|
-
*/
|
|
4193
|
-
function createActionsColumn(config) {
|
|
4194
|
-
const { onEdit, onDelete, onView, onCopy } = config;
|
|
4195
|
-
return {
|
|
4196
|
-
id: "actions",
|
|
4197
|
-
cell: ({ row }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuTrigger, {
|
|
4198
|
-
asChild: true,
|
|
4199
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
4200
|
-
"aria-label": "Open menu",
|
|
4201
|
-
variant: "ghost",
|
|
4202
|
-
className: "flex size-8 p-0 data-[state=open]:bg-muted",
|
|
4203
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Ellipsis, { className: "size-4" })
|
|
4204
|
-
})
|
|
4205
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuContent, {
|
|
4206
|
-
align: "end",
|
|
4207
|
-
className: "w-40",
|
|
4208
|
-
children: [
|
|
4209
|
-
onView && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
4210
|
-
onClick: () => onView(row.original),
|
|
4211
|
-
children: "查看"
|
|
4212
|
-
}),
|
|
4213
|
-
onEdit && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
4214
|
-
onClick: () => onEdit(row.original),
|
|
4215
|
-
children: "编辑"
|
|
4216
|
-
}),
|
|
4217
|
-
onCopy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
4218
|
-
onClick: () => onCopy(row.original),
|
|
4219
|
-
children: "复制"
|
|
4220
|
-
}),
|
|
4221
|
-
(onView || onEdit || onCopy) && onDelete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuSeparator, {}),
|
|
4222
|
-
onDelete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
4223
|
-
className: "text-destructive",
|
|
4224
|
-
onClick: () => onDelete(row.original),
|
|
4225
|
-
children: "删除"
|
|
4226
|
-
})
|
|
4227
|
-
]
|
|
4228
|
-
})] }),
|
|
4229
|
-
size: 40
|
|
4230
|
-
};
|
|
3932
|
+
}
|
|
3933
|
+
return columns;
|
|
3934
|
+
}
|
|
3935
|
+
/**
|
|
3936
|
+
* 创建选择列
|
|
3937
|
+
*/
|
|
3938
|
+
function createSelectColumn() {
|
|
3939
|
+
return {
|
|
3940
|
+
id: "select",
|
|
3941
|
+
header: ({ table }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Checkbox, {
|
|
3942
|
+
"aria-label": "Select all",
|
|
3943
|
+
className: "translate-y-0.5",
|
|
3944
|
+
checked: table.getIsAllPageRowsSelected() || table.getIsSomePageRowsSelected() && "indeterminate",
|
|
3945
|
+
onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value)
|
|
3946
|
+
}),
|
|
3947
|
+
cell: ({ row }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Checkbox, {
|
|
3948
|
+
"aria-label": "Select row",
|
|
3949
|
+
className: "translate-y-0.5",
|
|
3950
|
+
checked: row.getIsSelected(),
|
|
3951
|
+
onCheckedChange: (value) => row.toggleSelected(!!value)
|
|
3952
|
+
}),
|
|
3953
|
+
enableHiding: false,
|
|
3954
|
+
enableSorting: false,
|
|
3955
|
+
size: 40
|
|
3956
|
+
};
|
|
3957
|
+
}
|
|
3958
|
+
/**
|
|
3959
|
+
* 创建操作列
|
|
3960
|
+
*/
|
|
3961
|
+
function createActionsColumn(config) {
|
|
3962
|
+
const { onEdit, onDelete, onView, onCopy } = config;
|
|
3963
|
+
return {
|
|
3964
|
+
id: "actions",
|
|
3965
|
+
cell: ({ row }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuTrigger, {
|
|
3966
|
+
asChild: true,
|
|
3967
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
3968
|
+
"aria-label": "Open menu",
|
|
3969
|
+
variant: "ghost",
|
|
3970
|
+
className: "flex size-8 p-0 data-[state=open]:bg-muted",
|
|
3971
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Ellipsis, { className: "size-4" })
|
|
3972
|
+
})
|
|
3973
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuContent, {
|
|
3974
|
+
align: "end",
|
|
3975
|
+
className: "w-40",
|
|
3976
|
+
children: [
|
|
3977
|
+
onView && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
3978
|
+
onClick: () => onView(row.original),
|
|
3979
|
+
children: "查看"
|
|
3980
|
+
}),
|
|
3981
|
+
onEdit && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
3982
|
+
onClick: () => onEdit(row.original),
|
|
3983
|
+
children: "编辑"
|
|
3984
|
+
}),
|
|
3985
|
+
onCopy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
3986
|
+
onClick: () => onCopy(row.original),
|
|
3987
|
+
children: "复制"
|
|
3988
|
+
}),
|
|
3989
|
+
(onView || onEdit || onCopy) && onDelete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuSeparator, {}),
|
|
3990
|
+
onDelete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
3991
|
+
className: "text-destructive",
|
|
3992
|
+
onClick: () => onDelete(row.original),
|
|
3993
|
+
children: "删除"
|
|
3994
|
+
})
|
|
3995
|
+
]
|
|
3996
|
+
})] }),
|
|
3997
|
+
size: 40
|
|
3998
|
+
};
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
//#endregion
|
|
4002
|
+
//#region src/lib/import.ts
|
|
4003
|
+
/**
|
|
4004
|
+
* 解析 CSV 字符串为对象数组
|
|
4005
|
+
* 支持:引号包裹、逗号转义、换行符、空值
|
|
4006
|
+
*/
|
|
4007
|
+
function parseCSV(content) {
|
|
4008
|
+
const lines = parseCSVLines(content);
|
|
4009
|
+
if (lines.length === 0) return {
|
|
4010
|
+
headers: [],
|
|
4011
|
+
rows: []
|
|
4012
|
+
};
|
|
4013
|
+
const headers = lines[0];
|
|
4014
|
+
const rows = [];
|
|
4015
|
+
for (let i = 1; i < lines.length; i++) {
|
|
4016
|
+
const line = lines[i];
|
|
4017
|
+
if (line.length === 1 && line[0] === "") continue;
|
|
4018
|
+
const row = {};
|
|
4019
|
+
for (let j = 0; j < headers.length; j++) {
|
|
4020
|
+
const header = headers[j];
|
|
4021
|
+
row[header] = line[j] ?? "";
|
|
4022
|
+
}
|
|
4023
|
+
rows.push(row);
|
|
4024
|
+
}
|
|
4025
|
+
return {
|
|
4026
|
+
headers,
|
|
4027
|
+
rows
|
|
4028
|
+
};
|
|
4029
|
+
}
|
|
4030
|
+
/**
|
|
4031
|
+
* 将 CSV 文本解析为二维数组(处理引号转义)
|
|
4032
|
+
*/
|
|
4033
|
+
function parseCSVLines(text) {
|
|
4034
|
+
const result = [];
|
|
4035
|
+
let current = "";
|
|
4036
|
+
let inQuotes = false;
|
|
4037
|
+
let row = [];
|
|
4038
|
+
for (let i = 0; i < text.length; i++) {
|
|
4039
|
+
const char = text[i];
|
|
4040
|
+
const next = text[i + 1];
|
|
4041
|
+
if (inQuotes) if (char === "\"" && next === "\"") {
|
|
4042
|
+
current += "\"";
|
|
4043
|
+
i++;
|
|
4044
|
+
} else if (char === "\"") inQuotes = false;
|
|
4045
|
+
else current += char;
|
|
4046
|
+
else if (char === "\"") inQuotes = true;
|
|
4047
|
+
else if (char === ",") {
|
|
4048
|
+
row.push(current);
|
|
4049
|
+
current = "";
|
|
4050
|
+
} else if (char === "\r" && next === "\n") {
|
|
4051
|
+
row.push(current);
|
|
4052
|
+
current = "";
|
|
4053
|
+
result.push(row);
|
|
4054
|
+
row = [];
|
|
4055
|
+
i++;
|
|
4056
|
+
} else if (char === "\n") {
|
|
4057
|
+
row.push(current);
|
|
4058
|
+
current = "";
|
|
4059
|
+
result.push(row);
|
|
4060
|
+
row = [];
|
|
4061
|
+
} else current += char;
|
|
4062
|
+
}
|
|
4063
|
+
if (current !== "" || row.length > 0) {
|
|
4064
|
+
row.push(current);
|
|
4065
|
+
result.push(row);
|
|
4066
|
+
}
|
|
4067
|
+
return result;
|
|
4068
|
+
}
|
|
4069
|
+
/**
|
|
4070
|
+
* 解析 JSON 字符串为对象数组
|
|
4071
|
+
* 支持 JSON 数组或 { data: [...] } 格式
|
|
4072
|
+
*/
|
|
4073
|
+
function parseJSON(content) {
|
|
4074
|
+
const parsed = JSON.parse(content);
|
|
4075
|
+
if (Array.isArray(parsed)) return parsed;
|
|
4076
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.data)) return parsed.data;
|
|
4077
|
+
throw new Error("Invalid JSON format: expected an array or { data: [...] } object");
|
|
4078
|
+
}
|
|
4079
|
+
/**
|
|
4080
|
+
* 统一解析入口:根据文件扩展名自动选择解析器
|
|
4081
|
+
*/
|
|
4082
|
+
async function parseImportFile(file) {
|
|
4083
|
+
const content = await file.text();
|
|
4084
|
+
if (file.name.split(".").pop()?.toLowerCase() === "json") {
|
|
4085
|
+
const rows$1 = parseJSON(content);
|
|
4086
|
+
return {
|
|
4087
|
+
headers: rows$1.length > 0 ? Object.keys(rows$1[0]) : [],
|
|
4088
|
+
rows: rows$1,
|
|
4089
|
+
format: "json"
|
|
4090
|
+
};
|
|
4091
|
+
}
|
|
4092
|
+
const { headers, rows } = parseCSV(content);
|
|
4093
|
+
return {
|
|
4094
|
+
headers,
|
|
4095
|
+
rows,
|
|
4096
|
+
format: "csv"
|
|
4097
|
+
};
|
|
4098
|
+
}
|
|
4099
|
+
/**
|
|
4100
|
+
* 生成 CSV 模板(仅包含表头,用于下载空模板)
|
|
4101
|
+
*/
|
|
4102
|
+
function generateCSVTemplate(headers) {
|
|
4103
|
+
return headers.map((h) => escapeCSVField(h)).join(",") + "\n";
|
|
4104
|
+
}
|
|
4105
|
+
/**
|
|
4106
|
+
* 将数据数组转换为 CSV 字符串
|
|
4107
|
+
*/
|
|
4108
|
+
function dataToCSV(data, opts = {}) {
|
|
4109
|
+
if (data.length === 0) return "";
|
|
4110
|
+
const allHeaders = Object.keys(data[0]);
|
|
4111
|
+
const headers = (opts.headers ?? allHeaders).filter((h) => !opts.excludeColumns?.includes(h));
|
|
4112
|
+
return [headers.map((h) => escapeCSVField(h)).join(","), ...data.map((row) => headers.map((h) => escapeCSVField(String(row[h] ?? ""))).join(","))].join("\n");
|
|
4113
|
+
}
|
|
4114
|
+
function escapeCSVField(field) {
|
|
4115
|
+
if (field.includes(",") || field.includes("\"") || field.includes("\n") || field.includes("\r")) return `"${field.replace(/"/g, "\"\"")}"`;
|
|
4116
|
+
return field;
|
|
4117
|
+
}
|
|
4118
|
+
/**
|
|
4119
|
+
* 根据 Zod Schema 将 CSV 解析出的 string 值转换为正确类型
|
|
4120
|
+
*
|
|
4121
|
+
* CSV 解析后所有值都是 string,需要根据 schema 字段类型进行转换:
|
|
4122
|
+
* - number: parseFloat
|
|
4123
|
+
* - boolean: "true"/"1"/"yes" → true, 其他 → false
|
|
4124
|
+
* - date: new Date(value)
|
|
4125
|
+
* - 空字符串 → undefined(让 schema 默认值生效)
|
|
4126
|
+
*/
|
|
4127
|
+
function coerceRowValues(rows, schema) {
|
|
4128
|
+
const shape = schema.shape;
|
|
4129
|
+
const fieldTypes = /* @__PURE__ */ new Map();
|
|
4130
|
+
for (const [key, fieldSchema] of Object.entries(shape)) {
|
|
4131
|
+
const parsed = parseZodField(fieldSchema);
|
|
4132
|
+
fieldTypes.set(key, parsed.type);
|
|
4133
|
+
}
|
|
4134
|
+
return rows.map((row) => {
|
|
4135
|
+
const coerced = {};
|
|
4136
|
+
for (const [key, value] of Object.entries(row)) {
|
|
4137
|
+
const type = fieldTypes.get(key);
|
|
4138
|
+
if (!type) {
|
|
4139
|
+
coerced[key] = value;
|
|
4140
|
+
continue;
|
|
4141
|
+
}
|
|
4142
|
+
if (value === "" || value === null || value === void 0) {
|
|
4143
|
+
coerced[key] = void 0;
|
|
4144
|
+
continue;
|
|
4145
|
+
}
|
|
4146
|
+
if (typeof value !== "string") {
|
|
4147
|
+
coerced[key] = value;
|
|
4148
|
+
continue;
|
|
4149
|
+
}
|
|
4150
|
+
switch (type) {
|
|
4151
|
+
case "number": {
|
|
4152
|
+
const num = parseFloat(value);
|
|
4153
|
+
coerced[key] = isNaN(num) ? void 0 : num;
|
|
4154
|
+
break;
|
|
4155
|
+
}
|
|
4156
|
+
case "boolean": {
|
|
4157
|
+
const lower = value.toLowerCase().trim();
|
|
4158
|
+
coerced[key] = lower === "true" || lower === "1" || lower === "yes";
|
|
4159
|
+
break;
|
|
4160
|
+
}
|
|
4161
|
+
case "date": {
|
|
4162
|
+
const date = new Date(value);
|
|
4163
|
+
coerced[key] = isNaN(date.getTime()) ? void 0 : date;
|
|
4164
|
+
break;
|
|
4165
|
+
}
|
|
4166
|
+
default: coerced[key] = value;
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
return coerced;
|
|
4170
|
+
});
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
//#endregion
|
|
4174
|
+
//#region src/lib/export.ts
|
|
4175
|
+
function exportTableToCSV(table, opts = {}) {
|
|
4176
|
+
const { filename = "table", excludeColumns = [], onlySelected = false } = opts;
|
|
4177
|
+
const headers = table.getAllLeafColumns().map((column) => column.id).filter((id) => !excludeColumns.includes(id));
|
|
4178
|
+
downloadCSV([headers.join(","), ...(onlySelected ? table.getFilteredSelectedRowModel().rows : table.getRowModel().rows).map((row) => headers.map((header) => {
|
|
4179
|
+
const cellValue = row.getValue(header);
|
|
4180
|
+
return typeof cellValue === "string" ? `"${cellValue.replace(/"/g, "\"\"")}"` : cellValue;
|
|
4181
|
+
}).join(","))].join("\n"), filename);
|
|
4182
|
+
}
|
|
4183
|
+
/**
|
|
4184
|
+
* 导出原始数据数组为 CSV(用于服务端全量导出)
|
|
4185
|
+
*/
|
|
4186
|
+
function exportAllToCSV(data, opts = {}) {
|
|
4187
|
+
const { filename = "export",...csvOpts } = opts;
|
|
4188
|
+
downloadCSV(dataToCSV(data, csvOpts), filename);
|
|
4189
|
+
}
|
|
4190
|
+
/**
|
|
4191
|
+
* 下载 CSV 模板文件
|
|
4192
|
+
*/
|
|
4193
|
+
function downloadCSVTemplate(headers, filename = "template") {
|
|
4194
|
+
downloadCSV(headers.map((h) => {
|
|
4195
|
+
if (h.includes(",") || h.includes("\"") || h.includes("\n")) return `"${h.replace(/"/g, "\"\"")}"`;
|
|
4196
|
+
return h;
|
|
4197
|
+
}).join(",") + "\n", filename);
|
|
4198
|
+
}
|
|
4199
|
+
function downloadCSV(csvContent, filename) {
|
|
4200
|
+
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
|
4201
|
+
const url = URL.createObjectURL(blob);
|
|
4202
|
+
const link = document.createElement("a");
|
|
4203
|
+
link.setAttribute("href", url);
|
|
4204
|
+
link.setAttribute("download", `${filename}.csv`);
|
|
4205
|
+
link.style.visibility = "hidden";
|
|
4206
|
+
document.body.appendChild(link);
|
|
4207
|
+
link.click();
|
|
4208
|
+
document.body.removeChild(link);
|
|
4209
|
+
}
|
|
4210
|
+
|
|
4211
|
+
//#endregion
|
|
4212
|
+
//#region src/components/auto-crud/auto-table-action-bar.tsx
|
|
4213
|
+
function AutoTableActionBar({ table, onDeleteSelected, onUpdateSelected, batchUpdateFields, enableExport = true, enableDelete = true, extraActions }) {
|
|
4214
|
+
const rows = table.getFilteredSelectedRowModel().rows;
|
|
4215
|
+
const onOpenChange = react.useCallback((open) => {
|
|
4216
|
+
if (!open) table.toggleAllRowsSelected(false);
|
|
4217
|
+
}, [table]);
|
|
4218
|
+
const onExport = react.useCallback(() => {
|
|
4219
|
+
exportTableToCSV(table, {
|
|
4220
|
+
excludeColumns: ["select", "actions"],
|
|
4221
|
+
onlySelected: true
|
|
4222
|
+
});
|
|
4223
|
+
}, [table]);
|
|
4224
|
+
const onDelete = react.useCallback(() => {
|
|
4225
|
+
if (onDeleteSelected) onDeleteSelected(rows.map((row) => row.original));
|
|
4226
|
+
}, [rows, onDeleteSelected]);
|
|
4227
|
+
const handleBatchUpdate = react.useCallback((field, value) => {
|
|
4228
|
+
if (onUpdateSelected) {
|
|
4229
|
+
onUpdateSelected(rows.map((row) => row.original), { [field]: value });
|
|
4230
|
+
table.toggleAllRowsSelected(false);
|
|
4231
|
+
}
|
|
4232
|
+
}, [
|
|
4233
|
+
rows,
|
|
4234
|
+
onUpdateSelected,
|
|
4235
|
+
table
|
|
4236
|
+
]);
|
|
4237
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ActionBar, {
|
|
4238
|
+
open: rows.length > 0,
|
|
4239
|
+
onOpenChange,
|
|
4240
|
+
children: [
|
|
4241
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ActionBarSelection, { children: [
|
|
4242
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4243
|
+
className: "font-medium",
|
|
4244
|
+
children: rows.length
|
|
4245
|
+
}),
|
|
4246
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "selected" }),
|
|
4247
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionBarSeparator, {}),
|
|
4248
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionBarClose, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, {}) })
|
|
4249
|
+
] }),
|
|
4250
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionBarSeparator, {}),
|
|
4251
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ActionBarGroup, { children: [
|
|
4252
|
+
batchUpdateFields?.map((fieldConfig) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuTrigger, {
|
|
4253
|
+
asChild: true,
|
|
4254
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.Button, {
|
|
4255
|
+
variant: "ghost",
|
|
4256
|
+
size: "sm",
|
|
4257
|
+
className: "h-7 gap-1 px-2",
|
|
4258
|
+
children: [fieldConfig.label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "size-3.5" })]
|
|
4259
|
+
})
|
|
4260
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuContent, {
|
|
4261
|
+
align: "start",
|
|
4262
|
+
children: fieldConfig.options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
4263
|
+
onClick: () => handleBatchUpdate(fieldConfig.field, option.value),
|
|
4264
|
+
children: option.label
|
|
4265
|
+
}, option.value))
|
|
4266
|
+
})] }, fieldConfig.field)),
|
|
4267
|
+
extraActions,
|
|
4268
|
+
enableExport && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ActionBarItem, {
|
|
4269
|
+
onClick: onExport,
|
|
4270
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Download, {}), "Export"]
|
|
4271
|
+
}),
|
|
4272
|
+
enableDelete && onDeleteSelected && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ActionBarItem, {
|
|
4273
|
+
variant: "destructive",
|
|
4274
|
+
onClick: onDelete,
|
|
4275
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Trash2, {}), "Delete"]
|
|
4276
|
+
})
|
|
4277
|
+
] })
|
|
4278
|
+
]
|
|
4279
|
+
});
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
//#endregion
|
|
4283
|
+
//#region src/lib/parsers.ts
|
|
4284
|
+
/**
|
|
4285
|
+
* 创建自定义 Parser(替代 nuqs 的 createParser)
|
|
4286
|
+
*/
|
|
4287
|
+
function createParser(config) {
|
|
4288
|
+
return {
|
|
4289
|
+
parse: config.parse,
|
|
4290
|
+
serialize: (value) => value === null ? "" : config.serialize(value),
|
|
4291
|
+
withDefault: (defaultValue) => ({
|
|
4292
|
+
parse: (value) => {
|
|
4293
|
+
const parsed = config.parse(value);
|
|
4294
|
+
return parsed === null ? defaultValue : parsed;
|
|
4295
|
+
},
|
|
4296
|
+
serialize: config.serialize,
|
|
4297
|
+
defaultValue
|
|
4298
|
+
})
|
|
4299
|
+
};
|
|
4300
|
+
}
|
|
4301
|
+
const getSortingStateParser = (columnIds) => {
|
|
4302
|
+
const validKeys = columnIds ? columnIds instanceof Set ? columnIds : new Set(columnIds) : null;
|
|
4303
|
+
return createParser({
|
|
4304
|
+
parse: (value) => {
|
|
4305
|
+
try {
|
|
4306
|
+
if (!value || value.trim() === "") return [];
|
|
4307
|
+
const items = value.split(",").map((item) => {
|
|
4308
|
+
const lastDot = item.lastIndexOf(".");
|
|
4309
|
+
if (lastDot === -1) return null;
|
|
4310
|
+
const id = item.slice(0, lastDot);
|
|
4311
|
+
const dir = item.slice(lastDot + 1);
|
|
4312
|
+
if (!id || dir !== "asc" && dir !== "desc") return null;
|
|
4313
|
+
return {
|
|
4314
|
+
id,
|
|
4315
|
+
desc: dir === "desc"
|
|
4316
|
+
};
|
|
4317
|
+
});
|
|
4318
|
+
if (items.some((item) => item === null)) return null;
|
|
4319
|
+
const result = items;
|
|
4320
|
+
if (validKeys && result.some((item) => !validKeys.has(item.id))) return null;
|
|
4321
|
+
return result;
|
|
4322
|
+
} catch {
|
|
4323
|
+
return null;
|
|
4324
|
+
}
|
|
4325
|
+
},
|
|
4326
|
+
serialize: (value) => value.map((item) => `${item.id}.${item.desc ? "desc" : "asc"}`).join(",")
|
|
4327
|
+
});
|
|
4328
|
+
};
|
|
4329
|
+
const filterItemSchema = zod.z.object({
|
|
4330
|
+
id: zod.z.string(),
|
|
4331
|
+
value: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]),
|
|
4332
|
+
variant: zod.z.enum(dataTableConfig.filterVariants),
|
|
4333
|
+
operator: zod.z.enum(dataTableConfig.operators),
|
|
4334
|
+
filterId: zod.z.string()
|
|
4335
|
+
});
|
|
4336
|
+
|
|
4337
|
+
//#endregion
|
|
4338
|
+
//#region src/hooks/use-data-table.ts
|
|
4339
|
+
const PAGE_KEY = "page";
|
|
4340
|
+
const PER_PAGE_KEY = "perPage";
|
|
4341
|
+
const SORT_KEY = "sort";
|
|
4342
|
+
const FILTERS_KEY = "filters";
|
|
4343
|
+
const JOIN_OPERATOR_KEY = "joinOperator";
|
|
4344
|
+
const ARRAY_SEPARATOR = ",";
|
|
4345
|
+
const DEBOUNCE_MS = 300;
|
|
4346
|
+
const THROTTLE_MS = 50;
|
|
4347
|
+
function useDataTable(props) {
|
|
4348
|
+
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;
|
|
4349
|
+
const pageKey = queryKeys?.page ?? PAGE_KEY;
|
|
4350
|
+
const perPageKey = queryKeys?.perPage ?? PER_PAGE_KEY;
|
|
4351
|
+
const sortKey = queryKeys?.sort ?? SORT_KEY;
|
|
4352
|
+
const filtersKey = queryKeys?.filters ?? FILTERS_KEY;
|
|
4353
|
+
const joinOperatorKey = queryKeys?.joinOperator ?? JOIN_OPERATOR_KEY;
|
|
4354
|
+
const queryStateOptions = react.useMemo(() => ({
|
|
4355
|
+
history,
|
|
4356
|
+
scroll,
|
|
4357
|
+
shallow,
|
|
4358
|
+
throttleMs,
|
|
4359
|
+
debounceMs,
|
|
4360
|
+
clearOnDefault,
|
|
4361
|
+
startTransition
|
|
4362
|
+
}), [
|
|
4363
|
+
history,
|
|
4364
|
+
scroll,
|
|
4365
|
+
shallow,
|
|
4366
|
+
throttleMs,
|
|
4367
|
+
debounceMs,
|
|
4368
|
+
clearOnDefault,
|
|
4369
|
+
startTransition
|
|
4370
|
+
]);
|
|
4371
|
+
const [rowSelection, setRowSelection] = react.useState(initialState?.rowSelection ?? {});
|
|
4372
|
+
const [columnVisibility, setColumnVisibility] = react.useState(initialState?.columnVisibility ?? {});
|
|
4373
|
+
const [page, setPage] = useQueryState(pageKey, parseAsInteger.withDefault(1));
|
|
4374
|
+
const [perPage, setPerPage] = useQueryState(perPageKey, parseAsInteger.withDefault(initialState?.pagination?.pageSize ?? 10));
|
|
4375
|
+
const pagination = react.useMemo(() => {
|
|
4376
|
+
return {
|
|
4377
|
+
pageIndex: page - 1,
|
|
4378
|
+
pageSize: perPage
|
|
4379
|
+
};
|
|
4380
|
+
}, [page, perPage]);
|
|
4381
|
+
const onPaginationChange = react.useCallback((updaterOrValue) => {
|
|
4382
|
+
if (typeof updaterOrValue === "function") {
|
|
4383
|
+
const newPagination = updaterOrValue(pagination);
|
|
4384
|
+
setPage(newPagination.pageIndex + 1);
|
|
4385
|
+
setPerPage(newPagination.pageSize);
|
|
4386
|
+
} else {
|
|
4387
|
+
setPage(updaterOrValue.pageIndex + 1);
|
|
4388
|
+
setPerPage(updaterOrValue.pageSize);
|
|
4389
|
+
}
|
|
4390
|
+
}, [
|
|
4391
|
+
pagination,
|
|
4392
|
+
setPage,
|
|
4393
|
+
setPerPage
|
|
4394
|
+
]);
|
|
4395
|
+
const [sorting, setSorting] = useQueryState(sortKey, getSortingStateParser(react.useMemo(() => {
|
|
4396
|
+
return new Set(columns.map((column) => column.id).filter(Boolean));
|
|
4397
|
+
}, [columns])).withDefault(initialState?.sorting ?? []));
|
|
4398
|
+
const onSortingChange = react.useCallback((updaterOrValue) => {
|
|
4399
|
+
if (typeof updaterOrValue === "function") setSorting(updaterOrValue(sorting));
|
|
4400
|
+
else setSorting(updaterOrValue);
|
|
4401
|
+
}, [sorting, setSorting]);
|
|
4402
|
+
const filterableColumns = react.useMemo(() => {
|
|
4403
|
+
if (enableAdvancedFilter) return [];
|
|
4404
|
+
return columns.filter((column) => column.enableColumnFilter);
|
|
4405
|
+
}, [columns, enableAdvancedFilter]);
|
|
4406
|
+
const [filterValues, setFilterValues] = useQueryStates(react.useMemo(() => {
|
|
4407
|
+
if (enableAdvancedFilter) return {};
|
|
4408
|
+
return filterableColumns.reduce((acc, column) => {
|
|
4409
|
+
if (column.meta?.options) acc[column.id ?? ""] = parseAsArrayOf(parseAsString, ARRAY_SEPARATOR).withDefault([]);
|
|
4410
|
+
else acc[column.id ?? ""] = parseAsString.withDefault("");
|
|
4411
|
+
return acc;
|
|
4412
|
+
}, {});
|
|
4413
|
+
}, [filterableColumns, enableAdvancedFilter]));
|
|
4414
|
+
const debouncedSetFilterValues = useDebouncedCallback((values) => {
|
|
4415
|
+
setPage(1);
|
|
4416
|
+
setFilterValues(values);
|
|
4417
|
+
}, debounceMs);
|
|
4418
|
+
const initialColumnFilters = react.useMemo(() => {
|
|
4419
|
+
if (enableAdvancedFilter) return [];
|
|
4420
|
+
return Object.entries(filterValues).reduce((filters, [key, value]) => {
|
|
4421
|
+
if (value !== null) {
|
|
4422
|
+
const processedValue = Array.isArray(value) ? value : typeof value === "string" && /[^a-zA-Z0-9]/.test(value) ? value.split(/[^a-zA-Z0-9]+/).filter(Boolean) : [value];
|
|
4423
|
+
filters.push({
|
|
4424
|
+
id: key,
|
|
4425
|
+
value: processedValue
|
|
4426
|
+
});
|
|
4427
|
+
}
|
|
4428
|
+
return filters;
|
|
4429
|
+
}, []);
|
|
4430
|
+
}, [filterValues, enableAdvancedFilter]);
|
|
4431
|
+
const [columnFilters, setColumnFilters] = react.useState(initialColumnFilters);
|
|
4432
|
+
const onColumnFiltersChange = react.useCallback((updaterOrValue) => {
|
|
4433
|
+
if (enableAdvancedFilter) return;
|
|
4434
|
+
setColumnFilters((prev) => {
|
|
4435
|
+
const next = typeof updaterOrValue === "function" ? updaterOrValue(prev) : updaterOrValue;
|
|
4436
|
+
const filterUpdates = next.reduce((acc, filter) => {
|
|
4437
|
+
if (filterableColumns.find((column) => column.id === filter.id)) acc[filter.id] = filter.value;
|
|
4438
|
+
return acc;
|
|
4439
|
+
}, {});
|
|
4440
|
+
for (const prevFilter of prev) if (!next.some((filter) => filter.id === prevFilter.id)) filterUpdates[prevFilter.id] = null;
|
|
4441
|
+
debouncedSetFilterValues(filterUpdates);
|
|
4442
|
+
return next;
|
|
4443
|
+
});
|
|
4444
|
+
}, [
|
|
4445
|
+
debouncedSetFilterValues,
|
|
4446
|
+
filterableColumns,
|
|
4447
|
+
enableAdvancedFilter
|
|
4448
|
+
]);
|
|
4449
|
+
const table = (0, __tanstack_react_table.useReactTable)({
|
|
4450
|
+
...tableProps,
|
|
4451
|
+
columns,
|
|
4452
|
+
initialState,
|
|
4453
|
+
pageCount,
|
|
4454
|
+
state: {
|
|
4455
|
+
pagination,
|
|
4456
|
+
sorting,
|
|
4457
|
+
columnVisibility,
|
|
4458
|
+
rowSelection,
|
|
4459
|
+
columnFilters
|
|
4460
|
+
},
|
|
4461
|
+
defaultColumn: {
|
|
4462
|
+
...tableProps.defaultColumn,
|
|
4463
|
+
enableColumnFilter: false
|
|
4464
|
+
},
|
|
4465
|
+
enableRowSelection: true,
|
|
4466
|
+
onRowSelectionChange: setRowSelection,
|
|
4467
|
+
onPaginationChange,
|
|
4468
|
+
onSortingChange,
|
|
4469
|
+
onColumnFiltersChange,
|
|
4470
|
+
onColumnVisibilityChange: setColumnVisibility,
|
|
4471
|
+
getCoreRowModel: (0, __tanstack_react_table.getCoreRowModel)(),
|
|
4472
|
+
getFilteredRowModel: (0, __tanstack_react_table.getFilteredRowModel)(),
|
|
4473
|
+
getPaginationRowModel: (0, __tanstack_react_table.getPaginationRowModel)(),
|
|
4474
|
+
getSortedRowModel: (0, __tanstack_react_table.getSortedRowModel)(),
|
|
4475
|
+
getFacetedRowModel: (0, __tanstack_react_table.getFacetedRowModel)(),
|
|
4476
|
+
getFacetedUniqueValues: (0, __tanstack_react_table.getFacetedUniqueValues)(),
|
|
4477
|
+
getFacetedMinMaxValues: (0, __tanstack_react_table.getFacetedMinMaxValues)(),
|
|
4478
|
+
manualPagination: true,
|
|
4479
|
+
manualSorting: true,
|
|
4480
|
+
manualFiltering: true,
|
|
4481
|
+
meta: {
|
|
4482
|
+
...tableProps.meta,
|
|
4483
|
+
queryKeys: {
|
|
4484
|
+
page: pageKey,
|
|
4485
|
+
perPage: perPageKey,
|
|
4486
|
+
sort: sortKey,
|
|
4487
|
+
filters: filtersKey,
|
|
4488
|
+
joinOperator: joinOperatorKey
|
|
4489
|
+
},
|
|
4490
|
+
queryStateOptions
|
|
4491
|
+
}
|
|
4492
|
+
});
|
|
4493
|
+
return react.useMemo(() => ({
|
|
4494
|
+
table,
|
|
4495
|
+
shallow,
|
|
4496
|
+
debounceMs,
|
|
4497
|
+
throttleMs
|
|
4498
|
+
}), [
|
|
4499
|
+
table,
|
|
4500
|
+
shallow,
|
|
4501
|
+
debounceMs,
|
|
4502
|
+
throttleMs
|
|
4503
|
+
]);
|
|
4231
4504
|
}
|
|
4232
4505
|
|
|
4233
4506
|
//#endregion
|
|
@@ -4250,13 +4523,13 @@ const filterModeConfig = {
|
|
|
4250
4523
|
tooltip: "Linear-style command filters"
|
|
4251
4524
|
}
|
|
4252
4525
|
};
|
|
4253
|
-
function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection = true, exclude, actions, pinnedColumns, filterMode = "simple", onDeleteSelected, onUpdateSelected, batchUpdateFields, actionBarExtra, initialSorting, enableExport = true }) {
|
|
4526
|
+
function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection = true, exclude, actions, pinnedColumns, filterMode = "simple", onDeleteSelected, onUpdateSelected, batchUpdateFields, actionBarExtra, initialSorting, enableExport = true, onSelectedCountChange, getSelectedRows }) {
|
|
4254
4527
|
const modes = filterMode ? Array.isArray(filterMode) ? filterMode : [filterMode] : [
|
|
4255
4528
|
"simple",
|
|
4256
4529
|
"advanced",
|
|
4257
4530
|
"command"
|
|
4258
4531
|
];
|
|
4259
|
-
const [currentMode, setCurrentMode] = (
|
|
4532
|
+
const [currentMode, setCurrentMode] = useQueryState("filterMode", parseAsStringEnum(modes).withDefault(modes[0] ?? "simple"));
|
|
4260
4533
|
const showToggle = modes.length > 1;
|
|
4261
4534
|
const { table, shallow, debounceMs, throttleMs } = useDataTable({
|
|
4262
4535
|
data,
|
|
@@ -4287,6 +4560,17 @@ function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection
|
|
|
4287
4560
|
shallow: false,
|
|
4288
4561
|
clearOnDefault: true
|
|
4289
4562
|
});
|
|
4563
|
+
const getSelectedRowsFn = (0, react.useCallback)(() => table.getFilteredSelectedRowModel().rows.map((row) => row.original), [table]);
|
|
4564
|
+
(0, react.useEffect)(() => {
|
|
4565
|
+
if (getSelectedRows) getSelectedRows.current = getSelectedRowsFn;
|
|
4566
|
+
return () => {
|
|
4567
|
+
if (getSelectedRows) getSelectedRows.current = null;
|
|
4568
|
+
};
|
|
4569
|
+
}, [getSelectedRows, getSelectedRowsFn]);
|
|
4570
|
+
const selectedRowCount = table.getFilteredSelectedRowModel().rows.length;
|
|
4571
|
+
(0, react.useEffect)(() => {
|
|
4572
|
+
onSelectedCountChange?.(selectedRowCount);
|
|
4573
|
+
}, [selectedRowCount, onSelectedCountChange]);
|
|
4290
4574
|
const FilterModeSelect = showToggle ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuTrigger, {
|
|
4291
4575
|
asChild: true,
|
|
4292
4576
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
@@ -4340,7 +4624,8 @@ function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection
|
|
|
4340
4624
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4341
4625
|
className: "flex w-full items-start justify-between gap-2 p-1",
|
|
4342
4626
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4343
|
-
className: "flex flex-1
|
|
4627
|
+
className: "flex flex-1 items-start gap-2 min-h-[40px]",
|
|
4628
|
+
"data-filter-parent": true,
|
|
4344
4629
|
children: [currentMode !== "simple" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataTableSortList, {
|
|
4345
4630
|
table,
|
|
4346
4631
|
align: "start"
|
|
@@ -4618,19 +4903,342 @@ function CrudFormModal({ open, onOpenChange, mode, schema, initialValues, onSubm
|
|
|
4618
4903
|
});
|
|
4619
4904
|
}
|
|
4620
4905
|
|
|
4906
|
+
//#endregion
|
|
4907
|
+
//#region src/components/auto-crud/import-dialog.tsx
|
|
4908
|
+
function ImportDialog({ open, onOpenChange, onImport, columns = [], title = "导入数据" }) {
|
|
4909
|
+
const [step, setStep] = react.useState("upload");
|
|
4910
|
+
const [parsedData, setParsedData] = react.useState(null);
|
|
4911
|
+
const [error, setError] = react.useState(null);
|
|
4912
|
+
const [result, setResult] = react.useState(null);
|
|
4913
|
+
const [isDragging, setIsDragging] = react.useState(false);
|
|
4914
|
+
const fileInputRef = react.useRef(null);
|
|
4915
|
+
const reset = react.useCallback(() => {
|
|
4916
|
+
setStep("upload");
|
|
4917
|
+
setParsedData(null);
|
|
4918
|
+
setError(null);
|
|
4919
|
+
setResult(null);
|
|
4920
|
+
setIsDragging(false);
|
|
4921
|
+
}, []);
|
|
4922
|
+
const handleOpenChange = react.useCallback((value) => {
|
|
4923
|
+
if (!value) reset();
|
|
4924
|
+
onOpenChange(value);
|
|
4925
|
+
}, [onOpenChange, reset]);
|
|
4926
|
+
const handleFile = react.useCallback(async (file) => {
|
|
4927
|
+
setError(null);
|
|
4928
|
+
try {
|
|
4929
|
+
const data = await parseImportFile(file);
|
|
4930
|
+
if (data.rows.length === 0) {
|
|
4931
|
+
setError("文件中没有数据行");
|
|
4932
|
+
return;
|
|
4933
|
+
}
|
|
4934
|
+
setParsedData(data);
|
|
4935
|
+
setStep("preview");
|
|
4936
|
+
} catch (e) {
|
|
4937
|
+
setError(e instanceof Error ? e.message : "文件解析失败");
|
|
4938
|
+
}
|
|
4939
|
+
}, []);
|
|
4940
|
+
const handleDrop = react.useCallback((e) => {
|
|
4941
|
+
e.preventDefault();
|
|
4942
|
+
setIsDragging(false);
|
|
4943
|
+
const file = e.dataTransfer.files[0];
|
|
4944
|
+
if (file) handleFile(file);
|
|
4945
|
+
}, [handleFile]);
|
|
4946
|
+
const handleFileInput = react.useCallback((e) => {
|
|
4947
|
+
const file = e.target.files?.[0];
|
|
4948
|
+
if (file) handleFile(file);
|
|
4949
|
+
}, [handleFile]);
|
|
4950
|
+
const handleImport = react.useCallback(async () => {
|
|
4951
|
+
if (!parsedData) return;
|
|
4952
|
+
setStep("importing");
|
|
4953
|
+
try {
|
|
4954
|
+
setResult(await onImport(parsedData.rows));
|
|
4955
|
+
setStep("result");
|
|
4956
|
+
} catch (e) {
|
|
4957
|
+
setError(e instanceof Error ? e.message : "导入失败");
|
|
4958
|
+
setStep("preview");
|
|
4959
|
+
}
|
|
4960
|
+
}, [parsedData, onImport]);
|
|
4961
|
+
const handleDownloadTemplate = react.useCallback(() => {
|
|
4962
|
+
if (columns.length === 0) return;
|
|
4963
|
+
const csv = generateCSVTemplate(columns);
|
|
4964
|
+
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
|
4965
|
+
const url = URL.createObjectURL(blob);
|
|
4966
|
+
const link = document.createElement("a");
|
|
4967
|
+
link.href = url;
|
|
4968
|
+
link.download = "import-template.csv";
|
|
4969
|
+
link.click();
|
|
4970
|
+
URL.revokeObjectURL(url);
|
|
4971
|
+
}, [columns]);
|
|
4972
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Dialog, {
|
|
4973
|
+
open,
|
|
4974
|
+
onOpenChange: handleOpenChange,
|
|
4975
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DialogContent, {
|
|
4976
|
+
className: "sm:max-w-[600px]",
|
|
4977
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DialogHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DialogTitle, { children: title }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DialogDescription, { children: [
|
|
4978
|
+
step === "upload" && "上传 CSV 或 JSON 文件以批量导入数据",
|
|
4979
|
+
step === "preview" && `已解析 ${parsedData?.rows.length ?? 0} 条数据,确认后开始导入`,
|
|
4980
|
+
step === "importing" && "正在导入数据...",
|
|
4981
|
+
step === "result" && "导入完成"
|
|
4982
|
+
] })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4983
|
+
className: "space-y-4",
|
|
4984
|
+
children: [
|
|
4985
|
+
step === "upload" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4986
|
+
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"}`,
|
|
4987
|
+
onDragOver: (e) => {
|
|
4988
|
+
e.preventDefault();
|
|
4989
|
+
setIsDragging(true);
|
|
4990
|
+
},
|
|
4991
|
+
onDragLeave: () => setIsDragging(false),
|
|
4992
|
+
onDrop: handleDrop,
|
|
4993
|
+
onClick: () => fileInputRef.current?.click(),
|
|
4994
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4995
|
+
className: "flex flex-col items-center gap-2",
|
|
4996
|
+
children: [
|
|
4997
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
4998
|
+
className: "h-10 w-10 text-muted-foreground",
|
|
4999
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
5000
|
+
fill: "none",
|
|
5001
|
+
viewBox: "0 0 24 24",
|
|
5002
|
+
strokeWidth: 1.5,
|
|
5003
|
+
stroke: "currentColor",
|
|
5004
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
5005
|
+
strokeLinecap: "round",
|
|
5006
|
+
strokeLinejoin: "round",
|
|
5007
|
+
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"
|
|
5008
|
+
})
|
|
5009
|
+
}),
|
|
5010
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5011
|
+
className: "text-sm text-muted-foreground",
|
|
5012
|
+
children: "拖拽文件到此处,或点击选择文件"
|
|
5013
|
+
}),
|
|
5014
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5015
|
+
className: "text-xs text-muted-foreground/60",
|
|
5016
|
+
children: "支持 CSV、JSON 格式"
|
|
5017
|
+
})
|
|
5018
|
+
]
|
|
5019
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5020
|
+
ref: fileInputRef,
|
|
5021
|
+
type: "file",
|
|
5022
|
+
accept: ".csv,.json",
|
|
5023
|
+
className: "hidden",
|
|
5024
|
+
onChange: handleFileInput
|
|
5025
|
+
})]
|
|
5026
|
+
}), columns.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
5027
|
+
variant: "outline",
|
|
5028
|
+
size: "sm",
|
|
5029
|
+
className: "w-full",
|
|
5030
|
+
onClick: handleDownloadTemplate,
|
|
5031
|
+
children: "下载 CSV 模板"
|
|
5032
|
+
})] }),
|
|
5033
|
+
step === "preview" && parsedData && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
5034
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5035
|
+
className: "rounded-md border",
|
|
5036
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5037
|
+
className: "max-h-[300px] overflow-auto",
|
|
5038
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
5039
|
+
className: "w-full text-sm",
|
|
5040
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", {
|
|
5041
|
+
className: "sticky top-0 bg-muted",
|
|
5042
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
5043
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
5044
|
+
className: "px-3 py-2 text-left font-medium text-muted-foreground",
|
|
5045
|
+
children: "#"
|
|
5046
|
+
}),
|
|
5047
|
+
parsedData.headers.slice(0, 6).map((h) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
5048
|
+
className: "px-3 py-2 text-left font-medium text-muted-foreground",
|
|
5049
|
+
children: h
|
|
5050
|
+
}, h)),
|
|
5051
|
+
parsedData.headers.length > 6 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("th", {
|
|
5052
|
+
className: "px-3 py-2 text-left font-medium text-muted-foreground",
|
|
5053
|
+
children: [
|
|
5054
|
+
"+",
|
|
5055
|
+
parsedData.headers.length - 6,
|
|
5056
|
+
" 列"
|
|
5057
|
+
]
|
|
5058
|
+
})
|
|
5059
|
+
] })
|
|
5060
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: parsedData.rows.slice(0, 10).map((row, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
|
|
5061
|
+
className: "border-t",
|
|
5062
|
+
children: [
|
|
5063
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
5064
|
+
className: "px-3 py-2 text-muted-foreground",
|
|
5065
|
+
children: i + 1
|
|
5066
|
+
}),
|
|
5067
|
+
parsedData.headers.slice(0, 6).map((h) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
5068
|
+
className: "px-3 py-2 max-w-[150px] truncate",
|
|
5069
|
+
children: String(row[h] ?? "")
|
|
5070
|
+
}, h)),
|
|
5071
|
+
parsedData.headers.length > 6 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
5072
|
+
className: "px-3 py-2 text-muted-foreground",
|
|
5073
|
+
children: "..."
|
|
5074
|
+
})
|
|
5075
|
+
]
|
|
5076
|
+
}, i)) })]
|
|
5077
|
+
})
|
|
5078
|
+
})
|
|
5079
|
+
}),
|
|
5080
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
5081
|
+
className: "text-sm text-muted-foreground",
|
|
5082
|
+
children: [
|
|
5083
|
+
"共 ",
|
|
5084
|
+
parsedData.rows.length,
|
|
5085
|
+
" 条数据",
|
|
5086
|
+
parsedData.rows.length > 10 && "(预览前 10 条)",
|
|
5087
|
+
",",
|
|
5088
|
+
parsedData.headers.length,
|
|
5089
|
+
" 个字段 ,格式: ",
|
|
5090
|
+
parsedData.format.toUpperCase()
|
|
5091
|
+
]
|
|
5092
|
+
}),
|
|
5093
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5094
|
+
className: "flex gap-2 justify-end",
|
|
5095
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
5096
|
+
variant: "outline",
|
|
5097
|
+
onClick: reset,
|
|
5098
|
+
children: "重新选择"
|
|
5099
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
5100
|
+
onClick: handleImport,
|
|
5101
|
+
children: "确认导入"
|
|
5102
|
+
})]
|
|
5103
|
+
})
|
|
5104
|
+
] }),
|
|
5105
|
+
step === "importing" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5106
|
+
className: "flex flex-col items-center gap-4 py-8",
|
|
5107
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
5108
|
+
className: "text-sm text-muted-foreground",
|
|
5109
|
+
children: [
|
|
5110
|
+
"正在导入 ",
|
|
5111
|
+
parsedData?.rows.length ?? 0,
|
|
5112
|
+
" 条数据..."
|
|
5113
|
+
]
|
|
5114
|
+
})]
|
|
5115
|
+
}),
|
|
5116
|
+
step === "result" && result && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5117
|
+
className: "space-y-3",
|
|
5118
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5119
|
+
className: "flex gap-4",
|
|
5120
|
+
children: [
|
|
5121
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5122
|
+
className: "flex-1 rounded-lg bg-green-50 dark:bg-green-950/30 p-3 text-center",
|
|
5123
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5124
|
+
className: "text-2xl font-semibold text-green-600",
|
|
5125
|
+
children: result.success
|
|
5126
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5127
|
+
className: "text-xs text-green-600/80",
|
|
5128
|
+
children: "新建"
|
|
5129
|
+
})]
|
|
5130
|
+
}),
|
|
5131
|
+
(result.updated ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5132
|
+
className: "flex-1 rounded-lg bg-blue-50 dark:bg-blue-950/30 p-3 text-center",
|
|
5133
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5134
|
+
className: "text-2xl font-semibold text-blue-600",
|
|
5135
|
+
children: result.updated
|
|
5136
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5137
|
+
className: "text-xs text-blue-600/80",
|
|
5138
|
+
children: "更新"
|
|
5139
|
+
})]
|
|
5140
|
+
}),
|
|
5141
|
+
result.skipped > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5142
|
+
className: "flex-1 rounded-lg bg-yellow-50 dark:bg-yellow-950/30 p-3 text-center",
|
|
5143
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5144
|
+
className: "text-2xl font-semibold text-yellow-600",
|
|
5145
|
+
children: result.skipped
|
|
5146
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5147
|
+
className: "text-xs text-yellow-600/80",
|
|
5148
|
+
children: "跳过"
|
|
5149
|
+
})]
|
|
5150
|
+
}),
|
|
5151
|
+
result.failed.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5152
|
+
className: "flex-1 rounded-lg bg-red-50 dark:bg-red-950/30 p-3 text-center",
|
|
5153
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5154
|
+
className: "text-2xl font-semibold text-red-600",
|
|
5155
|
+
children: result.failed.length
|
|
5156
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5157
|
+
className: "text-xs text-red-600/80",
|
|
5158
|
+
children: "验证失败"
|
|
5159
|
+
})]
|
|
5160
|
+
})
|
|
5161
|
+
]
|
|
5162
|
+
}), result.failed.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5163
|
+
className: "rounded-md border border-red-200 dark:border-red-900",
|
|
5164
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5165
|
+
className: "max-h-[200px] overflow-auto",
|
|
5166
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
5167
|
+
className: "w-full text-sm",
|
|
5168
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", {
|
|
5169
|
+
className: "sticky top-0 bg-red-50 dark:bg-red-950/30",
|
|
5170
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
5171
|
+
className: "px-3 py-2 text-left font-medium text-red-600",
|
|
5172
|
+
children: "行号"
|
|
5173
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
5174
|
+
className: "px-3 py-2 text-left font-medium text-red-600",
|
|
5175
|
+
children: "错误"
|
|
5176
|
+
})] })
|
|
5177
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: result.failed.map((f) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
|
|
5178
|
+
className: "border-t border-red-100 dark:border-red-900",
|
|
5179
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
5180
|
+
className: "px-3 py-2 text-red-600",
|
|
5181
|
+
children: f.row + 1
|
|
5182
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
5183
|
+
className: "px-3 py-2 text-red-600/80",
|
|
5184
|
+
children: f.errors.join("; ")
|
|
5185
|
+
})]
|
|
5186
|
+
}, f.row)) })]
|
|
5187
|
+
})
|
|
5188
|
+
})
|
|
5189
|
+
})]
|
|
5190
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5191
|
+
className: "flex gap-2 justify-end",
|
|
5192
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
5193
|
+
variant: "outline",
|
|
5194
|
+
onClick: () => handleOpenChange(false),
|
|
5195
|
+
children: "关闭"
|
|
5196
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
5197
|
+
variant: "outline",
|
|
5198
|
+
onClick: reset,
|
|
5199
|
+
children: "继续导入"
|
|
5200
|
+
})]
|
|
5201
|
+
})] }),
|
|
5202
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5203
|
+
className: "rounded-md bg-destructive/10 p-3 text-sm text-destructive",
|
|
5204
|
+
children: error
|
|
5205
|
+
})
|
|
5206
|
+
]
|
|
5207
|
+
})]
|
|
5208
|
+
})
|
|
5209
|
+
});
|
|
5210
|
+
}
|
|
5211
|
+
|
|
4621
5212
|
//#endregion
|
|
4622
5213
|
//#region src/components/auto-crud/auto-crud-table.tsx
|
|
4623
5214
|
/**
|
|
4624
5215
|
* 从统一配置生成表格 overrides
|
|
5216
|
+
* 当 filter 配置存在时,合并到 meta 中(不影响列隐藏)
|
|
4625
5217
|
*/
|
|
4626
5218
|
function buildTableOverrides(fields, legacyOverrides) {
|
|
4627
5219
|
const result = { ...legacyOverrides };
|
|
4628
|
-
if (fields) for (const [key, config] of Object.entries(fields))
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
5220
|
+
if (fields) for (const [key, config] of Object.entries(fields)) {
|
|
5221
|
+
let filterMeta;
|
|
5222
|
+
if (config.filter && config.filter.enabled !== false && !config.filter.hidden) {
|
|
5223
|
+
const { enabled: _, hidden: __,...filterProps } = config.filter;
|
|
5224
|
+
if (Object.keys(filterProps).length > 0) filterMeta = filterProps;
|
|
5225
|
+
}
|
|
5226
|
+
result[key] = {
|
|
5227
|
+
...result[key],
|
|
5228
|
+
...config.label && { label: config.label },
|
|
5229
|
+
...config.hidden && { hidden: true },
|
|
5230
|
+
...config.table,
|
|
5231
|
+
...filterMeta && { meta: {
|
|
5232
|
+
...result[key]?.meta ?? {},
|
|
5233
|
+
...config.table?.meta ?? {},
|
|
5234
|
+
...filterMeta
|
|
5235
|
+
} }
|
|
5236
|
+
};
|
|
5237
|
+
if (config.filter?.enabled === false || config.filter?.hidden === true) result[key] = {
|
|
5238
|
+
...result[key],
|
|
5239
|
+
enableColumnFilter: false
|
|
5240
|
+
};
|
|
5241
|
+
}
|
|
4634
5242
|
return result;
|
|
4635
5243
|
}
|
|
4636
5244
|
/**
|
|
@@ -4777,9 +5385,52 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
|
|
|
4777
5385
|
create: permissions?.can?.create ?? true,
|
|
4778
5386
|
update: permissions?.can?.update ?? true,
|
|
4779
5387
|
delete: permissions?.can?.delete ?? true,
|
|
4780
|
-
export: permissions?.can?.export ?? true
|
|
5388
|
+
export: permissions?.can?.export ?? true,
|
|
5389
|
+
import: permissions?.can?.import ?? true
|
|
4781
5390
|
};
|
|
4782
5391
|
const denyFields = permissions?.deny;
|
|
5392
|
+
const canImport = can.import && !!resource.handlers.import;
|
|
5393
|
+
const canExport = can.export;
|
|
5394
|
+
const [importOpen, setImportOpen] = react.useState(false);
|
|
5395
|
+
const [selectedCount, setSelectedCount] = react.useState(0);
|
|
5396
|
+
const [exporting, setExporting] = react.useState(false);
|
|
5397
|
+
const getSelectedRowsRef = react.useRef(null);
|
|
5398
|
+
const importColumns = react.useMemo(() => {
|
|
5399
|
+
const shape = schema.shape;
|
|
5400
|
+
const denySet = new Set(denyFields ?? []);
|
|
5401
|
+
return Object.keys(shape).filter((key) => !denySet.has(key));
|
|
5402
|
+
}, [schema, denyFields]);
|
|
5403
|
+
const handleExport = react.useCallback(async (mode) => {
|
|
5404
|
+
const filename = title ?? "export";
|
|
5405
|
+
const excludeColumns = denyFields;
|
|
5406
|
+
if (mode === "selected") {
|
|
5407
|
+
const selectedRows = getSelectedRowsRef.current?.();
|
|
5408
|
+
if (!selectedRows || selectedRows.length === 0) return;
|
|
5409
|
+
exportAllToCSV(selectedRows, {
|
|
5410
|
+
filename,
|
|
5411
|
+
excludeColumns
|
|
5412
|
+
});
|
|
5413
|
+
} else {
|
|
5414
|
+
if (!resource.handlers.export) return;
|
|
5415
|
+
exportAllToCSV(await resource.handlers.export(), {
|
|
5416
|
+
filename,
|
|
5417
|
+
excludeColumns
|
|
5418
|
+
});
|
|
5419
|
+
}
|
|
5420
|
+
}, [
|
|
5421
|
+
resource.handlers.export,
|
|
5422
|
+
title,
|
|
5423
|
+
denyFields
|
|
5424
|
+
]);
|
|
5425
|
+
const handleExportClick = react.useCallback(async () => {
|
|
5426
|
+
const mode = selectedCount > 0 ? "selected" : "filtered";
|
|
5427
|
+
setExporting(true);
|
|
5428
|
+
try {
|
|
5429
|
+
await handleExport(mode);
|
|
5430
|
+
} finally {
|
|
5431
|
+
setExporting(false);
|
|
5432
|
+
}
|
|
5433
|
+
}, [selectedCount, handleExport]);
|
|
4783
5434
|
const tableOverrides = buildTableOverrides(fields, tableConfig?.overrides);
|
|
4784
5435
|
const formOverrides = buildFormOverrides(fields, formConfig?.overrides, denyFields);
|
|
4785
5436
|
const hiddenColumns = buildHiddenColumns(fields, tableConfig?.hidden, denyFields);
|
|
@@ -4804,10 +5455,26 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
|
|
|
4804
5455
|
children: slots?.toolbarStart
|
|
4805
5456
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4806
5457
|
className: "flex items-center gap-2",
|
|
4807
|
-
children: [
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
5458
|
+
children: [
|
|
5459
|
+
slots?.toolbarEnd,
|
|
5460
|
+
canImport && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.Button, {
|
|
5461
|
+
variant: "outline",
|
|
5462
|
+
size: "sm",
|
|
5463
|
+
onClick: () => setImportOpen(true),
|
|
5464
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Download, { className: "mr-2 h-4 w-4" }), "导入"]
|
|
5465
|
+
}),
|
|
5466
|
+
canExport && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.Button, {
|
|
5467
|
+
variant: "outline",
|
|
5468
|
+
size: "sm",
|
|
5469
|
+
onClick: handleExportClick,
|
|
5470
|
+
disabled: exporting || selectedCount === 0 && !resource.handlers.export,
|
|
5471
|
+
children: [exporting ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Upload, { className: "mr-2 h-4 w-4" }), selectedCount > 0 ? `导出(${selectedCount})` : "导出"]
|
|
5472
|
+
}),
|
|
5473
|
+
can.create && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
5474
|
+
onClick: resource.handlers.openCreate,
|
|
5475
|
+
children: "新建"
|
|
5476
|
+
})
|
|
5477
|
+
]
|
|
4811
5478
|
})]
|
|
4812
5479
|
}),
|
|
4813
5480
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutoTable, {
|
|
@@ -4826,8 +5493,10 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
|
|
|
4826
5493
|
onDeleteSelected: can.delete ? resource.handlers.deleteMany : void 0,
|
|
4827
5494
|
onUpdateSelected: can.update ? resource.handlers.updateMany : void 0,
|
|
4828
5495
|
batchUpdateFields: can.update ? batchFields : void 0,
|
|
4829
|
-
enableExport:
|
|
4830
|
-
initialSorting: tableConfig?.defaultSort
|
|
5496
|
+
enableExport: false,
|
|
5497
|
+
initialSorting: tableConfig?.defaultSort,
|
|
5498
|
+
onSelectedCountChange: setSelectedCount,
|
|
5499
|
+
getSelectedRows: getSelectedRowsRef
|
|
4831
5500
|
}),
|
|
4832
5501
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CrudFormModal, {
|
|
4833
5502
|
open: resource.modal.createOpen || resource.modal.editOpen,
|
|
@@ -4860,11 +5529,90 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
|
|
|
4860
5529
|
disabled: resource.mutations.isDeleting,
|
|
4861
5530
|
children: resource.mutations.isDeleting ? "删除中..." : "确认删除"
|
|
4862
5531
|
})] })] })
|
|
5532
|
+
}),
|
|
5533
|
+
canImport && resource.handlers.import && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImportDialog, {
|
|
5534
|
+
open: importOpen,
|
|
5535
|
+
onOpenChange: setImportOpen,
|
|
5536
|
+
onImport: resource.handlers.import,
|
|
5537
|
+
columns: importColumns
|
|
4863
5538
|
})
|
|
4864
5539
|
]
|
|
4865
5540
|
});
|
|
4866
5541
|
}
|
|
4867
5542
|
|
|
5543
|
+
//#endregion
|
|
5544
|
+
//#region src/components/auto-crud/export-dialog.tsx
|
|
5545
|
+
function ExportDialog({ open, onOpenChange, selectedCount, onExport, canExportFiltered = false }) {
|
|
5546
|
+
const [exporting, setExporting] = react.useState(null);
|
|
5547
|
+
const [done, setDone] = react.useState(false);
|
|
5548
|
+
const reset = react.useCallback(() => {
|
|
5549
|
+
setExporting(null);
|
|
5550
|
+
setDone(false);
|
|
5551
|
+
}, []);
|
|
5552
|
+
const handleOpenChange = react.useCallback((value) => {
|
|
5553
|
+
if (!value) reset();
|
|
5554
|
+
onOpenChange(value);
|
|
5555
|
+
}, [onOpenChange, reset]);
|
|
5556
|
+
const handleExport = react.useCallback(async (mode) => {
|
|
5557
|
+
setExporting(mode);
|
|
5558
|
+
try {
|
|
5559
|
+
await onExport(mode);
|
|
5560
|
+
setDone(true);
|
|
5561
|
+
setTimeout(() => handleOpenChange(false), 800);
|
|
5562
|
+
} catch {
|
|
5563
|
+
setExporting(null);
|
|
5564
|
+
}
|
|
5565
|
+
}, [onExport, handleOpenChange]);
|
|
5566
|
+
const hasSelected = selectedCount > 0;
|
|
5567
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Dialog, {
|
|
5568
|
+
open,
|
|
5569
|
+
onOpenChange: handleOpenChange,
|
|
5570
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DialogContent, {
|
|
5571
|
+
className: "sm:max-w-[400px]",
|
|
5572
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DialogHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DialogTitle, { children: "导出数据" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DialogDescription, { children: "选择导出范围" })] }), done ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5573
|
+
className: "flex flex-col items-center gap-3 py-6",
|
|
5574
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.CheckCircle2, { className: "h-10 w-10 text-green-500" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5575
|
+
className: "text-sm text-muted-foreground",
|
|
5576
|
+
children: "导出成功"
|
|
5577
|
+
})]
|
|
5578
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5579
|
+
className: "grid gap-3 py-2",
|
|
5580
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.Button, {
|
|
5581
|
+
variant: "outline",
|
|
5582
|
+
className: "h-auto justify-start gap-3 px-4 py-3",
|
|
5583
|
+
disabled: !hasSelected || exporting !== null,
|
|
5584
|
+
onClick: () => handleExport("selected"),
|
|
5585
|
+
children: [exporting === "selected" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Loader2, { className: "h-5 w-5 shrink-0 animate-spin" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.FileDown, { className: "h-5 w-5 shrink-0" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5586
|
+
className: "flex flex-col items-start",
|
|
5587
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5588
|
+
className: "font-medium",
|
|
5589
|
+
children: ["导出选中", hasSelected ? ` (${selectedCount} 条)` : ""]
|
|
5590
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5591
|
+
className: "text-xs text-muted-foreground",
|
|
5592
|
+
children: hasSelected ? "导出当前勾选的数据行" : "请先在表格中勾选数据行"
|
|
5593
|
+
})]
|
|
5594
|
+
})]
|
|
5595
|
+
}), canExportFiltered && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.Button, {
|
|
5596
|
+
variant: "outline",
|
|
5597
|
+
className: "h-auto justify-start gap-3 px-4 py-3",
|
|
5598
|
+
disabled: exporting !== null,
|
|
5599
|
+
onClick: () => handleExport("filtered"),
|
|
5600
|
+
children: [exporting === "filtered" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Loader2, { className: "h-5 w-5 shrink-0 animate-spin" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.FileDown, { className: "h-5 w-5 shrink-0" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5601
|
+
className: "flex flex-col items-start",
|
|
5602
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5603
|
+
className: "font-medium",
|
|
5604
|
+
children: "导出筛选结果"
|
|
5605
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5606
|
+
className: "text-xs text-muted-foreground",
|
|
5607
|
+
children: "导出当前筛选条件下的所有数据"
|
|
5608
|
+
})]
|
|
5609
|
+
})]
|
|
5610
|
+
})]
|
|
5611
|
+
})]
|
|
5612
|
+
})
|
|
5613
|
+
});
|
|
5614
|
+
}
|
|
5615
|
+
|
|
4868
5616
|
//#endregion
|
|
4869
5617
|
//#region src/components/data-table/data-table-advanced-toolbar.tsx
|
|
4870
5618
|
function DataTableAdvancedToolbar({ table, children, className,...props }) {
|
|
@@ -5536,15 +6284,44 @@ function modalReducer(state, action) {
|
|
|
5536
6284
|
/**
|
|
5537
6285
|
* Hook 主函数
|
|
5538
6286
|
*/
|
|
5539
|
-
function useAutoCrudResource({ router,
|
|
5540
|
-
const { idKey = "id", defaultVariant = "dialog", hooks, toast: toastAdapter } = options;
|
|
6287
|
+
function useAutoCrudResource({ router, schema, query: queryTransform, options = {} }) {
|
|
6288
|
+
const { idKey = "id", defaultVariant = "dialog", hooks, toast: toastAdapter, exportFetcher } = options;
|
|
5541
6289
|
const toast = (0, react.useMemo)(() => toastAdapter === false ? noopToastAdapter : toastAdapter ?? defaultToastAdapter, [toastAdapter]);
|
|
6290
|
+
const columns = (0, react.useMemo)(() => createTableSchema(schema), [schema]);
|
|
6291
|
+
const [urlParams] = useQueryStates({
|
|
6292
|
+
page: parseAsInteger.withDefault(1),
|
|
6293
|
+
perPage: parseAsInteger.withDefault(10),
|
|
6294
|
+
sort: getSortingStateParser().withDefault([]),
|
|
6295
|
+
joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and")
|
|
6296
|
+
}, { shallow: false });
|
|
6297
|
+
const [filters] = useReadableFilters(columns, { shallow: false });
|
|
6298
|
+
const queryInput = (0, react.useMemo)(() => {
|
|
6299
|
+
const autoParams = {
|
|
6300
|
+
page: urlParams.page,
|
|
6301
|
+
perPage: urlParams.perPage,
|
|
6302
|
+
sort: urlParams.sort,
|
|
6303
|
+
filters,
|
|
6304
|
+
joinOperator: urlParams.joinOperator
|
|
6305
|
+
};
|
|
6306
|
+
return queryTransform ? queryTransform(autoParams) : autoParams;
|
|
6307
|
+
}, [
|
|
6308
|
+
urlParams,
|
|
6309
|
+
filters,
|
|
6310
|
+
queryTransform
|
|
6311
|
+
]);
|
|
5542
6312
|
const listQuery = router.list.useQuery(queryInput, { placeholderData: keepPreviousData });
|
|
5543
6313
|
const createMutation = router.create.useMutation();
|
|
5544
6314
|
const updateMutation = router.update.useMutation();
|
|
5545
6315
|
const deleteMutation = router.delete.useMutation();
|
|
5546
6316
|
const deleteManyMutation = router.deleteMany?.useMutation();
|
|
5547
6317
|
const updateManyMutation = router.updateMany?.useMutation();
|
|
6318
|
+
const importMutation = router.import?.useMutation();
|
|
6319
|
+
const exportInput = (0, react.useMemo)(() => {
|
|
6320
|
+
if (!queryInput) return {};
|
|
6321
|
+
const { page: _p, perPage: _pp,...rest } = queryInput;
|
|
6322
|
+
return rest;
|
|
6323
|
+
}, [queryInput]);
|
|
6324
|
+
const exportQuery = router.export?.useQuery(exportInput, { enabled: false });
|
|
5548
6325
|
const [modal, dispatch] = (0, react.useReducer)(modalReducer, {
|
|
5549
6326
|
createOpen: false,
|
|
5550
6327
|
editOpen: false,
|
|
@@ -5756,6 +6533,33 @@ function useAutoCrudResource({ router, queryInput, options = {} }) {
|
|
|
5756
6533
|
payload: row
|
|
5757
6534
|
});
|
|
5758
6535
|
}, []);
|
|
6536
|
+
const handleImport = (0, react.useCallback)(async (rows) => {
|
|
6537
|
+
if (!importMutation) throw new Error("Import not supported: router has no import procedure");
|
|
6538
|
+
const coercedRows = coerceRowValues(rows, schema);
|
|
6539
|
+
const result = await importMutation.mutateAsync({
|
|
6540
|
+
rows: coercedRows,
|
|
6541
|
+
onConflict: "upsert"
|
|
6542
|
+
});
|
|
6543
|
+
listQuery.refetch();
|
|
6544
|
+
return result;
|
|
6545
|
+
}, [
|
|
6546
|
+
importMutation,
|
|
6547
|
+
listQuery,
|
|
6548
|
+
schema
|
|
6549
|
+
]);
|
|
6550
|
+
const handleExport = (0, react.useCallback)(async () => {
|
|
6551
|
+
if (exportFetcher) {
|
|
6552
|
+
const { page: _p, perPage: _pp,...exportParams } = queryInput ?? {};
|
|
6553
|
+
return (await exportFetcher(exportParams))?.data ?? [];
|
|
6554
|
+
}
|
|
6555
|
+
if (exportQuery) return (await exportQuery.refetch())?.data?.data ?? [];
|
|
6556
|
+
throw new Error("Export not supported");
|
|
6557
|
+
}, [
|
|
6558
|
+
exportFetcher,
|
|
6559
|
+
exportQuery,
|
|
6560
|
+
queryInput
|
|
6561
|
+
]);
|
|
6562
|
+
const canExport = !!(exportFetcher || exportQuery);
|
|
5759
6563
|
return {
|
|
5760
6564
|
tableData: {
|
|
5761
6565
|
data: listQuery.data?.data ?? [],
|
|
@@ -5767,7 +6571,8 @@ function useAutoCrudResource({ router, queryInput, options = {} }) {
|
|
|
5767
6571
|
mutations: {
|
|
5768
6572
|
isCreating: createMutation.isPending,
|
|
5769
6573
|
isUpdating: updateMutation.isPending,
|
|
5770
|
-
isDeleting: deleteMutation.isPending
|
|
6574
|
+
isDeleting: deleteMutation.isPending,
|
|
6575
|
+
isImporting: importMutation?.isPending ?? false
|
|
5771
6576
|
},
|
|
5772
6577
|
handlers: {
|
|
5773
6578
|
openCreate,
|
|
@@ -5781,7 +6586,9 @@ function useAutoCrudResource({ router, queryInput, options = {} }) {
|
|
|
5781
6586
|
confirmDelete,
|
|
5782
6587
|
deleteMany,
|
|
5783
6588
|
updateMany,
|
|
5784
|
-
setVariant
|
|
6589
|
+
setVariant,
|
|
6590
|
+
import: importMutation ? handleImport : null,
|
|
6591
|
+
export: canExport ? handleExport : null
|
|
5785
6592
|
}
|
|
5786
6593
|
};
|
|
5787
6594
|
}
|
|
@@ -6039,12 +6846,12 @@ function createMemoryDataSource(initialData = []) {
|
|
|
6039
6846
|
});
|
|
6040
6847
|
});
|
|
6041
6848
|
if (params.sort && params.sort.length > 0) {
|
|
6042
|
-
const
|
|
6849
|
+
const sortItem = params.sort[0];
|
|
6043
6850
|
filtered.sort((a, b) => {
|
|
6044
|
-
const aVal = a[id];
|
|
6045
|
-
const bVal = b[id];
|
|
6851
|
+
const aVal = a[sortItem.id];
|
|
6852
|
+
const bVal = b[sortItem.id];
|
|
6046
6853
|
const result = aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
|
|
6047
|
-
return desc ? -result : result;
|
|
6854
|
+
return sortItem.desc ? -result : result;
|
|
6048
6855
|
});
|
|
6049
6856
|
}
|
|
6050
6857
|
const start = (params.page - 1) * params.perPage;
|
|
@@ -6072,11 +6879,12 @@ function createMemoryDataSource(initialData = []) {
|
|
|
6072
6879
|
async update(id, updateData) {
|
|
6073
6880
|
const index = data.findIndex((item) => item.id === id);
|
|
6074
6881
|
if (index === -1) throw new Error(`Item with id ${id} not found`);
|
|
6075
|
-
|
|
6882
|
+
const updated = {
|
|
6076
6883
|
...data[index],
|
|
6077
6884
|
...updateData
|
|
6078
6885
|
};
|
|
6079
|
-
|
|
6886
|
+
data[index] = updated;
|
|
6887
|
+
return updated;
|
|
6080
6888
|
},
|
|
6081
6889
|
async delete(id) {
|
|
6082
6890
|
const index = data.findIndex((item) => item.id === id);
|
|
@@ -6103,8 +6911,11 @@ exports.DataTableFacetedFilter = DataTableFacetedFilter;
|
|
|
6103
6911
|
exports.DataTablePagination = DataTablePagination;
|
|
6104
6912
|
exports.DataTableToolbar = DataTableToolbar;
|
|
6105
6913
|
exports.DataTableViewOptions = DataTableViewOptions;
|
|
6914
|
+
exports.ExportDialog = ExportDialog;
|
|
6915
|
+
exports.ImportDialog = ImportDialog;
|
|
6106
6916
|
exports.SchemaAdapter = SchemaAdapter;
|
|
6107
6917
|
exports.cn = cn;
|
|
6918
|
+
exports.coerceRowValues = coerceRowValues;
|
|
6108
6919
|
exports.createActionsColumn = createActionsColumn;
|
|
6109
6920
|
exports.createEditFormSchema = createEditFormSchema;
|
|
6110
6921
|
exports.createFormSchema = createFormSchema;
|
|
@@ -6112,7 +6923,12 @@ exports.createMemoryDataSource = createMemoryDataSource;
|
|
|
6112
6923
|
exports.createSelectColumn = createSelectColumn;
|
|
6113
6924
|
exports.createTRPCDataSource = createTRPCDataSource;
|
|
6114
6925
|
exports.createTableSchema = createTableSchema;
|
|
6926
|
+
exports.dataToCSV = dataToCSV;
|
|
6927
|
+
exports.downloadCSVTemplate = downloadCSVTemplate;
|
|
6928
|
+
exports.exportAllToCSV = exportAllToCSV;
|
|
6929
|
+
exports.exportTableToCSV = exportTableToCSV;
|
|
6115
6930
|
exports.formatDate = formatDate;
|
|
6931
|
+
exports.generateCSVTemplate = generateCSVTemplate;
|
|
6116
6932
|
exports.getUrlParams = getUrlParams;
|
|
6117
6933
|
exports.humanize = humanize;
|
|
6118
6934
|
exports.noopToastAdapter = noopToastAdapter;
|
|
@@ -6120,6 +6936,9 @@ exports.parseAsArrayOf = parseAsArrayOf;
|
|
|
6120
6936
|
exports.parseAsInteger = parseAsInteger;
|
|
6121
6937
|
exports.parseAsString = parseAsString;
|
|
6122
6938
|
exports.parseAsStringEnum = parseAsStringEnum;
|
|
6939
|
+
exports.parseCSV = parseCSV;
|
|
6940
|
+
exports.parseImportFile = parseImportFile;
|
|
6941
|
+
exports.parseJSON = parseJSON;
|
|
6123
6942
|
exports.parseZodField = parseZodField;
|
|
6124
6943
|
exports.setSearchParams = setSearchParams;
|
|
6125
6944
|
exports.useAutoCrudResource = useAutoCrudResource;
|