@ram_28/kf-ai-sdk 1.0.18 → 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.
Files changed (77) hide show
  1. package/README.md +45 -12
  2. package/dist/api/client.d.ts.map +1 -1
  3. package/dist/api.cjs +1 -1
  4. package/dist/api.mjs +2 -2
  5. package/dist/auth.cjs +1 -1
  6. package/dist/auth.mjs +1 -1
  7. package/dist/{client-C15j4O5B.cjs → client-DgtkT50N.cjs} +1 -1
  8. package/dist/{client-CfvLiGfP.js → client-V-WzUb8H.js} +9 -5
  9. package/dist/components/hooks/useFilter/types.d.ts +14 -11
  10. package/dist/components/hooks/useFilter/types.d.ts.map +1 -1
  11. package/dist/components/hooks/useFilter/useFilter.d.ts +1 -1
  12. package/dist/components/hooks/useFilter/useFilter.d.ts.map +1 -1
  13. package/dist/components/hooks/useForm/apiClient.d.ts.map +1 -1
  14. package/dist/components/hooks/useForm/useForm.d.ts.map +1 -1
  15. package/dist/components/hooks/useKanban/context.d.ts +1 -1
  16. package/dist/components/hooks/useKanban/context.d.ts.map +1 -1
  17. package/dist/components/hooks/useKanban/types.d.ts +5 -22
  18. package/dist/components/hooks/useKanban/types.d.ts.map +1 -1
  19. package/dist/components/hooks/useKanban/useKanban.d.ts.map +1 -1
  20. package/dist/components/hooks/useTable/types.d.ts +19 -31
  21. package/dist/components/hooks/useTable/types.d.ts.map +1 -1
  22. package/dist/components/hooks/useTable/useTable.d.ts.map +1 -1
  23. package/dist/error-handling-CAoD0Kwb.cjs +1 -0
  24. package/dist/error-handling-CrhTtD88.js +14 -0
  25. package/dist/filter.cjs +1 -1
  26. package/dist/filter.mjs +1 -1
  27. package/dist/form.cjs +1 -1
  28. package/dist/form.mjs +825 -814
  29. package/dist/index.d.ts +18 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/kanban.cjs +2 -2
  32. package/dist/kanban.mjs +335 -323
  33. package/dist/{metadata-2FLBsFcf.cjs → metadata-0lZAfuTP.cjs} +1 -1
  34. package/dist/{metadata-DBcoDth-.js → metadata-B88D_pVS.js} +1 -1
  35. package/dist/table.cjs +1 -1
  36. package/dist/table.mjs +113 -96
  37. package/dist/table.types.d.ts +1 -1
  38. package/dist/table.types.d.ts.map +1 -1
  39. package/dist/types/common.d.ts +26 -6
  40. package/dist/types/common.d.ts.map +1 -1
  41. package/dist/useFilter-DzpP_ag0.cjs +1 -0
  42. package/dist/useFilter-H5bgAZQF.js +120 -0
  43. package/dist/utils/api/buildListOptions.d.ts +43 -0
  44. package/dist/utils/api/buildListOptions.d.ts.map +1 -0
  45. package/dist/utils/api/index.d.ts +2 -0
  46. package/dist/utils/api/index.d.ts.map +1 -0
  47. package/dist/utils/error-handling.d.ts +41 -0
  48. package/dist/utils/error-handling.d.ts.map +1 -0
  49. package/dist/utils/index.d.ts +2 -0
  50. package/dist/utils/index.d.ts.map +1 -1
  51. package/docs/QUICK_REFERENCE.md +142 -420
  52. package/docs/useAuth.md +52 -340
  53. package/docs/useFilter.md +858 -162
  54. package/docs/useForm.md +712 -501
  55. package/docs/useKanban.md +534 -279
  56. package/docs/useTable.md +725 -214
  57. package/package.json +1 -1
  58. package/sdk/api/client.ts +7 -1
  59. package/sdk/components/hooks/useFilter/types.ts +14 -11
  60. package/sdk/components/hooks/useFilter/useFilter.ts +20 -18
  61. package/sdk/components/hooks/useForm/apiClient.ts +2 -1
  62. package/sdk/components/hooks/useForm/useForm.ts +47 -13
  63. package/sdk/components/hooks/useKanban/context.ts +5 -3
  64. package/sdk/components/hooks/useKanban/types.ts +7 -23
  65. package/sdk/components/hooks/useKanban/useKanban.ts +54 -18
  66. package/sdk/components/hooks/useTable/types.ts +26 -32
  67. package/sdk/components/hooks/useTable/useTable.llm.txt +8 -22
  68. package/sdk/components/hooks/useTable/useTable.ts +70 -25
  69. package/sdk/index.ts +154 -10
  70. package/sdk/table.types.ts +3 -0
  71. package/sdk/types/common.ts +31 -6
  72. package/sdk/utils/api/buildListOptions.ts +120 -0
  73. package/sdk/utils/api/index.ts +2 -0
  74. package/sdk/utils/error-handling.ts +150 -0
  75. package/sdk/utils/index.ts +6 -0
  76. package/dist/useFilter-Dofowpr_.cjs +0 -1
  77. package/dist/useFilter-Dv-mr9QW.js +0 -117
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ram_28/kf-ai-sdk",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "Type-safe, AI-driven SDK for building modern web applications with role-based access control",
5
5
  "author": "Ramprasad",
