Files
AgentHub/client/src/components/lanes/git/CreateBranchDialog.tsx
T

129 lines
3.9 KiB
TypeScript

/**
* @file "Create branch here…" and "Create tag here…" from a History commit's
* menu: a name (and, for a tag, an optional message making it annotated),
* then git-refs.js's `branch-create {branch, from}` / `tag-create {tag,
* message?, from}` with that commit's sha as `from` — the server confirms the
* sha itself. Creating a branch never switches to it here. Announces
* `agenthub:git-changed` so the other views of the workspace read again.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useState, type KeyboardEvent } from "react";
import { useTranslation } from "react-i18next";
import { api, type GitTarget } from "../../../lib/api";
import { toast } from "../../../lib/toast";
import { isSubmitEnter } from "../../../lib/keys";
import { emitGitChanged } from "../../../lib/gitChanged";
import { GitModal, ModalButton } from "./GitModal";
export function CreateBranchDialog({
target,
sha,
short,
kind = "branch",
onClose,
onDone,
}: {
target: GitTarget;
sha: string;
short: string;
kind?: "branch" | "tag";
onClose: () => void;
onDone: () => void;
}) {
const { t } = useTranslation(["gitHistory"]);
const [name, setName] = useState("");
const [message, setMessage] = useState("");
const [error, setError] = useState<string | null>(null);
const [running, setRunning] = useState(false);
const tag = kind === "tag";
const run = async () => {
const ref = name.trim();
if (!ref) return;
setRunning(true);
setError(null);
const action = tag ? "tag-create" : "branch-create";
try {
await api.lanes.gitRefAction(
target,
action,
tag
? { tag: ref, from: sha, ...(message.trim() ? { message: message.trim() } : {}) }
: { branch: ref, from: sha }
);
toast.success(t(tag ? "tagCreated" : "branchCreated", { name: ref, branch: ref, short }));
onDone();
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
setRunning(false);
} finally {
emitGitChanged(typeof target === "number" ? target : target.lane, action);
}
};
const submit = (e: KeyboardEvent) => {
if (isSubmitEnter(e)) {
e.preventDefault();
void run();
}
};
const input =
"rounded border border-border-light bg-surface-0 px-2 py-1 font-mono text-[12px] text-fg-primary";
return (
<GitModal
testId="create-ref"
onClose={onClose}
title={t(tag ? "createTagTitle" : "createBranchTitle", { short })}
footer={
<>
<ModalButton onClick={onClose}>{t("cancel")}</ModalButton>
<ModalButton
primary
testId="create-ref-confirm"
disabled={!name.trim() || running}
onClick={() => void run()}
>
{t(tag ? "createTag" : "createBranch")}
</ModalButton>
</>
}
>
<label className="flex flex-col gap-1 text-[12px] text-fg-secondary">
{t(tag ? "tagName" : "branchName")}
<input
autoFocus
data-testid="create-ref-name"
value={name}
maxLength={200}
onChange={(e) => setName(e.target.value)}
onKeyDown={submit}
className={input}
/>
</label>
{tag && (
<label className="mt-2 flex flex-col gap-1 text-[12px] text-fg-secondary">
{t("tagMessage")}
<input
data-testid="create-ref-message"
value={message}
maxLength={1000}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={submit}
className={input}
/>
</label>
)}
<p className="mt-1.5 text-[11px] text-fg-muted">
{t(tag ? "createTagHint" : "createBranchHint")}
</p>
{error && (
<p role="alert" className="mt-1.5 text-[12px] text-status-danger [overflow-wrap:anywhere]">
{error}
</p>
)}
</GitModal>
);
}