@ram_28/kf-ai-sdk 1.0.16 → 1.0.19

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.
@@ -0,0 +1,421 @@
1
+ # useKanban
2
+
3
+ ## Brief Description
4
+
5
+ - Provides complete kanban board state management including column configuration, card CRUD operations, and drag-and-drop functionality
6
+ - Integrates with `useFilter` for filtering cards across the board with support for nested condition groups
7
+ - Includes built-in drag-and-drop handlers and prop getters (`getCardProps`, `getColumnProps`) for easy integration with any UI library
8
+ - Handles optimistic updates for card operations with automatic rollback on failure
9
+
10
+ ## Type Reference
11
+
12
+ ```typescript
13
+ import { useKanban } from "@ram_28/kf-ai-sdk/kanban";
14
+ import { isCondition, isConditionGroup } from "@ram_28/kf-ai-sdk/filter";
15
+ import type {
16
+ UseKanbanOptionsType,
17
+ UseKanbanReturnType,
18
+ KanbanCardType,
19
+ KanbanColumnType,
20
+ ColumnConfigType,
21
+ } from "@ram_28/kf-ai-sdk/kanban/types";
22
+ import type {
23
+ ConditionType,
24
+ ConditionGroupType,
25
+ ConditionGroupOperatorType,
26
+ FilterType,
27
+ UseFilterReturnType,
28
+ } from "@ram_28/kf-ai-sdk/filter/types";
29
+
30
+ // Static column configuration
31
+ interface ColumnConfigType {
32
+ id: string;
33
+ title: string;
34
+ position: number;
35
+ color?: string;
36
+ limit?: number;
37
+ }
38
+
39
+ // Kanban card with custom fields
40
+ type KanbanCardType<T = Record<string, any>> = {
41
+ _id: string;
42
+ title: string;
43
+ columnId: string;
44
+ position: number;
45
+ _created_at?: Date;
46
+ _modified_at?: Date;
47
+ } & T;
48
+
49
+ // Kanban column with cards
50
+ interface KanbanColumnType<T = Record<string, any>> {
51
+ _id: string;
52
+ title: string;
53
+ position: number;
54
+ cards: KanbanCardType<T>[];
55
+ color?: string;
56
+ limit?: number;
57
+ }
58
+
59
+ // Hook options
60
+ interface UseKanbanOptionsType<T> {
61
+ source: string;
62
+ columns: ColumnConfigType[];
63
+ enableDragDrop?: boolean;
64
+ enableFiltering?: boolean;
65
+ enableSearch?: boolean;
66
+ initialState?: {
67
+ filters?: Array<ConditionType | ConditionGroupType>;
68
+ filterOperator?: ConditionGroupOperatorType;
69
+ search?: string;
70
+ };
71
+ onCardMove?: (card: KanbanCardType<T>, fromColumnId: string, toColumnId: string) => void;
72
+ onCardCreate?: (card: KanbanCardType<T>) => void;
73
+ onCardUpdate?: (card: KanbanCardType<T>) => void;
74
+ onCardDelete?: (cardId: string) => void;
75
+ onError?: (error: Error) => void;
76
+ }
77
+
78
+ // Hook return type
79
+ interface UseKanbanReturnType<T> {
80
+ // Data
81
+ columns: KanbanColumnType<T>[];
82
+ totalCards: number;
83
+
84
+ // Loading states
85
+ isLoading: boolean;
86
+ isFetching: boolean;
87
+ isUpdating: boolean;
88
+ error: Error | null;
89
+
90
+ // Card operations
91
+ createCard: (card: Partial<KanbanCardType<T>> & { columnId: string }) => Promise<string>;
92
+ updateCard: (id: string, updates: Partial<KanbanCardType<T>>) => Promise<void>;
93
+ deleteCard: (id: string) => Promise<void>;
94
+ moveCard: (cardId: string, toColumnId: string, position?: number) => Promise<void>;
95
+ reorderCards: (cardIds: string[], columnId: string) => Promise<void>;
96
+
97
+ // Search
98
+ searchQuery: string;
99
+ setSearchQuery: (value: string) => void;
100
+ clearSearch: () => void;
101
+
102
+ // Filter (uses useFilter internally)
103
+ filter: UseFilterReturnType;
104
+
105
+ // Drag drop state
106
+ isDragging: boolean;
107
+ draggedCard: KanbanCardType<T> | null;
108
+ dragOverColumn: string | null;
109
+
110
+ // Prop getters
111
+ getCardProps: (card: KanbanCardType<T>) => {
112
+ draggable: boolean;
113
+ role: string;
114
+ "aria-selected": boolean;
115
+ "aria-grabbed": boolean;
116
+ onDragStart: (e: any) => void;
117
+ onDragEnd: () => void;
118
+ onKeyDown: (e: any) => void;
119
+ };
120
+ getColumnProps: (columnId: string) => {
121
+ "data-column-id": string;
122
+ role: string;
123
+ onDragOver: (e: any) => void;
124
+ onDrop: (e: any) => void;
125
+ };
126
+
127
+ // Utilities
128
+ refetch: () => Promise<void>;
129
+ loadMore: (columnId: string) => void;
130
+ }
131
+ ```
132
+
133
+ ## Usage Example
134
+
135
+ ```tsx
136
+ import { useKanban } from "@ram_28/kf-ai-sdk/kanban";
137
+ import { isCondition, isConditionGroup } from "@ram_28/kf-ai-sdk/filter";
138
+ import type {
139
+ UseKanbanOptionsType,
140
+ UseKanbanReturnType,
141
+ KanbanCardType,
142
+ KanbanColumnType,
143
+ ColumnConfigType,
144
+ } from "@ram_28/kf-ai-sdk/kanban/types";
145
+ import type {
146
+ ConditionType,
147
+ ConditionGroupType,
148
+ ConditionGroupOperatorType,
149
+ FilterType,
150
+ UseFilterReturnType,
151
+ } from "@ram_28/kf-ai-sdk/filter/types";
152
+ import { ProductRestocking, ProductRestockingType } from "../sources";
153
+ import { Roles } from "../sources/roles";
154
+
155
+ // Get the typed restocking record for the InventoryManager role
156
+ type RestockingRecord = ProductRestockingType<typeof Roles.InventoryManager>;
157
+
158
+ // Define custom card fields that extend the base KanbanCardType
159
+ interface RestockingCardData {
160
+ productTitle: string;
161
+ productSKU: string;
162
+ currentStock: number;
163
+ quantityOrdered: number;
164
+ warehouse: "Warehouse_A" | "Warehouse_B" | "Warehouse_C";
165
+ priority: "Low" | "Medium" | "High" | "Critical";
166
+ }
167
+
168
+ // Full card type with base fields + custom data
169
+ type RestockingCard = KanbanCardType<RestockingCardData>;
170
+
171
+ function InventoryRestockingBoard() {
172
+ // Instantiate the ProductRestocking source with role
173
+ const restocking = new ProductRestocking(Roles.InventoryManager);
174
+
175
+ // Static column configurations
176
+ const columnConfigs: ColumnConfigType[] = [
177
+ { id: "LowStockAlert", title: "Low Stock Alert", position: 0, color: "red" },
178
+ { id: "OrderPlaced", title: "Order Placed", position: 1, color: "yellow" },
179
+ { id: "InTransit", title: "In Transit", position: 2, color: "blue" },
180
+ { id: "Received", title: "Received & Restocked", position: 3, color: "green", limit: 20 },
181
+ ];
182
+
183
+ // Hook configuration with full options
184
+ const options: UseKanbanOptionsType<RestockingCardData> = {
185
+ source: restocking._id, // Use the Business Object ID from the source class
186
+ columns: columnConfigs,
187
+ enableDragDrop: true,
188
+ enableFiltering: true,
189
+ enableSearch: true,
190
+ initialState: {
191
+ filterOperator: "And",
192
+ },
193
+ onCardMove: (card: RestockingCard, fromColumnId: string, toColumnId: string) => {
194
+ console.log(`Moved "${card.productTitle}" from ${fromColumnId} to ${toColumnId}`);
195
+ },
196
+ onCardCreate: (card: RestockingCard) => console.log("Created:", card.productTitle),
197
+ onCardUpdate: (card: RestockingCard) => console.log("Updated:", card._id),
198
+ onCardDelete: (cardId: string) => console.log("Deleted:", cardId),
199
+ onError: (error: Error) => console.error("Kanban error:", error.message),
200
+ };
201
+
202
+ const kanban: UseKanbanReturnType<RestockingCardData> = useKanban<RestockingCardData>(options);
203
+
204
+ // Access filter state (UseFilterReturnType)
205
+ const filterState: UseFilterReturnType = kanban.filter;
206
+
207
+ // Create a new card
208
+ const handleCreateCard = async (columnId: string) => {
209
+ const cardId: string = await kanban.createCard({
210
+ columnId,
211
+ title: "New Restock Task",
212
+ productTitle: "Sample Product",
213
+ productSKU: "SKU-001",
214
+ currentStock: 5,
215
+ quantityOrdered: 50,
216
+ warehouse: "Warehouse_A",
217
+ priority: "High",
218
+ });
219
+ console.log("Created card with id:", cardId);
220
+ };
221
+
222
+ // Update card priority
223
+ const handleUpdatePriority = async (card: RestockingCard, priority: RestockingCardData["priority"]) => {
224
+ await kanban.updateCard(card._id, { priority });
225
+ };
226
+
227
+ // Delete a card
228
+ const handleDeleteCard = async (cardId: string) => {
229
+ await kanban.deleteCard(cardId);
230
+ };
231
+
232
+ // Move card between columns
233
+ const handleMoveCard = async (cardId: string, toColumnId: string) => {
234
+ await kanban.moveCard(cardId, toColumnId);
235
+ };
236
+
237
+ // Filter by priority
238
+ const filterByPriority = (priority: string) => {
239
+ kanban.filter.clearAllConditions();
240
+ if (priority !== "all") {
241
+ kanban.filter.addCondition({
242
+ Operator: "EQ",
243
+ LHSField: "priority",
244
+ RHSValue: priority,
245
+ });
246
+ }
247
+ };
248
+
249
+ // Add complex filter (Critical OR High priority)
250
+ const addUrgentFilter = () => {
251
+ const groupId = kanban.filter.addConditionGroup("Or");
252
+ kanban.filter.addCondition({
253
+ Operator: "EQ",
254
+ LHSField: "priority",
255
+ RHSValue: "Critical",
256
+ }, groupId);
257
+ kanban.filter.addCondition({
258
+ Operator: "EQ",
259
+ LHSField: "priority",
260
+ RHSValue: "High",
261
+ }, groupId);
262
+ };
263
+
264
+ // Render active filters using type guards
265
+ const renderActiveFilters = () => (
266
+ <div className="active-filters">
267
+ {kanban.filter.items.map((item) => {
268
+ if (isCondition(item)) {
269
+ return (
270
+ <span key={item.id} className="filter-tag">
271
+ {item.LHSField} {item.Operator} {String(item.RHSValue)}
272
+ <button onClick={() => kanban.filter.removeCondition(item.id!)}>×</button>
273
+ </span>
274
+ );
275
+ }
276
+ if (isConditionGroup(item)) {
277
+ return (
278
+ <span key={item.id} className="filter-group-tag">
279
+ {item.Operator} Group ({item.Condition.length})
280
+ <button onClick={() => kanban.filter.removeCondition(item.id!)}>×</button>
281
+ </span>
282
+ );
283
+ }
284
+ return null;
285
+ })}
286
+ </div>
287
+ );
288
+
289
+ // Get filter payload for debugging
290
+ const getFilterPayload = (): FilterType | undefined => {
291
+ return kanban.filter.payload;
292
+ };
293
+
294
+ if (kanban.isLoading) {
295
+ return <div>Loading board...</div>;
296
+ }
297
+
298
+ if (kanban.error) {
299
+ return (
300
+ <div>
301
+ <p>Error: {kanban.error.message}</p>
302
+ <button onClick={() => kanban.refetch()}>Retry</button>
303
+ </div>
304
+ );
305
+ }
306
+
307
+ return (
308
+ <div className="kanban-board">
309
+ {/* Search */}
310
+ <div className="search-bar">
311
+ <input
312
+ type="text"
313
+ placeholder="Search products..."
314
+ value={kanban.searchQuery}
315
+ onChange={(e) => kanban.setSearchQuery(e.target.value)}
316
+ />
317
+ <button onClick={kanban.clearSearch}>Clear</button>
318
+ </div>
319
+
320
+ {/* Filter Controls */}
321
+ <div className="filter-controls">
322
+ <button onClick={() => filterByPriority("Critical")}>Critical</button>
323
+ <button onClick={() => filterByPriority("High")}>High</button>
324
+ <button onClick={addUrgentFilter}>Urgent (Critical OR High)</button>
325
+ <button onClick={() => filterByPriority("all")}>All</button>
326
+ {kanban.filter.hasConditions && (
327
+ <button onClick={() => kanban.filter.clearAllConditions()}>Clear Filters</button>
328
+ )}
329
+ <span>Logic: {kanban.filter.operator}</span>
330
+ </div>
331
+
332
+ {/* Active Filters */}
333
+ {renderActiveFilters()}
334
+
335
+ {/* Board Stats */}
336
+ <div className="board-stats">
337
+ <span>Total cards: {kanban.totalCards}</span>
338
+ {kanban.isDragging && (
339
+ <span>Dragging: {kanban.draggedCard?.productTitle}</span>
340
+ )}
341
+ {kanban.isUpdating && <span>Updating...</span>}
342
+ </div>
343
+
344
+ {/* Kanban Columns */}
345
+ <div className="columns-container">
346
+ {kanban.columns.map((column: KanbanColumnType<RestockingCardData>) => (
347
+ <div
348
+ key={column._id}
349
+ className={`column ${kanban.dragOverColumn === column._id ? "drag-over" : ""}`}
350
+ style={{ backgroundColor: column.color }}
351
+ {...kanban.getColumnProps(column._id)}
352
+ >
353
+ {/* Column Header */}
354
+ <div className="column-header">
355
+ <h3>{column.title}</h3>
356
+ <span>
357
+ {column.cards.length}
358
+ {column.limit && ` / ${column.limit}`}
359
+ </span>
360
+ <button onClick={() => handleCreateCard(column._id)}>+ Add</button>
361
+ </div>
362
+
363
+ {/* Cards */}
364
+ <div className="cards-container">
365
+ {column.cards.map((card: RestockingCard) => (
366
+ <div
367
+ key={card._id}
368
+ className={`card ${kanban.draggedCard?._id === card._id ? "dragging" : ""}`}
369
+ {...kanban.getCardProps(card)}
370
+ >
371
+ <div className="card-header">
372
+ <span className={`priority-badge ${card.priority.toLowerCase()}`}>
373
+ {card.priority}
374
+ </span>
375
+ <button onClick={() => handleDeleteCard(card._id)}>×</button>
376
+ </div>
377
+ <h4>{card.productTitle}</h4>
378
+ <p>SKU: {card.productSKU}</p>
379
+ <p>Stock: {card.currentStock} / Ordered: {card.quantityOrdered}</p>
380
+ <p>Warehouse: {card.warehouse}</p>
381
+ <div className="card-actions">
382
+ <button onClick={() => handleUpdatePriority(card, "Critical")}>
383
+ Set Critical
384
+ </button>
385
+ <button onClick={() => handleMoveCard(card._id, "Received")}>
386
+ Mark Received
387
+ </button>
388
+ </div>
389
+ </div>
390
+ ))}
391
+ </div>
392
+
393
+ {/* Load More */}
394
+ {column.cards.length >= 10 && (
395
+ <button
396
+ className="load-more"
397
+ onClick={() => kanban.loadMore(column._id)}
398
+ >
399
+ Load More
400
+ </button>
401
+ )}
402
+ </div>
403
+ ))}
404
+ </div>
405
+
406
+ {/* Board Actions */}
407
+ <div className="board-actions">
408
+ <button onClick={() => kanban.refetch()} disabled={kanban.isFetching}>
409
+ {kanban.isFetching ? "Refreshing..." : "Refresh Board"}
410
+ </button>
411
+ </div>
412
+
413
+ {/* Debug: Filter payload */}
414
+ <details>
415
+ <summary>Filter Payload (Debug)</summary>
416
+ <pre>{JSON.stringify(getFilterPayload(), null, 2)}</pre>
417
+ </details>
418
+ </div>
419
+ );
420
+ }
421
+ ```