@wordrhyme/auto-crud 0.10.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 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 }) {
@@ -1291,159 +1437,6 @@ function formatDate(date, opts = {}) {
1291
1437
  }
1292
1438
  }
1293
1439
 
1294
- //#endregion
1295
- //#region src/lib/id.ts
1296
- const prefixes = {};
1297
- function generateId(prefixOrOptions, inputOptions = {}) {
1298
- const finalOptions = typeof prefixOrOptions === "object" ? prefixOrOptions : inputOptions;
1299
- const prefix = typeof prefixOrOptions === "object" ? void 0 : prefixOrOptions;
1300
- const { length = 12, separator = "_" } = finalOptions;
1301
- const id = (0, nanoid.customAlphabet)("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", length)();
1302
- return prefix && prefix in prefixes ? `${prefixes[prefix]}${separator}${id}` : id;
1303
- }
1304
-
1305
- //#endregion
1306
- //#region src/lib/readable-filters.ts
1307
- const OP_SUFFIX = "__op";
1308
- const INDEX_SEPARATOR = "__";
1309
- const RANGE_VARIANTS = new Set(["range", "dateRange"]);
1310
- function getColumnId(column) {
1311
- if ("id" in column && column.id) return String(column.id);
1312
- if ("accessorKey" in column && column.accessorKey) return String(column.accessorKey);
1313
- return null;
1314
- }
1315
- function getColumnVariant(column) {
1316
- if ("columnDef" in column) return column.columnDef.meta?.variant;
1317
- return column.meta?.variant;
1318
- }
1319
- function splitKey(key) {
1320
- const match = key.match(/^(.*?)(?:__(\d+))?$/);
1321
- if (!match) return { base: key };
1322
- const index = match[2] ? Number(match[2]) : void 0;
1323
- return {
1324
- base: match[1] ?? key,
1325
- index
1326
- };
1327
- }
1328
- function isFilterKey(key, columnIds) {
1329
- const { base } = splitKey(key.endsWith(OP_SUFFIX) ? key.slice(0, -4) : key);
1330
- return columnIds.has(base);
1331
- }
1332
- function parseValueByVariant(value, variant) {
1333
- if (variant === "multiSelect") return value ? value.split(",").filter(Boolean) : [];
1334
- if (RANGE_VARIANTS.has(variant)) {
1335
- const parts = value.split(",");
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
1440
  //#endregion
1448
1441
  //#region src/components/data-table/data-table-filter-list.tsx
1449
1442
  const DEBOUNCE_MS$2 = 300;
@@ -1459,11 +1452,7 @@ function DataTableFilterList({ table, debounceMs = DEBOUNCE_MS$2, throttleMs = T
1459
1452
  const columns = react.useMemo(() => {
1460
1453
  return table.getAllColumns().filter((column) => column.columnDef.enableColumnFilter);
1461
1454
  }, [table]);
1462
- const queryStateOptions = table.options.meta?.queryStateOptions;
1463
- const [filters, setFilters] = useReadableFilters(columns, {
1464
- ...queryStateOptions,
1465
- debounceMs: 0
1466
- });
1455
+ const [filters, setFilters] = useReadableFilters(columns, { debounceMs });
1467
1456
  const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);
1468
1457
  const [joinOperator, setJoinOperator] = useQueryState(table.options.meta?.queryKeys?.joinOperator ?? "", parseAsStringEnum(["and", "or"]).withDefault("and"));
1469
1458
  const onFilterAdd = react.useCallback(() => {
@@ -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(null);
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 queryStateOptions = table.options.meta?.queryStateOptions;
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;
@@ -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
- className: "flex flex-wrap items-center gap-2",
2755
- children: [columns.map((column) => {
2756
- const meta = column.columnDef.meta;
2757
- if (!meta?.variant) return null;
2758
- const value = getFilterValue(column.id);
2759
- switch (meta.variant) {
2760
- case "text": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Input, {
2761
- placeholder: meta.label ?? column.id,
2762
- value: typeof value === "string" ? value : "",
2763
- onChange: (e) => updateFilter(column.id, e.target.value || void 0),
2764
- className: "h-8 w-36"
2765
- }, column.id);
2766
- case "select":
2767
- case "multiSelect": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SimpleFacetedFilter, {
2768
- title: meta.label ?? column.id,
2769
- options: meta.options ?? [],
2770
- multiple: meta.variant === "multiSelect",
2771
- value: Array.isArray(value) ? value : value ? [value] : [],
2772
- onChange: (v) => updateFilter(column.id, v.length ? v : void 0)
2773
- }, column.id);
2774
- case "range": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SimpleSliderFilter, {
2775
- title: meta.label ?? column.id,
2776
- range: meta.range,
2777
- unit: meta.unit,
2778
- value: Array.isArray(value) ? value.map(Number) : void 0,
2779
- onChange: (v) => updateFilter(column.id, v ? v.map(String) : void 0)
2780
- }, column.id);
2781
- case "date":
2782
- case "dateRange": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SimpleDateFilter, {
2783
- title: meta.label ?? column.id,
2784
- multiple: meta.variant === "dateRange",
2785
- value,
2786
- onChange: (v) => updateFilter(column.id, v)
2787
- }, column.id);
2788
- default: return null;
2789
- }
2790
- }), hasFilters && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.Button, {
2791
- variant: "outline",
2792
- size: "sm",
2793
- className: "h-8 border-dashed",
2794
- onClick: () => setFilters([]),
2795
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { className: "size-4" }), "Reset"]
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 }) {
@@ -4222,25 +4298,32 @@ function createParser(config) {
4222
4298
  })
