feat(FN-907): add word wrap toggle to diff viewer in TaskChangesTab
- Add word wrap state and toggle button to TaskChangesTab component - Add CSS styles for word wrap toggle in diff viewer - Add tests for word wrap toggle functionality
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { FileCode, ChevronDown, ChevronRight, ChevronLeft, AlertCircle, GitCommit } from "lucide-react";
|
import { FileCode, ChevronDown, ChevronRight, ChevronLeft, AlertCircle, GitCommit, WrapText } from "lucide-react";
|
||||||
import type { MergeDetails, Column } from "@fusion/core";
|
import type { MergeDetails, Column } from "@fusion/core";
|
||||||
import { fetchTaskDiff, type TaskDiff } from "../api";
|
import { fetchTaskDiff, type TaskDiff } from "../api";
|
||||||
import { highlightDiff } from "../utils/highlightDiff";
|
import { highlightDiff } from "../utils/highlightDiff";
|
||||||
@@ -50,6 +50,7 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set());
|
const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set());
|
||||||
const [currentFileIndex, setCurrentFileIndex] = useState<number | null>(null);
|
const [currentFileIndex, setCurrentFileIndex] = useState<number | null>(null);
|
||||||
|
const [wordWrap, setWordWrap] = useState(true);
|
||||||
|
|
||||||
const canLoad = column === "in-progress" || column === "in-review" || column === "done";
|
const canLoad = column === "in-progress" || column === "in-review" || column === "done";
|
||||||
|
|
||||||
@@ -227,6 +228,14 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
className={`btn btn-sm ${wordWrap ? "btn-primary" : ""}`}
|
||||||
|
onClick={() => setWordWrap((prev) => !prev)}
|
||||||
|
title={wordWrap ? "Disable word wrap" : "Enable word wrap"}
|
||||||
|
aria-label="Toggle word wrap"
|
||||||
|
>
|
||||||
|
<WrapText size={14} />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm"
|
className="btn btn-sm"
|
||||||
onClick={loadDiff}
|
onClick={loadDiff}
|
||||||
@@ -272,7 +281,7 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai
|
|||||||
|
|
||||||
{isExpanded && file.patch && (
|
{isExpanded && file.patch && (
|
||||||
<div className="changes-file-content">
|
<div className="changes-file-content">
|
||||||
<pre className="changes-diff-patch">
|
<pre className={`changes-diff-patch ${wordWrap ? "changes-diff-patch--wrap" : "changes-diff-patch--nowrap"}`}>
|
||||||
<code>{highlightDiff(file.patch)}</code>
|
<code>{highlightDiff(file.patch)}</code>
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ vi.mock("lucide-react", () => ({
|
|||||||
ChevronLeft: ({ size }: any) => <span data-testid="chevron-left" />,
|
ChevronLeft: ({ size }: any) => <span data-testid="chevron-left" />,
|
||||||
AlertCircle: () => null,
|
AlertCircle: () => null,
|
||||||
GitCommit: () => null,
|
GitCommit: () => null,
|
||||||
|
WrapText: ({ size }: any) => <span data-testid="wrap-text-icon" />,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../utils/highlightDiff", () => ({
|
vi.mock("../../utils/highlightDiff", () => ({
|
||||||
@@ -618,3 +619,119 @@ describe("TaskChangesTab — file navigation", () => {
|
|||||||
expect(container.querySelectorAll(".changes-file-content")).toHaveLength(1);
|
expect(container.querySelectorAll(".changes-file-content")).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("TaskChangesTab — word wrap toggle", () => {
|
||||||
|
it("renders the word wrap toggle button", async () => {
|
||||||
|
mockFetchTaskDiff.mockResolvedValue(DONE_TASK_DIFF);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TaskChangesTab
|
||||||
|
taskId="FN-001"
|
||||||
|
worktree={undefined}
|
||||||
|
column="done"
|
||||||
|
mergeDetails={MERGE_DETAILS}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByLabelText("Toggle word wrap")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults to word wrap ON (btn-primary active)", async () => {
|
||||||
|
mockFetchTaskDiff.mockResolvedValue(DONE_TASK_DIFF);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TaskChangesTab
|
||||||
|
taskId="FN-001"
|
||||||
|
worktree={undefined}
|
||||||
|
column="done"
|
||||||
|
mergeDetails={MERGE_DETAILS}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const toggle = screen.getByLabelText("Toggle word wrap");
|
||||||
|
expect(toggle.className).toContain("btn-primary");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies wrap CSS class when word wrap is ON", async () => {
|
||||||
|
mockFetchTaskDiff.mockResolvedValue(DONE_TASK_DIFF);
|
||||||
|
|
||||||
|
const { container } = render(
|
||||||
|
<TaskChangesTab
|
||||||
|
taskId="FN-001"
|
||||||
|
worktree={undefined}
|
||||||
|
column="done"
|
||||||
|
mergeDetails={MERGE_DETAILS}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("src/app.ts")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const diffPatch = container.querySelector(".changes-diff-patch");
|
||||||
|
expect(diffPatch).toBeTruthy();
|
||||||
|
expect(diffPatch?.classList.contains("changes-diff-patch--wrap")).toBe(true);
|
||||||
|
expect(diffPatch?.classList.contains("changes-diff-patch--nowrap")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles to nowrap when clicked", async () => {
|
||||||
|
mockFetchTaskDiff.mockResolvedValue(DONE_TASK_DIFF);
|
||||||
|
|
||||||
|
const { container } = render(
|
||||||
|
<TaskChangesTab
|
||||||
|
taskId="FN-001"
|
||||||
|
worktree={undefined}
|
||||||
|
column="done"
|
||||||
|
mergeDetails={MERGE_DETAILS}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("src/app.ts")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default: wrap ON
|
||||||
|
let diffPatch = container.querySelector(".changes-diff-patch");
|
||||||
|
expect(diffPatch?.classList.contains("changes-diff-patch--wrap")).toBe(true);
|
||||||
|
|
||||||
|
// Click toggle
|
||||||
|
fireEvent.click(screen.getByLabelText("Toggle word wrap"));
|
||||||
|
|
||||||
|
// Now wrap should be OFF
|
||||||
|
diffPatch = container.querySelector(".changes-diff-patch");
|
||||||
|
expect(diffPatch?.classList.contains("changes-diff-patch--nowrap")).toBe(true);
|
||||||
|
expect(diffPatch?.classList.contains("changes-diff-patch--wrap")).toBe(false);
|
||||||
|
|
||||||
|
// Button should no longer have btn-primary
|
||||||
|
const toggle = screen.getByLabelText("Toggle word wrap");
|
||||||
|
expect(toggle.className).not.toContain("btn-primary");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates tooltip based on wrap state", async () => {
|
||||||
|
mockFetchTaskDiff.mockResolvedValue(DONE_TASK_DIFF);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TaskChangesTab
|
||||||
|
taskId="FN-001"
|
||||||
|
worktree={undefined}
|
||||||
|
column="done"
|
||||||
|
mergeDetails={MERGE_DETAILS}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("src/app.ts")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggle = screen.getByLabelText("Toggle word wrap");
|
||||||
|
expect(toggle.getAttribute("title")).toBe("Disable word wrap");
|
||||||
|
|
||||||
|
// Click to toggle OFF
|
||||||
|
fireEvent.click(toggle);
|
||||||
|
expect(toggle.getAttribute("title")).toBe("Enable word wrap");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -3608,6 +3608,16 @@ body {
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.changes-diff-patch.changes-diff-patch--wrap {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changes-diff-patch.changes-diff-patch--nowrap {
|
||||||
|
white-space: pre;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
/* Summary section - styled for done tasks with success accent */
|
/* Summary section - styled for done tasks with success accent */
|
||||||
.detail-summary {
|
.detail-summary {
|
||||||
background: rgba(63, 185, 80, 0.08);
|
background: rgba(63, 185, 80, 0.08);
|
||||||
|
|||||||
Reference in New Issue
Block a user