/// import { useEffect, useMemo, useRef, useState } from "react"; import { Check, GitBranch, Tag } from "lucide-react"; import { hydrateIsland } from "@/client/hydrate"; import { buttonClasses } from "@/client/components/ui/button"; type RefItem = { name: string; displayName: string; }; type RefApiResponse = { branches?: RefItem[]; tags?: RefItem[]; }; export type RefPickerProps = { owner: string; repo: string; currentRef: string; }; function decodeSafe(value: string): string { try { return decodeURIComponent(value); } catch { return value; } } function formatRefLabel(ref: string): string { return /^[0-9a-f]{40}$/i.test(ref) ? `${ref.slice(0, 7)}...` : ref || "..."; } function buildRefHref(currentUrl: string, nextRef: string): string { const url = new URL(currentUrl); url.searchParams.set("ref", nextRef); return url.toString(); } function RefPickerSection({ title, kind, currentRef, currentUrl, items, query, }: { title: string; kind: "branch" | "tag"; currentRef: string; currentUrl: string | null; items: RefItem[]; query: string; }) { const filtered = items.filter((item) => item.displayName.toLowerCase().includes(query)); if (!filtered.length) { return null; } const Icon = kind === "branch" ? GitBranch : Tag; return ( <>
{title}
{filtered.map((item) => { const raw = decodeSafe(item.name); const isCurrent = raw === currentRef; const href = currentUrl ? buildRefHref(currentUrl, raw) : null; return isCurrent ? ( ) : href ? ( ) : ( ); })} ); } export function RefPickerIsland({ owner, repo, currentRef }: RefPickerProps) { const rootRef = useRef(null); const filterRef = useRef(null); const [currentUrl, setCurrentUrl] = useState(null); const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [data, setData] = useState({ branches: [], tags: [] }); useEffect(() => { setCurrentUrl(window.location.href); }, []); useEffect(() => { let cancelled = false; async function loadRefs() { setLoading(true); setError(null); try { const response = await fetch(`/${owner}/${repo}/api/refs`); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const nextData = (await response.json()) as RefApiResponse; if (!cancelled) { setData({ branches: nextData.branches || [], tags: nextData.tags || [], }); } } catch (err) { if (!cancelled) { setError(err instanceof Error ? err.message : "Failed to load refs"); } } finally { if (!cancelled) { setLoading(false); } } } void loadRefs(); return () => { cancelled = true; }; }, [owner, repo]); useEffect(() => { if (!open) { return; } const timeout = window.setTimeout(() => { filterRef.current?.focus(); filterRef.current?.select(); }, 0); function onPointerDown(event: MouseEvent) { if (!rootRef.current?.contains(event.target as Node)) { setOpen(false); } } function onKeyDown(event: KeyboardEvent) { if (event.key === "Escape") { setOpen(false); } } document.addEventListener("mousedown", onPointerDown); document.addEventListener("keydown", onKeyDown); return () => { window.clearTimeout(timeout); document.removeEventListener("mousedown", onPointerDown); document.removeEventListener("keydown", onKeyDown); }; }, [open]); const branches = data.branches || []; const tags = data.tags || []; const queryLower = query.trim().toLowerCase(); const hasCurrentRef = useMemo( () => [...branches, ...tags].some((item) => decodeSafe(item.name) === currentRef), [branches, currentRef, tags] ); const showCurrentChip = !hasCurrentRef && currentRef && formatRefLabel(currentRef).includes(queryLower); return (
{ event.preventDefault(); setOpen((value) => !value); }} > {formatRefLabel(currentRef)}
setQuery(event.target.value)} className="w-full rounded-lg border border-zinc-300 bg-white px-2 py-1.5 text-sm dark:border-zinc-700 dark:bg-zinc-900" autoComplete="off" />
{loading ? (
Loading...
) : null} {!loading && error ? (
{error}
) : null} {!loading && !error ? ( <> {showCurrentChip ? ( <>
Current
) : null} {!showCurrentChip && !branches.some((item) => item.displayName.toLowerCase().includes(queryLower)) && !tags.some((item) => item.displayName.toLowerCase().includes(queryLower)) ? (
No refs
) : null} ) : null}
); } export function initRefPicker() { hydrateIsland("ref-picker", RefPickerIsland); }