@wordrhyme/auto-crud 1.1.2 → 1.1.4

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.js CHANGED
@@ -2,7 +2,7 @@ import * as React from "react";
2
2
  import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useReducer, useRef } from "react";
3
3
  import { ArrowDownUp, BadgeCheck, CalendarIcon, Check, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, ChevronsLeft, ChevronsRight, ChevronsUpDown, CommandIcon, Download, Ellipsis, EyeOff, FileDown, FileSpreadsheetIcon, GripVertical, ListFilter, ListFilterIcon, Loader2, PlusCircle, Settings2, Text, Trash2, Upload, X, XCircle } from "lucide-react";
4
4
  import { flexRender, getCoreRowModel, getFacetedMinMaxValues, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
5
- import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, Calendar, Checkbox, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, Input, Label, Popover, PopoverContent, PopoverTrigger, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Separator, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, Slider, Switch } from "@pixpilot/shadcn";
5
+ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, Calendar, Checkbox, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, Input, Label, Popover, PopoverContent, PopoverTrigger, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Separator, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, Slider, Switch, cn as cn$1 } from "@pixpilot/shadcn";
6
6
  import { clsx } from "clsx";
7
7
  import { twMerge } from "tailwind-merge";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -18,6 +18,7 @@ import { z } from "zod";
18
18
  import { createForm } from "@formily/core";
19
19
  import { Form, JsonSchemaField } from "@pixpilot/formily-shadcn";
20
20
  import { connect, mapProps } from "@formily/react";
21
+ import { Combobox } from "@pixpilot/shadcn-ui";
21
22
  import { toast } from "sonner";
22
23
 
23
24
  //#region src/lib/utils.ts
