@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/dist/index.js CHANGED
@@ -4328,6 +4328,7 @@ function normalizeDataSourceRegistration(registration) {
4328
4328
  debounceMs: typeof registration.debounceMs === "number" && registration.debounceMs >= 0 ? registration.debounceMs : 300,
4329
4329
  loadMore: registration.loadMore === true,
4330
4330
  pageSize: typeof registration.pageSize === "number" && registration.pageSize > 0 ? registration.pageSize : 20,
4331
+ resolveFields: registration.resolveFields,
4331
4332
  load: registration.load
4332
4333
  };
4333
4334
  }
@@ -4562,8 +4563,10 @@ function loadAutoCrudDataSource({ entry, field, registryVersion, search, sourceK
4562
4563
  }
4563
4564
  Promise.resolve(entry.load({
4564
4565
  field: field.path.toString(),
4566
+ type: "form",
4565
4567
  page,
4566
4568
  pageSize: entry.loadMore ? entry.pageSize : void 0,
4569
+ query: searchValue,
4567
4570
  search: searchValue,
4568
4571
  values,
4569
4572
  signal: controller?.signal
@@ -5852,8 +5855,8 @@ function haveSameRowSelection(left, right, idKey) {
5852
5855
  if (leftIds.length === left.length && rightIds.length === right.length) return areStringArraysEqual(leftIds, rightIds);
5853
5856
  return left.every((row, index$1) => row === right[index$1]);
5854
5857
  }
5855
- function readCreateAction(actions) {
5856
- return actions.find((action$1) => action$1.type === "create");
5858
+ function readToolbarAction(actions, type) {
5859
+ return actions.find((action$1) => action$1.type === type);
5857
5860
  }
5858
5861
  function resolveToolbarActionsWithResolver(targetId, ownerActions, context) {
5859
5862
  const resolver = autoCrudToolbarResolver;
@@ -5927,6 +5930,47 @@ function shouldLoadDynamicFilterOptions(config) {
5927
5930
  }
5928
5931
  return normalizeDataSourceConfig(getFilterDataSource(config)) !== void 0;
5929
5932
  }
5933
+ function isResolveValuePresent(value) {
5934
+ return value !== null && value !== void 0 && String(value).length > 0;
5935
+ }
5936
+ function getResolveValues(rows, field) {
5937
+ const values = /* @__PURE__ */ new Set();
5938
+ for (const row of rows) {
5939
+ const value = row[field];
5940
+ const items = Array.isArray(value) ? value : [value];
5941
+ for (const item of items) if (isResolveValuePresent(item)) values.add(String(item));
5942
+ }
5943
+ return Array.from(values);
5944
+ }
5945
+ function normalizeResolveCacheValue(value) {
5946
+ if (value instanceof Date) return value.toISOString();
5947
+ if (Array.isArray(value)) return value.map(normalizeResolveCacheValue);
5948
+ if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((key) => [key, normalizeResolveCacheValue(value[key])]));
5949
+ return value === void 0 ? { __autoCrudUndefined: true } : value;
5950
+ }
5951
+ function resolveRecordSignature(record) {
5952
+ return JSON.stringify(Object.keys(record).sort().map((key) => [key, normalizeResolveCacheValue(record[key])]));
5953
+ }
5954
+ function resolveRecordCacheKey(sourceKey, field, record) {
5955
+ return `${sourceKey}\u0000${field}\u0000${String(record[field])}\u0000${resolveRecordSignature(record)}`;
5956
+ }
5957
+ function mergeFieldOptions(current, incoming) {
5958
+ if ((!current || current.length === 0) && (!incoming || incoming.length === 0)) return;
5959
+ return mergeFilterOptions(current, incoming ?? []);
5960
+ }
5961
+ function mergeOptionsByField(left, right) {
5962
+ const keys = new Set([...Object.keys(left ?? {}), ...Object.keys(right ?? {})]);
5963
+ return Object.fromEntries(Array.from(keys).flatMap((key) => {
5964
+ const options = mergeFieldOptions(left?.[key], right?.[key]);
5965
+ return options ? [[key, options]] : [];
5966
+ }));
5967
+ }
5968
+ function shouldResolveOptions(field, config, hiddenColumns) {
5969
+ if (!config) return false;
5970
+ if (hiddenColumns.has(field)) return false;
5971
+ if (normalizeFieldOptions(config.enum)) return false;
5972
+ return normalizeDataSourceConfig(config.dataSource) !== void 0;
5973
+ }
5930
5974
  function useRegistryVersion(subscribe) {
5931
5975
  const [version$1, setVersion] = React.useState(0);
5932
5976
  React.useEffect(() => subscribe(() => setVersion((current) => current + 1)), [subscribe]);
@@ -6048,8 +6092,10 @@ function useDynamicFilterOptions(fields) {
6048
6092
  const timer = setTimeout(() => {
6049
6093
  Promise.resolve(entry.load({
6050
6094
  field,
6095
+ type: "filter",
6051
6096
  page,
6052
6097
  pageSize: entry.loadMore ? entry.pageSize : void 0,
6098
+ query: search,
6053
6099
  search,
6054
6100
  values: {},
6055
6101
  signal: controller?.signal
@@ -6142,6 +6188,117 @@ function useDynamicFilterOptions(fields) {
6142
6188
  setSearchValue
6143
6189
  };
6144
6190
  }
6191
+ function rowHasResolveValue(row, field, value) {
6192
+ const rowValue = row[field];
6193
+ return (Array.isArray(rowValue) ? rowValue : [rowValue]).some((item) => item !== null && item !== void 0 && String(item) === value);
6194
+ }
6195
+ function resolveExtraFields(entry, field) {
6196
+ const configured = typeof entry.resolveFields === "function" ? entry.resolveFields({ field }) : entry.resolveFields;
6197
+ return Array.from(new Set((configured ?? []).filter((key) => typeof key === "string" && key.length > 0)));
6198
+ }
6199
+ function buildResolveRecords(rows, field, values, extraFields) {
6200
+ return values.map((value) => {
6201
+ const row = rows.find((candidate) => rowHasResolveValue(candidate, field, value));
6202
+ const record = { [field]: value };
6203
+ for (const extraField of extraFields) if (row && Object.prototype.hasOwnProperty.call(row, extraField)) record[extraField] = row[extraField];
6204
+ return record;
6205
+ });
6206
+ }
6207
+ function useDynamicResolveOptions(fields, rows, hiddenColumns) {
6208
+ const registryVersion = useRegistryVersion(dataSources.subscribe);
6209
+ const [optionsByField, setOptionsByField] = React.useState({});
6210
+ const cacheRef = React.useRef(/* @__PURE__ */ new Map());
6211
+ const requestVersionRef = React.useRef(0);
6212
+ const sourceEntries = React.useMemo(() => {
6213
+ if (!fields || rows.length === 0) return [];
6214
+ const hiddenSet = new Set(hiddenColumns);
6215
+ return Object.entries(fields).flatMap(([field, config]) => {
6216
+ if (!shouldResolveOptions(field, config, hiddenSet)) return [];
6217
+ const source = normalizeDataSourceConfig(config.dataSource);
6218
+ const values = getResolveValues(rows, field);
6219
+ return source && values.length > 0 ? [{
6220
+ field,
6221
+ values,
6222
+ source
6223
+ }] : [];
6224
+ });
6225
+ }, [
6226
+ fields,
6227
+ hiddenColumns,
6228
+ rows
6229
+ ]);
6230
+ React.useEffect(() => {
6231
+ const requestVersion = requestVersionRef.current + 1;
6232
+ requestVersionRef.current = requestVersion;
6233
+ if (sourceEntries.length === 0) {
6234
+ setOptionsByField({});
6235
+ return;
6236
+ }
6237
+ let cancelled = false;
6238
+ const activeFields = new Set(sourceEntries.map(({ field }) => field));
6239
+ const readCachedOptions = (entry, records) => mergeFilterOptions(void 0, records.flatMap((record) => {
6240
+ const option = cacheRef.current.get(resolveRecordCacheKey(entry.source.key, entry.field, record));
6241
+ return option ? [option] : [];
6242
+ }));
6243
+ const recordsByField = new Map(sourceEntries.flatMap((sourceEntry) => {
6244
+ const entry = dataSources.get(sourceEntry.source.key);
6245
+ if (!entry) return [];
6246
+ return [[sourceEntry.field, buildResolveRecords(rows, sourceEntry.field, sourceEntry.values, resolveExtraFields(entry, sourceEntry.field))]];
6247
+ }));
6248
+ const readSourceEntryCachedOptions = (entry) => {
6249
+ return readCachedOptions(entry, recordsByField.get(entry.field) ?? []);
6250
+ };
6251
+ const findOptionForRecord = (options, field, record) => {
6252
+ const value = String(record[field]);
6253
+ return options.find((option) => option.value === value);
6254
+ };
6255
+ const missingRecordsFor = (entry, records) => records.filter((record) => !cacheRef.current.has(resolveRecordCacheKey(entry.source.key, entry.field, record)));
6256
+ setOptionsByField((current) => {
6257
+ const next = {};
6258
+ for (const entry of sourceEntries) {
6259
+ const cached = readSourceEntryCachedOptions(entry);
6260
+ if (cached.length > 0) next[entry.field] = cached;
6261
+ }
6262
+ for (const [field, options] of Object.entries(current)) if (activeFields.has(field) && !next[field]) next[field] = options;
6263
+ return next;
6264
+ });
6265
+ for (const sourceEntry of sourceEntries) {
6266
+ const entry = dataSources.get(sourceEntry.source.key);
6267
+ if (!entry) continue;
6268
+ const missingRecords = missingRecordsFor(sourceEntry, recordsByField.get(sourceEntry.field) ?? []);
6269
+ if (missingRecords.length === 0) continue;
6270
+ Promise.resolve(entry.load({
6271
+ field: sourceEntry.field,
6272
+ type: "resolve",
6273
+ values: missingRecords
6274
+ })).then((result) => {
6275
+ if (cancelled || requestVersionRef.current !== requestVersion) return;
6276
+ const options = normalizeOptions(result);
6277
+ for (const record of missingRecords) {
6278
+ const option = findOptionForRecord(options, sourceEntry.field, record);
6279
+ if (!option) continue;
6280
+ cacheRef.current.set(resolveRecordCacheKey(sourceEntry.source.key, sourceEntry.field, record), option);
6281
+ }
6282
+ const cached = readSourceEntryCachedOptions(sourceEntry);
6283
+ setOptionsByField((current) => ({
6284
+ ...current,
6285
+ [sourceEntry.field]: cached
6286
+ }));
6287
+ }, (error) => {
6288
+ if (cancelled || requestVersionRef.current !== requestVersion) return;
6289
+ console.warn(`[AutoCrud] Failed to resolve data source "${sourceEntry.source.key}".`, error);
6290
+ });
6291
+ }
6292
+ return () => {
6293
+ cancelled = true;
6294
+ };
6295
+ }, [
6296
+ registryVersion,
6297
+ rows,
6298
+ sourceEntries
6299
+ ]);
6300
+ return { optionsByField };
6301
+ }
6145
6302
  function getOptionLabel(value, options) {
6146
6303
  const stringValue = String(value);
6147
6304
  return options?.find((option) => option.value === stringValue)?.label ?? stringValue;
@@ -6150,7 +6307,7 @@ function getOptionLabel(value, options) {
6150
6307
  * 从统一配置生成表格 overrides
6151
6308
  * 当 filter 配置存在时,合并到 meta 中(不影响列隐藏)
6152
6309
  */
6153
- function buildTableOverrides(fields, legacyOverrides, dynamicFilterState) {
6310
+ function buildTableOverrides(fields, legacyOverrides, dynamicFilterState, dynamicResolveState) {
6154
6311
  const result = { ...legacyOverrides };
6155
6312
  if (fields) for (const [key, config] of Object.entries(fields)) {
6156
6313
  const fieldOptions = normalizeFieldOptions(config.enum);
@@ -6158,7 +6315,8 @@ function buildTableOverrides(fields, legacyOverrides, dynamicFilterState) {
6158
6315
  const filterDataSourceRegistered = dynamicFilterState?.registeredByField[key];
6159
6316
  const filterDataSourceSearchable = dynamicFilterState?.searchableByField[key];
6160
6317
  const currentDynamicOptions = !fieldOptions && filterDataSourceRegistered ? toTableOptions(dynamicFilterState?.optionsByField[key] ?? []) : void 0;
6161
- const dynamicOptions = !fieldOptions && filterDataSourceRegistered ? toTableOptions(dynamicFilterState?.labelOptionsByField[key] ?? dynamicFilterState?.optionsByField[key] ?? []) : void 0;
6318
+ const mergedDynamicOptions = mergeFieldOptions(!fieldOptions && filterDataSourceRegistered ? dynamicFilterState?.labelOptionsByField[key] ?? dynamicFilterState?.optionsByField[key] ?? [] : void 0, !fieldOptions ? dynamicResolveState?.optionsByField[key] : void 0);
6319
+ const dynamicOptions = !fieldOptions && mergedDynamicOptions ? toTableOptions(mergedDynamicOptions) : void 0;
6162
6320
  const tableMeta = config.table !== false && typeof config.table === "object" ? config.table.meta : void 0;
6163
6321
  const fieldEnumMeta = tableOptions ? {
6164
6322
  options: tableOptions,
@@ -6166,18 +6324,20 @@ function buildTableOverrides(fields, legacyOverrides, dynamicFilterState) {
6166
6324
  } : void 0;
6167
6325
  const fieldDataSourceMeta = dynamicOptions ? {
6168
6326
  options: dynamicOptions,
6169
- autoCrudFilterOptions: currentDynamicOptions ?? dynamicOptions,
6170
- autoCrudFilterHasMore: dynamicFilterState?.hasMoreByField[key] ?? false,
6171
- autoCrudFilterLoading: dynamicFilterState?.loadingByField[key] ?? false,
6172
- autoCrudFilterOnPopupScroll: (event) => {
6173
- if (!isNearPopupScrollBottom(event.currentTarget)) return;
6174
- dynamicFilterState?.loadMore(key);
6175
- },
6176
- variant: "multiSelect",
6177
- ...filterDataSourceSearchable ? {
6178
- autoCrudFilterSearchValue: dynamicFilterState?.searchValues[key] ?? "",
6179
- autoCrudFilterOnSearch: (value) => dynamicFilterState?.setSearchValue(key, value),
6180
- autoCrudFilterShouldFilter: false
6327
+ ...filterDataSourceRegistered ? {
6328
+ autoCrudFilterOptions: currentDynamicOptions ?? dynamicOptions,
6329
+ autoCrudFilterHasMore: dynamicFilterState?.hasMoreByField[key] ?? false,
6330
+ autoCrudFilterLoading: dynamicFilterState?.loadingByField[key] ?? false,
6331
+ autoCrudFilterOnPopupScroll: (event) => {
6332
+ if (!isNearPopupScrollBottom(event.currentTarget)) return;
6333
+ dynamicFilterState?.loadMore(key);
6334
+ },
6335
+ variant: "multiSelect",
6336
+ ...filterDataSourceSearchable ? {
6337
+ autoCrudFilterSearchValue: dynamicFilterState?.searchValues[key] ?? "",
6338
+ autoCrudFilterOnSearch: (value) => dynamicFilterState?.setSearchValue(key, value),
6339
+ autoCrudFilterShouldFilter: false
6340
+ } : void 0
6181
6341
  } : void 0
6182
6342
  } : void 0;
6183
6343
  let filterMeta;
@@ -6229,6 +6389,35 @@ function buildTableOverrides(fields, legacyOverrides, dynamicFilterState) {
6229
6389
  }
6230
6390
  return result;
6231
6391
  }
6392
+ function buildCapabilityTableOverrides(schema, capabilities) {
6393
+ if (!capabilities) return {};
6394
+ const result = {};
6395
+ const filterFields = capabilities.filters?.fields;
6396
+ const sortFields = capabilities.sort?.fields;
6397
+ const filterSet = Array.isArray(filterFields) ? new Set(filterFields) : null;
6398
+ const sortSet = Array.isArray(sortFields) ? new Set(sortFields) : null;
6399
+ for (const key of Object.keys(schema.shape)) {
6400
+ const override = {};
6401
+ if (capabilities.filters) {
6402
+ if (!capabilities.filters.enabled) override.enableColumnFilter = false;
6403
+ else if (filterSet) override.enableColumnFilter = filterSet.has(key);
6404
+ }
6405
+ if (capabilities.sort) {
6406
+ if (!capabilities.sort.enabled) override.enableSorting = false;
6407
+ else if (sortSet) override.enableSorting = sortSet.has(key);
6408
+ }
6409
+ if (Object.keys(override).length > 0) result[key] = override;
6410
+ }
6411
+ return result;
6412
+ }
6413
+ function mergeTableOverrides(base, enforced) {
6414
+ const result = { ...base ?? {} };
6415
+ for (const [key, override] of Object.entries(enforced)) result[key] = {
6416
+ ...result[key] ?? {},
6417
+ ...override
6418
+ };
6419
+ return result;
6420
+ }
6232
6421
  /**
6233
6422
  * 从统一配置生成隐藏列列表
6234
6423
  */
@@ -6506,6 +6695,14 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
6506
6695
  const [exporting, setExporting] = React.useState(false);
6507
6696
  const getSelectedRowsRef = React.useRef(null);
6508
6697
  const dynamicFilterOptions = useDynamicFilterOptions(resolvedFields);
6698
+ const hiddenColumns = React.useMemo(() => buildHiddenColumns(resolvedFields, tableConfig?.hidden, denyFields, tableConfig?.overrides), [
6699
+ resolvedFields,
6700
+ tableConfig?.hidden,
6701
+ denyFields,
6702
+ tableConfig?.overrides
6703
+ ]);
6704
+ const dynamicResolveOptions = useDynamicResolveOptions(resolvedFields, resource.tableData.data, hiddenColumns);
6705
+ const dynamicOptionsByField = React.useMemo(() => mergeOptionsByField(dynamicFilterOptions.labelOptionsByField, dynamicResolveOptions.optionsByField), [dynamicFilterOptions.labelOptionsByField, dynamicResolveOptions.optionsByField]);
6509
6706
  const resourceIdKey = resource.idKey ?? "id";
6510
6707
  const actionRegistryVersion = useCrudActionsVersion();
6511
6708
  const unifiedActions = React.useMemo(() => {
@@ -6539,28 +6736,10 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
6539
6736
  const handleSelectedRowsChange = React.useCallback((rows) => {
6540
6737
  setSelectedRows((currentRows) => haveSameRowSelection(currentRows, rows, resourceIdKey) ? currentRows : rows);
6541
6738
  }, [resourceIdKey]);
6542
- const ownerCreateAction = React.useMemo(() => readCreateAction(ownerToolbarActions), [ownerToolbarActions]);
6543
- const toolbarOpenCreate = can.create ? ownerCreateAction?.onClick ?? onCreate ?? resource.handlers.openCreate : void 0;
6544
- const toolbarContext = React.useMemo(() => ({
6545
- crudId: id ?? "",
6546
- idKey: resourceIdKey,
6547
- rowIds,
6548
- selectedRowIds,
6549
- selectedCount,
6550
- isRefreshing: resource.tableData.isFetching,
6551
- ...resource.handlers.refresh ? { refresh: resource.handlers.refresh } : {},
6552
- ...toolbarOpenCreate ? { openCreate: toolbarOpenCreate } : {}
6553
- }), [
6554
- id,
6555
- resourceIdKey,
6556
- rowIds,
6557
- selectedRowIds,
6558
- selectedCount,
6559
- resource.handlers.refresh,
6560
- resource.tableData.isFetching,
6561
- toolbarOpenCreate
6562
- ]);
6563
- const resolvedToolbarActions = resolveToolbarActionsWithResolver(id, registryToolbarActions, toolbarContext);
6739
+ const ownerRefreshAction = React.useMemo(() => readToolbarAction(ownerToolbarActions, "refresh"), [ownerToolbarActions]);
6740
+ const ownerImportAction = React.useMemo(() => readToolbarAction(ownerToolbarActions, "import"), [ownerToolbarActions]);
6741
+ const ownerExportAction = React.useMemo(() => readToolbarAction(ownerToolbarActions, "export"), [ownerToolbarActions]);
6742
+ const ownerCreateAction = React.useMemo(() => readToolbarAction(ownerToolbarActions, "create"), [ownerToolbarActions]);
6564
6743
  const importColumns = React.useMemo(() => {
6565
6744
  const shape = resolvedSchema.shape;
6566
6745
  const denySet = new Set([...DEFAULT_IMPORT_MANAGED_FIELDS, ...denyFields ?? []]);
@@ -6597,10 +6776,66 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
6597
6776
  setExporting(false);
6598
6777
  }
6599
6778
  }, [selectedCount, handleExport]);
6600
- const tableOverrides = React.useMemo(() => buildTableOverrides(resolvedFields, tableConfig?.overrides, dynamicFilterOptions), [
6779
+ const toolbarRefresh = React.useMemo(() => {
6780
+ const refresh = ownerRefreshAction?.onClick ?? resource.handlers.refresh;
6781
+ if (!refresh) return void 0;
6782
+ return async () => refresh();
6783
+ }, [ownerRefreshAction?.onClick, resource.handlers.refresh]);
6784
+ const toolbarOpenImport = React.useMemo(() => {
6785
+ if (!canImport) return void 0;
6786
+ return ownerImportAction?.onClick ?? (() => setImportOpen(true));
6787
+ }, [canImport, ownerImportAction?.onClick]);
6788
+ const toolbarExportData = React.useMemo(() => {
6789
+ if (!canExport) return void 0;
6790
+ const exportData = ownerExportAction?.onClick ?? handleExportClick;
6791
+ return async () => exportData();
6792
+ }, [
6793
+ canExport,
6794
+ handleExportClick,
6795
+ ownerExportAction?.onClick
6796
+ ]);
6797
+ const toolbarOpenCreate = React.useMemo(() => {
6798
+ if (!can.create) return void 0;
6799
+ return ownerCreateAction?.onClick ?? onCreate ?? resource.handlers.openCreate;
6800
+ }, [
6801
+ can.create,
6802
+ onCreate,
6803
+ ownerCreateAction?.onClick,
6804
+ resource.handlers.openCreate
6805
+ ]);
6806
+ const toolbarContext = React.useMemo(() => ({
6807
+ crudId: id ?? "",
6808
+ idKey: resourceIdKey,
6809
+ rowIds,
6810
+ selectedRowIds,
6811
+ selectedCount,
6812
+ isRefreshing: resource.tableData.isFetching,
6813
+ isExporting: exporting,
6814
+ ...toolbarRefresh ? { refresh: toolbarRefresh } : {},
6815
+ ...toolbarOpenImport ? { openImport: toolbarOpenImport } : {},
6816
+ ...toolbarExportData ? { exportData: toolbarExportData } : {},
6817
+ ...toolbarOpenCreate ? { openCreate: toolbarOpenCreate } : {}
6818
+ }), [
6819
+ id,
6820
+ resourceIdKey,
6821
+ rowIds,
6822
+ selectedRowIds,
6823
+ selectedCount,
6824
+ resource.tableData.isFetching,
6825
+ exporting,
6826
+ toolbarRefresh,
6827
+ toolbarOpenImport,
6828
+ toolbarExportData,
6829
+ toolbarOpenCreate
6830
+ ]);
6831
+ const resolvedToolbarActions = resolveToolbarActionsWithResolver(id, registryToolbarActions, toolbarContext);
6832
+ const tableOverrides = React.useMemo(() => mergeTableOverrides(buildTableOverrides(resolvedFields, tableConfig?.overrides, dynamicFilterOptions, dynamicResolveOptions), buildCapabilityTableOverrides(resolvedSchema, resource.capabilities)), [
6601
6833
  resolvedFields,
6602
6834
  tableConfig?.overrides,
6603
- dynamicFilterOptions
6835
+ dynamicFilterOptions,
6836
+ dynamicResolveOptions,
6837
+ resolvedSchema,
6838
+ resource.capabilities
6604
6839
  ]);
6605
6840
  const defaultSort = React.useMemo(() => resource.defaultSort ?? tableConfig?.defaultSort ?? getDefaultSortingForSchema(resolvedSchema), [
6606
6841
  resource.defaultSort,
@@ -6612,18 +6847,23 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
6612
6847
  formConfig?.overrides,
6613
6848
  denyFields
6614
6849
  ]);
6615
- const hiddenColumns = React.useMemo(() => buildHiddenColumns(resolvedFields, tableConfig?.hidden, denyFields, tableConfig?.overrides), [
6616
- resolvedFields,
6617
- tableConfig?.hidden,
6618
- denyFields,
6619
- tableConfig?.overrides
6620
- ]);
6621
6850
  const searchConfig = React.useMemo(() => {
6622
6851
  const tableSearch = tableConfig?.search;
6852
+ const searchCapability = resource.capabilities?.search;
6623
6853
  if (tableSearch === false) return false;
6854
+ if (searchCapability) {
6855
+ if (!searchCapability.enabled) return false;
6856
+ if (tableSearch && typeof tableSearch === "object") return tableSearch;
6857
+ return true;
6858
+ }
6624
6859
  if (tableSearch && typeof tableSearch === "object") return tableSearch;
6860
+ if (tableSearch === true) return true;
6625
6861
  return Object.values(resolvedFields).some((field) => field?.search === true) ? true : false;
6626
- }, [resolvedFields, tableConfig?.search]);
6862
+ }, [
6863
+ resolvedFields,
6864
+ resource.capabilities?.search,
6865
+ tableConfig?.search
6866
+ ]);
6627
6867
  const batchFields = React.useMemo(() => buildBatchUpdateFields(resolvedSchema, tableConfig?.batchFields, resolvedFields), [
6628
6868
  resolvedSchema,
6629
6869
  tableConfig?.batchFields,
@@ -6678,7 +6918,7 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
6678
6918
  children: (() => {
6679
6919
  const renderBuiltinButton = (type, overrides) => {
6680
6920
  if (type === "refresh") {
6681
- const onRefresh = overrides?.onClick ?? resource.handlers.refresh;
6921
+ const onRefresh = overrides?.onClick ?? toolbarRefresh;
6682
6922
  const label = overrides?.label ?? locale.toolbar.refresh;
6683
6923
  if (!onRefresh) return null;
6684
6924
  return /* @__PURE__ */ jsx(Button, {
@@ -6694,18 +6934,18 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
6694
6934
  if (type === "import" && canImport) return /* @__PURE__ */ jsxs(Button, {
6695
6935
  variant: "outline",
6696
6936
  size: "sm",
6697
- onClick: overrides?.onClick ?? (() => setImportOpen(true)),
6937
+ onClick: overrides?.onClick ?? toolbarOpenImport,
6698
6938
  children: [/* @__PURE__ */ jsx(Download, { className: "mr-2 h-4 w-4" }), overrides?.label ?? locale.toolbar.import]
6699
6939
  }, "import");
6700
6940
  if (type === "export" && canExport) return /* @__PURE__ */ jsxs(Button, {
6701
6941
  variant: "outline",
6702
6942
  size: "sm",
6703
- onClick: overrides?.onClick ?? handleExportClick,
6943
+ onClick: overrides?.onClick ?? toolbarExportData,
6704
6944
  disabled: exporting || selectedCount === 0 && !resource.handlers.export,
6705
6945
  children: [exporting ? /* @__PURE__ */ jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Upload, { className: "mr-2 h-4 w-4" }), overrides?.label ?? (selectedCount > 0 ? locale.toolbar.exportSelected(selectedCount) : locale.toolbar.export)]
6706
6946
  }, "export");
6707
6947
  if (type === "create" && can.create) return /* @__PURE__ */ jsx(Button, {
6708
- onClick: overrides?.onClick ?? onCreate ?? resource.handlers.openCreate,
6948
+ onClick: overrides?.onClick ?? toolbarOpenCreate,
6709
6949
  children: overrides?.label ?? locale.toolbar.create
6710
6950
  }, "create");
6711
6951
  return null;
@@ -6793,7 +7033,7 @@ function AutoCrudTable({ id, title, description, schema, resource, fields, table
6793
7033
  data: resource.modal.selected,
6794
7034
  schema: resolvedSchema,
6795
7035
  fields: resolvedFields,
6796
- dynamicOptions: dynamicFilterOptions.optionsByField,
7036
+ dynamicOptions: dynamicOptionsByField,
6797
7037
  denyFields,
6798
7038
  locale
6799
7039
  }),
@@ -7576,6 +7816,7 @@ function useAutoCrudResource({ router, schema, query: queryTransform, options =
7576
7816
  const resolvedSchema = useMemo(() => mergeMetadataSchema(schema, metadata?.schema), [schema, metadata?.schema]);
7577
7817
  const defaultSorting = useMemo(() => defaultSortOption === false ? [] : defaultSortOption ?? getDefaultSortingForSchema(resolvedSchema), [defaultSortOption, resolvedSchema]);
7578
7818
  const metadataFields = useMemo(() => readMetadataFields(metadata?.fields), [metadata?.fields]);
7819
+ const capabilities = metadata?.capabilities;
7579
7820
  const columns = useMemo(() => createTableSchema(resolvedSchema), [resolvedSchema]);
7580
7821
  const [urlParams] = useQueryStates({
7581
7822
  page: parseAsInteger.withDefault(1),
@@ -7868,6 +8109,7 @@ function useAutoCrudResource({ router, schema, query: queryTransform, options =
7868
8109
  },
7869
8110
  schema: resolvedSchema,
7870
8111
  fields: metadataFields,
8112
+ capabilities,
7871
8113
  defaultSort: defaultSorting,
7872
8114
  modal,
7873
8115
  mutations: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wordrhyme/auto-crud",
3
3
  "type": "module",
4
- "version": "1.3.8",
4
+ "version": "1.4.0",
5
5
  "description": "Schema-first CRUD components with auto-generated tables and forms",
6
6
  "author": "wordrhyme",
7
7
  "license": "MIT",
@@ -67,6 +67,8 @@
67
67
  "@wordrhyme/shadcn-ui": "1.32.4"
68
68
  },
69
69
  "devDependencies": {
70
+ "@storybook/react": "^8.6.18",
71
+ "@storybook/test": "^8.6.18",
70
72
  "@tanstack/react-query": "^5.90.15",
71
73
  "@testing-library/jest-dom": "^6.9.1",
72
74
  "@testing-library/react": "^16.3.2",
@@ -84,9 +86,9 @@
84
86
  "vitest": "^3.2.4",
85
87
  "@internal/eslint-config": "0.3.0",
86
88
  "@internal/prettier-config": "0.0.1",
89
+ "@internal/vitest-config": "0.1.0",
87
90
  "@internal/tsconfig": "0.1.0",
88
- "@internal/tsdown-config": "0.1.0",
89
- "@internal/vitest-config": "0.1.0"
91
+ "@internal/tsdown-config": "0.1.0"
90
92
  },
91
93
  "prettier": "@internal/prettier-config",
92
94
  "scripts": {