import { CheckIcon, Loader2Icon, PencilIcon, Trash2Icon, XIcon } from "lucide-react"; import { useEffect, useInsertionEffect, useLayoutEffect, useRef, useState } from "react"; import { RepositoryRefInput } from "./repository-ref-input"; import { Button } from "../../ui/button"; import { Input } from "../../ui/input"; import { MetaChip } from "../../ui/meta-chip"; import { userErrorText } from "../../ui/api-error"; import type { RepoDraft } from "../lib/session-tools"; export type ManualRepositoryAttachResult = { warning?: string | undefined } | void; export function ManualRepositoryEditor(props: { repository: RepoDraft; mounted: boolean; pending: boolean; onUpdate: (patch: Partial) => void; onRemove: () => void; onAttach: (repository: RepoDraft) => Promise; }) { const [editing, setEditing] = useState(props.repository.attached === false); const [url, setUrl] = useState(props.repository.url); const [ref, setRef] = useState(props.repository.ref); const [checking, setChecking] = useState(false); const [error, setError] = useState(null); const [warning, setWarning] = useState(null); const [confirmRemove, setConfirmRemove] = useState(false); useEffect(() => { if (editing) return; setUrl(props.repository.url); setRef(props.repository.ref); }, [editing, props.repository.ref, props.repository.url]); useEffect(() => { if (props.repository.attached === true) setEditing(false); }, [props.repository.attached]); // While an attached row is open for editing, the unsaved url/ref live only // here. Mark the row so catalog reconciliation does not fold it away (and // unmount this editor) mid-edit; clear the mark when the edit ends or the // editor goes away. Layout timing marks the row in the same commit. const onUpdateRef = useRef(props.onUpdate); useInsertionEffect(() => { onUpdateRef.current = props.onUpdate; }, [props.onUpdate]); const editingAttached = editing && props.repository.attached === true && !props.mounted; useLayoutEffect(() => { if (!editingAttached) return; onUpdateRef.current({ editing: true }); return () => onUpdateRef.current({ editing: undefined }); }, [editingAttached]); function updateDraft(patch: Partial): void { if (props.repository.attached === false) props.onUpdate({ ...patch, attached: false }); } async function attach(): Promise { if (checking || props.pending || props.mounted) return; const reattaching = props.repository.attached === true; setChecking(true); setError(null); setWarning(null); try { const result = await props.onAttach({ ...props.repository, url, ref, attached: false, }); setWarning(result?.warning ?? null); // A first attach ends the edit when the row turns attached. A row that // was already attached stays attached, so end its edit here: the saved // row is no longer marked as mid-edit. if (reattaching) setEditing(false); } catch (caught) { setError(userErrorText(caught)); } finally { setChecking(false); } } if (props.mounted || (props.repository.attached === true && !editing)) { return (
{props.repository.url}
Ref {props.repository.ref}
{props.mounted ? ( Mounted ) : ( Attached )}
{!props.mounted ? (
{confirmRemove ? ( <> ) : ( )}
) : null}
); } return (
{ if (event.key === "Enter" && event.target instanceof HTMLInputElement) { event.preventDefault(); void attach(); } }} >
{ setUrl(event.target.value); setError(null); updateDraft({ url: event.target.value }); }} disabled={props.pending || checking} placeholder="https://github.com/org/repo" aria-label="Repository URL" className="h-8 text-xs" /> { setRef(value); setError(null); updateDraft({ ref: value }); }} />
{props.repository.attached === true ? ( ) : null} {confirmRemove ? ( <> ) : ( )}
{error ? (

{error}

) : warning ? (

{warning}

) : null}
); }