blob: 37213b83c345bbf59747f737a88b8e5658a8c785 [file] [log] [blame]
giod0026612025-05-08 13:00:36 +00001import { NodeRect } from "./node-rect";
2import { GithubNode, nodeIsConnectable, nodeLabel, useStateStore, useGithubService } from "@/lib/state";
3import { useEffect, useMemo, useState } from "react";
gio5f2f1002025-03-20 18:38:48 +04004import { z } from "zod";
giod0026612025-05-08 13:00:36 +00005import { DeepPartial, EventType, useForm } from "react-hook-form";
6import { zodResolver } from "@hookform/resolvers/zod";
7import { Form, FormControl, FormField, FormItem, FormMessage } from "./ui/form";
gio5f2f1002025-03-20 18:38:48 +04008import { Handle, Position } from "@xyflow/react";
giod0026612025-05-08 13:00:36 +00009import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
10import { GitHubRepository } from "../lib/github";
11import { useProjectId } from "@/lib/state";
12import { Alert, AlertDescription } from "./ui/alert";
13import { AlertCircle } from "lucide-react";
gio5f2f1002025-03-20 18:38:48 +040014
15export function NodeGithub(node: GithubNode) {
giod0026612025-05-08 13:00:36 +000016 const { id, selected } = node;
17 const isConnectable = useMemo(() => nodeIsConnectable(node, "repository"), [node]);
18 return (
19 <NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
20 <div style={{ padding: "10px 20px" }}>
21 {nodeLabel(node)}
22 <Handle
23 id="repository"
24 type={"source"}
25 position={Position.Right}
26 isConnectableStart={isConnectable}
27 isConnectableEnd={isConnectable}
28 isConnectable={isConnectable}
29 />
30 </div>
31 </NodeRect>
32 );
gio5f2f1002025-03-20 18:38:48 +040033}
34
35const schema = z.object({
giod0026612025-05-08 13:00:36 +000036 repositoryId: z.number().optional(),
gio5f2f1002025-03-20 18:38:48 +040037});
38
39export function NodeGithubDetails(node: GithubNode) {
giod0026612025-05-08 13:00:36 +000040 const { id, data } = node;
41 const store = useStateStore();
42 const projectId = useProjectId();
43 const [repos, setRepos] = useState<GitHubRepository[]>([]);
44 const [loading, setLoading] = useState(false);
45 const [error, setError] = useState<string | null>(null);
46 const githubService = useGithubService();
gio7f98e772025-05-07 11:00:14 +000047
giod0026612025-05-08 13:00:36 +000048 useEffect(() => {
49 if (data.repository) {
50 const { id, sshURL } = data.repository;
51 setRepos((prevRepos) => {
52 if (!prevRepos.some((r) => r.id === id)) {
53 return [
54 ...prevRepos,
55 {
56 id,
57 name: sshURL.split("/").pop() || "",
58 full_name: sshURL.split("/").slice(-2).join("/"),
59 html_url: "",
60 ssh_url: sshURL,
61 description: null,
62 private: false,
63 default_branch: "main",
64 },
65 ];
66 }
67 return prevRepos;
68 });
69 }
70 }, [data.repository]);
gio7f98e772025-05-07 11:00:14 +000071
giod0026612025-05-08 13:00:36 +000072 const form = useForm<z.infer<typeof schema>>({
73 resolver: zodResolver(schema),
74 mode: "onChange",
75 defaultValues: {
76 repositoryId: data.repository?.id,
77 },
78 });
gio7f98e772025-05-07 11:00:14 +000079
giod0026612025-05-08 13:00:36 +000080 useEffect(() => {
81 const sub = form.watch(
82 (
83 value: DeepPartial<z.infer<typeof schema>>,
84 { name, type }: { name?: keyof z.infer<typeof schema> | undefined; type?: EventType | undefined },
85 ) => {
86 if (type !== "change") {
87 return;
88 }
89 switch (name) {
90 case "repositoryId":
91 if (value.repositoryId) {
92 const repo = repos.find((r) => r.id === value.repositoryId);
93 if (repo) {
94 store.updateNodeData<"github">(id, {
95 repository: {
96 id: repo.id,
97 sshURL: repo.ssh_url,
98 },
99 });
100 }
101 }
102 break;
103 }
104 },
105 );
106 return () => sub.unsubscribe();
107 }, [form, store, id, repos]);
gio7f98e772025-05-07 11:00:14 +0000108
giod0026612025-05-08 13:00:36 +0000109 useEffect(() => {
110 const fetchRepositories = async () => {
111 if (!githubService) return;
gio7f98e772025-05-07 11:00:14 +0000112
giod0026612025-05-08 13:00:36 +0000113 setLoading(true);
114 setError(null);
115 try {
116 const repositories = await githubService.getRepositories();
117 setRepos(repositories);
118 } catch (err) {
119 setError(err instanceof Error ? err.message : "Failed to fetch repositories");
120 } finally {
121 setLoading(false);
122 }
123 };
gio7f98e772025-05-07 11:00:14 +0000124
giod0026612025-05-08 13:00:36 +0000125 fetchRepositories();
126 }, [githubService]);
gio7f98e772025-05-07 11:00:14 +0000127
giod0026612025-05-08 13:00:36 +0000128 return (
129 <>
130 <Form {...form}>
131 <form className="space-y-2">
132 <FormField
133 control={form.control}
134 name="repositoryId"
135 render={({ field }) => (
136 <FormItem>
137 <Select
138 onValueChange={(value) => field.onChange(Number(value))}
139 value={field.value?.toString()}
140 disabled={loading || !projectId || !githubService}
141 >
142 <FormControl>
143 <SelectTrigger>
144 <SelectValue
145 placeholder={
146 githubService ? "Select a repository" : "GitHub not configured"
147 }
148 />
149 </SelectTrigger>
150 </FormControl>
151 <SelectContent>
152 {repos.map((repo) => (
153 <SelectItem key={repo.id} value={repo.id.toString()}>
154 {repo.full_name}
155 {repo.description && ` - ${repo.description}`}
156 </SelectItem>
157 ))}
158 </SelectContent>
159 </Select>
160 <FormMessage />
161 {error && <p className="text-sm text-red-500">{error}</p>}
162 {loading && <p className="text-sm text-gray-500">Loading repositories...</p>}
163 {!githubService && (
164 <Alert variant="destructive" className="mt-2">
165 <AlertCircle className="h-4 w-4" />
166 <AlertDescription>
167 GitHub access token is not configured. Please configure it in the
168 Integrations tab.
169 </AlertDescription>
170 </Alert>
171 )}
172 </FormItem>
173 )}
174 />
175 </form>
176 </Form>
177 </>
178 );
179}