feat(web): enhanced file panel — side-by-side diff, hide whitespace, inline review
Adds DiffSplitView component for side-by-side diff mode, whitespace-only change filtering, inline review comments with thread/gutter cell UI, diff preferences persistence, and write-file API support for in-browser editing. Backend: hideWhitespace param on git diff endpoint, write_file route.
This commit is contained in:
92
apps/web/src/components/InlineReviewThread.tsx
Normal file
92
apps/web/src/components/InlineReviewThread.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useState } from 'react';
|
||||
import { MessageSquare, Pencil, Trash2 } from 'lucide-react';
|
||||
import type { DiffComment } from '@/stores/useDiffCommentStore';
|
||||
import { InlineReviewEditor } from './InlineReviewEditor';
|
||||
|
||||
interface InlineReviewThreadProps {
|
||||
comments: DiffComment[];
|
||||
onEditComment: (id: string, body: string) => void;
|
||||
onDeleteComment: (id: string) => void;
|
||||
}
|
||||
|
||||
export function InlineReviewThread({
|
||||
comments,
|
||||
onEditComment,
|
||||
onDeleteComment,
|
||||
}: InlineReviewThreadProps) {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editBody, setEditBody] = useState('');
|
||||
|
||||
if (comments.length === 0) return null;
|
||||
|
||||
const handleStartEdit = (id: string, body: string) => {
|
||||
setEditingId(id);
|
||||
setEditBody(body);
|
||||
};
|
||||
|
||||
const handleSaveEdit = (body: string) => {
|
||||
if (editingId) {
|
||||
onEditComment(editingId, body);
|
||||
setEditingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setEditingId(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ml-1 border-l-2 border-blue-400/40 pl-2 my-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="flex items-center gap-1 text-[10px] text-muted-foreground hover:text-foreground mb-0.5"
|
||||
>
|
||||
<MessageSquare size={10} />
|
||||
<span>{comments.length} comment{comments.length > 1 ? 's' : ''}</span>
|
||||
<span className="text-[9px]">{expanded ? '▲' : '▼'}</span>
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="space-y-1">
|
||||
{comments.map((comment) => (
|
||||
<div key={comment.id} className="text-xs">
|
||||
{editingId === comment.id ? (
|
||||
<InlineReviewEditor
|
||||
initialBody={editBody}
|
||||
onSave={handleSaveEdit}
|
||||
onCancel={handleCancelEdit}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-start gap-1 group">
|
||||
<span className="flex-1 text-foreground/90 leading-relaxed whitespace-pre-wrap">
|
||||
{comment.body}
|
||||
</span>
|
||||
<div className="hidden group-hover:flex items-center gap-0.5 shrink-0 mt-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleStartEdit(comment.id, comment.body)}
|
||||
className="p-0.5 rounded hover:bg-muted text-muted-foreground"
|
||||
title="Edit"
|
||||
>
|
||||
<Pencil size={10} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onDeleteComment(comment.id)}
|
||||
className="p-0.5 rounded hover:bg-muted text-muted-foreground hover:text-destructive"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 size={10} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user