6
6
  "license": "MIT",
package/sdk/api/client.ts CHANGED
@@ -379,7 +379,13 @@ export function api<T = any>(bo_id: string): ResourceClient<T> {
379
379
  );
380
380
  }
381
381
 
382
- return response.json();
382
+ const json = await response.json();
383
+ // API returns {"Data":{"_id":"..."},"ValidationFailures":[]}
384
+ // Extract _id from Data wrapper and return flat structure
385
+ return {
386
+ ...json.Data,
387
+ _id: json.Data._id,
388
+ };
383
389
  },
384
390
 
385
391
  // ============================================================
@@ -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
- /** Initial filter conditions */
63
- initialConditions?: Array<ConditionType | ConditionGroupType>;
64
- /** Initial operator for combining conditions (defaults to "And") */
65
- initialOperator?: ConditionGroupOperatorType;
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, "id">, parentId?: string) => string;
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, "id">>) => void;
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
- return `filter_${Date.now()}_${++idCounter}`;
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.initialConditions || [])
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.initialOperator || "And"
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, "id">, parentId?: string): string => {
209
+ (condition: Omit<ConditionType<T>, "id">, parentId?: string): string => {
208
210
  const id = generateId();
209
- const newCondition: ConditionType = { ...condition, id };
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, "id">>): void => {
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 as 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,
@@ -81,6 +82,9 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
81
82
  // This allows us to detect changes since the last draft, not since form init
82
83
  const lastSyncedValuesRef = useRef<Partial<T> | null>(null);
83
84
 
85
+ // Track if draft creation has started (prevents duplicate calls in React strict mode)
86
+ const draftCreationStartedRef = useRef(false);
87
+
84
88
  // Stable callback ref to prevent dependency loops
85
89
  const onSchemaErrorRef = useRef(onSchemaError);
86
90
 
@@ -181,11 +185,16 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
181
185
  if (Object.keys(refFields).length > 0) {
182
186
  fetchAllReferenceData(refFields)
183
187
  .then(setReferenceData)
184
- .catch(console.warn);
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
+ });
185
194
  }
186
195
  } catch (error) {
187
196
  console.error("Schema processing failed:", error);
188
- onSchemaErrorRef.current?.(error as Error);
197
+ onSchemaErrorRef.current?.(toError(error));
189
198
  }
190
199
  }
191
200
  }, [schema, userRole]); // Removed onSchemaError - using ref instead
@@ -209,11 +218,18 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
209
218
  operation !== "create" ||
210
219
  !schemaConfig ||
211
220
  !enabled ||
212
- draftId
221
+ draftId ||
222
+ draftCreationStartedRef.current // Prevent duplicate calls in React strict mode
213
223
  ) {
214
224
  return;
215
225
  }
216
226
 
227
+ // Mark as started immediately to prevent duplicate calls
228
+ draftCreationStartedRef.current = true;
229
+
230
+ // Track if effect is still active (for cleanup/race condition handling)
231
+ let isActive = true;
232
+
217
233
  const createInitialDraft = async () => {
218
234
  setIsCreatingDraft(true);
219
235
  setDraftError(null);
@@ -223,6 +239,9 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
223
239
  // Call PATCH /{bdo_id}/draft with empty payload to get draft ID
224
240
  const response = await client.draftInteraction({});
225
241
 
242
+ // Check if effect is still active before setting state
243
+ if (!isActive) return;
244
+
226
245
  // Store the draft ID
227
246
  setDraftId(response._id);
228
247
 
@@ -242,15 +261,29 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
242
261
  });
243
262
  }
