Skip to content
123 changes: 123 additions & 0 deletions packages/ui/src/features/code-review/commentFileFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import type { PrCommentThread } from "@posthog/core/code-review/types";
import type { ReactNode } from "react";

export type CommentFileFilter = "none" | "commented" | "unresolved";

export interface ReviewListItem {
key: string;
scrollKey?: string;
filePaths?: string[];
node: ReactNode;
}

interface CommentFileFilterState {
activeFilter: CommentFileFilter;
visibleItems: ReviewListItem[];
commentedFileCount: number;
unresolvedCommentedFileCount: number;
}

interface DeriveCommentFileFilterStateArgs {
items: ReviewListItem[];
requestedFilter: CommentFileFilter;
commentedFilePaths?: ReadonlySet<string>;
unresolvedCommentedFilePaths?: ReadonlySet<string>;
}

export function getCommentedFilePaths(threads: Map<number, PrCommentThread>): {
all: Set<string>;
unresolved: Set<string>;
} {
const all = new Set<string>();
const unresolved = new Set<string>();

for (const thread of threads.values()) {
if (thread.comments.length === 0) continue;
all.add(thread.filePath);
if (!thread.isResolved) unresolved.add(thread.filePath);
}

return { all, unresolved };
}

export function filterReviewItemsByFilePaths(
items: ReviewListItem[],
filePaths: ReadonlySet<string>,
): ReviewListItem[] {
const filteredItems: ReviewListItem[] = [];
let pendingSectionItems: ReviewListItem[] = [];

for (const item of items) {
if (!item.filePaths) {
pendingSectionItems = [item];
continue;
}

if (!item.filePaths.some((filePath) => filePaths.has(filePath))) continue;

filteredItems.push(...pendingSectionItems, item);
pendingSectionItems = [];
}

return filteredItems;
}

export function deriveCommentFileFilterState({
items,
requestedFilter,
commentedFilePaths,
unresolvedCommentedFilePaths,
}: DeriveCommentFileFilterStateArgs): CommentFileFilterState {
if (!commentedFilePaths || !unresolvedCommentedFilePaths) {
return {
activeFilter: "none",
visibleItems: items,
commentedFileCount: 0,
unresolvedCommentedFileCount: 0,
};
}

const commentedItems = filterReviewItemsByFilePaths(
items,
commentedFilePaths,
);
const unresolvedCommentedItems = filterReviewItemsByFilePaths(
items,
unresolvedCommentedFilePaths,
);

let visibleItems: ReviewListItem[];
switch (requestedFilter) {
case "commented":
visibleItems = commentedItems;
break;
case "unresolved":
visibleItems = unresolvedCommentedItems;
break;
case "none":
visibleItems = items;
break;
}

return {
activeFilter: requestedFilter,
visibleItems,
commentedFileCount: commentedItems.filter((item) => item.filePaths).length,
unresolvedCommentedFileCount: unresolvedCommentedItems.filter(
(item) => item.filePaths,
).length,
};
}

export function getEmptyReviewMessage(
commentFilter: CommentFileFilter,
): string {
switch (commentFilter) {
case "commented":
return "No files with comments";
case "unresolved":
return "No files with unresolved comments";
case "none":
return "No file changes to review";
}
}
28 changes: 18 additions & 10 deletions packages/ui/src/features/code-review/components/CloudReviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ import { useMemo } from "react";
import { useDiffViewerStore } from "../../code-editor/diffViewerStore";
import { usePrDetails } from "../../git-interaction/usePrDetails";
import { useCloudChangedFiles } from "../../task-detail/hooks/useCloudChangedFiles";
import { useReviewNavigationStore } from "../reviewNavigationStore";
import { PatchedFileDiff } from "./PatchedFileDiff";
import {
buildItemIndex,
getCommentedFilePaths,
type ReviewListItem,
ReviewShell,
useReviewState,
} from "./ReviewShell";
} from "../commentFileFilter";
import { useReviewNavigationStore } from "../reviewNavigationStore";
import { PatchedFileDiff } from "./PatchedFileDiff";
import { ReviewShell, useReviewState } from "./ReviewShell";
import { changedFileSignature } from "./reviewItemBuilders";

