@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.
- package/LICENSE +21 -0
- package/README.md +840 -0
- package/dist/api/client.d.ts +78 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/datetime.d.ts +21 -0
- package/dist/api/datetime.d.ts.map +1 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/metadata.d.ts +75 -0
- package/dist/api/metadata.d.ts.map +1 -0
- package/dist/components/hooks/index.d.ts +8 -0
- package/dist/components/hooks/index.d.ts.map +1 -0
- package/dist/components/hooks/useFilter/index.d.ts +5 -0
- package/dist/components/hooks/useFilter/index.d.ts.map +1 -0
- package/dist/components/hooks/useFilter/payloadBuilder.utils.d.ts +33 -0
- package/dist/components/hooks/useFilter/payloadBuilder.utils.d.ts.map +1 -0
- package/dist/components/hooks/useFilter/types.d.ts +137 -0
- package/dist/components/hooks/useFilter/types.d.ts.map +1 -0
- package/dist/components/hooks/useFilter/useFilter.d.ts +3 -0
- package/dist/components/hooks/useFilter/useFilter.d.ts.map +1 -0
- package/dist/components/hooks/useFilter/validation.utils.d.ts +38 -0
- package/dist/components/hooks/useFilter/validation.utils.d.ts.map +1 -0
- package/dist/components/hooks/useForm/apiClient.d.ts +71 -0
- package/dist/components/hooks/useForm/apiClient.d.ts.map +1 -0
- package/dist/components/hooks/useForm/expressionValidator.utils.d.ts +28 -0
- package/dist/components/hooks/useForm/expressionValidator.utils.d.ts.map +1 -0
- package/dist/components/hooks/useForm/index.d.ts +6 -0
- package/dist/components/hooks/useForm/index.d.ts.map +1 -0
- package/dist/components/hooks/useForm/optimizedExpressionValidator.utils.d.ts +88 -0
- package/dist/components/hooks/useForm/optimizedExpressionValidator.utils.d.ts.map +1 -0
- package/dist/components/hooks/useForm/ruleClassifier.utils.d.ts +28 -0
- package/dist/components/hooks/useForm/ruleClassifier.utils.d.ts.map +1 -0
- package/dist/components/hooks/useForm/schemaParser.utils.d.ts +29 -0
- package/dist/components/hooks/useForm/schemaParser.utils.d.ts.map +1 -0
- package/dist/components/hooks/useForm/types.d.ts +412 -0
- package/dist/components/hooks/useForm/types.d.ts.map +1 -0
- package/dist/components/hooks/useForm/useForm.d.ts +3 -0
- package/dist/components/hooks/useForm/useForm.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/apiClient.d.ts +99 -0
- package/dist/components/hooks/useKanban/apiClient.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/context.d.ts +4 -0
- package/dist/components/hooks/useKanban/context.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/dragDropManager.d.ts +27 -0
- package/dist/components/hooks/useKanban/dragDropManager.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/index.d.ts +6 -0
- package/dist/components/hooks/useKanban/index.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/types.d.ts +438 -0
- package/dist/components/hooks/useKanban/types.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/useKanban.d.ts +3 -0
- package/dist/components/hooks/useKanban/useKanban.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/useKanbanSimple.d.ts +62 -0
- package/dist/components/hooks/useKanban/useKanbanSimple.d.ts.map +1 -0
- package/dist/components/hooks/useTable/index.d.ts +3 -0
- package/dist/components/hooks/useTable/index.d.ts.map +1 -0
- package/dist/components/hooks/useTable/types.d.ts +107 -0
- package/dist/components/hooks/useTable/types.d.ts.map +1 -0
- package/dist/components/hooks/useTable/useTable.d.ts +8 -0
- package/dist/components/hooks/useTable/useTable.d.ts.map +1 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/ui/index.d.ts +2 -0
- package/dist/components/ui/index.d.ts.map +1 -0
- package/dist/components/ui/kanban/Kanban.d.ts +12 -0
- package/dist/components/ui/kanban/Kanban.d.ts.map +1 -0
- package/dist/components/ui/kanban/index.d.ts +2 -0
- package/dist/components/ui/kanban/index.d.ts.map +1 -0
- package/dist/index.cjs +45 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +6522 -0
- package/dist/types/base-fields.d.ts +182 -0
- package/dist/types/base-fields.d.ts.map +1 -0
- package/dist/types/common.d.ts +238 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/cn.d.ts +7 -0
- package/dist/utils/cn.d.ts.map +1 -0
- package/dist/utils/formatting.d.ts +52 -0
- package/dist/utils/formatting.d.ts.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/package.json +98 -0
- package/sdk/api/client.ts +447 -0
- package/sdk/api/datetime.ts +33 -0
- package/sdk/api/index.ts +61 -0
- package/sdk/api/metadata.ts +148 -0
- package/sdk/components/hooks/index.ts +34 -0
- package/sdk/components/hooks/useFilter/index.ts +37 -0
- package/sdk/components/hooks/useFilter/payloadBuilder.utils.ts +298 -0
- package/sdk/components/hooks/useFilter/types.ts +158 -0
- package/sdk/components/hooks/useFilter/useFilter.llm.txt +497 -0
- package/sdk/components/hooks/useFilter/useFilter.ts +494 -0
- package/sdk/components/hooks/useFilter/validation.utils.ts +401 -0
- package/sdk/components/hooks/useForm/apiClient.ts +441 -0
- package/sdk/components/hooks/useForm/expressionValidator.utils.ts +444 -0
- package/sdk/components/hooks/useForm/index.ts +64 -0
- package/sdk/components/hooks/useForm/optimizedExpressionValidator.utils.ts +482 -0
- package/sdk/components/hooks/useForm/ruleClassifier.utils.ts +424 -0
- package/sdk/components/hooks/useForm/schemaParser.utils.ts +519 -0
- package/sdk/components/hooks/useForm/types.ts +630 -0
- package/sdk/components/hooks/useForm/useForm.llm.txt +340 -0
- package/sdk/components/hooks/useForm/useForm.ts +821 -0
- package/sdk/components/hooks/useKanban/apiClient.ts +494 -0
- package/sdk/components/hooks/useKanban/context.ts +14 -0
- package/sdk/components/hooks/useKanban/dragDropManager.ts +529 -0
- package/sdk/components/hooks/useKanban/index.ts +63 -0
- package/sdk/components/hooks/useKanban/types.ts +606 -0
- package/sdk/components/hooks/useKanban/useKanban.llm.txt +482 -0
- package/sdk/components/hooks/useKanban/useKanban.ts +725 -0
- package/sdk/components/hooks/useKanban/useKanbanSimple.ts +389 -0
- package/sdk/components/hooks/useTable/index.ts +5 -0
- package/sdk/components/hooks/useTable/types.ts +154 -0
- package/sdk/components/hooks/useTable/useTable.llm.txt +344 -0
- package/sdk/components/hooks/useTable/useTable.ts +413 -0
- package/sdk/components/index.ts +15 -0
- package/sdk/components/ui/index.ts +2 -0
- package/sdk/components/ui/kanban/Kanban.tsx +134 -0
- package/sdk/components/ui/kanban/index.ts +11 -0
- package/sdk/index.ts +13 -0
- package/sdk/types/base-fields.ts +221 -0
- package/sdk/types/common.ts +306 -0
- package/sdk/types/index.ts +5 -0
- package/sdk/utils/cn.ts +10 -0
- package/sdk/utils/formatting.ts +212 -0
- 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
|
+
}
|