Files
Codewalkers/apps/web/src/components/review/LineWithComments.tsx
Lukas May 1e723611e7 feat: Allow editing review comments
Add update method to ReviewCommentRepository, updateReviewComment tRPC
procedure, and inline edit UI in CommentThread. Edit button appears on
user-authored comments (not agent comments) when unresolved. Uses the
existing CommentForm with a new initialValue prop.
2026-03-06 11:58:08 +01:00

173 lines
5.3 KiB
TypeScript

import { useRef, useEffect } from "react";
import { MessageSquarePlus } from "lucide-react";
import type { DiffLine, ReviewComment } from "./types";
import { CommentThread } from "./CommentThread";
import { CommentForm } from "./CommentForm";
import type { TokenizedLine } from "./use-syntax-highlight";
interface LineWithCommentsProps {
line: DiffLine;
lineKey: number;
lineComments: ReviewComment[];
isCommenting: boolean;
onStartComment: () => void;
onCancelComment: () => void;
onSubmitComment: (body: string) => void;
onResolveComment: (commentId: string) => void;
onUnresolveComment: (commentId: string) => void;
onReplyComment?: (parentCommentId: string, body: string) => void;
onEditComment?: (commentId: string, body: string) => void;
/** Syntax-highlighted tokens for this line (if available) */
tokens?: TokenizedLine;
}
export function LineWithComments({
line,
lineKey: _lineKey,
lineComments,
isCommenting,
onStartComment,
onCancelComment,
onSubmitComment,
onResolveComment,
onUnresolveComment,
onReplyComment,
onEditComment,
tokens,
}: LineWithCommentsProps) {
const formRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (isCommenting) {
formRef.current?.focus();
}
}, [isCommenting]);
const bgClass =
line.type === "added"
? "bg-diff-add-bg"
: line.type === "removed"
? "bg-diff-remove-bg"
: "";
const gutterBgClass =
line.type === "added"
? "bg-diff-add-bg"
: line.type === "removed"
? "bg-diff-remove-bg"
: "bg-muted/30";
const prefix =
line.type === "added" ? "+" : line.type === "removed" ? "-" : " ";
const textColorClass =
line.type === "added"
? "text-diff-add-fg"
: line.type === "removed"
? "text-diff-remove-fg"
: "";
return (
<>
<tr
className={`group ${bgClass} hover:ring-1 hover:ring-inset hover:ring-primary/10`}
>
{/* Line numbers */}
<td
className={`w-[72px] min-w-[72px] select-none text-right text-muted-foreground pr-1 ${gutterBgClass} align-top`}
>
<div className="flex items-center justify-end gap-0">
<span className="w-8 inline-block text-right text-[11px] leading-5">
{line.oldLineNumber ?? ""}
</span>
<span className="w-8 inline-block text-right text-[11px] leading-5">
{line.newLineNumber ?? ""}
</span>
</div>
</td>
{/* Comment button gutter */}
<td className={`w-6 min-w-6 ${gutterBgClass} align-top`}>
{lineComments.length > 0 ? (
<div className="relative flex items-center justify-center h-5">
<button
className="flex items-center justify-center group/comment"
onClick={onStartComment}
title={`${lineComments.length} comment${lineComments.length > 1 ? "s" : ""} — click to reply`}
>
<span className="h-2 w-2 rounded-full bg-primary group-hover/comment:hidden" />
<MessageSquarePlus className="h-3.5 w-3.5 text-primary hidden group-hover/comment:block" />
{lineComments.length > 1 && (
<span className="absolute -top-0.5 -right-0.5 text-[8px] text-primary font-bold leading-none">
{lineComments.length}
</span>
)}
</button>
</div>
) : (
<button
className="opacity-0 group-hover:opacity-50 transition-opacity p-0.5 hover:!opacity-100 hover:text-primary text-muted-foreground"
onClick={onStartComment}
title="Add comment"
>
<MessageSquarePlus className="h-3.5 w-3.5" />
</button>
)}
</td>
{/* Code content */}
<td className="pl-1 pr-3 align-top">
<pre
className={`leading-5 whitespace-pre-wrap break-all ${tokens ? "" : textColorClass}`}
>
<span className="select-none text-muted-foreground/60">
{prefix}
</span>
{tokens
? tokens.map((token, i) => (
<span key={i} style={{ color: token.color }}>
{token.content}
</span>
))
: line.content}
</pre>
</td>
</tr>
{/* Existing comments on this line */}
{lineComments.length > 0 && (
<tr data-comment-id={lineComments.find((c) => !c.parentCommentId)?.id}>
<td
colSpan={3}
className="px-3 py-2 bg-muted/20 border-y border-border/50"
>
<CommentThread
comments={lineComments}
onResolve={onResolveComment}
onUnresolve={onUnresolveComment}
onReply={onReplyComment}
onEdit={onEditComment}
/>
</td>
</tr>
)}
{/* Inline comment form */}
{isCommenting && (
<tr>
<td
colSpan={3}
className="px-3 py-2 bg-diff-hunk-bg border-y border-status-active-border"
>
<CommentForm
ref={formRef}
onSubmit={onSubmitComment}
onCancel={onCancelComment}
/>
</td>
</tr>
)}
</>
);
}