244
263
  } catch (error) {
264
+ // Check if effect is still active before setting state
265
+ if (!isActive) return;
266
+
245
267
  console.error("Failed to create initial draft:", error);
246
- setDraftError(error as Error);
268
+ setDraftError(toError(error));
269
+ // Reset the ref on error so it can be retried
270
+ draftCreationStartedRef.current = false;
247
271
  } finally {
248
- setIsCreatingDraft(false);
272
+ // Check if effect is still active before setting state
273
+ if (isActive) {
274
+ setIsCreatingDraft(false);
275
+ }
249
276
  }
250
277
  };
251
278
 
252
279
  createInitialDraft();
253
- }, [isInteractiveMode, operation, schemaConfig, enabled, draftId, source, rhfForm]);
280
+
281
+ // Cleanup function to handle unmount during async operation
282
+ return () => {
283
+ isActive = false;
284
+ };
285
+ }, [isInteractiveMode, operation, schemaConfig, enabled, draftId, source]);
286
+ // Note: rhfForm removed from deps - we use ref pattern to avoid dependency loops
254
287
 
255
288
  // ============================================================
256
289
  // COMPUTED FIELD DEPENDENCY TRACKING AND OPTIMIZATION
@@ -321,10 +354,11 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
321
354
  }
322
355
 
323
356
  // Determine if draft should be triggered based on interaction mode
324
- // Interactive mode: Always trigger draft API on blur
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
325
359
  // Non-interactive mode: Only trigger for computed field dependencies
326
- const shouldTrigger = isInteractiveMode
327
- ? true // Interactive mode: always trigger
360
+ const shouldTrigger = (isInteractiveMode && operation !== "update")
361
+ ? true // Interactive mode (create only): always trigger
328
362
  : (computedFieldDependencies.length > 0 &&
329
363
  computedFieldDependencies.includes(fieldName as Path<T>));
330
364
 
@@ -643,8 +677,8 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
643
677
  } as any);
644
678
  result = { success: true, data: response };
645
679
  } else {
646
- // Interactive update: POST /{bdo_id}/{id}/draft
647
- const response = await client.draftUpdate(recordId!, cleanedData);
680
+ // Update operation - always use direct update API (non-interactive)
681
+ const response = await client.update(recordId!, cleanedData);
648
682
  result = { success: true, data: response };
649
683
  }
650
684
  } else {
@@ -674,7 +708,7 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
674
708
  await onSuccess?.(result.data || data, event);
675
709
  } catch (error) {
676
710
  // API error - call onError with Error object
677
- onError?.(error as Error, event);
711
+ onError?.(toError(error), event);
678
712
  } finally {
679
713
  setIsSubmitting(false);
680
714
  }
@@ -930,7 +964,7 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
930
964
  isCreatingDraft,
931
965
 
932
966
  // Error handling
933
- loadError: loadError as Error | null,
967
+ loadError: loadError ? toError(loadError) : null,
934
968
  hasError,
935
969
 
936
970
  // Schema information
@@ -1,13 +1,15 @@
1
1
  import { createContext, useContext } from "react";
2
- import { UseKanbanReturnType } from "./types";
2
+ import type { UseKanbanReturnType } from "./types";
3
3
 
4
- export const KanbanContext = createContext<UseKanbanReturnType<any> | null>(null);
4
+ export const KanbanContext = createContext<UseKanbanReturnType<any> | null>(
5
+ null,
6
+ );
5
7
 
6
8
  export function useKanbanContext<T extends Record<string, any> = any>() {
7
9
  const context = useContext(KanbanContext);
8
10
  if (!context) {
9
11
  throw new Error(
10
- "Kanban components must be used within a KanbanBoard component"
12
+ "Kanban components must be used within a KanbanBoard component",
11
13
  );
12
14
  }
13
15
  return context as UseKanbanReturnType<T>;
@@ -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 { ConditionType, ConditionGroupType, UseFilterReturnType } from "../useFilter";
8
- import type { ConditionGroupOperatorType } from "../../../types/common";
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
- filters?: Array<ConditionType | ConditionGroupType>;
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
- initialConditions: initialState?.filters,
85
- initialOperator: initialState?.filterOperator || "And",
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.query) {
144
- opts.Search = search.query;
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.query]);
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.query) opts.Search = search.query;
189
+ if (search.debouncedQuery) opts.Search = search.debouncedQuery;
184
190
  if (filter.payload) opts.Filter = filter.payload;
185
191
  return opts;
186
- }, [search.query, filter.payload]);
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 as Error);
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 as 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 as 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 as 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 as 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 as 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
- setSearch({ query: value });
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
- setSearch({ query: "" });
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 as 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 as Error | null,
613
+ error: error ? toError(error) : null,
578
614
 
579
615
  // Card Operations (Flat Access)
580
616
  createCard: createCardMutation.mutateAsync,