@afurgeri/crud-vue 0.1.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.
Files changed (95) hide show
  1. package/package.json +46 -0
  2. package/src/__test__/CrudCards.test.ts +118 -0
  3. package/src/__test__/CrudEmptyState.test.ts +82 -0
  4. package/src/__test__/CrudForm.test.ts +101 -0
  5. package/src/__test__/CrudShow.test.ts +135 -0
  6. package/src/__test__/CrudTable.test.ts +111 -0
  7. package/src/__test__/CrudToolbar.test.ts +102 -0
  8. package/src/__test__/setup.ts +43 -0
  9. package/src/__test__/useCrud.test.ts +349 -0
  10. package/src/components/CrudCards.vue +105 -0
  11. package/src/components/CrudDeleteDialog.vue +80 -0
  12. package/src/components/CrudEmptyState.vue +58 -0
  13. package/src/components/CrudFilters.vue +194 -0
  14. package/src/components/CrudForm.vue +232 -0
  15. package/src/components/CrudPage.vue +206 -0
  16. package/src/components/CrudPagination.vue +130 -0
  17. package/src/components/CrudSearch.vue +42 -0
  18. package/src/components/CrudShow.vue +216 -0
  19. package/src/components/CrudTable.vue +146 -0
  20. package/src/components/CrudToolbar.vue +86 -0
  21. package/src/components/InputError.vue +13 -0
  22. package/src/components/ui/button/Button.vue +27 -0
  23. package/src/components/ui/button/index.ts +36 -0
  24. package/src/components/ui/card/Card.vue +22 -0
  25. package/src/components/ui/card/CardAction.vue +17 -0
  26. package/src/components/ui/card/CardContent.vue +17 -0
  27. package/src/components/ui/card/CardDescription.vue +17 -0
  28. package/src/components/ui/card/CardFooter.vue +17 -0
  29. package/src/components/ui/card/CardHeader.vue +17 -0
  30. package/src/components/ui/card/CardTitle.vue +17 -0
  31. package/src/components/ui/card/index.ts +7 -0
  32. package/src/components/ui/checkbox/Checkbox.vue +37 -0
  33. package/src/components/ui/checkbox/index.ts +1 -0
  34. package/src/components/ui/combobox/ComboboxInput.vue +83 -0
  35. package/src/components/ui/combobox/index.ts +1 -0
  36. package/src/components/ui/dialog/Dialog.vue +17 -0
  37. package/src/components/ui/dialog/DialogClose.vue +14 -0
  38. package/src/components/ui/dialog/DialogContent.vue +49 -0
  39. package/src/components/ui/dialog/DialogDescription.vue +25 -0
  40. package/src/components/ui/dialog/DialogFooter.vue +15 -0
  41. package/src/components/ui/dialog/DialogHeader.vue +17 -0
  42. package/src/components/ui/dialog/DialogOverlay.vue +23 -0
  43. package/src/components/ui/dialog/DialogScrollContent.vue +59 -0
  44. package/src/components/ui/dialog/DialogTitle.vue +25 -0
  45. package/src/components/ui/dialog/DialogTrigger.vue +14 -0
  46. package/src/components/ui/dialog/index.ts +10 -0
  47. package/src/components/ui/dropdown-menu/DropdownMenu.vue +17 -0
  48. package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +41 -0
  49. package/src/components/ui/dropdown-menu/DropdownMenuContent.vue +39 -0
  50. package/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +14 -0
  51. package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +30 -0
  52. package/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +22 -0
  53. package/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +22 -0
  54. package/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +42 -0
  55. package/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +26 -0
  56. package/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +17 -0
  57. package/src/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
  58. package/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +31 -0
  59. package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +30 -0
  60. package/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +16 -0
  61. package/src/components/ui/dropdown-menu/index.ts +16 -0
  62. package/src/components/ui/input/Input.vue +33 -0
  63. package/src/components/ui/input/index.ts +1 -0
  64. package/src/components/ui/label/Label.vue +28 -0
  65. package/src/components/ui/label/index.ts +1 -0
  66. package/src/components/ui/select/Select.vue +15 -0
  67. package/src/components/ui/select/SelectContent.vue +49 -0
  68. package/src/components/ui/select/SelectGroup.vue +17 -0
  69. package/src/components/ui/select/SelectItem.vue +41 -0
  70. package/src/components/ui/select/SelectItemText.vue +12 -0
  71. package/src/components/ui/select/SelectLabel.vue +14 -0
  72. package/src/components/ui/select/SelectScrollDownButton.vue +22 -0
  73. package/src/components/ui/select/SelectScrollUpButton.vue +22 -0
  74. package/src/components/ui/select/SelectSeparator.vue +15 -0
  75. package/src/components/ui/select/SelectTrigger.vue +29 -0
  76. package/src/components/ui/select/SelectValue.vue +12 -0
  77. package/src/components/ui/select/index.ts +11 -0
  78. package/src/components/ui/separator/Separator.vue +28 -0
  79. package/src/components/ui/separator/index.ts +1 -0
  80. package/src/components/ui/spinner/Spinner.vue +17 -0
  81. package/src/components/ui/spinner/index.ts +1 -0
  82. package/src/components/ui/table/Table.vue +16 -0
  83. package/src/components/ui/table/TableBody.vue +14 -0
  84. package/src/components/ui/table/TableCaption.vue +14 -0
  85. package/src/components/ui/table/TableCell.vue +21 -0
  86. package/src/components/ui/table/TableEmpty.vue +34 -0
  87. package/src/components/ui/table/TableFooter.vue +14 -0
  88. package/src/components/ui/table/TableHead.vue +14 -0
  89. package/src/components/ui/table/TableHeader.vue +14 -0
  90. package/src/components/ui/table/TableRow.vue +14 -0
  91. package/src/components/ui/table/index.ts +9 -0
  92. package/src/composables/useCrud.ts +328 -0
  93. package/src/index.ts +33 -0
  94. package/src/lib/utils.ts +18 -0
  95. package/src/types/crud.ts +133 -0
