feat(FN-858): add click-to-diff in Remotes tab commit lists
- Add clickable commit rows in GitManagerModal Remotes tab that open inline diff view - Add diff viewer with syntax highlighting and file-level stats - Add CSS styles for diff view, commit detail, and click-to-diff interactions - Add comprehensive tests for click-to-diff behavior in Remotes tab - Clean up unused types and store methods (diffFile, getCommitDiff, DiffOptions) - Update dashboard README with inline commit diff feature documentation
This commit is contained in:
@@ -203,6 +203,7 @@ The Git Manager provides comprehensive repository visualization and management d
|
|||||||
- View operation results and error states
|
- View operation results and error states
|
||||||
- **Commits to Push**: See which local commits are ahead of the upstream tracking branch (pending push) with short hash, message, author, and relative date. The list stays synchronized with the ahead count — it refreshes automatically after fetch, pull, push, and manual refresh operations, and clears when the ahead count drops to zero (e.g., after a successful push).
|
- **Commits to Push**: See which local commits are ahead of the upstream tracking branch (pending push) with short hash, message, author, and relative date. The list stays synchronized with the ahead count — it refreshes automatically after fetch, pull, push, and manual refresh operations, and clears when the ahead count drops to zero (e.g., after a successful push).
|
||||||
- **Remote Commit Inspection**: Click any remote to view its recent commit history — useful for checking what's on a remote without switching to the terminal
|
- **Remote Commit Inspection**: Click any remote to view its recent commit history — useful for checking what's on a remote without switching to the terminal
|
||||||
|
- **Inline Commit Diffs**: Click any commit in the "Commits to Push" or "Recent commits on {remote}" lists to expand an inline diff viewer showing file changes (stat + patch). Click the same commit again to collapse. Only one diff is expanded per list at a time.
|
||||||
- Auto-selects the first remote and loads its recent commits on mount
|
- Auto-selects the first remote and loads its recent commits on mount
|
||||||
|
|
||||||
### File Browser
|
### File Browser
|
||||||
|
|||||||
@@ -1510,6 +1510,14 @@ function RemotesPanel({
|
|||||||
const [loadingRemoteCommits, setLoadingRemoteCommits] = useState(false);
|
const [loadingRemoteCommits, setLoadingRemoteCommits] = useState(false);
|
||||||
const [remoteCommitsError, setRemoteCommitsError] = useState<string | null>(null);
|
const [remoteCommitsError, setRemoteCommitsError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Inline commit diff expansion (one per list context)
|
||||||
|
const [expandedAheadCommit, setExpandedAheadCommit] = useState<string | null>(null);
|
||||||
|
const [aheadCommitDiff, setAheadCommitDiff] = useState<{ stat: string; patch: string } | null>(null);
|
||||||
|
const [loadingAheadCommitDiff, setLoadingAheadCommitDiff] = useState(false);
|
||||||
|
const [expandedRemoteCommit, setExpandedRemoteCommit] = useState<string | null>(null);
|
||||||
|
const [remoteCommitDiff, setRemoteCommitDiff] = useState<{ stat: string; patch: string } | null>(null);
|
||||||
|
const [loadingRemoteCommitDiff, setLoadingRemoteCommitDiff] = useState(false);
|
||||||
|
|
||||||
// Fetch remotes when panel mounts
|
// Fetch remotes when panel mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadRemotes();
|
loadRemotes();
|
||||||
@@ -1658,6 +1666,47 @@ function RemotesPanel({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCompactCommitClick = useCallback(async (
|
||||||
|
hash: string,
|
||||||
|
listType: "ahead" | "remote",
|
||||||
|
) => {
|
||||||
|
if (listType === "ahead") {
|
||||||
|
if (expandedAheadCommit === hash) {
|
||||||
|
setExpandedAheadCommit(null);
|
||||||
|
setAheadCommitDiff(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setExpandedAheadCommit(hash);
|
||||||
|
setAheadCommitDiff(null);
|
||||||
|
setLoadingAheadCommitDiff(true);
|
||||||
|
try {
|
||||||
|
const diff = await fetchCommitDiff(hash);
|
||||||
|
setAheadCommitDiff(diff);
|
||||||
|
} catch {
|
||||||
|
setAheadCommitDiff(null);
|
||||||
|
} finally {
|
||||||
|
setLoadingAheadCommitDiff(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (expandedRemoteCommit === hash) {
|
||||||
|
setExpandedRemoteCommit(null);
|
||||||
|
setRemoteCommitDiff(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setExpandedRemoteCommit(hash);
|
||||||
|
setRemoteCommitDiff(null);
|
||||||
|
setLoadingRemoteCommitDiff(true);
|
||||||
|
try {
|
||||||
|
const diff = await fetchCommitDiff(hash);
|
||||||
|
setRemoteCommitDiff(diff);
|
||||||
|
} catch {
|
||||||
|
setRemoteCommitDiff(null);
|
||||||
|
} finally {
|
||||||
|
setLoadingRemoteCommitDiff(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [expandedAheadCommit, expandedRemoteCommit]);
|
||||||
|
|
||||||
const startEditingUrl = (remote: GitRemoteDetailed) => {
|
const startEditingUrl = (remote: GitRemoteDetailed) => {
|
||||||
setEditingRemote(`url-${remote.name}`);
|
setEditingRemote(`url-${remote.name}`);
|
||||||
setEditUrlValue(remote.pushUrl || remote.fetchUrl);
|
setEditUrlValue(remote.pushUrl || remote.fetchUrl);
|
||||||
@@ -1737,20 +1786,48 @@ function RemotesPanel({
|
|||||||
) : aheadCommits.length > 0 ? (
|
) : aheadCommits.length > 0 ? (
|
||||||
<div className="gm-ahead-commits-list" data-testid="ahead-commits-list">
|
<div className="gm-ahead-commits-list" data-testid="ahead-commits-list">
|
||||||
{aheadCommits.map((commit) => (
|
{aheadCommits.map((commit) => (
|
||||||
<div key={commit.hash} className="gm-commit-item-compact">
|
<div key={commit.hash} className="gm-commit-item-compact-wrapper">
|
||||||
<div className="gm-commit-compact-hash">
|
<div
|
||||||
<code className="gm-hash">{commit.shortHash}</code>
|
className="gm-commit-item-compact gm-commit-clickable"
|
||||||
</div>
|
onClick={() => handleCompactCommitClick(commit.hash, "ahead")}
|
||||||
<div className="gm-commit-compact-info">
|
role="button"
|
||||||
<span className="gm-commit-message" title={commit.message}>
|
tabIndex={0}
|
||||||
{commit.message}
|
title="Click to view diff"
|
||||||
</span>
|
>
|
||||||
<span className="gm-commit-meta">
|
<div className="gm-commit-compact-hash">
|
||||||
<span>{commit.author}</span>
|
<code className="gm-hash">{commit.shortHash}</code>
|
||||||
<span>•</span>
|
</div>
|
||||||
<span>{relativeDate(commit.date)}</span>
|
<div className="gm-commit-compact-info">
|
||||||
|
<span className="gm-commit-message" title={commit.message}>
|
||||||
|
{commit.message}
|
||||||
|
</span>
|
||||||
|
<span className="gm-commit-meta">
|
||||||
|
<span>{commit.author}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{relativeDate(commit.date)}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="gm-commit-expand-icon">
|
||||||
|
{expandedAheadCommit === commit.hash ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{expandedAheadCommit === commit.hash && (
|
||||||
|
<div className="gm-commit-diff gm-commit-diff-compact">
|
||||||
|
{loadingAheadCommitDiff ? (
|
||||||
|
<div className="gm-diff-loading">
|
||||||
|
<Loader2 size={16} className="spin" />
|
||||||
|
Loading diff...
|
||||||
|
</div>
|
||||||
|
) : aheadCommitDiff ? (
|
||||||
|
<>
|
||||||
|
{aheadCommitDiff.stat && <pre className="gm-diff-stat">{aheadCommitDiff.stat}</pre>}
|
||||||
|
<pre className="gm-diff-patch">{aheadCommitDiff.patch}</pre>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="gm-diff-error">Failed to load diff</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1984,20 +2061,48 @@ function RemotesPanel({
|
|||||||
) : (
|
) : (
|
||||||
<div className="gm-remote-commits-list" data-testid="remote-commits-list">
|
<div className="gm-remote-commits-list" data-testid="remote-commits-list">
|
||||||
{remoteCommits.map((commit) => (
|
{remoteCommits.map((commit) => (
|
||||||
<div key={commit.hash} className="gm-commit-item-compact">
|
<div key={commit.hash} className="gm-commit-item-compact-wrapper">
|
||||||
<div className="gm-commit-compact-hash">
|
<div
|
||||||
<code className="gm-hash">{commit.shortHash}</code>
|
className="gm-commit-item-compact gm-commit-clickable"
|
||||||
</div>
|
onClick={() => handleCompactCommitClick(commit.hash, "remote")}
|
||||||
<div className="gm-commit-compact-info">
|
role="button"
|
||||||
<span className="gm-commit-message" title={commit.message}>
|
tabIndex={0}
|
||||||
{commit.message}
|
title="Click to view diff"
|
||||||
</span>
|
>
|
||||||
<span className="gm-commit-meta">
|
<div className="gm-commit-compact-hash">
|
||||||
<span>{commit.author}</span>
|
<code className="gm-hash">{commit.shortHash}</code>
|
||||||
<span>•</span>
|
</div>
|
||||||
<span>{relativeDate(commit.date)}</span>
|
<div className="gm-commit-compact-info">
|
||||||
|
<span className="gm-commit-message" title={commit.message}>
|
||||||
|
{commit.message}
|
||||||
|
</span>
|
||||||
|
<span className="gm-commit-meta">
|
||||||
|
<span>{commit.author}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{relativeDate(commit.date)}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="gm-commit-expand-icon">
|
||||||
|
{expandedRemoteCommit === commit.hash ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{expandedRemoteCommit === commit.hash && (
|
||||||
|
<div className="gm-commit-diff gm-commit-diff-compact">
|
||||||
|
{loadingRemoteCommitDiff ? (
|
||||||
|
<div className="gm-diff-loading">
|
||||||
|
<Loader2 size={16} className="spin" />
|
||||||
|
Loading diff...
|
||||||
|
</div>
|
||||||
|
) : remoteCommitDiff ? (
|
||||||
|
<>
|
||||||
|
{remoteCommitDiff.stat && <pre className="gm-diff-stat">{remoteCommitDiff.stat}</pre>}
|
||||||
|
<pre className="gm-diff-patch">{remoteCommitDiff.patch}</pre>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="gm-diff-error">Failed to load diff</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1376,4 +1376,266 @@ describe("GitManagerModal", () => {
|
|||||||
expect(fetchGitStatus).toHaveBeenCalledTimes(2);
|
expect(fetchGitStatus).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Remotes Tab: Click-to-Diff for Commits to Push ────────────
|
||||||
|
|
||||||
|
it("expands diff when clicking a commit in Commits to Push", async () => {
|
||||||
|
(fetchGitStatus as any).mockResolvedValue({
|
||||||
|
branch: "main",
|
||||||
|
commit: "abc1234",
|
||||||
|
isDirty: false,
|
||||||
|
ahead: 2,
|
||||||
|
behind: 0,
|
||||||
|
});
|
||||||
|
(fetchAheadCommits as any).mockResolvedValue([
|
||||||
|
{ hash: "aaa1111", shortHash: "aaa1", message: "First ahead commit", author: "Dev", date: "2026-01-01T00:00:00Z", parents: [] },
|
||||||
|
{ hash: "bbb2222", shortHash: "bbb2", message: "Second ahead commit", author: "Dev", date: "2026-01-02T00:00:00Z", parents: [] },
|
||||||
|
]);
|
||||||
|
(fetchCommitDiff as any).mockResolvedValue({
|
||||||
|
stat: " file.ts | 3 ++-",
|
||||||
|
patch: "diff --git a/file.ts b/file.ts\n-old\n+new",
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(
|
||||||
|
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: /remotes/i }));
|
||||||
|
|
||||||
|
// Wait for ahead commits to appear
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("commits-to-push")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("First ahead commit")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on the commit to expand diff
|
||||||
|
await user.click(screen.getByText("First ahead commit"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fetchCommitDiff).toHaveBeenCalledWith("aaa1111");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Diff content should be rendered
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/file\.ts \| 3/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/-old/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/\+new/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("collapses diff when clicking same ahead commit again", async () => {
|
||||||
|
(fetchGitStatus as any).mockResolvedValue({
|
||||||
|
branch: "main",
|
||||||
|
commit: "abc1234",
|
||||||
|
isDirty: false,
|
||||||
|
ahead: 1,
|
||||||
|
behind: 0,
|
||||||
|
});
|
||||||
|
(fetchAheadCommits as any).mockResolvedValue([
|
||||||
|
{ hash: "aaa1111", shortHash: "aaa1", message: "Toggle commit", author: "Dev", date: "2026-01-01T00:00:00Z", parents: [] },
|
||||||
|
]);
|
||||||
|
(fetchCommitDiff as any).mockResolvedValue({
|
||||||
|
stat: " file.ts | 1 +",
|
||||||
|
patch: "diff --git a/file.ts\n+line",
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(
|
||||||
|
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: /remotes/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Toggle commit")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click to expand
|
||||||
|
await user.click(screen.getByText("Toggle commit"));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fetchCommitDiff).toHaveBeenCalledWith("aaa1111");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Diff should be visible
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/file\.ts \| 1/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click again to collapse
|
||||||
|
await user.click(screen.getByText("Toggle commit"));
|
||||||
|
|
||||||
|
// Diff content should be gone
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText(/file\.ts \| 1/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows error state when diff fetch fails for ahead commit", async () => {
|
||||||
|
(fetchGitStatus as any).mockResolvedValue({
|
||||||
|
branch: "main",
|
||||||
|
commit: "abc1234",
|
||||||
|
isDirty: false,
|
||||||
|
ahead: 1,
|
||||||
|
behind: 0,
|
||||||
|
});
|
||||||
|
(fetchAheadCommits as any).mockResolvedValue([
|
||||||
|
{ hash: "aaa1111", shortHash: "aaa1", message: "Error commit", author: "Dev", date: "2026-01-01T00:00:00Z", parents: [] },
|
||||||
|
]);
|
||||||
|
(fetchCommitDiff as any).mockRejectedValue(new Error("Diff load failed"));
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(
|
||||||
|
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: /remotes/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Error commit")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click to expand
|
||||||
|
await user.click(screen.getByText("Error commit"));
|
||||||
|
|
||||||
|
// Should show error fallback
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Failed to load diff")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Remotes Tab: Click-to-Diff for Remote Commits ─────────────
|
||||||
|
|
||||||
|
it("expands diff when clicking a commit in remote commits section", async () => {
|
||||||
|
(fetchRemoteCommits as any).mockResolvedValue([
|
||||||
|
{ hash: "rc1hash1", shortHash: "rc1", message: "Remote commit 1", author: "Dev", date: "2026-01-01T00:00:00Z", parents: [] },
|
||||||
|
{ hash: "rc2hash2", shortHash: "rc2", message: "Remote commit 2", author: "Dev", date: "2026-01-02T00:00:00Z", parents: [] },
|
||||||
|
]);
|
||||||
|
(fetchCommitDiff as any).mockResolvedValue({
|
||||||
|
stat: " src/app.ts | 5 ++---",
|
||||||
|
patch: "diff --git a/src/app.ts\n-old line\n+new line",
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(
|
||||||
|
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: /remotes/i }));
|
||||||
|
|
||||||
|
// Wait for remote commits to appear
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("remote-commits-section")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Remote commit 1")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on the remote commit to expand diff
|
||||||
|
await user.click(screen.getByText("Remote commit 1"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fetchCommitDiff).toHaveBeenCalledWith("rc1hash1");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Diff content should be rendered
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/src\/app\.ts \| 5/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("collapses diff when clicking same remote commit again", async () => {
|
||||||
|
(fetchRemoteCommits as any).mockResolvedValue([
|
||||||
|
{ hash: "rc1hash1", shortHash: "rc1", message: "Remote toggle commit", author: "Dev", date: "2026-01-01T00:00:00Z", parents: [] },
|
||||||
|
]);
|
||||||
|
(fetchCommitDiff as any).mockResolvedValue({
|
||||||
|
stat: " file.ts | 2 +",
|
||||||
|
patch: "diff --git a/file.ts\n+line",
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(
|
||||||
|
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: /remotes/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Remote toggle commit")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click to expand
|
||||||
|
await user.click(screen.getByText("Remote toggle commit"));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fetchCommitDiff).toHaveBeenCalledWith("rc1hash1");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Diff should be visible
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/file\.ts \| 2/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click again to collapse
|
||||||
|
await user.click(screen.getByText("Remote toggle commit"));
|
||||||
|
|
||||||
|
// Diff content should be gone
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText(/file\.ts \| 2/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows error state when diff fetch fails for remote commit", async () => {
|
||||||
|
(fetchRemoteCommits as any).mockResolvedValue([
|
||||||
|
{ hash: "rc1hash1", shortHash: "rc1", message: "Error remote commit", author: "Dev", date: "2026-01-01T00:00:00Z", parents: [] },
|
||||||
|
]);
|
||||||
|
(fetchCommitDiff as any).mockRejectedValue(new Error("Diff load failed"));
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(
|
||||||
|
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: /remotes/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Error remote commit")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click to expand
|
||||||
|
await user.click(screen.getByText("Error remote commit"));
|
||||||
|
|
||||||
|
// Should show error fallback
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Failed to load diff")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only expands one diff at a time in remote commits list", async () => {
|
||||||
|
(fetchRemoteCommits as any).mockResolvedValue([
|
||||||
|
{ hash: "rc1hash1", shortHash: "rc1", message: "First remote", author: "Dev", date: "2026-01-01T00:00:00Z", parents: [] },
|
||||||
|
{ hash: "rc2hash2", shortHash: "rc2", message: "Second remote", author: "Dev", date: "2026-01-02T00:00:00Z", parents: [] },
|
||||||
|
]);
|
||||||
|
(fetchCommitDiff as any).mockResolvedValue({
|
||||||
|
stat: " file.ts | 1 +",
|
||||||
|
patch: "diff --git a/file.ts\n+line",
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(
|
||||||
|
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: /remotes/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("First remote")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Second remote")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click first commit
|
||||||
|
await user.click(screen.getByText("First remote"));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fetchCommitDiff).toHaveBeenCalledWith("rc1hash1");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click second commit — should collapse first, expand second
|
||||||
|
await user.click(screen.getByText("Second remote"));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fetchCommitDiff).toHaveBeenCalledWith("rc2hash2");
|
||||||
|
});
|
||||||
|
|
||||||
|
// fetchCommitDiff should have been called for both commits
|
||||||
|
expect(fetchCommitDiff).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14164,6 +14164,41 @@ html .column.drag-over * {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Remotes Tab: Clickable Commit Diff ── */
|
||||||
|
|
||||||
|
.gm-commit-item-compact-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-commit-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-commit-clickable:hover {
|
||||||
|
background: var(--card-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-commit-expand-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-commit-diff-compact {
|
||||||
|
margin: var(--space-xs) var(--space-sm) var(--space-sm) var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-ahead-commits-list {
|
||||||
|
max-height: 280px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Responsive ── */
|
/* ── Responsive ── */
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
|
|||||||
Reference in New Issue
Block a user