Skip to content

Commit 46f300a

Browse files
authored
feat(code-review): filter files with comments (#3590)
1 parent a0f9a42 commit 46f300a

13 files changed

Lines changed: 617 additions & 78 deletions
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import type { PrCommentThread } from "@posthog/core/code-review/types";
2+
import type { ReactNode } from "react";
3+
4+
export type CommentFileFilter = "none" | "commented" | "unresolved";
5+
6+
export interface ReviewListItem {
7+
key: string;
8+
scrollKey?: string;
9+
filePaths?: string[];
10+
node: ReactNode;
11+
}
12+
13+
interface CommentFileFilterState {
14+
activeFilter: CommentFileFilter;
15+
visibleItems: ReviewListItem[];
16+
commentedFileCount: number;
17+
unresolvedCommentedFileCount: number;
18+
}
19+
20+
interface DeriveCommentFileFilterStateArgs {
21+
items: ReviewListItem[];
22+
requestedFilter: CommentFileFilter;
23+
commentedFilePaths?: ReadonlySet<string>;
24+
unresolvedCommentedFilePaths?: ReadonlySet<string>;
25+
}
26+
27+
export function getCommentedFilePaths(threads: Map<number, PrCommentThread>): {
28+
all: Set<string>;
29+
unresolved: Set<string>;
30+
} {
31+
const all = new Set<string>();
32+
const unresolved = new Set<string>();
33+
34+
for (const thread of threads.values()) {
35+
if (thread.comments.length === 0) continue;
36+
all.add(thread.filePath);
37+
if (!thread.isResolved) unresolved.add(thread.filePath);
38+
}
39+
40+
return { all, unresolved };
41+
}
42+
43+
export function filterReviewItemsByFilePaths(
44+
items: ReviewListItem[],
45+
filePaths: ReadonlySet<string>,
46+
): ReviewListItem[] {
47+
const filteredItems: ReviewListItem[] = [];
48+
let pendingSectionItems: ReviewListItem[] = [];
49+
50+
for (const item of items) {
51+
if (!item.filePaths) {
52+
pendingSectionItems = [item];
53+
continue;
54+
}
55+
56+
if (!item.filePaths.some((filePath) => filePaths.has(filePath))) continue;
57+
58+
filteredItems.push(...pendingSectionItems, item);
59+
pendingSectionItems = [];
60+
}
61+
62+
return filteredItems;
63+
}
64+
65+
export function deriveCommentFileFilterState({
66+
items,
67+
requestedFilter,
68+
commentedFilePaths,
69+
unresolvedCommentedFilePaths,
70+
}: DeriveCommentFileFilterStateArgs): CommentFileFilterState {
71+
if (!commentedFilePaths || !unresolvedCommentedFilePaths) {
72+
return {
73+
activeFilter: "none",
74+
visibleItems: items,
75+
commentedFileCount: 0,
76+
unresolvedCommentedFileCount: 0,
77+
};
78+
}
79+
80+
const commentedItems = filterReviewItemsByFilePaths(
81+
items,
82+
commentedFilePaths,
83+
);
84+
const unresolvedCommentedItems = filterReviewItemsByFilePaths(
85+
items,
86+
unresolvedCommentedFilePaths,
87+
);
88+
89+
let visibleItems: ReviewListItem[];
90+
switch (requestedFilter) {
91+
case "commented":
92+
visibleItems = commentedItems;
93+
break;
94+
case "unresolved":
95+
visibleItems = unresolvedCommentedItems;
96+
break;
97+
case "none":
98+
visibleItems = items;
99+
break;
100+
}
101+
102+
return {
103+
activeFilter: requestedFilter,
104+
visibleItems,
105+
commentedFileCount: commentedItems.filter((item) => item.filePaths).length,
106+
unresolvedCommentedFileCount: unresolvedCommentedItems.filter(
107+
(item) => item.filePaths,
108+
).length,
109+
};
110+
}
111+
112+
export function getEmptyReviewMessage(
113+
commentFilter: CommentFileFilter,
114+
): string {
115+
switch (commentFilter) {
116+
case "commented":
117+
return "No files with comments";
118+
case "unresolved":
119+
return "No files with unresolved comments";
120+
case "none":
121+
return "No file changes to review";
122+
}
123+
}

packages/ui/src/features/code-review/components/CloudReviewPage.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,13 @@ import { useMemo } from "react";
77
import { useDiffViewerStore } from "../../code-editor/diffViewerStore";
88
import { usePrDetails } from "../../git-interaction/usePrDetails";
99
import { useCloudChangedFiles } from "../../task-detail/hooks/useCloudChangedFiles";
10-
import { useReviewNavigationStore } from "../reviewNavigationStore";
11-
import { PatchedFileDiff } from "./PatchedFileDiff";
1210
import {
13-
buildItemIndex,
11+
getCommentedFilePaths,
1412
type ReviewListItem,
15-
ReviewShell,
16-
useReviewState,
17-
} from "./ReviewShell";
13+
} from "../commentFileFilter";
14+
import { useReviewNavigationStore } from "../reviewNavigationStore";
15+
import { PatchedFileDiff } from "./PatchedFileDiff";
16+
import { ReviewShell, useReviewState } from "./ReviewShell";
1817
import { changedFileSignature } from "./reviewItemBuilders";
1918