@@ -0,0 +1,328 @@
1
+ import { ref, computed, onUnmounted, type Ref, type ComputedRef } from 'vue';
2
+ import { router } from '@inertiajs/vue3';
3
+ import type { CrudFilter, FilterDocument, UseCrudOptions } from '../types/crud';
4
+
5
+ export interface UseCrudReturn {
6
+ search: Ref<string>;
7
+ activeFilters: Ref<Record<string, unknown>>;
8
+ sortField: Ref<string>;
9
+ sortDirection: Ref<'asc' | 'desc'>;
10
+ hasActiveFilters: ComputedRef<boolean>;
11
+ filtersVisible: Ref<boolean>;
12
+ onSearchInput: (value: string) => void;
13
+ onFilterChange: (field: string, value: unknown) => void;
14
+ onSortToggle: (field: string) => void;
15
+ applyFilters: () => void;
16
+ clearFilters: () => void;
17
+ toggleFilters: () => void;
18
+ getSortParam: () => string;
19
+ }
20
+
21
+ export function buildFilters(state: Record<string, unknown>, config: CrudFilter[]): FilterDocument {
22
+ const result: FilterDocument = {};
23
+
24
+ for (const filter of config) {
25
+ const value = state[filter.field];
26
+ if (value === undefined || value === null || value === '') continue;
27
+
28
+ switch (filter.type) {
29
+ case 'text':
30
+ result['like'] = { ...(result['like'] as Record<string, unknown> ?? {}), [filter.field]: value };
31
+ break;
32
+ case 'select':
33
+ result['eq'] = { ...(result['eq'] as Record<string, unknown> ?? {}), [filter.field]: value };
34
+ break;
35
+ case 'number': {
36
+ const numValue = value as { min?: number; max?: number };
37
+ if (numValue.min !== undefined && numValue.min !== null && numValue.min !== '') {
38
+ result['gte'] = { ...(result['gte'] as Record<string, unknown> ?? {}), [filter.field]: Number(numValue.min) };
39
+ }
40
+ if (numValue.max !== undefined && numValue.max !== null && numValue.max !== '') {
41
+ result['lte'] = { ...(result['lte'] as Record<string, unknown> ?? {}), [filter.field]: Number(numValue.max) };
42
+ }
43
+ break;
44
+ }
45
+ case 'date': {
46
+ const dateValue = value as { from?: string; to?: string };
47
+ if (dateValue.from) {
48
+ result['date_gte'] = { ...(result['date_gte'] as Record<string, unknown> ?? {}), [filter.field]: dateValue.from };
49
+ }
50
+ if (dateValue.to) {
51
+ result['date_lte'] = { ...(result['date_lte'] as Record<string, unknown> ?? {}), [filter.field]: dateValue.to };
52
+ }
53
+ break;
54
+ }
55
+ }
56
+ }
57
+
58
+ return result;
59
+ }
60
+
61
+ function hasAnyFilterValue(state: Record<string, unknown>): boolean {
62
+ return Object.values(state).some((v) => {
63
+ if (v === undefined || v === null || v === '') return false;
64
+ if (typeof v === 'object' && !Array.isArray(v)) {
65
+ return Object.values(v as Record<string, unknown>).some(
66
+ (inner) => inner !== undefined && inner !== null && inner !== '',
67
+ );
68
+ }
69
+ return true;
70
+ });
71
+ }
72
+
73
+ export function useCrud(
74
+ filterConfig: CrudFilter[],
75
+ searchFields: string[],
76
+ itemsPropName?: string,
77
+ defaultSort?: string,
78
+ options?: UseCrudOptions,
79
+ ): UseCrudReturn {
80
+ const search = ref('');
81
+ const activeFilters = ref<Record<string, unknown>>({});
82
+ const sortField = ref('');
83
+ const sortDirection = ref<'asc' | 'desc'>('asc');
84
+ const filtersVisible = ref(false);
85
+ let searchTimeout: ReturnType<typeof setTimeout>;
86
+ let isDisposed = false;
87
+
88
+ const debounceMs = options?.searchDebounce ?? 300;
89
+
90
+ // Initialize from URL on mount
91
+ function initFromUrl() {
92
+ const params = new URLSearchParams(window.location.search);
93
+
94
+ search.value = params.get('search') || '';
95
+
96
+ const filtersParam = params.get('filters');
97
+ if (filtersParam) {
98
+ try {
99
+ const parsed = JSON.parse(decodeURIComponent(filtersParam));
100
+ // Reconstruct active filter state from the structured filters
101
+ for (const filter of filterConfig) {
102
+ let value: unknown = undefined;
103
+
104
+ switch (filter.type) {
105
+ case 'text':
106
+ value = parsed['like']?.[filter.field] ?? '';
107
+ break;
108
+ case 'select':
109
+ value = parsed['eq']?.[filter.field] ?? '';
110
+ break;
111
+ case 'number':
112
+ value = {
113
+ min: parsed['gte']?.[filter.field] ?? '',
114
+ max: parsed['lte']?.[filter.field] ?? '',
115
+ };
116
+ if (!(value as { min: unknown; max: unknown }).min && !(value as { min: unknown; max: unknown }).max) {
117
+ value = undefined;
118
+ }
119
+ break;
120
+ case 'date':
121
+ value = {
122
+ from: parsed['date_gte']?.[filter.field] ?? '',
123
+ to: parsed['date_lte']?.[filter.field] ?? '',
124
+ };
125
+ if (!(value as { from: unknown; to: unknown }).from && !(value as { from: unknown; to: unknown }).to) {
126
+ value = undefined;
127
+ }
128
+ break;
129
+ }
130
+
131
+ if (value !== undefined) {
132
+ activeFilters.value[filter.field] = value;
133
+ }
134
+ }
135
+ } catch (e) {
136
+ // Call onError hook if provided
137
+ options?.onError?.(e);
138
+ }
139
+ }
140
+
141
+ const sortParam = params.get('sort');
142
+ if (sortParam) {
143
+ if (sortParam.startsWith('-')) {
144
+ sortField.value = sortParam.slice(1);
145
+ sortDirection.value = 'desc';
146
+ } else {
147
+ sortField.value = sortParam;
148
+ sortDirection.value = 'asc';
149
+ }
150
+ } else if (defaultSort) {
151
+ if (defaultSort.startsWith('-')) {
152
+ sortField.value = defaultSort.slice(1);
153
+ sortDirection.value = 'desc';
154
+ } else {
155
+ sortField.value = defaultSort;
156
+ sortDirection.value = 'asc';
157
+ }
158
+ }
159
+ }
160
+
161
+ async function reloadWithParams(replace = true) {
162
+ if (isDisposed) return;
163
+
164
+ const url = new URL(window.location.href);
165
+
166
+ // Search
167
+ if (search.value) {
168
+ url.searchParams.set('search', search.value);
169
+ } else {
170
+ url.searchParams.delete('search');
171
+ }
172
+
173
+ // Filters
174
+ const filterBuilder = options?.buildCustomFilters ?? buildFilters;
175
+ const filters = filterBuilder(activeFilters.value, filterConfig);
176
+ if (Object.keys(filters).length > 0) {
177
+ url.searchParams.set('filters', encodeURIComponent(JSON.stringify(filters)));
178
+ } else {
179
+ url.searchParams.delete('filters');
180
+ }
181
+
182
+ // Sort
183
+ if (sortField.value) {
184
+ url.searchParams.set('sort', getSortParam());
185
+ } else {
186
+ url.searchParams.delete('sort');
187
+ }
188
+
189
+ // Reset to page 1 when filters/search/sort changes
190
+ url.searchParams.delete('page');
191
+
192
+ // onBeforeReload hook
193
+ if (options?.onBeforeReload) {
194
+ const hookResult = options.onBeforeReload(url.searchParams);
195
+
196
+ // Handle Promise (async) case
197
+ const resolved = hookResult instanceof Promise ? await hookResult : hookResult;
198
+
199
+ // Cancel navigation if hook returns false
200
+ if (resolved === false) return;
201
+
202
+ // If hook returns a DIFFERENT URLSearchParams object, replace the URL's params.
203
+ // (When the hook mutates url.searchParams in-place and returns the same reference,
204
+ // the mutations are already applied — no copy needed.)
205
+ if (resolved instanceof URLSearchParams && resolved !== url.searchParams) {
206
+ for (const key of [...url.searchParams.keys()]) {
207
+ url.searchParams.delete(key);
208
+ }
209
+ resolved.forEach((value, key) => {
210
+ url.searchParams.set(key, value);
211
+ });
212
+ }
213
+ }
214
+
215
+ const routerOptions: Record<string, unknown> = {
216
+ preserveState: true,
217
+ preserveScroll: false,
218
+ replace,
219
+ };
220
+
221
+ if (itemsPropName) {
222
+ routerOptions['only'] = [itemsPropName];
223
+ }
224
+
225
+ router.get(url.pathname + url.search, {}, routerOptions);
226
+ }
227
+
228
+ function onSearchInput(value: string) {
229
+ search.value = value;
230
+ clearTimeout(searchTimeout);
231
+ if (!isDisposed) {
232
+ if (debounceMs === 0) {
233
+ reloadWithParams();
234
+ } else {
235
+ searchTimeout = setTimeout(() => reloadWithParams(), debounceMs);
236
+ }
237
+ }
238
+ }
239
+
240
+ function onFilterChange(field: string, value: unknown) {
241
+ activeFilters.value[field] = value;
242
+ }
243
+
244
+ function onSortToggle(field: string) {
245
+ if (sortField.value === field) {
246
+ if (sortDirection.value === 'asc') {
247
+ sortDirection.value = 'desc';
248
+ } else {
249
+ sortField.value = '';
250
+ sortDirection.value = 'asc';
251
+ }
252
+ } else {
253
+ sortField.value = field;
254
+ sortDirection.value = 'asc';
255
+ }
256
+ reloadWithParams();
257
+ }
258
+
259
+ function applyFilters() {
260
+ reloadWithParams();
261
+ }
262
+
263
+ function clearFilters() {
264
+ search.value = '';
265
+ activeFilters.value = {};
266
+ sortField.value = '';
267
+ sortDirection.value = 'asc';
268
+ filtersVisible.value = false;
269
+
270
+ const url = new URL(window.location.href);
271
+ url.search = '';
272
+
273
+ const routerOptions: Record<string, unknown> = {
274
+ preserveState: true,
275
+ preserveScroll: false,
276
+ replace: true,
277
+ };
278
+
279
+ if (itemsPropName) {
280
+ routerOptions['only'] = [itemsPropName];
281
+ }
282
+
283
+ router.get(url.pathname, {}, routerOptions);
284
+ }
285
+
286
+ function toggleFilters() {
287
+ filtersVisible.value = !filtersVisible.value;
288
+ }
289
+
290
+ function getSortParam(): string {
291
+ if (!sortField.value) return '';
292
+ return sortDirection.value === 'desc' ? `-${sortField.value}` : sortField.value;
293
+ }
294
+
295
+ const hasActiveFilters = computed(() => {
296
+ return search.value !== '' || hasAnyFilterValue(activeFilters.value);
297
+ });
298
+
299
+ // Init
300
+ initFromUrl();
301
+
302
+ // If defaultSort was applied (not from URL), sync it to the URL on first load
303
+ if (defaultSort && !new URLSearchParams(window.location.search).get('sort')) {
304
+ // Delay to avoid blocking initial render
305
+ setTimeout(() => reloadWithParams(true), 0);
306
+ }
307
+
308
+ onUnmounted(() => {
309
+ isDisposed = true;
310
+ clearTimeout(searchTimeout);
311
+ });
312
+
313
+ return {
314
+ search,
315
+ activeFilters,
316
+ sortField,
317
+ sortDirection,
318
+ hasActiveFilters,
319
+ filtersVisible,
320
+ onSearchInput,
321
+ onFilterChange,
322
+ onSortToggle,
323
+ applyFilters,
324
+ clearFilters,
325
+ toggleFilters,
326
+ getSortParam,
327
+ };
328
+ }
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ export { default as CrudPage } from './components/CrudPage.vue';
2
+ export { default as CrudTable } from './components/CrudTable.vue';
3
+ export { default as CrudCards } from './components/CrudCards.vue';
4
+ export { default as CrudFilters } from './components/CrudFilters.vue';
5
+ export { default as CrudPagination } from './components/CrudPagination.vue';
6
+ export { default as CrudSearch } from './components/CrudSearch.vue';
7
+ export { default as CrudDeleteDialog } from './components/CrudDeleteDialog.vue';
8
+ export { default as CrudEmptyState } from './components/CrudEmptyState.vue';
9
+ export { default as CrudToolbar } from './components/CrudToolbar.vue';
10
+ export { default as CrudForm } from './components/CrudForm.vue';
11
+ export { default as CrudShow } from './components/CrudShow.vue';
12
+
13
+ export { useCrud, buildFilters } from './composables/useCrud';
14
+ export type { UseCrudReturn } from './composables/useCrud';
15
+
16
+ export type {
17
+ CrudColumn,
18
+ CrudFilter,
19
+ CrudFeatures,
20
+ CrudFeatureCreate,
21
+ CrudFeatureSearch,
22
+ CrudCardConfig,
23
+ CrudCardMeta,
24
+ CrudPageProps,
25
+ CrudFormField,
26
+ CrudShowField,
27
+ CrudShowSection,
28
+ CrudShowSidebarItem,
29
+ Paginator,
30
+ PaginatorLink,
31
+ FilterDocument,
32
+ UseCrudOptions,
33
+ } from './types/crud';
@@ -0,0 +1,18 @@
1
+ import { InertiaLinkProps } from '@inertiajs/vue3';
2
+ import { clsx, type ClassValue } from 'clsx';
3
+ import { twMerge } from 'tailwind-merge';
4
+
5
+ export function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs));
7
+ }
8
+
9
+ export function urlIsActive(
10
+ urlToCheck: NonNullable<InertiaLinkProps['href']>,
11
+ currentUrl: string,
12
+ ) {
13
+ return toUrl(urlToCheck) === currentUrl;
14
+ }
15
+
16
+ export function toUrl(href: NonNullable<InertiaLinkProps['href']>) {
17
+ return typeof href === 'string' ? href : href?.url;
18
+ }
@@ -0,0 +1,133 @@
1
+ export interface CrudColumn {
2
+ key: string;
3
+ title: string;
4
+ sortable?: boolean;
5
+ visible?: boolean;
6
+ width?: string;
7
+ align?: 'left' | 'center' | 'right';
8
+ }
9
+
10
+ export interface CrudFilter {
11
+ field: string;
12
+ label: string;
13
+ type: 'text' | 'select' | 'number' | 'date';
14
+ operator?: string;
15
+ options?: { label: string; value: string | number }[];
16
+ placeholder?: string;
17
+ }
18
+
19
+ export interface CrudFeatureCreate {
20
+ url: string;
21
+ label?: string;
22
+ }
23
+
24
+ export interface CrudFeatureSearch {
25
+ fields: string[];
26
+ placeholder?: string;
27
+ }
28
+
29
+ export interface CrudFeatures {
30
+ create?: boolean | CrudFeatureCreate;
31
+ show?: boolean;
32
+ edit?: boolean;
33
+ delete?: boolean;
34
+ search?: boolean | CrudFeatureSearch;
35
+ filters?: boolean | CrudFilter[];
36
+ sort?: boolean;
37
+ pagination?: boolean;
38
+ defaultSort?: string;
39
+ }
40
+
41
+ export interface CrudCardMeta {
42
+ label: string;
43
+ value: (item: Record<string, unknown>) => string;
44
+ }
45
+
46
+ export interface CrudCardConfig {
47
+ title: (item: Record<string, unknown>) => string;
48
+ subtitle?: (item: Record<string, unknown>) => string;
49
+ meta?: CrudCardMeta[];
50
+ }
51
+
52
+ export interface PaginatorLink {
53
+ url: string | null;
54
+ label: string;
55
+ active: boolean;
56
+ }
57
+
58
+ export interface Paginator<T = Record<string, unknown>> {
59
+ data: T[];
60
+ links: PaginatorLink[];
61
+ from: number | null;
62
+ to: number | null;
63
+ total: number;
64
+ current_page: number;
65
+ last_page: number;
66
+ per_page: number;
67
+ }
68
+
69
+ export interface CrudPageProps {
70
+ title: string;
71
+ description?: string;
72
+ items: Paginator;
73
+ columns: CrudColumn[];
74
+ features?: CrudFeatures;
75
+ card?: CrudCardConfig;
76
+ itemsPropName?: string;
77
+ }
78
+
79
+ export interface CrudFormField {
80
+ name: string;
81
+ label: string;
82
+ type: 'text' | 'email' | 'tel' | 'number' | 'select' | 'combobox' | 'textarea' | 'switch' | 'date' | 'password';
83
+ placeholder?: string;
84
+ required?: boolean;
85
+ options?: { label: string; value: string | number }[];
86
+ span?: 1 | 2;
87
+ tabindex?: number;
88
+ disabled?: boolean;
89
+ }
90
+
91
+ export interface CrudShowField {
92
+ label: string;
93
+ key?: string;
94
+ value?: unknown;
95
+ type?: 'text' | 'badge' | 'money' | 'email' | 'phone';
96
+ badgeClass?: string;
97
+ fallback?: string;
98
+ }
99
+
100
+ export interface CrudShowSection {
101
+ title: string;
102
+ icon?: string;
103
+ fields: CrudShowField[];
104
+ cols?: 1 | 2;
105
+ }
106
+
107
+ export interface CrudShowSidebarItem {
108
+ label: string;
109
+ key?: string;
110
+ value?: unknown;
111
+ type?: 'text' | 'badge';
112
+ badgeClass?: string;
113
+ }
114
+
115
+ export interface FilterDocument {
116
+ [operator: string]: Record<string, unknown>;
117
+ }
118
+
119
+ export interface UseCrudOptions {
120
+ onBeforeReload?: (
121
+ params: URLSearchParams,
122
+ ) =>
123
+ | URLSearchParams
124
+ | false
125
+ | void
126
+ | Promise<URLSearchParams | false | void>;
127
+ onError?: (error: unknown) => void;
128
+ buildCustomFilters?: (
129
+ state: Record<string, unknown>,
130
+ config: CrudFilter[],
131
+ ) => FilterDocument;
132
+ searchDebounce?: number;
133
+ }