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:
@@ -45,6 +45,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { api } from "../../lib/api";
|
||||
import { LaneDiffView, LaneFileView, type DiffLayout } from "./LaneDiffView";
|
||||
import { LaneGitHeader, useLaneGitFacts } from "./LaneGitHeader";
|
||||
import type {
|
||||
LaneChangeFile,
|
||||
LaneChangeRange,
|
||||
@@ -64,7 +65,7 @@ const TREE_CONCURRENCY = 4;
|
||||
|
||||
type Mode = "overview" | "review" | "file" | "history" | "browse";
|
||||
|
||||
/** Colour per porcelain status letter, matching LaneCard's file panel. */
|
||||
/** Colour per porcelain status letter, matching LaneGitHeader's file panel. */
|
||||
function statusClass(status: string, untracked: boolean) {
|
||||
if (untracked) return "text-status-success";
|
||||
const code = status.trim()[0];
|
||||
@@ -281,8 +282,21 @@ function ChangeRow({
|
||||
);
|
||||
}
|
||||
|
||||
export function LaneChangesPanel({ laneId }: { laneId: number }) {
|
||||
export function LaneChangesPanel({
|
||||
laneId,
|
||||
onNewLane,
|
||||
}: {
|
||||
laneId: number;
|
||||
/** Open the Add-lane flow for a sibling worktree; see `LaneGitBar`. */
|
||||
onNewLane?: (branch?: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation(["lanes"]);
|
||||
// One read of the working copy, shared by the header and every write below:
|
||||
// a stage, a discard or a commit changes the counts the header shows.
|
||||
const [git, refreshGit] = useLaneGitFacts(laneId);
|
||||
// Bumped by Refresh and by a git bar action: a pull or a switch moves
|
||||
// history and the tree too, not just the diff `load` re-reads.
|
||||
const [reread, setReread] = useState(0);
|
||||
const [range, setRange] = useState<LaneChangeRange>("work");
|
||||
const [mode, setMode] = useState<Mode>("overview");
|
||||
const [openFile, setOpenFile] = useState<string | null>(null);
|
||||
@@ -384,7 +398,7 @@ export function LaneChangesPanel({ laneId }: { laneId: number }) {
|
||||
.gitLog(laneId, { limit: 50 })
|
||||
.then(forLane(laneId, (r) => setCommits(r.commits)))
|
||||
.catch((err) => setError(err instanceof Error ? err.message : String(err)));
|
||||
}, [mode, laneId]);
|
||||
}, [mode, laneId, reread]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== "browse" || query.trim() || "" in treeChildren) return;
|
||||
@@ -394,7 +408,7 @@ export function LaneChangesPanel({ laneId }: { laneId: number }) {
|
||||
.catch((err) => setError(err instanceof Error ? err.message : String(err)));
|
||||
// `treeChildren` is the cache this fills — reading it as a dep would loop.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mode, laneId, query]);
|
||||
}, [mode, laneId, query, reread]);
|
||||
|
||||
// A folder's children are fetched lazily, the first time it is expanded.
|
||||
const toggleDir = (path: string) => {
|
||||
@@ -555,6 +569,7 @@ export function LaneChangesPanel({ laneId }: { laneId: number }) {
|
||||
await api.lanes.gitAction(lane, action, { paths: [path], ...(confirm ? { confirm } : {}) });
|
||||
if (laneNow.current !== lane) return;
|
||||
setConfirmDiscard(null);
|
||||
refreshGit();
|
||||
await load();
|
||||
} catch (err) {
|
||||
if (laneNow.current === lane) setError(err instanceof Error ? err.message : String(err));
|
||||
@@ -574,6 +589,7 @@ export function LaneChangesPanel({ laneId }: { laneId: number }) {
|
||||
await api.lanes.gitAction(lane, "commit", { message: message.trim(), only: "staged" });
|
||||
if (laneNow.current !== lane) return;
|
||||
setMessage("");
|
||||
refreshGit();
|
||||
await load();
|
||||
} catch (err) {
|
||||
if (laneNow.current === lane) setError(err instanceof Error ? err.message : String(err));
|
||||
@@ -582,6 +598,15 @@ export function LaneChangesPanel({ laneId }: { laneId: number }) {
|
||||
}
|
||||
};
|
||||
|
||||
const rereadAll = () => {
|
||||
refreshGit();
|
||||
// The tree cache is dropped whole: after a switch any folder may differ.
|
||||
setTreeChildren({});
|
||||
setExpandedDirs(new Set());
|
||||
setReread((n) => n + 1);
|
||||
void load();
|
||||
};
|
||||
|
||||
const toggleCollapsed = (path: string) =>
|
||||
setCollapsed((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -604,6 +629,7 @@ export function LaneChangesPanel({ laneId }: { laneId: number }) {
|
||||
|
||||
return (
|
||||
<div data-testid="lane-changes" className="flex min-h-0 flex-1 flex-col">
|
||||
<LaneGitHeader laneId={laneId} git={git} onChanged={rereadAll} onNewLane={onNewLane} />
|
||||
{/* Two rows on purpose: WHERE you are (mode, and the refresh that always
|
||||
applies) on top, HOW you are looking at it (range, layout, context,
|
||||
whole file) underneath. One row wrapped mid-group as soon as the
|
||||
@@ -648,7 +674,7 @@ export function LaneChangesPanel({ laneId }: { laneId: number }) {
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void load()}
|
||||
onClick={rereadAll}
|
||||
aria-label={t("changes.refresh")}
|
||||
title={t("changes.refresh")}
|
||||
className="ml-auto shrink-0 text-fg-muted hover:text-fg-primary"
|
||||
|
||||
Reference in New Issue
Block a user