Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/web/src/components/pullRequest/PullRequestCodeTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ function PullRequestCodeTab({
const reviewKey = referenceKey;
const pendingComments = usePendingReviewComments(reference);
const addComment = usePullRequestReviewStore((store) => store.addComment);
const editComment = usePullRequestReviewStore((store) => store.editComment);
const removeComment = usePullRequestReviewStore((store) => store.removeComment);
const replyToThread = useAtomCommand(pullRequestEnvironment.replyToThread, {
reportFailure: false,
Expand Down Expand Up @@ -977,6 +978,7 @@ function PullRequestCodeTab({
<PendingReviewCommentCard
key={comment.id}
comment={comment}
onEdit={(body) => editComment(reviewKey, comment.id, body)}
onRemove={() => removeComment(reviewKey, comment.id)}
/>
))}
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components/pullRequest/PullRequestGuideTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function PullRequestGuideTab({
const reviewKey = pullRequestReviewKey(reference);
const pending = usePendingReviewComments(reference);
const addComment = usePullRequestReviewStore((store) => store.addComment);
const editComment = usePullRequestReviewStore((store) => store.editComment);
const removeComment = usePullRequestReviewStore((store) => store.removeComment);
const commenting: ExcerptCommenting | null =
!canComment && onAskAboutLines === null
Expand All @@ -72,6 +73,7 @@ export function PullRequestGuideTab({
position,
body,
}),
onEdit: (commentId, body) => editComment(reviewKey, commentId, body),
onRemove: (commentId) => removeComment(reviewKey, commentId),
onAskAgent: onAskAboutLines,
};
Expand Down
73 changes: 62 additions & 11 deletions apps/web/src/components/pullRequest/PullRequestReviewAnnotation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
PullRequestThreadCommentsResult,
PullRequestThreadComment,
} from "@t3tools/contracts";
import { HammerIcon, MessageSquareIcon, Trash2Icon } from "lucide-react";
import { HammerIcon, MessageSquareIcon, PencilIcon, Trash2Icon } from "lucide-react";
import { Circle, CircleCheck } from "lucide";
import { useRef, useState } from "react";
import { settlePromise, squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
Expand Down Expand Up @@ -74,11 +74,21 @@ export function getReviewPositionAnchor(position: PullRequestReviewPosition): {

export function PendingReviewCommentCard({
comment,
onEdit,
onRemove,
}: {
comment: PendingReviewComment;
onEdit: (body: string) => void;
onRemove: () => void;
}) {
const [draft, setDraft] = useState<string | null>(null);
const save = () => {
const body = draft?.trim() ?? "";
if (body.length === 0) return;
onEdit(body);
setDraft(null);
};

return (
<div
className={cn(CARD_CLASS, "border-dashed")}
Expand All @@ -88,17 +98,58 @@ export function PendingReviewCommentCard({
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<MessageSquareIcon className="size-3.5" />
<span>Pending. Sent when you submit the review</span>
<Button
size="icon-xs"
variant="ghost"
className="ml-auto"
aria-label="Discard this comment"
onClick={onRemove}
>
<Trash2Icon className="size-3.5" />
</Button>
<div className="ml-auto flex items-center gap-1">
{draft === null ? (
<Button
size="icon-xs"
variant="ghost"
aria-label="Edit this comment"
onClick={() => setDraft(comment.body)}
>
<PencilIcon className="size-3.5" />
</Button>
) : null}
<Button
size="icon-xs"
variant="ghost"
aria-label="Discard this comment"
onClick={onRemove}
>
<Trash2Icon className="size-3.5" />
</Button>
</div>
</div>
<p className="mt-2 whitespace-pre-wrap leading-relaxed">{comment.body}</p>
{draft === null ? (
<p className="mt-2 whitespace-pre-wrap leading-relaxed">{comment.body}</p>
) : (
<div className="mt-2">
<Textarea
autoFocus
size="sm"
value={draft}
aria-label="Edit this comment"
onChange={(event) => setDraft(event.target.value)}
onFocus={(event) => {
const end = event.currentTarget.value.length;
event.currentTarget.setSelectionRange(end, end);
}}
onKeyDown={submitKeys({
value: draft,
pending: false,
onSubmit: save,
onCancel: () => setDraft(null),
})}
/>
<div className="mt-2 flex justify-end gap-2">
<Button size="xs" variant="ghost" onClick={() => setDraft(null)}>
Cancel
</Button>
<Button size="xs" disabled={draft.trim().length === 0} onClick={save}>
Save
</Button>
</div>
</div>
)}
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,7 @@ export function PullRequestReviewForm({
return;
}
// Preserve comments and summary edits made during submission.
removeComments(
reviewKey,
submittedComments.map((comment) => comment.id),
);
removeComments(reviewKey, submittedComments);
clearSummary(reviewKey, submittedBody);
toastManager.add({ type: "success", title: verdict.sent(term) });
onSubmitted();
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components/pullRequest/guide/GuideExcerpt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export interface ExcerptCommenting {
readonly startPosition?: PullRequestReviewPosition | undefined;
readonly body: string;
}) => void;
readonly onEdit: (commentId: string, body: string) => void;
readonly onRemove: (commentId: string) => void;
readonly onAskAgent: ((comment: ReviewCommentContext, request: string) => void) | null;
}
Expand Down Expand Up @@ -315,6 +316,7 @@ function ExcerptDiff({
<PendingReviewCommentCard
key={comment.id}
comment={comment}
onEdit={(body) => commenting?.onEdit(comment.id, body)}
onRemove={() => commenting?.onRemove(comment.id)}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ describe("pull request review drafts", () => {
it("removes only the line comments included in a submitted snapshot", () => {
const store = usePullRequestReviewStore.getState();
store.addComment("review-a", comment("submitted"));
const submittedIds =
usePullRequestReviewStore.getState().drafts["review-a"]?.map((entry) => entry.id) ?? [];
store.addComment("review-a", comment("edited-in-flight"));
const submitted = usePullRequestReviewStore.getState().drafts["review-a"] ?? [];

usePullRequestReviewStore.getState().addComment("review-a", comment("added-in-flight"));
usePullRequestReviewStore.getState().removeComments("review-a", submittedIds);
usePullRequestReviewStore.getState().editComment("review-a", "edited-in-flight", "Revised");
usePullRequestReviewStore.getState().removeComments("review-a", submitted);

expect(usePullRequestReviewStore.getState().drafts["review-a"]).toEqual([
comment("edited-in-flight", "Revised"),
comment("added-in-flight"),
]);
});
Expand Down
21 changes: 17 additions & 4 deletions apps/web/src/components/pullRequest/pullRequestReviewStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ interface PullRequestReviewStoreState {
readonly drafts: Readonly<Record<string, ReadonlyArray<PendingReviewComment>>>;
readonly summaries: Readonly<Record<string, string>>;
readonly addComment: (key: string, comment: PendingReviewComment) => void;
readonly editComment: (key: string, commentId: string, body: string) => void;
readonly removeComment: (key: string, commentId: string) => void;
readonly removeComments: (key: string, commentIds: ReadonlyArray<string>) => void;
readonly removeComments: (key: string, comments: ReadonlyArray<PendingReviewComment>) => void;
readonly clear: (key: string) => void;
readonly setSummary: (key: string, body: string) => void;
readonly clearSummary: (key: string, submittedBody: string) => void;
Expand All @@ -40,17 +41,29 @@ export const usePullRequestReviewStore = create<PullRequestReviewStoreState>()((
set((state) => ({
drafts: { ...state.drafts, [key]: [...(state.drafts[key] ?? EMPTY), comment] },
})),
editComment: (key, commentId, body) =>
set((state) => {
const comments = state.drafts[key] ?? EMPTY;
if (!comments.some((entry) => entry.id === commentId && entry.body !== body)) return state;
return {
drafts: {
...state.drafts,
[key]: comments.map((entry) => (entry.id === commentId ? { ...entry, body } : entry)),
},
};
}),
removeComment: (key, commentId) =>
set((state) => {
const remaining = (state.drafts[key] ?? EMPTY).filter((entry) => entry.id !== commentId);
if (remaining.length > 0) return { drafts: { ...state.drafts, [key]: remaining } };
const { [key]: _removed, ...rest } = state.drafts;
return { drafts: rest };
}),
removeComments: (key, commentIds) =>
removeComments: (key, comments) =>
set((state) => {
const submitted = new Set(commentIds);
const remaining = (state.drafts[key] ?? EMPTY).filter((entry) => !submitted.has(entry.id));
// By identity: a comment edited during submission is a new object and stays pending.
const submitted = new Set(comments);
const remaining = (state.drafts[key] ?? EMPTY).filter((entry) => !submitted.has(entry));
if (remaining.length > 0) return { drafts: { ...state.drafts, [key]: remaining } };
const { [key]: _removed, ...rest } = state.drafts;
return { drafts: rest };
Expand Down
Loading