@wordrhyme/auto-crud 1.17.0 → 1.18.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
@@ -1939,7 +1939,7 @@ container resizing and translated text widths, without changing other tables.
1939
1939
 
1940
1940
  ## 保存视图与动态标签
1941
1941
 
1942
- 在 `QueryClientProvider` 内配置一次服务,所有带稳定 `id` 的 `AutoCrudTable` 自动显示视图菜单:
1942
+ 视图默认关闭。在 `QueryClientProvider` 内配置存储服务后,通过 `table={{ views: true }}` 为带稳定 `id` 的 `AutoCrudTable` 开启视图菜单:
1943
1943
 
1944
1944
  ```tsx
1945
1945
  import {
@@ -1959,6 +1959,7 @@ const viewStorage = createLocalViewStorage(`${organizationId}:${userId}`);
1959
1959
  >
1960
1960
  <AutoCrudTable
1961
1961
  id="crm.customers"
1962
+ table={{ views: true }}
1962
1963
  schema={schema}
1963
1964
  resource={resource}
1964
1965
  fields={{
@@ -1969,7 +1970,7 @@ const viewStorage = createLocalViewStorage(`${organizationId}:${userId}`);
1969
1970
  </QueryClientProvider>;
1970
1971
  ```
1971
1972
 
1972
- - 视图保存筛选条件、条件间的 AND/OR、搜索词和排序;不保存分页、选中行。支持新建、复制、重命名、删除、保存和放弃修改。`table={{ views: false }}` 可在某个表关闭。
1973
+ - 视图保存筛选条件、条件间的 AND/OR、搜索词和排序;不保存分页、选中行。支持新建、复制、重命名、删除、保存和放弃修改。省略 `views` 或传 `false` 时关闭,传 `true` 或配置对象时开启。独立 `AutoTable` 使用 `views={true}`,并提供稳定的 `resourceId`。全局 `viewStorage` 和本地展示偏好不会自动开启视图。之前依赖自动开启的列表需补充 `table={{ views: true }}`。
1973
1974
  - 视图支持下拉与 Tab 两种展示:下拉放在搜索框前,设置收在下拉菜单内;Tab 放在筛选栏上方,末尾仅提供设置图标,新建操作与下拉模式一致,放在设置菜单内。在设置菜单的“展示方式”中切换,当前视图及未保存修改会保留。有修改时显示状态点、“保存修改”和“还原”。无本地偏好时默认使用下拉;`table={{ views: { selector: 'tabs' } }}` 可设置初始展示。独立使用 `AutoTable` 时传 `views={{ selector: 'tabs' }}`。本地记忆优先于 `selector`,展示方式不写入已保存的查询。
1974
1975
  - `scope` 必须包含当前用户与租户;切换时重置组件状态。`createLocalViewStorage` 仅保存当前浏览器中的视图。换用后端时传入 `ViewStorage`,组件调用方式不变。
1975
1976
  - `TagService.list` 接收 `resourceId/field` 和可选的 `query/page/pageSize/signal`,统一返回 `{ options: TagOption[], hasMore: boolean }`,两个字段都必须提供。每个选项包含稳定 ID `value`、名称 `label` 和颜色 `color`。`resolve` 返回指定稳定 ID 对应的名称与颜色。用户新建标签只增加选项,不增加 schema 字段。
@@ -2063,6 +2064,31 @@ Storybook 的 **Auto CRUD / Saved views and tags / Tabs With Counts** 使用内
2063
2064
 
2064
2065
  Storybook 提供 **Line Tabs**(下方蓝色强调线)和 **Top Line Tabs**(上方强调线)示例,均带视图数量;可通过 Controls 切换样式和线条位置。已有本地展示偏好时,仍可在视图设置中切换到 Tab。
2065
2066
 
2067
+ ### 自动标签接入
2068
+
2069
+ 字段声明 `tags: true` 后,`AutoCrudTable` 自动展示标签、启用标签筛选,并生成行操作和批量操作的设置入口:
2070
+
2071
+ ```tsx
2072
+ const schema = z.object({
2073
+ id: z.string(),
2074
+ recordTags: z.array(z.string()).optional(),
2075
+ });
2076
+
2077
+ <AutoCrudTable
2078
+ id="crm.customers"
2079
+ schema={schema}
2080
+ resource={resource}
2081
+ permissions={permissions}
2082
+ fields={{ recordTags: { label: '标签', form: false, tags: true } }}
2083
+ />;
2084
+ ```
2085
+
2086
+ 应用级 `AutoCrudProvider` 提供 `tagService`,页面无需再维护 `TagAssignment`、弹窗状态或选中记录转换。资源 ID 复用表格 `id`,记录主键复用 `resource.idKey`(默认 `id`),保存后调用 `resource.handlers.refresh`。未填写的标签值按空数组处理。
2087
+
2088
+ 自动入口跟随 `permissions.can.update`,字段 `hidden: true` 或被 `permissions.deny` 禁止时不生成入口;服务未提供 `assign` 时只展示和筛选。已有行操作、批量操作和注册扩展仍会保留。自动操作的稳定 ID 为 `tags:<字段名>`,可通过现有操作注册机制调整。`locale.tagAssignment` 可覆盖入口与弹窗标题。
2089
+
2090
+ `tags: true` 默认补充 `filter.variant: 'tags'`,显式筛选配置优先,`filter: false` 仍可禁用筛选且保留标签展示和设置入口。省略或设置 `tags: false` 不生成自动入口,已有 `filter.variant: 'tags'` 和手动弹窗接入方式保持兼容。标签仍由现有服务读写,无需新增页面接口或业务表。
2091
+
2066
2092
  ### 自定义标签编辑入口
