138 lines
5.3 KiB
TypeScript
138 lines
5.3 KiB
TypeScript
import { useEffect, useMemo } from "react";
|
|
import { FileEdit, FileMinus, FilePlus, FileSymlink, FolderGit2, X } from "lucide-react";
|
|
import { useChangedFiles } from "../hooks/useChangedFiles";
|
|
import { highlightDiff } from "../utils/highlightDiff";
|
|
import type { TaskFileDiff } from "../api";
|
|
|
|
interface ChangedFilesModalProps {
|
|
taskId: string;
|
|
worktree: string | undefined;
|
|
column: string;
|
|
projectId?: string;
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
function getStatusLabel(status: TaskFileDiff["status"]): string {
|
|
switch (status) {
|
|
case "added":
|
|
return "A";
|
|
case "deleted":
|
|
return "D";
|
|
case "renamed":
|
|
return "R";
|
|
default:
|
|
return "M";
|
|
}
|
|
}
|
|
|
|
function getStatusIcon(status: TaskFileDiff["status"]) {
|
|
switch (status) {
|
|
case "added":
|
|
return <FilePlus size={16} />;
|
|
case "deleted":
|
|
return <FileMinus size={16} />;
|
|
case "renamed":
|
|
return <FileSymlink size={16} />;
|
|
default:
|
|
return <FileEdit size={16} />;
|
|
}
|
|
}
|
|
|
|
function getDiffStat(diff: string): string {
|
|
const lines = diff.split("\n");
|
|
const statLines = lines.filter((line) => line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("--- ") || line.startsWith("+++ "));
|
|
return statLines.join("\n").trim();
|
|
}
|
|
|
|
export function ChangedFilesModal({ taskId, worktree, column, projectId, isOpen, onClose }: ChangedFilesModalProps) {
|
|
const { files, loading, error, selectedFile, setSelectedFile } = useChangedFiles(taskId, worktree, column, projectId);
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key === "Escape") {
|
|
onClose();
|
|
}
|
|
};
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
}, [isOpen, onClose]);
|
|
|
|
const selectedStat = useMemo(() => (selectedFile ? getDiffStat(selectedFile.diff) : ""), [selectedFile]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div className="modal-overlay open" onClick={onClose}>
|
|
<div className="modal file-browser-modal changed-files-modal" onClick={(event) => event.stopPropagation()}>
|
|
<div className="modal-header file-browser-modal-header">
|
|
<div className="file-browser-header-title">
|
|
<FolderGit2 size={18} />
|
|
<span>Changed Files — {taskId}</span>
|
|
</div>
|
|
<button className="modal-close" onClick={onClose} aria-label="Close changed files viewer">
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="file-browser-body changed-files-layout">
|
|
<aside className="file-browser-sidebar changed-files-sidebar">
|
|
{loading ? (
|
|
<div className="gm-diff-loading">Loading changed files…</div>
|
|
) : error ? (
|
|
<div className="gm-diff-error">{error}</div>
|
|
) : files.length === 0 ? (
|
|
<div className="file-browser-empty">No files changed</div>
|
|
) : (
|
|
<div className="file-browser-list" role="list" aria-label="Changed files list">
|
|
{files.map((file) => {
|
|
const active = selectedFile?.path === file.path && selectedFile?.oldPath === file.oldPath;
|
|
return (
|
|
<button
|
|
key={`${file.oldPath ?? ""}:${file.path}`}
|
|
type="button"
|
|
role="listitem"
|
|
aria-label={file.path}
|
|
className={`file-node file-node--file changed-files-entry ${active ? "active" : ""}`}
|
|
onClick={() => setSelectedFile(file)}
|
|
>
|
|
<span className="file-node-icon">{getStatusIcon(file.status)}</span>
|
|
<span className="file-node-name">{file.path}</span>
|
|
<span className="detail-column-badge changed-files-badge">{getStatusLabel(file.status)}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</aside>
|
|
|
|
<section className="file-browser-content changed-files-content">
|
|
{!loading && !error && files.length > 0 && !selectedFile ? (
|
|
<div className="file-browser-empty">Select a file to view changes</div>
|
|
) : null}
|
|
|
|
{selectedFile ? (
|
|
<div className="gm-diff-section changed-files-diff-section" aria-label={`Diff for ${selectedFile.path}`}>
|
|
<div className="file-browser-toolbar">
|
|
<div className="file-browser-file-info">
|
|
<strong>{selectedFile.path}</strong>
|
|
<span className="detail-column-badge changed-files-badge">{getStatusLabel(selectedFile.status)}</span>
|
|
{selectedFile.oldPath ? <span>Renamed from {selectedFile.oldPath}</span> : null}
|
|
</div>
|
|
</div>
|
|
<div className="gm-diff-viewer">
|
|
{selectedStat ? <pre className="gm-diff-stat">{selectedStat}</pre> : null}
|
|
<pre className="gm-diff-patch">
|
|
<code>{highlightDiff(selectedFile.diff || "No diff available")}</code>
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|