2019
interface CloudReviewPageProps {
@@ -36,9 +35,16 @@ export function CloudReviewPage({ task }: CloudReviewPageProps) {
3635
toolCalls,
3736
isLoading,
3837
} = useCloudChangedFiles(taskId, task, isReviewOpen);
39-
const { commentThreads } = usePrDetails(prUrl, {
38+
const { commentThreads, commentsLoading } = usePrDetails(prUrl, {
4039
includeComments: isReviewOpen && showReviewComments,
4140
});
41+
const commentedFilePaths = useMemo(
42+
() =>
43+
prUrl && !commentsLoading
44+
? getCommentedFilePaths(commentThreads)
45+
: undefined,
46+
[commentThreads, commentsLoading, prUrl],
47+
);
4248

4349
const allPaths = useMemo(() => reviewFiles.map((f) => f.path), [reviewFiles]);
4450

@@ -83,6 +89,9 @@ export function CloudReviewPage({ task }: CloudReviewPageProps) {
8389
return {
8490
key: file.path,
8591
scrollKey: file.path,
92+
filePaths: [file.path, file.originalPath].filter(
93+
(path): path is string => !!path,
94+
),
8695
node: (
8796
<PatchedFileDiff
8897
file={file}
@@ -111,8 +120,6 @@ export function CloudReviewPage({ task }: CloudReviewPageProps) {
111120
toolCallFallbacks,
112121
]);
113122

114-
const itemIndexByFilePath = useMemo(() => buildItemIndex(items), [items]);
115-
116123
if (!prUrl && !effectiveBranch && reviewFiles.length === 0) {
117124
if (isRunActive) {
118125
return (
@@ -146,7 +153,8 @@ export function CloudReviewPage({ task }: CloudReviewPageProps) {
146153
onUncollapseFile={uncollapseFile}
147154
onCollapseFiles={collapseFiles}
148155
items={items}
149-
itemIndexByFilePath={itemIndexByFilePath}
156+
commentedFilePaths={commentedFilePaths?.all}
157+
unresolvedCommentedFilePaths={commentedFilePaths?.unresolved}
150158
currentSignatures={currentSignatures}
151159
viewedRecord={viewedRecord}
152160
onToggleViewed={toggleViewed}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {
2+
DropdownMenuItem,
3+
DropdownMenuRadioGroup,
4+
DropdownMenuRadioItem,
5+
DropdownMenuSeparator,
6+
DropdownMenuSub,
7+
DropdownMenuSubContent,
8+
DropdownMenuSubTrigger,
9+
} from "@posthog/quill";
10+
import type { CommentFileFilter } from "../commentFileFilter";
11+
12+
interface CommentFilterSubmenuProps {
13+
commentedFileCount: number;
14+
unresolvedCommentedFileCount: number;
15+
commentFilter: CommentFileFilter;
16+
onCommentFilterChange: (filter: CommentFileFilter) => void;
17+
}
18+
19+
function getCommentFilterSuffix(commentFilter: CommentFileFilter): string {
20+
switch (commentFilter) {
21+
case "commented":
22+
return " · All";
23+
case "unresolved":
24+
return " · Unresolved";
25+
case "none":
26+
return "";
27+
}
28+
}
29+
30+
export function CommentFilterSubmenu({
31+
commentedFileCount,
32+
unresolvedCommentedFileCount,
33+
commentFilter,
34+
onCommentFilterChange,
35+
}: CommentFilterSubmenuProps) {
36+
return (
37+
<DropdownMenuSub>
38+
<DropdownMenuSubTrigger>
39+
Comment filter{getCommentFilterSuffix(commentFilter)}
40+
</DropdownMenuSubTrigger>
41+
<DropdownMenuSubContent side="right" sideOffset={4}>
42+
<DropdownMenuRadioGroup
43+
value={commentFilter === "none" ? "" : commentFilter}
44+
onValueChange={(value) =>
45+
onCommentFilterChange(value as CommentFileFilter)
46+
}
47+
>
48+
<DropdownMenuRadioItem value="commented">
49+
All comments ({commentedFileCount})
50+
</DropdownMenuRadioItem>
51+
<DropdownMenuRadioItem value="unresolved">
52+
Unresolved comments ({unresolvedCommentedFileCount})
53+
</DropdownMenuRadioItem>
54+
</DropdownMenuRadioGroup>
55+
{commentFilter !== "none" && (
56+
<>
57+
<DropdownMenuSeparator />
58+
<DropdownMenuItem onClick={() => onCommentFilterChange("none")}>
59+
Clear comment filter
60+
</DropdownMenuItem>
61+
</>
62+
)}
63+
</DropdownMenuSubContent>
64+
</DropdownMenuSub>
65+
);
66+
}

packages/ui/src/features/code-review/components/DiffSettingsMenu.tsx

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,22 @@ import {
88
DropdownMenuTrigger,
99
} from "@posthog/quill";
1010
import { useDiffViewerStore } from "@posthog/ui/features/code-editor/diffViewerStore";
11+
import type { CommentFileFilter } from "../commentFileFilter";
12+
import { CommentFilterSubmenu } from "./CommentFilterSubmenu";
1113

12-
export function DiffSettingsMenu() {
14+
interface DiffSettingsMenuProps {
15+
commentedFileCount: number;
16+
unresolvedCommentedFileCount: number;
17+
commentFilter: CommentFileFilter;
18+
onCommentFilterChange?: (filter: CommentFileFilter) => void;
19+
}
20+
21+
export function DiffSettingsMenu({
22+
commentedFileCount,
23+
unresolvedCommentedFileCount,
24+
commentFilter,
25+
onCommentFilterChange,
26+
}: DiffSettingsMenuProps) {
1327
const wordWrap = useDiffViewerStore((s) => s.wordWrap);
1428
const toggleWordWrap = useDiffViewerStore((s) => s.toggleWordWrap);
1529
const wordDiffs = useDiffViewerStore((s) => s.wordDiffs);
@@ -24,14 +38,25 @@ export function DiffSettingsMenu() {
2438
const toggleShowReviewComments = useDiffViewerStore(
2539
(s) => s.toggleShowReviewComments,
2640
);
41+
const handleToggleReviewComments = () => {
42+
if (showReviewComments && commentFilter !== "none") {
43+
onCommentFilterChange?.("none");
44+
}
45+
toggleShowReviewComments();
46+
};
2747

2848
return (
2949
<DropdownMenu>
3050
<DropdownMenuTrigger
3151
render={
3252
<Button
3353
size="icon-sm"
34-
aria-label="Diff settings"
54+
variant={commentFilter === "none" ? "default" : "primary"}
55+
aria-label={
56+
commentFilter === "none"
57+
? "Diff settings"
58+
: `Diff settings, ${commentFilter} comment filter active`
59+
}
3560
className="rounded-xs"
3661
>
3762
<DotsThree size={16} weight="bold" />
@@ -54,9 +79,17 @@ export function DiffSettingsMenu() {
5479
{hideWhitespaceChanges ? "Show whitespace" : "Hide whitespace"}
5580
</DropdownMenuItem>
5681
<DropdownMenuSeparator />
57-
<DropdownMenuItem onClick={toggleShowReviewComments}>
82+
<DropdownMenuItem onClick={handleToggleReviewComments}>
5883
{showReviewComments ? "Hide review comments" : "Show review comments"}
5984
</DropdownMenuItem>
85+
{showReviewComments && onCommentFilterChange && (
86+
<CommentFilterSubmenu
87+
commentedFileCount={commentedFileCount}
88+
unresolvedCommentedFileCount={unresolvedCommentedFileCount}
89+
commentFilter={commentFilter}
90+
onCommentFilterChange={onCommentFilterChange}
91+
/>
92+
)}
6093
</DropdownMenuContent>
6194
</DropdownMenu>
6295
);

0 commit comments

Comments
 (0)