@wordrhyme/auto-crud 0.4.5 → 0.10.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 +1237 -514
- package/dist/index.d.cts +202 -21
- package/dist/index.d.ts +188 -7
- package/dist/index.js +1229 -517
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -2684,7 +2684,7 @@ function DataTableViewOptions({ table, disabled,...props }) {
|
|
|
2684
2684
|
role: "combobox",
|
|
2685
2685
|
variant: "outline",
|
|
2686
2686
|
size: "sm",
|
|
2687
|
-
className: "ml-auto
|
|
2687
|
+
className: "ml-auto flex h-8 font-normal",
|
|
2688
2688
|
disabled,
|
|
2689
2689
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Settings2, { className: "text-muted-foreground" }), "View"]
|
|
2690
2690
|
})
|
|
@@ -3515,454 +3515,146 @@ function ActionBarSeparator(props) {
|
|
|
3515
3515
|
}
|
|
3516
3516
|
|
|
3517
3517
|
//#endregion
|
|
3518
|
-
//#region src/
|
|
3519
|
-
function
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3518
|
+
//#region src/components/data-table/data-table-column-header.tsx
|
|
3519
|
+
function DataTableColumnHeader({ column, label, className,...props }) {
|
|
3520
|
+
if (!column.getCanSort() && !column.getCanHide()) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3521
|
+
className: cn(className),
|
|
3522
|
+
children: label
|
|
3523
|
+
});
|
|
3524
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuTrigger, {
|
|
3525
|
+
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),
|
|
3526
|
+
...props,
|
|
3527
|
+
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, {}))]
|
|
3528
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuContent, {
|
|
3529
|
+
align: "start",
|
|
3530
|
+
className: "w-28",
|
|
3531
|
+
children: [column.getCanSort() && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
3532
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuCheckboxItem, {
|
|
3533
|
+
className: "relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground",
|
|
3534
|
+
checked: column.getIsSorted() === "asc",
|
|
3535
|
+
onClick: () => column.toggleSorting(false),
|
|
3536
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronUp, {}), "Asc"]
|
|
3537
|
+
}),
|
|
3538
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuCheckboxItem, {
|
|
3539
|
+
className: "relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground",
|
|
3540
|
+
checked: column.getIsSorted() === "desc",
|
|
3541
|
+
onClick: () => column.toggleSorting(true),
|
|
3542
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, {}), "Desc"]
|
|
3543
|
+
}),
|
|
3544
|
+
column.getIsSorted() && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
3545
|
+
className: "pl-2 [&_svg]:text-muted-foreground",
|
|
3546
|
+
onClick: () => column.clearSorting(),
|
|
3547
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, {}), "Reset"]
|
|
3548
|
+
})
|
|
3549
|
+
] }), column.getCanHide() && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuCheckboxItem, {
|
|
3550
|
+
className: "relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground",
|
|
3551
|
+
checked: !column.getIsVisible(),
|
|
3552
|
+
onClick: () => column.toggleVisibility(false),
|
|
3553
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.EyeOff, {}), "Hide"]
|
|
3554
|
+
})]
|
|
3555
|
+
})] });
|
|
3535
3556
|
}
|
|
3536
3557
|
|
|
3537
3558
|
//#endregion
|
|
3538
|
-
//#region src/
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
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
|
-
]
|
|
3605
|
-
});
|
|
3559
|
+
//#region src/lib/humanize.ts
|
|
3560
|
+
/**
|
|
3561
|
+
* 将字符串转为人类可读格式
|
|
3562
|
+
*
|
|
3563
|
+
* @example
|
|
3564
|
+
* humanize("createdAt") // "Created At"
|
|
3565
|
+
* humanize("user_name") // "User Name"
|
|
3566
|
+
* humanize("firstName") // "First Name"
|
|
3567
|
+
*/
|
|
3568
|
+
function humanize(str) {
|
|
3569
|
+
return str.replace(/([A-Z])/g, " $1").replace(/[_-]/g, " ").replace(/^./, (s) => s.toUpperCase()).trim();
|
|
3606
3570
|
}
|
|
3607
3571
|
|
|
3608
3572
|
//#endregion
|
|
3609
|
-
//#region src/lib/
|
|
3573
|
+
//#region src/lib/schema-bridge/types.ts
|
|
3610
3574
|
/**
|
|
3611
|
-
*
|
|
3575
|
+
* 字段类型到 Filter Variant 映射
|
|
3576
|
+
* - enum 默认 multiSelect(通常需要多选)
|
|
3577
|
+
* - date 默认 dateRange(通常需要范围筛选)
|
|
3578
|
+
* - number 默认 range(通常需要范围筛选)
|
|
3612
3579
|
*/
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3580
|
+
const fieldTypeToFilterVariant = {
|
|
3581
|
+
string: "text",
|
|
3582
|
+
number: "range",
|
|
3583
|
+
boolean: "boolean",
|
|
3584
|
+
date: "dateRange",
|
|
3585
|
+
enum: "multiSelect",
|
|
3586
|
+
array: "multiSelect",
|
|
3587
|
+
object: "text"
|
|
3588
|
+
};
|
|
3589
|
+
/**
|
|
3590
|
+
* 字段类型到 Formily 组件映射
|
|
3591
|
+
*/
|
|
3592
|
+
const fieldTypeToFormilyComponent = {
|
|
3593
|
+
string: "Input",
|
|
3594
|
+
number: "NumberInput",
|
|
3595
|
+
boolean: "Switch",
|
|
3596
|
+
date: "DatePicker",
|
|
3597
|
+
enum: "Select",
|
|
3598
|
+
array: "ArrayCards",
|
|
3599
|
+
object: "ObjectField"
|
|
3600
|
+
};
|
|
3601
|
+
/**
|
|
3602
|
+
* 字段名模式到 Formily 组件映射
|
|
3603
|
+
* 优先级高于 fieldTypeToFormilyComponent
|
|
3604
|
+
* 使用正则表达式匹配字段名
|
|
3605
|
+
*/
|
|
3606
|
+
const fieldNamePatternToComponent = [
|
|
3607
|
+
{
|
|
3608
|
+
pattern: /^(description|content|body|notes|summary|bio|about|comment|message|text)$/i,
|
|
3609
|
+
component: "Textarea"
|
|
3610
|
+
},
|
|
3611
|
+
{
|
|
3612
|
+
pattern: /(description|content|body|notes|summary|bio|about|comment|message)$/i,
|
|
3613
|
+
component: "Textarea"
|
|
3614
|
+
},
|
|
3615
|
+
{
|
|
3616
|
+
pattern: /^color$/i,
|
|
3617
|
+
component: "ColorSelect"
|
|
3618
|
+
},
|
|
3619
|
+
{
|
|
3620
|
+
pattern: /color$/i,
|
|
3621
|
+
component: "ColorSelect"
|
|
3622
|
+
},
|
|
3623
|
+
{
|
|
3624
|
+
pattern: /^icon$/i,
|
|
3625
|
+
component: "IconPicker"
|
|
3626
|
+
},
|
|
3627
|
+
{
|
|
3628
|
+
pattern: /icon$/i,
|
|
3629
|
+
component: "IconPicker"
|
|
3630
|
+
}
|
|
3631
|
+
];
|
|
3632
|
+
/**
|
|
3633
|
+
* 根据字段名推断 Formily 组件
|
|
3634
|
+
*/
|
|
3635
|
+
function inferComponentByFieldName(fieldName) {
|
|
3636
|
+
for (const rule of fieldNamePatternToComponent) if (rule.pattern.test(fieldName)) return {
|
|
3637
|
+
component: rule.component,
|
|
3638
|
+
props: rule.props
|
|
3625
3639
|
};
|
|
3640
|
+
return null;
|
|
3626
3641
|
}
|
|
3627
|
-
const sortingItemSchema = zod.z.object({
|
|
3628
|
-
id: zod.z.string(),
|
|
3629
|
-
desc: zod.z.boolean()
|
|
3630
|
-
});
|
|
3631
|
-
const getSortingStateParser = (columnIds) => {
|
|
3632
|
-
const validKeys = columnIds ? columnIds instanceof Set ? columnIds : new Set(columnIds) : null;
|
|
3633
|
-
return createParser({
|
|
3634
|
-
parse: (value) => {
|
|
3635
|
-
try {
|
|
3636
|
-
const parsed = JSON.parse(value);
|
|
3637
|
-
const result = zod.z.array(sortingItemSchema).safeParse(parsed);
|
|
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
|
-
})] });
|
|
3864
|
-
}
|
|
3865
|
-
|
|
3866
|
-
//#endregion
|
|
3867
|
-
//#region src/lib/humanize.ts
|
|
3868
|
-
/**
|
|
3869
|
-
* 将字符串转为人类可读格式
|
|
3870
|
-
*
|
|
3871
|
-
* @example
|
|
3872
|
-
* humanize("createdAt") // "Created At"
|
|
3873
|
-
* humanize("user_name") // "User Name"
|
|
3874
|
-
* humanize("firstName") // "First Name"
|
|
3875
|
-
*/
|
|
3876
|
-
function humanize(str) {
|
|
3877
|
-
return str.replace(/([A-Z])/g, " $1").replace(/[_-]/g, " ").replace(/^./, (s) => s.toUpperCase()).trim();
|
|
3878
|
-
}
|
|
3879
|
-
|
|
3880
|
-
//#endregion
|
|
3881
|
-
//#region src/lib/schema-bridge/types.ts
|
|
3882
|
-
/**
|
|
3883
|
-
* 字段类型到 Filter Variant 映射
|
|
3884
|
-
* - enum 默认 multiSelect(通常需要多选)
|
|
3885
|
-
* - date 默认 dateRange(通常需要范围筛选)
|
|
3886
|
-
* - number 默认 range(通常需要范围筛选)
|
|
3887
|
-
*/
|
|
3888
|
-
const fieldTypeToFilterVariant = {
|
|
3889
|
-
string: "text",
|
|
3890
|
-
number: "range",
|
|
3891
|
-
boolean: "boolean",
|
|
3892
|
-
date: "dateRange",
|
|
3893
|
-
enum: "multiSelect",
|
|
3894
|
-
array: "multiSelect",
|
|
3895
|
-
object: "text"
|
|
3896
|
-
};
|
|
3897
|
-
/**
|
|
3898
|
-
* 字段类型到 Formily 组件映射
|
|
3899
|
-
*/
|
|
3900
|
-
const fieldTypeToFormilyComponent = {
|
|
3901
|
-
string: "Input",
|
|
3902
|
-
number: "NumberInput",
|
|
3903
|
-
boolean: "Switch",
|
|
3904
|
-
date: "DatePicker",
|
|
3905
|
-
enum: "Select",
|
|
3906
|
-
array: "ArrayCards",
|
|
3907
|
-
object: "ObjectField"
|
|
3908
|
-
};
|
|
3909
3642
|
/**
|
|
3910
|
-
* 字段名模式到
|
|
3911
|
-
* 优先级高于
|
|
3912
|
-
* 使用正则表达式匹配字段名
|
|
3643
|
+
* 字段名模式到 Filter Variant 映射
|
|
3644
|
+
* 优先级高于 fieldTypeToFilterVariant
|
|
3913
3645
|
*/
|
|
3914
|
-
const
|
|
3646
|
+
const fieldNamePatternToFilterVariant = [
|
|
3915
3647
|
{
|
|
3916
3648
|
pattern: /^(description|content|body|notes|summary|bio|about|comment|message|text)$/i,
|
|
3917
|
-
|
|
3649
|
+
variant: "text"
|
|
3918
3650
|
},
|
|
3919
3651
|
{
|
|
3920
3652
|
pattern: /(description|content|body|notes|summary|bio|about|comment|message)$/i,
|
|
3921
|
-
|
|
3922
|
-
},
|
|
3923
|
-
{
|
|
3924
|
-
pattern: /^color$/i,
|
|
3925
|
-
component: "ColorSelect"
|
|
3653
|
+
variant: "text"
|
|
3926
3654
|
},
|
|
3927
3655
|
{
|
|
3928
|
-
pattern:
|
|
3929
|
-
|
|
3930
|
-
},
|
|
3931
|
-
{
|
|
3932
|
-
pattern: /^icon$/i,
|
|
3933
|
-
component: "IconPicker"
|
|
3934
|
-
},
|
|
3935
|
-
{
|
|
3936
|
-
pattern: /icon$/i,
|
|
3937
|
-
component: "IconPicker"
|
|
3938
|
-
}
|
|
3939
|
-
];
|
|
3940
|
-
/**
|
|
3941
|
-
* 根据字段名推断 Formily 组件
|
|
3942
|
-
*/
|
|
3943
|
-
function inferComponentByFieldName(fieldName) {
|
|
3944
|
-
for (const rule of fieldNamePatternToComponent) if (rule.pattern.test(fieldName)) return {
|
|
3945
|
-
component: rule.component,
|
|
3946
|
-
props: rule.props
|
|
3947
|
-
};
|
|
3948
|
-
return null;
|
|
3949
|
-
}
|
|
3950
|
-
/**
|
|
3951
|
-
* 字段名模式到 Filter Variant 映射
|
|
3952
|
-
* 优先级高于 fieldTypeToFilterVariant
|
|
3953
|
-
*/
|
|
3954
|
-
const fieldNamePatternToFilterVariant = [
|
|
3955
|
-
{
|
|
3956
|
-
pattern: /^(description|content|body|notes|summary|bio|about|comment|message|text)$/i,
|
|
3957
|
-
variant: "text"
|
|
3958
|
-
},
|
|
3959
|
-
{
|
|
3960
|
-
pattern: /(description|content|body|notes|summary|bio|about|comment|message)$/i,
|
|
3961
|
-
variant: "text"
|
|
3962
|
-
},
|
|
3963
|
-
{
|
|
3964
|
-
pattern: /^(date|day)$/i,
|
|
3965
|
-
variant: "date"
|
|
3656
|
+
pattern: /^(date|day)$/i,
|
|
3657
|
+
variant: "date"
|
|
3966
3658
|
},
|
|
3967
3659
|
{
|
|
3968
3660
|
pattern: /(at|time|timestamp)$/i,
|
|
@@ -4161,73 +3853,571 @@ function createTableSchema(schema, options) {
|
|
|
4161
3853
|
meta,
|
|
4162
3854
|
...restOverride
|
|
4163
3855
|
});
|
|
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
|
-
};
|
|
3856
|
+
}
|
|
3857
|
+
return columns;
|
|
3858
|
+
}
|
|
3859
|
+
/**
|
|
3860
|
+
* 创建选择列
|
|
3861
|
+
*/
|
|
3862
|
+
function createSelectColumn() {
|
|
3863
|
+
return {
|
|
3864
|
+
id: "select",
|
|
3865
|
+
header: ({ table }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Checkbox, {
|
|
3866
|
+
"aria-label": "Select all",
|
|
3867
|
+
className: "translate-y-0.5",
|
|
3868
|
+
checked: table.getIsAllPageRowsSelected() || table.getIsSomePageRowsSelected() && "indeterminate",
|
|
3869
|
+
onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value)
|
|
3870
|
+
}),
|
|
3871
|
+
cell: ({ row }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Checkbox, {
|
|
3872
|
+
"aria-label": "Select row",
|
|
3873
|
+
className: "translate-y-0.5",
|
|
3874
|
+
checked: row.getIsSelected(),
|
|
3875
|
+
onCheckedChange: (value) => row.toggleSelected(!!value)
|
|
3876
|
+
}),
|
|
3877
|
+
enableHiding: false,
|
|
3878
|
+
enableSorting: false,
|
|
3879
|
+
size: 40
|
|
3880
|
+
};
|
|
3881
|
+
}
|
|
3882
|
+
/**
|
|
3883
|
+
* 创建操作列
|
|
3884
|
+
*/
|
|
3885
|
+
function createActionsColumn(config) {
|
|
3886
|
+
const { onEdit, onDelete, onView, onCopy } = config;
|
|
3887
|
+
return {
|
|
3888
|
+
id: "actions",
|
|
3889
|
+
cell: ({ row }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuTrigger, {
|
|
3890
|
+
asChild: true,
|
|
3891
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
3892
|
+
"aria-label": "Open menu",
|
|
3893
|
+
variant: "ghost",
|
|
3894
|
+
className: "flex size-8 p-0 data-[state=open]:bg-muted",
|
|
3895
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Ellipsis, { className: "size-4" })
|
|
3896
|
+
})
|
|
3897
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenuContent, {
|
|
3898
|
+
align: "end",
|
|
3899
|
+
className: "w-40",
|
|
3900
|
+
children: [
|
|
3901
|
+
onView && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
3902
|
+
onClick: () => onView(row.original),
|
|
3903
|
+
children: "查看"
|
|
3904
|
+
}),
|
|
3905
|
+
onEdit && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
3906
|
+
onClick: () => onEdit(row.original),
|
|
3907
|
+
children: "编辑"
|
|
3908
|
+
}),
|
|
3909
|
+
onCopy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
3910
|
+
onClick: () => onCopy(row.original),
|
|
3911
|
+
children: "复制"
|
|
3912
|
+
}),
|
|
3913
|
+
(onView || onEdit || onCopy) && onDelete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuSeparator, {}),
|
|
3914
|
+
onDelete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
3915
|
+
className: "text-destructive",
|
|
3916
|
+
onClick: () => onDelete(row.original),
|
|
3917
|
+
children: "删除"
|
|
3918
|
+
})
|
|
3919
|
+
]
|
|
3920
|
+
})] }),
|
|
3921
|
+
size: 40
|
|
3922
|
+
};
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
//#endregion
|
|
3926
|
+
//#region src/lib/import.ts
|
|
3927
|
+
/**
|
|
3928
|
+
* 解析 CSV 字符串为对象数组
|
|
3929
|
+
* 支持:引号包裹、逗号转义、换行符、空值
|
|
3930
|
+
*/
|
|
3931
|
+
function parseCSV(content) {
|
|
3932
|
+
const lines = parseCSVLines(content);
|
|
3933
|
+
if (lines.length === 0) return {
|
|
3934
|
+
headers: [],
|
|
3935
|
+
rows: []
|
|
3936
|
+
};
|
|
3937
|
+
const headers = lines[0];
|
|
3938
|
+
const rows = [];
|
|
3939
|
+
for (let i = 1; i < lines.length; i++) {
|
|
3940
|
+
const line = lines[i];
|
|
3941
|
+
if (line.length === 1 && line[0] === "") continue;
|
|
3942
|
+
const row = {};
|
|
3943
|
+
for (let j = 0; j < headers.length; j++) {
|
|
3944
|
+
const header = headers[j];
|
|
3945
|
+
row[header] = line[j] ?? "";
|
|
3946
|
+
}
|
|
3947
|
+
rows.push(row);
|
|
3948
|
+
}
|
|
3949
|
+
return {
|
|
3950
|
+
headers,
|
|
3951
|
+
rows
|
|
3952
|
+
};
|
|
3953
|
+
}
|
|
3954
|
+
/**
|
|
3955
|
+
* 将 CSV 文本解析为二维数组(处理引号转义)
|
|
3956
|
+
*/
|
|
3957
|
+
function parseCSVLines(text) {
|
|
3958
|
+
const result = [];
|
|
3959
|
+
let current = "";
|
|
3960
|
+
let inQuotes = false;
|
|
3961
|
+
let row = [];
|
|
3962
|
+
for (let i = 0; i < text.length; i++) {
|
|
3963
|
+
const char = text[i];
|
|
3964
|
+
const next = text[i + 1];
|
|
3965
|
+
if (inQuotes) if (char === "\"" && next === "\"") {
|
|
3966
|
+
current += "\"";
|
|
3967
|
+
i++;
|
|
3968
|
+
} else if (char === "\"") inQuotes = false;
|
|
3969
|
+
else current += char;
|
|
3970
|
+
else if (char === "\"") inQuotes = true;
|
|
3971
|
+
else if (char === ",") {
|
|
3972
|
+
row.push(current);
|
|
3973
|
+
current = "";
|
|
3974
|
+
} else if (char === "\r" && next === "\n") {
|
|
3975
|
+
row.push(current);
|
|
3976
|
+
current = "";
|
|
3977
|
+
result.push(row);
|
|
3978
|
+
row = [];
|
|
3979
|
+
i++;
|
|
3980
|
+
} else if (char === "\n") {
|
|
3981
|
+
row.push(current);
|
|
3982
|
+
current = "";
|
|
3983
|
+
result.push(row);
|
|
3984
|
+
row = [];
|
|
3985
|
+
} else current += char;
|
|
3986
|
+
}
|
|
3987
|
+
if (current !== "" || row.length > 0) {
|
|
3988
|
+
row.push(current);
|
|
3989
|
+
result.push(row);
|
|
3990
|
+
}
|
|
3991
|
+
return result;
|
|
3992
|
+
}
|
|
3993
|
+
/**
|
|
3994
|
+
* 解析 JSON 字符串为对象数组
|
|
3995
|
+
* 支持 JSON 数组或 { data: [...] } 格式
|
|
3996
|
+
*/
|
|
3997
|
+
function parseJSON(content) {
|
|
3998
|
+
const parsed = JSON.parse(content);
|
|
3999
|
+
if (Array.isArray(parsed)) return parsed;
|
|
4000
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.data)) return parsed.data;
|
|
4001
|
+
throw new Error("Invalid JSON format: expected an array or { data: [...] } object");
|
|
4002
|
+
}
|
|
4003
|
+
/**
|
|
4004
|
+
* 统一解析入口:根据文件扩展名自动选择解析器
|
|
4005
|
+
*/
|
|
4006
|
+
async function parseImportFile(file) {
|
|
4007
|
+
const content = await file.text();
|
|
4008
|
+
if (file.name.split(".").pop()?.toLowerCase() === "json") {
|
|
4009
|
+
const rows$1 = parseJSON(content);
|
|
4010
|
+
return {
|
|
4011
|
+
headers: rows$1.length > 0 ? Object.keys(rows$1[0]) : [],
|
|
4012
|
+
rows: rows$1,
|
|
4013
|
+
format: "json"
|
|
4014
|
+
};
|
|
4015
|
+
}
|
|
4016
|
+
const { headers, rows } = parseCSV(content);
|
|
4017
|
+
return {
|
|
4018
|
+
headers,
|
|
4019
|
+
rows,
|
|
4020
|
+
format: "csv"
|
|
4021
|
+
};
|
|
4022
|
+
}
|
|
4023
|
+
/**
|
|
4024
|
+
* 生成 CSV 模板(仅包含表头,用于下载空模板)
|
|
4025
|
+
*/
|
|
4026
|
+
function generateCSVTemplate(headers) {
|
|
4027
|
+
return headers.map((h) => escapeCSVField(h)).join(",") + "\n";
|
|
4028
|
+
}
|
|
4029
|
+
/**
|
|
4030
|
+
* 将数据数组转换为 CSV 字符串
|
|
4031
|
+
*/
|
|
4032
|
+
function dataToCSV(data, opts = {}) {
|
|
4033
|
+
if (data.length === 0) return "";
|
|
4034
|
+
const allHeaders = Object.keys(data[0]);
|
|
4035
|
+
const headers = (opts.headers ?? allHeaders).filter((h) => !opts.excludeColumns?.includes(h));
|
|
4036
|
+
return [headers.map((h) => escapeCSVField(h)).join(","), ...data.map((row) => headers.map((h) => escapeCSVField(String(row[h] ?? ""))).join(","))].join("\n");
|
|
4037
|
+
}
|
|
4038
|
+
function escapeCSVField(field) {
|
|
4039
|
+
if (field.includes(",") || field.includes("\"") || field.includes("\n") || field.includes("\r")) return `"${field.replace(/"/g, "\"\"")}"`;
|
|
4040
|
+
return field;
|
|
4041
|
+
}
|
|
4042
|
+
/**
|
|
4043
|
+
* 根据 Zod Schema 将 CSV 解析出的 string 值转换为正确类型
|
|
4044
|
+
*
|
|
4045
|
+
* CSV 解析后所有值都是 string,需要根据 schema 字段类型进行转换:
|
|
4046
|
+
* - number: parseFloat
|
|
4047
|
+
* - boolean: "true"/"1"/"yes" → true, 其他 → false
|
|
4048
|
+
* - date: new Date(value)
|
|
4049
|
+
* - 空字符串 → undefined(让 schema 默认值生效)
|
|
4050
|
+
*/
|
|
4051
|
+
function coerceRowValues(rows, schema) {
|
|
4052
|
+
const shape = schema.shape;
|
|
4053
|
+
const fieldTypes = /* @__PURE__ */ new Map();
|
|
4054
|
+
for (const [key, fieldSchema] of Object.entries(shape)) {
|
|
4055
|
+
const parsed = parseZodField(fieldSchema);
|
|
4056
|
+
fieldTypes.set(key, parsed.type);
|
|
4057
|
+
}
|
|
4058
|
+
return rows.map((row) => {
|
|
4059
|
+
const coerced = {};
|
|
4060
|
+
for (const [key, value] of Object.entries(row)) {
|
|
4061
|
+
const type = fieldTypes.get(key);
|
|
4062
|
+
if (!type) {
|
|
4063
|
+
coerced[key] = value;
|
|
4064
|
+
continue;
|
|
4065
|
+
}
|
|
4066
|
+
if (value === "" || value === null || value === void 0) {
|
|
4067
|
+
coerced[key] = void 0;
|
|
4068
|
+
continue;
|
|
4069
|
+
}
|
|
4070
|
+
if (typeof value !== "string") {
|
|
4071
|
+
coerced[key] = value;
|
|
4072
|
+
continue;
|
|
4073
|
+
}
|
|
4074
|
+
switch (type) {
|
|
4075
|
+
case "number": {
|
|
4076
|
+
const num = parseFloat(value);
|
|
4077
|
+
coerced[key] = isNaN(num) ? void 0 : num;
|
|
4078
|
+
break;
|
|
4079
|
+
}
|
|
4080
|
+
case "boolean": {
|
|
4081
|
+
const lower = value.toLowerCase().trim();
|
|
4082
|
+
coerced[key] = lower === "true" || lower === "1" || lower === "yes";
|
|
4083
|
+
break;
|
|
4084
|
+
}
|
|
4085
|
+
case "date": {
|
|
4086
|
+
const date = new Date(value);
|
|
4087
|
+
coerced[key] = isNaN(date.getTime()) ? void 0 : date;
|
|
4088
|
+
break;
|
|
4089
|
+
}
|
|
4090
|
+
default: coerced[key] = value;
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
return coerced;
|
|
4094
|
+
});
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
//#endregion
|
|
4098
|
+
//#region src/lib/export.ts
|
|
4099
|
+
function exportTableToCSV(table, opts = {}) {
|
|
4100
|
+
const { filename = "table", excludeColumns = [], onlySelected = false } = opts;
|
|
4101
|
+
const headers = table.getAllLeafColumns().map((column) => column.id).filter((id) => !excludeColumns.includes(id));
|
|
4102
|
+
downloadCSV([headers.join(","), ...(onlySelected ? table.getFilteredSelectedRowModel().rows : table.getRowModel().rows).map((row) => headers.map((header) => {
|
|
4103
|
+
const cellValue = row.getValue(header);
|
|
4104
|
+
return typeof cellValue === "string" ? `"${cellValue.replace(/"/g, "\"\"")}"` : cellValue;
|
|
4105
|
+
}).join(","))].join("\n"), filename);
|
|
4106
|
+
}
|
|
4107
|
+
/**
|
|
4108
|
+
* 导出原始数据数组为 CSV(用于服务端全量导出)
|
|
4109
|
+
*/
|
|
4110
|
+
function exportAllToCSV(data, opts = {}) {
|
|
4111
|
+
const { filename = "export",...csvOpts } = opts;
|
|
4112
|
+
downloadCSV(dataToCSV(data, csvOpts), filename);
|
|
4113
|
+
}
|
|
4114
|
+
/**
|
|
4115
|
+
* 下载 CSV 模板文件
|
|
4116
|
+
*/
|
|
4117
|
+
function downloadCSVTemplate(headers, filename = "template") {
|
|
4118
|
+
downloadCSV(headers.map((h) => {
|
|
4119
|
+
if (h.includes(",") || h.includes("\"") || h.includes("\n")) return `"${h.replace(/"/g, "\"\"")}"`;
|
|
4120
|
+
return h;
|
|
4121
|
+
}).join(",") + "\n", filename);
|
|
4122
|
+
}
|
|
4123
|
+
function downloadCSV(csvContent, filename) {
|
|
4124
|
+
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
|
4125
|
+
const url = URL.createObjectURL(blob);
|
|
4126
|
+
const link = document.createElement("a");
|
|
4127
|
+
link.setAttribute("href", url);
|
|
4128
|
+
link.setAttribute("download", `${filename}.csv`);
|
|
4129
|
+
link.style.visibility = "hidden";
|
|
4130
|
+
document.body.appendChild(link);
|
|
4131
|
+
link.click();
|
|
4132
|
+
document.body.removeChild(link);
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
//#endregion
|
|
4136
|
+
//#region src/components/auto-crud/auto-table-action-bar.tsx
|
|
4137
|
+
function AutoTableActionBar({ table, onDeleteSelected, onUpdateSelected, batchUpdateFields, enableExport = true, enableDelete = true, extraActions }) {
|
|
4138
|
+
const rows = table.getFilteredSelectedRowModel().rows;
|
|
4139
|
+
const onOpenChange = react.useCallback((open) => {
|
|
4140
|
+
if (!open) table.toggleAllRowsSelected(false);
|
|
4141
|
+
}, [table]);
|
|
4142
|
+
const onExport = react.useCallback(() => {
|
|
4143
|
+
exportTableToCSV(table, {
|
|
4144
|
+
excludeColumns: ["select", "actions"],
|
|
4145
|
+
onlySelected: true
|
|
4146
|
+
});
|
|
4147
|
+
}, [table]);
|
|
4148
|
+
const onDelete = react.useCallback(() => {
|
|
4149
|
+
if (onDeleteSelected) onDeleteSelected(rows.map((row) => row.original));
|
|
4150
|
+
}, [rows, onDeleteSelected]);
|
|
4151
|
+
const handleBatchUpdate = react.useCallback((field, value) => {
|
|
4152
|
+
if (onUpdateSelected) {
|
|
4153
|
+
onUpdateSelected(rows.map((row) => row.original), { [field]: value });
|
|
4154
|
+
table.toggleAllRowsSelected(false);
|
|
4155
|
+
}
|
|
4156
|
+
}, [
|
|
4157
|
+
rows,
|
|
4158
|
+
onUpdateSelected,
|
|
4159
|
+
table
|
|
4160
|
+
]);
|
|
4161
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ActionBar, {
|
|
4162
|
+
open: rows.length > 0,
|
|
4163
|
+
onOpenChange,
|
|
4164
|
+
children: [
|
|
4165
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ActionBarSelection, { children: [
|
|
4166
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4167
|
+
className: "font-medium",
|
|
4168
|
+
children: rows.length
|
|
4169
|
+
}),
|
|
4170
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "selected" }),
|
|
4171
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionBarSeparator, {}),
|
|
4172
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionBarClose, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, {}) })
|
|
4173
|
+
] }),
|
|
4174
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionBarSeparator, {}),
|
|
4175
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ActionBarGroup, { children: [
|
|
4176
|
+
batchUpdateFields?.map((fieldConfig) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuTrigger, {
|
|
4177
|
+
asChild: true,
|
|
4178
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.Button, {
|
|
4179
|
+
variant: "ghost",
|
|
4180
|
+
size: "sm",
|
|
4181
|
+
className: "h-7 gap-1 px-2",
|
|
4182
|
+
children: [fieldConfig.label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "size-3.5" })]
|
|
4183
|
+
})
|
|
4184
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuContent, {
|
|
4185
|
+
align: "start",
|
|
4186
|
+
children: fieldConfig.options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuItem, {
|
|
4187
|
+
onClick: () => handleBatchUpdate(fieldConfig.field, option.value),
|
|
4188
|
+
children: option.label
|
|
4189
|
+
}, option.value))
|
|
4190
|
+
})] }, fieldConfig.field)),
|
|
4191
|
+
extraActions,
|
|
4192
|
+
enableExport && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ActionBarItem, {
|
|
4193
|
+
onClick: onExport,
|
|
4194
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Download, {}), "Export"]
|
|
4195
|
+
}),
|
|
4196
|
+
enableDelete && onDeleteSelected && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ActionBarItem, {
|
|
4197
|
+
variant: "destructive",
|
|
4198
|
+
onClick: onDelete,
|
|
4199
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Trash2, {}), "Delete"]
|
|
4200
|
+
})
|
|
4201
|
+
] })
|
|
4202
|
+
]
|
|
4203
|
+
});
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
//#endregion
|
|
4207
|
+
//#region src/lib/parsers.ts
|
|
4208
|
+
/**
|
|
4209
|
+
* 创建自定义 Parser(替代 nuqs 的 createParser)
|
|
4210
|
+
*/
|
|
4211
|
+
function createParser(config) {
|
|
4212
|
+
return {
|
|
4213
|
+
parse: config.parse,
|
|
4214
|
+
serialize: (value) => value === null ? "" : config.serialize(value),
|
|
4215
|
+
withDefault: (defaultValue) => ({
|
|
4216
|
+
parse: (value) => {
|
|
4217
|
+
const parsed = config.parse(value);
|
|
4218
|
+
return parsed === null ? defaultValue : parsed;
|
|
4219
|
+
},
|
|
4220
|
+
serialize: config.serialize,
|
|
4221
|
+
defaultValue
|
|
4222
|
+
})
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
const sortingItemSchema = zod.z.object({
|
|
4226
|
+
id: zod.z.string(),
|
|
4227
|
+
desc: zod.z.boolean()
|
|
4228
|
+
});
|
|
4229
|
+
const getSortingStateParser = (columnIds) => {
|
|
4230
|
+
const validKeys = columnIds ? columnIds instanceof Set ? columnIds : new Set(columnIds) : null;
|
|
4231
|
+
return createParser({
|
|
4232
|
+
parse: (value) => {
|
|
4233
|
+
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;
|
|
4239
|
+
} catch {
|
|
4240
|
+
return null;
|
|
4241
|
+
}
|
|
4242
|
+
},
|
|
4243
|
+
serialize: (value) => JSON.stringify(value)
|
|
4244
|
+
});
|
|
4245
|
+
};
|
|
4246
|
+
const filterItemSchema = zod.z.object({
|
|
4247
|
+
id: zod.z.string(),
|
|
4248
|
+
value: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]),
|
|
4249
|
+
variant: zod.z.enum(dataTableConfig.filterVariants),
|
|
4250
|
+
operator: zod.z.enum(dataTableConfig.operators),
|
|
4251
|
+
filterId: zod.z.string()
|
|
4252
|
+
});
|
|
4253
|
+
|
|
4254
|
+
//#endregion
|
|
4255
|
+
//#region src/hooks/use-data-table.ts
|
|
4256
|
+
const PAGE_KEY = "page";
|
|
4257
|
+
const PER_PAGE_KEY = "perPage";
|
|
4258
|
+
const SORT_KEY = "sort";
|
|
4259
|
+
const FILTERS_KEY = "filters";
|
|
4260
|
+
const JOIN_OPERATOR_KEY = "joinOperator";
|
|
4261
|
+
const ARRAY_SEPARATOR = ",";
|
|
4262
|
+
const DEBOUNCE_MS = 300;
|
|
4263
|
+
const THROTTLE_MS = 50;
|
|
4264
|
+
function useDataTable(props) {
|
|
4265
|
+
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;
|
|
4266
|
+
const pageKey = queryKeys?.page ?? PAGE_KEY;
|
|
4267
|
+
const perPageKey = queryKeys?.perPage ?? PER_PAGE_KEY;
|
|
4268
|
+
const sortKey = queryKeys?.sort ?? SORT_KEY;
|
|
4269
|
+
const filtersKey = queryKeys?.filters ?? FILTERS_KEY;
|
|
4270
|
+
const joinOperatorKey = queryKeys?.joinOperator ?? JOIN_OPERATOR_KEY;
|
|
4271
|
+
const queryStateOptions = react.useMemo(() => ({
|
|
4272
|
+
history,
|
|
4273
|
+
scroll,
|
|
4274
|
+
shallow,
|
|
4275
|
+
throttleMs,
|
|
4276
|
+
debounceMs,
|
|
4277
|
+
clearOnDefault,
|
|
4278
|
+
startTransition
|
|
4279
|
+
}), [
|
|
4280
|
+
history,
|
|
4281
|
+
scroll,
|
|
4282
|
+
shallow,
|
|
4283
|
+
throttleMs,
|
|
4284
|
+
debounceMs,
|
|
4285
|
+
clearOnDefault,
|
|
4286
|
+
startTransition
|
|
4287
|
+
]);
|
|
4288
|
+
const [rowSelection, setRowSelection] = react.useState(initialState?.rowSelection ?? {});
|
|
4289
|
+
const [columnVisibility, setColumnVisibility] = react.useState(initialState?.columnVisibility ?? {});
|
|
4290
|
+
const [page, setPage] = useQueryState(pageKey, parseAsInteger.withDefault(1));
|
|
4291
|
+
const [perPage, setPerPage] = useQueryState(perPageKey, parseAsInteger.withDefault(initialState?.pagination?.pageSize ?? 10));
|
|
4292
|
+
const pagination = react.useMemo(() => {
|
|
4293
|
+
return {
|
|
4294
|
+
pageIndex: page - 1,
|
|
4295
|
+
pageSize: perPage
|
|
4296
|
+
};
|
|
4297
|
+
}, [page, perPage]);
|
|
4298
|
+
const onPaginationChange = react.useCallback((updaterOrValue) => {
|
|
4299
|
+
if (typeof updaterOrValue === "function") {
|
|
4300
|
+
const newPagination = updaterOrValue(pagination);
|
|
4301
|
+
setPage(newPagination.pageIndex + 1);
|
|
4302
|
+
setPerPage(newPagination.pageSize);
|
|
4303
|
+
} else {
|
|
4304
|
+
setPage(updaterOrValue.pageIndex + 1);
|
|
4305
|
+
setPerPage(updaterOrValue.pageSize);
|
|
4306
|
+
}
|
|
4307
|
+
}, [
|
|
4308
|
+
pagination,
|
|
4309
|
+
setPage,
|
|
4310
|
+
setPerPage
|
|
4311
|
+
]);
|
|
4312
|
+
const [sorting, setSorting] = useQueryState(sortKey, getSortingStateParser(react.useMemo(() => {
|
|
4313
|
+
return new Set(columns.map((column) => column.id).filter(Boolean));
|
|
4314
|
+
}, [columns])).withDefault(initialState?.sorting ?? []));
|
|
4315
|
+
const onSortingChange = react.useCallback((updaterOrValue) => {
|
|
4316
|
+
if (typeof updaterOrValue === "function") setSorting(updaterOrValue(sorting));
|
|
4317
|
+
else setSorting(updaterOrValue);
|
|
4318
|
+
}, [sorting, setSorting]);
|
|
4319
|
+
const filterableColumns = react.useMemo(() => {
|
|
4320
|
+
if (enableAdvancedFilter) return [];
|
|
4321
|
+
return columns.filter((column) => column.enableColumnFilter);
|
|
4322
|
+
}, [columns, enableAdvancedFilter]);
|
|
4323
|
+
const [filterValues, setFilterValues] = useQueryStates(react.useMemo(() => {
|
|
4324
|
+
if (enableAdvancedFilter) return {};
|
|
4325
|
+
return filterableColumns.reduce((acc, column) => {
|
|
4326
|
+
if (column.meta?.options) acc[column.id ?? ""] = parseAsArrayOf(parseAsString, ARRAY_SEPARATOR).withDefault([]);
|
|
4327
|
+
else acc[column.id ?? ""] = parseAsString.withDefault("");
|
|
4328
|
+
return acc;
|
|
4329
|
+
}, {});
|
|
4330
|
+
}, [filterableColumns, enableAdvancedFilter]));
|
|
4331
|
+
const debouncedSetFilterValues = useDebouncedCallback((values) => {
|
|
4332
|
+
setPage(1);
|
|
4333
|
+
setFilterValues(values);
|
|
4334
|
+
}, debounceMs);
|
|
4335
|
+
const initialColumnFilters = react.useMemo(() => {
|
|
4336
|
+
if (enableAdvancedFilter) return [];
|
|
4337
|
+
return Object.entries(filterValues).reduce((filters, [key, value]) => {
|
|
4338
|
+
if (value !== null) {
|
|
4339
|
+
const processedValue = Array.isArray(value) ? value : typeof value === "string" && /[^a-zA-Z0-9]/.test(value) ? value.split(/[^a-zA-Z0-9]+/).filter(Boolean) : [value];
|
|
4340
|
+
filters.push({
|
|
4341
|
+
id: key,
|
|
4342
|
+
value: processedValue
|
|
4343
|
+
});
|
|
4344
|
+
}
|
|
4345
|
+
return filters;
|
|
4346
|
+
}, []);
|
|
4347
|
+
}, [filterValues, enableAdvancedFilter]);
|
|
4348
|
+
const [columnFilters, setColumnFilters] = react.useState(initialColumnFilters);
|
|
4349
|
+
const onColumnFiltersChange = react.useCallback((updaterOrValue) => {
|
|
4350
|
+
if (enableAdvancedFilter) return;
|
|
4351
|
+
setColumnFilters((prev) => {
|
|
4352
|
+
const next = typeof updaterOrValue === "function" ? updaterOrValue(prev) : updaterOrValue;
|
|
4353
|
+
const filterUpdates = next.reduce((acc, filter) => {
|
|
4354
|
+
if (filterableColumns.find((column) => column.id === filter.id)) acc[filter.id] = filter.value;
|
|
4355
|
+
return acc;
|
|
4356
|
+
}, {});
|
|
4357
|
+
for (const prevFilter of prev) if (!next.some((filter) => filter.id === prevFilter.id)) filterUpdates[prevFilter.id] = null;
|
|
4358
|
+
debouncedSetFilterValues(filterUpdates);
|
|
4359
|
+
return next;
|
|
4360
|
+
});
|
|
4361
|
+
}, [
|
|
4362
|
+
debouncedSetFilterValues,
|
|
4363
|
+
filterableColumns,
|
|
4364
|
+
enableAdvancedFilter
|
|
4365
|
+
]);
|
|
4366
|
+
const table = (0, __tanstack_react_table.useReactTable)({
|
|
4367
|
+
...tableProps,
|
|
4368
|
+
columns,
|
|
4369
|
+
initialState,
|
|
4370
|
+
pageCount,
|
|
4371
|
+
state: {
|
|
4372
|
+
pagination,
|
|
4373
|
+
sorting,
|
|
4374
|
+
columnVisibility,
|
|
4375
|
+
rowSelection,
|
|
4376
|
+
columnFilters
|
|
4377
|
+
},
|
|
4378
|
+
defaultColumn: {
|
|
4379
|
+
...tableProps.defaultColumn,
|
|
4380
|
+
enableColumnFilter: false
|
|
4381
|
+
},
|
|
4382
|
+
enableRowSelection: true,
|
|
4383
|
+
onRowSelectionChange: setRowSelection,
|
|
4384
|
+
onPaginationChange,
|
|
4385
|
+
onSortingChange,
|
|
4386
|
+
onColumnFiltersChange,
|
|
4387
|
+
onColumnVisibilityChange: setColumnVisibility,
|
|
4388
|
+
getCoreRowModel: (0, __tanstack_react_table.getCoreRowModel)(),
|
|
4389
|
+
getFilteredRowModel: (0, __tanstack_react_table.getFilteredRowModel)(),
|
|
4390
|
+
getPaginationRowModel: (0, __tanstack_react_table.getPaginationRowModel)(),
|
|
4391
|
+
getSortedRowModel: (0, __tanstack_react_table.getSortedRowModel)(),
|
|
4392
|
+
getFacetedRowModel: (0, __tanstack_react_table.getFacetedRowModel)(),
|
|
4393
|
+
getFacetedUniqueValues: (0, __tanstack_react_table.getFacetedUniqueValues)(),
|
|
4394
|
+
getFacetedMinMaxValues: (0, __tanstack_react_table.getFacetedMinMaxValues)(),
|
|
4395
|
+
manualPagination: true,
|
|
4396
|
+
manualSorting: true,
|
|
4397
|
+
manualFiltering: true,
|
|
4398
|
+
meta: {
|
|
4399
|
+
...tableProps.meta,
|
|
4400
|
+
queryKeys: {
|
|
4401
|
+
page: pageKey,
|
|
4402
|
+
perPage: perPageKey,
|
|
4403
|
+
sort: sortKey,
|
|
4404
|
+
filters: filtersKey,
|
|
4405
|
+
joinOperator: joinOperatorKey
|
|
4406
|
+
},
|
|
4407
|
+
queryStateOptions
|
|
4408
|
+
}
|
|
4409
|
+
});
|
|
4410
|
+
return react.useMemo(() => ({
|
|
4411
|
+
table,
|
|
4412
|
+
shallow,
|
|
4413
|
+
debounceMs,
|
|
4414
|
+
throttleMs
|
|
4415
|
+
}), [
|
|
4416
|
+
table,
|
|
4417
|
+
shallow,
|
|
4418
|
+
debounceMs,
|
|
4419
|
+
throttleMs
|
|
4420
|
+
]);
|
|
4231
4421
|
}
|
|
4232
4422
|
|
|
4233
4423
|
//#endregion
|
|
@@ -4250,7 +4440,7 @@ const filterModeConfig = {
|
|
|
4250
4440
|
tooltip: "Linear-style command filters"
|
|
4251
4441
|
}
|
|
4252
4442
|
};
|
|
4253
|
-
function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection = true, exclude, actions, pinnedColumns, filterMode = "simple", onDeleteSelected, onUpdateSelected, batchUpdateFields, actionBarExtra, initialSorting, enableExport = true }) {
|
|
4443
|
+
function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection = true, exclude, actions, pinnedColumns, filterMode = "simple", onDeleteSelected, onUpdateSelected, batchUpdateFields, actionBarExtra, initialSorting, enableExport = true, onSelectedCountChange, getSelectedRows }) {
|
|
4254
4444
|
const modes = filterMode ? Array.isArray(filterMode) ? filterMode : [filterMode] : [
|
|
4255
4445
|
"simple",
|
|
4256
4446
|
"advanced",
|
|
@@ -4287,6 +4477,17 @@ function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection
|
|
|
4287
4477
|
shallow: false,
|
|
4288
4478
|
clearOnDefault: true
|
|
4289
4479
|
});
|
|
4480
|
+
const getSelectedRowsFn = (0, react.useCallback)(() => table.getFilteredSelectedRowModel().rows.map((row) => row.original), [table]);
|
|
4481
|
+
(0, react.useEffect)(() => {
|
|
4482
|
+
if (getSelectedRows) getSelectedRows.current = getSelectedRowsFn;
|
|
4483
|
+
return () => {
|
|
4484
|
+
if (getSelectedRows) getSelectedRows.current = null;
|
|
4485
|
+
};
|
|
4486
|
+
}, [getSelectedRows, getSelectedRowsFn]);
|
|
4487
|
+
const selectedRowCount = table.getFilteredSelectedRowModel().rows.length;
|
|
4488
|
+
(0, react.useEffect)(() => {
|
|
4489
|
+
onSelectedCountChange?.(selectedRowCount);
|
|
4490
|
+
}, [selectedRowCount, onSelectedCountChange]);
|
|
4290
4491
|
const FilterModeSelect = showToggle ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.DropdownMenuTrigger, {
|
|
4291
4492
|
asChild: true,
|
|
4292
4493
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
@@ -4618,6 +4819,385 @@ function CrudFormModal({ open, onOpenChange, mode, schema, initialValues, onSubm
|
|
|
4618
4819
|
});
|
|
4619
4820
|
}
|
|
4620
4821
|
|
|
4822
|
+
//#endregion
|
|
4823
|
+
//#region src/components/auto-crud/import-dialog.tsx
|
|
4824
|
+
function ImportDialog({ open, onOpenChange, onImport, columns = [], title = "导入数据" }) {
|
|
4825
|
+
const [step, setStep] = react.useState("upload");
|
|
4826
|
+
const [parsedData, setParsedData] = react.useState(null);
|
|
4827
|
+
const [error, setError] = react.useState(null);
|
|
4828
|
+
const [result, setResult] = react.useState(null);
|
|
4829
|
+
const [isDragging, setIsDragging] = react.useState(false);
|
|
4830
|
+
const fileInputRef = react.useRef(null);
|
|
4831
|
+
const reset = react.useCallback(() => {
|
|
4832
|
+
setStep("upload");
|
|
4833
|
+
setParsedData(null);
|
|
4834
|
+
setError(null);
|
|
4835
|
+
setResult(null);
|
|
4836
|
+
setIsDragging(false);
|
|
4837
|
+
}, []);
|
|
4838
|
+
const handleOpenChange = react.useCallback((value) => {
|
|
4839
|
+
if (!value) reset();
|
|
4840
|
+
onOpenChange(value);
|
|
4841
|
+
}, [onOpenChange, reset]);
|
|
4842
|
+
const handleFile = react.useCallback(async (file) => {
|
|
4843
|
+
setError(null);
|
|
4844
|
+
try {
|
|
4845
|
+
const data = await parseImportFile(file);
|
|
4846
|
+
if (data.rows.length === 0) {
|
|
4847
|
+
setError("文件中没有数据行");
|
|
4848
|
+
return;
|
|
4849
|
+
}
|
|
4850
|
+
setParsedData(data);
|
|
4851
|
+
setStep("preview");
|
|
4852
|
+
} catch (e) {
|
|
4853
|
+
setError(e instanceof Error ? e.message : "文件解析失败");
|
|
4854
|
+
}
|
|
4855
|
+
}, []);
|
|
4856
|
+
const handleDrop = react.useCallback((e) => {
|
|
4857
|
+
e.preventDefault();
|
|
4858
|
+
setIsDragging(false);
|
|
4859
|
+
const file = e.dataTransfer.files[0];
|
|
4860
|
+
if (file) handleFile(file);
|
|
4861
|
+
}, [handleFile]);
|
|
4862
|
+
const handleFileInput = react.useCallback((e) => {
|
|
4863
|
+
const file = e.target.files?.[0];
|
|
4864
|
+
if (file) handleFile(file);
|
|
4865
|
+
}, [handleFile]);
|
|
4866
|
+
const handleImport = react.useCallback(async () => {
|
|
4867
|
+
if (!parsedData) return;
|
|
4868
|
+
setStep("importing");
|
|
4869
|
+
try {
|
|
4870
|
+
setResult(await onImport(parsedData.rows));
|
|
4871
|
+
setStep("result");
|
|
4872
|
+
} catch (e) {
|
|
4873
|
+
setError(e instanceof Error ? e.message : "导入失败");
|
|
4874
|
+
setStep("preview");
|
|
4875
|
+
}
|
|
4876
|
+
}, [parsedData, onImport]);
|
|
4877
|
+
const handleDownloadTemplate = react.useCallback(() => {
|
|
4878
|
+
if (columns.length === 0) return;
|
|
4879
|
+
const csv = generateCSVTemplate(columns);
|
|
4880
|
+
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
|
4881
|
+
const url = URL.createObjectURL(blob);
|
|
4882
|
+
const link = document.createElement("a");
|
|
4883
|
+
link.href = url;
|
|
4884
|
+
link.download = "import-template.csv";
|
|
4885
|
+
link.click();
|
|
4886
|
+
URL.revokeObjectURL(url);
|
|
4887
|
+
}, [columns]);
|
|
4888
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Dialog, {
|
|
4889
|
+
open,
|
|
4890
|
+
onOpenChange: handleOpenChange,
|
|
4891
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.DialogContent, {
|
|
4892
|
+
className: "sm:max-w-[600px]",
|
|
4893
|
+
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: [
|
|
4894
|
+
step === "upload" && "上传 CSV 或 JSON 文件以批量导入数据",
|
|
4895
|
+
step === "preview" && `已解析 ${parsedData?.rows.length ?? 0} 条数据,确认后开始导入`,
|
|
4896
|
+
step === "importing" && "正在导入数据...",
|
|
4897
|
+
step === "result" && "导入完成"
|
|
4898
|
+
] })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4899
|
+
className: "space-y-4",
|
|
4900
|
+
children: [
|
|
4901
|
+
step === "upload" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4902
|
+
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"}`,
|
|
4903
|
+
onDragOver: (e) => {
|
|
4904
|
+
e.preventDefault();
|
|
4905
|
+
setIsDragging(true);
|
|
4906
|
+
},
|
|
4907
|
+
onDragLeave: () => setIsDragging(false),
|
|
4908
|
+
onDrop: handleDrop,
|
|
4909
|
+
onClick: () => fileInputRef.current?.click(),
|
|
4910
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4911
|
+
className: "flex flex-col items-center gap-2",
|
|
4912
|
+
children: [
|
|
4913
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
4914
|
+
className: "h-10 w-10 text-muted-foreground",
|
|
4915
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
4916
|
+
fill: "none",
|
|
4917
|
+
viewBox: "0 0 24 24",
|
|
4918
|
+
strokeWidth: 1.5,
|
|
4919
|
+
stroke: "currentColor",
|
|
4920
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
4921
|
+
strokeLinecap: "round",
|
|
4922
|
+
strokeLinejoin: "round",
|
|
4923
|
+
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"
|
|
4924
|
+
})
|
|
4925
|
+
}),
|
|
4926
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4927
|
+
className: "text-sm text-muted-foreground",
|
|
4928
|
+
children: "拖拽文件到此处,或点击选择文件"
|
|
4929
|
+
}),
|
|
4930
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4931
|
+
className: "text-xs text-muted-foreground/60",
|
|
4932
|
+
children: "支持 CSV、JSON 格式"
|
|
4933
|
+
})
|
|
4934
|
+
]
|
|
4935
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4936
|
+
ref: fileInputRef,
|
|
4937
|
+
type: "file",
|
|
4938
|
+
accept: ".csv,.json",
|
|
4939
|
+
className: "hidden",
|
|
4940
|
+
onChange: handleFileInput
|
|
4941
|
+
})]
|
|
4942
|
+
}), columns.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
4943
|
+
variant: "outline",
|
|
4944
|
+
size: "sm",
|
|
4945
|
+
className: "w-full",
|
|
4946
|
+
onClick: handleDownloadTemplate,
|
|
4947
|
+
children: "下载 CSV 模板"
|
|
4948
|
+
})] }),
|
|
4949
|
+
step === "preview" && parsedData && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
4950
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4951
|
+
className: "rounded-md border",
|
|
4952
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4953
|
+
className: "max-h-[300px] overflow-auto",
|
|
4954
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
4955
|
+
className: "w-full text-sm",
|
|
4956
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", {
|
|
4957
|
+
className: "sticky top-0 bg-muted",
|
|
4958
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
4959
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
4960
|
+
className: "px-3 py-2 text-left font-medium text-muted-foreground",
|
|
4961
|
+
children: "#"
|
|
4962
|
+
}),
|
|
4963
|
+
parsedData.headers.slice(0, 6).map((h) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
4964
|
+
className: "px-3 py-2 text-left font-medium text-muted-foreground",
|
|
4965
|
+
children: h
|
|
4966
|
+
}, h)),
|
|
4967
|
+
parsedData.headers.length > 6 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("th", {
|
|
4968
|
+
className: "px-3 py-2 text-left font-medium text-muted-foreground",
|
|
4969
|
+
children: [
|
|
4970
|
+
"+",
|
|
4971
|
+
parsedData.headers.length - 6,
|
|
4972
|
+
" 列"
|
|
4973
|
+
]
|
|
4974
|
+
})
|
|
4975
|
+
] })
|
|
4976
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: parsedData.rows.slice(0, 10).map((row, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
|
|
4977
|
+
className: "border-t",
|
|
4978
|
+
children: [
|
|
4979
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
4980
|
+
className: "px-3 py-2 text-muted-foreground",
|
|
4981
|
+
children: i + 1
|
|
4982
|
+
}),
|
|
4983
|
+
parsedData.headers.slice(0, 6).map((h) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
4984
|
+
className: "px-3 py-2 max-w-[150px] truncate",
|
|
4985
|
+
children: String(row[h] ?? "")
|
|
4986
|
+
}, h)),
|
|
4987
|
+
parsedData.headers.length > 6 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
4988
|
+
className: "px-3 py-2 text-muted-foreground",
|
|
4989
|
+
children: "..."
|
|
4990
|
+
})
|
|
4991
|
+
]
|
|
4992
|
+
}, i)) })]
|
|
4993
|
+
})
|
|
4994
|
+
})
|
|
4995
|
+
}),
|
|
4996
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
4997
|
+
className: "text-sm text-muted-foreground",
|
|
4998
|
+
children: [
|
|
4999
|
+
"共 ",
|
|
5000
|
+
parsedData.rows.length,
|
|
5001
|
+
" 条数据",
|
|
5002
|
+
parsedData.rows.length > 10 && "(预览前 10 条)",
|
|
5003
|
+
",",
|
|
5004
|
+
parsedData.headers.length,
|
|
5005
|
+
" 个字段 ,格式: ",
|
|
5006
|
+
parsedData.format.toUpperCase()
|
|
5007
|
+
]
|
|
5008
|
+
}),
|
|
5009
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5010
|
+
className: "flex gap-2 justify-end",
|
|
5011
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
5012
|
+
variant: "outline",
|
|
5013
|
+
onClick: reset,
|
|
5014
|
+
children: "重新选择"
|
|
5015
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
5016
|
+
onClick: handleImport,
|
|
5017
|
+
children: "确认导入"
|
|
5018
|
+
})]
|
|
5019
|
+
})
|
|
5020
|
+
] }),
|
|
5021
|
+
step === "importing" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5022
|
+
className: "flex flex-col items-center gap-4 py-8",
|
|
5023
|
+
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", {
|
|
5024
|
+
className: "text-sm text-muted-foreground",
|
|
5025
|
+
children: [
|
|
5026
|
+
"正在导入 ",
|
|
5027
|
+
parsedData?.rows.length ?? 0,
|
|
5028
|
+
" 条数据..."
|
|
5029
|
+
]
|
|
5030
|
+
})]
|
|
5031
|
+
}),
|
|
5032
|
+
step === "result" && result && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5033
|
+
className: "space-y-3",
|
|
5034
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5035
|
+
className: "flex gap-4",
|
|
5036
|
+
children: [
|
|
5037
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5038
|
+
className: "flex-1 rounded-lg bg-green-50 dark:bg-green-950/30 p-3 text-center",
|
|
5039
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5040
|
+
className: "text-2xl font-semibold text-green-600",
|
|
5041
|
+
children: result.success
|
|
5042
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5043
|
+
className: "text-xs text-green-600/80",
|
|
5044
|
+
children: "新建"
|
|
5045
|
+
})]
|
|
5046
|
+
}),
|
|
5047
|
+
(result.updated ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5048
|
+
className: "flex-1 rounded-lg bg-blue-50 dark:bg-blue-950/30 p-3 text-center",
|
|
5049
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5050
|
+
className: "text-2xl font-semibold text-blue-600",
|
|
5051
|
+
children: result.updated
|
|
5052
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5053
|
+
className: "text-xs text-blue-600/80",
|
|
5054
|
+
children: "更新"
|
|
5055
|
+
})]
|
|
5056
|
+
}),
|
|
5057
|
+
result.skipped > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5058
|
+
className: "flex-1 rounded-lg bg-yellow-50 dark:bg-yellow-950/30 p-3 text-center",
|
|
5059
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5060
|
+
className: "text-2xl font-semibold text-yellow-600",
|
|
5061
|
+
children: result.skipped
|
|
5062
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5063
|
+
className: "text-xs text-yellow-600/80",
|
|
5064
|
+
children: "跳过"
|
|
5065
|
+
})]
|
|
5066
|
+
}),
|
|
5067
|
+
result.failed.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5068
|
+
className: "flex-1 rounded-lg bg-red-50 dark:bg-red-950/30 p-3 text-center",
|
|
5069
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5070
|
+
className: "text-2xl font-semibold text-red-600",
|
|
5071
|
+
children: result.failed.length
|
|
5072
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5073
|
+
className: "text-xs text-red-600/80",
|
|
5074
|
+
children: "验证失败"
|
|
5075
|
+
})]
|
|
5076
|
+
})
|
|
5077
|
+
]
|
|
5078
|
+
}), result.failed.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5079
|
+
className: "rounded-md border border-red-200 dark:border-red-900",
|
|
5080
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5081
|
+
className: "max-h-[200px] overflow-auto",
|
|
5082
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
5083
|
+
className: "w-full text-sm",
|
|
5084
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", {
|
|
5085
|
+
className: "sticky top-0 bg-red-50 dark:bg-red-950/30",
|
|
5086
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
5087
|
+
className: "px-3 py-2 text-left font-medium text-red-600",
|
|
5088
|
+
children: "行号"
|
|
5089
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
5090
|
+
className: "px-3 py-2 text-left font-medium text-red-600",
|
|
5091
|
+
children: "错误"
|
|
5092
|
+
})] })
|
|
5093
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: result.failed.map((f) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
|
|
5094
|
+
className: "border-t border-red-100 dark:border-red-900",
|
|
5095
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
5096
|
+
className: "px-3 py-2 text-red-600",
|
|
5097
|
+
children: f.row + 1
|
|
5098
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
5099
|
+
className: "px-3 py-2 text-red-600/80",
|
|
5100
|
+
children: f.errors.join("; ")
|
|
5101
|
+
})]
|
|
5102
|
+
}, f.row)) })]
|
|
5103
|
+
})
|
|
5104
|
+
})
|
|
5105
|
+
})]
|
|
5106
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5107
|
+
className: "flex gap-2 justify-end",
|
|
5108
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
5109
|
+
variant: "outline",
|
|
5110
|
+
onClick: () => handleOpenChange(false),
|
|
5111
|
+
children: "关闭"
|
|
5112
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
5113
|
+
variant: "outline",
|
|
5114
|
+
onClick: reset,
|
|
5115
|
+
children: "继续导入"
|
|
5116
|
+
})]
|
|
5117
|
+
})] }),
|
|
5118
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5119
|
+
className: "rounded-md bg-destructive/10 p-3 text-sm text-destructive",
|
|
5120
|
+
children: error
|
|
5121
|
+
})
|
|
5122
|
+
]
|
|
5123
|
+
})]
|
|
5124
|
+
})
|
|
5125
|
+
});
|
|
5126
|
+
}
|
|
5127
|
+
|
|
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
|
+
|
|
4621
5201
|
//#endregion
|
|
4622
5202
|
//#region src/components/auto-crud/auto-crud-table.tsx
|
|
4623
5203
|
/**
|
|
@@ -4635,9 +5215,14 @@ function buildTableOverrides(fields, legacyOverrides) {
|
|
|
4635
5215
|
}
|
|
4636
5216
|
/**
|
|
4637
5217
|
* 从统一配置生成表单 overrides
|
|
5218
|
+
* Critical #2: 支持 denyFields 隐藏敏感字段
|
|
4638
5219
|
*/
|
|
4639
|
-
function buildFormOverrides(fields, legacyOverrides) {
|
|
5220
|
+
function buildFormOverrides(fields, legacyOverrides, denyFields) {
|
|
4640
5221
|
const result = { ...legacyOverrides };
|
|
5222
|
+
if (denyFields) for (const field of denyFields) result[field] = {
|
|
5223
|
+
...result[field],
|
|
5224
|
+
"x-hidden": true
|
|
5225
|
+
};
|
|
4641
5226
|
if (fields) for (const [key, config] of Object.entries(fields)) result[key] = {
|
|
4642
5227
|
...result[key],
|
|
4643
5228
|
...config.label && { title: config.label },
|
|
@@ -4723,12 +5308,14 @@ function renderFieldValue(value, type) {
|
|
|
4723
5308
|
/**
|
|
4724
5309
|
* ViewModal 组件 - 详情查看弹窗
|
|
4725
5310
|
*/
|
|
4726
|
-
function ViewModal({ open, onOpenChange, variant, data, schema, fields: fieldConfig }) {
|
|
5311
|
+
function ViewModal({ open, onOpenChange, variant, data, schema, fields: fieldConfig, denyFields }) {
|
|
4727
5312
|
if (!data) return null;
|
|
4728
5313
|
const shape = schema.shape;
|
|
5314
|
+
const denySet = new Set(denyFields ?? []);
|
|
4729
5315
|
const content = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", {
|
|
4730
5316
|
className: "grid gap-4 py-4",
|
|
4731
5317
|
children: Object.entries(shape).filter(([key]) => {
|
|
5318
|
+
if (denySet.has(key)) return false;
|
|
4732
5319
|
return !(fieldConfig?.[key])?.hidden;
|
|
4733
5320
|
}).map(([key, fieldSchema]) => {
|
|
4734
5321
|
const parsed = parseZodField(fieldSchema);
|
|
@@ -4770,11 +5357,46 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
|
|
|
4770
5357
|
create: permissions?.can?.create ?? true,
|
|
4771
5358
|
update: permissions?.can?.update ?? true,
|
|
4772
5359
|
delete: permissions?.can?.delete ?? true,
|
|
4773
|
-
export: permissions?.can?.export ?? true
|
|
5360
|
+
export: permissions?.can?.export ?? true,
|
|
5361
|
+
import: permissions?.can?.import ?? true
|
|
4774
5362
|
};
|
|
5363
|
+
const denyFields = permissions?.deny;
|
|
5364
|
+
const canImport = can.import && !!resource.handlers.import;
|
|
5365
|
+
const canExport = can.export;
|
|
5366
|
+
const [importOpen, setImportOpen] = react.useState(false);
|
|
5367
|
+
const [exportOpen, setExportOpen] = react.useState(false);
|
|
5368
|
+
const [selectedCount, setSelectedCount] = react.useState(0);
|
|
5369
|
+
const getSelectedRowsRef = react.useRef(null);
|
|
5370
|
+
const importColumns = react.useMemo(() => {
|
|
5371
|
+
const shape = schema.shape;
|
|
5372
|
+
const denySet = new Set(denyFields ?? []);
|
|
5373
|
+
return Object.keys(shape).filter((key) => !denySet.has(key));
|
|
5374
|
+
}, [schema, denyFields]);
|
|
5375
|
+
const handleExport = react.useCallback(async (mode) => {
|
|
5376
|
+
const filename = title ?? "export";
|
|
5377
|
+
const excludeColumns = denyFields;
|
|
5378
|
+
if (mode === "selected") {
|
|
5379
|
+
const selectedRows = getSelectedRowsRef.current?.();
|
|
5380
|
+
if (!selectedRows || selectedRows.length === 0) return;
|
|
5381
|
+
exportAllToCSV(selectedRows, {
|
|
5382
|
+
filename,
|
|
5383
|
+
excludeColumns
|
|
5384
|
+
});
|
|
5385
|
+
} else {
|
|
5386
|
+
if (!resource.handlers.export) return;
|
|
5387
|
+
exportAllToCSV(await resource.handlers.export(), {
|
|
5388
|
+
filename,
|
|
5389
|
+
excludeColumns
|
|
5390
|
+
});
|
|
5391
|
+
}
|
|
5392
|
+
}, [
|
|
5393
|
+
resource.handlers.export,
|
|
5394
|
+
title,
|
|
5395
|
+
denyFields
|
|
5396
|
+
]);
|
|
4775
5397
|
const tableOverrides = buildTableOverrides(fields, tableConfig?.overrides);
|
|
4776
|
-
const formOverrides = buildFormOverrides(fields, formConfig?.overrides);
|
|
4777
|
-
const hiddenColumns = buildHiddenColumns(fields, tableConfig?.hidden,
|
|
5398
|
+
const formOverrides = buildFormOverrides(fields, formConfig?.overrides, denyFields);
|
|
5399
|
+
const hiddenColumns = buildHiddenColumns(fields, tableConfig?.hidden, denyFields);
|
|
4778
5400
|
const batchFields = buildBatchUpdateFields(schema, tableConfig?.batchFields, fields);
|
|
4779
5401
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4780
5402
|
className: "space-y-4",
|
|
@@ -4796,10 +5418,25 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
|
|
|
4796
5418
|
children: slots?.toolbarStart
|
|
4797
5419
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4798
5420
|
className: "flex items-center gap-2",
|
|
4799
|
-
children: [
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
5421
|
+
children: [
|
|
5422
|
+
slots?.toolbarEnd,
|
|
5423
|
+
canImport && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.Button, {
|
|
5424
|
+
variant: "outline",
|
|
5425
|
+
size: "sm",
|
|
5426
|
+
onClick: () => setImportOpen(true),
|
|
5427
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Download, { className: "mr-2 h-4 w-4" }), "导入"]
|
|
5428
|
+
}),
|
|
5429
|
+
canExport && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__pixpilot_shadcn.Button, {
|
|
5430
|
+
variant: "outline",
|
|
5431
|
+
size: "sm",
|
|
5432
|
+
onClick: () => setExportOpen(true),
|
|
5433
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Upload, { className: "mr-2 h-4 w-4" }), "导出"]
|
|
5434
|
+
}),
|
|
5435
|
+
can.create && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.Button, {
|
|
5436
|
+
onClick: resource.handlers.openCreate,
|
|
5437
|
+
children: "新建"
|
|
5438
|
+
})
|
|
5439
|
+
]
|
|
4803
5440
|
})]
|
|
4804
5441
|
}),
|
|
4805
5442
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutoTable, {
|
|
@@ -4818,8 +5455,10 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
|
|
|
4818
5455
|
onDeleteSelected: can.delete ? resource.handlers.deleteMany : void 0,
|
|
4819
5456
|
onUpdateSelected: can.update ? resource.handlers.updateMany : void 0,
|
|
4820
5457
|
batchUpdateFields: can.update ? batchFields : void 0,
|
|
4821
|
-
enableExport:
|
|
4822
|
-
initialSorting: tableConfig?.defaultSort
|
|
5458
|
+
enableExport: false,
|
|
5459
|
+
initialSorting: tableConfig?.defaultSort,
|
|
5460
|
+
onSelectedCountChange: setSelectedCount,
|
|
5461
|
+
getSelectedRows: getSelectedRowsRef
|
|
4823
5462
|
}),
|
|
4824
5463
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CrudFormModal, {
|
|
4825
5464
|
open: resource.modal.createOpen || resource.modal.editOpen,
|
|
@@ -4841,7 +5480,8 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
|
|
|
4841
5480
|
variant: resource.modal.variant,
|
|
4842
5481
|
data: resource.modal.selected,
|
|
4843
5482
|
schema,
|
|
4844
|
-
fields
|
|
5483
|
+
fields,
|
|
5484
|
+
denyFields
|
|
4845
5485
|
}),
|
|
4846
5486
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__pixpilot_shadcn.AlertDialog, {
|
|
4847
5487
|
open: resource.modal.deleteOpen,
|
|
@@ -4851,6 +5491,19 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
|
|
|
4851
5491
|
disabled: resource.mutations.isDeleting,
|
|
4852
5492
|
children: resource.mutations.isDeleting ? "删除中..." : "确认删除"
|
|
4853
5493
|
})] })] })
|
|
5494
|
+
}),
|
|
5495
|
+
canImport && resource.handlers.import && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImportDialog, {
|
|
5496
|
+
open: importOpen,
|
|
5497
|
+
onOpenChange: setImportOpen,
|
|
5498
|
+
onImport: resource.handlers.import,
|
|
5499
|
+
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
|
|
4854
5507
|
})
|
|
4855
5508
|
]
|
|
4856
5509
|
});
|
|
@@ -5527,15 +6180,44 @@ function modalReducer(state, action) {
|
|
|
5527
6180
|
/**
|
|
5528
6181
|
* Hook 主函数
|
|
5529
6182
|
*/
|
|
5530
|
-
function useAutoCrudResource({ router,
|
|
5531
|
-
const { idKey = "id", defaultVariant = "dialog", hooks, toast: toastAdapter } = options;
|
|
6183
|
+
function useAutoCrudResource({ router, schema, query: queryTransform, options = {} }) {
|
|
6184
|
+
const { idKey = "id", defaultVariant = "dialog", hooks, toast: toastAdapter, exportFetcher } = options;
|
|
5532
6185
|
const toast = (0, react.useMemo)(() => toastAdapter === false ? noopToastAdapter : toastAdapter ?? defaultToastAdapter, [toastAdapter]);
|
|
6186
|
+
const columns = (0, react.useMemo)(() => createTableSchema(schema), [schema]);
|
|
6187
|
+
const [urlParams] = useQueryStates({
|
|
6188
|
+
page: parseAsInteger.withDefault(1),
|
|
6189
|
+
perPage: parseAsInteger.withDefault(10),
|
|
6190
|
+
sort: getSortingStateParser().withDefault([]),
|
|
6191
|
+
joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and")
|
|
6192
|
+
}, { shallow: false });
|
|
6193
|
+
const [filters] = useReadableFilters(columns, { shallow: false });
|
|
6194
|
+
const queryInput = (0, react.useMemo)(() => {
|
|
6195
|
+
const autoParams = {
|
|
6196
|
+
page: urlParams.page,
|
|
6197
|
+
perPage: urlParams.perPage,
|
|
6198
|
+
sort: urlParams.sort,
|
|
6199
|
+
filters,
|
|
6200
|
+
joinOperator: urlParams.joinOperator
|
|
6201
|
+
};
|
|
6202
|
+
return queryTransform ? queryTransform(autoParams) : autoParams;
|
|
6203
|
+
}, [
|
|
6204
|
+
urlParams,
|
|
6205
|
+
filters,
|
|
6206
|
+
queryTransform
|
|
6207
|
+
]);
|
|
5533
6208
|
const listQuery = router.list.useQuery(queryInput, { placeholderData: keepPreviousData });
|
|
5534
6209
|
const createMutation = router.create.useMutation();
|
|
5535
6210
|
const updateMutation = router.update.useMutation();
|
|
5536
6211
|
const deleteMutation = router.delete.useMutation();
|
|
5537
6212
|
const deleteManyMutation = router.deleteMany?.useMutation();
|
|
5538
6213
|
const updateManyMutation = router.updateMany?.useMutation();
|
|
6214
|
+
const importMutation = router.import?.useMutation();
|
|
6215
|
+
const exportInput = (0, react.useMemo)(() => {
|
|
6216
|
+
if (!queryInput) return {};
|
|
6217
|
+
const { page: _p, perPage: _pp,...rest } = queryInput;
|
|
6218
|
+
return rest;
|
|
6219
|
+
}, [queryInput]);
|
|
6220
|
+
const exportQuery = router.export?.useQuery(exportInput, { enabled: false });
|
|
5539
6221
|
const [modal, dispatch] = (0, react.useReducer)(modalReducer, {
|
|
5540
6222
|
createOpen: false,
|
|
5541
6223
|
editOpen: false,
|
|
@@ -5747,6 +6429,33 @@ function useAutoCrudResource({ router, queryInput, options = {} }) {
|
|
|
5747
6429
|
payload: row
|
|
5748
6430
|
});
|
|
5749
6431
|
}, []);
|
|
6432
|
+
const handleImport = (0, react.useCallback)(async (rows) => {
|
|
6433
|
+
if (!importMutation) throw new Error("Import not supported: router has no import procedure");
|
|
6434
|
+
const coercedRows = coerceRowValues(rows, schema);
|
|
6435
|
+
const result = await importMutation.mutateAsync({
|
|
6436
|
+
rows: coercedRows,
|
|
6437
|
+
onConflict: "upsert"
|
|
6438
|
+
});
|
|
6439
|
+
listQuery.refetch();
|
|
6440
|
+
return result;
|
|
6441
|
+
}, [
|
|
6442
|
+
importMutation,
|
|
6443
|
+
listQuery,
|
|
6444
|
+
schema
|
|
6445
|
+
]);
|
|
6446
|
+
const handleExport = (0, react.useCallback)(async () => {
|
|
6447
|
+
if (exportFetcher) {
|
|
6448
|
+
const { page: _p, perPage: _pp,...exportParams } = queryInput ?? {};
|
|
6449
|
+
return (await exportFetcher(exportParams))?.data ?? [];
|
|
6450
|
+
}
|
|
6451
|
+
if (exportQuery) return (await exportQuery.refetch())?.data?.data ?? [];
|
|
6452
|
+
throw new Error("Export not supported");
|
|
6453
|
+
}, [
|
|
6454
|
+
exportFetcher,
|
|
6455
|
+
exportQuery,
|
|
6456
|
+
queryInput
|
|
6457
|
+
]);
|
|
6458
|
+
const canExport = !!(exportFetcher || exportQuery);
|
|
5750
6459
|
return {
|
|
5751
6460
|
tableData: {
|
|
5752
6461
|
data: listQuery.data?.data ?? [],
|
|
@@ -5758,7 +6467,8 @@ function useAutoCrudResource({ router, queryInput, options = {} }) {
|
|
|
5758
6467
|
mutations: {
|
|
5759
6468
|
isCreating: createMutation.isPending,
|
|
5760
6469
|
isUpdating: updateMutation.isPending,
|
|
5761
|
-
isDeleting: deleteMutation.isPending
|
|
6470
|
+
isDeleting: deleteMutation.isPending,
|
|
6471
|
+
isImporting: importMutation?.isPending ?? false
|
|
5762
6472
|
},
|
|
5763
6473
|
handlers: {
|
|
5764
6474
|
openCreate,
|
|
@@ -5772,7 +6482,9 @@ function useAutoCrudResource({ router, queryInput, options = {} }) {
|
|
|
5772
6482
|
confirmDelete,
|
|
5773
6483
|
deleteMany,
|
|
5774
6484
|
updateMany,
|
|
5775
|
-
setVariant
|
|
6485
|
+
setVariant,
|
|
6486
|
+
import: importMutation ? handleImport : null,
|
|
6487
|
+
export: canExport ? handleExport : null
|
|
5776
6488
|
}
|
|
5777
6489
|
};
|
|
5778
6490
|
}
|
|
@@ -6094,8 +6806,11 @@ exports.DataTableFacetedFilter = DataTableFacetedFilter;
|
|
|
6094
6806
|
exports.DataTablePagination = DataTablePagination;
|
|
6095
6807
|
exports.DataTableToolbar = DataTableToolbar;
|
|
6096
6808
|
exports.DataTableViewOptions = DataTableViewOptions;
|
|
6809
|
+
exports.ExportDialog = ExportDialog;
|
|
6810
|
+
exports.ImportDialog = ImportDialog;
|
|
6097
6811
|
exports.SchemaAdapter = SchemaAdapter;
|
|
6098
6812
|
exports.cn = cn;
|
|
6813
|
+
exports.coerceRowValues = coerceRowValues;
|
|
6099
6814
|
exports.createActionsColumn = createActionsColumn;
|
|
6100
6815
|
exports.createEditFormSchema = createEditFormSchema;
|
|
6101
6816
|
exports.createFormSchema = createFormSchema;
|
|
@@ -6103,7 +6818,12 @@ exports.createMemoryDataSource = createMemoryDataSource;
|
|
|
6103
6818
|
exports.createSelectColumn = createSelectColumn;
|
|
6104
6819
|
exports.createTRPCDataSource = createTRPCDataSource;
|
|
6105
6820
|
exports.createTableSchema = createTableSchema;
|
|
6821
|
+
exports.dataToCSV = dataToCSV;
|
|
6822
|
+
exports.downloadCSVTemplate = downloadCSVTemplate;
|
|
6823
|
+
exports.exportAllToCSV = exportAllToCSV;
|
|
6824
|
+
exports.exportTableToCSV = exportTableToCSV;
|
|
6106
6825
|
exports.formatDate = formatDate;
|
|
6826
|
+
exports.generateCSVTemplate = generateCSVTemplate;
|
|
6107
6827
|
exports.getUrlParams = getUrlParams;
|
|
6108
6828
|
exports.humanize = humanize;
|
|
6109
6829
|
exports.noopToastAdapter = noopToastAdapter;
|
|
@@ -6111,6 +6831,9 @@ exports.parseAsArrayOf = parseAsArrayOf;
|
|
|
6111
6831
|
exports.parseAsInteger = parseAsInteger;
|
|
6112
6832
|
exports.parseAsString = parseAsString;
|
|
6113
6833
|
exports.parseAsStringEnum = parseAsStringEnum;
|
|
6834
|
+
exports.parseCSV = parseCSV;
|
|
6835
|
+
exports.parseImportFile = parseImportFile;
|
|
6836
|
+
exports.parseJSON = parseJSON;
|
|
6114
6837
|
exports.parseZodField = parseZodField;
|
|
6115
6838
|
exports.setSearchParams = setSearchParams;
|
|
6116
6839
|
exports.useAutoCrudResource = useAutoCrudResource;
|