@wordrhyme/auto-crud 1.8.2 → 1.9.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/README.md CHANGED
@@ -1651,6 +1651,14 @@ MIT © [wordrhyme](https://github.com/denvey/wordrhyme-components)
1651
1651
  - [Notion](https://notion.so) - 高级筛选器设计
1652
1652
  - [Linear](https://linear.app) - 命令面板设计
1653
1653
 
1654
+ ### Status badge colors
1655
+
1656
+ For `table.display: "badge"`, a scalar field's `table.options` entries may set `badgeTone` to `neutral`, `success`, `warning`, or `info`. This renders a tinted, outlined rounded label with a decorative dot. Labels remain readable without color; omitted tones retain the default badge. `display: "text"` is unaffected. Domain-specific state-to-tone mappings belong in the consuming plugin metadata.
1657
+
1658
+ Registered custom actions may set `before: "delete"` (or another builtin action type) to appear immediately before that action. If the builtin is absent, the existing `position` fallback applies. This only controls ordering and does not add or grant the target action.
1659
+
1660
+ Custom row components receive `MenuItem` in their action context. Use this supplied primitive instead of importing a menu item from another UI bundle: the menu root and item must share the same Radix runtime context. Prevent the select event's default dismissal when the component owns an open dialog, so the menu does not unmount the workflow.
1661
+
1654
1662
  ### Detail presentation
1655
1663
 
1656
1664
  Details automatically share field labels, `enum`/`dataSource` mappings and
@@ -1711,6 +1719,31 @@ is pinned to the already-published alpha.19 for reproducible ESM tests and type
1711
1719
  checks until alpha.20 is published; it is not the supported CommonJS runtime.
1712
1720
  The CommonJS export/identity regression belongs to the UI provider repository.
1713
1721
 
1722
+ ### Filter-only fields
1723
+
1724
+ Use `table: false` with an explicit `filter` to keep a field in the standard
1725
+ filter bar without rendering a data column. Shared `hidden`, permission `deny`,
1726
+ and explicit table exclusions still hide the filter. Pass the same filter
1727
+ configuration to the resource so URL values use the same variants/operators:
1728
+
1729
+ ```tsx
1730
+ const fields = {
1731
+ customerId: {
1732
+ label: 'Customer',
1733
+ table: false as const,
1734
+ form: false as const,
1735
+ filter: { variant: 'select' as const, dataSource: 'customers', index: 20 },
1736
+ },
1737
+ };
1738
+ const resource = useAutoCrudResource({ router, schema, fields });
1739
+ return <AutoCrudTable schema={schema} resource={resource} fields={fields} />;
1740
+ ```
1741
+
1742
+ The field must exist in the schema or resource metadata, and the server must
1743
+ resolve its filter. A data source loads options; it does not translate query
1744
+ conditions. Standard filters share URL state, Reset, and pagination. Dynamically
1745
+ registered schema fields hydrate existing URL values without resetting the page.
1746
+
1714
1747
  ### 按 ID 隐藏或替换自定义操作
1715
1748
 
1716
1749
  拥有方为 custom 操作声明稳定 `id` 后,扩展可以在相同 CRUD target 和操作区域
package/dist/index.cjs CHANGED
@@ -1012,6 +1012,11 @@ function useReadableFilters(columns, options = {}) {
1012
1012
  filtersRef.current = initial;
1013
1013
  return initial;
1014
1014
  });
1015
+ react.useEffect(() => {
1016
+ const next = parseReadableFilters(getUrlParams(), columnSnapshot);
1017
+ filtersRef.current = next;
1018
+ setFilters((previous) => JSON.stringify(previous) === JSON.stringify(next) ? previous : next);
1019
+ }, [columnSnapshot]);
1015
1020
  react.useEffect(() => {
1016
1021
  const handleUrlChange = () => {
1017
1022
  if (lastWrittenUrlRef.current !== null && window.location.search === lastWrittenUrlRef.current) {
@@ -4414,7 +4419,7 @@ const filterModeConfig = {
4414
4419
  tooltip: "Linear-style command filters"
4415
4420
  }
4416
4421
  };
4417
- function AutoTable({ schema, data, pageCount = 1, pagination, total, overrides, classNames, enableRowSelection = true, exclude, actions, pinnedColumns, filterMode, search = false, onDeleteSelected, onUpdateSelected, batchUpdateFields, actionBarExtra, actionBarActions, deleteConfirmation, initialSorting, enableExport = true, showDefaultExport = true, onSelectedCountChange, onSelectedRowsChange, getSelectedRows }) {
4422
+ function AutoTable({ schema, data, pageCount = 1, pagination, total, overrides, classNames, enableRowSelection = true, exclude, filterOnly, actions, pinnedColumns, filterMode, search = false, onDeleteSelected, onUpdateSelected, batchUpdateFields, actionBarExtra, actionBarActions, deleteConfirmation, initialSorting, enableExport = true, showDefaultExport = true, onSelectedCountChange, onSelectedRowsChange, getSelectedRows }) {
4418
4423
  const modes = filterMode ? Array.isArray(filterMode) ? filterMode : [filterMode] : DEFAULT_MODES;
4419
4424
  const [currentMode, setCurrentMode] = useQueryState("filterMode", parseAsStringEnum(modes).withDefault(modes[0] ?? "simple"));
4420
4425
  const showToggle = modes.length > 1;
@@ -4429,9 +4434,24 @@ function AutoTable({ schema, data, pageCount = 1, pagination, total, overrides,
4429
4434
  const searchConfig = search && typeof search === "object" ? search : {};
4430
4435
  const enableAdvancedFilter = true;
4431
4436
  const columns = (0, react.useMemo)(() => {
4437
+ const columnOverrides = Object.assign({}, overrides);
4438
+ for (const key of filterOnly ?? []) {
4439
+ const field = key;
4440
+ Object.assign(columnOverrides, { [field]: {
4441
+ ...columnOverrides[field],
4442
+ hidden: false
4443
+ } });
4444
+ }
4432
4445
  const dataColumns = createTableSchema(schema, {
4433
- overrides,
4446
+ overrides: columnOverrides,
4434
4447
  exclude
4448
+ }).map((column) => {
4449
+ const key = column.id ?? ("accessorKey" in column ? String(column.accessorKey) : "");
4450
+ return filterOnly?.includes(key) ? {
4451
+ ...column,
4452
+ enableHiding: false,
4453
+ enableSorting: false
4454
+ } : column;
4435
4455
  });
4436
4456
  const result = enableRowSelection ? [createSelectColumn(), ...dataColumns] : dataColumns;
4437
4457
  if (actions) result.push(createActionsColumn(actions));
@@ -4441,7 +4461,8 @@ function AutoTable({ schema, data, pageCount = 1, pagination, total, overrides,
4441
4461
  overrides,
4442
4462
  enableRowSelection,
4443
4463
  exclude,
4444
- actions
4464
+ actions,
4465
+ filterOnly
4445
4466
  ]);
4446
4467
  const stableInitialState = (0, react.useMemo)(() => ({
4447
4468
  sorting: initialSorting,
@@ -4465,6 +4486,16 @@ function AutoTable({ schema, data, pageCount = 1, pagination, total, overrides,
4465
4486
  shallow: false,
4466
4487
  clearOnDefault: true
4467
4488
  });
4489
+ (0, react.useEffect)(() => {
4490
+ if (!filterOnly?.length) return;
4491
+ table.setColumnVisibility((previous) => {
4492
+ if (filterOnly.every((key) => previous[key] === false)) return previous;
4493
+ return {
4494
+ ...previous,
4495
+ ...Object.fromEntries(filterOnly.map((key) => [key, false]))
4496
+ };
4497
+ });
4498
+ }, [table, filterOnly]);
4468
4499
  const rowSelection = table.getState().rowSelection;
4469
4500
  const columnFilters = table.getState().columnFilters;
4470
4501
  const selectedRows = (0, react.useMemo)(() => table.getFilteredSelectedRowModel().rows.map((row) => row.original), [
@@ -4905,6 +4936,30 @@ function normalizeHasMore(result) {
4905
4936
 
4906
4937
  //#endregion
4907
4938
  //#region src/lib/field-config.ts
4939
+ function mergeFieldPart(base, override) {
4940
+ if (override === void 0) return base;
4941
+ if (override === false || base === false || typeof base !== "object" || typeof override !== "object" || base === null || override === null || Array.isArray(base) || Array.isArray(override)) return override;
4942
+ return {
4943
+ ...base,
4944
+ ...override
4945
+ };
4946
+ }
4947
+ function mergeFieldConfig(base, override) {
4948
+ return {
4949
+ ...base,
4950
+ ...override,
4951
+ enum: override?.enum ?? base?.enum,
4952
+ dataSource: override?.dataSource ?? base?.dataSource,
4953
+ table: mergeFieldPart(base?.table, override?.table),
4954
+ filter: mergeFieldPart(base?.filter, override?.filter),
4955
+ form: mergeFieldPart(base?.form, override?.form)
4956
+ };
4957
+ }
4958
+ function mergeFields(base, override) {
4959
+ if (!base && !override) return void 0;
4960
+ const keys = new Set([...Object.keys(base ?? {}), ...Object.keys(override ?? {})]);
4961
+ return Object.fromEntries(Array.from(keys).map((key) => [key, mergeFieldConfig(base?.[key], override?.[key])]));
4962
+ }
4908
4963
  function normalizeFieldOptions$1(options) {
4909
4964
  if (!options || options.length === 0) return void 0;
4910
4965
  return options.map((option) => ({
@@ -6292,7 +6347,6 @@ function resolveActions$1(targetId, zone, ownerActions) {
6292
6347
  const ownerCustomIds = new Set(ownerActions.flatMap((action$1) => isCustomAction$1(action$1) && action$1.id ? [action$1.id] : []));
6293
6348
  const baseActions = ownerActions.filter((action$1) => !action$1.hidden && !isMaskedCustom(action$1)).map((action$1) => ({ ...isCustomAction$1(action$1) && action$1.id ? customOverrides.get(action$1.id)?.action ?? action$1 : action$1 }));
6294
6349
  const isReplacedCustom = ({ action: action$1, seq: seq$1 }) => isCustomAction$1(action$1) && Boolean(action$1.id && (ownerCustomIds.has(action$1.id) || customOverrides.get(action$1.id)?.seq !== seq$1));
6295
- if (registered.length === 0) return require_position.positionActions(baseActions).map(withoutRegistryMeta);
6296
6350
  const nextActions = [...baseActions];
6297
6351
  const extraActions = [];
6298
6352
  const groups = /* @__PURE__ */ new Map();
@@ -6319,7 +6373,13 @@ function resolveActions$1(targetId, zone, ownerActions) {
6319
6373
  const items = group.sort((left, right) => left.seq - right.seq).flatMap((entry) => {
6320
6374
  const { action: action$1 } = entry;
6321
6375
  if (action$1.hidden || isReplacedCustom(entry)) return [];
6322
- if (isCustomAction$1(action$1)) return [{ ...action$1 }];
6376
+ if (isCustomAction$1(action$1)) {
6377
+ if (action$1.before) {
6378
+ extraActions.push({ ...action$1 });
6379
+ return [];
6380
+ }
6381
+ return [{ ...action$1 }];
6382
+ }
6323
6383
  const builtin = nextActions.find((item) => item.type === action$1.type);
6324
6384
  return builtin ? [builtin] : [];
6325
6385
  });
@@ -6340,6 +6400,15 @@ function resolveActions$1(targetId, zone, ownerActions) {
6340
6400
  }
6341
6401
  } else extraActions.push(...items);
6342
6402
  }
6403
+ for (const action$1 of [...nextActions, ...extraActions]) {
6404
+ if (!isCustomAction$1(action$1) || !action$1.before) continue;
6405
+ const anchor = nextActions.find((item) => !isCustomAction$1(item) && item.type === action$1.before);
6406
+ if (anchor) action$1.position = {
6407
+ anchor: anchor.id ?? anchor.type,
6408
+ side: "before"
6409
+ };
6410
+ else if (typeof action$1.position === "object") action$1.position = "end";
6411
+ }
6343
6412
  return require_position.positionActions(nextActions, extraActions).map(withoutRegistryMeta);
6344
6413
  }
6345
6414
  const crudActions = {
@@ -6489,30 +6558,6 @@ function resolveToolbarActionsWithResolver(targetId, ownerActions, context) {
6489
6558
  const resolver = autoCrudToolbarResolver;
6490
6559
  return targetId !== void 0 && targetId.length > 0 && resolver !== null ? resolver(targetId, ownerActions, context) : ownerActions;
6491
6560
  }
6492
- function mergeFieldPart(base, override) {
6493
- if (override === void 0) return base;
6494
- if (override === false || base === false || typeof base !== "object" || typeof override !== "object" || base === null || override === null || Array.isArray(base) || Array.isArray(override)) return override;
6495
- return {
6496
- ...base,
6497
- ...override
6498
- };
6499
- }
6500
- function mergeFieldConfig(base, override) {
6501
- return {
6502
- ...base,
6503
- ...override,
6504
- enum: override?.enum ?? base?.enum,
6505
- dataSource: override?.dataSource ?? base?.dataSource,
6506
- table: mergeFieldPart(base?.table, override?.table),
6507
- filter: mergeFieldPart(base?.filter, override?.filter),
6508
- form: mergeFieldPart(base?.form, override?.form)
6509
- };
6510
- }
6511
- function mergeFields(base, override) {
6512
- if (!base && !override) return void 0;
6513
- const keys = new Set([...Object.keys(base ?? {}), ...Object.keys(override ?? {})]);
6514
- return Object.fromEntries(Array.from(keys).map((key) => [key, mergeFieldConfig(base?.[key], override?.[key])]));
6515
- }
6516
6561
  function applyResourceTablePresentation(fields, resourceFields) {
6517
6562
  if (!fields || !resourceFields) return fields;
6518
6563
  return Object.fromEntries(Object.entries(fields).map(([key, config]) => {
@@ -7414,6 +7459,12 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
7414
7459
  const [exporting, setExporting] = react.useState(false);
7415
7460
  const getSelectedRowsRef = react.useRef(null);
7416
7461
  const dynamicFilterOptions = useDynamicFilterOptions(resolvedFields);
7462
+ const filterOnly = react.useMemo(() => Object.entries(resolvedFields).filter(([key, config]) => config.table === false && config.filter && config.hidden !== true && !denyFields?.includes(key) && !tableConfig?.hidden?.includes(key) && tableConfig?.overrides?.[key]?.hidden !== true).map(([key]) => key), [
7463
+ resolvedFields,
7464
+ denyFields,
7465
+ tableConfig?.hidden,
7466
+ tableConfig?.overrides
7467
+ ]);
7417
7468
  const hiddenColumns = react.useMemo(() => buildHiddenColumns(resolvedFields, tableConfig?.hidden, denyFields, tableConfig?.overrides), [
7418
7469
  resolvedFields,
7419
7470
  tableConfig?.hidden,
@@ -7764,7 +7815,8 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
7764
7815
  ...resource.tableData.total !== void 0 ? { total: resource.tableData.total } : {},
7765
7816
  overrides: tableOverrides,
7766
7817
  classNames: tableConfig?.classNames,
7767
- exclude: hiddenColumns,
7818
+ exclude: hiddenColumns.filter((key) => !filterOnly.includes(key)),
7819
+ filterOnly,
7768
7820
  filterMode: tableConfig?.filterModes,
7769
7821
  search: searchConfig,
7770
7822
  actions: tableRowActions,
@@ -8597,7 +8649,7 @@ function modalReducer(state, action$1) {
8597
8649
  /**
8598
8650
  * Hook 主函数
8599
8651
  */
8600
- function useAutoCrudResource({ router, schema, query: queryTransform, options = {} }) {
8652
+ function useAutoCrudResource({ router, schema, query: queryTransform, fields, options = {} }) {
8601
8653
  const { idKey = "id", defaultVariant = "dialog", hooks, toast: toastAdapter, exportFetcher, defaultSort: defaultSortOption } = options;
8602
8654
  const toast = (0, react.useMemo)(() => toastAdapter === false ? noopToastAdapter : toastAdapter ?? defaultToastAdapter, [toastAdapter]);
8603
8655
  const hooksRef = (0, react.useRef)(hooks);
@@ -8609,7 +8661,14 @@ function useAutoCrudResource({ router, schema, query: queryTransform, options =
8609
8661
  const defaultSorting = (0, react.useMemo)(() => defaultSortOption === false ? [] : defaultSortOption ?? getDefaultSortingForSchema(resolvedSchema), [defaultSortOption, resolvedSchema]);
8610
8662
  const metadataFields = (0, react.useMemo)(() => readMetadataFields(metadata?.fields), [metadata?.fields]);
8611
8663
  const capabilities = metadata?.capabilities;
8612
- const columns = (0, react.useMemo)(() => createTableSchema(resolvedSchema), [resolvedSchema]);
8664
+ const columns = (0, react.useMemo)(() => createTableSchema(resolvedSchema, { overrides: Object.fromEntries(Object.entries(mergeFields(metadataFields, fields) ?? {}).flatMap(([key, field]) => {
8665
+ const filter = field.filter;
8666
+ return filter && typeof filter === "object" && filter.variant ? [[key, { meta: { variant: filter.variant } }]] : [];
8667
+ })) }), [
8668
+ resolvedSchema,
8669
+ fields,
8670
+ metadataFields
8671
+ ]);
8613
8672
  const [urlParams] = useQueryStates({
8614
8673
  page: parseAsInteger.withDefault(1),
8615
8674
  perPage: parseAsInteger.withDefault(10),
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as positionActions, i as matchesPosition, n as actionPositionSchema, r as compareOrder, t as ActionPosition } from "./position-DoBV7yB6.cjs";
2
- import * as react_jsx_runtime2 from "react/jsx-runtime";
2
+ import * as react_jsx_runtime4 from "react/jsx-runtime";
3
3
  import * as React$1 from "react";
4
4
  import * as _tanstack_react_table0 from "@tanstack/react-table";
5
5
  import { Column, ColumnDef, ColumnMeta, ColumnSort, Row, RowData, Table, TableOptions, TableState } from "@tanstack/react-table";
@@ -573,6 +573,8 @@ interface UseAutoCrudResourceParams<TSchema extends z.ZodObject<z.ZodRawShape>,
573
573
  router: CrudRouter;
574
574
  /** Zod schema */
575
575
  schema: TSchema;
576
+ /** Field filter configuration shared with AutoCrudTable. */
577
+ fields?: Fields;
576
578
  /**
577
579
  * 查询参数变换函数
578
580
  *
@@ -599,6 +601,7 @@ declare function useAutoCrudResource<TSchema extends z.ZodObject<z.ZodRawShape>,
599
601
  router,
600
602
  schema,
601
603
  query: queryTransform,
604
+ fields,
602
605
  options
603
606
  }: UseAutoCrudResourceParams<TSchema, TListItem>): UseAutoCrudResourceReturn<TSchema, TListItem>;
604
607
  //#endregion
@@ -735,7 +738,7 @@ declare function AutoTableActionBar<TData>({
735
738
  extraActions,
736
739
  actions,
737
740
  deleteConfirmation
738
- }: AutoTableActionBarProps<TData>): react_jsx_runtime2.JSX.Element;
741
+ }: AutoTableActionBarProps<TData>): react_jsx_runtime4.JSX.Element;
739
742
  //#endregion
740
743
  //#region src/lib/schema-bridge/types.d.ts
741
744
  /**
@@ -856,6 +859,7 @@ interface AutoTableProps<T extends z.ZodObject<z.ZodRawShape>> {
856
859
  classNames?: DataTableClassNames;
857
860
  enableRowSelection?: boolean;
858
861
  exclude?: (keyof z.infer<T>)[];
862
+ filterOnly?: string[];
859
863
  actions?: ActionsColumnConfig<z.infer<T>>;
860
864
  /** 自定义固定列配置 */
861
865
  pinnedColumns?: {
@@ -908,6 +912,7 @@ declare function AutoTable<T extends z.ZodObject<z.ZodRawShape>>({
908
912
  classNames,
909
913
  enableRowSelection,
910
914
  exclude,
915
+ filterOnly,
911
916
  actions,
912
917
  pinnedColumns,
913
918
  filterMode,
@@ -924,7 +929,7 @@ declare function AutoTable<T extends z.ZodObject<z.ZodRawShape>>({
924
929
  onSelectedCountChange,
925
930
  onSelectedRowsChange,
926
931
  getSelectedRows
927
- }: AutoTableProps<T>): react_jsx_runtime2.JSX.Element;
932
+ }: AutoTableProps<T>): react_jsx_runtime4.JSX.Element;
928
933
  //#endregion
929
934
  //#region src/lib/row-action-dialog.d.ts
930
935
  interface RowActionDialogProps {
@@ -1272,6 +1277,8 @@ type RowCustomActionItem<T> = ActionMeta & {
1272
1277
  onClick?: (row: T) => void;
1273
1278
  component?: ActionComponent<AutoCrudRowActionContext<T>>;
1274
1279
  position?: ActionPosition;
1280
+ /** Place a registered custom action before the named builtin, when present. */
1281
+ before?: RowBuiltinActionType;
1275
1282
  separator?: boolean;
1276
1283
  variant?: 'default' | 'destructive';
1277
1284
  };
@@ -1476,7 +1483,7 @@ declare function AutoCrudTable<TSchema extends z.ZodObject<z.ZodRawShape>>({
1476
1483
  toolbarActions,
1477
1484
  locale: localeProp,
1478
1485
  onCreate
1479
- }: AutoCrudTableProps<TSchema>): react_jsx_runtime2.JSX.Element;
1486
+ }: AutoCrudTableProps<TSchema>): react_jsx_runtime4.JSX.Element;
1480
1487
  //#endregion
1481
1488
  //#region src/components/auto-crud/auto-form.d.ts
1482
1489
  interface AutoFormProps<T extends z.ZodObject<z.ZodRawShape>> {
@@ -1515,7 +1522,7 @@ declare function AutoFormInner<T extends z.ZodObject<z.ZodRawShape>>({
1515
1522
  labelAlign,
1516
1523
  labelWidth,
1517
1524
  showSubmitButton
1518
- }: AutoFormProps<T>, ref: React.Ref<AutoFormRef>): react_jsx_runtime2.JSX.Element;
1525
+ }: AutoFormProps<T>, ref: React.Ref<AutoFormRef>): react_jsx_runtime4.JSX.Element;
1519
1526
  declare const AutoForm: <T extends z.ZodObject<z.ZodRawShape>>(props: AutoFormProps<T> & {
1520
1527
  ref?: React.Ref<AutoFormRef>;
1521
1528
  }) => ReturnType<typeof AutoFormInner>;
@@ -1534,7 +1541,7 @@ declare function AutoTableSimpleFilters<TData>({
1534
1541
  filters: externalFilters,
1535
1542
  onFiltersChange,
1536
1543
  leading
1537
- }: AutoTableSimpleFiltersProps<TData>): react_jsx_runtime2.JSX.Element | null;
1544
+ }: AutoTableSimpleFiltersProps<TData>): react_jsx_runtime4.JSX.Element | null;
1538
1545
  //#endregion
1539
1546
  //#region src/components/auto-crud/form-modal.d.ts
1540
1547
  type ModalVariant = 'dialog' | 'sheet';
@@ -1578,7 +1585,7 @@ declare function CrudFormModal<T extends z.ZodObject<z.ZodRawShape>>({
1578
1585
  labelAlign,
1579
1586
  labelWidth,
1580
1587
  className
1581
- }: CrudFormModalProps<T>): react_jsx_runtime2.JSX.Element;
1588
+ }: CrudFormModalProps<T>): react_jsx_runtime4.JSX.Element;
1582
1589
  //#endregion
1583
1590
  //#region src/components/auto-crud/import-dialog.d.ts
1584
1591
  interface ImportDialogProps {
@@ -1600,7 +1607,7 @@ declare function ImportDialog({
1600
1607
  columns,
1601
1608
  title,
1602
1609
  locale
1603
- }: ImportDialogProps): react_jsx_runtime2.JSX.Element;
1610
+ }: ImportDialogProps): react_jsx_runtime4.JSX.Element;
1604
1611
  //#endregion
1605
1612
  //#region src/components/auto-crud/export-dialog.d.ts
1606
1613
  type ExportMode = 'selected' | 'filtered';
@@ -1620,7 +1627,7 @@ declare function ExportDialog({
1620
1627
  selectedCount,
1621
1628
  onExport,
1622
1629
  canExportFiltered
1623
- }: ExportDialogProps): react_jsx_runtime2.JSX.Element;
1630
+ }: ExportDialogProps): react_jsx_runtime4.JSX.Element;
1624
1631
  //#endregion
1625
1632
  //#region src/components/data-table/data-table.d.ts
1626
1633
  interface DataTableProps<TData> extends React$1.ComponentProps<'div'> {
@@ -1637,7 +1644,7 @@ declare function DataTable<TData>({
1637
1644
  className,
1638
1645
  classNames,
1639
1646
  ...props
1640
- }: DataTableProps<TData>): react_jsx_runtime2.JSX.Element;
1647
+ }: DataTableProps<TData>): react_jsx_runtime4.JSX.Element;
1641
1648
  //#endregion
1642
1649
  //#region src/components/data-table/data-table-advanced-toolbar.d.ts
1643
1650
  interface DataTableAdvancedToolbarProps<TData> extends React$1.ComponentProps<'div'> {
@@ -1648,7 +1655,7 @@ declare function DataTableAdvancedToolbar<TData>({
1648
1655
  children,
1649
1656
  className,
1650
1657
  ...props
1651
- }: DataTableAdvancedToolbarProps<TData>): react_jsx_runtime2.JSX.Element;
1658
+ }: DataTableAdvancedToolbarProps<TData>): react_jsx_runtime4.JSX.Element;
1652
1659
  //#endregion
1653
1660
  //#region src/components/data-table/data-table-column-header.d.ts
1654
1661
  interface DataTableColumnHeaderProps<TData, TValue> extends React.ComponentProps<typeof DropdownMenuTrigger> {
@@ -1660,7 +1667,7 @@ declare function DataTableColumnHeader<TData, TValue>({
1660
1667
  label,
1661
1668
  className,
1662
1669
  ...props
1663
- }: DataTableColumnHeaderProps<TData, TValue>): react_jsx_runtime2.JSX.Element;
1670
+ }: DataTableColumnHeaderProps<TData, TValue>): react_jsx_runtime4.JSX.Element;
1664
1671
  //#endregion
1665
1672
  //#region src/components/data-table/data-table-faceted-filter.d.ts
1666
1673
  interface DataTableFacetedFilterProps<TData, TValue> {
@@ -1674,7 +1681,7 @@ declare function DataTableFacetedFilter<TData, TValue>({
1674
1681
  title,
1675
1682
  options,
1676
1683
  multiple
1677
- }: DataTableFacetedFilterProps<TData, TValue>): react_jsx_runtime2.JSX.Element;
1684
+ }: DataTableFacetedFilterProps<TData, TValue>): react_jsx_runtime4.JSX.Element;
1678
1685
  //#endregion
1679
1686
  //#region src/components/data-table/data-table-pagination.d.ts
1680
1687
  interface DataTablePaginationProps<TData> extends React.ComponentProps<'div'>, TablePaginationOptions {
@@ -1690,7 +1697,7 @@ declare function DataTablePagination<TData>({
1690
1697
  pageJumpLabel,
1691
1698
  className,
1692
1699
  ...props
1693
- }: DataTablePaginationProps<TData>): react_jsx_runtime2.JSX.Element;
1700
+ }: DataTablePaginationProps<TData>): react_jsx_runtime4.JSX.Element;
1694
1701
  //#endregion
1695
1702
  //#region src/components/data-table/data-table-toolbar.d.ts
1696
1703
  interface DataTableToolbarProps<TData> extends React$1.ComponentProps<'div'> {
@@ -1701,7 +1708,7 @@ declare function DataTableToolbar<TData>({
1701
1708
  children,
1702
1709
  className,
1703
1710
  ...props
1704
- }: DataTableToolbarProps<TData>): react_jsx_runtime2.JSX.Element;
1711
+ }: DataTableToolbarProps<TData>): react_jsx_runtime4.JSX.Element;
1705
1712
  //#endregion
1706
1713
  //#region src/components/data-table/data-table-view-options.d.ts
1707
1714
  interface DataTableViewOptionsProps<TData> extends React$1.ComponentProps<typeof PopoverContent> {
@@ -1712,7 +1719,7 @@ declare function DataTableViewOptions<TData>({
1712
1719
  table,
1713
1720
  disabled,
1714
1721
  ...props
1715
- }: DataTableViewOptionsProps<TData>): react_jsx_runtime2.JSX.Element;
1722
+ }: DataTableViewOptionsProps<TData>): react_jsx_runtime4.JSX.Element;
1716
1723
  //#endregion
1717
1724
  //#region src/lib/crud-actions.d.ts
1718
1725
  type CrudActionZone = 'toolbar' | 'row' | 'batch';
@@ -1722,6 +1729,8 @@ type CrudActionBase = {
1722
1729
  order?: number;
1723
1730
  hidden?: boolean;
1724
1731
  position?: ActionPosition;
1732
+ /** Insert a custom action before a builtin action type, if present. */
1733
+ before?: string;
1725
1734
  };
1726
1735
  type CrudActionEntry<TAction extends CrudActionBase = CrudActionBase> = {
1727
1736
  targetId: string;
package/dist/index.d.ts CHANGED
@@ -573,6 +573,8 @@ interface UseAutoCrudResourceParams<TSchema extends z.ZodObject<z.ZodRawShape>,
573
573
  router: CrudRouter;
574
574
  /** Zod schema */
575
575
  schema: TSchema;
576
+ /** Field filter configuration shared with AutoCrudTable. */
577
+ fields?: Fields;
576
578
  /**
577
579
  * 查询参数变换函数
578
580
  *
@@ -599,6 +601,7 @@ declare function useAutoCrudResource<TSchema extends z.ZodObject<z.ZodRawShape>,
599
601
  router,
600
602
  schema,
601
603
  query: queryTransform,
604
+ fields,
602
605
  options
603
606
  }: UseAutoCrudResourceParams<TSchema, TListItem>): UseAutoCrudResourceReturn<TSchema, TListItem>;
604
607
  //#endregion
@@ -856,6 +859,7 @@ interface AutoTableProps<T extends z.ZodObject<z.ZodRawShape>> {
856
859
  classNames?: DataTableClassNames;
857
860
  enableRowSelection?: boolean;
858
861
  exclude?: (keyof z.infer<T>)[];
862
+ filterOnly?: string[];
859
863
  actions?: ActionsColumnConfig<z.infer<T>>;
860
864
  /** 自定义固定列配置 */
861
865
  pinnedColumns?: {
@@ -908,6 +912,7 @@ declare function AutoTable<T extends z.ZodObject<z.ZodRawShape>>({
908
912
  classNames,
909
913
  enableRowSelection,
910
914
  exclude,
915
+ filterOnly,
911
916
  actions,
912
917
  pinnedColumns,
913
918
  filterMode,
@@ -1272,6 +1277,8 @@ type RowCustomActionItem<T> = ActionMeta & {
1272
1277
  onClick?: (row: T) => void;
1273
1278
  component?: ActionComponent<AutoCrudRowActionContext<T>>;
1274
1279
  position?: ActionPosition;
1280
+ /** Place a registered custom action before the named builtin, when present. */
1281
+ before?: RowBuiltinActionType;
1275
1282
  separator?: boolean;
1276
1283
  variant?: 'default' | 'destructive';
1277
1284
  };
@@ -1722,6 +1729,8 @@ type CrudActionBase = {
1722
1729
  order?: number;
1723
1730
  hidden?: boolean;
1724
1731
  position?: ActionPosition;
1732
+ /** Insert a custom action before a builtin action type, if present. */
1733
+ before?: string;
1725
1734
  };
1726
1735
  type CrudActionEntry<TAction extends CrudActionBase = CrudActionBase> = {
1727
1736
  targetId: string;
package/dist/index.js CHANGED
@@ -997,6 +997,11 @@ function useReadableFilters(columns, options = {}) {
997
997
  filtersRef.current = initial;
998
998
  return initial;
999
999
  });
1000
+ React.useEffect(() => {
1001
+ const next = parseReadableFilters(getUrlParams(), columnSnapshot);
1002
+ filtersRef.current = next;
1003
+ setFilters((previous) => JSON.stringify(previous) === JSON.stringify(next) ? previous : next);
1004
+ }, [columnSnapshot]);
1000
1005
  React.useEffect(() => {
1001
1006
  const handleUrlChange = () => {
1002
1007
  if (lastWrittenUrlRef.current !== null && window.location.search === lastWrittenUrlRef.current) {
@@ -4399,7 +4404,7 @@ const filterModeConfig = {
4399
4404
  tooltip: "Linear-style command filters"
4400
4405
  }
4401
4406
  };
4402
- function AutoTable({ schema, data, pageCount = 1, pagination, total, overrides, classNames, enableRowSelection = true, exclude, actions, pinnedColumns, filterMode, search = false, onDeleteSelected, onUpdateSelected, batchUpdateFields, actionBarExtra, actionBarActions, deleteConfirmation, initialSorting, enableExport = true, showDefaultExport = true, onSelectedCountChange, onSelectedRowsChange, getSelectedRows }) {
4407
+ function AutoTable({ schema, data, pageCount = 1, pagination, total, overrides, classNames, enableRowSelection = true, exclude, filterOnly, actions, pinnedColumns, filterMode, search = false, onDeleteSelected, onUpdateSelected, batchUpdateFields, actionBarExtra, actionBarActions, deleteConfirmation, initialSorting, enableExport = true, showDefaultExport = true, onSelectedCountChange, onSelectedRowsChange, getSelectedRows }) {
4403
4408
  const modes = filterMode ? Array.isArray(filterMode) ? filterMode : [filterMode] : DEFAULT_MODES;
4404
4409
  const [currentMode, setCurrentMode] = useQueryState("filterMode", parseAsStringEnum(modes).withDefault(modes[0] ?? "simple"));
4405
4410
  const showToggle = modes.length > 1;
@@ -4414,9 +4419,24 @@ function AutoTable({ schema, data, pageCount = 1, pagination, total, overrides,
4414
4419
  const searchConfig = search && typeof search === "object" ? search : {};
4415
4420
  const enableAdvancedFilter = true;
4416
4421
  const columns = useMemo(() => {
4422
+ const columnOverrides = Object.assign({}, overrides);
4423
+ for (const key of filterOnly ?? []) {
4424
+ const field = key;
4425
+ Object.assign(columnOverrides, { [field]: {
4426
+ ...columnOverrides[field],
4427
+ hidden: false
4428
+ } });
4429
+ }
4417
4430
  const dataColumns = createTableSchema(schema, {
4418
- overrides,
4431
+ overrides: columnOverrides,
4419
4432
  exclude
4433
+ }).map((column) => {
4434
+ const key = column.id ?? ("accessorKey" in column ? String(column.accessorKey) : "");
4435
+ return filterOnly?.includes(key) ? {
4436
+ ...column,
4437
+ enableHiding: false,
4438
+ enableSorting: false
4439
+ } : column;
4420
4440
  });
4421
4441
  const result = enableRowSelection ? [createSelectColumn(), ...dataColumns] : dataColumns;
4422
4442
  if (actions) result.push(createActionsColumn(actions));
@@ -4426,7 +4446,8 @@ function AutoTable({ schema, data, pageCount = 1, pagination, total, overrides,
4426
4446
  overrides,
4427
4447
  enableRowSelection,
4428
4448
  exclude,
4429
- actions
4449
+ actions,
4450
+ filterOnly
4430
4451
  ]);
4431
4452
  const stableInitialState = useMemo(() => ({
4432
4453
  sorting: initialSorting,
@@ -4450,6 +4471,16 @@ function AutoTable({ schema, data, pageCount = 1, pagination, total, overrides,
4450
4471
  shallow: false,
4451
4472
  clearOnDefault: true
4452
4473
  });
4474
+ useEffect(() => {
4475
+ if (!filterOnly?.length) return;
4476
+ table.setColumnVisibility((previous) => {
4477
+ if (filterOnly.every((key) => previous[key] === false)) return previous;
4478
+ return {
4479
+ ...previous,
4480
+ ...Object.fromEntries(filterOnly.map((key) => [key, false]))
4481
+ };
4482
+ });
4483
+ }, [table, filterOnly]);
4453
4484
  const rowSelection = table.getState().rowSelection;
4454
4485
  const columnFilters = table.getState().columnFilters;
4455
4486
  const selectedRows = useMemo(() => table.getFilteredSelectedRowModel().rows.map((row) => row.original), [
@@ -4890,6 +4921,30 @@ function normalizeHasMore(result) {
4890
4921
 
4891
4922
  //#endregion
4892
4923
  //#region src/lib/field-config.ts
4924
+ function mergeFieldPart(base, override) {
4925
+ if (override === void 0) return base;
4926
+ if (override === false || base === false || typeof base !== "object" || typeof override !== "object" || base === null || override === null || Array.isArray(base) || Array.isArray(override)) return override;
4927
+ return {
4928
+ ...base,
4929
+ ...override
4930
+ };
4931
+ }
4932
+ function mergeFieldConfig(base, override) {
4933
+ return {
4934
+ ...base,
4935
+ ...override,
4936
+ enum: override?.enum ?? base?.enum,
4937
+ dataSource: override?.dataSource ?? base?.dataSource,
4938
+ table: mergeFieldPart(base?.table, override?.table),
4939
+ filter: mergeFieldPart(base?.filter, override?.filter),
4940
+ form: mergeFieldPart(base?.form, override?.form)
4941
+ };
4942
+ }
4943
+ function mergeFields(base, override) {
4944
+ if (!base && !override) return void 0;
4945
+ const keys = new Set([...Object.keys(base ?? {}), ...Object.keys(override ?? {})]);
4946
+ return Object.fromEntries(Array.from(keys).map((key) => [key, mergeFieldConfig(base?.[key], override?.[key])]));
4947
+ }
4893
4948
  function normalizeFieldOptions$1(options) {
4894
4949
  if (!options || options.length === 0) return void 0;
4895
4950
  return options.map((option) => ({
@@ -6277,7 +6332,6 @@ function resolveActions$1(targetId, zone, ownerActions) {
6277
6332
  const ownerCustomIds = new Set(ownerActions.flatMap((action$1) => isCustomAction$1(action$1) && action$1.id ? [action$1.id] : []));
6278
6333
  const baseActions = ownerActions.filter((action$1) => !action$1.hidden && !isMaskedCustom(action$1)).map((action$1) => ({ ...isCustomAction$1(action$1) && action$1.id ? customOverrides.get(action$1.id)?.action ?? action$1 : action$1 }));
6279
6334
  const isReplacedCustom = ({ action: action$1, seq: seq$1 }) => isCustomAction$1(action$1) && Boolean(action$1.id && (ownerCustomIds.has(action$1.id) || customOverrides.get(action$1.id)?.seq !== seq$1));
6280
- if (registered.length === 0) return positionActions(baseActions).map(withoutRegistryMeta);
6281
6335
  const nextActions = [...baseActions];
6282
6336
  const extraActions = [];
6283
6337
  const groups = /* @__PURE__ */ new Map();
@@ -6304,7 +6358,13 @@ function resolveActions$1(targetId, zone, ownerActions) {
6304
6358
  const items = group.sort((left, right) => left.seq - right.seq).flatMap((entry) => {
6305
6359
  const { action: action$1 } = entry;
6306
6360
  if (action$1.hidden || isReplacedCustom(entry)) return [];
6307
- if (isCustomAction$1(action$1)) return [{ ...action$1 }];
6361
+ if (isCustomAction$1(action$1)) {
6362
+ if (action$1.before) {
6363
+ extraActions.push({ ...action$1 });
6364
+ return [];
6365
+ }
6366
+ return [{ ...action$1 }];
6367
+ }
6308
6368
  const builtin = nextActions.find((item) => item.type === action$1.type);
6309
6369
  return builtin ? [builtin] : [];
6310
6370
  });
@@ -6325,6 +6385,15 @@ function resolveActions$1(targetId, zone, ownerActions) {
6325
6385
  }
6326
6386
  } else extraActions.push(...items);
6327
6387
  }
6388
+ for (const action$1 of [...nextActions, ...extraActions]) {
6389
+ if (!isCustomAction$1(action$1) || !action$1.before) continue;
6390
+ const anchor = nextActions.find((item) => !isCustomAction$1(item) && item.type === action$1.before);
6391
+ if (anchor) action$1.position = {
6392
+ anchor: anchor.id ?? anchor.type,
6393
+ side: "before"
6394
+ };
6395
+ else if (typeof action$1.position === "object") action$1.position = "end";
6396
+ }
6328
6397
  return positionActions(nextActions, extraActions).map(withoutRegistryMeta);
6329
6398
  }
6330
6399
  const crudActions = {
@@ -6474,30 +6543,6 @@ function resolveToolbarActionsWithResolver(targetId, ownerActions, context) {
6474
6543
  const resolver = autoCrudToolbarResolver;
6475
6544
  return targetId !== void 0 && targetId.length > 0 && resolver !== null ? resolver(targetId, ownerActions, context) : ownerActions;
6476
6545
  }
6477
- function mergeFieldPart(base, override) {
6478
- if (override === void 0) return base;
6479
- if (override === false || base === false || typeof base !== "object" || typeof override !== "object" || base === null || override === null || Array.isArray(base) || Array.isArray(override)) return override;
6480
- return {
6481
- ...base,
6482
- ...override
6483
- };
6484
- }
6485
- function mergeFieldConfig(base, override) {
6486
- return {
6487
- ...base,
6488
- ...override,
6489
- enum: override?.enum ?? base?.enum,
6490
- dataSource: override?.dataSource ?? base?.dataSource,
6491
- table: mergeFieldPart(base?.table, override?.table),
6492
- filter: mergeFieldPart(base?.filter, override?.filter),
6493
- form: mergeFieldPart(base?.form, override?.form)
6494
- };
6495
- }
6496
- function mergeFields(base, override) {
6497
- if (!base && !override) return void 0;
6498
- const keys = new Set([...Object.keys(base ?? {}), ...Object.keys(override ?? {})]);
6499
- return Object.fromEntries(Array.from(keys).map((key) => [key, mergeFieldConfig(base?.[key], override?.[key])]));
6500
- }
6501
6546
  function applyResourceTablePresentation(fields, resourceFields) {
6502
6547
  if (!fields || !resourceFields) return fields;
6503
6548
  return Object.fromEntries(Object.entries(fields).map(([key, config]) => {
@@ -7399,6 +7444,12 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
7399
7444
  const [exporting, setExporting] = React.useState(false);
7400
7445
  const getSelectedRowsRef = React.useRef(null);
7401
7446
  const dynamicFilterOptions = useDynamicFilterOptions(resolvedFields);
7447
+ const filterOnly = React.useMemo(() => Object.entries(resolvedFields).filter(([key, config]) => config.table === false && config.filter && config.hidden !== true && !denyFields?.includes(key) && !tableConfig?.hidden?.includes(key) && tableConfig?.overrides?.[key]?.hidden !== true).map(([key]) => key), [
7448
+ resolvedFields,
7449
+ denyFields,
7450
+ tableConfig?.hidden,
7451
+ tableConfig?.overrides
7452
+ ]);
7402
7453
  const hiddenColumns = React.useMemo(() => buildHiddenColumns(resolvedFields, tableConfig?.hidden, denyFields, tableConfig?.overrides), [
7403
7454
  resolvedFields,
7404
7455
  tableConfig?.hidden,
@@ -7749,7 +7800,8 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
7749
7800
  ...resource.tableData.total !== void 0 ? { total: resource.tableData.total } : {},
7750
7801
  overrides: tableOverrides,
7751
7802
  classNames: tableConfig?.classNames,
7752
- exclude: hiddenColumns,
7803
+ exclude: hiddenColumns.filter((key) => !filterOnly.includes(key)),
7804
+ filterOnly,
7753
7805
  filterMode: tableConfig?.filterModes,
7754
7806
  search: searchConfig,
7755
7807
  actions: tableRowActions,
@@ -8582,7 +8634,7 @@ function modalReducer(state, action$1) {
8582
8634
  /**
8583
8635
  * Hook 主函数
8584
8636
  */
8585
- function useAutoCrudResource({ router, schema, query: queryTransform, options = {} }) {
8637
+ function useAutoCrudResource({ router, schema, query: queryTransform, fields, options = {} }) {
8586
8638
  const { idKey = "id", defaultVariant = "dialog", hooks, toast: toastAdapter, exportFetcher, defaultSort: defaultSortOption } = options;
8587
8639
  const toast$1 = useMemo(() => toastAdapter === false ? noopToastAdapter : toastAdapter ?? defaultToastAdapter, [toastAdapter]);
8588
8640
  const hooksRef = useRef(hooks);
@@ -8594,7 +8646,14 @@ function useAutoCrudResource({ router, schema, query: queryTransform, options =
8594
8646
  const defaultSorting = useMemo(() => defaultSortOption === false ? [] : defaultSortOption ?? getDefaultSortingForSchema(resolvedSchema), [defaultSortOption, resolvedSchema]);
8595
8647
  const metadataFields = useMemo(() => readMetadataFields(metadata?.fields), [metadata?.fields]);
8596
8648
  const capabilities = metadata?.capabilities;
8597
- const columns = useMemo(() => createTableSchema(resolvedSchema), [resolvedSchema]);
8649
+ const columns = useMemo(() => createTableSchema(resolvedSchema, { overrides: Object.fromEntries(Object.entries(mergeFields(metadataFields, fields) ?? {}).flatMap(([key, field]) => {
8650
+ const filter = field.filter;
8651
+ return filter && typeof filter === "object" && filter.variant ? [[key, { meta: { variant: filter.variant } }]] : [];
8652
+ })) }), [
8653
+ resolvedSchema,
8654
+ fields,
8655
+ metadataFields
8656
+ ]);
8598
8657
  const [urlParams] = useQueryStates({
8599
8658
  page: parseAsInteger.withDefault(1),
8600
8659
  perPage: parseAsInteger.withDefault(10),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wordrhyme/auto-crud",
3
3
  "type": "module",
4
- "version": "1.8.2",
4
+ "version": "1.9.0",
5
5
  "description": "Schema-first CRUD components with auto-generated tables and forms",
6
6
  "author": "wordrhyme",
7
7
  "license": "MIT",
@@ -71,8 +71,8 @@
71
71
  "tailwind-merge": "^3.5.0",
72
72
  "vaul": "^1.1.2",
73
73
  "@wordrhyme/shadcn": "2.0.1",
74
- "@wordrhyme/formily-shadcn": "2.1.1",
75
- "@wordrhyme/shadcn-ui": "2.1.1"
74
+ "@wordrhyme/shadcn-ui": "2.1.1",
75
+ "@wordrhyme/formily-shadcn": "2.1.1"
76
76
  },
77
77
  "devDependencies": {
78
78
  "@storybook/react": "^8.6.18",
@@ -93,11 +93,11 @@
93
93
  "tsdown": "^0.15.12",
94
94
  "typescript": "^5.9.3",
95
95
  "vitest": "^3.2.4",
96
- "@internal/tsconfig": "0.1.0",
97
96
  "@internal/prettier-config": "0.0.1",
98
- "@internal/eslint-config": "0.3.0",
99
97
  "@internal/tsdown-config": "0.1.0",
100
- "@internal/vitest-config": "0.1.0"
98
+ "@internal/tsconfig": "0.1.0",
99
+ "@internal/vitest-config": "0.1.0",
100
+ "@internal/eslint-config": "0.3.0"
101
101
  },
102
102
  "prettier": "@internal/prettier-config",
103
103
  "scripts": {