2067
2093
 
2068
2094
  `TagAssignment` 是受控弹窗,不包含按钮。打开后直接显示搜索和标签列表,已有标签默认选中,通过勾选/取消勾选编辑,无需选择添加或移除。已选标签的名称、颜色实时显示;清空和保存位于同一行,点击保存后才提交记录的标签关联。
package/dist/index.cjs CHANGED
@@ -1794,6 +1794,7 @@ function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown"
1794
1794
  "aria-hidden": "true",
1795
1795
  className: cn("pointer-events-none absolute inset-x-0 h-1 bg-primary", linePosition === "top" ? "top-0" : "bottom-0", tabs?.classNames?.indicator)
1796
1796
  }) : null;
1797
+ const triggerLabel = display === "tabs" ? "视图设置" : selected === null ? "切换视图" : void 0;
1797
1798
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [children(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1798
1799
  className: "flex min-w-0 flex-wrap items-center gap-2",
1799
1800
  children: [
@@ -1837,19 +1838,22 @@ function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown"
1837
1838
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Button, {
1838
1839
  type: "button",
1839
1840
  variant: display === "tabs" ? "ghost" : "outline",
1840
- size: display === "tabs" ? "icon-sm" : "sm",
1841
- className: "shrink-0",
1842
- "aria-label": display === "tabs" ? "视图设置" : void 0,
1843
- title: display === "tabs" ? "视图设置" : void 0,
1841
+ size: display === "tabs" || selected === null ? "icon-sm" : "sm",
1842
+ className: cn("relative shrink-0", display === "dropdown" && selected === null && counts && "w-auto px-2"),
1843
+ "aria-label": triggerLabel,
1844
+ title: triggerLabel,
1844
1845
  disabled: !fieldsReady || views.isPending || mutation.isPending,
1845
1846
  children: display === "tabs" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Settings, { className: "size-4" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1846
- selected?.name ?? "全部",
1847
+ selected ? selected.name : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Layers, {
1848
+ "aria-hidden": "true",
1849
+ className: "size-4"
1850
+ }),
1847
1851
  renderCount(selected ? `view:${views.data?.findIndex((view) => view.id === selected.id)}` : "all"),
1848
1852
  dirty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1849
1853
  "aria-label": "有未保存修改",
1850
- className: "size-1.5 rounded-full bg-current"
1854
+ className: cn("size-1.5 rounded-full bg-current", selected === null && "absolute right-1 top-1")
1851
1855
  }),
1852
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "size-4" })
1856
+ selected && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "size-4" })
1853
1857
  ] })
1854
1858
  })
1855
1859
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.DropdownMenuContent, {
@@ -2180,6 +2184,7 @@ const enUSTableControls = {
2180
2184
  }
2181
2185
  };
