@ram_28/kf-ai-sdk 1.0.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 (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +840 -0
  3. package/dist/api/client.d.ts +78 -0
  4. package/dist/api/client.d.ts.map +1 -0
  5. package/dist/api/datetime.d.ts +21 -0
  6. package/dist/api/datetime.d.ts.map +1 -0
  7. package/dist/api/index.d.ts +7 -0
  8. package/dist/api/index.d.ts.map +1 -0
  9. package/dist/api/metadata.d.ts +75 -0
  10. package/dist/api/metadata.d.ts.map +1 -0
  11. package/dist/components/hooks/index.d.ts +8 -0
  12. package/dist/components/hooks/index.d.ts.map +1 -0
  13. package/dist/components/hooks/useFilter/index.d.ts +5 -0
  14. package/dist/components/hooks/useFilter/index.d.ts.map +1 -0
  15. package/dist/components/hooks/useFilter/payloadBuilder.utils.d.ts +33 -0
  16. package/dist/components/hooks/useFilter/payloadBuilder.utils.d.ts.map +1 -0
  17. package/dist/components/hooks/useFilter/types.d.ts +137 -0
  18. package/dist/components/hooks/useFilter/types.d.ts.map +1 -0
  19. package/dist/components/hooks/useFilter/useFilter.d.ts +3 -0
  20. package/dist/components/hooks/useFilter/useFilter.d.ts.map +1 -0
  21. package/dist/components/hooks/useFilter/validation.utils.d.ts +38 -0
  22. package/dist/components/hooks/useFilter/validation.utils.d.ts.map +1 -0
  23. package/dist/components/hooks/useForm/apiClient.d.ts +71 -0
  24. package/dist/components/hooks/useForm/apiClient.d.ts.map +1 -0
  25. package/dist/components/hooks/useForm/expressionValidator.utils.d.ts +28 -0
  26. package/dist/components/hooks/useForm/expressionValidator.utils.d.ts.map +1 -0
  27. package/dist/components/hooks/useForm/index.d.ts +6 -0
  28. package/dist/components/hooks/useForm/index.d.ts.map +1 -0
  29. package/dist/components/hooks/useForm/optimizedExpressionValidator.utils.d.ts +88 -0
  30. package/dist/components/hooks/useForm/optimizedExpressionValidator.utils.d.ts.map +1 -0
  31. package/dist/components/hooks/useForm/ruleClassifier.utils.d.ts +28 -0
  32. package/dist/components/hooks/useForm/ruleClassifier.utils.d.ts.map +1 -0
  33. package/dist/components/hooks/useForm/schemaParser.utils.d.ts +29 -0
  34. package/dist/components/hooks/useForm/schemaParser.utils.d.ts.map +1 -0
  35. package/dist/components/hooks/useForm/types.d.ts +412 -0
  36. package/dist/components/hooks/useForm/types.d.ts.map +1 -0
  37. package/dist/components/hooks/useForm/useForm.d.ts +3 -0
  38. package/dist/components/hooks/useForm/useForm.d.ts.map +1 -0
  39. package/dist/components/hooks/useKanban/apiClient.d.ts +99 -0
  40. package/dist/components/hooks/useKanban/apiClient.d.ts.map +1 -0
  41. package/dist/components/hooks/useKanban/context.d.ts +4 -0
  42. package/dist/components/hooks/useKanban/context.d.ts.map +1 -0
  43. package/dist/components/hooks/useKanban/dragDropManager.d.ts +27 -0
  44. package/dist/components/hooks/useKanban/dragDropManager.d.ts.map +1 -0
  45. package/dist/components/hooks/useKanban/index.d.ts +6 -0
  46. package/dist/components/hooks/useKanban/index.d.ts.map +1 -0
  47. package/dist/components/hooks/useKanban/types.d.ts +438 -0
  48. package/dist/components/hooks/useKanban/types.d.ts.map +1 -0
  49. package/dist/components/hooks/useKanban/useKanban.d.ts +3 -0
  50. package/dist/components/hooks/useKanban/useKanban.d.ts.map +1 -0
  51. package/dist/components/hooks/useKanban/useKanbanSimple.d.ts +62 -0
  52. package/dist/components/hooks/useKanban/useKanbanSimple.d.ts.map +1 -0
  53. package/dist/components/hooks/useTable/index.d.ts +3 -0
  54. package/dist/components/hooks/useTable/index.d.ts.map +1 -0
  55. package/dist/components/hooks/useTable/types.d.ts +107 -0
  56. package/dist/components/hooks/useTable/types.d.ts.map +1 -0
  57. package/dist/components/hooks/useTable/useTable.d.ts +8 -0
  58. package/dist/components/hooks/useTable/useTable.d.ts.map +1 -0
  59. package/dist/components/index.d.ts +3 -0
  60. package/dist/components/index.d.ts.map +1 -0
  61. package/dist/components/ui/index.d.ts +2 -0
  62. package/dist/components/ui/index.d.ts.map +1 -0
  63. package/dist/components/ui/kanban/Kanban.d.ts +12 -0
  64. package/dist/components/ui/kanban/Kanban.d.ts.map +1 -0
  65. package/dist/components/ui/kanban/index.d.ts +2 -0
  66. package/dist/components/ui/kanban/index.d.ts.map +1 -0
  67. package/dist/index.cjs +45 -0
  68. package/dist/index.d.ts +5 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.mjs +6522 -0
  71. package/dist/types/base-fields.d.ts +182 -0
  72. package/dist/types/base-fields.d.ts.map +1 -0
  73. package/dist/types/common.d.ts +238 -0
  74. package/dist/types/common.d.ts.map +1 -0
  75. package/dist/types/index.d.ts +3 -0
  76. package/dist/types/index.d.ts.map +1 -0
  77. package/dist/utils/cn.d.ts +7 -0
  78. package/dist/utils/cn.d.ts.map +1 -0
  79. package/dist/utils/formatting.d.ts +52 -0
  80. package/dist/utils/formatting.d.ts.map +1 -0
  81. package/dist/utils/index.d.ts +3 -0
  82. package/dist/utils/index.d.ts.map +1 -0
  83. package/package.json +98 -0
  84. package/sdk/api/client.ts +447 -0
  85. package/sdk/api/datetime.ts +33 -0
  86. package/sdk/api/index.ts +61 -0
  87. package/sdk/api/metadata.ts +148 -0
  88. package/sdk/components/hooks/index.ts +34 -0
  89. package/sdk/components/hooks/useFilter/index.ts +37 -0
  90. package/sdk/components/hooks/useFilter/payloadBuilder.utils.ts +298 -0
  91. package/sdk/components/hooks/useFilter/types.ts +158 -0
  92. package/sdk/components/hooks/useFilter/useFilter.llm.txt +497 -0
  93. package/sdk/components/hooks/useFilter/useFilter.ts +494 -0
  94. package/sdk/components/hooks/useFilter/validation.utils.ts +401 -0
  95. package/sdk/components/hooks/useForm/apiClient.ts +441 -0
  96. package/sdk/components/hooks/useForm/expressionValidator.utils.ts +444 -0
  97. package/sdk/components/hooks/useForm/index.ts +64 -0
  98. package/sdk/components/hooks/useForm/optimizedExpressionValidator.utils.ts +482 -0
  99. package/sdk/components/hooks/useForm/ruleClassifier.utils.ts +424 -0
  100. package/sdk/components/hooks/useForm/schemaParser.utils.ts +519 -0
  101. package/sdk/components/hooks/useForm/types.ts +630 -0
  102. package/sdk/components/hooks/useForm/useForm.llm.txt +340 -0
  103. package/sdk/components/hooks/useForm/useForm.ts +821 -0
  104. package/sdk/components/hooks/useKanban/apiClient.ts +494 -0
  105. package/sdk/components/hooks/useKanban/context.ts +14 -0
  106. package/sdk/components/hooks/useKanban/dragDropManager.ts +529 -0
  107. package/sdk/components/hooks/useKanban/index.ts +63 -0
  108. package/sdk/components/hooks/useKanban/types.ts +606 -0
  109. package/sdk/components/hooks/useKanban/useKanban.llm.txt +482 -0
  110. package/sdk/components/hooks/useKanban/useKanban.ts +725 -0
  111. package/sdk/components/hooks/useKanban/useKanbanSimple.ts +389 -0
  112. package/sdk/components/hooks/useTable/index.ts +5 -0
  113. package/sdk/components/hooks/useTable/types.ts +154 -0
  114. package/sdk/components/hooks/useTable/useTable.llm.txt +344 -0
  115. package/sdk/components/hooks/useTable/useTable.ts +413 -0
  116. package/sdk/components/index.ts +15 -0
  117. package/sdk/components/ui/index.ts +2 -0
  118. package/sdk/components/ui/kanban/Kanban.tsx +134 -0
  119. package/sdk/components/ui/kanban/index.ts +11 -0
  120. package/sdk/index.ts +13 -0
  121. package/sdk/types/base-fields.ts +221 -0
  122. package/sdk/types/common.ts +306 -0
  123. package/sdk/types/index.ts +5 -0
  124. package/sdk/utils/cn.ts +10 -0
  125. package/sdk/utils/formatting.ts +212 -0
  126. package/sdk/utils/index.ts +5 -0
@@ -0,0 +1,725 @@
1
+ // ============================================================
2
+ // USE KANBAN HOOK - Main Implementation
3
+ // ============================================================
4
+ // Simplified hook following useTable pattern - provides clean abstraction
5
+
6
+ import { useState, useMemo, useCallback, useRef, useEffect } from "react";
7
+ import { useQuery, useMutation, useQueryClient, useQueries, keepPreviousData } from "@tanstack/react-query";
8
+ import { api } from "../../../api";
9
+ import type { ListOptions, ListResponse } from "../../../types/common";
10
+ import { useFilter } from "../useFilter";
11
+
12
+ import type {
13
+ UseKanbanOptions,
14
+ UseKanbanReturn,
15
+ KanbanCard,
16
+ } from "./types";
17
+ import { useDragDropManager } from "./dragDropManager";
18
+
19
+ // ============================================================
20
+ // MAIN HOOK IMPLEMENTATION
21
+ // ============================================================
22
+
23
+ export function useKanban<T extends Record<string, any> = Record<string, any>>(
24
+ options: UseKanbanOptions<T>
25
+ ): UseKanbanReturn<T> {
26
+ const {
27
+ columns: columnConfigs,
28
+ cardSource,
29
+ source,
30
+ enableDragDrop = true,
31
+ cardFieldDefinitions,
32
+ initialState,
33
+ onCardMove,
34
+ onCardCreate,
35
+ onCardUpdate,
36
+ onCardDelete,
37
+ onError,
38
+ onFilterError,
39
+ } = options;
40
+
41
+ // Use source or cardSource (backwards compatibility)
42
+ const dataSource = source || cardSource;
43
+
44
+ if (!dataSource) {
45
+ throw new Error('useKanban requires either "source" or "cardSource" parameter');
46
+ }
47
+
48
+ // ============================================================
49
+ // STATE MANAGEMENT
50
+ // ============================================================
51
+
52
+ const [search, setSearch] = useState({
53
+ query: initialState?.search || "",
54
+ });
55
+
56
+ const [sorting] = useState({
57
+ field: initialState?.sorting?.field || null,
58
+ direction: initialState?.sorting?.direction || null,
59
+ });
60
+
61
+ // Per-column pagination state (PageSize expansion strategy)
62
+ const [columnPagination, setColumnPagination] = useState<Record<string, number>>(() => {
63
+ const initial: Record<string, number> = {};
64
+ columnConfigs.forEach(col => {
65
+ initial[col.id] = 10; // Default page size
66
+ });
67
+ return initial;
68
+ });
69
+
70
+ // Stable callback refs to prevent dependency loops
71
+ const onErrorRef = useRef(onError);
72
+ const onCardMoveRef = useRef(onCardMove);
73
+ const onCardCreateRef = useRef(onCardCreate);
74
+ const onCardUpdateRef = useRef(onCardUpdate);
75
+ const onCardDeleteRef = useRef(onCardDelete);
76
+
77
+ // Update refs when callbacks change
78
+ useEffect(() => {
79
+ onErrorRef.current = onError;
80
+ onCardMoveRef.current = onCardMove;
81
+ onCardCreateRef.current = onCardCreate;
82
+ onCardUpdateRef.current = onCardUpdate;
83
+ onCardDeleteRef.current = onCardDelete;
84
+ }, [onError, onCardMove, onCardCreate, onCardUpdate, onCardDelete]);
85
+
86
+ // Query client for cache management
87
+ const queryClient = useQueryClient();
88
+
89
+ // ============================================================
90
+ // FILTER INTEGRATION
91
+ // ============================================================
92
+
93
+ const filterHook = useFilter<T>({
94
+ initialConditions: initialState?.filters,
95
+ initialLogicalOperator: initialState?.filterOperator || "And",
96
+ fieldDefinitions: cardFieldDefinitions,
97
+ validateOnChange: true,
98
+ onValidationError: onFilterError,
99
+ });
100
+
101
+ // Helper to generate API options for a specific column
102
+ // This is used for both initial fetching and mutation updates
103
+ const getColumnApiOptions = useCallback((columnId: string): ListOptions => {
104
+ // 1. Construct Compound Filter Payload
105
+ const columnFilterObject = {
106
+ LHSField: "columnId",
107
+ Operator: "EQ",
108
+ RHSValue: columnId,
109
+ RHSType: "Constant"
110
+ };
111
+
112
+ const basePayload = filterHook.filterPayload;
113
+ let combinedPayload: any;
114
+
115
+ if (!basePayload) {
116
+ combinedPayload = {
117
+ Operator: "And",
118
+ Condition: [columnFilterObject]
119
+ };
120
+ } else {
121
+ // If base is And, append. If base is Or, wrap in new And.
122
+ if (basePayload.Operator === "And") {
123
+ combinedPayload = {
124
+ ...basePayload,
125
+ Condition: [...(basePayload.Condition || []), columnFilterObject]
126
+ };
127
+ } else {
128
+ combinedPayload = {
129
+ Operator: "And",
130
+ Condition: [basePayload, columnFilterObject]
131
+ };
132
+ }
133
+ }
134
+
135
+ // 2. Construct API Options
136
+ const opts: ListOptions = {
137
+ Page: 1, // Always page 1 due to expanding PageSize strategy
138
+ PageSize: columnPagination[columnId] || 10,
139
+ Filter: combinedPayload,
140
+ };
141
+
142
+ // Add Sorting - using correct API format: [{ "fieldName": "ASC" }]
143
+ if (sorting.field && sorting.direction) {
144
+ opts.Sort = [
145
+ { [String(sorting.field)]: sorting.direction === "asc" ? "ASC" : "DESC" },
146
+ { position: "ASC" },
147
+ ];
148
+ } else {
149
+ opts.Sort = [
150
+ { columnId: "ASC" },
151
+ { position: "ASC" },
152
+ ];
153
+ }
154
+
155
+ // Add Search
156
+ if (search.query) {
157
+ opts.Search = search.query;
158
+ }
159
+
160
+ return opts;
161
+ }, [filterHook.filterPayload, columnPagination, sorting, search.query]);
162
+
163
+ // ============================================================
164
+ // COLUMN QUERY GENERATION
165
+ // ============================================================
166
+
167
+ const columnQueries = useQueries({
168
+ queries: columnConfigs.map((column) => {
169
+ const opts = getColumnApiOptions(column.id);
170
+ return {
171
+ queryKey: ["kanban-cards", dataSource, column.id, opts],
172
+ queryFn: async (): Promise<ListResponse<KanbanCard<T>>> => {
173
+ try {
174
+ return await api<KanbanCard<T>>(dataSource).list(opts);
175
+ } catch (err) {
176
+ throw err;
177
+ }
178
+ },
179
+ placeholderData: keepPreviousData,
180
+ staleTime: 30 * 1000,
181
+ };
182
+ }),
183
+ });
184
+
185
+ // Aggregate loading and error states
186
+ const isLoadingCards = columnQueries.some(q => q.isLoading);
187
+ const isFetchingCards = columnQueries.some(q => q.isFetching);
188
+ const cardsError = columnQueries.find(q => q.error)?.error || null;
189
+ const refetchCards = async () => {
190
+ await Promise.all(columnQueries.map(q => q.refetch()));
191
+ };
192
+
193
+ // Get total card count (Global count, or sum of columns? Usually global might differ if filters applied)
194
+ // For simplicity, we can fetch global count but restricted by filters?
195
+ // Actually, standard Kanban usually doesn't show "Total Cards" unless it's per column.
196
+ // We will keep the global count query for now but it might be slightly inaccurate if we want "Total Visible".
197
+ // Let's rely on summing up column counts for "Total Visible" and keep this for "Total Database"?
198
+ // Or just query with base filters?
199
+ // Let's keep existing logic but apply ONLY base filters + search.
200
+
201
+ const cardApiOptions = useMemo((): ListOptions => {
202
+ // This is for the GLOBAL count (ignoring column split)
203
+ const opts: ListOptions = {};
204
+ if (search.query) opts.Search = search.query;
205
+ if (filterHook.filterPayload) opts.Filter = filterHook.filterPayload;
206
+ return opts;
207
+ }, [search.query, filterHook.filterPayload]);
208
+
209
+ const {
210
+ data: countData,
211
+ isLoading: isLoadingCount,
212
+ error: countError,
213
+ } = useQuery({
214
+ queryKey: ["kanban-count", dataSource, cardApiOptions],
215
+ queryFn: async () => {
216
+ try {
217
+ return await api<KanbanCard<T>>(dataSource).count(cardApiOptions);
218
+ } catch (err) {
219
+ if (onErrorRef.current) {
220
+ onErrorRef.current(err as Error);
221
+ }
222
+ throw err;
223
+ }
224
+ },
225
+ staleTime: 30 * 1000,
226
+ gcTime: 60 * 1000,
227
+ });
228
+
229
+ // ============================================================
230
+ // CARD MUTATIONS
231
+ // ============================================================
232
+
233
+ const createCardMutation = useMutation({
234
+ mutationFn: async (card: Partial<KanbanCard<T>> & { columnId: string }) => {
235
+ // We need to fetch the current count or max position to append correclty if not provided
236
+ // Simplification: just send position=999999 or let backend handle it?
237
+ // Since we want optimistic UI, we should calculate it.
238
+ // But calculating it from partial data (paginated) is risky.
239
+ // Let's rely on backend or default to top/bottom logic.
240
+ const position = card.position ?? 999999;
241
+ const response = await api<KanbanCard<T>>(dataSource).create({ ...card, position });
242
+ return response._id;
243
+ },
244
+ onMutate: async (newCardVariables) => {
245
+ const columnId = newCardVariables.columnId;
246
+ const opts = getColumnApiOptions(columnId);
247
+ const queryKey = ["kanban-cards", dataSource, columnId, opts];
248
+
249
+ await queryClient.cancelQueries({ queryKey });
250
+
251
+ const previousCards = queryClient.getQueryData<ListResponse<KanbanCard<T>>>(queryKey);
252
+
253
+ if (previousCards) {
254
+ // Determine position
255
+ const currentCards = previousCards.Data;
256
+ const position = newCardVariables.position ?? currentCards.length;
257
+
258
+ const tempId = `temp-${Date.now()}`;
259
+ const newCard = {
260
+ ...newCardVariables,
261
+ _id: tempId,
262
+ position,
263
+ _created_at: new Date(),
264
+ _modified_at: new Date(),
265
+ } as KanbanCard<T>;
266
+
267
+ queryClient.setQueryData<ListResponse<KanbanCard<T>>>(queryKey, {
268
+ ...previousCards,
269
+ Data: [...previousCards.Data, newCard],
270
+ });
271
+ }
272
+
273
+ return { previousCards, queryKey };
274
+ },
275
+ onSuccess: async (cardId, _variables, context) => {
276
+ // Refetch the specific column
277
+ if (context?.queryKey) {
278
+ await queryClient.invalidateQueries({ queryKey: context.queryKey });
279
+ }
280
+ onCardCreateRef.current?.({
281
+ _id: cardId,
282
+ ..._variables,
283
+ } as KanbanCard<T>);
284
+ },
285
+ onError: (error, _variables, context) => {
286
+ if (context?.previousCards && context?.queryKey) {
287
+ queryClient.setQueryData(context.queryKey, context.previousCards);
288
+ }
289
+ onErrorRef.current?.(error as Error);
290
+ },
291
+ onSettled: (_data, _error, variables) => {
292
+ const columnId = variables.columnId;
293
+ const opts = getColumnApiOptions(columnId);
294
+ queryClient.invalidateQueries({ queryKey: ["kanban-cards", dataSource, columnId, opts] });
295
+ }
296
+ });
297
+
298
+ const updateCardMutation = useMutation({
299
+ mutationFn: async ({ id, updates }: { id: string; updates: Partial<KanbanCard<T>> }) => {
300
+ await api<KanbanCard<T>>(dataSource).update(id, updates);
301
+ return { id, updates };
302
+ },
303
+ onMutate: async () => {
304
+ // We don't know the columnId easily without passing it.
305
+ // We can try to finding it in all column queries
306
+ // For now, simpler to just invalidate everything on success/error
307
+ // OR pass columnId in updates if available.
308
+ // If we want optimistic updates, we need to iterate all queries.
309
+
310
+ // Strategy: Invalidate all columns. Optimistic update is hard without knowing columnId.
311
+ // If the user passes columnId in updates, we can optimize.
312
+
313
+ await queryClient.cancelQueries({ queryKey: ["kanban-cards", dataSource] });
314
+ return {};
315
+ },
316
+ onSuccess: async (result) => {
317
+ // Find the card to trigger callback
318
+ // Since we don't have a single list, this is harder.
319
+ // We can skip finding it for now or iterate queries.
320
+ onCardUpdateRef.current?.({ _id: result.id, ...result.updates } as any);
321
+ },
322
+ onError: (error, _variables, _context) => {
323
+ onErrorRef.current?.(error as Error);
324
+ },
325
+ onSettled: () => {
326
+ queryClient.invalidateQueries({ queryKey: ["kanban-cards", dataSource] });
327
+ }
328
+ });
329
+
330
+ const deleteCardMutation = useMutation({
331
+ mutationFn: async (id: string) => {
332
+ await api(dataSource).delete(id);
333
+ return id;
334
+ },
335
+ onMutate: async () => {
336
+ await queryClient.cancelQueries({ queryKey: ["kanban-cards", dataSource] });
337
+ // Optimistic delete: Iterate all column queries and remove?
338
+ // For now, simple invalidation
339
+ return {};
340
+ },
341
+ onSuccess: async (id) => {
342
+ onCardDeleteRef.current?.(id);
343
+ },
344
+ onError: (error, _id, _context) => {
345
+ onErrorRef.current?.(error as Error);
346
+ },
347
+ onSettled: () => {
348
+ queryClient.invalidateQueries({ queryKey: ["kanban-cards", dataSource] });
349
+ }
350
+ });
351
+
352
+ const moveCardMutation = useMutation({
353
+ mutationFn: async ({ cardId, fromColumnId, toColumnId, position }: { cardId: string; fromColumnId: string; toColumnId: string; position?: number }) => {
354
+ const updates: any = { columnId: toColumnId, ...(position !== undefined && { position }) };
355
+ await api<KanbanCard<T>>(dataSource).update(cardId, updates);
356
+ return { cardId, fromColumnId, toColumnId, position };
357
+ },
358
+ onMutate: async ({ cardId, fromColumnId, toColumnId, position }) => {
359
+ // Cancel queries for only the affected columns
360
+ const fromOpts = getColumnApiOptions(fromColumnId);
361
+ const toOpts = getColumnApiOptions(toColumnId);
362
+ const fromQueryKey = ["kanban-cards", dataSource, fromColumnId, fromOpts];
363
+ const toQueryKey = ["kanban-cards", dataSource, toColumnId, toOpts];
364
+
365
+ await queryClient.cancelQueries({ queryKey: fromQueryKey });
366
+ await queryClient.cancelQueries({ queryKey: toQueryKey });
367
+
368
+ // Get current data for both columns
369
+ const previousFromData = queryClient.getQueryData<ListResponse<KanbanCard<T>>>(fromQueryKey);
370
+ const previousToData = queryClient.getQueryData<ListResponse<KanbanCard<T>>>(toQueryKey);
371
+
372
+ // Optimistic update: move card between columns
373
+ if (previousFromData && previousToData) {
374
+ // Find the card in the source column
375
+ const cardToMove = previousFromData.Data.find(c => c._id === cardId);
376
+
377
+ if (cardToMove) {
378
+ // Remove card from source column
379
+ const newFromData = {
380
+ ...previousFromData,
381
+ Data: previousFromData.Data.filter(c => c._id !== cardId)
382
+ };
383
+
384
+ // Add card to target column with updated columnId
385
+ const movedCard = {
386
+ ...cardToMove,
387
+ columnId: toColumnId,
388
+ position: position ?? previousToData.Data.length,
389
+ _modified_at: new Date()
390
+ };
391
+
392
+ const newToData = {
393
+ ...previousToData,
394
+ Data: [...previousToData.Data, movedCard].sort((a, b) => a.position - b.position)
395
+ };
396
+
397
+ // Update cache optimistically
398
+ queryClient.setQueryData(fromQueryKey, newFromData);
399
+ queryClient.setQueryData(toQueryKey, newToData);
400
+ }
401
+ }
402
+
403
+ return {
404
+ previousFromData,
405
+ previousToData,
406
+ fromQueryKey,
407
+ toQueryKey,
408
+ fromColumnId,
409
+ toColumnId
410
+ };
411
+ },
412
+ onSuccess: async (result) => {
413
+ onCardMoveRef.current?.(
414
+ { _id: result.cardId } as any,
415
+ result.fromColumnId,
416
+ result.toColumnId
417
+ );
418
+ },
419
+ onError: (error, _variables, context) => {
420
+ // Rollback optimistic update on error
421
+ if (context?.previousFromData && context?.fromQueryKey) {
422
+ queryClient.setQueryData(context.fromQueryKey, context.previousFromData);
423
+ }
424
+ if (context?.previousToData && context?.toQueryKey) {
425
+ queryClient.setQueryData(context.toQueryKey, context.previousToData);
426
+ }
427
+ onErrorRef.current?.(error as Error);
428
+ },
429
+ onSettled: (_data, _error, variables) => {
430
+ // Invalidate queries to ensure sync with server
431
+ const fromOpts = getColumnApiOptions(variables.fromColumnId);
432
+ const toOpts = getColumnApiOptions(variables.toColumnId);
433
+ queryClient.invalidateQueries({ queryKey: ["kanban-cards", dataSource, variables.fromColumnId, fromOpts] });
434
+ queryClient.invalidateQueries({ queryKey: ["kanban-cards", dataSource, variables.toColumnId, toOpts] });
435
+ }
436
+ });
437
+
438
+ const reorderCardsMutation = useMutation({
439
+ mutationFn: async ({ cardIds, columnId }: { cardIds: string[]; columnId: string }) => {
440
+ const updates = cardIds.map((id, index) => ({ id, position: index, columnId }));
441
+ await Promise.all(
442
+ updates.map((update) =>
443
+ api<KanbanCard<T>>(dataSource).update(update.id, { position: update.position, columnId: update.columnId } as Partial<KanbanCard<T>>)
444
+ )
445
+ );
446
+ },
447
+ onMutate: async ({ columnId }) => {
448
+ // Optimistic reorder restricted to single column
449
+ const opts = getColumnApiOptions(columnId);
450
+ const queryKey = ["kanban-cards", dataSource, columnId, opts];
451
+ await queryClient.cancelQueries({ queryKey });
452
+ // Can implement optimistic reorder here if we want to parse cardIds
453
+ // But simpler to just invalidate.
454
+ return {};
455
+ },
456
+ onSuccess: () => {},
457
+ onError: (error, _variables, _context) => {
458
+ onErrorRef.current?.(error as Error);
459
+ },
460
+ onSettled: (_data, _error, variables) => {
461
+ const opts = getColumnApiOptions(variables.columnId);
462
+ queryClient.invalidateQueries({ queryKey: ["kanban-cards", dataSource, variables.columnId, opts] });
463
+ }
464
+ });
465
+
466
+ // ============================================================
467
+ // DRAG & DROP INTEGRATION
468
+ // ============================================================
469
+
470
+ const handleCardMove = useCallback(
471
+ async (card: KanbanCard<T>, fromColumnId: string, toColumnId: string) => {
472
+ try {
473
+ await moveCardMutation.mutateAsync({
474
+ cardId: card._id,
475
+ fromColumnId,
476
+ toColumnId,
477
+ position: undefined, // Let the backend calculate optimal position
478
+ });
479
+ } catch (error) {
480
+ // Error already handled in mutation onError
481
+ }
482
+ },
483
+ [moveCardMutation]
484
+ );
485
+
486
+ // ============================================================
487
+ // COMPUTED VALUES (Moved up for dependencies)
488
+ // ============================================================
489
+
490
+ const processedColumns = useMemo(() => {
491
+ // Map column configs to KanbanColumn structure using query results
492
+ return columnConfigs
493
+ .sort((a, b) => a.position - b.position)
494
+ .map((config, index) => {
495
+ const query = columnQueries[index];
496
+ const cards = query.data?.Data || [];
497
+
498
+ return {
499
+ _id: config.id,
500
+ title: config.title,
501
+ position: config.position,
502
+ color: config.color,
503
+ limit: config.limit,
504
+ cards: cards.sort((a, b) => a.position - b.position),
505
+ // We can expose loading/error state per column here if needed in the future
506
+ _created_at: new Date(),
507
+ _modified_at: new Date(),
508
+ };
509
+ });
510
+ }, [columnConfigs, columnQueries]);
511
+
512
+ const dragDropManager = useDragDropManager<T>({
513
+ onCardMove: handleCardMove,
514
+ onError: onErrorRef.current,
515
+ columns: processedColumns,
516
+ announceMove: (card, fromColumn, toColumn) => {
517
+ console.log(
518
+ `Kanban: Moved "${card.title}" from ${fromColumn} to ${toColumn}`
519
+ );
520
+ },
521
+ });
522
+
523
+ // ============================================================
524
+ // PROP GETTERS
525
+ // ============================================================
526
+
527
+ const getCardProps = useCallback(
528
+ (card: KanbanCard<T>) => ({
529
+ draggable: true,
530
+ role: "option",
531
+ "aria-selected": enableDragDrop && dragDropManager.draggedCard?._id === card._id,
532
+ "aria-grabbed": enableDragDrop && dragDropManager.draggedCard?._id === card._id,
533
+ onDragStart: (e: any) => {
534
+ if (!enableDragDrop) return;
535
+ // Abstracting the dataTransfer logic
536
+ e.dataTransfer.setData("text/plain", JSON.stringify(card));
537
+ // Use native event if available (ShadCN/Radix sometimes wraps events) or fallback to e
538
+ const nativeEvent = e.nativeEvent || e;
539
+ dragDropManager.handleDragStart(nativeEvent, card);
540
+ },
541
+ onDragEnd: dragDropManager.handleDragEnd,
542
+ onKeyDown: (e: any) => {
543
+ if (!enableDragDrop) return;
544
+ const nativeEvent = e.nativeEvent || e;
545
+ dragDropManager.handleKeyDown(nativeEvent, card);
546
+ }
547
+ }),
548
+ [enableDragDrop, dragDropManager]
549
+ );
550
+
551
+ const getColumnProps = useCallback(
552
+ (columnId: string) => ({
553
+ "data-column-id": columnId,
554
+ role: "listbox",
555
+ onDragOver: (e: any) => {
556
+ if (!enableDragDrop) return;
557
+ const nativeEvent = e.nativeEvent || e;
558
+ dragDropManager.handleDragOver(nativeEvent, columnId);
559
+ },
560
+ onDrop: (e: any) => {
561
+ if (!enableDragDrop) return;
562
+ const nativeEvent = e.nativeEvent || e;
563
+ dragDropManager.handleDrop(nativeEvent, columnId);
564
+ }
565
+ }),
566
+ [enableDragDrop, dragDropManager]
567
+ );
568
+
569
+ // ============================================================
570
+ // SEARCH OPERATIONS
571
+ // ============================================================
572
+
573
+ const setSearchQuery = useCallback((value: string) => {
574
+ setSearch({ query: value });
575
+ }, []);
576
+
577
+ const clearSearch = useCallback(() => {
578
+ setSearch({ query: "" });
579
+ }, []);
580
+
581
+
582
+
583
+ // ============================================================
584
+ // PROP GETTERS
585
+ // ============================================================
586
+
587
+
588
+
589
+ const totalCards = countData?.Count || 0;
590
+
591
+ const isLoading = isLoadingCards || isLoadingCount;
592
+ const isFetching = isFetchingCards;
593
+ const isUpdating =
594
+ createCardMutation.isPending ||
595
+ updateCardMutation.isPending ||
596
+ deleteCardMutation.isPending ||
597
+ moveCardMutation.isPending ||
598
+ reorderCardsMutation.isPending;
599
+
600
+ const error = cardsError || countError;
601
+
602
+ // ============================================================
603
+ // REFETCH OPERATIONS
604
+ // ============================================================
605
+
606
+ const refetch = useCallback(async (): Promise<void> => {
607
+ await refetchCards();
608
+ }, [refetchCards]);
609
+
610
+ const refresh = useCallback(async (): Promise<void> => {
611
+ await queryClient.invalidateQueries({
612
+ predicate: (query) =>
613
+ query.queryKey[0] === "kanban-cards" ||
614
+ query.queryKey[0] === "kanban-count",
615
+ });
616
+ }, [queryClient]);
617
+
618
+ // ============================================================
619
+ // ERROR HANDLING
620
+ // ============================================================
621
+
622
+ useEffect(() => {
623
+ if (error && onErrorRef.current) {
624
+ onErrorRef.current(error as Error);
625
+ }
626
+ }, [error]);
627
+
628
+ // ============================================================
629
+ // RETURN OBJECT
630
+ // ============================================================
631
+
632
+ return {
633
+ // Data
634
+ columns: processedColumns,
635
+ totalCards,
636
+
637
+ // Loading States
638
+ isLoading,
639
+ isFetching,
640
+ isUpdating,
641
+
642
+ // Error Handling
643
+ error: error as Error | null,
644
+
645
+ // Card Operations (Flat Access)
646
+ createCard: createCardMutation.mutateAsync,
647
+ updateCard: useCallback(
648
+ async (id: string, updates: Partial<KanbanCard<T>>) => {
649
+ await updateCardMutation.mutateAsync({ id, updates });
650
+ },
651
+ [updateCardMutation]
652
+ ),
653
+ deleteCard: useCallback(
654
+ async (id: string) => {
655
+ await deleteCardMutation.mutateAsync(id);
656
+ },
657
+ [deleteCardMutation]
658
+ ),
659
+ moveCard: useCallback(
660
+ async (cardId: string, toColumnId: string, position?: number, fromColumnId?: string) => {
661
+ // If fromColumnId is not provided, we need to find it
662
+ if (!fromColumnId) {
663
+ // Find the card in the columns to get its current columnId
664
+ for (const column of processedColumns) {
665
+ const card = column.cards.find(c => c._id === cardId);
666
+ if (card) {
667
+ fromColumnId = column._id;
668
+ break;
669
+ }
670
+ }
671
+ if (!fromColumnId) {
672
+ throw new Error(`Card ${cardId} not found in any column`);
673
+ }
674
+ }
675
+ await moveCardMutation.mutateAsync({ cardId, fromColumnId, toColumnId, position });
676
+ },
677
+ [moveCardMutation, processedColumns]
678
+ ),
679
+ reorderCards: useCallback(
680
+ async (cardIds: string[], columnId: string) => {
681
+ await reorderCardsMutation.mutateAsync({ cardIds, columnId });
682
+ },
683
+ [reorderCardsMutation]
684
+ ),
685
+
686
+ // Search (Flat Access)
687
+ searchQuery: search.query,
688
+ setSearchQuery,
689
+ clearSearch,
690
+
691
+ // Filter (Nested - following useTable pattern)
692
+ filter: filterHook,
693
+
694
+ // Drag Drop (Flat Access)
695
+ isDragging: enableDragDrop ? dragDropManager.isDragging : false,
696
+ draggedCard: enableDragDrop ? dragDropManager.draggedCard : null,
697
+ dragOverColumn: enableDragDrop ? dragDropManager.dragOverColumn : null,
698
+ handleDragStart: enableDragDrop
699
+ ? dragDropManager.handleDragStart
700
+ : () => {},
701
+ handleDragOver: enableDragDrop ? dragDropManager.handleDragOver : () => {},
702
+ handleDrop: enableDragDrop ? dragDropManager.handleDrop : () => {},
703
+ handleDragEnd: enableDragDrop ? dragDropManager.handleDragEnd : () => {},
704
+ handleKeyDown: enableDragDrop ? dragDropManager.handleKeyDown : () => {},
705
+
706
+ // Prop Getters
707
+ // Prop Getters
708
+ getCardProps: enableDragDrop ? getCardProps : (_card: any) => ({} as any),
709
+ getColumnProps: enableDragDrop ? getColumnProps : (_columnId: string) => ({} as any),
710
+
711
+
712
+
713
+ // Load More (Per Column)
714
+ loadMore: useCallback((columnId: string) => {
715
+ setColumnPagination((prev) => ({
716
+ ...prev,
717
+ [columnId]: (prev[columnId] || 10) + 10,
718
+ }));
719
+ }, []),
720
+
721
+ // Utilities
722
+ refetch,
723
+ refresh,
724
+ };
725
+ }