blob: 6ece1b08bcef38736fa26b2181ae04cd9e566d0f [file] [log] [blame]
gio5f2f1002025-03-20 18:38:48 +04001import { NodeRect } from './node-rect';
gio7f98e772025-05-07 11:00:14 +00002import { GithubNode, nodeIsConnectable, nodeLabel, useStateStore, useGithubService } from '@/lib/state';
3import { useEffect, useMemo, useState } from 'react';
gio5f2f1002025-03-20 18:38:48 +04004import { z } from "zod";
5import { 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";
gio7f98e772025-05-07 11:00:14 +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) {
16 const { id, selected } = node;
17 const isConnectable = useMemo(() => nodeIsConnectable(node, "repository"), [node]);
18 return (
gio1dc800a2025-04-24 17:15:43 +000019 <NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
gio5f2f1002025-03-20 18:38:48 +040020 <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}
gio7f98e772025-05-07 11:00:14 +000029 />
gio5f2f1002025-03-20 18:38:48 +040030 </div>
31 </NodeRect>
32 );
33}
34
35const schema = z.object({
gio7f98e772025-05-07 11:00:14 +000036 repositoryId: z.number().optional(),
gio5f2f1002025-03-20 18:38:48 +040037});
38
39export function NodeGithubDetails(node: GithubNode) {
40 const { id, data } = node;
41 const store = useStateStore();
gio7f98e772025-05-07 11:00:14 +000042 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();
47
48 useEffect(() => {
49 if (data.repository) {
50 const { id, sshURL } = data.repository;
51 setRepos(prevRepos => {
52 if (!prevRepos.some(r => r.id === id)) {
53 return [...prevRepos, {
54 id,
55 name: sshURL.split('/').pop() || '',
56 full_name: sshURL.split('/').slice(-2).join('/'),
57 html_url: '',
58 ssh_url: sshURL,
59 description: null,
60 private: false,
61 default_branch: 'main'
62 }];
63 }
64 return prevRepos;
65 });
66 }
67 }, [data.repository]);
68
gio5f2f1002025-03-20 18:38:48 +040069 const form = useForm<z.infer<typeof schema>>({
70 resolver: zodResolver(schema),
71 mode: "onChange",
72 defaultValues: {
gio7f98e772025-05-07 11:00:14 +000073 repositoryId: data.repository?.id,
gio5f2f1002025-03-20 18:38:48 +040074 }
75 });
gio7f98e772025-05-07 11:00:14 +000076
gio5f2f1002025-03-20 18:38:48 +040077 useEffect(() => {
78 const sub = form.watch((value: DeepPartial<z.infer<typeof schema>>, { name, type }: { name?: keyof z.infer<typeof schema> | undefined, type?: EventType | undefined }) => {
79 if (type !== "change") {
80 return;
81 }
82 switch (name) {
gio7f98e772025-05-07 11:00:14 +000083 case "repositoryId":
84 if (value.repositoryId) {
85 const repo = repos.find(r => r.id === value.repositoryId);
86 if (repo) {
87 store.updateNodeData<"github">(id, {
88 repository: {
89 id: repo.id,
90 sshURL: repo.ssh_url,
91 },
92 });
93 }
94 }
gio5f2f1002025-03-20 18:38:48 +040095 break;
96 }
97 });
98 return () => sub.unsubscribe();
gio7f98e772025-05-07 11:00:14 +000099 }, [form, store, id, repos]);
100
101 useEffect(() => {
102 const fetchRepositories = async () => {
gio6cf8c272025-05-08 09:01:38 +0000103 if (!githubService) return;
gio7f98e772025-05-07 11:00:14 +0000104
105 setLoading(true);
106 setError(null);
107 try {
108 const repositories = await githubService.getRepositories();
109 setRepos(repositories);
110 } catch (err) {
111 setError(err instanceof Error ? err.message : "Failed to fetch repositories");
112 } finally {
113 setLoading(false);
114 }
115 };
116
117 fetchRepositories();
118 }, [githubService]);
119
gio5f2f1002025-03-20 18:38:48 +0400120 return (
121 <>
122 <Form {...form}>
123 <form className="space-y-2">
gio7f98e772025-05-07 11:00:14 +0000124 <FormField
gio5f2f1002025-03-20 18:38:48 +0400125 control={form.control}
gio7f98e772025-05-07 11:00:14 +0000126 name="repositoryId"
gio5f2f1002025-03-20 18:38:48 +0400127 render={({ field }) => (
128 <FormItem>
gio7f98e772025-05-07 11:00:14 +0000129 <Select
130 onValueChange={(value) => field.onChange(Number(value))}
131 value={field.value?.toString()}
gio6cf8c272025-05-08 09:01:38 +0000132 disabled={loading || !projectId || !githubService}
gio7f98e772025-05-07 11:00:14 +0000133 >
134 <FormControl>
135 <SelectTrigger>
gio6cf8c272025-05-08 09:01:38 +0000136 <SelectValue placeholder={githubService ? "Select a repository" : "GitHub not configured"} />
gio7f98e772025-05-07 11:00:14 +0000137 </SelectTrigger>
138 </FormControl>
139 <SelectContent>
140 {repos.map((repo) => (
141 <SelectItem key={repo.id} value={repo.id.toString()}>
142 {repo.full_name}
143 {repo.description && ` - ${repo.description}`}
144 </SelectItem>
145 ))}
146 </SelectContent>
147 </Select>
gio5f2f1002025-03-20 18:38:48 +0400148 <FormMessage />
gio7f98e772025-05-07 11:00:14 +0000149 {error && <p className="text-sm text-red-500">{error}</p>}
150 {loading && <p className="text-sm text-gray-500">Loading repositories...</p>}
gio6cf8c272025-05-08 09:01:38 +0000151 {!githubService && (
gio7f98e772025-05-07 11:00:14 +0000152 <Alert variant="destructive" className="mt-2">
153 <AlertCircle className="h-4 w-4" />
154 <AlertDescription>
155 GitHub access token is not configured. Please configure it in the Integrations tab.
156 </AlertDescription>
157 </Alert>
158 )}
gio5f2f1002025-03-20 18:38:48 +0400159 </FormItem>
160 )}
161 />
162 </form>
163 </Form>
164 </>);
165}