/** * @file CloneHereDialog.tsx * @description The Files page's **New → Clone repository**: clones a URL into * the folder the File Explorer has open, through the same * `POST /api/lanes/clone` as Add workspace → Clone — the server holds the URL * to its allowlist and the folder to the user's own, and clones as the * folder's account with that account's own SSH key / HTTPS login — and opens * a workspace on the new folder. A 409 `ECREDENTIALS` opens a login row (sent * once, optionally remembered), `ESSHAUTH` points to Settings → Git. * @author Nguyễn Ngọc Trí Vĩ */ import { useEffect, useId, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; import { FolderGit2 } from "lucide-react"; import { api } from "../../lib/api"; import type { Lane } from "../../lib/types"; import { useDialog } from "../../hooks/useDialog"; import { cloneDirName } from "./AddLaneModal"; type Login = { host: string; username: string; token: string; remember: boolean }; export function CloneHereDialog({ open, parent, onClose, onCloned, }: { open: boolean; /** The absolute folder the clone goes into. */ parent: string; onClose: () => void; onCloned: (lane: Lane, path: string) => void; }) { const { t } = useTranslation(["lanes"]); const panelRef = useRef(null); const titleId = useId(); const [url, setUrl] = useState(""); const [name, setName] = useState(""); const [login, setLogin] = useState(null); const [sshRefused, setSshRefused] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); useDialog(open, panelRef, busy ? () => {} : onClose); useEffect(() => { if (!open) return; setUrl(""); setName(""); setLogin(null); setSshRefused(false); setBusy(false); setError(null); }, [open, parent]); if (!open) return null; const folder = name.trim() || cloneDirName(url); const into = `${parent.replace(/\/+$/, "")}/${folder || "…"}`; const submit = async () => { const u = url.trim(); if (!u || busy) return; setBusy(true); setError(null); setSshRefused(false); try { const result = await api.lanes.clone({ url: u, parent, name: name.trim() || undefined, ...(login?.username.trim() && login.token ? { credentials: { username: login.username.trim(), password: login.token, remember: login.remember, }, } : {}), }); onCloned(result.lane, result.path); } catch (err) { const e = err as { code?: string; auth?: { host?: string }; message?: string }; const host = e.auth?.host || u; if (e.code === "ECREDENTIALS") { setError(t(login?.token ? "clone.loginRejected" : "clone.loginNeeded", { host })); setLogin((l) => ({ host, username: l?.username ?? "", token: "", remember: l?.remember ?? true, })); } else if (e.code === "ESSHAUTH") { setSshRefused(true); setError(t("clone.sshRefused", { host })); } else { setError(err instanceof Error ? err.message : String(err)); } setBusy(false); } }; return createPortal(
e.stopPropagation()} role="presentation" >
e.stopPropagation()} >

{t("fx.cloneRepo")}

{ e.preventDefault(); void submit(); }} className="space-y-3" >
setUrl(e.target.value)} placeholder="https://host/owner/repo.git" disabled={busy} className="input w-full font-mono text-xs" />

{t("clone.urlHint")}

setName(e.target.value)} placeholder={cloneDirName(url)} disabled={busy} className="input w-full font-mono text-xs" />

{t("clone.parentHint", { path: into })}

{login && (
setLogin({ ...login, username: e.target.value })} disabled={busy} className="input w-full text-xs" /> setLogin({ ...login, token: e.target.value })} disabled={busy} className="input w-full text-xs" />
)} {error && (

{error}{" "} {sshRefused && ( {t("clone.sshSettings")} )}

)}
, document.body ); }