feat(FN-4521): complete Step 3 — unify active diff data source
Fusion-Task-Id: FN-4521 Fusion-Task-Lineage: 675472a2-480d-4b9e-8861-98d2d82680db
This commit is contained in:
@@ -1301,13 +1301,10 @@ function TaskCardComponent({
|
|||||||
if (task.column === "in-progress") {
|
if (task.column === "in-progress") {
|
||||||
const activeDiffCount = diffStats?.filesChanged;
|
const activeDiffCount = diffStats?.filesChanged;
|
||||||
const fallbackCount =
|
const fallbackCount =
|
||||||
activeDiffCount == null || activeDiffCount === 0
|
activeDiffCount == null
|
||||||
? task.modifiedFiles?.length
|
? task.modifiedFiles?.length
|
||||||
: undefined;
|
: undefined;
|
||||||
const displayCount =
|
const displayCount = activeDiffCount ?? fallbackCount;
|
||||||
activeDiffCount != null && activeDiffCount > 0
|
|
||||||
? activeDiffCount
|
|
||||||
: fallbackCount;
|
|
||||||
if (displayCount == null || displayCount === 0) {
|
if (displayCount == null || displayCount === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1328,13 +1325,10 @@ function TaskCardComponent({
|
|||||||
if (task.column === "in-review") {
|
if (task.column === "in-review") {
|
||||||
const reviewDiffCount = diffStats?.filesChanged;
|
const reviewDiffCount = diffStats?.filesChanged;
|
||||||
const fallbackCount =
|
const fallbackCount =
|
||||||
reviewDiffCount == null || reviewDiffCount === 0
|
reviewDiffCount == null
|
||||||
? task.modifiedFiles?.length
|
? task.modifiedFiles?.length
|
||||||
: undefined;
|
: undefined;
|
||||||
const displayCount =
|
const displayCount = reviewDiffCount ?? fallbackCount;
|
||||||
reviewDiffCount != null && reviewDiffCount > 0
|
|
||||||
? reviewDiffCount
|
|
||||||
: fallbackCount;
|
|
||||||
if (displayCount == null || displayCount === 0) {
|
if (displayCount == null || displayCount === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,8 +231,8 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-done task without a worktree → show worktree empty state
|
// Non-done task without a worktree → only show fallback state when branch-fallback diff is empty.
|
||||||
if (!isDone && !worktree) {
|
if (!isDone && !worktree && files.length === 0) {
|
||||||
if (modifiedFiles && modifiedFiles.length > 0) {
|
if (modifiedFiles && modifiedFiles.length > 0) {
|
||||||
return renderModifiedFilesFallback(modifiedFiles, false);
|
return renderModifiedFilesFallback(modifiedFiles, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,28 @@ describe("TaskChangesTab — worktree-backed (non-done tasks)", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders fetched file rows for active task without worktree when branch fallback has files", async () => {
|
||||||
|
mockFetchTaskDiff.mockResolvedValue({
|
||||||
|
files: [
|
||||||
|
{ path: "a.ts", status: "modified", additions: 1, deletions: 0, patch: "@@ -1 +1,2 @@" },
|
||||||
|
],
|
||||||
|
stats: { filesChanged: 1, additions: 1, deletions: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TaskChangesTab
|
||||||
|
taskId="FN-001"
|
||||||
|
worktree={undefined}
|
||||||
|
column="in-progress"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("a.ts")).toBeTruthy();
|
||||||
|
});
|
||||||
|
expect(screen.queryByText("No worktree available for this task.")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("shows modifiedFiles fallback when an active task has no worktree diff", async () => {
|
it("shows modifiedFiles fallback when an active task has no worktree diff", async () => {
|
||||||
mockFetchTaskDiff.mockResolvedValue({ files: [], stats: { filesChanged: 0, additions: 0, deletions: 0 } });
|
mockFetchTaskDiff.mockResolvedValue({ files: [], stats: { filesChanged: 0, additions: 0, deletions: 0 } });
|
||||||
|
|
||||||
|
|||||||
@@ -53,25 +53,25 @@ describe("useTaskDiffStats", () => {
|
|||||||
expect(mockFetchTaskDiff).toHaveBeenCalledWith("FN-123", undefined, "proj-1");
|
expect(mockFetchTaskDiff).toHaveBeenCalledWith("FN-123", undefined, "proj-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not fetch for active columns without a worktree", async () => {
|
it("fetches for active columns without a worktree", async () => {
|
||||||
|
mockFetchTaskDiff.mockResolvedValue({
|
||||||
|
files: [],
|
||||||
|
stats: { filesChanged: 2, additions: 4, deletions: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
const { result: inProgress } = renderHook(() =>
|
const { result: inProgress } = renderHook(() =>
|
||||||
useTaskDiffStats("FN-123", "in-progress", "abc1234", undefined),
|
useTaskDiffStats("FN-123", "in-progress", "abc1234", undefined),
|
||||||
);
|
);
|
||||||
const { result: todo } = renderHook(() =>
|
|
||||||
useTaskDiffStats("FN-123", "todo", "abc1234", undefined),
|
|
||||||
);
|
|
||||||
const { result: inReview } = renderHook(() =>
|
const { result: inReview } = renderHook(() =>
|
||||||
useTaskDiffStats("FN-123", "in-review", "abc1234", undefined),
|
useTaskDiffStats("FN-123", "in-review", "abc1234", undefined),
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => expect(inProgress.current.loading).toBe(false));
|
await waitFor(() => expect(inProgress.current.loading).toBe(false));
|
||||||
await waitFor(() => expect(todo.current.loading).toBe(false));
|
|
||||||
await waitFor(() => expect(inReview.current.loading).toBe(false));
|
await waitFor(() => expect(inReview.current.loading).toBe(false));
|
||||||
|
|
||||||
expect(inProgress.current.stats).toBeNull();
|
expect(inProgress.current.stats).toEqual({ filesChanged: 2, additions: 4, deletions: 1 });
|
||||||
expect(todo.current.stats).toBeNull();
|
expect(inReview.current.stats).toEqual({ filesChanged: 2, additions: 4, deletions: 1 });
|
||||||
expect(inReview.current.stats).toBeNull();
|
expect(mockFetchTaskDiff).toHaveBeenCalledWith("FN-123", undefined, undefined);
|
||||||
expect(mockFetchTaskDiff).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fetches diff stats for active tasks with a worktree", async () => {
|
it("fetches diff stats for active tasks with a worktree", async () => {
|
||||||
@@ -169,6 +169,16 @@ describe("useTaskDiffStats", () => {
|
|||||||
expect(mockFetchTaskDiff).toHaveBeenCalledTimes(2);
|
expect(mockFetchTaskDiff).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not fetch for inactive columns", async () => {
|
||||||
|
const { result: todo } = renderHook(() =>
|
||||||
|
useTaskDiffStats("FN-123", "todo", "abc1234", undefined),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(todo.current.loading).toBe(false));
|
||||||
|
expect(todo.current.stats).toBeNull();
|
||||||
|
expect(mockFetchTaskDiff).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("resets stats when column changes from done to non-done", async () => {
|
it("resets stats when column changes from done to non-done", async () => {
|
||||||
mockFetchTaskDiff.mockResolvedValueOnce({
|
mockFetchTaskDiff.mockResolvedValueOnce({
|
||||||
files: [],
|
files: [],
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ interface UseTaskDiffStatsOptions {
|
|||||||
const diffStatsCache = new Map<string, { stats: DiffStats; expiresAt: number }>();
|
const diffStatsCache = new Map<string, { stats: DiffStats; expiresAt: number }>();
|
||||||
const CACHE_TTL_MS = 30_000; // 30 seconds
|
const CACHE_TTL_MS = 30_000; // 30 seconds
|
||||||
|
|
||||||
function getCacheKey(taskId: string, projectId?: string, worktree?: string, stepVersion?: string): string {
|
function getCacheKey(taskId: string, projectId?: string, worktree?: string, stepVersion?: string, mode?: "done" | "active"): string {
|
||||||
return `${taskId}:${projectId ?? ""}:${worktree ?? ""}:${stepVersion ?? ""}`;
|
return `${taskId}:${projectId ?? ""}:${worktree ?? ""}:${stepVersion ?? ""}:${mode ?? ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCachedStats(taskId: string, projectId?: string, worktree?: string, stepVersion?: string): DiffStats | null {
|
function getCachedStats(taskId: string, projectId?: string, worktree?: string, stepVersion?: string, mode?: "done" | "active"): DiffStats | null {
|
||||||
const key = getCacheKey(taskId, projectId, worktree, stepVersion);
|
const key = getCacheKey(taskId, projectId, worktree, stepVersion, mode);
|
||||||
const entry = diffStatsCache.get(key);
|
const entry = diffStatsCache.get(key);
|
||||||
|
|
||||||
if (!entry) return null;
|
if (!entry) return null;
|
||||||
@@ -50,8 +50,8 @@ function getCachedStats(taskId: string, projectId?: string, worktree?: string, s
|
|||||||
return entry.stats;
|
return entry.stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCachedStats(taskId: string, projectId: string | undefined, worktree: string | undefined, stepVersion: string | undefined, stats: DiffStats): void {
|
function setCachedStats(taskId: string, projectId: string | undefined, worktree: string | undefined, stepVersion: string | undefined, mode: "done" | "active", stats: DiffStats): void {
|
||||||
const key = getCacheKey(taskId, projectId, worktree, stepVersion);
|
const key = getCacheKey(taskId, projectId, worktree, stepVersion, mode);
|
||||||
diffStatsCache.set(key, {
|
diffStatsCache.set(key, {
|
||||||
stats,
|
stats,
|
||||||
expiresAt: Date.now() + CACHE_TTL_MS,
|
expiresAt: Date.now() + CACHE_TTL_MS,
|
||||||
@@ -103,7 +103,7 @@ export function useTaskDiffStats(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shouldFetchDoneTask = column === "done";
|
const shouldFetchDoneTask = column === "done";
|
||||||
const shouldFetchActiveTask = (column === "in-progress" || column === "in-review") && Boolean(worktree);
|
const shouldFetchActiveTask = column === "in-progress" || column === "in-review";
|
||||||
|
|
||||||
if (!taskId || (!shouldFetchDoneTask && !shouldFetchActiveTask)) {
|
if (!taskId || (!shouldFetchDoneTask && !shouldFetchActiveTask)) {
|
||||||
setStats(null);
|
setStats(null);
|
||||||
@@ -113,12 +113,13 @@ export function useTaskDiffStats(
|
|||||||
|
|
||||||
const activeWorktree = shouldFetchActiveTask ? worktree : undefined;
|
const activeWorktree = shouldFetchActiveTask ? worktree : undefined;
|
||||||
const stepVersionStr = stepVersion !== undefined ? String(stepVersion) : undefined;
|
const stepVersionStr = stepVersion !== undefined ? String(stepVersion) : undefined;
|
||||||
|
const mode: "done" | "active" = shouldFetchDoneTask ? "done" : "active";
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
async function load(forceRefresh = false) {
|
async function load(forceRefresh = false) {
|
||||||
// Check cache first - return immediately without loading flicker (unless force refresh)
|
// Check cache first - return immediately without loading flicker (unless force refresh)
|
||||||
if (!forceRefresh) {
|
if (!forceRefresh) {
|
||||||
const cached = getCachedStats(taskId, projectId, activeWorktree, stepVersionStr);
|
const cached = getCachedStats(taskId, projectId, activeWorktree, stepVersionStr, mode);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setStats(cached);
|
setStats(cached);
|
||||||
@@ -134,7 +135,7 @@ export function useTaskDiffStats(
|
|||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setStats(data.stats);
|
setStats(data.stats);
|
||||||
// Store in cache
|
// Store in cache
|
||||||
setCachedStats(taskId, projectId, activeWorktree, stepVersionStr, data.stats);
|
setCachedStats(taskId, projectId, activeWorktree, stepVersionStr, mode, data.stats);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
|
|||||||
Reference in New Issue
Block a user