blob: 4c065be78ac5f7b0db14051f219edc63756a60fe [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
gio3ec94242025-05-16 12:46:57 +000039export function NodeGithubDetails({ id, data, disabled }: GithubNode & { disabled?: boolean }) {
giod0026612025-05-08 13:00:36 +000040 const store = useStateStore();
41 const projectId = useProjectId();
42 const [repos, setRepos] = useState<GitHubRepository[]>([]);
43 const [loading, setLoading] = useState(false);
44 const [error, setError] = useState<string | null>(null);
45 const githubService = useGithubService();
gio7f98e772025-05-07 11:00:14 +000046
giod0026612025-05-08 13:00:36 +000047 useEffect(() => {
48 if (data.repository) {
49 const { id, sshURL } = data.repository;
50 setRepos((prevRepos) => {
51 if (!prevRepos.some((r) => r.id === id)) {
52 return [
53 ...prevRepos,
54 {
55 id,
56 name: sshURL.split("/").pop() || "",
57 full_name: sshURL.split("/").slice(-2).join("/"),
58 html_url: "",
59 ssh_url: sshURL,
60 description: null,
61 private: false,
62 default_branch: "main",
63 },
64 ];
65 }
66 return prevRepos;
67 });
68 }
69 }, [data.repository]);
gio7f98e772025-05-07 11:00:14 +000070
giod0026612025-05-08 13:00:36 +000071 const form = useForm<z.infer<typeof schema>>({
72 resolver: zodResolver(schema),
73 mode: "onChange",
74 defaultValues: {
75 repositoryId: data.repository?.id,
76 },
77 });
gio7f98e772025-05-07 11:00:14 +000078
giod0026612025-05-08 13:00:36 +000079 useEffect(() => {
80 const sub = form.watch(
81 (
82 value: DeepPartial<z.infer<typeof schema>>,
83 { name, type }: { name?: keyof z.infer<typeof schema> | undefined; type?: EventType | undefined },
84 ) => {
85 if (type !== "change") {
86 return;
87 }
88 switch (name) {
89 case "repositoryId":
90 if (value.repositoryId) {
91 const repo = repos.find((r) => r.id === value.repositoryId);
92 if (repo) {
93 store.updateNodeData<"github">(id, {
94 repository: {
95 id: repo.id,
96 sshURL: repo.ssh_url,
gio818da4e2025-05-12 14:45:35 +000097 fullName: repo.full_name,
giod0026612025-05-08 13:00:36 +000098 },
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()}
gio3ec94242025-05-16 12:46:57 +0000140 disabled={loading || !projectId || !githubService || disabled}
giod0026612025-05-08 13:00:36 +0000141 >
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>
gio818da4e2025-05-12 14:45:35 +0000167 Please configure Github Personal Access Token in the Integrations tab.
giod0026612025-05-08 13:00:36 +0000168 </AlertDescription>
169 </Alert>
170 )}
171 </FormItem>
172 )}
173 />
174 </form>
175 </Form>
176 </>
177 );
178}