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); // Auto-populate if exactly one remote and fields are empty if (fetchedRemotes.length === 1) { const remote = fetchedRemotes[0]; setOwner(remote.owner); setRepo(remote.repo); setSelectedRemoteName(remote.name); } }) .catch(() => { // Silently fail - manual input remains available if (mountedRef.current) { setLoadingRemotes(false); } }); return () => { mountedRef.current = false; }; } }, [isOpen]); // Handle remote selection change const handleRemoteChange = useCallback((remoteName: string) => { setSelectedRemoteName(remoteName); if (remoteName === "") { // Manual mode - clear fields 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("Owner and repo are required"); 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 if we should show the remote dropdown const showRemoteDropdown = remotes.length > 1 || (remotes.length === 1 && !loadingRemotes); const hasRemotes = remotes.length > 0; return (
e.target === e.currentTarget && onClose()}>

Import from GitHub

{/* Remote Selection Dropdown */} {(showRemoteDropdown || loadingRemotes) && (
)} {/* Form Row */}
setOwner(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleLoad()} disabled={loading || importing} />
setRepo(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleLoad()} disabled={loading || importing} />
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)"}
)} )}
); }