@@ -2663,8 +2664,17 @@ function DataTableViewOptions({ table, disabled,...props }) {
2663
2664
 
2664
2665
  //#endregion
2665
2666
  //#region src/components/auto-crud/auto-table-simple-filters.tsx
2666
- function AutoTableSimpleFilters({ table, shallow = false, filters: externalFilters, onFiltersChange }) {
2667
- const columns = React.useMemo(() => table.getAllColumns().filter((col) => col.getCanFilter()), [table]);
2667
+ function AutoTableSimpleFilters({ table, shallow = false, filters: externalFilters, onFiltersChange, leading }) {
2668
+ const columns = React.useMemo(() => table.getAllColumns().filter((col) => col.getCanFilter()).sort((a, b) => {
2669
+ const readOrder = (column) => {
2670
+ const metaIndex = column.columnDef.meta?.index;
2671
+ const columnIndex = column.columnDef.index;
2672
+ return typeof metaIndex === "number" ? metaIndex : typeof columnIndex === "number" ? columnIndex : 1e4;
2673
+ };
2674
+ const orderDiff = readOrder(a) - readOrder(b);
2675
+ if (orderDiff !== 0) return orderDiff;
2676
+ return a.getIndex() - b.getIndex();
2677
+ }), [table]);
2668
2678
  const queryStateOptions = table.options.meta?.queryStateOptions;
2669
2679
  const [queryFilters, setQueryFilters] = useReadableFilters(columns, {
2670
2680
  ...queryStateOptions,
@@ -2721,32 +2731,65 @@ function AutoTableSimpleFilters({ table, shallow = false, filters: externalFilte
2721
2731
  const [expanded, setExpanded] = React.useState(false);
2722
2732
  const [visibleCount, setVisibleCount] = React.useState(null);
2723
2733
  const containerRef = React.useRef(null);
2734
+ const hasLeading = leading != null;
2735
+ const filterMeasureKey = React.useMemo(() => filters.map((filter) => [
2736
+ filter.id,
2737
+ filter.operator,
2738
+ Array.isArray(filter.value) ? filter.value.join(",") : String(filter.value ?? "")
2739
+ ].join(":")).join("|"), [filters]);
2724
2740
  const calcVisibleCount = React.useCallback(() => {
2725
2741
  const el = containerRef.current;
2726
2742
  if (!el) return;
2727
- const items = el.querySelectorAll("[data-filter-item]");
2728
- if (items.length === 0) return;
2743
+ const items = Array.from(el.querySelectorAll("[data-filter-item]"));
2744
+ if (items.length === 0) {
2745
+ setVisibleCount(null);
2746
+ return;
2747
+ }
2729
2748
  const restoreList = [];
2730
- el.querySelectorAll(".hidden").forEach((node) => {
2731
- const htmlNode = node;
2732
- htmlNode.classList.remove("hidden");
2733
- restoreList.push(htmlNode);
2749
+ el.querySelectorAll("[data-filter-item].hidden, [data-filter-toggle].hidden").forEach((node) => {
2750
+ node.classList.remove("hidden");
2751
+ restoreList.push(node);
2734
2752
  });
2735
- el.style.flex = "1 1 0%";
2736
- el.offsetHeight;
2737
- const firstTop = items[0].offsetTop;
2753
+ const availableWidth = el.getBoundingClientRect().width;
2754
+ const previousFlex = el.style.flex;
2755
+ const previousWidth = el.style.width;
2756
+ el.style.flex = `0 0 ${availableWidth}px`;
2757
+ el.style.width = `${availableWidth}px`;
2758
+ el.getBoundingClientRect();
2759
+ const computedStyle = window.getComputedStyle(el);
2760
+ const gap = Number.parseFloat(computedStyle.columnGap) || Number.parseFloat(computedStyle.gap) || 0;
2761
+ const leadingEl = el.querySelector("[data-filter-leading]");
2762
+ const toggle = el.querySelector("[data-filter-toggle]");
2763
+ const reset = el.querySelector("[data-filter-reset]");
2764
+ const addWidth = (current, next) => {
2765
+ if (next <= 0) return current;
2766
+ if (current === 0) return next;
2767
+ return current + gap + next;
2768
+ };
2769
+ const leadingWidth = leadingEl?.offsetWidth ?? 0;
2770
+ const itemWidths = items.map((item) => item.offsetWidth);
2771
+ const toggleWidth = toggle?.offsetWidth ?? 0;
2772
+ const resetWidth = reset?.offsetWidth ?? 0;
2773
+ if (itemWidths.reduce((width, itemWidth) => addWidth(width, itemWidth), addWidth(leadingWidth, resetWidth)) <= availableWidth) {
2774
+ el.style.flex = previousFlex;
2775
+ el.style.width = previousWidth;
2776
+ restoreList.forEach((node) => node.classList.add("hidden"));
2777
+ setVisibleCount(null);
2778
+ return;
2779
+ }
2780
+ const trailingWidth = addWidth(addWidth(0, toggleWidth), resetWidth);
2781
+ let usedWidth = leadingWidth;
2738
2782
  let count = 0;
2739
- for (const item of Array.from(items)) {
2740
- if (item.offsetTop > firstTop) break;
2783
+ for (const itemWidth of itemWidths) {
2784
+ const nextWidth = addWidth(usedWidth, itemWidth);
2785
+ if (addWidth(nextWidth, trailingWidth) > availableWidth) break;
2786
+ usedWidth = nextWidth;
2741
2787
  count++;
2742
2788
  }
2743
- const btn = el.querySelector("[data-filter-toggle]");
2744
- if (btn && count < items.length && count > 0) {
2745
- if (btn.offsetTop > firstTop) count--;
2746
- }
2747
- el.style.flex = "";
2789
+ el.style.flex = previousFlex;
2790
+ el.style.width = previousWidth;
2748
2791
  restoreList.forEach((node) => node.classList.add("hidden"));
2749
- setVisibleCount(count >= items.length ? null : count);
2792
+ setVisibleCount(count);
2750
2793
  }, []);
2751
2794
  React.useLayoutEffect(() => {
2752
2795
  if (!mounted) return;
@@ -2754,6 +2797,9 @@ function AutoTableSimpleFilters({ table, shallow = false, filters: externalFilte
2754
2797
  }, [
2755
2798
  mounted,
2756
2799
  filterColumns.length,
2800
+ hasLeading,
2801
+ hasFilters,
2802
+ filterMeasureKey,
2757
2803
  calcVisibleCount
2758
2804
  ]);
2759
2805
  React.useEffect(() => {
@@ -2776,13 +2822,19 @@ function AutoTableSimpleFilters({ table, shallow = false, filters: externalFilte
2776
2822
  }, [
2777
2823
  mounted,
2778
2824
  filterColumns.length,
2825
+ hasLeading,
2779
2826
  calcVisibleCount
2780
2827
  ]);
2781
2828
  if (!mounted) return null;
2782
2829
  return /* @__PURE__ */ jsxs("div", {
2783
2830
  ref: containerRef,
2784
- className: "flex flex-wrap items-center gap-2 min-w-0",
2831
+ className: "flex flex-1 flex-wrap items-center gap-2 min-w-0",
2785
2832
  children: [
2833
+ leading != null ? /* @__PURE__ */ jsx("div", {
2834
+ className: "shrink-0",
2835
+ "data-filter-leading": true,
2836
+ children: leading
2837
+ }) : null,
2786
2838
  filterColumns.map((column, index) => {
2787
2839
  const meta = column.columnDef.meta;
2788
2840
  const value = getFilterValue(column.id);
@@ -2846,6 +2898,7 @@ function AutoTableSimpleFilters({ table, shallow = false, filters: externalFilte
2846
2898
  children: expanded ? /* @__PURE__ */ jsx(ChevronUp, { className: "size-4" }) : /* @__PURE__ */ jsx(ChevronDown, { className: "size-4" })
2847
2899
  }),
2848
2900
  hasFilters && /* @__PURE__ */ jsxs(Button, {
2901
+ "data-filter-reset": true,
2849
2902
  variant: "outline",
2850
2903
  size: "sm",
2851
2904
  className: "h-8 border-dashed shrink-0",
@@ -3915,7 +3968,7 @@ function createTableSchema(schema, options) {
3915
3968
  if (cached) return [...cached];
3916
3969
  }
3917
3970
  const shape = schema.shape;
3918
- const columns = [];
3971
+ const columnEntries = [];
3919
3972
  for (const [key, fieldSchema] of Object.entries(shape)) {
3920
3973
  if (exclude.includes(key)) continue;
3921
3974
  const override = overrides?.[key];
@@ -3936,20 +3989,31 @@ function createTableSchema(schema, options) {
3936
3989
  options: override?.meta?.options ?? autoOptions
3937
3990
  };
3938
3991
  const { meta: _ignoredMeta,...restOverride } = override ?? {};
3939
- columns.push({
3940
- id: key,
3941
- accessorKey: key,
3942
- header: ({ column }) => /* @__PURE__ */ jsx(DataTableColumnHeader, {
3943
- column,
3944
- label
3945
- }),
3946
- cell: ({ row }) => renderCell(row.getValue(key), parsed.type),
3947
- enableColumnFilter: true,
3948
- enableSorting: true,
3949
- meta,
3950
- ...restOverride
3992
+ const sourceIndex = columnEntries.length;
3993
+ const order = typeof restOverride.index === "number" ? restOverride.index : typeof meta.index === "number" ? meta.index : 1e4 + sourceIndex;
3994
+ columnEntries.push({
3995
+ order,
3996
+ sourceIndex,
3997
+ column: {
3998
+ id: key,
3999
+ accessorKey: key,
4000
+ header: ({ column }) => /* @__PURE__ */ jsx(DataTableColumnHeader, {
4001
+ column,
4002
+ label
4003
+ }),
4004
+ cell: ({ row }) => renderCell(row.getValue(key), parsed.type),
4005
+ enableColumnFilter: true,
4006
+ enableSorting: true,
4007
+ meta,
4008
+ ...restOverride
4009
+ }
3951
4010
  });
3952
4011
  }
4012
+ const columns = columnEntries.sort((a, b) => {
4013
+ const orderDiff = a.order - b.order;
4014
+ if (orderDiff !== 0) return orderDiff;
4015
+ return a.sourceIndex - b.sourceIndex;
4016
+ }).map((entry) => entry.column);
3953
4017
  if (canCache) baseTableSchemaCache.set(schema, columns);
3954
4018
  return columns;
3955
4019
  }
@@ -4621,10 +4685,19 @@ const filterModeConfig = {
4621
4685
  tooltip: "Linear-style command filters"
4622
4686
  }
4623
4687
  };
4624
- function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection = true, exclude, actions, pinnedColumns, filterMode = "simple", onDeleteSelected, onUpdateSelected, batchUpdateFields, actionBarExtra, actionBarActions, initialSorting, enableExport = true, showDefaultExport = true, onSelectedCountChange, getSelectedRows }) {
4688
+ function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection = true, exclude, actions, pinnedColumns, filterMode = "simple", search = false, onDeleteSelected, onUpdateSelected, batchUpdateFields, actionBarExtra, actionBarActions, initialSorting, enableExport = true, showDefaultExport = true, onSelectedCountChange, getSelectedRows }) {
4625
4689
  const modes = filterMode ? Array.isArray(filterMode) ? filterMode : [filterMode] : DEFAULT_MODES;
4626
4690
  const [currentMode, setCurrentMode] = useQueryState("filterMode", parseAsStringEnum(modes).withDefault(modes[0] ?? "simple"));
4627
4691
  const showToggle = modes.length > 1;
4692
+ const [searchValue, setSearchValue] = useUrlState("search", parseAsString.withDefault(""), {
4693
+ shallow: false,
4694
+ clearOnDefault: true
4695
+ });
4696
+ const [, setSearchPage] = useUrlState("page", parseAsInteger.withDefault(1), {
4697
+ shallow: false,
4698
+ clearOnDefault: true
4699
+ });
4700
+ const searchConfig = search && typeof search === "object" ? search : {};
4628
4701
  const { table, shallow, debounceMs, throttleMs } = useDataTable({
4629
4702
  data,
4630
4703
  columns: useMemo(() => {
@@ -4696,11 +4769,21 @@ function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection
4696
4769
  })
4697
4770
  })
4698
4771
  })] }) : null;
4772
+ const searchInput = search ? /* @__PURE__ */ jsx(Input, {
4773
+ value: searchValue,
4774
+ onChange: (event) => {
4775
+ setSearchValue(event.target.value);
4776
+ setSearchPage(1);
4777
+ },
4778
+ placeholder: searchConfig.placeholder ?? "搜索",
4779
+ className: "h-8 w-48 shrink-0"
4780
+ }) : null;
4699
4781
  const filtersContent = useMemo(() => {
4700
4782
  switch (currentMode) {
4701
4783
  case "simple": return /* @__PURE__ */ jsx(AutoTableSimpleFilters, {
4702
4784
  table,
4703
- shallow
4785
+ shallow,
4786
+ leading: searchInput
4704
4787
  });
4705
4788
  case "command": return /* @__PURE__ */ jsx(DataTableFilterMenu, {
4706
4789
  table,
@@ -4720,6 +4803,7 @@ function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection
4720
4803
  currentMode,
4721
4804
  table,
4722
4805
  shallow,
4806
+ searchInput,
4723
4807
  debounceMs,
4724
4808
  throttleMs
4725
4809
  ]);
@@ -4729,12 +4813,16 @@ function AutoTable({ schema, data, pageCount = 1, overrides, enableRowSelection
4729
4813
  /* @__PURE__ */ jsxs("div", {
4730
4814
  className: "flex w-full items-start justify-between gap-2 p-1",
4731
4815
  children: [/* @__PURE__ */ jsxs("div", {
4732
- className: "flex flex-1 items-start gap-2 min-h-[40px]",
4816
+ className: "flex min-h-[40px] min-w-0 flex-1 items-start gap-2",
4733
4817
  "data-filter-parent": true,
4734
- children: [currentMode !== "simple" && /* @__PURE__ */ jsx(DataTableSortList, {
4735
- table,
4736
- align: "start"
4737
- }), filtersContent]
4818
+ children: [
4819
+ currentMode !== "simple" && searchInput,
4820
+ currentMode !== "simple" && /* @__PURE__ */ jsx(DataTableSortList, {
4821
+ table,
4822
+ align: "start"
4823
+ }),
4824
+ filtersContent
4825
+ ]
4738
4826
  }), /* @__PURE__ */ jsxs("div", {
4739
4827
  className: "flex items-center gap-2",
4740
4828
  children: [
@@ -4923,10 +5011,44 @@ function createEditFormSchema(schema, options) {
4923
5011
 
4924
5012
  //#endregion
4925
5013
  //#region src/components/auto-crud/auto-form.tsx
5014
+ const COMBOBOX_LIST_CLASS = "[&_[cmdk-list]]:max-h-[360px]";
4926
5015
  const FormilySwitch = connect(Switch, mapProps({
4927
5016
  value: "checked",
4928
5017
  onInput: "onCheckedChange"
4929
5018
  }));
5019
+ const FormilyCombobox = connect(AutoCrudCombobox, mapProps({ dataSource: "options" }));
5020
+ function AutoCrudCombobox({ options = [], filter, className,...props }) {
5021
+ const resolvedFilter = useMemo(() => {
5022
+ if (typeof filter === "function") return filter;
5023
+ return createComboboxFilter(options);
5024
+ }, [filter, options]);
5025
+ return /* @__PURE__ */ jsx(Combobox, {
5026
+ ...props,
5027
+ options,
5028
+ filter: resolvedFilter,
5029
+ className: cn$1(COMBOBOX_LIST_CLASS, className)
5030
+ });
5031
+ }
5032
+ function createComboboxFilter(options) {
5033
+ const searchTextByValue = new Map(options.map((option) => [option.value, collectSearchText(option.value, option.label, option.searchText, option.keywords).toLocaleLowerCase()]));
5034
+ return (value, search, keywords) => {
5035
+ const query = search.trim().toLocaleLowerCase();
5036
+ if (!query) return 1;
5037
+ const searchText = collectSearchText(searchTextByValue.get(value), keywords).toLocaleLowerCase();
5038
+ if (searchText === query) return 2;
5039
+ return searchText.includes(query) ? 1 : 0;
5040
+ };
5041
+ }
5042
+ function collectSearchText(...parts) {
5043
+ const texts = [];
5044
+ for (const part of parts) {
5045
+ if (!part) continue;
5046
+ if (Array.isArray(part)) texts.push(...part.map(String));
5047
+ else if (typeof part === "object") texts.push(...Object.values(part).filter(Boolean).map(String));
5048
+ else texts.push(String(part));
5049
+ }
5050
+ return texts.join(" ");
5051
+ }
4930
5052
  function AutoFormInner({ schema: zodSchema, initialValues, onSubmit, overrides, mode = "create", loading = false, gridColumns = 1, labelAlign = "top", labelWidth, showSubmitButton = true }, ref) {
4931
5053
  const form = useMemo(() => createForm({ initialValues }), [JSON.stringify(initialValues)]);
4932
5054
  const formSchema = useMemo(() => createEditFormSchema(zodSchema, {
@@ -4951,10 +5073,16 @@ function AutoFormInner({ schema: zodSchema, initialValues, onSubmit, overrides,
4951
5073
  form,
4952
5074
  children: [/* @__PURE__ */ jsx(JsonSchemaField, {
4953
5075
  schema: formSchema,
4954
- components: { fields: { Switch: {
4955
- component: FormilySwitch,
4956
- decorator: "FormItem"
4957
- } } }
5076
+ components: { fields: {
5077
+ Combobox: {
5078
+ component: FormilyCombobox,
5079
+ decorator: "FormItem"
5080
+ },
5081
+ Switch: {
5082
+ component: FormilySwitch,
5083
+ decorator: "FormItem"
5084
+ }
5085
+ } }
4958
5086
  }), showSubmitButton && /* @__PURE__ */ jsx("div", {
4959
5087
  className: "flex justify-end gap-2 mt-6",
4960
5088
  children: /* @__PURE__ */ jsx(Button, {
@@ -6046,6 +6174,11 @@ function resolveActions(actionsOrFn, defaults, rowActionsLocale) {
6046
6174
  */
6047
6175
  function AutoCrudTable({ title, description, schema, resource, fields, table: tableConfig, form: formConfig, permissions, actions: actionItems, toolbarActions, locale: localeProp, onCreate }) {
6048
6176
  const locale = resolveLocale(localeProp);
6177
+ const resolvedSchema = resource.schema ?? schema;
6178
+ const resolvedFields = React.useMemo(() => ({
6179
+ ...fields,
6180
+ ...resource.fields
6181
+ }), [fields, resource.fields]);
6049
6182
  const can = {
6050
6183
  create: permissions?.can?.create ?? true,
6051
6184
  update: permissions?.can?.update ?? true,
@@ -6061,10 +6194,10 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
6061
6194
  const [exporting, setExporting] = React.useState(false);
6062
6195
  const getSelectedRowsRef = React.useRef(null);
6063
6196
  const importColumns = React.useMemo(() => {
6064
- const shape = schema.shape;
6197
+ const shape = resolvedSchema.shape;
6065
6198
  const denySet = new Set(denyFields ?? []);
6066
6199
  return Object.keys(shape).filter((key) => !denySet.has(key));
6067
- }, [schema, denyFields]);
6200
+ }, [resolvedSchema, denyFields]);
6068
6201
  const handleExport = React.useCallback(async (mode) => {
6069
6202
  const filename = title ?? "export";
6070
6203
  const excludeColumns = denyFields;
@@ -6096,21 +6229,34 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
6096
6229
  setExporting(false);
6097
6230
  }
6098
6231
  }, [selectedCount, handleExport]);
6099
- const tableOverrides = React.useMemo(() => buildTableOverrides(fields, tableConfig?.overrides), [fields, tableConfig?.overrides]);
6100
- const formOverrides = React.useMemo(() => buildFormOverrides(fields, formConfig?.overrides, denyFields), [
6101
- fields,
6232
+ const tableOverrides = React.useMemo(() => buildTableOverrides(resolvedFields, tableConfig?.overrides), [resolvedFields, tableConfig?.overrides]);
6233
+ const formOverrides = React.useMemo(() => buildFormOverrides(resolvedFields, formConfig?.overrides, denyFields), [
6234
+ resolvedFields,
6102
6235
  formConfig?.overrides,
6103
6236
  denyFields
6104
6237
  ]);
6105
- const hiddenColumns = React.useMemo(() => buildHiddenColumns(fields, tableConfig?.hidden, denyFields), [
6106
- fields,
6238
+ const hiddenColumns = React.useMemo(() => buildHiddenColumns(resolvedFields, tableConfig?.hidden, denyFields), [
6239
+ resolvedFields,
6107
6240
  tableConfig?.hidden,
6108
6241
  denyFields
6109
6242
  ]);
6110
- const batchFields = React.useMemo(() => buildBatchUpdateFields(schema, tableConfig?.batchFields, fields), [
6111
- schema,
6243
+ const searchConfig = React.useMemo(() => {
6244
+ const tableSearch = tableConfig?.search;
6245
+ if (tableSearch === false) return false;
6246
+ if (tableSearch && typeof tableSearch === "object") return tableSearch;
6247
+ return Object.values(resolvedFields).some((field) => field?.search === true) ? true : false;
6248
+ }, [resolvedFields, tableConfig?.search]);
6249
+ const batchFields = React.useMemo(() => buildBatchUpdateFields(resolvedSchema, tableConfig?.batchFields, resolvedFields), [
6250
+ resolvedSchema,
6112
6251
  tableConfig?.batchFields,
6113
- fields
6252
+ resolvedFields
6253
+ ]);
6254
+ const formInitialValues = React.useMemo(() => {
6255
+ return resource.modal.createOpen ? resource.modal.copySource ?? void 0 : resource.modal.selected ?? void 0;
6256
+ }, [
6257
+ resource.modal.createOpen,
6258
+ resource.modal.copySource,
6259
+ resource.modal.selected
6114
6260
  ]);
6115
6261
  return /* @__PURE__ */ jsxs("div", {
6116
6262
  className: "space-y-4",
@@ -6191,11 +6337,12 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
6191
6337
  }),
6192
6338
  /* @__PURE__ */ jsx(AutoTable, {
6193
6339
  data: resource.tableData.data,
6194
- schema,
6340
+ schema: resolvedSchema,
6195
6341
  pageCount: resource.tableData.pageCount,
6196
6342
  overrides: tableOverrides,
6197
6343
  exclude: hiddenColumns,
6198
6344
  filterMode: tableConfig?.filterModes,
6345
+ search: searchConfig,
6199
6346
  actions: resolveActions(actionItems, {
6200
6347
  openView: resource.handlers.openView,
6201
6348
  openEdit: can.update ? resource.handlers.openEdit : void 0,
@@ -6216,8 +6363,8 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
6216
6363
  open: resource.modal.createOpen || resource.modal.editOpen,
6217
6364
  onOpenChange: (open) => !open && resource.handlers.closeModals(),
6218
6365
  mode: resource.modal.createOpen ? "create" : "edit",
6219
- schema,
6220
- initialValues: resource.modal.createOpen ? resource.modal.copySource ?? void 0 : resource.modal.selected ?? void 0,
6366
+ schema: resolvedSchema,
6367
+ initialValues: formInitialValues,
6221
6368
  onSubmit: (values) => {
6222
6369
  if (resource.modal.createOpen) resource.handlers.submitCreate(values);
6223
6370
  else resource.handlers.submitUpdate(values);
@@ -6234,8 +6381,8 @@ function AutoCrudTable({ title, description, schema, resource, fields, table: ta
6234
6381
  onOpenChange: (open) => !open && resource.handlers.closeModals(),
6235
6382
  variant: resource.modal.variant,
6236
6383
  data: resource.modal.selected,
6237
- schema,
6238
- fields,
6384
+ schema: resolvedSchema,
6385
+ fields: resolvedFields,
6239
6386
  denyFields,
6240
6387
  locale
6241
6388
  }),
@@ -6954,6 +7101,53 @@ const noopToastAdapter = {
6954
7101
  info: () => {},
6955
7102
  warning: () => {}
6956
7103
  };
7104
+ function isRecord(value) {
7105
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7106
+ }
7107
+ function isJsonSchemaObject(value) {
7108
+ return isRecord(value) && isRecord(value.properties);
7109
+ }
7110
+ function readJsonSchemaType(property) {
7111
+ return Array.isArray(property.type) ? property.type.find((item) => item !== "null") : property.type;
7112
+ }
7113
+ function jsonPropertyToZod(property, required) {
7114
+ const enumValues = property.enum?.filter((value) => typeof value === "string");
7115
+ let schema;
7116
+ if (enumValues && enumValues.length > 0) schema = z.enum(enumValues);
7117
+ else switch (readJsonSchemaType(property)) {
7118
+ case "number":
7119
+ case "integer":
7120
+ schema = z.number();
7121
+ break;
7122
+ case "boolean":
7123
+ schema = z.boolean();
7124
+ break;
7125
+ case "array":
7126
+ schema = z.array(z.unknown());
7127
+ break;
7128
+ case "object":
7129
+ schema = z.record(z.string(), z.unknown());
7130
+ break;
7131
+ case "string":
7132
+ schema = property.format === "date" || property.format === "date-time" ? z.coerce.date() : z.string();
7133
+ break;
7134
+ default: schema = z.unknown();
7135
+ }
7136
+ return required ? schema : schema.optional();
7137
+ }
7138
+ function mergeMetadataSchema(baseSchema, metadataSchema) {
7139
+ if (!isJsonSchemaObject(metadataSchema)) return baseSchema;
7140
+ const required = new Set(Array.isArray(metadataSchema.required) ? metadataSchema.required.filter((field) => typeof field === "string") : []);
7141
+ const extensionShape = {};
7142
+ for (const [field, property] of Object.entries(metadataSchema.properties ?? {})) {
7143
+ if (field in baseSchema.shape) continue;
7144
+ extensionShape[field] = jsonPropertyToZod(property, required.has(field));
7145
+ }
7146
+ return Object.keys(extensionShape).length > 0 ? baseSchema.extend(extensionShape) : baseSchema;
7147
+ }
7148
+ function readMetadataFields(fields) {
7149
+ return isRecord(fields) ? fields : void 0;
7150
+ }
6957
7151
  /**
6958
7152
  * Modal 状态 Reducer
6959
7153
  */
@@ -7012,12 +7206,16 @@ function useAutoCrudResource({ router, schema, query: queryTransform, options =
7012
7206
  useEffect(() => {
7013
7207
  hooksRef.current = hooks;
7014
7208
  }, [hooks]);
7015
- const columns = useMemo(() => createTableSchema(schema), [schema]);
7209
+ const metadata = (router.meta?.useQuery(void 0, { staleTime: 3e4 }))?.data;
7210
+ const resolvedSchema = useMemo(() => mergeMetadataSchema(schema, metadata?.schema), [schema, metadata?.schema]);
7211
+ const metadataFields = useMemo(() => readMetadataFields(metadata?.fields), [metadata?.fields]);
7212
+ const columns = useMemo(() => createTableSchema(resolvedSchema), [resolvedSchema]);
7016
7213
  const [urlParams] = useQueryStates({
7017
7214
  page: parseAsInteger.withDefault(1),
7018
7215
  perPage: parseAsInteger.withDefault(10),
7019
7216
  sort: getSortingStateParser().withDefault([]),
7020
- joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and")
7217
+ joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"),
7218
+ search: parseAsString.withDefault("")
7021
7219
  }, { shallow: false });
7022
7220
  const [filters] = useReadableFilters(columns, { shallow: false });
7023
7221
  const queryInput = useMemo(() => {
@@ -7026,7 +7224,8 @@ function useAutoCrudResource({ router, schema, query: queryTransform, options =
7026
7224
  perPage: urlParams.perPage,
7027
7225
  sort: urlParams.sort,
7028
7226
  filters,
7029
- joinOperator: urlParams.joinOperator
7227
+ joinOperator: urlParams.joinOperator,
7228
+ ...urlParams.search.trim() ? { search: urlParams.search.trim() } : {}
7030
7229
  };
7031
7230
  return queryTransform ? queryTransform(autoParams) : autoParams;
7032
7231
  }, [
@@ -7038,6 +7237,7 @@ function useAutoCrudResource({ router, schema, query: queryTransform, options =
7038
7237
  placeholderData: keepPreviousData,
7039
7238
  staleTime: 0
7040
7239
  });
7240
+ const tableRows = listQuery.data?.data ?? [];
7041
7241
  const createMutation = router.create.useMutation();
7042
7242
  const updateMutation = router.update.useMutation();
7043
7243
  const deleteMutation = router.delete.useMutation();
@@ -7290,11 +7490,13 @@ function useAutoCrudResource({ router, schema, query: queryTransform, options =
7290
7490
  const canExport = !!(exportFetcher || exportQuery);
7291
7491
  return {
7292
7492
  tableData: {
7293
- data: listQuery.data?.data ?? [],
7493
+ data: tableRows,
7294
7494
  pageCount: listQuery.data?.pageCount ?? 0,
7295
7495
  isLoading: listQuery.isLoading,
7296
7496
  isFetching: listQuery.isFetching
7297
7497
  },
7498
+ schema: resolvedSchema,
7499
+ fields: metadataFields,
7298
7500
  modal,
7299
7501
  mutations: {
7300
7502
  isCreating: createMutation.isPending,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wordrhyme/auto-crud",
3
3
  "type": "module",
4
- "version": "1.1.2",
4
+ "version": "1.1.4",
5
5
  "description": "Schema-first CRUD components with auto-generated tables and forms",
6
6
  "author": "wordrhyme",
7
7
  "license": "MIT",
@@ -83,10 +83,10 @@
83
83
  "tsdown": "^0.15.12",
84
84
  "typescript": "^5.9.3",
85
85
  "@internal/prettier-config": "0.0.1",
86
- "@internal/tsconfig": "0.1.0",
87
86
  "@internal/eslint-config": "0.3.0",
88
- "@internal/vitest-config": "0.1.0",
89
- "@internal/tsdown-config": "0.1.0"
87
+ "@internal/tsconfig": "0.1.0",
88
+ "@internal/tsdown-config": "0.1.0",
89
+ "@internal/vitest-config": "0.1.0"
90
90
  },
91
91
  "prettier": "@internal/prettier-config",
92
92
  "scripts": {