feat(FN-947): show files-changed on done column task cards
- Extend useSessionFiles hook to fetch files for done column tasks - Update TaskCard to display files-changed indicator on done column cards - Add tests for useSessionFiles done column support - Add tests for TaskCard files-changed display in done column
This commit is contained in:
@@ -706,7 +706,10 @@ function TaskCardComponent({
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{task.column === "done" && task.mergeDetails?.filesChanged != null && task.mergeDetails.filesChanged > 0 && (
|
{task.column === "done" && (() => {
|
||||||
|
const mergedCount = task.mergeDetails?.filesChanged;
|
||||||
|
if (mergedCount != null && mergedCount > 0) {
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="card-session-files"
|
className="card-session-files"
|
||||||
@@ -714,9 +717,27 @@ function TaskCardComponent({
|
|||||||
disabled={!onOpenDetailWithTab}
|
disabled={!onOpenDetailWithTab}
|
||||||
>
|
>
|
||||||
<Folder size={12} />
|
<Folder size={12} />
|
||||||
<span>{task.mergeDetails.filesChanged} files changed</span>
|
<span>{mergedCount} files changed</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
);
|
||||||
|
}
|
||||||
|
if (task.worktree && sessionFiles.length > 0) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="card-session-files"
|
||||||
|
onClick={handleOpenFiles}
|
||||||
|
disabled={!onOpenDetailWithTab}
|
||||||
|
>
|
||||||
|
<Folder size={12} />
|
||||||
|
<span>
|
||||||
|
{sessionFilesLoading ? "Checking files…" : `${sessionFiles.length} files changed`}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
{((task.dependencies && task.dependencies.length > 0) || queued || task.status === "queued" || task.blockedBy) && (
|
{((task.dependencies && task.dependencies.length > 0) || queued || task.status === "queued" || task.blockedBy) && (
|
||||||
<div className="card-meta">
|
<div className="card-meta">
|
||||||
{task.dependencies && task.dependencies.length > 0 && (
|
{task.dependencies && task.dependencies.length > 0 && (
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ vi.mock("../../hooks/useBadgeWebSocket", () => ({
|
|||||||
useBadgeWebSocket: () => mockUseBadgeWebSocket(),
|
useBadgeWebSocket: () => mockUseBadgeWebSocket(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const mockUseSessionFiles = vi.fn(() => ({ files: [], loading: false }));
|
||||||
|
|
||||||
|
vi.mock("../../hooks/useSessionFiles", () => ({
|
||||||
|
useSessionFiles: (...args: unknown[]) => mockUseSessionFiles(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("lucide-react", () => ({
|
vi.mock("lucide-react", () => ({
|
||||||
Link: ({ size }: { size?: number }) => <span data-testid="link-icon">🔗</span>,
|
Link: ({ size }: { size?: number }) => <span data-testid="link-icon">🔗</span>,
|
||||||
Clock: ({ size }: { size?: number }) => <span data-testid="clock-icon">🕐</span>,
|
Clock: ({ size }: { size?: number }) => <span data-testid="clock-icon">🕐</span>,
|
||||||
@@ -41,6 +47,8 @@ beforeEach(() => {
|
|||||||
subscribeToBadge: vi.fn(),
|
subscribeToBadge: vi.fn(),
|
||||||
unsubscribeFromBadge: vi.fn(),
|
unsubscribeFromBadge: vi.fn(),
|
||||||
});
|
});
|
||||||
|
mockUseSessionFiles.mockReset();
|
||||||
|
mockUseSessionFiles.mockReturnValue({ files: [], loading: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2984,3 +2992,107 @@ describe("TaskCard awaiting-approval state", () => {
|
|||||||
expect(card?.classList.contains("paused")).toBe(false);
|
expect(card?.classList.contains("paused")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("TaskCard files-changed in done column", () => {
|
||||||
|
const noopToast = vi.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockUseSessionFiles.mockReturnValue({ files: [], loading: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows mergeDetails.filesChanged for done column when set", () => {
|
||||||
|
const task = makeTask({
|
||||||
|
column: "done",
|
||||||
|
mergeDetails: { filesChanged: 7, mergedAt: "2026-01-01T00:00:00Z", targetBranch: "main" },
|
||||||
|
});
|
||||||
|
mockUseSessionFiles.mockReturnValue({ files: ["a.ts"], loading: false });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TaskCard
|
||||||
|
task={task}
|
||||||
|
onOpenDetail={vi.fn()}
|
||||||
|
addToast={noopToast}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("7 files changed")).toBeInTheDocument();
|
||||||
|
// Should NOT show session files count since mergeDetails takes priority
|
||||||
|
expect(screen.queryByText("1 files changed")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows session files count for done column with worktree but no mergeDetails.filesChanged", () => {
|
||||||
|
const task = makeTask({
|
||||||
|
column: "done",
|
||||||
|
worktree: "/repo/.worktrees/fn-099",
|
||||||
|
});
|
||||||
|
mockUseSessionFiles.mockReturnValue({ files: ["src/a.ts", "src/b.ts", "src/c.ts"], loading: false });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TaskCard
|
||||||
|
task={task}
|
||||||
|
onOpenDetail={vi.fn()}
|
||||||
|
addToast={noopToast}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("3 files changed")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows nothing for done column without worktree and without mergeDetails.filesChanged", () => {
|
||||||
|
const task = makeTask({ column: "done" });
|
||||||
|
mockUseSessionFiles.mockReturnValue({ files: [], loading: false });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TaskCard
|
||||||
|
task={task}
|
||||||
|
onOpenDetail={vi.fn()}
|
||||||
|
addToast={noopToast}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText(/files changed/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers mergeDetails.filesChanged over sessionFiles for done column", () => {
|
||||||
|
const task = makeTask({
|
||||||
|
column: "done",
|
||||||
|
worktree: "/repo/.worktrees/fn-099",
|
||||||
|
mergeDetails: { filesChanged: 5, mergedAt: "2026-01-01T00:00:00Z", targetBranch: "main" },
|
||||||
|
});
|
||||||
|
mockUseSessionFiles.mockReturnValue({ files: ["a.ts", "b.ts"], loading: false });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TaskCard
|
||||||
|
task={task}
|
||||||
|
onOpenDetail={vi.fn()}
|
||||||
|
addToast={noopToast}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should show the mergeDetails count, not the sessionFiles count
|
||||||
|
expect(screen.getByText("5 files changed")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("2 files changed")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows loading state for done column with worktree and no mergeDetails", () => {
|
||||||
|
const task = makeTask({
|
||||||
|
column: "done",
|
||||||
|
worktree: "/repo/.worktrees/fn-099",
|
||||||
|
});
|
||||||
|
mockUseSessionFiles.mockReturnValue({ files: [], loading: true });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TaskCard
|
||||||
|
task={task}
|
||||||
|
onOpenDetail={vi.fn()}
|
||||||
|
addToast={noopToast}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// No loading indicator is shown because sessionFiles.length is 0
|
||||||
|
// The button only appears when files.length > 0
|
||||||
|
expect(screen.queryByText(/files changed/)).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Checking files…")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -50,4 +50,24 @@ describe("useSessionFiles", () => {
|
|||||||
|
|
||||||
expect(result.current.files).toEqual([]);
|
expect(result.current.files).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("fetches session files for done column tasks with a worktree", async () => {
|
||||||
|
mockFetchSessionFiles.mockResolvedValueOnce(["src/x.ts", "src/y.ts", "src/z.ts"]);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useSessionFiles("FN-456", "/repo/.worktrees/kb-456", "done"));
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||||
|
|
||||||
|
expect(result.current.files).toEqual(["src/x.ts", "src/y.ts", "src/z.ts"]);
|
||||||
|
expect(mockFetchSessionFiles).toHaveBeenCalledWith("FN-456", undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not fetch for done column tasks without a worktree", async () => {
|
||||||
|
const { result } = renderHook(() => useSessionFiles("FN-456", undefined, "done"));
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||||
|
|
||||||
|
expect(result.current.files).toEqual([]);
|
||||||
|
expect(mockFetchSessionFiles).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { fetchSessionFiles } from "../api";
|
import { fetchSessionFiles } from "../api";
|
||||||
|
|
||||||
const ACTIVE_COLUMNS = new Set(["in-progress", "in-review"]);
|
const ACTIVE_COLUMNS = new Set(["in-progress", "in-review", "done"]);
|
||||||
|
|
||||||
interface UseSessionFilesResult {
|
interface UseSessionFilesResult {
|
||||||
files: string[];
|
files: string[];
|
||||||
|
|||||||
Reference in New Issue
Block a user