@wordrhyme/auto-crud 1.3.8 → 1.4.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
@@ -546,11 +546,11 @@ table={{
546
546
 
547
547
  `toolbar` 数组控制页面顶部右侧工具栏的按钮,设计思路完全对齐行操作 `actions`。旧 prop `toolbarActions` 仍兼容读取,但新代码请使用 `toolbar`。
548
548
 
549
- 内置操作类型:`create`(新建)、`import`(导入)、`export`(导出)。
549
+ 内置操作类型:`refresh`(刷新)、`create`(新建)、`import`(导入)、`export`(导出)。
550
550
 
551
551
  ### 只添加自定义按钮(内置保持默认)
552
552
 
553
- 只传 `type: "custom"` 项时,所有内置按钮(导入、导出、新建)保持原样,custom 项按 `position` 插入首部或尾部。
553
+ 只传 `type: "custom"` 项时,所有内置按钮(刷新、导入、导出、新建)保持原样,custom 项按 `position` 插入首部或尾部。
554
554
 
555
555
  ```tsx
556
556
  <AutoCrudTable
@@ -563,7 +563,7 @@ table={{
563
563
  { type: 'custom', component: <Button variant="outline">批量标签</Button> }, // 默认 position: "end"
564
564
  ]}
565
565
  />
566
- // 渲染顺序: 分类管理 · [导入] · [导出] · [新建] · 批量标签
566
+ // 渲染顺序: 分类管理 · [刷新] · [导入] · [导出] · [新建] · 批量标签
567
567
  ```
568
568
 
569
569
  ### 调整内置按钮顺序
@@ -669,7 +669,7 @@ table={{
669
669
 
670
670
  ```tsx
671
671
  <AutoCrudTable
672
- // defaults 即内置三个按钮的信息,在此数组里你可以随意 map、过滤或插值
672
+ // defaults 即内置按钮的信息,在此数组里你可以随意 map、过滤或插值
673
673
  toolbar={(defaults) =>
674
674
  defaults.map((btn) =>
675
675
  btn.type === 'create'
@@ -697,7 +697,7 @@ type ToolbarActionConfig =
697
697
  | ((defaults: ToolbarBuiltinActionItem[]) => ToolbarActionItem[]);
698
698
 
699
699
  type ToolbarBuiltinActionItem = ActionMeta & {
700
- type: 'create' | 'import' | 'export';
700
+ type: 'refresh' | 'create' | 'import' | 'export';
701
701
  onClick?: () => void; // 覆盖默认行为
702
702
  label?: string; // 覆盖默认文案
703
703
  component?: React.ReactNode | ((context: AutoCrudToolbarContext) => React.ReactNode);
@@ -715,10 +715,34 @@ interface AutoCrudToolbarContext {
715
715
  rowIds: string[];
716
716
  selectedRowIds: string[];
717
717
  selectedCount: number;
718
+ refresh?: () => Promise<unknown>;
719
+ openImport?: () => void;
720
+ exportData?: () => Promise<void>;
718
721
  openCreate?: () => void;
722
+ isRefreshing: boolean;
723
+ isExporting: boolean;
719
724
  }
720
725
  ```
721
726
 
727
+ `AutoCrudToolbarContext` 是工具栏自定义组件的正式 command contract。自定义按钮需要复用 AutoCrud 内置能力时,直接调用 context 中的命令,不要绕过内部状态重新实现。
728
+
729
+ 例如业务侧希望把默认“新建”按钮替换成“手动创建”,可以替换 `type: "create"` 的组件并调用 `openCreate`:
730
+
731
+ ```tsx
732
+ <AutoCrudTable
733
+ toolbar={[
734
+ {
735
+ type: 'create',
736
+ component: ({ openCreate }) => (
737
+ <Button onClick={() => openCreate?.()}>手动创建</Button>
738
+ ),
739
+ },
740
+ ]}
741
+ />
742
+ ```
743
+
744
+ 这里“手动创建”只是业务按钮文案,不是新的 AutoCrud 流程;它仍然打开 AutoCrud 原生的新建弹窗。`openCreate`、`openImport`、`exportData` 会按对应权限和能力可选暴露,扩展方应使用 `?.()` 或自行控制 disabled 状态。
745
+
722
746
  ### 工具栏扩展 Resolver
723
747
 
724
748
  宿主应用可以通过 `setToolbarResolver` 为指定 CRUD 注入额外工具栏动作。resolver 是普通纯函数,不是 React Hook;不要在 resolver 内调用 `useRouter`、`useMemo` 等 React Hooks。
package/dist/index.cjs CHANGED
@@ -4365,6 +4365,7 @@ function normalizeDataSourceRegistration(registration) {
4365
4365
  debounceMs: typeof registration.debounceMs === "number" && registration.debounceMs >= 0 ? registration.debounceMs : 300,
4366
4366
  loadMore: registration.loadMore === true,
4367
4367
  pageSize: typeof registration.pageSize === "number" && registration.pageSize > 0 ? registration.pageSize : 20,
4368
+ resolveFields: registration.resolveFields,
4368
4369
  load: registration.load
4369
4370
  };
4370
4371
  }
@@ -4599,8 +4600,10 @@ function loadAutoCrudDataSource({ entry, field, registryVersion, search, sourceK
4599
4600
  }
4600
4601
  Promise.resolve(entry.load({
4601
4602
  field: field.path.toString(),
4603
+ type: "form",
4602
4604
  page,
4603
4605
  pageSize: entry.loadMore ? entry.pageSize : void 0,
4606
+ query: searchValue,
4604
4607
  search: searchValue,
4605
4608
  values,
4606
4609
  signal: controller?.signal
@@ -5889,8 +5892,8 @@ function haveSameRowSelection(left, right, idKey) {
5889
5892
  if (leftIds.length === left.length && rightIds.length === right.length) return areStringArraysEqual(leftIds, rightIds);
5890
5893
  return left.every((row, index$1) => row === right[index$1]);
5891
5894
  }
5892
- function readCreateAction(actions) {
5893
- return actions.find((action$1) => action$1.type === "create");
5895
+ function readToolbarAction(actions, type) {
5896
+ return actions.find((action$1) => action$1.type === type);
5894
5897
  }
5895
5898
  function resolveToolbarActionsWithResolver(targetId, ownerActions, context) {
5896
5899
  const resolver = autoCrudToolbarResolver;
@@ -5964,6 +5967,47 @@ function shouldLoadDynamicFilterOptions(config) {
5964
5967
  }
5965
5968
  return normalizeDataSourceConfig(getFilterDataSource(config)) !== void 0;
5966
5969
  }
5970
+ function isResolveValuePresent(value) {
5971
+ return value !== null && value !== void 0 && String(value).length > 0;
5972
+ }
5973
+ function getResolveValues(rows, field) {
5974
+ const values = /* @__PURE__ */ new Set();
5975
+ for (const row of rows) {
5976
+ const value = row[field];
5977
+ const items = Array.isArray(value) ? value : [value];
5978
+ for (const item of items) if (isResolveValuePresent(item)) values.add(String(item));
5979
+ }
5980
+ return Array.from(values);
5981
+ }
5982
+ function normalizeResolveCacheValue(value) {
5983
+ if (value instanceof Date) return value.toISOString();
5984
+ if (Array.isArray(value)) return value.map(normalizeResolveCacheValue);
5985
+ if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((key) => [key, normalizeResolveCacheValue(value[key])]));
5986
+ return value === void 0 ? { __autoCrudUndefined: true } : value;
5987
+ }
5988
+ function resolveRecordSignature(record) {
5989
+ return JSON.stringify(Object.keys(record).sort().map((key) => [key, normalizeResolveCacheValue(record[key])]));
5990
+ }
5991
+ function resolveRecordCacheKey(sourceKey, field, record) {
5992
+ return `${sourceKey}\u0000${field}\u0000${String(record[field])}\u0000${resolveRecordSignature(record)}`;
5993
+ }
5994
+ function mergeFieldOptions(current, incoming) {
5995
+ if ((!current || current.length === 0) && (!incoming || incoming.length === 0)) return;
5996
+ return mergeFilterOptions(current, incoming ?? []);
5997
+ }
5998
+ function mergeOptionsByField(left, right) {
5999
+ const keys = new Set([...Object.keys(left ?? {}), ...Object.keys(right ?? {})]);
6000
+ return Object.fromEntries(Array.from(keys).flatMap((key) => {
6001
+ const options = mergeFieldOptions(left?.[key], right?.[key]);
6002
+ return options ? [[key, options]] : [];
6003
+ }));
6004
+ }
6005
+ function shouldResolveOptions(field, config, hiddenColumns) {
6006
+ if (!config) return false;
6007
+ if (hiddenColumns.has(field)) return false;
6008
+ if (normalizeFieldOptions(config.enum)) return false;
6009
+ return normalizeDataSourceConfig(config.dataSource) !== void 0;
6010
+ }
5967
6011
  function useRegistryVersion(subscribe) {
5968
6012
  const [version$1, setVersion] = react.useState(0);
5969
6013
  react.useEffect(() => subscribe(() => setVersion((current) => current + 1)), [subscribe]);
@@ -6085,8 +6129,10 @@ function useDynamicFilterOptions(fields) {
6085
6129
  const timer = setTimeout(() => {
6086
6130
  Promise.resolve(entry.load({
6087
6131
  field,
6132
+ type: "filter",
6088
6133
  page,
6089
6134
  pageSize: entry.loadMore ? entry.pageSize : void 0,
6135
+ query: search,
6090
6136
  search,
6091
6137
  values: {},
6092
6138
  signal: controller?.signal
@@ -6179,6 +6225,117 @@ function useDynamicFilterOptions(fields) {
6179
6225
  setSearchValue
6180
6226
  };
6181
6227
  }
6228
+ function rowHasResolveValue(row, field, value) {
6229
+ const rowValue = row[field];
6230
+ return (Array.isArray(rowValue) ? rowValue : [rowValue]).some((item) => item !== null && item !== void 0 && String(item) === value);
6231
+ }
6232
+ function resolveExtraFields(entry, field) {
6233
+ const configured = typeof entry.resolveFields === "function" ? entry.resolveFields({ field }) : entry.resolveFields;
6234
+ return Array.from(new Set((configured ?? []).filter((key) => typeof key === "string" && key.length > 0)));
6235
+ }
6236
+ function buildResolveRecords(rows, field, values, extraFields) {
6237
+ return values.map((value) => {
6238
+ const row = rows.find((candidate) => rowHasResolveValue(candidate, field, value));
6239
+ const record = { [field]: value };
6240
+ for (const extraField of extraFields) if (row && Object.prototype.hasOwnProperty.call(row, extraField)) record[extraField] = row[extraField];
6241
+ return record;
6242
+ });
6243
+ }
6244
+ function useDynamicResolveOptions(fields, rows, hiddenColumns) {
6245
+ const registryVersion = useRegistryVersion(dataSources.subscribe);
6246
+ const [optionsByField, setOptionsByField] = react.useState({});
6247
+ const cacheRef = react.useRef(/* @__PURE__ */ new Map());
6248
+ const requestVersionRef = react.useRef(0);
6249
+ const sourceEntries = react.useMemo(() => {
6250
+ if (!fields || rows.length === 0) return [];
6251
+ const hiddenSet = new Set(hiddenColumns);
6252
+ return Object.entries(fields).flatMap(([field, config]) => {
6253
+ if (!shouldResolveOptions(field, config, hiddenSet)) return [];
6254
+ const source = normalizeDataSourceConfig(config.dataSource);
6255
+ const values = getResolveValues(rows, field);
6256
+ return source && values.length > 0 ? [{
6257
+ field,
6258
+ values,
6259
+ source
6260
+ }] : [];
6261
+ });
6262
+ }, [
6263
+ fields,
6264
+ hiddenColumns,
6265
+ rows
6266
+ ]);
6267
+ react.useEffect(() => {
6268
+ const requestVersion = requestVersionRef.current + 1;
6269
+ requestVersionRef.current = requestVersion;
6270
+ if (sourceEntries.length === 0) {
6271
+ setOptionsByField({});
6272
+ return;
6273
+ }
6274
+ let cancelled = false;
6275
+ const activeFields = new Set(sourceEntries.map(({ field }) => field));
6276
+ const readCachedOptions = (entry, records) => mergeFilterOptions(void 0, records.flatMap((record) => {
6277
+ const option = cacheRef.current.get(resolveRecordCacheKey(entry.source.key, entry.field, record));
6278
+ return option ? [option] : [];
6279
+ }));
6280
+ const recordsByField = new Map(sourceEntries.flatMap((sourceEntry) => {
6281
+ const entry = dataSources.get(sourceEntry.source.key);
6282
+ if (!entry) return [];
6283
+ return [[sourceEntry.field, buildResolveRecords(rows, sourceEntry.field, sourceEntry.values, resolveExtraFields(entry, sourceEntry.field))]];
6284
+ }));
6285
+ const readSourceEntryCachedOptions = (entry) => {
6286
+ return readCachedOptions(entry, recordsByField.get(entry.field) ?? []);
6287
+ };
6288
+ const findOptionForRecord = (options, field, record) => {
6289
+ const value = String(record[field]);
6290
+ return options.find((option) => option.value === value);
6291
+ };
6292
+ const missingRecordsFor = (entry, records) => records.filter((record) => !cacheRef.current.has(resolveRecordCacheKey(entry.source.key, entry.field, record)));
6293
+ setOptionsByField((current) => {
6294
+ const next = {};
6295
+ for (const entry of sourceEntries) {
6296
+ const cached = readSourceEntryCachedOptions(entry);
6297
+ if (cached.length > 0) next[entry.field] = cached;
6298
+ }
6299
+ for (const [field, options] of Object.entries(current)) if (activeFields.has(field) && !next[field]) next[field] = options;
6300
+ return next;
6301
+ });
6302
+ for (const sourceEntry of sourceEntries) {
6303
+ const entry = dataSources.get(sourceEntry.source.key);
6304
+ if (!entry) continue;
6305
+ const missingRecords = missingRecordsFor(sourceEntry, recordsByField.get(sourceEntry.field) ?? []);
6306
+ if (missingRecords.length === 0) continue;
6307
+ Promise.resolve(entry.load({
6308
+ field: sourceEntry.field,
6309
+ type: "resolve",
6310
+ values: missingRecords
6311
+ })).then((result) => {
6312
+ if (cancelled || requestVersionRef.current !== requestVersion) return;
6313
+ const options = normalizeOptions(result);
6314
+ for (const record of missingRecords) {
6315
+ const option = findOptionForRecord(options, sourceEntry.field, record);
6316
+ if (!option) continue;
6317
+ cacheRef.current.set(resolveRecordCacheKey(sourceEntry.source.key, sourceEntry.field, record), option);
6318
+ }
6319
+ const cached = readSourceEntryCachedOptions(sourceEntry);
6320
+ setOptionsByField((current) => ({
6321
+ ...current,
6322
+ [sourceEntry.field]: cached
6323
+ }));
6324
+ }, (error) => {
6325
+ if (cancelled || requestVersionRef.current !== requestVersion) return;
6326
+ console.warn(`[AutoCrud] Failed to resolve data source "${sourceEntry.source.key}".`, error);
6327
+ });
6328
+ }
6329
+ return () => {
6330
+ cancelled = true;
6331
+ };
6332
+ }, [
6333
+ registryVersion,
6334
+ rows,
6335
+ sourceEntries
6336
+ ]);
6337
+ return { optionsByField };
6338
+ }
6182
6339
  function getOptionLabel(value, options) {
6183
6340
  const stringValue = String(value);
6184
6341
  return options?.find((option) => option.value === stringValue)?.label ?? stringValue;
@@ -6187,7 +6344,7 @@ function getOptionLabel(value, options) {
6187
6344
  * 从统一配置生成表格 overrides
6188
6345
  * 当 filter 配置存在时,合并到 meta 中(不影响列隐藏)
6189
6346
  */
6190
- function buildTableOverrides(fields, legacyOverrides, dynamicFilterState) {
6347
+ function buildTableOverrides(fields, legacyOverrides, dynamicFilterState, dynamicResolveState) {
6191
6348
  const result = { ...legacyOverrides };
6192
6349
  if (fields) for (const [key, config] of Object.entries(fields)) {
6193
6350
  const fieldOptions = normalizeFieldOptions(config.enum);
@@ -6195,7 +6352,8 @@ function buildTableOverrides(fields, legacyOverrides, dynamicFilterState) {
6195
6352
  const filterDataSourceRegistered = dynamicFilterState?.registeredByField[key];
6196
6353
  const filterDataSourceSearchable = dynamicFilterState?.searchableByField[key];
6197
6354
  const currentDynamicOptions = !fieldOptions && filterDataSourceRegistered ? toTableOptions(dynamicFilterState?.optionsByField[key] ?? []) : void 0;
6198
- const dynamicOptions = !fieldOptions && filterDataSourceRegistered ? toTableOptions(dynamicFilterState?.labelOptionsByField[key] ?? dynamicFilterState?.optionsByField[key] ?? []) : void 0;
6355
+ const mergedDynamicOptions = mergeFieldOptions(!fieldOptions && filterDataSourceRegistered ? dynamicFilterState?.labelOptionsByField[key] ?? dynamicFilterState?.optionsByField[key] ?? [] : void 0, !fieldOptions ? dynamicResolveState?.optionsByField[key] : void 0);
6356
+ const dynamicOptions = !fieldOptions && mergedDynamicOptions ? toTableOptions(mergedDynamicOptions) : void 0;
6199
6357
  const tableMeta = config.table !== false && typeof config.table === "object" ? config.table.meta : void 0;
6200
6358
  const fieldEnumMeta = tableOptions ? {
6201
6359
  options: tableOptions,
@@ -6203,18 +6361,20 @@ function buildTableOverrides(fields, legacyOverrides, dynamicFilterState) {
6203
6361
  } : void 0;
6204
6362
  const fieldDataSourceMeta = dynamicOptions ? {
6205
6363
  options: dynamicOptions,
6206
- autoCrudFilterOptions: currentDynamicOptions ?? dynamicOptions,
6207
- autoCrudFilterHasMore: dynamicFilterState?.hasMoreByField[key] ?? false,
6208
- autoCrudFilterLoading: dynamicFilterState?.loadingByField[key] ?? false,
6209
- autoCrudFilterOnPopupScroll: (event) => {
6210
- if (!isNearPopupScrollBottom(event.currentTarget)) return;
6211
- dynamicFilterState?.loadMore(key);
6212
- },
6213
- variant: "multiSelect",
6214
- ...filterDataSourceSearchable ? {
6215
- autoCrudFilterSearchValue: dynamicFilterState?.searchValues[key] ?? "",
6216
- autoCrudFilterOnSearch: (value) => dynamicFilterState?.setSearchValue(key, value),
6217
- autoCrudFilterShouldFilter: false
6364
+ ...filterDataSourceRegistered ? {
6365
+ autoCrudFilterOptions: currentDynamicOptions ?? dynamicOptions,
6366
+ autoCrudFilterHasMore: dynamicFilterState?.hasMoreByField[key] ?? false,
6367
+ autoCrudFilterLoading: dynamicFilterState?.loadingByField[key] ?? false,
6368
+ autoCrudFilterOnPopupScroll: (event) => {
6369
+ if (!isNearPopupScrollBottom(event.currentTarget)) return;
6370
+ dynamicFilterState?.loadMore(key);
6371
+ },
6372
+ variant: "multiSelect",
6373
+ ...filterDataSourceSearchable ? {
6374
+ autoCrudFilterSearchValue: dynamicFilterState?.searchValues[key] ?? "",
6375
+ autoCrudFilterOnSearch: (value) => dynamicFilterState?.setSearchValue(key, value),
6376
+ autoCrudFilterShouldFilter: false
6377
+ } : void 0
6218
6378
  } : void 0
6219
6379
  } : void 0;
6220
6380
  let filterMeta;
@@ -6266,6 +6426,35 @@ function buildTableOverrides(fields, legacyOverrides, dynamicFilterState) {
6266
6426
  }
6267
6427
  return result;
6268
6428
  }
6429
+ function buildCapabilityTableOverrides(schema, capabilities) {
6430
+ if (!capabilities) return {};
6431
+ const result = {};
6432
+ const filterFields = capabilities.filters?.fields;
6433
+ const sortFields = capabilities.sort?.fields;
6434
+ const filterSet = Array.isArray(filterFields) ? new Set(filterFields) : null;
6435
+ const sortSet = Array.isArray(sortFields) ? new Set(sortFields) : null;
6436
+ for (const key of Object.keys(schema.shape)) {
6437
+ const override = {};
6438
+ if (capabilities.filters) {
6439
+ if (!capabilities.filters.enabled) override.enableColumnFilter = false;
6440
+ else if (filterSet) override.enableColumnFilter = filterSet.has(key);
6441
+ }
6442
+ if (capabilities.sort) {
6443
+ if (!capabilities.sort.enabled) override.enableSorting = false;
6444
+ else if (sortSet) override.enableSorting = sortSet.has(key);
6445
+ }
6446
+ if (Object.keys(override).length > 0) result[key] = override;
6447
+ }
6448
+ return result;
6449
+ }
6450
+ function mergeTableOverrides(base, enforced) {
6451
+ const result = { ...base ?? {} };
6452
+ for (const [key, override] of Object.entries(enforced)) result[key] = {
6453
+ ...result[key] ?? {},
6454
+ ...override
6455
+ };
6456
+ return result;
6457
+ }
6269
6458
  /**
6270
6459
  * 从统一配置生成隐藏列列表
6271
6460
  */
@@ -6543,6 +6732,14 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
6543
6732
  const [exporting, setExporting] = react.useState(false);
6544
6733
  const getSelectedRowsRef = react.useRef(null);
6545
6734
  const dynamicFilterOptions = useDynamicFilterOptions(resolvedFields);
6735
+ const hiddenColumns = react.useMemo(() => buildHiddenColumns(resolvedFields, tableConfig?.hidden, denyFields, tableConfig?.overrides), [
6736
+ resolvedFields,
6737
+ tableConfig?.hidden,
6738
+ denyFields,
6739
+ tableConfig?.overrides
6740
+ ]);
6741
+ const dynamicResolveOptions = useDynamicResolveOptions(resolvedFields, resource.tableData.data, hiddenColumns);
6742
+ const dynamicOptionsByField = react.useMemo(() => mergeOptionsByField(dynamicFilterOptions.labelOptionsByField, dynamicResolveOptions.optionsByField), [dynamicFilterOptions.labelOptionsByField, dynamicResolveOptions.optionsByField]);
6546
6743
  const resourceIdKey = resource.idKey ?? "id";
6547
6744
  const actionRegistryVersion = useCrudActionsVersion();
6548
6745
  const unifiedActions = react.useMemo(() => {
@@ -6576,28 +6773,10 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
6576
6773
  const handleSelectedRowsChange = react.useCallback((rows) => {
6577
6774
  setSelectedRows((currentRows) => haveSameRowSelection(currentRows, rows, resourceIdKey) ? currentRows : rows);
6578
6775
  }, [resourceIdKey]);
6579
- const ownerCreateAction = react.useMemo(() => readCreateAction(ownerToolbarActions), [ownerToolbarActions]);
6580
- const toolbarOpenCreate = can.create ? ownerCreateAction?.onClick ?? onCreate ?? resource.handlers.openCreate : void 0;
6581
- const toolbarContext = react.useMemo(() => ({
6582
- crudId: id ?? "",
6583
- idKey: resourceIdKey,
6584
- rowIds,
6585
- selectedRowIds,
6586
- selectedCount,
6587
- isRefreshing: resource.tableData.isFetching,
6588
- ...resource.handlers.refresh ? { refresh: resource.handlers.refresh } : {},
6589
- ...toolbarOpenCreate ? { openCreate: toolbarOpenCreate } : {}
6590
- }), [
6591
- id,
6592
- resourceIdKey,
6593
- rowIds,
6594
- selectedRowIds,
6595
- selectedCount,
6596
- resource.handlers.refresh,
6597
- resource.tableData.isFetching,
6598
- toolbarOpenCreate
6599
- ]);
6600
- const resolvedToolbarActions = resolveToolbarActionsWithResolver(id, registryToolbarActions, toolbarContext);
6776
+ const ownerRefreshAction = react.useMemo(() => readToolbarAction(ownerToolbarActions, "refresh"), [ownerToolbarActions]);
6777
+ const ownerImportAction = react.useMemo(() => readToolbarAction(ownerToolbarActions, "import"), [ownerToolbarActions]);
6778
+ const ownerExportAction = react.useMemo(() => readToolbarAction(ownerToolbarActions, "export"), [ownerToolbarActions]);
6779
+ const ownerCreateAction = react.useMemo(() => readToolbarAction(ownerToolbarActions, "create"), [ownerToolbarActions]);
6601
6780
  const importColumns = react.useMemo(() => {
6602
6781
  const shape = resolvedSchema.shape;
6603
6782
  const denySet = new Set([...DEFAULT_IMPORT_MANAGED_FIELDS, ...denyFields ?? []]);
@@ -6634,10 +6813,66 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
6634
6813
  setExporting(false);
6635
6814
  }
6636
6815
  }, [selectedCount, handleExport]);
6637
- const tableOverrides = react.useMemo(() => buildTableOverrides(resolvedFields, tableConfig?.overrides, dynamicFilterOptions), [
6816
+ const toolbarRefresh = react.useMemo(() => {
6817
+ const refresh = ownerRefreshAction?.onClick ?? resource.handlers.refresh;
6818
+ if (!refresh) return void 0;
6819
+ return async () => refresh();
6820
+ }, [ownerRefreshAction?.onClick, resource.handlers.refresh]);
6821
+ const toolbarOpenImport = react.useMemo(() => {
6822
+ if (!canImport) return void 0;
6823
+ return ownerImportAction?.onClick ?? (() => setImportOpen(true));
6824
+ }, [canImport, ownerImportAction?.onClick]);
6825
+ const toolbarExportData = react.useMemo(() => {
6826
+ if (!canExport) return void 0;
6827
+ const exportData = ownerExportAction?.onClick ?? handleExportClick;
6828
+ return async () => exportData();
6829
+ }, [
6830
+ canExport,
6831
+ handleExportClick,
6832
+ ownerExportAction?.onClick
6833
+ ]);
6834
+ const toolbarOpenCreate = react.useMemo(() => {
6835
+ if (!can.create) return void 0;
6836
+ return ownerCreateAction?.onClick ?? onCreate ?? resource.handlers.openCreate;
6837
+ }, [
6838
+ can.create,
6839
+ onCreate,
6840
+ ownerCreateAction?.onClick,
6841
+ resource.handlers.openCreate
6842
+ ]);
6843
+ const toolbarContext = react.useMemo(() => ({
6844
+ crudId: id ?? "",
6845
+ idKey: resourceIdKey,
6846
+ rowIds,
6847
+ selectedRowIds,
6848
+ selectedCount,
6849
+ isRefreshing: resource.tableData.isFetching,
6850
+ isExporting: exporting,
6851
+ ...toolbarRefresh ? { refresh: toolbarRefresh } : {},
6852
+ ...toolbarOpenImport ? { openImport: toolbarOpenImport } : {},
6853
+ ...toolbarExportData ? { exportData: toolbarExportData } : {},
6854
+ ...toolbarOpenCreate ? { openCreate: toolbarOpenCreate } : {}
6855
+ }), [
6856
+ id,
6857
+ resourceIdKey,
6858
+ rowIds,
6859
+ selectedRowIds,
6860
+ selectedCount,
6861
+ resource.tableData.isFetching,
6862
+ exporting,
6863
+ toolbarRefresh,
6864
+ toolbarOpenImport,
6865
+ toolbarExportData,
6866
+ toolbarOpenCreate
6867
+ ]);
6868
+ const resolvedToolbarActions = resolveToolbarActionsWithResolver(id, registryToolbarActions, toolbarContext);
6869
+ const tableOverrides = react.useMemo(() => mergeTableOverrides(buildTableOverrides(resolvedFields, tableConfig?.overrides, dynamicFilterOptions, dynamicResolveOptions), buildCapabilityTableOverrides(resolvedSchema, resource.capabilities)), [
6638
6870
  resolvedFields,
6639
6871
  tableConfig?.overrides,
6640
- dynamicFilterOptions
6872
+ dynamicFilterOptions,
6873
+ dynamicResolveOptions,
6874
+ resolvedSchema,
6875
+ resource.capabilities
6641
6876
  ]);
6642
6877
  const defaultSort = react.useMemo(() => resource.defaultSort ?? tableConfig?.defaultSort ?? getDefaultSortingForSchema(resolvedSchema), [
6643
6878
  resource.defaultSort,
@@ -6649,18 +6884,23 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
6649
6884
  formConfig?.overrides,
6650
6885
  denyFields
6651
6886
  ]);
6652
- const hiddenColumns = react.useMemo(() => buildHiddenColumns(resolvedFields, tableConfig?.hidden, denyFields, tableConfig?.overrides), [
6653
- resolvedFields,
6654
- tableConfig?.hidden,
6655
- denyFields,
6656
- tableConfig?.overrides
6657
- ]);
6658
6887
  const searchConfig = react.useMemo(() => {
6659
6888
  const tableSearch = tableConfig?.search;
6889
+ const searchCapability = resource.capabilities?.search;
6660
6890
  if (tableSearch === false) return false;
6891
+ if (searchCapability) {
6892
+ if (!searchCapability.enabled) return false;
6893
+ if (tableSearch && typeof tableSearch === "object") return tableSearch;
6894
+ return true;
6895
+ }
6661
6896
  if (tableSearch && typeof tableSearch === "object") return tableSearch;
6897
+ if (tableSearch === true) return true;
6662
6898
  return Object.values(resolvedFields).some((field) => field?.search === true) ? true : false;
6663
- }, [resolvedFields, tableConfig?.search]);
6899
+ }, [
6900
+ resolvedFields,
6901
+ resource.capabilities?.search,
6902
+ tableConfig?.search
6903
+ ]);
6664
6904
  const batchFields = react.useMemo(() => buildBatchUpdateFields(resolvedSchema, tableConfig?.batchFields, resolvedFields), [
6665
6905
  resolvedSchema,
6666
6906
  tableConfig?.batchFields,
@@ -6715,7 +6955,7 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
6715
6955
  children: (() => {
6716
6956
  const renderBuiltinButton = (type, overrides) => {
6717
6957
  if (type === "refresh") {
6718
- const onRefresh = overrides?.onClick ?? resource.handlers.refresh;
6958
+ const onRefresh = overrides?.onClick ?? toolbarRefresh;
6719
6959
  const label = overrides?.label ?? locale.toolbar.refresh;
6720
6960
  if (!onRefresh) return null;
6721
6961
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Button, {
@@ -6731,18 +6971,18 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
6731
6971
  if (type === "import" && canImport) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.Button, {
6732
6972
  variant: "outline",
6733
6973
  size: "sm",
6734
- onClick: overrides?.onClick ?? (() => setImportOpen(true)),
6974
+ onClick: overrides?.onClick ?? toolbarOpenImport,
6735
6975
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Download, { className: "mr-2 h-4 w-4" }), overrides?.label ?? locale.toolbar.import]
6736
6976
  }, "import");
6737
6977
  if (type === "export" && canExport) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__wordrhyme_shadcn.Button, {
6738
6978
  variant: "outline",
6739
6979
  size: "sm",
6740
- onClick: overrides?.onClick ?? handleExportClick,
6980
+ onClick: overrides?.onClick ?? toolbarExportData,
6741
6981
  disabled: exporting || selectedCount === 0 && !resource.handlers.export,
6742
6982
  children: [exporting ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Upload, { className: "mr-2 h-4 w-4" }), overrides?.label ?? (selectedCount > 0 ? locale.toolbar.exportSelected(selectedCount) : locale.toolbar.export)]
6743
6983
  }, "export");
6744
6984
  if (type === "create" && can.create) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__wordrhyme_shadcn.Button, {
6745
- onClick: overrides?.onClick ?? onCreate ?? resource.handlers.openCreate,
6985
+ onClick: overrides?.onClick ?? toolbarOpenCreate,
6746
6986
  children: overrides?.label ?? locale.toolbar.create
6747
6987
  }, "create");
6748
6988
  return null;
@@ -6830,7 +7070,7 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
6830
7070
  data: resource.modal.selected,
6831
7071
  schema: resolvedSchema,
6832
7072
  fields: resolvedFields,
6833
- dynamicOptions: dynamicFilterOptions.optionsByField,
7073
+ dynamicOptions: dynamicOptionsByField,
6834
7074
  denyFields,
6835
7075
  locale
6836
7076
  }),
@@ -7613,6 +7853,7 @@ function useAutoCrudResource({ router, schema, query: queryTransform, options =
7613
7853
  const resolvedSchema = (0, react.useMemo)(() => mergeMetadataSchema(schema, metadata?.schema), [schema, metadata?.schema]);
7614
7854
  const defaultSorting = (0, react.useMemo)(() => defaultSortOption === false ? [] : defaultSortOption ?? getDefaultSortingForSchema(resolvedSchema), [defaultSortOption, resolvedSchema]);
7615
7855
  const metadataFields = (0, react.useMemo)(() => readMetadataFields(metadata?.fields), [metadata?.fields]);
7856
+ const capabilities = metadata?.capabilities;
7616
7857
  const columns = (0, react.useMemo)(() => createTableSchema(resolvedSchema), [resolvedSchema]);
7617
7858
  const [urlParams] = useQueryStates({
7618
7859
  page: parseAsInteger.withDefault(1),
@@ -7905,6 +8146,7 @@ function useAutoCrudResource({ router, schema, query: queryTransform, options =
7905
8146
  },
7906
8147
  schema: resolvedSchema,
7907
8148
  fields: metadataFields,
8149
+ capabilities,
7908
8150
  defaultSort: defaultSorting,
7909
8151
  modal,
7910
8152
  mutations: {