@ram_28/kf-ai-sdk 1.0.19 → 1.0.20
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 +45 -12
- package/dist/components/hooks/useFilter/types.d.ts +14 -11
- package/dist/components/hooks/useFilter/types.d.ts.map +1 -1
- package/dist/components/hooks/useFilter/useFilter.d.ts +1 -1
- package/dist/components/hooks/useFilter/useFilter.d.ts.map +1 -1
- package/dist/components/hooks/useForm/apiClient.d.ts.map +1 -1
- package/dist/components/hooks/useForm/useForm.d.ts.map +1 -1
- package/dist/components/hooks/useKanban/types.d.ts +5 -22
- package/dist/components/hooks/useKanban/types.d.ts.map +1 -1
- package/dist/components/hooks/useKanban/useKanban.d.ts.map +1 -1
- package/dist/components/hooks/useTable/types.d.ts +19 -31
- package/dist/components/hooks/useTable/types.d.ts.map +1 -1
- package/dist/components/hooks/useTable/useTable.d.ts.map +1 -1
- package/dist/error-handling-CAoD0Kwb.cjs +1 -0
- package/dist/error-handling-CrhTtD88.js +14 -0
- package/dist/filter.cjs +1 -1
- package/dist/filter.mjs +1 -1
- package/dist/form.cjs +1 -1
- package/dist/form.mjs +338 -327
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/kanban.cjs +2 -2
- package/dist/kanban.mjs +332 -322
- package/dist/table.cjs +1 -1
- package/dist/table.mjs +113 -96
- package/dist/table.types.d.ts +1 -1
- package/dist/table.types.d.ts.map +1 -1
- package/dist/types/common.d.ts +26 -6
- package/dist/types/common.d.ts.map +1 -1
- package/dist/useFilter-DzpP_ag0.cjs +1 -0
- package/dist/useFilter-H5bgAZQF.js +120 -0
- package/dist/utils/api/buildListOptions.d.ts +43 -0
- package/dist/utils/api/buildListOptions.d.ts.map +1 -0
- package/dist/utils/api/index.d.ts +2 -0
- package/dist/utils/api/index.d.ts.map +1 -0
- package/dist/utils/error-handling.d.ts +41 -0
- package/dist/utils/error-handling.d.ts.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/docs/QUICK_REFERENCE.md +142 -420
- package/docs/useAuth.md +52 -340
- package/docs/useFilter.md +858 -162
- package/docs/useForm.md +712 -501
- package/docs/useKanban.md +534 -279
- package/docs/useTable.md +725 -214
- package/package.json +1 -1
- package/sdk/components/hooks/useFilter/types.ts +14 -11
- package/sdk/components/hooks/useFilter/useFilter.ts +20 -18
- package/sdk/components/hooks/useForm/apiClient.ts +2 -1
- package/sdk/components/hooks/useForm/useForm.ts +35 -11
- package/sdk/components/hooks/useKanban/types.ts +7 -23
- package/sdk/components/hooks/useKanban/useKanban.ts +54 -18
- package/sdk/components/hooks/useTable/types.ts +26 -32
- package/sdk/components/hooks/useTable/useTable.llm.txt +8 -22
- package/sdk/components/hooks/useTable/useTable.ts +70 -25
- package/sdk/index.ts +154 -10
- package/sdk/table.types.ts +3 -0
- package/sdk/types/common.ts +31 -6
- package/sdk/utils/api/buildListOptions.ts +120 -0
- package/sdk/utils/api/index.ts +2 -0
- package/sdk/utils/error-handling.ts +150 -0
- package/sdk/utils/index.ts +6 -0
- package/dist/useFilter-Dofowpr_.cjs +0 -1
- package/dist/useFilter-Dv-mr9QW.js +0 -117
package/package.json
CHANGED
|
@@ -57,18 +57,21 @@ export interface ConditionGroupBuilder {
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Hook options (minimal configuration)
|
|
60
|
+
* Used for initializing useFilter, and also for initialState in useTable/useKanban
|
|
61
|
+
* @template T - Data type for type-safe field names (defaults to any)
|
|
60
62
|
*/
|
|
61
|
-
export interface UseFilterOptionsType {
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
|
|
63
|
+
export interface UseFilterOptionsType<T = any> {
|
|
64
|
+
/** Filter conditions */
|
|
65
|
+
conditions?: Array<ConditionType<T> | ConditionGroupType<T>>;
|
|
66
|
+
/** Operator for combining conditions (defaults to "And") */
|
|
67
|
+
operator?: ConditionGroupOperatorType;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
71
|
* Hook return interface with nested filter support
|
|
72
|
+
* @template T - Data type for type-safe field names (defaults to any)
|
|
70
73
|
*/
|
|
71
|
-
export interface UseFilterReturnType {
|
|
74
|
+
export interface UseFilterReturnType<T = any> {
|
|
72
75
|
// ============================================================
|
|
73
76
|
// STATE (read-only)
|
|
74
77
|
// ============================================================
|
|
@@ -77,10 +80,10 @@ export interface UseFilterReturnType {
|
|
|
77
80
|
operator: ConditionGroupOperatorType;
|
|
78
81
|
|
|
79
82
|
/** Current filter items (with id populated) */
|
|
80
|
-
items: Array<ConditionType | ConditionGroupType
|
|
83
|
+
items: Array<ConditionType<T> | ConditionGroupType<T>>;
|
|
81
84
|
|
|
82
85
|
/** Ready-to-use API payload (id stripped, undefined if no conditions) */
|
|
83
|
-
payload: FilterType | undefined;
|
|
86
|
+
payload: FilterType<T> | undefined;
|
|
84
87
|
|
|
85
88
|
/** Whether any conditions exist */
|
|
86
89
|
hasConditions: boolean;
|
|
@@ -95,7 +98,7 @@ export interface UseFilterReturnType {
|
|
|
95
98
|
* @param parentId - Optional id of the parent ConditionGroup. If omitted, adds at root level
|
|
96
99
|
* @returns The id of the created condition
|
|
97
100
|
*/
|
|
98
|
-
addCondition: (condition: Omit<ConditionType
|
|
101
|
+
addCondition: (condition: Omit<ConditionType<T>, "id">, parentId?: string) => string;
|
|
99
102
|
|
|
100
103
|
/**
|
|
101
104
|
* Add a condition group at root level or to a specific parent group
|
|
@@ -114,7 +117,7 @@ export interface UseFilterReturnType {
|
|
|
114
117
|
* @param id - The id of the condition to update
|
|
115
118
|
* @param updates - Partial updates to apply
|
|
116
119
|
*/
|
|
117
|
-
updateCondition: (id: string, updates: Partial<Omit<ConditionType
|
|
120
|
+
updateCondition: (id: string, updates: Partial<Omit<ConditionType<T>, "id">>) => void;
|
|
118
121
|
|
|
119
122
|
/**
|
|
120
123
|
* Update a condition group's operator by id
|
|
@@ -138,7 +141,7 @@ export interface UseFilterReturnType {
|
|
|
138
141
|
* @param id - The id to look up
|
|
139
142
|
* @returns The item or undefined if not found
|
|
140
143
|
*/
|
|
141
|
-
getCondition: (id: string) => ConditionType | ConditionGroupType | undefined;
|
|
144
|
+
getCondition: (id: string) => ConditionType<T> | ConditionGroupType<T> | undefined;
|
|
142
145
|
|
|
143
146
|
// ============================================================
|
|
144
147
|
// UTILITY
|
|
@@ -16,9 +16,11 @@ let idCounter = 0;
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Generate a unique ID for conditions and groups
|
|
19
|
+
* Uses timestamp + random component + counter to prevent collisions
|
|
19
20
|
*/
|
|
20
21
|
const generateId = (): string => {
|
|
21
|
-
|
|
22
|
+
const random = Math.random().toString(36).substring(2, 9);
|
|
23
|
+
return `filter_${Date.now()}_${random}_${++idCounter}`;
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -178,22 +180,22 @@ const addToParent = (
|
|
|
178
180
|
// USE FILTER HOOK - Nested Filter Support
|
|
179
181
|
// ============================================================
|
|
180
182
|
|
|
181
|
-
export function useFilter(options: UseFilterOptionsType = {}): UseFilterReturnType {
|
|
183
|
+
export function useFilter<T = any>(options: UseFilterOptionsType<T> = {}): UseFilterReturnType<T> {
|
|
182
184
|
// Initialize items with ids
|
|
183
|
-
const [items, setItems] = useState<Array<ConditionType | ConditionGroupType
|
|
184
|
-
cloneWithIds(options.
|
|
185
|
+
const [items, setItems] = useState<Array<ConditionType<T> | ConditionGroupType<T>>>(() =>
|
|
186
|
+
cloneWithIds(options.conditions || []) as Array<ConditionType<T> | ConditionGroupType<T>>
|
|
185
187
|
);
|
|
186
188
|
|
|
187
189
|
const [operator, setOperatorState] = useState<ConditionGroupOperatorType>(
|
|
188
|
-
options.
|
|
190
|
+
options.operator || "And"
|
|
189
191
|
);
|
|
190
192
|
|
|
191
193
|
// Build payload for API (strip ids)
|
|
192
|
-
const payload = useMemo((): FilterType | undefined => {
|
|
194
|
+
const payload = useMemo((): FilterType<T> | undefined => {
|
|
193
195
|
if (items.length === 0) return undefined;
|
|
194
196
|
return {
|
|
195
197
|
Operator: operator,
|
|
196
|
-
Condition: stripIds(items)
|
|
198
|
+
Condition: stripIds(items) as Array<ConditionType<T> | ConditionGroupType<T>>,
|
|
197
199
|
};
|
|
198
200
|
}, [items, operator]);
|
|
199
201
|
|
|
@@ -204,11 +206,11 @@ export function useFilter(options: UseFilterOptionsType = {}): UseFilterReturnTy
|
|
|
204
206
|
// ============================================================
|
|
205
207
|
|
|
206
208
|
const addCondition = useCallback(
|
|
207
|
-
(condition: Omit<ConditionType
|
|
209
|
+
(condition: Omit<ConditionType<T>, "id">, parentId?: string): string => {
|
|
208
210
|
const id = generateId();
|
|
209
|
-
const newCondition
|
|
211
|
+
const newCondition = { ...condition, id } as ConditionType<T>;
|
|
210
212
|
if (parentId) {
|
|
211
|
-
setItems((prev) => addToParent(prev, parentId, newCondition));
|
|
213
|
+
setItems((prev) => addToParent(prev, parentId, newCondition) as Array<ConditionType<T> | ConditionGroupType<T>>);
|
|
212
214
|
} else {
|
|
213
215
|
setItems((prev) => [...prev, newCondition]);
|
|
214
216
|
}
|
|
@@ -220,13 +222,13 @@ export function useFilter(options: UseFilterOptionsType = {}): UseFilterReturnTy
|
|
|
220
222
|
const addConditionGroup = useCallback(
|
|
221
223
|
(groupOperator: ConditionGroupOperatorType, parentId?: string): string => {
|
|
222
224
|
const id = generateId();
|
|
223
|
-
const newGroup: ConditionGroupType = {
|
|
225
|
+
const newGroup: ConditionGroupType<T> = {
|
|
224
226
|
id,
|
|
225
227
|
Operator: groupOperator,
|
|
226
228
|
Condition: [],
|
|
227
229
|
};
|
|
228
230
|
if (parentId) {
|
|
229
|
-
setItems((prev) => addToParent(prev, parentId, newGroup));
|
|
231
|
+
setItems((prev) => addToParent(prev, parentId, newGroup) as Array<ConditionType<T> | ConditionGroupType<T>>);
|
|
230
232
|
} else {
|
|
231
233
|
setItems((prev) => [...prev, newGroup]);
|
|
232
234
|
}
|
|
@@ -240,14 +242,14 @@ export function useFilter(options: UseFilterOptionsType = {}): UseFilterReturnTy
|
|
|
240
242
|
// ============================================================
|
|
241
243
|
|
|
242
244
|
const updateCondition = useCallback(
|
|
243
|
-
(id: string, updates: Partial<Omit<ConditionType
|
|
245
|
+
(id: string, updates: Partial<Omit<ConditionType<T>, "id">>): void => {
|
|
244
246
|
setItems((prev) =>
|
|
245
247
|
updateInTree(prev, id, (item) => {
|
|
246
248
|
if (!isConditionGroup(item)) {
|
|
247
249
|
return { ...item, ...updates };
|
|
248
250
|
}
|
|
249
251
|
return item;
|
|
250
|
-
})
|
|
252
|
+
}) as Array<ConditionType<T> | ConditionGroupType<T>>
|
|
251
253
|
);
|
|
252
254
|
},
|
|
253
255
|
[]
|
|
@@ -261,7 +263,7 @@ export function useFilter(options: UseFilterOptionsType = {}): UseFilterReturnTy
|
|
|
261
263
|
return { ...item, Operator: newOperator };
|
|
262
264
|
}
|
|
263
265
|
return item;
|
|
264
|
-
})
|
|
266
|
+
}) as Array<ConditionType<T> | ConditionGroupType<T>>
|
|
265
267
|
);
|
|
266
268
|
},
|
|
267
269
|
[]
|
|
@@ -272,12 +274,12 @@ export function useFilter(options: UseFilterOptionsType = {}): UseFilterReturnTy
|
|
|
272
274
|
// ============================================================
|
|
273
275
|
|
|
274
276
|
const removeCondition = useCallback((id: string): void => {
|
|
275
|
-
setItems((prev) => removeFromTree(prev, id));
|
|
277
|
+
setItems((prev) => removeFromTree(prev, id) as Array<ConditionType<T> | ConditionGroupType<T>>);
|
|
276
278
|
}, []);
|
|
277
279
|
|
|
278
280
|
const getCondition = useCallback(
|
|
279
|
-
(id: string): ConditionType | ConditionGroupType | undefined => {
|
|
280
|
-
return findById(items, id);
|
|
281
|
+
(id: string): ConditionType<T> | ConditionGroupType<T> | undefined => {
|
|
282
|
+
return findById(items, id) as ConditionType<T> | ConditionGroupType<T> | undefined;
|
|
281
283
|
},
|
|
282
284
|
[items]
|
|
283
285
|
);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { api, getBdoSchema } from "../../../api";
|
|
7
7
|
import type { BDOSchemaType, FormOperationType, SubmissionResultType } from "./types";
|
|
8
|
+
import { toError } from "../../../utils/error-handling";
|
|
8
9
|
|
|
9
10
|
// ============================================================
|
|
10
11
|
// SCHEMA FETCHING
|
|
@@ -47,7 +48,7 @@ export async function fetchFormSchemaWithRetry(
|
|
|
47
48
|
try {
|
|
48
49
|
return await fetchFormSchema(source);
|
|
49
50
|
} catch (error) {
|
|
50
|
-
lastError = error
|
|
51
|
+
lastError = toError(error);
|
|
51
52
|
|
|
52
53
|
if (attempt < maxRetries) {
|
|
53
54
|
// Wait before retrying (exponential backoff)
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import { api } from "../../../api";
|
|
30
30
|
|
|
31
31
|
import { validateCrossField } from "./expressionValidator.utils";
|
|
32
|
+
import { toError } from "../../../utils/error-handling";
|
|
32
33
|
import {
|
|
33
34
|
validateFieldOptimized,
|
|
34
35
|
getFieldDependencies,
|
|
@@ -184,11 +185,16 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
184
185
|
if (Object.keys(refFields).length > 0) {
|
|
185
186
|
fetchAllReferenceData(refFields)
|
|
186
187
|
.then(setReferenceData)
|
|
187
|
-
.catch(
|
|
188
|
+
.catch((err) => {
|
|
189
|
+
const error = toError(err);
|
|
190
|
+
console.warn("Failed to fetch reference data:", error);
|
|
191
|
+
// Notify via callback but don't block form - reference data is non-critical
|
|
192
|
+
onSchemaErrorRef.current?.(error);
|
|
193
|
+
});
|
|
188
194
|
}
|
|
189
195
|
} catch (error) {
|
|
190
196
|
console.error("Schema processing failed:", error);
|
|
191
|
-
onSchemaErrorRef.current?.(error
|
|
197
|
+
onSchemaErrorRef.current?.(toError(error));
|
|
192
198
|
}
|
|
193
199
|
}
|
|
194
200
|
}, [schema, userRole]); // Removed onSchemaError - using ref instead
|
|
@@ -221,6 +227,9 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
221
227
|
// Mark as started immediately to prevent duplicate calls
|
|
222
228
|
draftCreationStartedRef.current = true;
|
|
223
229
|
|
|
230
|
+
// Track if effect is still active (for cleanup/race condition handling)
|
|
231
|
+
let isActive = true;
|
|
232
|
+
|
|
224
233
|
const createInitialDraft = async () => {
|
|
225
234
|
setIsCreatingDraft(true);
|
|
226
235
|
setDraftError(null);
|
|
@@ -230,6 +239,9 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
230
239
|
// Call PATCH /{bdo_id}/draft with empty payload to get draft ID
|
|
231
240
|
const response = await client.draftInteraction({});
|
|
232
241
|
|
|
242
|
+
// Check if effect is still active before setting state
|
|
243
|
+
if (!isActive) return;
|
|
244
|
+
|
|
233
245
|
// Store the draft ID
|
|
234
246
|
setDraftId(response._id);
|
|
235
247
|
|
|
@@ -249,16 +261,27 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
249
261
|
});
|
|
250
262
|
}
|
|
251
263
|
} catch (error) {
|
|
264
|
+
// Check if effect is still active before setting state
|
|
265
|
+
if (!isActive) return;
|
|
266
|
+
|
|
252
267
|
console.error("Failed to create initial draft:", error);
|
|
253
|
-
setDraftError(error
|
|
268
|
+
setDraftError(toError(error));
|
|
254
269
|
// Reset the ref on error so it can be retried
|
|
255
270
|
draftCreationStartedRef.current = false;
|
|
256
271
|
} finally {
|
|
257
|
-
|
|
272
|
+
// Check if effect is still active before setting state
|
|
273
|
+
if (isActive) {
|
|
274
|
+
setIsCreatingDraft(false);
|
|
275
|
+
}
|
|
258
276
|
}
|
|
259
277
|
};
|
|
260
278
|
|
|
261
279
|
createInitialDraft();
|
|
280
|
+
|
|
281
|
+
// Cleanup function to handle unmount during async operation
|
|
282
|
+
return () => {
|
|
283
|
+
isActive = false;
|
|
284
|
+
};
|
|
262
285
|
}, [isInteractiveMode, operation, schemaConfig, enabled, draftId, source]);
|
|
263
286
|
// Note: rhfForm removed from deps - we use ref pattern to avoid dependency loops
|
|
264
287
|
|
|
@@ -331,10 +354,11 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
331
354
|
}
|
|
332
355
|
|
|
333
356
|
// Determine if draft should be triggered based on interaction mode
|
|
334
|
-
//
|
|
357
|
+
// For update mode, always behave as non-interactive (only trigger for computed deps)
|
|
358
|
+
// Interactive mode (create only): Always trigger draft API on blur
|
|
335
359
|
// Non-interactive mode: Only trigger for computed field dependencies
|
|
336
|
-
const shouldTrigger = isInteractiveMode
|
|
337
|
-
? true // Interactive mode: always trigger
|
|
360
|
+
const shouldTrigger = (isInteractiveMode && operation !== "update")
|
|
361
|
+
? true // Interactive mode (create only): always trigger
|
|
338
362
|
: (computedFieldDependencies.length > 0 &&
|
|
339
363
|
computedFieldDependencies.includes(fieldName as Path<T>));
|
|
340
364
|
|
|
@@ -653,8 +677,8 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
653
677
|
} as any);
|
|
654
678
|
result = { success: true, data: response };
|
|
655
679
|
} else {
|
|
656
|
-
//
|
|
657
|
-
const response = await client.
|
|
680
|
+
// Update operation - always use direct update API (non-interactive)
|
|
681
|
+
const response = await client.update(recordId!, cleanedData);
|
|
658
682
|
result = { success: true, data: response };
|
|
659
683
|
}
|
|
660
684
|
} else {
|
|
@@ -684,7 +708,7 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
684
708
|
await onSuccess?.(result.data || data, event);
|
|
685
709
|
} catch (error) {
|
|
686
710
|
// API error - call onError with Error object
|
|
687
|
-
onError?.(error
|
|
711
|
+
onError?.(toError(error), event);
|
|
688
712
|
} finally {
|
|
689
713
|
setIsSubmitting(false);
|
|
690
714
|
}
|
|
@@ -940,7 +964,7 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
|
940
964
|
isCreatingDraft,
|
|
941
965
|
|
|
942
966
|
// Error handling
|
|
943
|
-
loadError: loadError
|
|
967
|
+
loadError: loadError ? toError(loadError) : null,
|
|
944
968
|
hasError,
|
|
945
969
|
|
|
946
970
|
// Schema information
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
// Core TypeScript interfaces for the kanban board functionality
|
|
5
5
|
// Following patterns from useTable and useForm
|
|
6
6
|
|
|
7
|
-
import type {
|
|
8
|
-
import type {
|
|
7
|
+
import type { UseFilterReturnType, UseFilterOptionsType } from "../useFilter";
|
|
8
|
+
import type { ColumnDefinitionType } from "../../../types/common";
|
|
9
|
+
|
|
10
|
+
// Re-export ColumnDefinitionType for backwards compatibility
|
|
11
|
+
export type { ColumnDefinitionType };
|
|
9
12
|
|
|
10
13
|
// ============================================================
|
|
11
14
|
// CORE DATA STRUCTURES
|
|
@@ -95,23 +98,6 @@ export interface KanbanColumnType<T = Record<string, any>> {
|
|
|
95
98
|
_modified_at?: Date;
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
/**
|
|
99
|
-
* Column definition for display and behavior
|
|
100
|
-
* Similar to ColumnDefinition in useTable
|
|
101
|
-
*/
|
|
102
|
-
export interface ColumnDefinitionType<T> {
|
|
103
|
-
/** Field name from the card type */
|
|
104
|
-
fieldId: keyof T;
|
|
105
|
-
/** Display label (optional, defaults to fieldId) */
|
|
106
|
-
label?: string;
|
|
107
|
-
/** Enable sorting for this field */
|
|
108
|
-
enableSorting?: boolean;
|
|
109
|
-
/** Enable filtering for this field */
|
|
110
|
-
enableFiltering?: boolean;
|
|
111
|
-
/** Custom transform function (overrides auto-formatting) */
|
|
112
|
-
transform?: (value: any, card: T) => React.ReactNode;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
101
|
// ============================================================
|
|
116
102
|
// DRAG & DROP TYPES
|
|
117
103
|
// ============================================================
|
|
@@ -195,10 +181,8 @@ export interface UseKanbanOptionsType<T> {
|
|
|
195
181
|
|
|
196
182
|
/** Initial state */
|
|
197
183
|
initialState?: {
|
|
198
|
-
/** Initial filter conditions */
|
|
199
|
-
|
|
200
|
-
/** Initial filter operator for combining filter conditions */
|
|
201
|
-
filterOperator?: ConditionGroupOperatorType;
|
|
184
|
+
/** Initial filter configuration: { conditions, operator } */
|
|
185
|
+
filter?: UseFilterOptionsType;
|
|
202
186
|
/** Initial search query */
|
|
203
187
|
search?: string;
|
|
204
188
|
/** Initial column order */
|
|
@@ -7,6 +7,7 @@ import { useState, useMemo, useCallback, useRef, useEffect } from "react";
|
|
|
7
7
|
import { useQuery, useMutation, useQueryClient, useQueries, keepPreviousData } from "@tanstack/react-query";
|
|
8
8
|
import { api } from "../../../api";
|
|
9
9
|
import type { ListOptionsType, ListResponseType } from "../../../types/common";
|
|
10
|
+
import { toError } from "../../../utils/error-handling";
|
|
10
11
|
import { useFilter } from "../useFilter";
|
|
11
12
|
|
|
12
13
|
import type {
|
|
@@ -41,8 +42,13 @@ export function useKanban<T extends Record<string, any> = Record<string, any>>(
|
|
|
41
42
|
|
|
42
43
|
const [search, setSearch] = useState({
|
|
43
44
|
query: initialState?.search || "",
|
|
45
|
+
debouncedQuery: initialState?.search || "",
|
|
44
46
|
});
|
|
45
47
|
|
|
48
|
+
// Debounce timeout ref for search
|
|
49
|
+
const searchDebounceRef = useRef<NodeJS.Timeout | null>(null);
|
|
50
|
+
const SEARCH_DEBOUNCE_MS = 300;
|
|
51
|
+
|
|
46
52
|
const [sorting] = useState({
|
|
47
53
|
field: initialState?.sorting?.field || null,
|
|
48
54
|
direction: initialState?.sorting?.direction || null,
|
|
@@ -81,8 +87,8 @@ export function useKanban<T extends Record<string, any> = Record<string, any>>(
|
|
|
81
87
|
// ============================================================
|
|
82
88
|
|
|
83
89
|
const filter = useFilter({
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
conditions: initialState?.filter?.conditions,
|
|
91
|
+
operator: initialState?.filter?.operator || "And",
|
|
86
92
|
});
|
|
87
93
|
|
|
88
94
|
// Helper to generate API options for a specific column
|
|
@@ -139,13 +145,13 @@ export function useKanban<T extends Record<string, any> = Record<string, any>>(
|
|
|
139
145
|
];
|
|
140
146
|
}
|
|
141
147
|
|
|
142
|
-
// Add Search
|
|
143
|
-
if (search.
|
|
144
|
-
opts.Search = search.
|
|
148
|
+
// Add Search (debounced)
|
|
149
|
+
if (search.debouncedQuery) {
|
|
150
|
+
opts.Search = search.debouncedQuery;
|
|
145
151
|
}
|
|
146
152
|
|
|
147
153
|
return opts;
|
|
148
|
-
}, [filter.payload, columnPagination, sorting, search.
|
|
154
|
+
}, [filter.payload, columnPagination, sorting, search.debouncedQuery]);
|
|
149
155
|
|
|
150
156
|
// ============================================================
|
|
151
157
|
// COLUMN QUERY GENERATION
|
|
@@ -180,10 +186,10 @@ export function useKanban<T extends Record<string, any> = Record<string, any>>(
|
|
|
180
186
|
const cardApiOptions = useMemo((): ListOptionsType => {
|
|
181
187
|
// This is for the GLOBAL count (ignoring column split)
|
|
182
188
|
const opts: ListOptionsType = {};
|
|
183
|
-
if (search.
|
|
189
|
+
if (search.debouncedQuery) opts.Search = search.debouncedQuery;
|
|
184
190
|
if (filter.payload) opts.Filter = filter.payload;
|
|
185
191
|
return opts;
|
|
186
|
-
}, [search.
|
|
192
|
+
}, [search.debouncedQuery, filter.payload]);
|
|
187
193
|
|
|
188
194
|
const {
|
|
189
195
|
data: countData,
|
|
@@ -196,7 +202,7 @@ export function useKanban<T extends Record<string, any> = Record<string, any>>(
|
|
|
196
202
|
return await api<KanbanCardType<T>>(source).count(cardApiOptions);
|
|
197
203
|
} catch (err) {
|
|
198
204
|
if (onErrorRef.current) {
|
|
199
|
-
onErrorRef.current(err
|
|
205
|
+
onErrorRef.current(toError(err));
|
|
200
206
|
}
|
|
201
207
|
throw err;
|
|
202
208
|
}
|
|
@@ -258,7 +264,7 @@ export function useKanban<T extends Record<string, any> = Record<string, any>>(
|
|
|
258
264
|
if (context?.previousCards && context?.queryKey) {
|
|
259
265
|
queryClient.setQueryData(context.queryKey, context.previousCards);
|
|
260
266
|
}
|
|
261
|
-
onErrorRef.current?.(error
|
|
267
|
+
onErrorRef.current?.(toError(error));
|
|
262
268
|
},
|
|
263
269
|
onSettled: (_data, _error, variables) => {
|
|
264
270
|
const columnId = variables.columnId;
|
|
@@ -280,7 +286,7 @@ export function useKanban<T extends Record<string, any> = Record<string, any>>(
|
|
|
280
286
|
onCardUpdateRef.current?.({ _id: result.id, ...result.updates } as any);
|
|
281
287
|
},
|
|
282
288
|
onError: (error, _variables, _context) => {
|
|
283
|
-
onErrorRef.current?.(error
|
|
289
|
+
onErrorRef.current?.(toError(error));
|
|
284
290
|
},
|
|
285
291
|
onSettled: () => {
|
|
286
292
|
queryClient.invalidateQueries({ queryKey: ["kanban-cards", source] });
|
|
@@ -300,7 +306,7 @@ export function useKanban<T extends Record<string, any> = Record<string, any>>(
|
|
|
300
306
|
onCardDeleteRef.current?.(id);
|
|
301
307
|
},
|
|
302
308
|
onError: (error, _id, _context) => {
|
|
303
|
-
onErrorRef.current?.(error
|
|
309
|
+
onErrorRef.current?.(toError(error));
|
|
304
310
|
},
|
|
305
311
|
onSettled: () => {
|
|
306
312
|
queryClient.invalidateQueries({ queryKey: ["kanban-cards", source] });
|
|
@@ -374,7 +380,7 @@ export function useKanban<T extends Record<string, any> = Record<string, any>>(
|
|
|
374
380
|
if (context?.previousToData && context?.toQueryKey) {
|
|
375
381
|
queryClient.setQueryData(context.toQueryKey, context.previousToData);
|
|
376
382
|
}
|
|
377
|
-
onErrorRef.current?.(error
|
|
383
|
+
onErrorRef.current?.(toError(error));
|
|
378
384
|
},
|
|
379
385
|
onSettled: (_data, _error, variables) => {
|
|
380
386
|
const fromOpts = getColumnApiOptions(variables.fromColumnId);
|
|
@@ -401,7 +407,7 @@ export function useKanban<T extends Record<string, any> = Record<string, any>>(
|
|
|
401
407
|
},
|
|
402
408
|
onSuccess: () => {},
|
|
403
409
|
onError: (error, _variables, _context) => {
|
|
404
|
-
onErrorRef.current?.(error
|
|
410
|
+
onErrorRef.current?.(toError(error));
|
|
405
411
|
},
|
|
406
412
|
onSettled: (_data, _error, variables) => {
|
|
407
413
|
const opts = getColumnApiOptions(variables.columnId);
|
|
@@ -513,11 +519,32 @@ export function useKanban<T extends Record<string, any> = Record<string, any>>(
|
|
|
513
519
|
// ============================================================
|
|
514
520
|
|
|
515
521
|
const setSearchQuery = useCallback((value: string) => {
|
|
516
|
-
|
|
522
|
+
// Validate search query length to prevent DoS
|
|
523
|
+
if (value.length > 255) {
|
|
524
|
+
console.warn("Search query exceeds maximum length of 255 characters");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Update immediate value for UI
|
|
529
|
+
setSearch((prev) => ({ ...prev, query: value }));
|
|
530
|
+
|
|
531
|
+
// Clear existing debounce timeout
|
|
532
|
+
if (searchDebounceRef.current) {
|
|
533
|
+
clearTimeout(searchDebounceRef.current);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Debounce the actual API query update
|
|
537
|
+
searchDebounceRef.current = setTimeout(() => {
|
|
538
|
+
setSearch((prev) => ({ ...prev, debouncedQuery: value }));
|
|
539
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
517
540
|
}, []);
|
|
518
541
|
|
|
519
542
|
const clearSearch = useCallback(() => {
|
|
520
|
-
|
|
543
|
+
// Clear debounce timeout
|
|
544
|
+
if (searchDebounceRef.current) {
|
|
545
|
+
clearTimeout(searchDebounceRef.current);
|
|
546
|
+
}
|
|
547
|
+
setSearch({ query: "", debouncedQuery: "" });
|
|
521
548
|
}, []);
|
|
522
549
|
|
|
523
550
|
const totalCards = countData?.Count || 0;
|
|
@@ -555,10 +582,19 @@ export function useKanban<T extends Record<string, any> = Record<string, any>>(
|
|
|
555
582
|
|
|
556
583
|
useEffect(() => {
|
|
557
584
|
if (error && onErrorRef.current) {
|
|
558
|
-
onErrorRef.current(error
|
|
585
|
+
onErrorRef.current(toError(error));
|
|
559
586
|
}
|
|
560
587
|
}, [error]);
|
|
561
588
|
|
|
589
|
+
// Cleanup debounce timeout on unmount
|
|
590
|
+
useEffect(() => {
|
|
591
|
+
return () => {
|
|
592
|
+
if (searchDebounceRef.current) {
|
|
593
|
+
clearTimeout(searchDebounceRef.current);
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
}, []);
|
|
597
|
+
|
|
562
598
|
// ============================================================
|
|
563
599
|
// RETURN OBJECT
|
|
564
600
|
// ============================================================
|
|
@@ -574,7 +610,7 @@ export function useKanban<T extends Record<string, any> = Record<string, any>>(
|
|
|
574
610
|
isUpdating,
|
|
575
611
|
|
|
576
612
|
// Error Handling
|
|
577
|
-
error: error
|
|
613
|
+
error: error ? toError(error) : null,
|
|
578
614
|
|
|
579
615
|
// Card Operations (Flat Access)
|
|
580
616
|
createCard: createCardMutation.mutateAsync,
|
|
@@ -1,46 +1,40 @@
|
|
|
1
|
-
import type { ListResponseType,
|
|
2
|
-
|
|
1
|
+
import type { ListResponseType, SortType, ColumnDefinitionType } from "../../../types/common";
|
|
2
|
+
|
|
3
|
+
// Re-export ColumnDefinitionType for backwards compatibility
|
|
4
|
+
export type { ColumnDefinitionType };
|
|
5
|
+
import type { UseFilterReturnType, UseFilterOptionsType } from "../useFilter";
|
|
3
6
|
|
|
4
7
|
// ============================================================
|
|
5
|
-
// TYPE DEFINITIONS
|
|
8
|
+
// STATE TYPE DEFINITIONS
|
|
6
9
|
// ============================================================
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
enableFiltering?: boolean;
|
|
17
|
-
/** Custom transform function (overrides auto-formatting) */
|
|
18
|
-
transform?: (value: any, row: T) => React.ReactNode;
|
|
11
|
+
/**
|
|
12
|
+
* Pagination state type
|
|
13
|
+
*/
|
|
14
|
+
export interface PaginationStateType {
|
|
15
|
+
/** Page number (1-indexed) */
|
|
16
|
+
pageNo: number;
|
|
17
|
+
/** Number of items per page */
|
|
18
|
+
pageSize: number;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// ============================================================
|
|
22
|
+
// TYPE DEFINITIONS
|
|
23
|
+
// ============================================================
|
|
24
|
+
|
|
21
25
|
export interface UseTableOptionsType<T> {
|
|
22
26
|
/** Data source identifier */
|
|
23
27
|
source: string;
|
|
24
28
|
/** Column configurations */
|
|
25
29
|
columns: ColumnDefinitionType<T>[];
|
|
26
|
-
/** Enable sorting functionality */
|
|
27
|
-
enableSorting?: boolean;
|
|
28
|
-
/** Enable filtering functionality */
|
|
29
|
-
enableFiltering?: boolean;
|
|
30
|
-
/** Enable pagination */
|
|
31
|
-
enablePagination?: boolean;
|
|
32
30
|
/** Initial state */
|
|
33
31
|
initialState?: {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
direction: "asc" | "desc";
|
|
41
|
-
};
|
|
42
|
-
filters?: Array<ConditionType | ConditionGroupType>;
|
|
43
|
-
filterOperator?: ConditionGroupOperatorType;
|
|
32
|
+
/** Sort configuration: [{ "fieldName": "ASC" }] */
|
|
33
|
+
sort?: SortType;
|
|
34
|
+
/** Pagination state: { pageNo, pageSize } */
|
|
35
|
+
pagination?: PaginationStateType;
|
|
36
|
+
/** Filter state: { conditions, operator } */
|
|
37
|
+
filter?: UseFilterOptionsType<T>;
|
|
44
38
|
};
|
|
45
39
|
/** Error callback */
|
|
46
40
|
onError?: (error: Error) => void;
|
|
@@ -77,11 +71,11 @@ export interface UseTableReturnType<T> {
|
|
|
77
71
|
};
|
|
78
72
|
|
|
79
73
|
// Filter (Simplified chainable API)
|
|
80
|
-
filter: UseFilterReturnType
|
|
74
|
+
filter: UseFilterReturnType<T>;
|
|
81
75
|
|
|
82
76
|
// Pagination (Flat Access)
|
|
83
77
|
pagination: {
|
|
84
|
-
|
|
78
|
+
pageNo: number;
|
|
85
79
|
pageSize: number;
|
|
86
80
|
totalPages: number;
|
|
87
81
|
totalItems: number;
|