feat(lanes): move the lane's git section into the Changes panel
- The branch line, the uncommitted-files hover panel and the git action row (fetch/pull/push/branch) move from the lane card to the top of the Changes panel (new LaneGitHeader.tsx); the card has no git row now. - Header and panel share one git-facts read: stage/unstage/discard/commit and Refresh re-read it, and a git bar action re-reads the diff, the history and the file tree. - The git bar no longer commits: the panel's staged-only box is the one commit path. Orphaned gitBar.commit*/cancel strings removed (en, vi). - "New lane here" is wired from Workspace to the panel.
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
/**
|
||||
* @file The lane's git action row: ahead/behind against the upstream, and the
|
||||
* writes `POST /api/lanes/:id/git/:action` offers that are not per-file —
|
||||
* fetch, pull, push, commit (everything, or only what the Changes panel has
|
||||
* staged), switch branch, and create/delete a branch. Per-file staging and
|
||||
* discard live in the Changes panel, beside the file they act on.
|
||||
* fetch, pull, push, switch branch, and create/delete a branch. Committing is
|
||||
* NOT here: it is the Changes panel's staged-only commit box, beside the
|
||||
* per-file stage/unstage/discard it commits — one way to commit, and it only
|
||||
* ever takes what the user staged.
|
||||
*
|
||||
* It renders beside the read-only git line in LaneCard rather than inside the
|
||||
* ⋯ menu: that menu holds the rare and dangerous (`reset worktree`), and these
|
||||
* are the opposite — the ordinary moves a lane needs several times an hour.
|
||||
* It renders at the top of the Changes panel (`LaneGitHeader`) rather than in
|
||||
* the lane card's ⋯ menu: that menu holds the rare and dangerous (`reset
|
||||
* worktree`), and these are the ordinary moves a lane needs several times an hour.
|
||||
* Nothing here decides what an action may do; the server does (pull is always
|
||||
* --ff-only, switch refuses a dirty tree), and this only reports what it said.
|
||||
* Failures print git's own text, because "! [rejected] non-fast-forward" tells
|
||||
@@ -17,21 +18,11 @@
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Check,
|
||||
GitBranch,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
Upload,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { ArrowDown, ArrowUp, GitBranch, Plus, RefreshCw, Trash2, Upload } from "lucide-react";
|
||||
import { api } from "../../lib/api";
|
||||
import type { LaneGitFacts, LaneWorktree } from "../../lib/types";
|
||||
|
||||
type Action = "fetch" | "pull" | "push" | "commit" | "switch" | "branch-create" | "branch-delete";
|
||||
type Action = "fetch" | "pull" | "push" | "switch" | "branch-create" | "branch-delete";
|
||||
|
||||
function Btn({
|
||||
label,
|
||||
@@ -79,17 +70,15 @@ export function LaneGitBar({
|
||||
const { t } = useTranslation(["lanes"]);
|
||||
const [busy, setBusy] = useState<Action | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [committing, setCommitting] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
const [switching, setSwitching] = useState(false);
|
||||
const [branches, setBranches] = useState<string[] | null>(null);
|
||||
const [worktrees, setWorktrees] = useState<LaneWorktree[]>([]);
|
||||
const [picked, setPicked] = useState("");
|
||||
const [newBranch, setNewBranch] = useState("");
|
||||
// LaneCard is not keyed by lane, so this bar outlives a lane switch. All of
|
||||
// the state above describes ONE lane — a message typed for lane A, Entered
|
||||
// after switching, would commit B's whole tree under it — so it is reset
|
||||
// during render the moment the id changes, before anything can act on it.
|
||||
// The Changes panel is not keyed by lane, so this bar outlives a lane switch.
|
||||
// All of the state above describes ONE lane — a branch picked for lane A,
|
||||
// applied after switching, would move B's HEAD — so it is reset during
|
||||
// render the moment the id changes, before anything can act on it.
|
||||
const [shownLane, setShownLane] = useState(laneId);
|
||||
// One action at a time, checked synchronously: `busy` is state, and a second
|
||||
// Enter can land before the render that disables anything.
|
||||
@@ -98,8 +87,6 @@ export function LaneGitBar({
|
||||
setShownLane(laneId);
|
||||
setBusy(null);
|
||||
setError(null);
|
||||
setCommitting(false);
|
||||
setMessage("");
|
||||
setSwitching(false);
|
||||
setBranches(null);
|
||||
setWorktrees([]);
|
||||
@@ -113,10 +100,7 @@ export function LaneGitBar({
|
||||
if (!git.available) return null;
|
||||
const dirty = git.dirty + git.untracked;
|
||||
|
||||
const run = async (
|
||||
action: Action,
|
||||
body?: { message?: string; only?: "staged"; branch?: string }
|
||||
) => {
|
||||
const run = async (action: Action, body?: { branch?: string }) => {
|
||||
if (inFlight.current) return;
|
||||
inFlight.current = true;
|
||||
const lane = laneId;
|
||||
@@ -127,12 +111,6 @@ export function LaneGitBar({
|
||||
// Landed after a lane switch: every write below belongs to the lane
|
||||
// no longer shown.
|
||||
if (laneNow.current !== lane) return;
|
||||
// Only a landed commit consumes the message; a fetch or pull mid-typing
|
||||
// must leave it where it was.
|
||||
if (action === "commit") {
|
||||
setCommitting(false);
|
||||
setMessage("");
|
||||
}
|
||||
// The branch list this bar is showing is stale the moment a branch is
|
||||
// created, deleted or switched to, so it is re-read rather than patched.
|
||||
if (action === "switch" || action.startsWith("branch-")) {
|
||||
@@ -244,16 +222,6 @@ export function LaneGitBar({
|
||||
title={t("gitBar.pushHint")}
|
||||
onClick={() => run("push")}
|
||||
/>
|
||||
<Btn
|
||||
label={t("gitBar.commit")}
|
||||
icon={<Check className="h-3 w-3" />}
|
||||
disabled={dirty === 0 || !!busy}
|
||||
title={dirty === 0 ? t("gitBar.commitNothing") : t("gitBar.commitHint")}
|
||||
onClick={() => {
|
||||
setCommitting((v) => !v);
|
||||
setError(null);
|
||||
}}
|
||||
/>
|
||||
<Btn
|
||||
label={t("gitBar.switch")}
|
||||
icon={<GitBranch className="h-3 w-3" />}
|
||||
@@ -265,47 +233,6 @@ export function LaneGitBar({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{committing && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<input
|
||||
autoFocus
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && message.trim() && !busy) void run("commit", { message });
|
||||
if (e.key === "Escape") setCommitting(false);
|
||||
}}
|
||||
placeholder={t("gitBar.commitPlaceholder")}
|
||||
aria-label={t("gitBar.commitPlaceholder")}
|
||||
className="min-w-0 flex-1 rounded border border-border-light bg-surface-0 px-2 py-0.5 text-[11px] text-fg-primary placeholder:text-fg-muted focus:border-accent focus:outline-none"
|
||||
/>
|
||||
<Btn
|
||||
label={t("gitBar.commitAll", { count: dirty })}
|
||||
busy={busy === "commit"}
|
||||
disabled={!message.trim() || !!busy}
|
||||
onClick={() => void run("commit", { message })}
|
||||
/>
|
||||
{/* The Changes panel's per-file staging only means something if a
|
||||
commit can take the index as it stands. The server answers
|
||||
ENOCHANGES when nothing is staged, and that text lands below. */}
|
||||
<Btn
|
||||
label={t("gitBar.commitStaged")}
|
||||
title={t("gitBar.commitStagedHint")}
|
||||
busy={busy === "commit"}
|
||||
disabled={!message.trim() || !!busy}
|
||||
onClick={() => void run("commit", { message, only: "staged" })}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("gitBar.cancel")}
|
||||
onClick={() => setCommitting(false)}
|
||||
className="text-fg-muted hover:text-fg-primary"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{switching && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<select
|
||||
|
||||
Reference in New Issue
Block a user