diff --git a/packages/ui/src/features/code-review/commentFileFilter.ts b/packages/ui/src/features/code-review/commentFileFilter.ts new file mode 100644 index 0000000000..4ea3a8f0a5 --- /dev/null +++ b/packages/ui/src/features/code-review/commentFileFilter.ts @@ -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; + unresolvedCommentedFilePaths?: ReadonlySet; +} + +export function getCommentedFilePaths(threads: Map): { + all: Set; + unresolved: Set; +} { + const all = new Set(); + const unresolved = new Set(); + + 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, +): 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"; + } +} diff --git a/packages/ui/src/features/code-review/components/CloudReviewPage.tsx b/packages/ui/src/features/code-review/components/CloudReviewPage.tsx index fc45568bd8..db6e01f571 100644 --- a/packages/ui/src/features/code-review/components/CloudReviewPage.tsx +++ b/packages/ui/src/features/code-review/components/CloudReviewPage.tsx @@ -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 { @@ -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]); @@ -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: ( buildItemIndex(items), [items]); - if (!prUrl && !effectiveBranch && reviewFiles.length === 0) { if (isRunActive) { return ( @@ -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} diff --git a/packages/ui/src/features/code-review/components/CommentFilterSubmenu.tsx b/packages/ui/src/features/code-review/components/CommentFilterSubmenu.tsx new file mode 100644 index 0000000000..3e64cad8cd --- /dev/null +++ b/packages/ui/src/features/code-review/components/CommentFilterSubmenu.tsx @@ -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 ( + + + Comment filter{getCommentFilterSuffix(commentFilter)} + + + + onCommentFilterChange(value as CommentFileFilter) + } + > + + All comments ({commentedFileCount}) + + + Unresolved comments ({unresolvedCommentedFileCount}) + + + {commentFilter !== "none" && ( + <> + + onCommentFilterChange("none")}> + Clear comment filter + + + )} + + + ); +} diff --git a/packages/ui/src/features/code-review/components/DiffSettingsMenu.tsx b/packages/ui/src/features/code-review/components/DiffSettingsMenu.tsx index 3fca6ae674..d4eb4f2590 100644 --- a/packages/ui/src/features/code-review/components/DiffSettingsMenu.tsx +++ b/packages/ui/src/features/code-review/components/DiffSettingsMenu.tsx @@ -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); @@ -24,6 +38,12 @@ export function DiffSettingsMenu() { const toggleShowReviewComments = useDiffViewerStore( (s) => s.toggleShowReviewComments, ); + const handleToggleReviewComments = () => { + if (showReviewComments && commentFilter !== "none") { + onCommentFilterChange?.("none"); + } + toggleShowReviewComments(); + }; return ( @@ -31,7 +51,12 @@ export function DiffSettingsMenu() { render={