@critical-path/react 0.13.11 → 0.14.1

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/src/hooks.ts DELETED
@@ -1,1218 +0,0 @@
1
- import type { CreateAttachmentBody, CreateCommentBody, CreateTaskBody, UpdateCommentBody, UpdateDeliverableBody, UpdateTaskBody } from '@critical-path/core/schemas';
2
- import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
3
- import type {
4
- Project,
5
- Task,
6
- TaskStatus,
7
- Workflow,
8
- Comment,
9
- CommentReaction,
10
- Attachment,
11
- Deliverable,
12
- DeliverableSummary,
13
- CreateDeliverableInput,
14
- StatusDefinition,
15
- SemanticStatus,
16
- CriticalPathAnalysis,
17
- TimelineLadder,
18
- TimelineLadderOptions,
19
- TaskLadderView,
20
- AbstractionLevel,
21
- TaskMetrics,
22
- TaskProgressHistory,
23
- WorkloadDistribution,
24
- WorkloadDistributionOptions,
25
- WorkloadInterval,
26
- WorkloadGroupBy,
27
- WorkloadMetric
28
- } from '@critical-path/core';
29
- import { resolveStatusDefinition, isTempTaskId } from '@critical-path/core';
30
- import { registerWebMcpTools } from '@critical-path/mcp/web';
31
- import { useCriticalPathClient } from './provider.js';
32
-
33
- export function useWorkflows() {
34
- const client = useCriticalPathClient();
35
- const [workflows, setWorkflows] = useState<Workflow[]>([]);
36
- const [loading, setLoading] = useState(true);
37
- const [error, setError] = useState<Error | null>(null);
38
-
39
- const fetchWorkflows = useCallback(async () => {
40
- try {
41
- setLoading(true);
42
- const data = await client.getWorkflows();
43
- setWorkflows(data);
44
- setError(null);
45
- } catch (err) {
46
- setError(err instanceof Error ? err : new Error(String(err)));
47
- } finally {
48
- setLoading(false);
49
- }
50
- }, [client]);
51
-
52
- useEffect(() => {
53
- fetchWorkflows();
54
- }, [fetchWorkflows]);
55
-
56
- const createWorkflow = async (input: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>) => {
57
- try {
58
- const created = await client.createWorkflow(input);
59
- setWorkflows((prev) => [...prev, created]);
60
- return created;
61
- } catch (err) {
62
- const errorObj = err instanceof Error ? err : new Error(String(err));
63
- setError(errorObj);
64
- throw errorObj;
65
- }
66
- };
67
-
68
- const updateWorkflow = async (id: string, updates: Partial<Workflow>) => {
69
- try {
70
- const updated = await client.updateWorkflow(id, updates);
71
- setWorkflows((prev) => prev.map((w) => (w.id === id ? updated : w)));
72
- return updated;
73
- } catch (err) {
74
- const errorObj = err instanceof Error ? err : new Error(String(err));
75
- setError(errorObj);
76
- throw errorObj;
77
- }
78
- };
79
-
80
- const deleteWorkflow = async (id: string) => {
81
- try {
82
- await client.deleteWorkflow(id);
83
- setWorkflows((prev) => prev.filter((w) => w.id !== id));
84
- } catch (err) {
85
- const errorObj = err instanceof Error ? err : new Error(String(err));
86
- setError(errorObj);
87
- throw errorObj;
88
- }
89
- };
90
-
91
- return { workflows, loading, error, refresh: fetchWorkflows, createWorkflow, updateWorkflow, deleteWorkflow };
92
- }
93
-
94
- export function useTaskTransitions(taskId?: string) {
95
- const client = useCriticalPathClient();
96
- const [allowedTransitions, setAllowedTransitions] = useState<string[]>([]);
97
- const [loading, setLoading] = useState(false);
98
- const [error, setError] = useState<Error | null>(null);
99
-
100
- const fetchTransitions = useCallback(async () => {
101
- if (!taskId) {
102
- setAllowedTransitions([]);
103
- return;
104
- }
105
- try {
106
- setLoading(true);
107
- const data = await client.getAllowedTaskTransitions(taskId);
108
- setAllowedTransitions(data);
109
- setError(null);
110
- } catch (err) {
111
- setError(err instanceof Error ? err : new Error(String(err)));
112
- } finally {
113
- setLoading(false);
114
- }
115
- }, [client, taskId]);
116
-
117
- useEffect(() => {
118
- fetchTransitions();
119
- }, [fetchTransitions]);
120
-
121
- return { allowedTransitions, loading, error, refresh: fetchTransitions };
122
- }
123
-
124
- export function useProjects() {
125
- const client = useCriticalPathClient();
126
- const [projects, setProjects] = useState<Project[]>([]);
127
- const [loading, setLoading] = useState(true);
128
- const [error, setError] = useState<Error | null>(null);
129
-
130
- const fetchProjects = useCallback(async () => {
131
- try {
132
- setLoading(true);
133
- const data = await client.getProjects();
134
- setProjects(data);
135
- setError(null);
136
- } catch (err) {
137
- setError(err instanceof Error ? err : new Error(String(err)));
138
- } finally {
139
- setLoading(false);
140
- }
141
- }, [client]);
142
-
143
- useEffect(() => {
144
- fetchProjects();
145
- }, [fetchProjects]);
146
-
147
- const createProject = async (input: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>) => {
148
- const tempId = `temp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
149
- const now = new Date().toISOString();
150
- const tempProject: Project = {
151
- ...input,
152
- id: tempId,
153
- createdAt: now,
154
- updatedAt: now
155
- };
156
-
157
- setProjects((prev) => [...prev, tempProject]);
158
-
159
- try {
160
- const created = await client.createProject(input);
161
- setProjects((prev) => prev.map((p) => (p.id === tempId ? created : p)));
162
- return created;
163
- } catch (err) {
164
- setProjects((prev) => prev.filter((p) => p.id !== tempId));
165
- const errorObj = err instanceof Error ? err : new Error(String(err));
166
- setError(errorObj);
167
- throw errorObj;
168
- }
169
- };
170
-
171
- return { projects, loading, error, refresh: fetchProjects, createProject };
172
- }
173
-
174
- export function useTasks(projectId?: string) {
175
- const client = useCriticalPathClient();
176
- const [tasks, setTasks] = useState<Task[]>([]);
177
- const [loading, setLoading] = useState(true);
178
- const [error, setError] = useState<Error | null>(null);
179
-
180
- const fetchTasks = useCallback(async () => {
181
- try {
182
- setLoading(true);
183
- const data = await client.getTasks(projectId);
184
- setTasks(data);
185
- setError(null);
186
- } catch (err) {
187
- setError(err instanceof Error ? err : new Error(String(err)));
188
- } finally {
189
- setLoading(false);
190
- }
191
- }, [client, projectId]);
192
-
193
- const pendingCreationsRef = useRef<Map<string, Promise<Task>>>(new Map());
194
- const tempToRealIdMapRef = useRef<Map<string, string>>(new Map());
195
-
196
- useEffect(() => {
197
- fetchTasks();
198
- }, [fetchTasks]);
199
-
200
- const createTask = async (input: CreateTaskBody) => {
201
- const tempId = `temp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
202
- const now = new Date().toISOString();
203
- const tempTask: Task = {
204
- ...input,
205
- // Placeholders until the server responds with the workflow's real defaults
206
- status: input.status ?? 'todo',
207
- priority: input.priority ?? 'medium',
208
- id: tempId,
209
- createdAt: now,
210
- updatedAt: now
211
- };
212
-
213
- setTasks((prev) => [...prev, tempTask]);
214
-
215
- const promise = client.createTask(input);
216
- pendingCreationsRef.current.set(tempId, promise);
217
-
218
- try {
219
- const created = await promise;
220
- tempToRealIdMapRef.current.set(tempId, created.id);
221
- setTasks((prev) => prev.map((t) => (t.id === tempId ? created : t)));
222
- return created;
223
- } catch (err) {
224
- setTasks((prev) => prev.filter((t) => t.id !== tempId));
225
- const errorObj = err instanceof Error ? err : new Error(String(err));
226
- setError(errorObj);
227
- throw errorObj;
228
- } finally {
229
- pendingCreationsRef.current.delete(tempId);
230
- }
231
- };
232
-
233
- const updateTask = async (taskId: string, updates: UpdateTaskBody) => {
234
- let targetId = taskId;
235
- if (tempToRealIdMapRef.current.has(taskId)) {
236
- targetId = tempToRealIdMapRef.current.get(taskId)!;
237
- }
238
-
239
- let previousTask: Task | undefined;
240
-
241
- // 1. Immediately apply optimistic updates to local tasks
242
- setTasks((prev) => {
243
- previousTask = prev.find((t) => t.id === targetId || t.id === taskId);
244
- if (!previousTask) return prev;
245
- return prev.map((t) =>
246
- t.id === targetId || t.id === taskId
247
- ? { ...t, ...updates, updatedAt: new Date().toISOString() }
248
- : t
249
- );
250
- });
251
-
252
- // 2. If it's a temporary task, await pending creation if in flight
253
- if (isTempTaskId(targetId)) {
254
- const pendingPromise =
255
- pendingCreationsRef.current.get(targetId) ||
256
- (pendingCreationsRef.current.size === 1
257
- ? Array.from(pendingCreationsRef.current.values())[0]
258
- : undefined);
259
-
260
- if (pendingPromise) {
261
- try {
262
- const created = await pendingPromise;
263
- if (created && created.id) {
264
- targetId = created.id;
265
- tempToRealIdMapRef.current.set(taskId, targetId);
266
- }
267
- } catch {
268
- throw new Error(`Failed to update task: task creation failed for "${taskId}"`);
269
- }
270
- }
271
- }
272
-
273
- // If still a temp ID, the task exists purely in local state; avoid sending unresolvable ID to backend
274
- if (isTempTaskId(targetId)) {
275
- let resolvedTask: Task | undefined;
276
- setTasks((prev) => {
277
- resolvedTask = prev.find((t) => t.id === targetId || t.id === taskId);
278
- return prev;
279
- });
280
- return (resolvedTask || { id: targetId, ...updates }) as Task;
281
- }
282
-
283
- try {
284
- const updated = await client.updateTask(targetId, updates);
285
- setTasks((prev) =>
286
- prev.map((t) => (t.id === targetId || t.id === taskId ? updated : t))
287
- );
288
- return updated;
289
- } catch (err) {
290
- if (previousTask) {
291
- const revertTask = previousTask;
292
- setTasks((prev) =>
293
- prev.map((t) => (t.id === targetId || t.id === taskId ? revertTask : t))
294
- );
295
- }
296
- const errorObj = err instanceof Error ? err : new Error(String(err));
297
- setError(errorObj);
298
- throw errorObj;
299
- }
300
- };
301
-
302
- const updateTaskStatus = async (taskId: string, status: TaskStatus) => {
303
- return updateTask(taskId, { status });
304
- };
305
-
306
- const deleteTask = async (taskId: string) => {
307
- let targetId = taskId;
308
- if (tempToRealIdMapRef.current.has(taskId)) {
309
- targetId = tempToRealIdMapRef.current.get(taskId)!;
310
- }
311
-
312
- let previousTask: Task | undefined;
313
- let previousIndex = -1;
314
-
315
- setTasks((prev) => {
316
- previousIndex = prev.findIndex((t) => t.id === targetId || t.id === taskId);
317
- if (previousIndex !== -1) {
318
- previousTask = prev[previousIndex];
319
- }
320
- return prev.filter((t) => t.id !== targetId && t.id !== taskId);
321
- });
322
-
323
- if (isTempTaskId(targetId)) {
324
- const pendingPromise = pendingCreationsRef.current.get(targetId);
325
- if (pendingPromise) {
326
- try {
327
- const created = await pendingPromise;
328
- if (created && created.id) {
329
- await client.deleteTask(created.id);
330
- }
331
- } catch {
332
- // If creation failed anyway, task is already gone
333
- }
334
- }
335
- return;
336
- }
337
-
338
- try {
339
- await client.deleteTask(targetId);
340
- } catch (err) {
341
- if (previousTask && previousIndex !== -1) {
342
- const restoreTask = previousTask;
343
- const indexToInsert = previousIndex;
344
- setTasks((prev) => {
345
- const restored = [...prev];
346
- restored.splice(indexToInsert, 0, restoreTask);
347
- return restored;
348
- });
349
- }
350
- const errorObj = err instanceof Error ? err : new Error(String(err));
351
- setError(errorObj);
352
- throw errorObj;
353
- }
354
- };
355
-
356
- return { tasks, loading, error, refresh: fetchTasks, createTask, updateTask, updateTaskStatus, deleteTask };
357
- }
358
-
359
- export interface UseKanbanOptions {
360
- groupBy?: 'workflow' | 'semantic';
361
- customDefinitions?: StatusDefinition[];
362
- }
363
-
364
- export function useKanban(projectId?: string, options?: UseKanbanOptions) {
365
- const { tasks, loading, error, refresh, updateTaskStatus, createTask } = useTasks(projectId);
366
-
367
- const columns = useMemo<Record<string, Task[]>>(() => {
368
- if (options?.groupBy === 'semantic') {
369
- const semanticCols: Record<SemanticStatus, Task[]> = {
370
- not_started: [],
371
- in_progress: [],
372
- completed: [],
373
- canceled: []
374
- };
375
- for (const task of tasks) {
376
- const category = task.semanticStatus || resolveStatusDefinition(task.status, options?.customDefinitions).category;
377
- if (semanticCols[category]) {
378
- semanticCols[category].push(task);
379
- } else {
380
- semanticCols.not_started.push(task);
381
- }
382
- }
383
- return semanticCols;
384
- }
385
-
386
- const workflowCols: Record<string, Task[]> = {};
387
- if (options?.customDefinitions && options.customDefinitions.length > 0) {
388
- for (const def of options.customDefinitions) {
389
- workflowCols[def.key] = [];
390
- }
391
- } else {
392
- workflowCols.backlog = [];
393
- workflowCols.todo = [];
394
- workflowCols.in_progress = [];
395
- workflowCols.in_review = [];
396
- workflowCols.done = [];
397
- workflowCols.canceled = [];
398
- }
399
-
400
- for (const task of tasks) {
401
- if (!workflowCols[task.status]) {
402
- workflowCols[task.status] = [];
403
- }
404
- workflowCols[task.status].push(task);
405
- }
406
-
407
- return workflowCols;
408
- }, [tasks, options?.groupBy, options?.customDefinitions]);
409
-
410
- const moveTask = async (taskId: string, targetStatus: TaskStatus) => {
411
- return updateTaskStatus(taskId, targetStatus);
412
- };
413
-
414
- return { columns, tasks, loading, error, refresh, moveTask, createTask };
415
- }
416
-
417
- export interface ThreadedComment extends Comment {
418
- replies: ThreadedComment[];
419
- }
420
-
421
- export function useComments(taskId: string) {
422
- const client = useCriticalPathClient();
423
- const [comments, setComments] = useState<Comment[]>([]);
424
- const [loading, setLoading] = useState(true);
425
- const [error, setError] = useState<Error | null>(null);
426
-
427
- const fetchComments = useCallback(async () => {
428
- if (!taskId) {
429
- setComments([]);
430
- setLoading(false);
431
- return;
432
- }
433
- try {
434
- setLoading(true);
435
- const data = await client.getComments(taskId);
436
- setComments(data);
437
- setError(null);
438
- } catch (err) {
439
- setError(err instanceof Error ? err : new Error(String(err)));
440
- } finally {
441
- setLoading(false);
442
- }
443
- }, [client, taskId]);
444
-
445
- useEffect(() => {
446
- fetchComments();
447
- }, [fetchComments]);
448
-
449
- const addComment = async (input: Omit<CreateCommentBody, 'taskId'>) => {
450
- try {
451
- const created = await client.addComment({ ...input, taskId });
452
- setComments((prev) => [...prev, created]);
453
- return created;
454
- } catch (err) {
455
- const errorObj = err instanceof Error ? err : new Error(String(err));
456
- setError(errorObj);
457
- throw errorObj;
458
- }
459
- };
460
-
461
- const updateComment = async (id: string, updates: UpdateCommentBody) => {
462
- try {
463
- const updated = await client.updateComment(id, updates);
464
- setComments((prev) => prev.map((c) => (c.id === id ? updated : c)));
465
- return updated;
466
- } catch (err) {
467
- const errorObj = err instanceof Error ? err : new Error(String(err));
468
- setError(errorObj);
469
- throw errorObj;
470
- }
471
- };
472
-
473
- const deleteComment = async (id: string) => {
474
- try {
475
- await client.deleteComment(id);
476
- setComments((prev) => prev.filter((c) => c.id !== id));
477
- } catch (err) {
478
- const errorObj = err instanceof Error ? err : new Error(String(err));
479
- setError(errorObj);
480
- throw errorObj;
481
- }
482
- };
483
-
484
- const addReaction = async (
485
- commentId: string,
486
- emoji: string
487
- ) => {
488
- try {
489
- const payload = { emoji };
490
- const updated = await client.addCommentReaction(commentId, payload);
491
- setComments((prev) => prev.map((c) => (c.id === commentId ? updated : c)));
492
- return updated;
493
- } catch (err) {
494
- const errorObj = err instanceof Error ? err : new Error(String(err));
495
- setError(errorObj);
496
- throw errorObj;
497
- }
498
- };
499
-
500
- const removeReaction = async (
501
- commentId: string,
502
- emoji: string
503
- ) => {
504
- try {
505
- const payload = { emoji };
506
- const updated = await client.removeCommentReaction(commentId, payload);
507
- setComments((prev) => prev.map((c) => (c.id === commentId ? updated : c)));
508
- return updated;
509
- } catch (err) {
510
- const errorObj = err instanceof Error ? err : new Error(String(err));
511
- setError(errorObj);
512
- throw errorObj;
513
- }
514
- };
515
-
516
- // Build tree of threaded comments
517
- const threads = useMemo(() => {
518
- const map = new Map<string, ThreadedComment>();
519
- const roots: ThreadedComment[] = [];
520
-
521
- // First pass: wrap comments
522
- for (const c of comments) {
523
- map.set(c.id, { ...c, replies: [] });
524
- }
525
-
526
- // Second pass: link replies to parents
527
- for (const c of comments) {
528
- const threaded = map.get(c.id)!;
529
- if (c.parentId && map.has(c.parentId)) {
530
- map.get(c.parentId)!.replies.push(threaded);
531
- } else {
532
- roots.push(threaded);
533
- }
534
- }
535
-
536
- return roots;
537
- }, [comments]);
538
-
539
- return {
540
- comments,
541
- threads,
542
- loading,
543
- error,
544
- refresh: fetchComments,
545
- addComment,
546
- updateComment,
547
- deleteComment,
548
- addReaction,
549
- removeReaction
550
- };
551
- }
552
-
553
- export function useAttachments(filter?: { taskId?: string; projectId?: string; commentId?: string }) {
554
- const client = useCriticalPathClient();
555
- const [attachments, setAttachments] = useState<Attachment[]>([]);
556
- const [loading, setLoading] = useState(true);
557
- const [error, setError] = useState<Error | null>(null);
558
-
559
- const filterKey = JSON.stringify(filter || {});
560
-
561
- const fetchAttachments = useCallback(async () => {
562
- try {
563
- setLoading(true);
564
- const data = await client.getAttachments(filter);
565
- setAttachments(data);
566
- setError(null);
567
- } catch (err) {
568
- setError(err instanceof Error ? err : new Error(String(err)));
569
- } finally {
570
- setLoading(false);
571
- }
572
- }, [client, filterKey]);
573
-
574
- useEffect(() => {
575
- fetchAttachments();
576
- }, [fetchAttachments]);
577
-
578
- const createAttachment = async (input: Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>) => {
579
- try {
580
- const created = await client.createAttachment(input);
581
- setAttachments((prev) => [created, ...prev]);
582
- return created;
583
- } catch (err) {
584
- const errorObj = err instanceof Error ? err : new Error(String(err));
585
- setError(errorObj);
586
- throw errorObj;
587
- }
588
- };
589
-
590
- const deleteAttachment = async (id: string) => {
591
- try {
592
- await client.deleteAttachment(id);
593
- setAttachments((prev) => prev.filter((a) => a.id !== id));
594
- } catch (err) {
595
- const errorObj = err instanceof Error ? err : new Error(String(err));
596
- setError(errorObj);
597
- throw errorObj;
598
- }
599
- };
600
-
601
- return {
602
- attachments,
603
- loading,
604
- error,
605
- refresh: fetchAttachments,
606
- createAttachment,
607
- deleteAttachment
608
- };
609
- }
610
-
611
- export function useDeliverables(projectId?: string) {
612
- const client = useCriticalPathClient();
613
- const [deliverables, setDeliverables] = useState<Deliverable[]>([]);
614
- const [loading, setLoading] = useState(true);
615
- const [error, setError] = useState<Error | null>(null);
616
-
617
- const fetchDeliverables = useCallback(async () => {
618
- if (!projectId) {
619
- setDeliverables([]);
620
- setLoading(false);
621
- return;
622
- }
623
- try {
624
- setLoading(true);
625
- const data = await client.getDeliverables(projectId);
626
- setDeliverables(data);
627
- setError(null);
628
- } catch (err) {
629
- setError(err instanceof Error ? err : new Error(String(err)));
630
- } finally {
631
- setLoading(false);
632
- }
633
- }, [client, projectId]);
634
-
635
- useEffect(() => {
636
- fetchDeliverables();
637
- }, [fetchDeliverables]);
638
-
639
- const createDeliverable = async (input: CreateDeliverableInput) => {
640
- try {
641
- const created = await client.createDeliverable(input);
642
- setDeliverables((prev) => [...prev, created]);
643
- return created;
644
- } catch (err) {
645
- const errorObj = err instanceof Error ? err : new Error(String(err));
646
- setError(errorObj);
647
- throw errorObj;
648
- }
649
- };
650
-
651
- const updateDeliverable = async (id: string, updates: UpdateDeliverableBody) => {
652
- try {
653
- const updated = await client.updateDeliverable(id, updates);
654
- setDeliverables((prev) => prev.map((d) => (d.id === id ? updated : d)));
655
- return updated;
656
- } catch (err) {
657
- const errorObj = err instanceof Error ? err : new Error(String(err));
658
- setError(errorObj);
659
- throw errorObj;
660
- }
661
- };
662
-
663
- const deleteDeliverable = async (id: string) => {
664
- try {
665
- await client.deleteDeliverable(id);
666
- setDeliverables((prev) => prev.filter((d) => d.id !== id));
667
- } catch (err) {
668
- const errorObj = err instanceof Error ? err : new Error(String(err));
669
- setError(errorObj);
670
- throw errorObj;
671
- }
672
- };
673
-
674
- return {
675
- deliverables,
676
- loading,
677
- error,
678
- refresh: fetchDeliverables,
679
- createDeliverable,
680
- updateDeliverable,
681
- deleteDeliverable
682
- };
683
- }
684
-
685
- export function useDeliverableSummary(deliverableId?: string) {
686
- const client = useCriticalPathClient();
687
- const [summary, setSummary] = useState<DeliverableSummary | null>(null);
688
- const [loading, setLoading] = useState(true);
689
- const [error, setError] = useState<Error | null>(null);
690
-
691
- const fetchSummary = useCallback(async () => {
692
- if (!deliverableId) {
693
- setSummary(null);
694
- setLoading(false);
695
- return;
696
- }
697
- try {
698
- setLoading(true);
699
- const data = await client.getDeliverableSummary(deliverableId);
700
- setSummary(data);
701
- setError(null);
702
- } catch (err) {
703
- setError(err instanceof Error ? err : new Error(String(err)));
704
- } finally {
705
- setLoading(false);
706
- }
707
- }, [client, deliverableId]);
708
-
709
- useEffect(() => {
710
- fetchSummary();
711
- }, [fetchSummary]);
712
-
713
- return {
714
- summary,
715
- loading,
716
- error,
717
- refresh: fetchSummary
718
- };
719
- }
720
-
721
- export interface UseWebMCPOptions {
722
- projectId?: string;
723
- tools?: string[];
724
- enabled?: boolean;
725
- onToolExecuted?: (toolName: string, input: any, result: any) => void;
726
- }
727
-
728
- export function useWebMCP(options: UseWebMCPOptions = {}) {
729
- const client = useCriticalPathClient();
730
- const { projectId, tools, enabled = true, onToolExecuted } = options;
731
- const [registered, setRegistered] = useState(false);
732
- const [registeredTools, setRegisteredTools] = useState<string[]>([]);
733
- const [error, setError] = useState<Error | null>(null);
734
-
735
- useEffect(() => {
736
- if (!enabled) {
737
- setRegistered(false);
738
- setRegisteredTools([]);
739
- return;
740
- }
741
-
742
- try {
743
- const handle = registerWebMcpTools({
744
- client,
745
- projectId,
746
- tools,
747
- onToolExecuted
748
- });
749
-
750
- setRegistered(true);
751
- setRegisteredTools(handle.getRegisteredTools().map((t) => t.name));
752
- setError(null);
753
-
754
- return () => {
755
- handle.unregister();
756
- setRegistered(false);
757
- setRegisteredTools([]);
758
- };
759
- } catch (err) {
760
- setError(err instanceof Error ? err : new Error(String(err)));
761
- setRegistered(false);
762
- }
763
- }, [client, projectId, enabled, tools, onToolExecuted]);
764
-
765
- return {
766
- registered,
767
- tools: registeredTools,
768
- error
769
- };
770
- }
771
-
772
- export interface ThreadedCommentWithAttachments extends Comment {
773
- attachments: Attachment[];
774
- replies: ThreadedCommentWithAttachments[];
775
- }
776
-
777
- export function useTaskActivity(taskId?: string) {
778
- const client = useCriticalPathClient();
779
- const [comments, setComments] = useState<Comment[]>([]);
780
- const [attachments, setAttachments] = useState<Attachment[]>([]);
781
- const [loading, setLoading] = useState(false);
782
- const [error, setError] = useState<Error | null>(null);
783
-
784
- const fetchActivity = useCallback(async () => {
785
- if (!taskId) {
786
- setComments([]);
787
- setAttachments([]);
788
- setLoading(false);
789
- return;
790
- }
791
- try {
792
- setLoading(true);
793
- const [fetchedComments, fetchedAttachments] = await Promise.all([
794
- client.getComments(taskId),
795
- client.getAttachments({ taskId })
796
- ]);
797
- setComments(fetchedComments);
798
- setAttachments(fetchedAttachments);
799
- setError(null);
800
- } catch (err) {
801
- setError(err instanceof Error ? err : new Error(String(err)));
802
- } finally {
803
- setLoading(false);
804
- }
805
- }, [client, taskId]);
806
-
807
- useEffect(() => {
808
- fetchActivity();
809
- }, [fetchActivity]);
810
-
811
- const threads = useMemo<ThreadedCommentWithAttachments[]>(() => {
812
- const map = new Map<string, ThreadedCommentWithAttachments>();
813
- const roots: ThreadedCommentWithAttachments[] = [];
814
-
815
- const attachmentsByComment = new Map<string, Attachment[]>();
816
- for (const a of attachments) {
817
- if (a.commentId) {
818
- if (!attachmentsByComment.has(a.commentId)) {
819
- attachmentsByComment.set(a.commentId, []);
820
- }
821
- attachmentsByComment.get(a.commentId)!.push(a);
822
- }
823
- }
824
-
825
- for (const c of comments) {
826
- map.set(c.id, {
827
- ...c,
828
- attachments: attachmentsByComment.get(c.id) || [],
829
- replies: []
830
- });
831
- }
832
-
833
- for (const c of comments) {
834
- const threaded = map.get(c.id)!;
835
- if (c.parentId && map.has(c.parentId)) {
836
- map.get(c.parentId)!.replies.push(threaded);
837
- } else {
838
- roots.push(threaded);
839
- }
840
- }
841
-
842
- return roots;
843
- }, [comments, attachments]);
844
-
845
- const standaloneAttachments = useMemo<Attachment[]>(() => {
846
- return attachments.filter((a) => !a.commentId);
847
- }, [attachments]);
848
-
849
- const addComment = async (
850
- input: Omit<CreateCommentBody, 'taskId'>,
851
- attachmentInputs?: Array<Omit<CreateAttachmentBody, 'taskId' | 'commentId'>>
852
- ) => {
853
- if (!taskId) {
854
- throw new Error('useTaskActivity requires a taskId to add comments.');
855
- }
856
- try {
857
- const comment = await client.addComment({ ...input, taskId });
858
- let createdAttachments: Attachment[] = [];
859
-
860
- if (attachmentInputs && attachmentInputs.length > 0) {
861
- createdAttachments = await Promise.all(
862
- attachmentInputs.map((att) =>
863
- client.createAttachment({
864
- ...att,
865
- taskId,
866
- commentId: comment.id
867
- })
868
- )
869
- );
870
- }
871
-
872
- setComments((prev) => [...prev, comment]);
873
- if (createdAttachments.length > 0) {
874
- setAttachments((prev) => [...prev, ...createdAttachments]);
875
- }
876
- return { comment, attachments: createdAttachments };
877
- } catch (err) {
878
- const errorObj = err instanceof Error ? err : new Error(String(err));
879
- setError(errorObj);
880
- throw errorObj;
881
- }
882
- };
883
-
884
- const addAttachment = async (input: Omit<Attachment, 'id' | 'taskId' | 'createdAt' | 'updatedAt'>) => {
885
- if (!taskId) {
886
- throw new Error('useTaskActivity requires a taskId to add attachments.');
887
- }
888
- try {
889
- const created = await client.createAttachment({ ...input, taskId });
890
- setAttachments((prev) => [created, ...prev]);
891
- return created;
892
- } catch (err) {
893
- const errorObj = err instanceof Error ? err : new Error(String(err));
894
- setError(errorObj);
895
- throw errorObj;
896
- }
897
- };
898
-
899
- const deleteComment = async (id: string) => {
900
- let previousComments: Comment[] = [];
901
- setComments((prev) => {
902
- previousComments = prev;
903
- return prev.filter((c) => c.id !== id);
904
- });
905
-
906
- try {
907
- await client.deleteComment(id);
908
- } catch (err) {
909
- setComments(previousComments);
910
- const errorObj = err instanceof Error ? err : new Error(String(err));
911
- setError(errorObj);
912
- throw errorObj;
913
- }
914
- };
915
-
916
- const deleteAttachment = async (id: string) => {
917
- let previousAttachments: Attachment[] = [];
918
- setAttachments((prev) => {
919
- previousAttachments = prev;
920
- return prev.filter((a) => a.id !== id);
921
- });
922
-
923
- try {
924
- await client.deleteAttachment(id);
925
- } catch (err) {
926
- setAttachments(previousAttachments);
927
- const errorObj = err instanceof Error ? err : new Error(String(err));
928
- setError(errorObj);
929
- throw errorObj;
930
- }
931
- };
932
-
933
- const addReaction = async (
934
- commentId: string,
935
- emoji: string
936
- ) => {
937
- try {
938
- const payload = { emoji };
939
- const updated = await client.addCommentReaction(commentId, payload);
940
- setComments((prev) => prev.map((c) => (c.id === commentId ? updated : c)));
941
- return updated;
942
- } catch (err) {
943
- const errorObj = err instanceof Error ? err : new Error(String(err));
944
- setError(errorObj);
945
- throw errorObj;
946
- }
947
- };
948
-
949
- const removeReaction = async (
950
- commentId: string,
951
- emoji: string
952
- ) => {
953
- try {
954
- const payload = { emoji };
955
- const updated = await client.removeCommentReaction(commentId, payload);
956
- setComments((prev) => prev.map((c) => (c.id === commentId ? updated : c)));
957
- return updated;
958
- } catch (err) {
959
- const errorObj = err instanceof Error ? err : new Error(String(err));
960
- setError(errorObj);
961
- throw errorObj;
962
- }
963
- };
964
-
965
- return {
966
- comments,
967
- attachments,
968
- threads,
969
- standaloneAttachments,
970
- loading,
971
- error,
972
- refresh: fetchActivity,
973
- addComment,
974
- addAttachment,
975
- deleteComment,
976
- deleteAttachment,
977
- addReaction,
978
- removeReaction
979
- };
980
- }
981
-
982
- export function useCriticalPath(projectId: string | undefined) {
983
- const client = useCriticalPathClient();
984
- const [analysis, setAnalysis] = useState<CriticalPathAnalysis | null>(null);
985
- const [loading, setLoading] = useState(true);
986
- const [error, setError] = useState<Error | null>(null);
987
-
988
- const fetchCriticalPath = useCallback(async () => {
989
- if (!projectId) {
990
- setAnalysis(null);
991
- setLoading(false);
992
- return;
993
- }
994
- try {
995
- setLoading(true);
996
- const data = await client.calculateCriticalPath(projectId);
997
- setAnalysis(data);
998
- setError(null);
999
- } catch (err) {
1000
- setError(err instanceof Error ? err : new Error(String(err)));
1001
- } finally {
1002
- setLoading(false);
1003
- }
1004
- }, [client, projectId]);
1005
-
1006
- useEffect(() => {
1007
- fetchCriticalPath();
1008
- }, [fetchCriticalPath]);
1009
-
1010
- return {
1011
- analysis,
1012
- loading,
1013
- error,
1014
- refresh: fetchCriticalPath
1015
- };
1016
- }
1017
-
1018
- export function useTimelineLadder(
1019
- projectId: string | undefined,
1020
- initialOptions: TimelineLadderOptions = {}
1021
- ) {
1022
- const client = useCriticalPathClient();
1023
- const [level, setLevel] = useState<AbstractionLevel>(initialOptions.level || 'all');
1024
- const [ladder, setLadder] = useState<TimelineLadder | null>(null);
1025
- const [loading, setLoading] = useState(true);
1026
- const [error, setError] = useState<Error | null>(null);
1027
-
1028
- const fetchLadder = useCallback(async () => {
1029
- if (!projectId) {
1030
- setLadder(null);
1031
- setLoading(false);
1032
- return;
1033
- }
1034
- try {
1035
- setLoading(true);
1036
- const data = await client.getTimelineLadder(projectId, {
1037
- ...initialOptions,
1038
- level
1039
- });
1040
- setLadder(data);
1041
- setError(null);
1042
- } catch (err) {
1043
- setError(err instanceof Error ? err : new Error(String(err)));
1044
- } finally {
1045
- setLoading(false);
1046
- }
1047
- }, [client, projectId, level, initialOptions.containerId, initialOptions.iterationId]);
1048
-
1049
- useEffect(() => {
1050
- fetchLadder();
1051
- }, [fetchLadder]);
1052
-
1053
- return {
1054
- ladder,
1055
- level,
1056
- setLevel,
1057
- macro: ladder?.macro ?? null,
1058
- standard: ladder?.standard ?? null,
1059
- concrete: ladder?.concrete ?? null,
1060
- loading,
1061
- error,
1062
- refresh: fetchLadder
1063
- };
1064
- }
1065
-
1066
- export function useTaskLadder(taskId: string | undefined) {
1067
- const client = useCriticalPathClient();
1068
- const [taskLadder, setTaskLadder] = useState<TaskLadderView | null>(null);
1069
- const [loading, setLoading] = useState(true);
1070
- const [error, setError] = useState<Error | null>(null);
1071
-
1072
- const fetchTaskLadder = useCallback(async () => {
1073
- if (!taskId) {
1074
- setTaskLadder(null);
1075
- setLoading(false);
1076
- return;
1077
- }
1078
- try {
1079
- setLoading(true);
1080
- const data = await client.getTaskLadder(taskId);
1081
- setTaskLadder(data);
1082
- setError(null);
1083
- } catch (err) {
1084
- setError(err instanceof Error ? err : new Error(String(err)));
1085
- } finally {
1086
- setLoading(false);
1087
- }
1088
- }, [client, taskId]);
1089
-
1090
- useEffect(() => {
1091
- fetchTaskLadder();
1092
- }, [fetchTaskLadder]);
1093
-
1094
- return {
1095
- taskLadder,
1096
- macroPhase: taskLadder?.macroPhase ?? null,
1097
- standard: taskLadder?.standard ?? null,
1098
- concrete: taskLadder?.concrete ?? null,
1099
- metrics: taskLadder?.metrics ?? null,
1100
- loading,
1101
- error,
1102
- refresh: fetchTaskLadder
1103
- };
1104
- }
1105
-
1106
- export function useTaskMetrics(taskId: string | undefined) {
1107
- const client = useCriticalPathClient();
1108
- const [metrics, setMetrics] = useState<TaskMetrics | null>(null);
1109
- const [history, setHistory] = useState<TaskProgressHistory | null>(null);
1110
- const [loading, setLoading] = useState(true);
1111
- const [error, setError] = useState<Error | null>(null);
1112
-
1113
- const fetchMetrics = useCallback(async () => {
1114
- if (!taskId) {
1115
- setMetrics(null);
1116
- setHistory(null);
1117
- setLoading(false);
1118
- return;
1119
- }
1120
- try {
1121
- setLoading(true);
1122
- const [m, h] = await Promise.all([
1123
- client.getTaskMetrics(taskId),
1124
- client.getTaskProgressHistory(taskId)
1125
- ]);
1126
- setMetrics(m);
1127
- setHistory(h);
1128
- setError(null);
1129
- } catch (err) {
1130
- setError(err instanceof Error ? err : new Error(String(err)));
1131
- } finally {
1132
- setLoading(false);
1133
- }
1134
- }, [client, taskId]);
1135
-
1136
- useEffect(() => {
1137
- fetchMetrics();
1138
- }, [fetchMetrics]);
1139
-
1140
- return {
1141
- metrics,
1142
- history,
1143
- inferredActuals: metrics?.inferredActuals ?? null,
1144
- realityDelta: metrics?.realityDelta ?? null,
1145
- progress: metrics?.progress ?? null,
1146
- evm: metrics?.evm ?? null,
1147
- curveProfile: history?.curveProfile ?? null,
1148
- loading,
1149
- error,
1150
- refresh: fetchMetrics
1151
- };
1152
- }
1153
-
1154
- export function useWorkloadDistribution(
1155
- projectId?: string,
1156
- initialOptions: WorkloadDistributionOptions = {}
1157
- ) {
1158
- const client = useCriticalPathClient();
1159
- const [interval, setInterval] = useState<WorkloadInterval>(initialOptions.interval || 'week');
1160
- const [groupBy, setGroupBy] = useState<WorkloadGroupBy>(initialOptions.groupBy || 'assignee');
1161
- const [metric, setMetric] = useState<WorkloadMetric>(initialOptions.metric || 'blended');
1162
- const [workload, setWorkload] = useState<WorkloadDistribution | null>(null);
1163
- const [loading, setLoading] = useState(true);
1164
- const [error, setError] = useState<Error | null>(null);
1165
-
1166
- const fetchWorkload = useCallback(async () => {
1167
- try {
1168
- setLoading(true);
1169
- const data = await client.getWorkloadDistribution(projectId, {
1170
- ...initialOptions,
1171
- interval,
1172
- groupBy,
1173
- metric
1174
- });
1175
- setWorkload(data);
1176
- setError(null);
1177
- } catch (err) {
1178
- setError(err instanceof Error ? err : new Error(String(err)));
1179
- } finally {
1180
- setLoading(false);
1181
- }
1182
- }, [
1183
- client,
1184
- projectId,
1185
- interval,
1186
- groupBy,
1187
- metric,
1188
- initialOptions.startDate,
1189
- initialOptions.endDate,
1190
- initialOptions.defaultWeeklyCapacityHours
1191
- ]);
1192
-
1193
- useEffect(() => {
1194
- fetchWorkload();
1195
- }, [fetchWorkload]);
1196
-
1197
- return {
1198
- workload,
1199
- interval,
1200
- setInterval,
1201
- groupBy,
1202
- setGroupBy,
1203
- metric,
1204
- setMetric,
1205
- buckets: workload?.buckets ?? [],
1206
- seriesKeys: workload?.seriesKeys ?? [],
1207
- seriesLabels: workload?.seriesLabels ?? {},
1208
- totalHours: workload?.totalHours ?? 0,
1209
- totalCapacity: workload?.totalCapacity ?? 0,
1210
- averageUtilization: workload?.averageUtilization,
1211
- loading,
1212
- error,
1213
- refresh: fetchWorkload
1214
- };
1215
- }
1216
-
1217
-
1218
-