@wordrhyme/auto-crud 1.17.1 → 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
@@ -2184,6 +2184,7 @@ const enUSTableControls = {
2184
2184
  }
2185
2185
  };
2186
2186
  const zhCN = {
2187
+ tagAssignment: (label) => `设置${label}`,
2187
2188
  batchSearch: zhCNBatchSearch,
2188
2189
  tableControls: {
2189
2190
  sort: {
@@ -2299,6 +2300,7 @@ const zhCN = {
2299
2300
  }
2300
2301
  };
2301
2302
  const enUS = {
2303
+ tagAssignment: (label) => `Set ${label}`,
2302
2304
  batchSearch: enUSBatchSearch,
2303
2305
  tableControls: enUSTableControls,
2304
2306
  pagination: enUSPagination,
@@ -7341,8 +7343,7 @@ const filterModeConfig = {
7341
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 }) {
7342
7344
  const inheritedResourceId = (0, react.useContext)(CrudResourceContext);
7343
7345
  const activeResourceId = resourceId ?? inheritedResourceId;
7344
- const services = useAutoCrudConfig();
7345
- const viewsEnabled = views ?? (!!activeResourceId && !!services?.viewStorage);
7346
+ const viewsEnabled = views ?? false;
7346
7347
  if (viewsEnabled && !activeResourceId) throw new Error("Saved views require a stable resourceId");
7347
7348
  if (expandedRow && !getRowId) throw new Error("AutoTable requires getRowId when expandedRow is provided.");
7348
7349
  const enableExpansion = !!expandedRow;
@@ -8049,7 +8050,7 @@ function mergeFieldPart(base, override) {
8049
8050
  };
8050
8051
  }
8051
8052
  function mergeFieldConfig(base, override) {
8052
- return {
8053
+ const config = {
8053
8054
  ...base,
8054
8055
  ...override,
8055
8056
  enum: override?.enum ?? base?.enum,
@@ -8058,6 +8059,11 @@ function mergeFieldConfig(base, override) {
8058
8059
  filter: mergeFieldPart(base?.filter, override?.filter),
8059
8060
  form: mergeFieldPart(base?.form, override?.form)
8060
8061
  };
8062
+ if (config.tags && config.filter !== false) config.filter = {
8063
+ variant: "tags",
8064
+ ...config.filter
8065
+ };
8066
+ return config;
8061
8067
  }
8062
8068
  function mergeFields(base, override) {
8063
8069
  if (!base && !override) return void 0;
@@ -10130,7 +10136,7 @@ function resolveActions(actions, defaults, rowActionsLocale, context) {
10130
10136
  */
10131
10137
  function AutoCrudTable({ id, title, description, schema, resource, fields, table: tableConfig, form: formConfig, view: viewConfig, permissions, actions: actionItems, toolbar, toolbarActions, locale: localeProp, onCreate, onExport }) {
10132
10138
  const services = useAutoCrudConfig();
10133
- const viewsEnabled = tableConfig?.views ?? (!!id && !!services?.viewStorage);
10139
+ const viewsEnabled = tableConfig?.views ?? false;
10134
10140
  if (viewsEnabled && !id) throw new Error("Saved views require a stable AutoCrudTable id");
10135
10141
  const locale = react.useMemo(() => resolveLocale(localeProp), [localeProp]);
10136
10142
  const getPagination = react.useCallback(() => id ? tablePagination.get(id) : void 0, [id]);
@@ -10175,6 +10181,51 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
10175
10181
  const excludedColumns = react.useMemo(() => hiddenColumns.filter((key) => !filterOnly.includes(key)), [hiddenColumns, filterOnly]);
10176
10182
  const dynamicResolveOptions = useDynamicResolveOptions(resolvedFields, resource.tableData.data, hiddenColumns);
10177
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
+ ]);
10178
10229
  const actionRegistryVersion = useCrudActionsVersion();
10179
10230
  const unifiedActions = react.useMemo(() => {
10180
10231
  if (isUnifiedActionsConfig(actionItems)) return actionItems;
@@ -10184,8 +10235,28 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
10184
10235
  const ownerRowConfig = unifiedActions.row;
10185
10236
  const ownerBatchConfig = unifiedActions.batch ?? tableConfig?.batchActions;
10186
10237
  const ownerToolbarActions = react.useMemo(() => resolveOwnerToolbarActions(ownerToolbarConfig), [ownerToolbarConfig]);
10187
- const ownerRowActions = react.useMemo(() => resolveOwnerRowActions(ownerRowConfig), [ownerRowConfig]);
10188
- 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
+ ]);
10189
10260
  const registryToolbarActions = react.useMemo(() => crudActions.resolve(id, "toolbar", ownerToolbarActions), [
10190
10261
  id,
10191
10262
  ownerToolbarActions,
@@ -10375,7 +10446,7 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
10375
10446
  }
10376
10447
  }
10377
10448
  for (const [field, config] of Object.entries(resolvedFields)) {
10378
- 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;
10379
10450
  formattedOverrides[field] = {
10380
10451
  ...formattedOverrides[field],
10381
10452
  enableSorting: formattedOverrides[field]?.enableSorting ?? false,
@@ -10625,6 +10696,14 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
10625
10696
  ...onExport ? { exportTableRef } : {}
10626
10697
  }),
10627
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
+ }),
10628
10707
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CrudFormModal, {
10629
10708
  open: resource.modal.createOpen || resource.modal.editOpen,
10630
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
@@ -2168,6 +2168,7 @@ const enUSTableControls = {
2168
2168
  }
2169
2169
  };
2170
2170
  const zhCN = {
2171
+ tagAssignment: (label) => `设置${label}`,
2171
2172
  batchSearch: zhCNBatchSearch,
2172
2173
  tableControls: {
2173
2174
  sort: {
@@ -2283,6 +2284,7 @@ const zhCN = {
2283
2284
  }
2284
2285
  };
2285
2286
  const enUS = {
2287
+ tagAssignment: (label) => `Set ${label}`,
2286
2288
  batchSearch: enUSBatchSearch,
2287
2289
  tableControls: enUSTableControls,
2288
2290
  pagination: enUSPagination,
@@ -7325,8 +7327,7 @@ const filterModeConfig = {
7325
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 }) {
7326
7328
  const inheritedResourceId = useContext(CrudResourceContext);
7327
7329
  const activeResourceId = resourceId ?? inheritedResourceId;
7328
- const services = useAutoCrudConfig();
7329
- const viewsEnabled = views ?? (!!activeResourceId && !!services?.viewStorage);
7330
+ const viewsEnabled = views ?? false;
7330
7331
  if (viewsEnabled && !activeResourceId) throw new Error("Saved views require a stable resourceId");
7331
7332
  if (expandedRow && !getRowId) throw new Error("AutoTable requires getRowId when expandedRow is provided.");
7332
7333
  const enableExpansion = !!expandedRow;
@@ -8033,7 +8034,7 @@ function mergeFieldPart(base, override) {
8033
8034
  };
8034
8035
  }
8035
8036
  function mergeFieldConfig(base, override) {
8036
- return {
8037
+ const config = {
8037
8038
  ...base,
8038
8039
  ...override,
8039
8040
  enum: override?.enum ?? base?.enum,
@@ -8042,6 +8043,11 @@ function mergeFieldConfig(base, override) {
8042
8043
  filter: mergeFieldPart(base?.filter, override?.filter),
8043
8044
  form: mergeFieldPart(base?.form, override?.form)
8044
8045
  };
8046
+ if (config.tags && config.filter !== false) config.filter = {
8047
+ variant: "tags",
8048
+ ...config.filter
8049
+ };
8050
+ return config;
8045
8051
  }
8046
8052
  function mergeFields(base, override) {
8047
8053
  if (!base && !override) return void 0;
@@ -10114,7 +10120,7 @@ function resolveActions(actions, defaults, rowActionsLocale, context) {
10114
10120
  */
10115
10121
  function AutoCrudTable({ id, title, description, schema, resource, fields, table: tableConfig, form: formConfig, view: viewConfig, permissions, actions: actionItems, toolbar, toolbarActions, locale: localeProp, onCreate, onExport }) {
10116
10122
  const services = useAutoCrudConfig();
10117
- const viewsEnabled = tableConfig?.views ?? (!!id && !!services?.viewStorage);
10123
+ const viewsEnabled = tableConfig?.views ?? false;
10118
10124
  if (viewsEnabled && !id) throw new Error("Saved views require a stable AutoCrudTable id");
10119
10125
  const locale = React.useMemo(() => resolveLocale(localeProp), [localeProp]);
10120
10126
  const getPagination = React.useCallback(() => id ? tablePagination.get(id) : void 0, [id]);
@@ -10159,6 +10165,51 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
10159
10165
  const excludedColumns = React.useMemo(() => hiddenColumns.filter((key) => !filterOnly.includes(key)), [hiddenColumns, filterOnly]);
10160
10166
  const dynamicResolveOptions = useDynamicResolveOptions(resolvedFields, resource.tableData.data, hiddenColumns);
10161
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
+ ]);
10162
10213
  const actionRegistryVersion = useCrudActionsVersion();
10163
10214
  const unifiedActions = React.useMemo(() => {
10164
10215
  if (isUnifiedActionsConfig(actionItems)) return actionItems;
@@ -10168,8 +10219,28 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
10168
10219
  const ownerRowConfig = unifiedActions.row;
10169
10220
  const ownerBatchConfig = unifiedActions.batch ?? tableConfig?.batchActions;
10170
10221
  const ownerToolbarActions = React.useMemo(() => resolveOwnerToolbarActions(ownerToolbarConfig), [ownerToolbarConfig]);
10171
- const ownerRowActions = React.useMemo(() => resolveOwnerRowActions(ownerRowConfig), [ownerRowConfig]);
10172
- 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
+ ]);
10173
10244
  const registryToolbarActions = React.useMemo(() => crudActions.resolve(id, "toolbar", ownerToolbarActions), [
10174
10245
  id,
10175
10246
  ownerToolbarActions,
@@ -10359,7 +10430,7 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
10359
10430
  }
10360
10431
  }
10361
10432
  for (const [field, config] of Object.entries(resolvedFields)) {
10362
- 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;
10363
10434
  formattedOverrides[field] = {
10364
10435
  ...formattedOverrides[field],
10365
10436
  enableSorting: formattedOverrides[field]?.enableSorting ?? false,
@@ -10609,6 +10680,14 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
10609
10680
  ...onExport ? { exportTableRef } : {}
10610
10681
  }),
10611
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
+ }),
10612
10691
  /* @__PURE__ */ jsx(CrudFormModal, {
10613
10692
  open: resource.modal.createOpen || resource.modal.editOpen,
10614
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.1",
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",
@@ -72,8 +72,8 @@
72
72
  "tailwind-merge": "^3.5.0",
73
73
  "vaul": "^1.1.2",
74
74
  "@wordrhyme/formily-shadcn": "2.1.3",
75
- "@wordrhyme/shadcn": "2.0.2",
76
- "@wordrhyme/shadcn-ui": "2.1.3"
75
+ "@wordrhyme/shadcn-ui": "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": {