import { useState, useEffect, useCallback, useRef } from "react"; import type { Task } from "@kb/core"; import { apiFetchGitHubIssues, apiImportGitHubIssue, fetchGitRemotes, type GitHubIssue, type GitRemote } from "../api"; import { Loader2 } from "lucide-react"; interface GitHubImportModalProps { isOpen: boolean; onClose: () => void; onImport: (task: Task) => void; tasks: Task[]; } export function GitHubImportModal({ isOpen, onClose, onImport, tasks }: GitHubImportModalProps) { const [owner, setOwner] = useState(""); const [repo, setRepo] = useState(""); const [labels, setLabels] = useState(""); const [loading, setLoading] = useState(false); const [issues, setIssues] = useState([]); const [selectedIssueNumber, setSelectedIssueNumber] = useState(null); const [error, setError] = useState(null); const [importing, setImporting] = useState(false); // Git remotes state const [remotes, setRemotes] = useState([]); const [loadingRemotes, setLoadingRemotes] = useState(false); const [selectedRemoteName, setSelectedRemoteName] = useState(""); const mountedRef = useRef(false); // Build set of already imported URLs from existing tasks const importedUrls = new Set(); for (const task of tasks) { const match = task.description.match(/Source: (https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+)/); if (match) { importedUrls.add(match[1]); } } // Reset state when modal opens and fetch remotes useEffect(() => { if (isOpen) { setOwner(""); setRepo(""); setLabels(""); setIssues([]); setSelectedIssueNumber(null); setError(null); setImporting(false); setRemotes([]); setLoadingRemotes(true); setSelectedRemoteName(""); mountedRef.current = true; // Fetch git remotes fetchGitRemotes() .then((fetchedRemotes) => { if (!mountedRef.current) return; setRemotes(fetchedRemotes); setLoadingRemotes(false); if (fetchedRemotes.length === 1) { // Single remote: auto-select it const remote = fetchedRemotes[0]; setOwner(remote.owner); setRepo(remote.repo); setSelectedRemoteName(remote.name); } else if (fetchedRemotes.length > 1) { // Multiple remotes: don't auto-select, user must choose setOwner(""); setRepo(""); setSelectedRemoteName(""); } // If no remotes, owner/repo remain empty }) .catch(() => { if (mountedRef.current) { setLoadingRemotes(false); } }); return () => { mountedRef.current = false; }; } }, [isOpen]); // Handle remote selection change const handleRemoteChange = useCallback((remoteName: string) => { setSelectedRemoteName(remoteName); if (remoteName === "") { setOwner(""); setRepo(""); } else { const remote = remotes.find((r) => r.name === remoteName); if (remote) { setOwner(remote.owner); setRepo(remote.repo); } } }, [remotes]); // Handle escape key useEffect(() => { if (!isOpen) return; const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", handleKey); return () => document.removeEventListener("keydown", handleKey); }, [isOpen, onClose]); const handleLoad = useCallback(async () => { if (!owner.trim() || !repo.trim()) { setError("Repository must be selected"); return; } setLoading(true); setError(null); setIssues([]); setSelectedIssueNumber(null); try { const labelArray = labels .split(",") .map((l) => l.trim()) .filter(Boolean); const fetchedIssues = await apiFetchGitHubIssues(owner.trim(), repo.trim(), 30, labelArray.length > 0 ? labelArray : undefined); setIssues(fetchedIssues); if (fetchedIssues.length === 0) { setError("No open issues found"); } } catch (err: any) { setError(err.message || "Failed to fetch issues"); } finally { setLoading(false); } }, [owner, repo, labels]); const handleImport = useCallback(async () => { if (selectedIssueNumber === null) return; setImporting(true); setError(null); try { const task = await apiImportGitHubIssue(owner.trim(), repo.trim(), selectedIssueNumber); onImport(task); onClose(); } catch (err: any) { if (err.message?.includes("already imported")) { setError(err.message); } else { setError(err.message || "Failed to import issue"); } } finally { setImporting(false); } }, [selectedIssueNumber, owner, repo, onImport, onClose]); const selectedIssue = issues.find((i) => i.number === selectedIssueNumber); if (!isOpen) return null; // Determine the repository selection UI state const hasRemotes = remotes.length > 0; const singleRemote = remotes.length === 1; const multipleRemotes = remotes.length > 1; return (
e.target === e.currentTarget && onClose()}>

Import from GitHub

{/* Repository Selection Section */}
{/* Loading state */} {loadingRemotes && ( Loading remotes... )} {/* No remotes */} {!loadingRemotes && !hasRemotes && (
No GitHub remotes detected. Add a remote with: git remote add origin https://github.com/owner/repo.git
)} {/* Single remote - read only display */} {!loadingRemotes && singleRemote && ( {remotes[0].name} ({remotes[0].owner}/{remotes[0].repo}) )} {/* Multiple remotes - dropdown */} {!loadingRemotes && multipleRemotes && ( )}
setLabels(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleLoad()} disabled={loading || importing} />
{/* Error Display */} {error &&
{error}
} {/* Issues List */} {issues.length > 0 && ( <>

Found {issues.length} issues:

{issues.map((issue) => { const isImported = importedUrls.has(issue.html_url); return (
!isImported && setSelectedIssueNumber(issue.number)} > setSelectedIssueNumber(issue.number)} disabled={isImported} /> #{issue.number} {issue.title} {issue.labels.length > 0 && ( {issue.labels.map((l) => ( {l.name} ))} )} {isImported && Imported}
); })}
{/* Preview */} {selectedIssue && (

Preview

{selectedIssue.title}
{selectedIssue.body ? selectedIssue.body.slice(0, 200) + (selectedIssue.body.length > 200 ? "…" : "") : "(no description)"}
)} )}
); }