2cc492bb88
GET /git/refs lists local branches (upstream, ahead/behind, last commit, merged, held by a worktree, protected), remote branches grouped by remote and tags (target, annotated message/tagger/date). New writes in server/lib/git-refs.js, dispatched from git-ops.js: branch-rename, branch-upstream (set/unset), tag-create (light/annotated), tag-delete (confirm), tag-push (one tag to origin, no --tags, no force), and branch-create with a looked-up from + switch on a clean tree. Every ref is looked up, never passed through; repo-wide writes are repo-root only (ESUBDIR) and refused during any operation (EMERGING). Client: git/BranchManager + TagManager dialog opened from the git bar's tags icon, en+vi (gitRefs namespace), keyboard navigable. # Conflicts: # CLAUDE.md # client/src/components/lanes/LaneGitBar.tsx # docs/API.md # server/__tests__/lanes-api.test.js # server/routes/lanes.js
72 lines
2.0 KiB
TypeScript
72 lines
2.0 KiB
TypeScript
/**
|
|
* @file The git bar's "Branches & tags" icon button, which owns the Branches
|
|
* panel (`BranchManager`) it opens — portalled to `document.body`, so no
|
|
* scrolling or transformed ancestor of the Changes panel can clip it. The
|
|
* panel closes when the bar starts showing another workspace.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { useState } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Tags } from "lucide-react";
|
|
import type { GitSource } from "../../../lib/api";
|
|
import { BranchManager } from "./BranchManager";
|
|
|
|
export function BranchesButton({
|
|
laneId,
|
|
session,
|
|
source,
|
|
dirty,
|
|
merging,
|
|
disabled,
|
|
onChanged,
|
|
}: {
|
|
laneId: number;
|
|
session?: string;
|
|
source?: GitSource;
|
|
dirty: number;
|
|
merging: boolean;
|
|
disabled?: boolean;
|
|
onChanged: () => void;
|
|
}) {
|
|
const { t } = useTranslation(["gitRefs"]);
|
|
const [open, setOpen] = useState(false);
|
|
const [shown, setShown] = useState(`${laneId}|${session ?? ""}|${source ?? ""}`);
|
|
const key = `${laneId}|${session ?? ""}|${source ?? ""}`;
|
|
if (shown !== key) {
|
|
setShown(key);
|
|
setOpen(false);
|
|
}
|
|
return (
|
|
<>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen(true)}
|
|
disabled={disabled}
|
|
title={t("openPanelHint")}
|
|
aria-label={t("openPanel")}
|
|
aria-haspopup="dialog"
|
|
className="flex h-6 w-6 items-center justify-center rounded text-[11px] text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg-primary disabled:cursor-not-allowed disabled:opacity-40"
|
|
>
|
|
<Tags className="h-3.5 w-3.5" />
|
|
</button>
|
|
{open &&
|
|
createPortal(
|
|
<BranchManager
|
|
laneId={laneId}
|
|
session={session}
|
|
source={source}
|
|
dirty={dirty}
|
|
merging={merging}
|
|
onClose={() => setOpen(false)}
|
|
onChanged={onChanged}
|
|
/>,
|
|
document.body
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default BranchesButton;
|