@critical-path/react 0.13.10 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +5 -0
- package/dist/cancellation.test.d.ts +2 -0
- package/dist/cancellation.test.d.ts.map +1 -0
- package/dist/cancellation.test.js +42 -0
- package/dist/cancellation.test.js.map +1 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +213 -72
- package/dist/hooks.js.map +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +1 -0
- package/dist/provider.js.map +1 -1
- package/package.json +8 -4
- package/src/cancellation.test.tsx +58 -0
- package/src/hooks.ts +172 -72
- package/src/provider.tsx +2 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/hooks.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
1
3
|
import type { CreateAttachmentBody, CreateCommentBody, CreateTaskBody, UpdateCommentBody, UpdateDeliverableBody, UpdateTaskBody } from '@critical-path/core/schemas';
|
|
2
4
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
3
5
|
import type {
|
|
@@ -36,21 +38,28 @@ export function useWorkflows() {
|
|
|
36
38
|
const [loading, setLoading] = useState(true);
|
|
37
39
|
const [error, setError] = useState<Error | null>(null);
|
|
38
40
|
|
|
39
|
-
const fetchWorkflows = useCallback(async () => {
|
|
41
|
+
const fetchWorkflows = useCallback(async (signal?: AbortSignal) => {
|
|
42
|
+
const api = signal ? client.with({ signal }) : client;
|
|
40
43
|
try {
|
|
41
44
|
setLoading(true);
|
|
42
|
-
const data = await
|
|
45
|
+
const data = await api.getWorkflows();
|
|
46
|
+
if (signal?.aborted) return;
|
|
43
47
|
setWorkflows(data);
|
|
44
48
|
setError(null);
|
|
45
49
|
} catch (err) {
|
|
50
|
+
if (signal?.aborted) return;
|
|
46
51
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
47
52
|
} finally {
|
|
48
|
-
setLoading(false);
|
|
53
|
+
if (!signal?.aborted) setLoading(false);
|
|
49
54
|
}
|
|
50
55
|
}, [client]);
|
|
51
56
|
|
|
52
57
|
useEffect(() => {
|
|
53
|
-
|
|
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();
|
|
54
63
|
}, [fetchWorkflows]);
|
|
55
64
|
|
|
56
65
|
const createWorkflow = async (input: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>) => {
|
|
@@ -88,7 +97,7 @@ export function useWorkflows() {
|
|
|
88
97
|
}
|
|
89
98
|
};
|
|
90
99
|
|
|
91
|
-
return { workflows, loading, error, refresh: fetchWorkflows, createWorkflow, updateWorkflow, deleteWorkflow };
|
|
100
|
+
return { workflows, loading, error, refresh: () => fetchWorkflows(), createWorkflow, updateWorkflow, deleteWorkflow };
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
export function useTaskTransitions(taskId?: string) {
|
|
@@ -97,28 +106,35 @@ export function useTaskTransitions(taskId?: string) {
|
|
|
97
106
|
const [loading, setLoading] = useState(false);
|
|
98
107
|
const [error, setError] = useState<Error | null>(null);
|
|
99
108
|
|
|
100
|
-
const fetchTransitions = useCallback(async () => {
|
|
109
|
+
const fetchTransitions = useCallback(async (signal?: AbortSignal) => {
|
|
110
|
+
const api = signal ? client.with({ signal }) : client;
|
|
101
111
|
if (!taskId) {
|
|
102
112
|
setAllowedTransitions([]);
|
|
103
113
|
return;
|
|
104
114
|
}
|
|
105
115
|
try {
|
|
106
116
|
setLoading(true);
|
|
107
|
-
const data = await
|
|
117
|
+
const data = await api.getAllowedTaskTransitions(taskId);
|
|
118
|
+
if (signal?.aborted) return;
|
|
108
119
|
setAllowedTransitions(data);
|
|
109
120
|
setError(null);
|
|
110
121
|
} catch (err) {
|
|
122
|
+
if (signal?.aborted) return;
|
|
111
123
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
112
124
|
} finally {
|
|
113
|
-
setLoading(false);
|
|
125
|
+
if (!signal?.aborted) setLoading(false);
|
|
114
126
|
}
|
|
115
127
|
}, [client, taskId]);
|
|
116
128
|
|
|
117
129
|
useEffect(() => {
|
|
118
|
-
|
|
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();
|
|
119
135
|
}, [fetchTransitions]);
|
|
120
136
|
|
|
121
|
-
return { allowedTransitions, loading, error, refresh: fetchTransitions };
|
|
137
|
+
return { allowedTransitions, loading, error, refresh: () => fetchTransitions() };
|
|
122
138
|
}
|
|
123
139
|
|
|
124
140
|
export function useProjects() {
|
|
@@ -127,21 +143,28 @@ export function useProjects() {
|
|
|
127
143
|
const [loading, setLoading] = useState(true);
|
|
128
144
|
const [error, setError] = useState<Error | null>(null);
|
|
129
145
|
|
|
130
|
-
const fetchProjects = useCallback(async () => {
|
|
146
|
+
const fetchProjects = useCallback(async (signal?: AbortSignal) => {
|
|
147
|
+
const api = signal ? client.with({ signal }) : client;
|
|
131
148
|
try {
|
|
132
149
|
setLoading(true);
|
|
133
|
-
const data = await
|
|
150
|
+
const data = await api.getProjects();
|
|
151
|
+
if (signal?.aborted) return;
|
|
134
152
|
setProjects(data);
|
|
135
153
|
setError(null);
|
|
136
154
|
} catch (err) {
|
|
155
|
+
if (signal?.aborted) return;
|
|
137
156
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
138
157
|
} finally {
|
|
139
|
-
setLoading(false);
|
|
158
|
+
if (!signal?.aborted) setLoading(false);
|
|
140
159
|
}
|
|
141
160
|
}, [client]);
|
|
142
161
|
|
|
143
162
|
useEffect(() => {
|
|
144
|
-
|
|
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();
|
|
145
168
|
}, [fetchProjects]);
|
|
146
169
|
|
|
147
170
|
const createProject = async (input: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>) => {
|
|
@@ -168,7 +191,7 @@ export function useProjects() {
|
|
|
168
191
|
}
|
|
169
192
|
};
|
|
170
193
|
|
|
171
|
-
return { projects, loading, error, refresh: fetchProjects, createProject };
|
|
194
|
+
return { projects, loading, error, refresh: () => fetchProjects(), createProject };
|
|
172
195
|
}
|
|
173
196
|
|
|
174
197
|
export function useTasks(projectId?: string) {
|
|
@@ -177,16 +200,19 @@ export function useTasks(projectId?: string) {
|
|
|
177
200
|
const [loading, setLoading] = useState(true);
|
|
178
201
|
const [error, setError] = useState<Error | null>(null);
|
|
179
202
|
|
|
180
|
-
const fetchTasks = useCallback(async () => {
|
|
203
|
+
const fetchTasks = useCallback(async (signal?: AbortSignal) => {
|
|
204
|
+
const api = signal ? client.with({ signal }) : client;
|
|
181
205
|
try {
|
|
182
206
|
setLoading(true);
|
|
183
|
-
const data = await
|
|
207
|
+
const data = await api.getTasks(projectId);
|
|
208
|
+
if (signal?.aborted) return;
|
|
184
209
|
setTasks(data);
|
|
185
210
|
setError(null);
|
|
186
211
|
} catch (err) {
|
|
212
|
+
if (signal?.aborted) return;
|
|
187
213
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
188
214
|
} finally {
|
|
189
|
-
setLoading(false);
|
|
215
|
+
if (!signal?.aborted) setLoading(false);
|
|
190
216
|
}
|
|
191
217
|
}, [client, projectId]);
|
|
192
218
|
|
|
@@ -194,7 +220,11 @@ export function useTasks(projectId?: string) {
|
|
|
194
220
|
const tempToRealIdMapRef = useRef<Map<string, string>>(new Map());
|
|
195
221
|
|
|
196
222
|
useEffect(() => {
|
|
197
|
-
|
|
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();
|
|
198
228
|
}, [fetchTasks]);
|
|
199
229
|
|
|
200
230
|
const createTask = async (input: CreateTaskBody) => {
|
|
@@ -353,7 +383,7 @@ export function useTasks(projectId?: string) {
|
|
|
353
383
|
}
|
|
354
384
|
};
|
|
355
385
|
|
|
356
|
-
return { tasks, loading, error, refresh: fetchTasks, createTask, updateTask, updateTaskStatus, deleteTask };
|
|
386
|
+
return { tasks, loading, error, refresh: () => fetchTasks(), createTask, updateTask, updateTaskStatus, deleteTask };
|
|
357
387
|
}
|
|
358
388
|
|
|
359
389
|
export interface UseKanbanOptions {
|
|
@@ -424,7 +454,8 @@ export function useComments(taskId: string) {
|
|
|
424
454
|
const [loading, setLoading] = useState(true);
|
|
425
455
|
const [error, setError] = useState<Error | null>(null);
|
|
426
456
|
|
|
427
|
-
const fetchComments = useCallback(async () => {
|
|
457
|
+
const fetchComments = useCallback(async (signal?: AbortSignal) => {
|
|
458
|
+
const api = signal ? client.with({ signal }) : client;
|
|
428
459
|
if (!taskId) {
|
|
429
460
|
setComments([]);
|
|
430
461
|
setLoading(false);
|
|
@@ -432,18 +463,24 @@ export function useComments(taskId: string) {
|
|
|
432
463
|
}
|
|
433
464
|
try {
|
|
434
465
|
setLoading(true);
|
|
435
|
-
const data = await
|
|
466
|
+
const data = await api.getComments(taskId);
|
|
467
|
+
if (signal?.aborted) return;
|
|
436
468
|
setComments(data);
|
|
437
469
|
setError(null);
|
|
438
470
|
} catch (err) {
|
|
471
|
+
if (signal?.aborted) return;
|
|
439
472
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
440
473
|
} finally {
|
|
441
|
-
setLoading(false);
|
|
474
|
+
if (!signal?.aborted) setLoading(false);
|
|
442
475
|
}
|
|
443
476
|
}, [client, taskId]);
|
|
444
477
|
|
|
445
478
|
useEffect(() => {
|
|
446
|
-
|
|
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();
|
|
447
484
|
}, [fetchComments]);
|
|
448
485
|
|
|
449
486
|
const addComment = async (input: Omit<CreateCommentBody, 'taskId'>) => {
|
|
@@ -541,7 +578,7 @@ export function useComments(taskId: string) {
|
|
|
541
578
|
threads,
|
|
542
579
|
loading,
|
|
543
580
|
error,
|
|
544
|
-
refresh: fetchComments,
|
|
581
|
+
refresh: () => fetchComments(),
|
|
545
582
|
addComment,
|
|
546
583
|
updateComment,
|
|
547
584
|
deleteComment,
|
|
@@ -558,21 +595,28 @@ export function useAttachments(filter?: { taskId?: string; projectId?: string; c
|
|
|
558
595
|
|
|
559
596
|
const filterKey = JSON.stringify(filter || {});
|
|
560
597
|
|
|
561
|
-
const fetchAttachments = useCallback(async () => {
|
|
598
|
+
const fetchAttachments = useCallback(async (signal?: AbortSignal) => {
|
|
599
|
+
const api = signal ? client.with({ signal }) : client;
|
|
562
600
|
try {
|
|
563
601
|
setLoading(true);
|
|
564
|
-
const data = await
|
|
602
|
+
const data = await api.getAttachments(filter);
|
|
603
|
+
if (signal?.aborted) return;
|
|
565
604
|
setAttachments(data);
|
|
566
605
|
setError(null);
|
|
567
606
|
} catch (err) {
|
|
607
|
+
if (signal?.aborted) return;
|
|
568
608
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
569
609
|
} finally {
|
|
570
|
-
setLoading(false);
|
|
610
|
+
if (!signal?.aborted) setLoading(false);
|
|
571
611
|
}
|
|
572
612
|
}, [client, filterKey]);
|
|
573
613
|
|
|
574
614
|
useEffect(() => {
|
|
575
|
-
|
|
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();
|
|
576
620
|
}, [fetchAttachments]);
|
|
577
621
|
|
|
578
622
|
const createAttachment = async (input: Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>) => {
|
|
@@ -602,7 +646,7 @@ export function useAttachments(filter?: { taskId?: string; projectId?: string; c
|
|
|
602
646
|
attachments,
|
|
603
647
|
loading,
|
|
604
648
|
error,
|
|
605
|
-
refresh: fetchAttachments,
|
|
649
|
+
refresh: () => fetchAttachments(),
|
|
606
650
|
createAttachment,
|
|
607
651
|
deleteAttachment
|
|
608
652
|
};
|
|
@@ -614,7 +658,8 @@ export function useDeliverables(projectId?: string) {
|
|
|
614
658
|
const [loading, setLoading] = useState(true);
|
|
615
659
|
const [error, setError] = useState<Error | null>(null);
|
|
616
660
|
|
|
617
|
-
const fetchDeliverables = useCallback(async () => {
|
|
661
|
+
const fetchDeliverables = useCallback(async (signal?: AbortSignal) => {
|
|
662
|
+
const api = signal ? client.with({ signal }) : client;
|
|
618
663
|
if (!projectId) {
|
|
619
664
|
setDeliverables([]);
|
|
620
665
|
setLoading(false);
|
|
@@ -622,18 +667,24 @@ export function useDeliverables(projectId?: string) {
|
|
|
622
667
|
}
|
|
623
668
|
try {
|
|
624
669
|
setLoading(true);
|
|
625
|
-
const data = await
|
|
670
|
+
const data = await api.getDeliverables(projectId);
|
|
671
|
+
if (signal?.aborted) return;
|
|
626
672
|
setDeliverables(data);
|
|
627
673
|
setError(null);
|
|
628
674
|
} catch (err) {
|
|
675
|
+
if (signal?.aborted) return;
|
|
629
676
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
630
677
|
} finally {
|
|
631
|
-
setLoading(false);
|
|
678
|
+
if (!signal?.aborted) setLoading(false);
|
|
632
679
|
}
|
|
633
680
|
}, [client, projectId]);
|
|
634
681
|
|
|
635
682
|
useEffect(() => {
|
|
636
|
-
|
|
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();
|
|
637
688
|
}, [fetchDeliverables]);
|
|
638
689
|
|
|
639
690
|
const createDeliverable = async (input: CreateDeliverableInput) => {
|
|
@@ -675,7 +726,7 @@ export function useDeliverables(projectId?: string) {
|
|
|
675
726
|
deliverables,
|
|
676
727
|
loading,
|
|
677
728
|
error,
|
|
678
|
-
refresh: fetchDeliverables,
|
|
729
|
+
refresh: () => fetchDeliverables(),
|
|
679
730
|
createDeliverable,
|
|
680
731
|
updateDeliverable,
|
|
681
732
|
deleteDeliverable
|
|
@@ -688,7 +739,8 @@ export function useDeliverableSummary(deliverableId?: string) {
|
|
|
688
739
|
const [loading, setLoading] = useState(true);
|
|
689
740
|
const [error, setError] = useState<Error | null>(null);
|
|
690
741
|
|
|
691
|
-
const fetchSummary = useCallback(async () => {
|
|
742
|
+
const fetchSummary = useCallback(async (signal?: AbortSignal) => {
|
|
743
|
+
const api = signal ? client.with({ signal }) : client;
|
|
692
744
|
if (!deliverableId) {
|
|
693
745
|
setSummary(null);
|
|
694
746
|
setLoading(false);
|
|
@@ -696,25 +748,31 @@ export function useDeliverableSummary(deliverableId?: string) {
|
|
|
696
748
|
}
|
|
697
749
|
try {
|
|
698
750
|
setLoading(true);
|
|
699
|
-
const data = await
|
|
751
|
+
const data = await api.getDeliverableSummary(deliverableId);
|
|
752
|
+
if (signal?.aborted) return;
|
|
700
753
|
setSummary(data);
|
|
701
754
|
setError(null);
|
|
702
755
|
} catch (err) {
|
|
756
|
+
if (signal?.aborted) return;
|
|
703
757
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
704
758
|
} finally {
|
|
705
|
-
setLoading(false);
|
|
759
|
+
if (!signal?.aborted) setLoading(false);
|
|
706
760
|
}
|
|
707
761
|
}, [client, deliverableId]);
|
|
708
762
|
|
|
709
763
|
useEffect(() => {
|
|
710
|
-
|
|
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();
|
|
711
769
|
}, [fetchSummary]);
|
|
712
770
|
|
|
713
771
|
return {
|
|
714
772
|
summary,
|
|
715
773
|
loading,
|
|
716
774
|
error,
|
|
717
|
-
refresh: fetchSummary
|
|
775
|
+
refresh: () => fetchSummary()
|
|
718
776
|
};
|
|
719
777
|
}
|
|
720
778
|
|
|
@@ -781,7 +839,8 @@ export function useTaskActivity(taskId?: string) {
|
|
|
781
839
|
const [loading, setLoading] = useState(false);
|
|
782
840
|
const [error, setError] = useState<Error | null>(null);
|
|
783
841
|
|
|
784
|
-
const fetchActivity = useCallback(async () => {
|
|
842
|
+
const fetchActivity = useCallback(async (signal?: AbortSignal) => {
|
|
843
|
+
const api = signal ? client.with({ signal }) : client;
|
|
785
844
|
if (!taskId) {
|
|
786
845
|
setComments([]);
|
|
787
846
|
setAttachments([]);
|
|
@@ -791,21 +850,27 @@ export function useTaskActivity(taskId?: string) {
|
|
|
791
850
|
try {
|
|
792
851
|
setLoading(true);
|
|
793
852
|
const [fetchedComments, fetchedAttachments] = await Promise.all([
|
|
794
|
-
|
|
795
|
-
|
|
853
|
+
api.getComments(taskId),
|
|
854
|
+
api.getAttachments({ taskId })
|
|
796
855
|
]);
|
|
856
|
+
if (signal?.aborted) return;
|
|
797
857
|
setComments(fetchedComments);
|
|
798
858
|
setAttachments(fetchedAttachments);
|
|
799
859
|
setError(null);
|
|
800
860
|
} catch (err) {
|
|
861
|
+
if (signal?.aborted) return;
|
|
801
862
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
802
863
|
} finally {
|
|
803
|
-
setLoading(false);
|
|
864
|
+
if (!signal?.aborted) setLoading(false);
|
|
804
865
|
}
|
|
805
866
|
}, [client, taskId]);
|
|
806
867
|
|
|
807
868
|
useEffect(() => {
|
|
808
|
-
|
|
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();
|
|
809
874
|
}, [fetchActivity]);
|
|
810
875
|
|
|
811
876
|
const threads = useMemo<ThreadedCommentWithAttachments[]>(() => {
|
|
@@ -969,7 +1034,7 @@ export function useTaskActivity(taskId?: string) {
|
|
|
969
1034
|
standaloneAttachments,
|
|
970
1035
|
loading,
|
|
971
1036
|
error,
|
|
972
|
-
refresh: fetchActivity,
|
|
1037
|
+
refresh: () => fetchActivity(),
|
|
973
1038
|
addComment,
|
|
974
1039
|
addAttachment,
|
|
975
1040
|
deleteComment,
|
|
@@ -985,7 +1050,8 @@ export function useCriticalPath(projectId: string | undefined) {
|
|
|
985
1050
|
const [loading, setLoading] = useState(true);
|
|
986
1051
|
const [error, setError] = useState<Error | null>(null);
|
|
987
1052
|
|
|
988
|
-
const fetchCriticalPath = useCallback(async () => {
|
|
1053
|
+
const fetchCriticalPath = useCallback(async (signal?: AbortSignal) => {
|
|
1054
|
+
const api = signal ? client.with({ signal }) : client;
|
|
989
1055
|
if (!projectId) {
|
|
990
1056
|
setAnalysis(null);
|
|
991
1057
|
setLoading(false);
|
|
@@ -993,25 +1059,31 @@ export function useCriticalPath(projectId: string | undefined) {
|
|
|
993
1059
|
}
|
|
994
1060
|
try {
|
|
995
1061
|
setLoading(true);
|
|
996
|
-
const data = await
|
|
1062
|
+
const data = await api.calculateCriticalPath(projectId);
|
|
1063
|
+
if (signal?.aborted) return;
|
|
997
1064
|
setAnalysis(data);
|
|
998
1065
|
setError(null);
|
|
999
1066
|
} catch (err) {
|
|
1067
|
+
if (signal?.aborted) return;
|
|
1000
1068
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1001
1069
|
} finally {
|
|
1002
|
-
setLoading(false);
|
|
1070
|
+
if (!signal?.aborted) setLoading(false);
|
|
1003
1071
|
}
|
|
1004
1072
|
}, [client, projectId]);
|
|
1005
1073
|
|
|
1006
1074
|
useEffect(() => {
|
|
1007
|
-
|
|
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();
|
|
1008
1080
|
}, [fetchCriticalPath]);
|
|
1009
1081
|
|
|
1010
1082
|
return {
|
|
1011
1083
|
analysis,
|
|
1012
1084
|
loading,
|
|
1013
1085
|
error,
|
|
1014
|
-
refresh: fetchCriticalPath
|
|
1086
|
+
refresh: () => fetchCriticalPath()
|
|
1015
1087
|
};
|
|
1016
1088
|
}
|
|
1017
1089
|
|
|
@@ -1025,7 +1097,8 @@ export function useTimelineLadder(
|
|
|
1025
1097
|
const [loading, setLoading] = useState(true);
|
|
1026
1098
|
const [error, setError] = useState<Error | null>(null);
|
|
1027
1099
|
|
|
1028
|
-
const fetchLadder = useCallback(async () => {
|
|
1100
|
+
const fetchLadder = useCallback(async (signal?: AbortSignal) => {
|
|
1101
|
+
const api = signal ? client.with({ signal }) : client;
|
|
1029
1102
|
if (!projectId) {
|
|
1030
1103
|
setLadder(null);
|
|
1031
1104
|
setLoading(false);
|
|
@@ -1033,21 +1106,27 @@ export function useTimelineLadder(
|
|
|
1033
1106
|
}
|
|
1034
1107
|
try {
|
|
1035
1108
|
setLoading(true);
|
|
1036
|
-
const data = await
|
|
1109
|
+
const data = await api.getTimelineLadder(projectId, {
|
|
1037
1110
|
...initialOptions,
|
|
1038
1111
|
level
|
|
1039
1112
|
});
|
|
1113
|
+
if (signal?.aborted) return;
|
|
1040
1114
|
setLadder(data);
|
|
1041
1115
|
setError(null);
|
|
1042
1116
|
} catch (err) {
|
|
1117
|
+
if (signal?.aborted) return;
|
|
1043
1118
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1044
1119
|
} finally {
|
|
1045
|
-
setLoading(false);
|
|
1120
|
+
if (!signal?.aborted) setLoading(false);
|
|
1046
1121
|
}
|
|
1047
1122
|
}, [client, projectId, level, initialOptions.containerId, initialOptions.iterationId]);
|
|
1048
1123
|
|
|
1049
1124
|
useEffect(() => {
|
|
1050
|
-
|
|
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();
|
|
1051
1130
|
}, [fetchLadder]);
|
|
1052
1131
|
|
|
1053
1132
|
return {
|
|
@@ -1059,7 +1138,7 @@ export function useTimelineLadder(
|
|
|
1059
1138
|
concrete: ladder?.concrete ?? null,
|
|
1060
1139
|
loading,
|
|
1061
1140
|
error,
|
|
1062
|
-
refresh: fetchLadder
|
|
1141
|
+
refresh: () => fetchLadder()
|
|
1063
1142
|
};
|
|
1064
1143
|
}
|
|
1065
1144
|
|
|
@@ -1069,7 +1148,8 @@ export function useTaskLadder(taskId: string | undefined) {
|
|
|
1069
1148
|
const [loading, setLoading] = useState(true);
|
|
1070
1149
|
const [error, setError] = useState<Error | null>(null);
|
|
1071
1150
|
|
|
1072
|
-
const fetchTaskLadder = useCallback(async () => {
|
|
1151
|
+
const fetchTaskLadder = useCallback(async (signal?: AbortSignal) => {
|
|
1152
|
+
const api = signal ? client.with({ signal }) : client;
|
|
1073
1153
|
if (!taskId) {
|
|
1074
1154
|
setTaskLadder(null);
|
|
1075
1155
|
setLoading(false);
|
|
@@ -1077,18 +1157,24 @@ export function useTaskLadder(taskId: string | undefined) {
|
|
|
1077
1157
|
}
|
|
1078
1158
|
try {
|
|
1079
1159
|
setLoading(true);
|
|
1080
|
-
const data = await
|
|
1160
|
+
const data = await api.getTaskLadder(taskId);
|
|
1161
|
+
if (signal?.aborted) return;
|
|
1081
1162
|
setTaskLadder(data);
|
|
1082
1163
|
setError(null);
|
|
1083
1164
|
} catch (err) {
|
|
1165
|
+
if (signal?.aborted) return;
|
|
1084
1166
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1085
1167
|
} finally {
|
|
1086
|
-
setLoading(false);
|
|
1168
|
+
if (!signal?.aborted) setLoading(false);
|
|
1087
1169
|
}
|
|
1088
1170
|
}, [client, taskId]);
|
|
1089
1171
|
|
|
1090
1172
|
useEffect(() => {
|
|
1091
|
-
|
|
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();
|
|
1092
1178
|
}, [fetchTaskLadder]);
|
|
1093
1179
|
|
|
1094
1180
|
return {
|
|
@@ -1099,7 +1185,7 @@ export function useTaskLadder(taskId: string | undefined) {
|
|
|
1099
1185
|
metrics: taskLadder?.metrics ?? null,
|
|
1100
1186
|
loading,
|
|
1101
1187
|
error,
|
|
1102
|
-
refresh: fetchTaskLadder
|
|
1188
|
+
refresh: () => fetchTaskLadder()
|
|
1103
1189
|
};
|
|
1104
1190
|
}
|
|
1105
1191
|
|
|
@@ -1110,7 +1196,8 @@ export function useTaskMetrics(taskId: string | undefined) {
|
|
|
1110
1196
|
const [loading, setLoading] = useState(true);
|
|
1111
1197
|
const [error, setError] = useState<Error | null>(null);
|
|
1112
1198
|
|
|
1113
|
-
const fetchMetrics = useCallback(async () => {
|
|
1199
|
+
const fetchMetrics = useCallback(async (signal?: AbortSignal) => {
|
|
1200
|
+
const api = signal ? client.with({ signal }) : client;
|
|
1114
1201
|
if (!taskId) {
|
|
1115
1202
|
setMetrics(null);
|
|
1116
1203
|
setHistory(null);
|
|
@@ -1120,21 +1207,27 @@ export function useTaskMetrics(taskId: string | undefined) {
|
|
|
1120
1207
|
try {
|
|
1121
1208
|
setLoading(true);
|
|
1122
1209
|
const [m, h] = await Promise.all([
|
|
1123
|
-
|
|
1124
|
-
|
|
1210
|
+
api.getTaskMetrics(taskId),
|
|
1211
|
+
api.getTaskProgressHistory(taskId)
|
|
1125
1212
|
]);
|
|
1213
|
+
if (signal?.aborted) return;
|
|
1126
1214
|
setMetrics(m);
|
|
1127
1215
|
setHistory(h);
|
|
1128
1216
|
setError(null);
|
|
1129
1217
|
} catch (err) {
|
|
1218
|
+
if (signal?.aborted) return;
|
|
1130
1219
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1131
1220
|
} finally {
|
|
1132
|
-
setLoading(false);
|
|
1221
|
+
if (!signal?.aborted) setLoading(false);
|
|
1133
1222
|
}
|
|
1134
1223
|
}, [client, taskId]);
|
|
1135
1224
|
|
|
1136
1225
|
useEffect(() => {
|
|
1137
|
-
|
|
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();
|
|
1138
1231
|
}, [fetchMetrics]);
|
|
1139
1232
|
|
|
1140
1233
|
return {
|
|
@@ -1147,7 +1240,7 @@ export function useTaskMetrics(taskId: string | undefined) {
|
|
|
1147
1240
|
curveProfile: history?.curveProfile ?? null,
|
|
1148
1241
|
loading,
|
|
1149
1242
|
error,
|
|
1150
|
-
refresh: fetchMetrics
|
|
1243
|
+
refresh: () => fetchMetrics()
|
|
1151
1244
|
};
|
|
1152
1245
|
}
|
|
1153
1246
|
|
|
@@ -1163,21 +1256,24 @@ export function useWorkloadDistribution(
|
|
|
1163
1256
|
const [loading, setLoading] = useState(true);
|
|
1164
1257
|
const [error, setError] = useState<Error | null>(null);
|
|
1165
1258
|
|
|
1166
|
-
const fetchWorkload = useCallback(async () => {
|
|
1259
|
+
const fetchWorkload = useCallback(async (signal?: AbortSignal) => {
|
|
1260
|
+
const api = signal ? client.with({ signal }) : client;
|
|
1167
1261
|
try {
|
|
1168
1262
|
setLoading(true);
|
|
1169
|
-
const data = await
|
|
1263
|
+
const data = await api.getWorkloadDistribution(projectId, {
|
|
1170
1264
|
...initialOptions,
|
|
1171
1265
|
interval,
|
|
1172
1266
|
groupBy,
|
|
1173
1267
|
metric
|
|
1174
1268
|
});
|
|
1269
|
+
if (signal?.aborted) return;
|
|
1175
1270
|
setWorkload(data);
|
|
1176
1271
|
setError(null);
|
|
1177
1272
|
} catch (err) {
|
|
1273
|
+
if (signal?.aborted) return;
|
|
1178
1274
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1179
1275
|
} finally {
|
|
1180
|
-
setLoading(false);
|
|
1276
|
+
if (!signal?.aborted) setLoading(false);
|
|
1181
1277
|
}
|
|
1182
1278
|
}, [
|
|
1183
1279
|
client,
|
|
@@ -1191,7 +1287,11 @@ export function useWorkloadDistribution(
|
|
|
1191
1287
|
]);
|
|
1192
1288
|
|
|
1193
1289
|
useEffect(() => {
|
|
1194
|
-
|
|
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();
|
|
1195
1295
|
}, [fetchWorkload]);
|
|
1196
1296
|
|
|
1197
1297
|
return {
|
|
@@ -1210,7 +1310,7 @@ export function useWorkloadDistribution(
|
|
|
1210
1310
|
averageUtilization: workload?.averageUtilization,
|
|
1211
1311
|
loading,
|
|
1212
1312
|
error,
|
|
1213
|
-
refresh: fetchWorkload
|
|
1313
|
+
refresh: () => fetchWorkload()
|
|
1214
1314
|
};
|
|
1215
1315
|
}
|
|
1216
1316
|
|
package/src/provider.tsx
CHANGED
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/hooks.ts","./src/index.test.ts","./src/index.ts","./src/provider.tsx"],"version":"5.9.3"}
|
|
1
|
+
{"root":["./src/cancellation.test.tsx","./src/hooks.ts","./src/index.test.ts","./src/index.ts","./src/provider.tsx"],"version":"5.9.3"}
|