Repository navigation
feat(web): hide viewed files in the pull request Code tab #31163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5980384
aa788a2
88df585
26e2807
8090fe6
eacefe9
b6d06b1
6a218e4
775f8f2
78c7f0f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ import { | |
| ChevronDownIcon, | ||
| ChevronRightIcon, | ||
| Columns2Icon, | ||
| EyeOffIcon, | ||
| FlaskConicalOffIcon, | ||
| FolderTreeIcon, | ||
| InfoIcon, | ||
|
|
@@ -149,6 +150,7 @@ type ReviewAnnotation = DiffLineAnnotation<ReviewAnnotationGroup>; | |
| const COMMIT_PAGE_SIZE = 10; | ||
|
|
||
| const PULL_REQUEST_FILE_TREE_STORAGE_KEY = "t3code.pullRequestFileTreeOpen"; | ||
| const PULL_REQUEST_HIDE_VIEWED_FILES_STORAGE_KEY = "t3code.pullRequestHideViewedFiles"; | ||
|
|
||
| interface DiffSlice { | ||
| readonly source: PullRequestDiffResult; | ||
|
|
@@ -401,6 +403,11 @@ function PullRequestCodeTab({ | |
| Schema.Boolean, | ||
| ); | ||
| const [hideTestFiles, setHideTestFiles] = useHideTestFiles(); | ||
| const [hideViewedFiles, setHideViewedFiles] = useLocalStorage( | ||
| PULL_REQUEST_HIDE_VIEWED_FILES_STORAGE_KEY, | ||
| false, | ||
| Schema.Boolean, | ||
| ); | ||
| const [selectedLines, setSelectedLines] = useState<{ | ||
| id: string; | ||
| range: SelectedLineRange; | ||
|
|
@@ -603,15 +610,28 @@ function PullRequestCodeTab({ | |
| const testFileCount = loadedFiles.filter((file) => | ||
| isTestChangedFile(resolveFileDiffPath(file)), | ||
| ).length; | ||
| // A requested file stays visible so a jump to a hidden test still lands. | ||
| const files = hideTestFiles | ||
| // A filter never hides the file being jumped to or one holding unsent review comments. | ||
| const isPinned = (path: string) => | ||
| path === revealRequest?.path || | ||
| path === linkPath || | ||
| path === draft?.path || | ||
| pendingComments.some((comment) => comment.path === path); | ||
| const reviewFiles = hideTestFiles | ||
| ? loadedFiles.filter((file) => { | ||
| const path = resolveFileDiffPath(file); | ||
| return path === revealRequest?.path || path === linkPath || !isTestChangedFile(path); | ||
| return isPinned(path) || !isTestChangedFile(path); | ||
| }) | ||
| : loadedFiles; | ||
| const hiddenTestFileCount = loadedFiles.length - files.length; | ||
| const filePaths = files.map((file) => resolveFileDiffPath(file)); | ||
| const hiddenTestFileCount = loadedFiles.length - reviewFiles.length; | ||
| const viewedFileCount = reviewFiles.filter((file) => | ||
| isFileViewed(resolveFileDiffPath(file)), | ||
| ).length; | ||
| const files = hideViewedFiles | ||
| ? reviewFiles.filter((file) => { | ||
| const path = resolveFileDiffPath(file); | ||
| return isPinned(path) || !isFileViewed(path); | ||
|
Comment on lines
+629
to
+632
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve open thread replies and edits. If a user enables this filter while composing a reply on an expanded viewed file, the file is removed and its
|
||
| }) | ||
| : reviewFiles; | ||
| const omittedFileStats = new Map( | ||
| loadedSlices.flatMap((slice) => | ||
| slice.omittedFileStats.map((file) => [file.path, file] as const), | ||
|
|
@@ -805,11 +825,12 @@ function PullRequestCodeTab({ | |
| }; | ||
|
|
||
| const setFileViewed = (fileKey: string, viewed: boolean, extendRange: boolean) => { | ||
| const range = getFileViewedRange( | ||
| annotatedFiles, | ||
| fileKey, | ||
| extendRange ? lastViewedFileKey : null, | ||
| ); | ||
| // Include files hidden as viewed: the Shift-click anchor is usually one of them. | ||
| const rangeFiles = reviewFiles.map((file) => ({ | ||
| fileKey: buildFileDiffRenderKey(file), | ||
| path: resolveFileDiffPath(file), | ||
| })); | ||
| const range = getFileViewedRange(rangeFiles, fileKey, extendRange ? lastViewedFileKey : null); | ||
| setLastViewedFileKey(fileKey); | ||
| for (const file of range) setViewed(file.path, viewed); | ||
| setFoldChoices((current) => | ||
|
|
@@ -1190,10 +1211,10 @@ function PullRequestCodeTab({ | |
| {hiddenTestFileCount} test {hiddenTestFileCount === 1 ? "file" : "files"} hidden | ||
| </Button> | ||
| ) : null} | ||
| {filesViewed.enabled && files.length > 0 ? ( | ||
| {filesViewed.enabled && reviewFiles.length > 0 ? ( | ||
| <span className="flex min-w-0 items-center gap-1 tabular-nums"> | ||
| <span className="shrink-0"> | ||
| {filePaths.filter(isFileViewed).length} / {files.length} | ||
| {viewedFileCount} / {reviewFiles.length} | ||
| </span> | ||
| <span className="truncate"> | ||
| {viewedFilesStore === "environment" ? `viewed in ${APP_BASE_NAME}` : "viewed"} | ||
|
|
@@ -1423,6 +1444,26 @@ function PullRequestCodeTab({ | |
| </TooltipPopup> | ||
| </Tooltip> | ||
| ) : null} | ||
| {viewedFileCount > 0 || hideViewedFiles ? ( | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| <Tooltip> | ||
| <TooltipTrigger | ||
| render={ | ||
| <Toggle | ||
| aria-label="Hide viewed files" | ||
| variant="ghost" | ||
| size="sm" | ||
| pressed={hideViewedFiles} | ||
| onPressedChange={(pressed) => setHideViewedFiles(Boolean(pressed))} | ||
| /> | ||
| } | ||
| > | ||
| <EyeOffIcon className="size-3.5" /> | ||
| </TooltipTrigger> | ||
| <TooltipPopup side="top"> | ||
| {hideViewedFiles ? "Show viewed files" : "Hide viewed files"} | ||
| </TooltipPopup> | ||
| </Tooltip> | ||
| ) : null} | ||
| {fileKeys.length > 0 ? ( | ||
| <Tooltip> | ||
| <TooltipTrigger | ||
|
|
@@ -1539,78 +1580,83 @@ function PullRequestCodeTab({ | |
| return withToolbar(rawSlicesView); | ||
| } | ||
|
|
||
| if (items.length === 0 && nextCursor === null) { | ||
| return withToolbar( | ||
| <p className="px-4 py-5 text-sm text-muted-foreground"> | ||
| {commit === null ? `This ${term} has no file changes.` : "This commit has no file changes."} | ||
| </p>, | ||
| ); | ||
| } | ||
|
|
||
| const orphanThreads = detail.reviewThreads.filter((thread) => !placedThreadIds.has(thread.id)); | ||
| const orphanFiles = new Map<string, PullRequestReviewThread[]>(); | ||
| for (const thread of orphanThreads) { | ||
| const existing = orphanFiles.get(thread.path); | ||
| if (existing) existing.push(thread); | ||
| else orphanFiles.set(thread.path, [thread]); | ||
| } | ||
| const orphanConversations = | ||
| orphanFiles.size > 0 ? ( | ||
| <div className="shrink-0 border-b border-border/60"> | ||
| <Collapsible open={orphansOpen} onOpenChange={setOrphansOpen}> | ||
| <h2> | ||
| <CollapsibleTrigger className="flex w-full items-center gap-1.5 px-4 py-2 text-left text-xs text-muted-foreground"> | ||
| <span> | ||
| {nextCursor === null | ||
| ? "Conversations not on the current diff" | ||
| : "Conversations not on the diff loaded so far"} | ||
| </span> | ||
| <ChevronRightIcon | ||
| aria-hidden | ||
| className={cn("size-3.5 transition-transform", orphansOpen && "rotate-90")} | ||
| /> | ||
| <span aria-hidden className="tabular-nums"> | ||
| {orphanThreads.length} | ||
| </span> | ||
| <span className="sr-only"> | ||
| {orphanThreads.length === 1 | ||
| ? "1 conversation" | ||
| : `${orphanThreads.length} conversations`} | ||
| </span> | ||
| </CollapsibleTrigger> | ||
| </h2> | ||
| <CollapsiblePanel> | ||
| <div className="max-h-64 space-y-1 overflow-auto px-1 pb-1"> | ||
| {[...orphanFiles].map(([path, threads]) => ( | ||
| <div key={path}> | ||
| <Tooltip> | ||
| <TooltipTrigger | ||
| render={<p className="truncate px-3 text-xs text-muted-foreground">{path}</p>} | ||
| /> | ||
| <TooltipPopup side="top">{path}</TooltipPopup> | ||
| </Tooltip> | ||
| <div className="mt-1 space-y-2"> | ||
| {threads.map((thread) => ( | ||
| <div key={thread.id}> | ||
| {thread.line === null ? null : ( | ||
| <p className="px-3 text-xs text-muted-foreground">Line {thread.line}</p> | ||
| )} | ||
| {renderThreadCard(thread)} | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </CollapsiblePanel> | ||
| </Collapsible> | ||
| </div> | ||
| ) : null; | ||
|
|
||
| if (items.length === 0 && nextCursor === null) { | ||
| const noFilesMessage = | ||
| commit === null ? `This ${term} has no file changes.` : "This commit has no file changes."; | ||
| return withToolbar( | ||
| <> | ||
| {orphanConversations} | ||
| <p className="px-4 py-5 text-sm text-muted-foreground"> | ||
| {loadedFiles.length > 0 ? "All files are hidden." : noFilesMessage} | ||
| </p> | ||
| </>, | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="flex h-full min-h-0 flex-col"> | ||
| {toolbar} | ||
| {orphanFiles.size > 0 ? ( | ||
| <div className="shrink-0 border-b border-border/60"> | ||
| <Collapsible open={orphansOpen} onOpenChange={setOrphansOpen}> | ||
| <h2> | ||
| <CollapsibleTrigger className="flex w-full items-center gap-1.5 px-4 py-2 text-left text-xs text-muted-foreground"> | ||
| <span> | ||
| {nextCursor === null | ||
| ? "Conversations not on the current diff" | ||
| : "Conversations not on the diff loaded so far"} | ||
| </span> | ||
| <ChevronRightIcon | ||
| aria-hidden | ||
| className={cn("size-3.5 transition-transform", orphansOpen && "rotate-90")} | ||
| /> | ||
| <span aria-hidden className="tabular-nums"> | ||
| {orphanThreads.length} | ||
| </span> | ||
| <span className="sr-only"> | ||
| {orphanThreads.length === 1 | ||
| ? "1 conversation" | ||
| : `${orphanThreads.length} conversations`} | ||
| </span> | ||
| </CollapsibleTrigger> | ||
| </h2> | ||
| <CollapsiblePanel> | ||
| <div className="max-h-64 space-y-1 overflow-auto px-1 pb-1"> | ||
| {[...orphanFiles].map(([path, threads]) => ( | ||
| <div key={path}> | ||
| <Tooltip> | ||
| <TooltipTrigger | ||
| render={ | ||
| <p className="truncate px-3 text-xs text-muted-foreground">{path}</p> | ||
| } | ||
| /> | ||
| <TooltipPopup side="top">{path}</TooltipPopup> | ||
| </Tooltip> | ||
| <div className="mt-1 space-y-2"> | ||
| {threads.map((thread) => ( | ||
| <div key={thread.id}> | ||
| {thread.line === null ? null : ( | ||
| <p className="px-3 text-xs text-muted-foreground">Line {thread.line}</p> | ||
| )} | ||
| {renderThreadCard(thread)} | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </CollapsiblePanel> | ||
| </Collapsible> | ||
| </div> | ||
| ) : null} | ||
| {orphanConversations} | ||
| <div className="flex min-h-0 flex-1 overflow-hidden"> | ||
| <div | ||
| className="relative min-h-0 min-w-0 flex-1" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium
pullRequest/PullRequestCodeTab.tsx:591With Hide viewed files enabled, Shift-clicking a later file marks only that file viewed instead of the full range. Marking the anchor viewed removes it from
annotatedFiles, sogetFileViewedRangecannot findlastViewedFileKey; calculate the range from the list before the viewed-file filter, while retaining the test-file filter.🤖 Copy this AI Prompt to have your agent fix this:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in aa788a2. The range is now found among the files left after the test filter, so a hidden anchor still counts. Covered by a new browser test that fails without the fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I'm unable to act on this request because you do not have permissions within this repository.