Skip to content
Source
Sign inSign up
Open History to see the last commit

useProjectCloneBanner.tsx

133 lines4.8 KB

1import { useStableCallback } from "../../hooks/useStableCallback";
2import {
3 isAtomCommandInterrupted,
4 squashAtomCommandFailure,
5 type AtomCommandResult,
6} from "@t3tools/client-runtime/state/runtime";
7import { DownloadIcon } from "lucide-react";
8import { stackedThreadToast, toastManager } from "../ui/toast";
9import { useRemoveClonedProject } from "../../hooks/useRemoveClonedProject";
10import { sourceControlEnvironment } from "../../state/sourceControl";
11import { useProjectClone } from "../../state/projectClones";
12import {
13 projectCloneDisplayName,
14 projectCloneProgressSummary,
15 type ScopedProjectRef,
16} from "@t3tools/contracts";
17import { dismissProjectFileCopy, useProjectFileCopy } from "../../lib/addProjectToMachine";
18import { type ComposerBannerStackItem } from "./ComposerBannerStack";
19import { useAtomCommand } from "../../state/use-atom-command";
20import { Button } from "../ui/button";
21/**
22 * A project still being cloned or copied into place: the composer banner with its cancel,
23 * retry, and remove actions, and the reason sending waits until it is done.
24 */
25export function useProjectCloneBanner(activeProjectRef: ScopedProjectRef | null) {
26 const activeProjectClone = useProjectClone(activeProjectRef);
27 const cancelProjectClone = useAtomCommand(sourceControlEnvironment.cancelProjectClone, {
28 reportFailure: false,
29 });
30 const retryProjectClone = useAtomCommand(sourceControlEnvironment.retryProjectClone, {
31 reportFailure: false,
32 });
33 const removeClonedProject = useRemoveClonedProject();
34 const runProjectCloneAction = async (
35 title: string,
36 action: () => Promise<AtomCommandResult<unknown, unknown>>,
37 ): Promise<void> => {
38 const result = await action();
39 if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
40 const error = squashAtomCommandFailure(result);
41 toastManager.add(
42 stackedThreadToast({
43 type: "error",
44 title,
45 description: error instanceof Error ? error.message : "An error occurred.",
46 }),
47 );
48 }
49 };
50 const runProjectCloneActionStable = useStableCallback(runProjectCloneAction);
51 const activeProjectFileCopy = useProjectFileCopy(activeProjectRef);
52 const projectCloneSendBlockReason =
53 activeProjectClone?.phase === "running"
54 ? "Cloning repository"
55 : activeProjectClone !== null && activeProjectClone.phase !== "done"
56 ? "Repository not cloned"
57 : activeProjectFileCopy?.phase === "copying"
58 ? "Copying files"
59 : null;
60 const projectCloneBannerItem = (() => {
61 if (activeProjectFileCopy?.phase === "failed" && activeProjectRef) {
62 return {
63 id: `project-file-copy:${activeProjectRef.projectId}`,
64 variant: "error",
65 icon: <DownloadIcon />,
66 title: activeProjectFileCopy.title,
67 description: activeProjectFileCopy.failures.join("\n"),
68 onDismiss: () => dismissProjectFileCopy(activeProjectRef),
69 };
70 }
71 if (!activeProjectClone || !activeProjectRef || activeProjectClone.phase === "done") {
72 return null;
73 }
74 const name = projectCloneDisplayName(activeProjectClone);
75 const { environmentId, projectId } = activeProjectRef;
76 if (activeProjectClone.phase === "running") {
77 return {
78 id: `project-clone:${projectId}`,
79 variant: "info",
80 icon: <DownloadIcon />,
81 title: `Cloning ${name}`,
82 description: projectCloneProgressSummary(activeProjectClone),
83 actions: (
84 <Button
85 size="xs"
86 variant="ghost"
87 onClick={() =>
88 void runProjectCloneActionStable("Failed to cancel clone", () =>
89 cancelProjectClone({ environmentId, input: { projectId } }),
90 )
91 }
92 >
93 Cancel
94 </Button>
95 ),
96 };
97 }
98 const cancelled = activeProjectClone.phase === "cancelled";
99 return {
100 id: `project-clone:${projectId}`,
101 variant: cancelled ? "warning" : "error",
102 icon: <DownloadIcon />,
103 title: cancelled ? `Cancelled cloning ${name}` : `Failed to clone ${name}`,
104 description: cancelled ? "Retry to bring in the repository." : activeProjectClone.error,
105 actions: (
106 <>
107 <Button
108 size="xs"
109 variant="ghost"
110 onClick={() => void removeClonedProject({ environmentId, projectId })}
111 >
112 Remove project
113 </Button>
114 <Button
115 size="xs"
116 variant="ghost"
117 onClick={() =>
118 void runProjectCloneActionStable("Failed to retry clone", () =>
119 retryProjectClone({ environmentId, input: { projectId } }),
120 )
121 }
122 >
123 Retry
124 </Button>
125 </>
126 ),
127 };
128 })() as ComposerBannerStackItem | null;
129 return {
130 projectCloneBannerItem,
131 projectCloneSendBlockReason,
132 };