interface CloudReviewPageProps {
Expand All @@ -36,9 +35,16 @@ export function CloudReviewPage({ task }: CloudReviewPageProps) {
toolCalls,
isLoading,
} = useCloudChangedFiles(taskId, task, isReviewOpen);
const { commentThreads } = usePrDetails(prUrl, {
const { commentThreads, commentsLoading } = usePrDetails(prUrl, {
includeComments: isReviewOpen && showReviewComments,
});
const commentedFilePaths = useMemo(
() =>
prUrl && !commentsLoading
? getCommentedFilePaths(commentThreads)
: undefined,
[commentThreads, commentsLoading, prUrl],
);

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

Expand Down Expand Up @@ -83,6 +89,9 @@ export function CloudReviewPage({ task }: CloudReviewPageProps) {
return {
key: file.path,
scrollKey: file.path,
filePaths: [file.path, file.originalPath].filter(
(path): path is string => !!path,
),
node: (
<PatchedFileDiff
file={file}
Expand Down Expand Up @@ -111,8 +120,6 @@ export function CloudReviewPage({ task }: CloudReviewPageProps) {
toolCallFallbacks,
]);

const itemIndexByFilePath = useMemo(() => buildItemIndex(items), [items]);

if (!prUrl && !effectiveBranch && reviewFiles.length === 0) {
if (isRunActive) {
return (
Expand Down Expand Up @@ -146,7 +153,8 @@ export function CloudReviewPage({ task }: CloudReviewPageProps) {
onUncollapseFile={uncollapseFile}
onCollapseFiles={collapseFiles}
items={items}
itemIndexByFilePath={itemIndexByFilePath}
commentedFilePaths={commentedFilePaths?.all}
unresolvedCommentedFilePaths={commentedFilePaths?.unresolved}
currentSignatures={currentSignatures}
viewedRecord={viewedRecord}
onToggleViewed={toggleViewed}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
} from "@posthog/quill";
import type { CommentFileFilter } from "../commentFileFilter";

interface CommentFilterSubmenuProps {
commentedFileCount: number;
unresolvedCommentedFileCount: number;
commentFilter: CommentFileFilter;
onCommentFilterChange: (filter: CommentFileFilter) => void;
}

function getCommentFilterSuffix(commentFilter: CommentFileFilter): string {
switch (commentFilter) {
case "commented":
return " · All";
case "unresolved":
return " · Unresolved";
case "none":
return "";
}
}

export function CommentFilterSubmenu({
commentedFileCount,
unresolvedCommentedFileCount,
commentFilter,
onCommentFilterChange,
}: CommentFilterSubmenuProps) {
return (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
Comment filter{getCommentFilterSuffix(commentFilter)}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent side="right" sideOffset={4}>
<DropdownMenuRadioGroup
value={commentFilter === "none" ? "" : commentFilter}
onValueChange={(value) =>
onCommentFilterChange(value as CommentFileFilter)
}
>
<DropdownMenuRadioItem value="commented">
All comments ({commentedFileCount})
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="unresolved">
Unresolved comments ({unresolvedCommentedFileCount})
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
{commentFilter !== "none" && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onCommentFilterChange("none")}>
Clear comment filter
</DropdownMenuItem>
</>
)}
</DropdownMenuSubContent>
</DropdownMenuSub>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,22 @@ import {
DropdownMenuTrigger,
} from "@posthog/quill";
import { useDiffViewerStore } from "@posthog/ui/features/code-editor/diffViewerStore";
import type { CommentFileFilter } from "../commentFileFilter";
import { CommentFilterSubmenu } from "./CommentFilterSubmenu";

export function DiffSettingsMenu() {
interface DiffSettingsMenuProps {
commentedFileCount: number;
unresolvedCommentedFileCount: number;
commentFilter: CommentFileFilter;
onCommentFilterChange?: (filter: CommentFileFilter) => void;
}

export function DiffSettingsMenu({
commentedFileCount,
unresolvedCommentedFileCount,
commentFilter,
onCommentFilterChange,
}: DiffSettingsMenuProps) {
const wordWrap = useDiffViewerStore((s) => s.wordWrap);
const toggleWordWrap = useDiffViewerStore((s) => s.toggleWordWrap);
const wordDiffs = useDiffViewerStore((s) => s.wordDiffs);
Expand All @@ -24,14 +38,25 @@ export function DiffSettingsMenu() {
const toggleShowReviewComments = useDiffViewerStore(
(s) => s.toggleShowReviewComments,
);
const handleToggleReviewComments = () => {
if (showReviewComments && commentFilter !== "none") {
onCommentFilterChange?.("none");
}
toggleShowReviewComments();
};

return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
size="icon-sm"
aria-label="Diff settings"
variant={commentFilter === "none" ? "default" : "primary"}
aria-label={
commentFilter === "none"
? "Diff settings"
: `Diff settings, ${commentFilter} comment filter active`
}
className="rounded-xs"
>
<DotsThree size={16} weight="bold" />
Expand All @@ -54,9 +79,17 @@ export function DiffSettingsMenu() {
{hideWhitespaceChanges ? "Show whitespace" : "Hide whitespace"}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={toggleShowReviewComments}>
<DropdownMenuItem onClick={handleToggleReviewComments}>
{showReviewComments ? "Hide review comments" : "Show review comments"}
</DropdownMenuItem>
{showReviewComments && onCommentFilterChange && (
<CommentFilterSubmenu
commentedFileCount={commentedFileCount}
unresolvedCommentedFileCount={unresolvedCommentedFileCount}
commentFilter={commentFilter}
onCommentFilterChange={onCommentFilterChange}
/>
)}
</DropdownMenuContent>
</DropdownMenu>
);
Expand Down
Loading
Loading