4223
4299
  };
4224
4300
  }
4225
- const sortingItemSchema = zod.z.object({
4226
- id: zod.z.string(),
4227
- desc: zod.z.boolean()
4228
- });
4229
4301
  const getSortingStateParser = (columnIds) => {
4230
4302
  const validKeys = columnIds ? columnIds instanceof Set ? columnIds : new Set(columnIds) : null;
4231
4303
  return createParser({
4232
4304
  parse: (value) => {
4233
4305
  try {
4234
- const parsed = JSON.parse(value);
4235
- const result = zod.z.array(sortingItemSchema).safeParse(parsed);
4236
- if (!result.success) return null;
4237
- if (validKeys && result.data.some((item) => !validKeys.has(item.id))) return null;
4238
- return result.data;
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;
4239
4322
  } catch {
4240
4323
  return null;
4241
4324
  }
4242
4325
  },
4243
- serialize: (value) => JSON.stringify(value)
4326
+ serialize: (value) => value.map((item) => `${item.id}.${item.desc ? "desc" : "asc"}`).join(",")
4244
4327
  });
4245
4328
  };
4246
4329
  const filterItemSchema = zod.z.object({
@@ -4446,7 +4529,7 @@ function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection
4446
4529
  "advanced",
4447
4530
  "command"
4448
4531
  ];
4449
- const [currentMode, setCurrentMode] = (0, react.useState)(modes[0] ?? "simple");
4532
+ const [currentMode, setCurrentMode] = useQueryState("filterMode", parseAsStringEnum(modes).withDefault(modes[0] ?? "simple"));
4450
4533
  const showToggle = modes.length > 1;
4451
4534
  const { table, shallow, debounceMs, throttleMs } = useDataTable({
4452
4535
  data,
@@ -4541,7 +4624,8 @@ function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection
4541
4624
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4542
4625
  className: "flex w-full items-start justify-between gap-2 p-1",
4543
4626
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4544
- className: "flex flex-1 flex-wrap items-center gap-2",
4627
+ className: "flex flex-1 items-start gap-2 min-h-[40px]",
4628
+ "data-filter-parent": true,
4545
4629
  children: [currentMode !== "simple" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataTableSortList, {
4546
4630
  table,
4547
4631
  align: "start"
@@ -5125,92 +5209,36 @@ function ImportDialog({ open, onOpenChange, onImport, columns = [], title = "导
5125
5209
  });
5126
5210
  }
5127
5211
 
5128
- //#endregion
5129
- //#region src/components/auto-crud/export-dialog.tsx
5130
- function ExportDialog({ open, onOpenChange, selectedCount, onExport, canExportFiltered = false }) {
5131
- const [exporting, setExporting] = react.useState(null);
5132
- const [done, setDone] = react.useState(false);
5133
- const reset = react.useCallback(() => {
5134
- setExporting(null);
5135
- setDone(false);
5136
- }, []);
5137
- const handleOpenChange = react.useCallback((value) => {
5138
- if (!value) reset();
5139
- onOpenChange(value);
5140
- }, [onOpenChange, reset]);
5141
- const handleExport = react.useCallback(async (mode) => {
5142
- setExporting(mode);
5143
- try {
5144
- await onExport(mode);
5145
- setDone(true);
5146
- setTimeout(() => handleOpenChange(false), 800);
5147
- } catch {
5148
- setExporting(null);
5149
- }
5150
- }, [onExport, handleOpenChange]);
5151
- const hasSelected = selectedCount > 0;
5152
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Dialog, {
5153
- open,
5154
- onOpenChange: handleOpenChange,
5155
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DialogContent, {
5156
- className: "sm:max-w-[400px]",
5157
- 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", {
5158
- className: "flex flex-col items-center gap-3 py-6",
5159
- 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", {
5160
- className: "text-sm text-muted-foreground",
5161
- children: "导出成功"
5162
- })]
5163
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5164
- className: "grid gap-3 py-2",
5165
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.Button, {
5166
- variant: "outline",
5167
- className: "h-auto justify-start gap-3 px-4 py-3",
5168
- disabled: !hasSelected || exporting !== null,
5169
- onClick: () => handleExport("selected"),
5170
- 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", {
5171
- className: "flex flex-col items-start",
5172
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5173
- className: "font-medium",
5174
- children: ["导出选中", hasSelected ? ` (${selectedCount} 条)` : ""]
5175
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5176
- className: "text-xs text-muted-foreground",
5177
- children: hasSelected ? "导出当前勾选的数据行" : "请先在表格中勾选数据行"
5178
- })]
5179
- })]
5180
- }), canExportFiltered && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.Button, {
5181
- variant: "outline",
5182
- className: "h-auto justify-start gap-3 px-4 py-3",
5183
- disabled: exporting !== null,
5184
- onClick: () => handleExport("filtered"),
5185
- 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", {
5186
- className: "flex flex-col items-start",
5187
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5188
- className: "font-medium",
5189
- children: "导出筛选结果"
5190
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5191
- className: "text-xs text-muted-foreground",
5192
- children: "导出当前筛选条件下的所有数据"
5193
- })]
5194
- })]
5195
- })]
5196
- })]
5197
- })
5198
- });
5199
- }
5200
-
5201
5212
  //#endregion
