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