2182
2186
  const zhCN = {
2187
+ tagAssignment: (label) => `设置${label}`,
2183
2188
  batchSearch: zhCNBatchSearch,
2184
2189
  tableControls: {
2185
2190
  sort: {
@@ -2295,6 +2300,7 @@ const zhCN = {
2295
2300
  }
2296
2301
  };
2297
2302
  const enUS = {
2303
+ tagAssignment: (label) => `Set ${label}`,
2298
2304
  batchSearch: enUSBatchSearch,
2299
2305
  tableControls: enUSTableControls,
2300
2306
  pagination: enUSPagination,
@@ -7337,8 +7343,7 @@ const filterModeConfig = {
7337
7343
  function AutoTable({ resourceId, views, schema, data, getRowId, expandedRow, defaultExpanded = false, pageCount = 1, pagination, total, overrides, classNames, enableRowSelection = true, queryKeys, initialColumnVisibility, exclude, filterOnly, actions, pinnedColumns, filterMode, search = false, batchSearch, filterState, toolbarExtra, locale = enUSTableControls, onDeleteSelected, onUpdateSelected, batchUpdateFields, actionBarExtra, actionBarLocale, actionBarActions, deleteConfirmation, initialSorting, enableExport = true, showDefaultExport = true, onSelectedCountChange, onSelectedRowsChange, getSelectedRows, exportTableRef }) {
7338
7344
  const inheritedResourceId = (0, react.useContext)(CrudResourceContext);
7339
7345
  const activeResourceId = resourceId ?? inheritedResourceId;
7340
- const services = useAutoCrudConfig();
7341
- const viewsEnabled = views ?? (!!activeResourceId && !!services?.viewStorage);
7346
+ const viewsEnabled = views ?? false;
7342
7347
  if (viewsEnabled && !activeResourceId) throw new Error("Saved views require a stable resourceId");
7343
7348
  if (expandedRow && !getRowId) throw new Error("AutoTable requires getRowId when expandedRow is provided.");
7344
7349
  const enableExpansion = !!expandedRow;
@@ -8045,7 +8050,7 @@ function mergeFieldPart(base, override) {
8045
8050
  };
8046
8051
  }
8047
8052
  function mergeFieldConfig(base, override) {
8048
- return {
8053
+ const config = {
8049
8054
  ...base,
8050
8055
  ...override,
8051
8056
  enum: override?.enum ?? base?.enum,
@@ -8054,6 +8059,11 @@ function mergeFieldConfig(base, override) {
8054
8059
  filter: mergeFieldPart(base?.filter, override?.filter),
8055
8060
  form: mergeFieldPart(base?.form, override?.form)
8056
8061
  };
8062
+ if (config.tags && config.filter !== false) config.filter = {
8063
+ variant: "tags",
8064
+ ...config.filter
8065
+ };
8066
+ return config;
8057
8067
  }
8058
8068
  function mergeFields(base, override) {
8059
8069
  if (!base && !override) return void 0;
@@ -10126,7 +10136,7 @@ function resolveActions(actions, defaults, rowActionsLocale, context) {
10126
10136
  */
10127
10137
  function AutoCrudTable({ id, title, description, schema, resource, fields, table: tableConfig, form: formConfig, view: viewConfig, permissions, actions: actionItems, toolbar, toolbarActions, locale: localeProp, onCreate, onExport }) {
10128
10138
  const services = useAutoCrudConfig();
10129
- const viewsEnabled = tableConfig?.views ?? (!!id && !!services?.viewStorage);
10139
+ const viewsEnabled = tableConfig?.views ?? false;
10130
10140
  if (viewsEnabled && !id) throw new Error("Saved views require a stable AutoCrudTable id");
10131
10141
  const locale = react.useMemo(() => resolveLocale(localeProp), [localeProp]);
10132
10142
  const getPagination = react.useCallback(() => id ? tablePagination.get(id) : void 0, [id]);
@@ -10171,6 +10181,51 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
10171
10181
  const excludedColumns = react.useMemo(() => hiddenColumns.filter((key) => !filterOnly.includes(key)), [hiddenColumns, filterOnly]);
10172
10182
  const dynamicResolveOptions = useDynamicResolveOptions(resolvedFields, resource.tableData.data, hiddenColumns);
10173
10183
  const resourceIdKey = resource.idKey ?? "id";
10184
+ const tagFields = react.useMemo(() => Object.entries(resolvedFields).filter(([field, config]) => config.tags && !config.hidden && !denyFields?.includes(field)).map(([field, config]) => {
10185
+ const label = config.label ?? humanize(field);
10186
+ return {
10187
+ field,
10188
+ actionId: `tags:${field}`,
10189
+ label: locale.tagAssignment?.(label) ?? `${locale.rowActions.edit} ${label}`
10190
+ };
10191
+ }), [
10192
+ resolvedFields,
10193
+ denyFields,
10194
+ locale
10195
+ ]);
10196
+ if (tagFields.length && (!id || !services?.tagService)) throw new Error("Tag fields require a CRUD id and AutoCrudProvider.tagService");
10197
+ const [tagAssignment, setTagAssignment] = react.useState(null);
10198
+ const canAssignTags = can.update && Boolean(services?.tagService?.assign);
10199
+ const tagAssignmentVisible = canAssignTags && tagAssignment?.resourceId === id && tagFields.some(({ field }) => field === tagAssignment?.field);
10200
+ react.useEffect(() => {
10201
+ if (!tagAssignmentVisible) setTagAssignment(null);
10202
+ }, [tagAssignmentVisible]);
10203
+ const openTagAssignment = react.useCallback((field, dialogTitle, rows) => {
10204
+ if (!id || !canAssignTags || !rows.length) return;
10205
+ const entities = [];
10206
+ for (const row of rows) {
10207
+ const rowId = readRowId(row, resourceIdKey);
10208
+ const tagIds = row[field] ?? [];
10209
+ if (!rowId || !Array.isArray(tagIds) || !tagIds.every((tagId) => typeof tagId === "string")) {
10210
+ sonner.toast.error("Tag assignment requires a record id and an array of tag ids");
10211
+ return;
10212
+ }
10213
+ entities.push({
10214
+ id: rowId,
10215
+ tagIds: [...tagIds]
10216
+ });
10217
+ }
10218
+ setTagAssignment({
10219
+ resourceId: id,
10220
+ field,
10221
+ title: dialogTitle,
10222
+ entities
10223
+ });
10224
+ }, [
10225
+ id,
10226
+ canAssignTags,
10227
+ resourceIdKey
10228
+ ]);
10174
10229
  const actionRegistryVersion = useCrudActionsVersion();
10175
10230
  const unifiedActions = react.useMemo(() => {
10176
10231
  if (isUnifiedActionsConfig(actionItems)) return actionItems;
@@ -10180,8 +10235,28 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
10180
10235
  const ownerRowConfig = unifiedActions.row;
10181
10236
  const ownerBatchConfig = unifiedActions.batch ?? tableConfig?.batchActions;
10182
10237
  const ownerToolbarActions = react.useMemo(() => resolveOwnerToolbarActions(ownerToolbarConfig), [ownerToolbarConfig]);
10183
- const ownerRowActions = react.useMemo(() => resolveOwnerRowActions(ownerRowConfig), [ownerRowConfig]);
10184
- const ownerBatchActions = react.useMemo(() => resolveOwnerBatchActions(ownerBatchConfig), [ownerBatchConfig]);
10238
+ const ownerRowActions = react.useMemo(() => [...resolveOwnerRowActions(ownerRowConfig), ...canAssignTags ? tagFields.map(({ field, actionId, label }) => ({
10239
+ type: "custom",
10240
+ id: actionId,
10241
+ label,
10242
+ onClick: (row) => openTagAssignment(field, label, [row])
10243
+ })) : []], [
10244
+ ownerRowConfig,
10245
+ canAssignTags,
10246
+ tagFields,
10247
+ openTagAssignment
10248
+ ]);
10249
+ const ownerBatchActions = react.useMemo(() => [...resolveOwnerBatchActions(ownerBatchConfig), ...canAssignTags ? tagFields.map(({ field, actionId, label }) => ({
10250
+ type: "custom",
10251
+ id: actionId,
10252
+ label,
10253
+ onClick: (rows) => openTagAssignment(field, label, rows)
10254
+ })) : []], [
10255
+ ownerBatchConfig,
10256
+ canAssignTags,
10257
+ tagFields,
10258
+ openTagAssignment
10259
+ ]);
10185
10260
  const registryToolbarActions = react.useMemo(() => crudActions.resolve(id, "toolbar", ownerToolbarActions), [
10186
10261
  id,
10187
10262
  ownerToolbarActions,
@@ -10371,7 +10446,7 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
10371
10446
  }
10372
10447
  }
10373
10448
  for (const [field, config] of Object.entries(resolvedFields)) {
10374
- if (!config.filter || config.filter.variant !== "tags" || formattedOverrides[field]?.cell) continue;
10449
+ if (!config.tags && (!config.filter || config.filter.variant !== "tags") || formattedOverrides[field]?.cell) continue;
10375
10450
  formattedOverrides[field] = {
10376
10451
  ...formattedOverrides[field],
10377
10452
  enableSorting: formattedOverrides[field]?.enableSorting ?? false,
@@ -10621,6 +10696,14 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
10621
10696
  ...onExport ? { exportTableRef } : {}
10622
10697
  }),
10623
10698
  rowActionDialog,
10699
+ tagAssignmentVisible && tagAssignment && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TagAssignment, {
10700
+ ...tagAssignment,
10701
+ open: true,
10702
+ onOpenChange: (open) => {
10703
+ if (!open) setTagAssignment(null);
10704
+ },
10705
+ onSaved: resource.handlers.refresh
10706
+ }),
10624
10707
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CrudFormModal, {
10625
10708
  open: resource.modal.createOpen || resource.modal.editOpen,
10626
10709
  onOpenChange: (open) => !open && resource.handlers.closeModals(),
package/dist/index.d.cts CHANGED
@@ -465,6 +465,8 @@ interface TableActionBarLocale {
465
465
  clearSelection: string;
466
466
  }
467
467
  interface AutoCrudLocale {
468
+ /** Label shared by automatic tag assignment actions and their dialog. */
469
+ tagAssignment?: (fieldLabel: string) => string;
468
470
  batchSearch?: BatchSearchLocale;
469
471
  /** Sorting and column visibility controls. Optional for existing custom locales. */
470
472
  tableControls?: TableControlsLocale;
@@ -1508,7 +1510,7 @@ interface TableSearchConfig {
1508
1510
  interface AutoTableProps<T extends z.ZodObject<z.ZodRawShape>> extends Pick<DataTableProps<z.infer<T>>, 'expandedRow'> {
1509
1511
  /** Stable CRUD identity for saved views and tag services. */
1510
1512
  resourceId?: string;
1511
- /** Defaults to enabled when a global viewStorage is configured. */
1513
+ /** Disabled by default. Pass true or a config to enable; requires viewStorage and a resourceId. */
1512
1514
  views?: boolean | SavedViewsConfig;
1513
1515
  schema: T;
1514
1516
  data: z.infer<T>[];
@@ -1725,6 +1727,8 @@ type FieldTableDisplay = 'auto' | 'text' | 'badge' | 'date' | 'datetime';
1725
1727
  interface Field {
1726
1728
  /** 字段标签(表格和表单共用) */
1727
1729
  label?: string;
1730
+ /** Enable tag display, filtering and assignment actions using the table id and resource idKey. */
1731
+ tags?: boolean;
1728
1732
  /** Format default table/detail text and CSV values; undefined keeps the default. */
1729
1733
  format?: (value: unknown, context: {
1730
1734
  row: Record<string, unknown>;
@@ -1925,7 +1929,7 @@ interface AutoCrudTableProps<TSchema extends z.ZodObject<z.ZodRawShape>> {
1925
1929
  };
1926
1930
  /** 表格配置 */
1927
1931
  table?: {
1928
- /** Enable named query views using the global viewStorage. */
1932
+ /** Disabled by default. Pass true or a config to enable; requires viewStorage and a stable id. */
1929
1933
  views?: AutoTableProps<TSchema>['views'];
1930
1934
  /** Optional controls; overrides the application registration for this CRUD id. */
1931
1935
  pagination?: TablePaginationOptions;
package/dist/index.d.ts CHANGED
@@ -465,6 +465,8 @@ interface TableActionBarLocale {
465
465
  clearSelection: string;
466
466
  }
467
467
  interface AutoCrudLocale {
468
+ /** Label shared by automatic tag assignment actions and their dialog. */
469
+ tagAssignment?: (fieldLabel: string) => string;
468
470
  batchSearch?: BatchSearchLocale;
469
471
  /** Sorting and column visibility controls. Optional for existing custom locales. */
470
472
  tableControls?: TableControlsLocale;
@@ -1508,7 +1510,7 @@ interface TableSearchConfig {
1508
1510
  interface AutoTableProps<T extends z.ZodObject<z.ZodRawShape>> extends Pick<DataTableProps<z.infer<T>>, 'expandedRow'> {
1509
1511
  /** Stable CRUD identity for saved views and tag services. */
1510
1512
  resourceId?: string;
1511
- /** Defaults to enabled when a global viewStorage is configured. */
1513
+ /** Disabled by default. Pass true or a config to enable; requires viewStorage and a resourceId. */
1512
1514
  views?: boolean | SavedViewsConfig;
1513
1515
  schema: T;
1514
1516
  data: z.infer<T>[];
@@ -1725,6 +1727,8 @@ type FieldTableDisplay = 'auto' | 'text' | 'badge' | 'date' | 'datetime';
1725
1727
  interface Field {
1726
1728
  /** 字段标签(表格和表单共用) */
1727
1729
  label?: string;
1730
+ /** Enable tag display, filtering and assignment actions using the table id and resource idKey. */
1731
+ tags?: boolean;
1728
1732
  /** Format default table/detail text and CSV values; undefined keeps the default. */
1729
1733
  format?: (value: unknown, context: {
1730
1734
  row: Record<string, unknown>;
@@ -1925,7 +1929,7 @@ interface AutoCrudTableProps<TSchema extends z.ZodObject<z.ZodRawShape>> {
1925
1929
  };
1926
1930
  /** 表格配置 */
1927
1931
  table?: {
1928
- /** Enable named query views using the global viewStorage. */
1932
+ /** Disabled by default. Pass true or a config to enable; requires viewStorage and a stable id. */
1929
1933
  views?: AutoTableProps<TSchema>['views'];
1930
1934
  /** Optional controls; overrides the application registration for this CRUD id. */
1931
1935
  pagination?: TablePaginationOptions;
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { Fragment, createContext, forwardRef, useCallback, useContext, useEffect
4
4
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { keepPreviousData, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
6
6
  import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, Calendar, Checkbox, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Input, InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, Label, Popover, PopoverContent, PopoverTrigger, RadioGroup, RadioGroupItem, Select as Select$1, SelectContent, SelectItem, SelectTrigger, SelectValue, Separator, Slider, Switch, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea, ToggleGroup, ToggleGroupItem, cn as cn$1 } from "@wordrhyme/shadcn";
7
- import { AppWindow, ArrowDownUp, BadgeCheck, CalendarIcon, Check, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight, ChevronRightIcon, ChevronUp, ChevronsLeft, ChevronsRight, ChevronsRightIcon, ChevronsUpDown, CommandIcon, Copy, Download, Ellipsis, EyeOff, FileDown, FileSpreadsheetIcon, GripVertical, Info, List, ListFilter, ListFilterIcon, Loader2, PanelsTopLeft, Pencil, Plus, PlusCircle, RefreshCw, Search, Settings, Settings2, Text, Trash2, Upload, X, XCircle } from "lucide-react";
7
+ import { AppWindow, ArrowDownUp, BadgeCheck, CalendarIcon, Check, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight, ChevronRightIcon, ChevronUp, ChevronsLeft, ChevronsRight, ChevronsRightIcon, ChevronsUpDown, CommandIcon, Copy, Download, Ellipsis, EyeOff, FileDown, FileSpreadsheetIcon, GripVertical, Info, Layers, List, ListFilter, ListFilterIcon, Loader2, PanelsTopLeft, Pencil, Plus, PlusCircle, RefreshCw, Search, Settings, Settings2, Text, Trash2, Upload, X, XCircle } from "lucide-react";
8
8
  import { z } from "zod";
9
9
  import { clsx } from "clsx";
10
10
  import { twMerge } from "tailwind-merge";
@@ -1778,6 +1778,7 @@ function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown"
1778
1778
  "aria-hidden": "true",
1779
1779
  className: cn("pointer-events-none absolute inset-x-0 h-1 bg-primary", linePosition === "top" ? "top-0" : "bottom-0", tabs?.classNames?.indicator)
1780
1780
  }) : null;
1781
+ const triggerLabel = display === "tabs" ? "视图设置" : selected === null ? "切换视图" : void 0;
1781
1782
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [children(/* @__PURE__ */ jsxs("div", {
1782
1783
  className: "flex min-w-0 flex-wrap items-center gap-2",
1783
1784
  children: [
@@ -1821,19 +1822,22 @@ function ViewMenu({ resourceId, table, fieldsReady = true, selector = "dropdown"
1821
1822
  children: /* @__PURE__ */ jsx(Button, {
1822
1823
  type: "button",
1823
1824
  variant: display === "tabs" ? "ghost" : "outline",
1824
- size: display === "tabs" ? "icon-sm" : "sm",
1825
- className: "shrink-0",
1826
- "aria-label": display === "tabs" ? "视图设置" : void 0,
1827
- title: display === "tabs" ? "视图设置" : void 0,
1825
+ size: display === "tabs" || selected === null ? "icon-sm" : "sm",
1826
+ className: cn("relative shrink-0", display === "dropdown" && selected === null && counts && "w-auto px-2"),
1827
+ "aria-label": triggerLabel,
1828
+ title: triggerLabel,
1828
1829
  disabled: !fieldsReady || views.isPending || mutation.isPending,
1829
1830
  children: display === "tabs" ? /* @__PURE__ */ jsx(Settings, { className: "size-4" }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
1830
- selected?.name ?? "全部",
1831
+ selected ? selected.name : /* @__PURE__ */ jsx(Layers, {
1832
+ "aria-hidden": "true",
1833
+ className: "size-4"
1834
+ }),
1831
1835
  renderCount(selected ? `view:${views.data?.findIndex((view) => view.id === selected.id)}` : "all"),
1832
1836
  dirty && /* @__PURE__ */ jsx("span", {
1833
1837
  "aria-label": "有未保存修改",
1834
- className: "size-1.5 rounded-full bg-current"
1838
+ className: cn("size-1.5 rounded-full bg-current", selected === null && "absolute right-1 top-1")
1835
1839
  }),
1836
- /* @__PURE__ */ jsx(ChevronDown, { className: "size-4" })
1840
+ selected && /* @__PURE__ */ jsx(ChevronDown, { className: "size-4" })
1837
1841
  ] })
1838
1842
  })
1839
1843
  }), /* @__PURE__ */ jsxs(DropdownMenuContent, {
@@ -2164,6 +2168,7 @@ const enUSTableControls = {
2164
2168
  }
2165
2169
  };
2166
2170
  const zhCN = {
2171
+ tagAssignment: (label) => `设置${label}`,
2167
2172
  batchSearch: zhCNBatchSearch,
2168
2173
  tableControls: {
2169
2174
  sort: {
@@ -2279,6 +2284,7 @@ const zhCN = {
2279
2284
  }
2280
2285
  };
2281
2286
  const enUS = {
2287
+ tagAssignment: (label) => `Set ${label}`,
2282
2288
  batchSearch: enUSBatchSearch,
2283
2289
  tableControls: enUSTableControls,
2284
2290
  pagination: enUSPagination,
@@ -7321,8 +7327,7 @@ const filterModeConfig = {
7321
7327
  function AutoTable({ resourceId, views, schema, data, getRowId, expandedRow, defaultExpanded = false, pageCount = 1, pagination, total, overrides, classNames, enableRowSelection = true, queryKeys, initialColumnVisibility, exclude, filterOnly, actions, pinnedColumns, filterMode, search = false, batchSearch, filterState, toolbarExtra, locale = enUSTableControls, onDeleteSelected, onUpdateSelected, batchUpdateFields, actionBarExtra, actionBarLocale, actionBarActions, deleteConfirmation, initialSorting, enableExport = true, showDefaultExport = true, onSelectedCountChange, onSelectedRowsChange, getSelectedRows, exportTableRef }) {
7322
7328
  const inheritedResourceId = useContext(CrudResourceContext);
7323
7329
  const activeResourceId = resourceId ?? inheritedResourceId;
7324
- const services = useAutoCrudConfig();
7325
- const viewsEnabled = views ?? (!!activeResourceId && !!services?.viewStorage);
7330
+ const viewsEnabled = views ?? false;
7326
7331
  if (viewsEnabled && !activeResourceId) throw new Error("Saved views require a stable resourceId");
7327
7332
  if (expandedRow && !getRowId) throw new Error("AutoTable requires getRowId when expandedRow is provided.");
7328
7333
  const enableExpansion = !!expandedRow;
@@ -8029,7 +8034,7 @@ function mergeFieldPart(base, override) {
8029
8034
  };
8030
8035
  }
8031
8036
  function mergeFieldConfig(base, override) {
8032
- return {
8037
+ const config = {
8033
8038
  ...base,
8034
8039
  ...override,
8035
8040
  enum: override?.enum ?? base?.enum,
@@ -8038,6 +8043,11 @@ function mergeFieldConfig(base, override) {
8038
8043
  filter: mergeFieldPart(base?.filter, override?.filter),
8039
8044
  form: mergeFieldPart(base?.form, override?.form)
8040
8045
  };
8046
+ if (config.tags && config.filter !== false) config.filter = {
8047
+ variant: "tags",
8048
+ ...config.filter
8049
+ };
8050
+ return config;
8041
8051
  }
8042
8052
  function mergeFields(base, override) {
8043
8053
  if (!base && !override) return void 0;
@@ -10110,7 +10120,7 @@ function resolveActions(actions, defaults, rowActionsLocale, context) {
10110
10120
  */
10111
10121
  function AutoCrudTable({ id, title, description, schema, resource, fields, table: tableConfig, form: formConfig, view: viewConfig, permissions, actions: actionItems, toolbar, toolbarActions, locale: localeProp, onCreate, onExport }) {
10112
10122
  const services = useAutoCrudConfig();
10113
- const viewsEnabled = tableConfig?.views ?? (!!id && !!services?.viewStorage);
10123
+ const viewsEnabled = tableConfig?.views ?? false;
10114
10124
  if (viewsEnabled && !id) throw new Error("Saved views require a stable AutoCrudTable id");
10115
10125
  const locale = React.useMemo(() => resolveLocale(localeProp), [localeProp]);
10116
10126
  const getPagination = React.useCallback(() => id ? tablePagination.get(id) : void 0, [id]);
@@ -10155,6 +10165,51 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
10155
10165
  const excludedColumns = React.useMemo(() => hiddenColumns.filter((key) => !filterOnly.includes(key)), [hiddenColumns, filterOnly]);
10156
10166
  const dynamicResolveOptions = useDynamicResolveOptions(resolvedFields, resource.tableData.data, hiddenColumns);
10157
10167
  const resourceIdKey = resource.idKey ?? "id";
10168
+ const tagFields = React.useMemo(() => Object.entries(resolvedFields).filter(([field, config]) => config.tags && !config.hidden && !denyFields?.includes(field)).map(([field, config]) => {
10169
+ const label = config.label ?? humanize(field);
10170
+ return {
10171
+ field,
10172
+ actionId: `tags:${field}`,
10173
+ label: locale.tagAssignment?.(label) ?? `${locale.rowActions.edit} ${label}`
10174
+ };
10175
+ }), [
10176
+ resolvedFields,
10177
+ denyFields,
10178
+ locale
10179
+ ]);
10180
+ if (tagFields.length && (!id || !services?.tagService)) throw new Error("Tag fields require a CRUD id and AutoCrudProvider.tagService");
10181
+ const [tagAssignment, setTagAssignment] = React.useState(null);
10182
+ const canAssignTags = can.update && Boolean(services?.tagService?.assign);
10183
+ const tagAssignmentVisible = canAssignTags && tagAssignment?.resourceId === id && tagFields.some(({ field }) => field === tagAssignment?.field);
10184
+ React.useEffect(() => {
10185
+ if (!tagAssignmentVisible) setTagAssignment(null);
10186
+ }, [tagAssignmentVisible]);
10187
+ const openTagAssignment = React.useCallback((field, dialogTitle, rows) => {
10188
+ if (!id || !canAssignTags || !rows.length) return;
10189
+ const entities = [];
10190
+ for (const row of rows) {
10191
+ const rowId = readRowId(row, resourceIdKey);
10192
+ const tagIds = row[field] ?? [];
10193
+ if (!rowId || !Array.isArray(tagIds) || !tagIds.every((tagId) => typeof tagId === "string")) {
10194
+ toast.error("Tag assignment requires a record id and an array of tag ids");
10195
+ return;
10196
+ }
10197
+ entities.push({
10198
+ id: rowId,
10199
+ tagIds: [...tagIds]
10200
+ });
10201
+ }
10202
+ setTagAssignment({
10203
+ resourceId: id,
10204
+ field,
10205
+ title: dialogTitle,
10206
+ entities
10207
+ });
10208
+ }, [
10209
+ id,
10210
+ canAssignTags,
10211
+ resourceIdKey
10212
+ ]);
10158
10213
  const actionRegistryVersion = useCrudActionsVersion();
10159
10214
  const unifiedActions = React.useMemo(() => {
10160
10215
  if (isUnifiedActionsConfig(actionItems)) return actionItems;
@@ -10164,8 +10219,28 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
10164
10219
  const ownerRowConfig = unifiedActions.row;
10165
10220
  const ownerBatchConfig = unifiedActions.batch ?? tableConfig?.batchActions;
10166
10221
  const ownerToolbarActions = React.useMemo(() => resolveOwnerToolbarActions(ownerToolbarConfig), [ownerToolbarConfig]);
10167
- const ownerRowActions = React.useMemo(() => resolveOwnerRowActions(ownerRowConfig), [ownerRowConfig]);
10168
- const ownerBatchActions = React.useMemo(() => resolveOwnerBatchActions(ownerBatchConfig), [ownerBatchConfig]);
10222
+ const ownerRowActions = React.useMemo(() => [...resolveOwnerRowActions(ownerRowConfig), ...canAssignTags ? tagFields.map(({ field, actionId, label }) => ({
10223
+ type: "custom",
10224
+ id: actionId,
10225
+ label,
10226
+ onClick: (row) => openTagAssignment(field, label, [row])
10227
+ })) : []], [
10228
+ ownerRowConfig,
10229
+ canAssignTags,
10230
+ tagFields,
10231
+ openTagAssignment
10232
+ ]);
10233
+ const ownerBatchActions = React.useMemo(() => [...resolveOwnerBatchActions(ownerBatchConfig), ...canAssignTags ? tagFields.map(({ field, actionId, label }) => ({
10234
+ type: "custom",
10235
+ id: actionId,
10236
+ label,
10237
+ onClick: (rows) => openTagAssignment(field, label, rows)
10238
+ })) : []], [
10239
+ ownerBatchConfig,
10240
+ canAssignTags,
10241
+ tagFields,
10242
+ openTagAssignment
10243
+ ]);
10169
10244
  const registryToolbarActions = React.useMemo(() => crudActions.resolve(id, "toolbar", ownerToolbarActions), [
10170
10245
  id,
10171
10246
  ownerToolbarActions,
@@ -10355,7 +10430,7 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
10355
10430
  }
10356
10431
  }
10357
10432
  for (const [field, config] of Object.entries(resolvedFields)) {
10358
- if (!config.filter || config.filter.variant !== "tags" || formattedOverrides[field]?.cell) continue;
10433
+ if (!config.tags && (!config.filter || config.filter.variant !== "tags") || formattedOverrides[field]?.cell) continue;
10359
10434
  formattedOverrides[field] = {
10360
10435
  ...formattedOverrides[field],
10361
10436
  enableSorting: formattedOverrides[field]?.enableSorting ?? false,
@@ -10605,6 +10680,14 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
10605
10680
  ...onExport ? { exportTableRef } : {}
10606
10681
  }),
10607
10682
  rowActionDialog,
10683
+ tagAssignmentVisible && tagAssignment && /* @__PURE__ */ jsx(TagAssignment, {
10684
+ ...tagAssignment,
10685
+ open: true,
10686
+ onOpenChange: (open) => {
10687
+ if (!open) setTagAssignment(null);
10688
+ },
10689
+ onSaved: resource.handlers.refresh
10690
+ }),
10608
10691
  /* @__PURE__ */ jsx(CrudFormModal, {
10609
10692
  open: resource.modal.createOpen || resource.modal.editOpen,
10610
10693
  onOpenChange: (open) => !open && resource.handlers.closeModals(),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wordrhyme/auto-crud",
3
3
  "type": "module",
4
- "version": "1.17.0",
4
+ "version": "1.18.0",
5
5
  "description": "Schema-first CRUD components with auto-generated tables and forms",
6
6
  "author": "wordrhyme",
7
7
  "license": "MIT",
@@ -71,9 +71,9 @@
71
71
  "nanoid": "^5.1.6",
72
72
  "tailwind-merge": "^3.5.0",
73
73
  "vaul": "^1.1.2",
74
- "@wordrhyme/shadcn": "2.0.2",
74
+ "@wordrhyme/formily-shadcn": "2.1.3",
75
75
  "@wordrhyme/shadcn-ui": "2.1.3",
76
- "@wordrhyme/formily-shadcn": "2.1.3"
76
+ "@wordrhyme/shadcn": "2.0.2"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@storybook/react": "^8.6.18",
@@ -97,8 +97,8 @@
97
97
  "@internal/eslint-config": "0.3.0",
98
98
  "@internal/prettier-config": "0.0.1",
99
99
  "@internal/tsconfig": "0.1.0",
100
- "@internal/tsdown-config": "0.1.0",
101
- "@internal/vitest-config": "0.1.0"
100
+ "@internal/vitest-config": "0.1.0",
101
+ "@internal/tsdown-config": "0.1.0"
102
102
  },
103
103
  "prettier": "@internal/prettier-config",
104
104
  "scripts": {