"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { AlertCircle, GitBranch, Loader2, X } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { ConfirmActionDialog } from "@/components/ui/confirm-action-dialog"; import { Input } from "@/components/ui/input"; import { type BoardForgejoRepositoriesResponse, type BoardForgejoRepositoryLink, type ForgejoRepository, getBoardForgejoRepositories, getForgejoRepositories, linkBoardForgejoRepository, unlinkBoardForgejoRepository, } from "@/lib/api-forgejo"; interface BoardForgejoRepositoryLinksProps { boardId: string; canWrite?: boolean; } type LinkedRepository = BoardForgejoRepositoryLink & { repository: ForgejoRepository; }; const normalizeBoardLinks = ( result: BoardForgejoRepositoriesResponse, ): BoardForgejoRepositoryLink[] => Array.isArray(result) ? result : (result.repositories ?? []); const repositoryDisplayName = (repository: ForgejoRepository): string => repository.display_name || `${repository.owner}/${repository.repo}`; export function BoardForgejoRepositoryLinks({ boardId, canWrite = false, }: BoardForgejoRepositoryLinksProps) { const [linkedLinks, setLinkedLinks] = useState( [], ); const [allRepos, setAllRepos] = useState([]); const [searchTerm, setSearchTerm] = useState(""); const [isLoading, setIsLoading] = useState(true); const [linkError, setLinkError] = useState(null); const [unlinkError, setUnlinkError] = useState(null); const [isLinking, setIsLinking] = useState(false); const [isUnlinking, setIsUnlinking] = useState(false); const [unlinkTarget, setUnlinkTarget] = useState( null, ); const fetchLinkedRepos = useCallback(async () => { try { const result = await getBoardForgejoRepositories(boardId); setLinkedLinks(normalizeBoardLinks(result)); setLinkError(null); } catch (err) { const message = err instanceof Error ? err.message : "Unable to load linked Git Project repositories."; setLinkError(message); } }, [boardId]); const fetchAllRepositories = useCallback(async () => { try { const repos = await getForgejoRepositories(); setAllRepos(repos); setLinkError(null); } catch (err) { const message = err instanceof Error ? err.message : "Unable to load available Git Project repositories."; setLinkError(message); } }, []); useEffect(() => { let isMounted = true; const loadRepositories = async () => { setIsLoading(true); await Promise.all([fetchLinkedRepos(), fetchAllRepositories()]); if (isMounted) { setIsLoading(false); } }; loadRepositories(); return () => { isMounted = false; }; }, [fetchAllRepositories, fetchLinkedRepos]); const linkedRepoIds = useMemo( () => new Set(linkedLinks.map((link) => link.repository_id)), [linkedLinks], ); const repoById = useMemo( () => new Map(allRepos.map((repository) => [repository.id, repository])), [allRepos], ); const linkedRepos = useMemo( () => linkedLinks .map((link) => ({ ...link, repository: link.repository ?? repoById.get(link.repository_id), })) .filter( (link): link is LinkedRepository => link.repository !== undefined, ), [linkedLinks, repoById], ); const availableRepos = useMemo(() => { const query = searchTerm.toLowerCase().trim(); return allRepos .filter((repository) => !linkedRepoIds.has(repository.id)) .filter((repository) => { if (!query) { return true; } const haystack = [ repositoryDisplayName(repository), repository.owner, repository.repo, ] .join(" ") .toLowerCase(); return haystack.includes(query); }); }, [allRepos, linkedRepoIds, searchTerm]); const handleLinkRepo = async (repositoryId: string) => { if (!canWrite) { return; } setIsLinking(true); setLinkError(null); try { await linkBoardForgejoRepository(boardId, repositoryId); await fetchLinkedRepos(); } catch (err) { const message = err instanceof Error ? err.message : "Unable to link this repository to the board."; setLinkError(message); } finally { setIsLinking(false); } }; const handleUnlinkRepo = async () => { if (!unlinkTarget || !canWrite) { return; } setIsUnlinking(true); setUnlinkError(null); try { await unlinkBoardForgejoRepository(boardId, unlinkTarget.repository_id); await fetchLinkedRepos(); setUnlinkTarget(null); } catch (err) { const message = err instanceof Error ? err.message : "Unable to unlink this repository from the board."; setUnlinkError(message); } finally { setIsUnlinking(false); } }; return ( <>

Linked Git Project Repositories

Choose which synced repositories appear on this Pipeline board.

{linkedRepos.length} linked
{linkError && (
{linkError}
)} {isLoading ? (
Loading Git Project repositories...
) : (
On This Board
{linkedRepos.length === 0 ? (

No repositories linked yet

{canWrite ? "Link a Git Project repository below to bring its issues onto this board." : "No Git Project repositories are linked to this board yet."}

) : (
{linkedRepos.map((link) => { const repository = link.repository; return (

{repositoryDisplayName(repository)}

{repository.owner}/{repository.repo}

{canWrite ? ( ) : null}
); })}
)}
{canWrite ? (
Available Repositories

Link repositories that are already configured in Git Projects.

setSearchTerm(event.target.value)} placeholder="Search repositories..." className="w-full sm:w-72" />
{availableRepos.length === 0 ? (

{allRepos.length === 0 ? "No Git Project repositories configured" : "No matching repositories"}

{allRepos.length === 0 ? "Add repositories in Git Projects before linking them to boards." : "Adjust the search or unlink a repository from this board."}

) : (
{availableRepos.slice(0, 9).map((repository) => (

{repositoryDisplayName(repository)}

{repository.active ? "Active" : "Paused"}

{repository.owner}/{repository.repo}

))}
)}
) : null}
)}
{ if (!open && !isUnlinking) { setUnlinkTarget(null); setUnlinkError(null); } }} title="Unlink Git Project repository" description={ unlinkTarget ? `Remove "${repositoryDisplayName(unlinkTarget.repository)}" from this board? Issues from this repository will no longer appear on the board.` : "Remove this repository from the board?" } onConfirm={handleUnlinkRepo} isConfirming={isUnlinking} errorMessage={unlinkError} confirmLabel="Unlink Repository" confirmingLabel="Unlinking..." confirmClassName="bg-[color:var(--danger)] text-white hover:bg-[color:var(--danger)]/90" cancelLabel="Keep Linked" /> ); }