veritas-kanban/web/src/components/task/diff/FileDiffView.tsx
Brad Groux 39eccf3556 feat: Sprint US-1200 Refactoring batch — 13 tasks complete
Completed refactors:
- RF-02: Fix dependency vulnerabilities (xlsx → exceljs, Hono updates)
- RF-05: Add React error boundaries (FeatureErrorBoundary wrapper)
- RF-06: Server error handling middleware (AppError classes, asyncHandler)
- RF-10: Split shared types.ts into domain modules (6 files)
- RF-11: Consolidate frontend API layer (hooks now use api.ts)
- RF-13: TaskConfigContext — eliminate prop drilling
- RF-14: Split god components (GitSection, TaskDetailPanel, CreateTaskDialog, DiffViewer)
- RF-16: Frontend accessibility (ARIA labels, sr-only text)
- RF-17: Modularize CLI (899 → commands/ structure)
- RF-18: Modularize MCP (843 → tools/ structure)
- RF-19: Create shared API client library
- RF-21: Server performance (batch loading, memory limits, timeouts, graceful shutdown)
- RF-23: Extract shared utilities (path, format, constants)

Stats: ~59 files changed, significant code reduction through modularization
2026-01-28 06:08:59 -06:00

120 lines
3.5 KiB
TypeScript

import { useState } from 'react';
import { useFileDiff } from '@/hooks/useDiff';
import { DiffHunkView } from './DiffHunk';
import {
FileCode,
FilePlus,
FileMinus,
FileEdit,
Loader2,
AlertCircle,
MessageSquare,
} from 'lucide-react';
import type { ReviewComment } from '@veritas-kanban/shared';
import type { FileChange } from '@/lib/api';
import { nanoid } from 'nanoid';
const statusIcons: Record<FileChange['status'], React.ReactNode> = {
added: <FilePlus className="h-4 w-4 text-green-500" />,
modified: <FileEdit className="h-4 w-4 text-amber-500" />,
deleted: <FileMinus className="h-4 w-4 text-red-500" />,
renamed: <FileCode className="h-4 w-4 text-blue-500" />,
};
interface FileDiffViewProps {
taskId: string;
filePath: string;
comments: ReviewComment[];
onAddComment: (comment: ReviewComment) => void;
onRemoveComment: (commentId: string) => void;
}
export function FileDiffView({
taskId,
filePath,
comments,
onAddComment,
onRemoveComment,
}: FileDiffViewProps) {
const { data: diff, isLoading, error } = useFileDiff(taskId, filePath);
const [addingCommentAtLine, setAddingCommentAtLine] = useState<number | null>(null);
const fileComments = comments.filter(c => c.file === filePath);
const handleSubmitComment = (content: string) => {
if (addingCommentAtLine === null) return;
const comment: ReviewComment = {
id: `comment_${nanoid(8)}`,
file: filePath,
line: addingCommentAtLine,
content,
created: new Date().toISOString(),
};
onAddComment(comment);
setAddingCommentAtLine(null);
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64 text-muted-foreground">
<Loader2 className="h-5 w-5 animate-spin mr-2" />
Loading diff...
</div>
);
}
if (error || !diff) {
return (
<div className="flex items-center justify-center h-64 text-muted-foreground">
<AlertCircle className="h-5 w-5 mr-2" />
{(error as Error)?.message || 'Failed to load diff'}
</div>
);
}
if (diff.hunks.length === 0) {
return (
<div className="flex items-center justify-center h-64 text-muted-foreground">
No changes in this file
</div>
);
}
return (
<div className="border rounded-md overflow-hidden bg-card">
<div className="flex items-center justify-between px-4 py-2 bg-muted/50 border-b">
<div className="flex items-center gap-2">
{statusIcons[diff.status]}
<span className="font-mono text-sm">{diff.path}</span>
</div>
<div className="flex items-center gap-2 text-xs">
{fileComments.length > 0 && (
<span className="flex items-center gap-1 text-amber-500">
<MessageSquare className="h-3 w-3" />
{fileComments.length}
</span>
)}
<span className="text-green-500">+{diff.additions}</span>
<span className="text-red-500">-{diff.deletions}</span>
</div>
</div>
<div className="overflow-x-auto">
{diff.hunks.map((hunk, idx) => (
<DiffHunkView
key={idx}
hunk={hunk}
comments={fileComments}
addingCommentAtLine={addingCommentAtLine}
onStartAddComment={setAddingCommentAtLine}
onSubmitComment={handleSubmitComment}
onCancelComment={() => setAddingCommentAtLine(null)}
onRemoveComment={onRemoveComment}
/>
))}
</div>
</div>
);
}