5202
5213
  //#region src/components/auto-crud/auto-crud-table.tsx
5203
5214
  /**
5204
5215
  * 从统一配置生成表格 overrides
5216
+ * 当 filter 配置存在时,合并到 meta 中(不影响列隐藏)
5205
5217
  */
5206
5218
  function buildTableOverrides(fields, legacyOverrides) {
5207
5219
  const result = { ...legacyOverrides };
5208
- if (fields) for (const [key, config] of Object.entries(fields)) result[key] = {
5209
- ...result[key],
5210
- ...config.label && { label: config.label },
5211
- ...config.hidden && { hidden: true },
5212
- ...config.table
5213
- };
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
+ }
5214
5242
  return result;
5215
5243
  }
5216
5244
  /**
@@ -5364,8 +5392,8 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
5364
5392
  const canImport = can.import && !!resource.handlers.import;
5365
5393
  const canExport = can.export;
5366
5394
  const [importOpen, setImportOpen] = react.useState(false);
5367
- const [exportOpen, setExportOpen] = react.useState(false);
5368
5395
  const [selectedCount, setSelectedCount] = react.useState(0);
5396
+ const [exporting, setExporting] = react.useState(false);
5369
5397
  const getSelectedRowsRef = react.useRef(null);
5370
5398
  const importColumns = react.useMemo(() => {
5371
5399
  const shape = schema.shape;
@@ -5394,6 +5422,15 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
5394
5422
  title,
5395
5423
  denyFields
5396
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]);
5397
5434
  const tableOverrides = buildTableOverrides(fields, tableConfig?.overrides);
5398
5435
  const formOverrides = buildFormOverrides(fields, formConfig?.overrides, denyFields);
5399
5436
  const hiddenColumns = buildHiddenColumns(fields, tableConfig?.hidden, denyFields);
@@ -5429,8 +5466,9 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
5429
5466
  canExport && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.Button, {
5430
5467
  variant: "outline",
5431
5468
  size: "sm",
5432
- onClick: () => setExportOpen(true),
5433
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Upload, { className: "mr-2 h-4 w-4" }), "导出"]
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})` : "导出"]
5434
5472
  }),
5435
5473
  can.create && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
5436
5474
  onClick: resource.handlers.openCreate,
@@ -5497,18 +5535,84 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
5497
5535
  onOpenChange: setImportOpen,
5498
5536
  onImport: resource.handlers.import,
5499
5537
  columns: importColumns
5500
- }),
5501
- canExport && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportDialog, {
5502
- open: exportOpen,
5503
- onOpenChange: setExportOpen,
5504
- selectedCount,
5505
- onExport: handleExport,
5506
- canExportFiltered: !!resource.handlers.export
5507
5538
  })
5508
5539
  ]
5509
5540
  });
5510
5541
  }
5511
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
+
5512
5616
  //#endregion
5513
5617
  //#region src/components/data-table/data-table-advanced-toolbar.tsx
5514
5618
  function DataTableAdvancedToolbar({ table, children, className,...props }) {
@@ -6742,12 +6846,12 @@ function createMemoryDataSource(initialData = []) {
6742
6846
  });
6743
6847
  });
6744
6848
  if (params.sort && params.sort.length > 0) {
6745
- const { id, desc } = params.sort[0];
6849
+ const sortItem = params.sort[0];
6746
6850
  filtered.sort((a, b) => {
6747
- const aVal = a[id];
6748
- const bVal = b[id];
6851
+ const aVal = a[sortItem.id];
6852
+ const bVal = b[sortItem.id];
6749
6853
  const result = aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
6750
- return desc ? -result : result;
6854
+ return sortItem.desc ? -result : result;
6751
6855
  });
6752
6856
  }
6753
6857
  const start = (params.page - 1) * params.perPage;
@@ -6775,11 +6879,12 @@ function createMemoryDataSource(initialData = []) {
6775
6879
  async update(id, updateData) {
6776
6880
  const index = data.findIndex((item) => item.id === id);
6777
6881
  if (index === -1) throw new Error(`Item with id ${id} not found`);
6778
- data[index] = {
6882
+ const updated = {
6779
6883
  ...data[index],
6780
6884
  ...updateData
6781
6885
  };
6782
- return data[index];
6886
+ data[index] = updated;
6887
+ return updated;
6783
6888
  },
6784
6889
  async delete(id) {
6785
6890
  const index = data.findIndex((item) => item.id === id);