import { useState } from "react" import { useNavigate } from "react-router-dom" import { X, Folder, ArrowUp, Check, GitBranch } from "lucide-react" import { useBrowse, useAdoptProgram } from "@/services/api/hooks" import { TextField } from "./detail/fields" const IS_GIT_URL = /^(https?:\/\/|git@|ssh:\/\/)|\.git$/ /** Split the typed path into the directory to browse and the trailing partial * segment to filter by — the autocomplete engine. Typing within a directory only * changes the filter (client-side, no refetch); crossing a "/" browses the new dir. * "/data/repos/wid" -> browse "/data/repos", filter "wid" * "/data/repos/" -> browse "/data/repos", filter "" * "wid" -> browse the default repos dir, filter "wid" */ function splitPath(raw: string): { dir: string | null; filter: string } { if (raw === "") return { dir: null, filter: "" } if (raw.endsWith("/")) return { dir: raw.length > 1 ? raw.slice(0, -1) : "/", filter: "" } const idx = raw.lastIndexOf("/") if (idx === -1) return { dir: null, filter: raw } if (idx === 0) return { dir: "/", filter: raw.slice(1) } return { dir: raw.slice(0, idx), filter: raw.slice(idx + 1) } } /** Adopt an existing repo as a program (the web `wildpc program add`). Programs * live on the *server's* filesystem, so the picker browses the server's dirs: type * a path (autocompletes as you go), navigate the listing, or paste a git URL. */ export function AddProgramForm({ existingNames, onCancel, }: { existingNames: string[] onCancel: () => void }) { const navigate = useNavigate() const adopt = useAdoptProgram() // The path bar is the single source of truth — its value is the adopt target. // Empty browses the server's repos dir (surfaced as the placeholder), so the // listing is populated from the first render without seeding the field. const [input, setInput] = useState("") const [name, setName] = useState("") const [description, setDescription] = useState("") const [error, setError] = useState("") const target = input.trim() const isGit = IS_GIT_URL.test(target) const { dir, filter } = splitPath(target) const { data: browse, isFetching, error: browseError } = useBrowse(dir, !isGit) const entries = (browse?.entries ?? []).filter((e) => filter ? e.name.toLowerCase().startsWith(filter.toLowerCase()) : true, ) const suggestedName = target ? target.replace(/\/+$/, "").split("/").pop()!.replace(/\.git$/, "") : "" const effectiveName = name.trim() || suggestedName const nameError = name && !/^[a-z0-9][a-z0-9-]*$/.test(name) ? "lowercase letters, numbers, hyphens" : effectiveName && existingNames.includes(effectiveName) ? "already exists" : "" const submit = async () => { if (!target || nameError) return setError("") try { const res = await adopt.mutateAsync({ target, name: name.trim() || undefined, description: description.trim() || undefined, }) navigate(`/programs/${res.program}`) } catch (e: unknown) { let msg = e instanceof Error ? e.message : String(e) try { msg = JSON.parse((e as Error).message).detail ?? msg } catch { /* keep msg */ } setError(msg) } } return (
{(() => { const m = browseError instanceof Error ? browseError.message : "" try { return JSON.parse(m).detail ?? "Cannot read directory" } catch { return m || "Cannot read directory" } })()}
) : entries.length > 0 ? ( entries.map((e) => ({filter ? "No matching sub-directories." : "No sub-directories."}
)}Registering {target} {isGit && " (cloned later via wildpc clone)"}
)}{nameError}
}