fix(web): Change buttons into Links in various places (#532)

This commit is contained in:
Brendan Kellam 2025-09-21 15:20:27 -07:00 committed by GitHub
parent ef46c0181d
commit a698afdf13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 124 additions and 132 deletions

View file

@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved repository query performance by adding db indices. [#526](https://github.com/sourcebot-dev/sourcebot/pull/526) - Improved repository query performance by adding db indices. [#526](https://github.com/sourcebot-dev/sourcebot/pull/526)
- Improved repository query performance by removing JOIN on `Connection` table. [#527](https://github.com/sourcebot-dev/sourcebot/pull/527) - Improved repository query performance by removing JOIN on `Connection` table. [#527](https://github.com/sourcebot-dev/sourcebot/pull/527)
- Changed repo carousel and repo list links to redirect to the file browser. [#528](https://github.com/sourcebot-dev/sourcebot/pull/528) - Changed repo carousel and repo list links to redirect to the file browser. [#528](https://github.com/sourcebot-dev/sourcebot/pull/528)
- Changed file headers, files/directories in file tree, and reference list buttons into links. [#532](https://github.com/sourcebot-dev/sourcebot/pull/532)
## [4.7.1] - 2025-09-19 ## [4.7.1] - 2025-09-19

View file

@ -1,11 +1,12 @@
'use client'; 'use client';
import { useCallback, useRef } from "react"; import { useRef } from "react";
import { FileTreeItem } from "@/features/fileTree/actions"; import { FileTreeItem } from "@/features/fileTree/actions";
import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeItemComponent"; import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeItemComponent";
import { useBrowseNavigation } from "../../hooks/useBrowseNavigation"; import { getBrowsePath } from "../../hooks/useBrowseNavigation";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { useBrowseParams } from "../../hooks/useBrowseParams"; import { useBrowseParams } from "../../hooks/useBrowseParams";
import { useDomain } from "@/hooks/useDomain";
interface PureTreePreviewPanelProps { interface PureTreePreviewPanelProps {
items: FileTreeItem[]; items: FileTreeItem[];
@ -13,17 +14,8 @@ interface PureTreePreviewPanelProps {
export const PureTreePreviewPanel = ({ items }: PureTreePreviewPanelProps) => { export const PureTreePreviewPanel = ({ items }: PureTreePreviewPanelProps) => {
const { repoName, revisionName } = useBrowseParams(); const { repoName, revisionName } = useBrowseParams();
const { navigateToPath } = useBrowseNavigation();
const scrollAreaRef = useRef<HTMLDivElement>(null); const scrollAreaRef = useRef<HTMLDivElement>(null);
const domain = useDomain();
const onNodeClicked = useCallback((node: FileTreeItem) => {
navigateToPath({
repoName: repoName,
revisionName: revisionName,
path: node.path,
pathType: node.type === 'tree' ? 'tree' : 'blob',
});
}, [navigateToPath, repoName, revisionName]);
return ( return (
<ScrollArea <ScrollArea
@ -37,8 +29,14 @@ export const PureTreePreviewPanel = ({ items }: PureTreePreviewPanelProps) => {
isActive={false} isActive={false}
depth={0} depth={0}
isCollapseChevronVisible={false} isCollapseChevronVisible={false}
onClick={() => onNodeClicked(item)}
parentRef={scrollAreaRef} parentRef={scrollAreaRef}
href={getBrowsePath({
repoName,
revisionName,
path: item.path,
pathType: item.type === 'tree' ? 'tree' : 'blob',
domain,
})}
/> />
))} ))}
</ScrollArea> </ScrollArea>

View file

@ -3,7 +3,7 @@
import { cn, getCodeHostInfoForRepo } from "@/lib/utils"; import { cn, getCodeHostInfoForRepo } from "@/lib/utils";
import { LaptopIcon } from "@radix-ui/react-icons"; import { LaptopIcon } from "@radix-ui/react-icons";
import Image from "next/image"; import Image from "next/image";
import { useBrowseNavigation } from "../browse/hooks/useBrowseNavigation"; import { getBrowsePath } from "../browse/hooks/useBrowseNavigation";
import { ChevronRight, MoreHorizontal } from "lucide-react"; import { ChevronRight, MoreHorizontal } from "lucide-react";
import { useCallback, useState, useMemo, useRef, useEffect } from "react"; import { useCallback, useState, useMemo, useRef, useEffect } from "react";
import { useToast } from "@/components/hooks/use-toast"; import { useToast } from "@/components/hooks/use-toast";
@ -15,6 +15,8 @@ import {
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon"; import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
import { CopyIconButton } from "./copyIconButton"; import { CopyIconButton } from "./copyIconButton";
import Link from "next/link";
import { useDomain } from "@/hooks/useDomain";
interface FileHeaderProps { interface FileHeaderProps {
path: string; path: string;
@ -64,11 +66,11 @@ export const PathHeader = ({
webUrl: repo.webUrl, webUrl: repo.webUrl,
}); });
const { navigateToPath } = useBrowseNavigation();
const { toast } = useToast(); const { toast } = useToast();
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const breadcrumbsRef = useRef<HTMLDivElement>(null); const breadcrumbsRef = useRef<HTMLDivElement>(null);
const [visibleSegmentCount, setVisibleSegmentCount] = useState<number | null>(null); const [visibleSegmentCount, setVisibleSegmentCount] = useState<number | null>(null);
const domain = useDomain();
// Create breadcrumb segments from file path // Create breadcrumb segments from file path
const breadcrumbSegments = useMemo(() => { const breadcrumbSegments = useMemo(() => {
@ -179,16 +181,6 @@ export const PathHeader = ({
return true; return true;
}, [path, toast]); }, [path, toast]);
const onBreadcrumbClick = useCallback((segment: BreadcrumbSegment) => {
navigateToPath({
repoName: repo.name,
path: segment.fullPath,
pathType: segment.isLastSegment ? pathType : 'tree',
revisionName: branchDisplayName,
});
}, [repo.name, branchDisplayName, navigateToPath, pathType]);
const renderSegmentWithHighlight = (segment: BreadcrumbSegment) => { const renderSegmentWithHighlight = (segment: BreadcrumbSegment) => {
if (!segment.highlightRange) { if (!segment.highlightRange) {
return segment.name; return segment.name;
@ -224,17 +216,18 @@ export const PathHeader = ({
</> </>
)} )}
<div <Link
className={cn("font-medium cursor-pointer hover:underline", repoNameClassName)} className={cn("font-medium cursor-pointer hover:underline", repoNameClassName)}
onClick={() => navigateToPath({ href={getBrowsePath({
repoName: repo.name, repoName: repo.name,
path: '', path: '/',
pathType: 'tree', pathType: 'tree',
revisionName: branchDisplayName, revisionName: branchDisplayName,
domain,
})} })}
> >
{info?.displayName} {info?.displayName}
</div> </Link>
{branchDisplayName && ( {branchDisplayName && (
<p <p
className="text-xs font-semibold text-gray-500 dark:text-gray-400 mt-[3px] flex items-center gap-0.5" className="text-xs font-semibold text-gray-500 dark:text-gray-400 mt-[3px] flex items-center gap-0.5"
@ -263,13 +256,21 @@ export const PathHeader = ({
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="min-w-[200px]"> <DropdownMenuContent align="start" className="min-w-[200px]">
{hiddenSegments.map((segment) => ( {hiddenSegments.map((segment) => (
<DropdownMenuItem <Link
href={getBrowsePath({
repoName: repo.name,
path: segment.fullPath,
pathType: segment.isLastSegment ? pathType : 'tree',
revisionName: branchDisplayName,
domain,
})}
className="font-mono text-sm hover:cursor cursor-pointer"
key={segment.fullPath} key={segment.fullPath}
onClick={() => onBreadcrumbClick(segment)}
className="font-mono text-sm cursor-pointer"
> >
{renderSegmentWithHighlight(segment)} <DropdownMenuItem className="hover:cursor cursor-pointer">
</DropdownMenuItem> {renderSegmentWithHighlight(segment)}
</DropdownMenuItem>
</Link>
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@ -281,14 +282,20 @@ export const PathHeader = ({
{(isFileIconVisible && index === visibleSegments.length - 1) && ( {(isFileIconVisible && index === visibleSegments.length - 1) && (
<VscodeFileIcon fileName={segment.name} className="h-4 w-4 mr-1" /> <VscodeFileIcon fileName={segment.name} className="h-4 w-4 mr-1" />
)} )}
<span <Link
className={cn( className={cn(
"font-mono text-sm truncate cursor-pointer hover:underline", "font-mono text-sm truncate cursor-pointer hover:underline",
)} )}
onClick={() => onBreadcrumbClick(segment)} href={getBrowsePath({
repoName: repo.name,
path: segment.fullPath,
pathType: segment.isLastSegment ? pathType : 'tree',
revisionName: branchDisplayName,
domain,
})}
> >
{renderSegmentWithHighlight(segment)} {renderSegmentWithHighlight(segment)}
</span> </Link>
{index < visibleSegments.length - 1 && ( {index < visibleSegments.length - 1 && (
<ChevronRight className="h-3 w-3 mx-0.5 text-muted-foreground flex-shrink-0" /> <ChevronRight className="h-3 w-3 mx-0.5 text-muted-foreground flex-shrink-0" />
)} )}

View file

@ -1,27 +1,22 @@
'use client'; 'use client';
import { useCallback } from "react";
import { SearchResultFile, SearchResultChunk } from "@/features/search/types"; import { SearchResultFile, SearchResultChunk } from "@/features/search/types";
import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter"; import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter";
import Link from "next/link";
import { getBrowsePath } from "@/app/[domain]/browse/hooks/useBrowseNavigation";
import { useDomain } from "@/hooks/useDomain";
interface FileMatchProps { interface FileMatchProps {
match: SearchResultChunk; match: SearchResultChunk;
file: SearchResultFile; file: SearchResultFile;
onOpen: (startLineNumber: number, endLineNumber: number, isCtrlKeyPressed: boolean) => void;
} }
export const FileMatch = ({ export const FileMatch = ({
match, match,
file, file,
onOpen: _onOpen,
}: FileMatchProps) => { }: FileMatchProps) => {
const onOpen = useCallback((isCtrlKeyPressed: boolean) => { const domain = useDomain();
const startLineNumber = match.contentStart.lineNumber;
const endLineNumber = match.content.trimEnd().split('\n').length + startLineNumber - 1;
_onOpen(startLineNumber, endLineNumber, isCtrlKeyPressed);
}, [match.content, match.contentStart.lineNumber, _onOpen]);
// If it's just the title, don't show a code preview // If it's just the title, don't show a code preview
if (match.matchRanges.length === 0) { if (match.matchRanges.length === 0) {
@ -29,19 +24,24 @@ export const FileMatch = ({
} }
return ( return (
<div <Link
tabIndex={0} tabIndex={0}
className="cursor-pointer focus:ring-inset focus:ring-4 bg-background hover:bg-editor-lineHighlight" className="cursor-pointer focus:ring-inset focus:ring-4 bg-background hover:bg-editor-lineHighlight"
onKeyDown={(e) => { href={getBrowsePath({
if (e.key !== "Enter") { repoName: file.repository,
return; revisionName: file.branches?.[0] ?? 'HEAD',
path: file.fileName.text,
pathType: 'blob',
domain,
highlightRange: {
start: {
lineNumber: match.contentStart.lineNumber,
},
end: {
lineNumber: match.content.trimEnd().split('\n').length + match.contentStart.lineNumber - 1,
}
} }
})}
onOpen(e.metaKey || e.ctrlKey);
}}
onClick={(e) => {
onOpen(e.metaKey || e.ctrlKey);
}}
title="open file: click, open file preview: cmd/ctrl + click" title="open file: click, open file preview: cmd/ctrl + click"
> >
<LightweightCodeHighlighter <LightweightCodeHighlighter
@ -53,6 +53,6 @@ export const FileMatch = ({
> >
{match.content} {match.content}
</LightweightCodeHighlighter> </LightweightCodeHighlighter>
</div> </Link>
); );
} }

View file

@ -7,7 +7,6 @@ import { useMemo } from "react";
import { FileMatch } from "./fileMatch"; import { FileMatch } from "./fileMatch";
import { RepositoryInfo, SearchResultFile } from "@/features/search/types"; import { RepositoryInfo, SearchResultFile } from "@/features/search/types";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation";
export const MAX_MATCHES_TO_PREVIEW = 3; export const MAX_MATCHES_TO_PREVIEW = 3;
@ -33,7 +32,6 @@ export const FileMatchContainer = ({
const matchCount = useMemo(() => { const matchCount = useMemo(() => {
return file.chunks.length; return file.chunks.length;
}, [file]); }, [file]);
const { navigateToPath } = useBrowseNavigation();
const matches = useMemo(() => { const matches = useMemo(() => {
const sortedMatches = file.chunks.sort((a, b) => { const sortedMatches = file.chunks.sort((a, b) => {
@ -123,29 +121,6 @@ export const FileMatchContainer = ({
<FileMatch <FileMatch
match={match} match={match}
file={file} file={file}
onOpen={(startLineNumber, endLineNumber, isCtrlKeyPressed) => {
if (isCtrlKeyPressed) {
const matchIndex = matches.slice(0, index).reduce((acc, match) => {
return acc + match.matchRanges.length;
}, 0);
onOpenFilePreview(matchIndex);
} else {
navigateToPath({
repoName: file.repository,
revisionName: file.branches?.[0] ?? 'HEAD',
path: file.fileName.text,
pathType: 'blob',
highlightRange: {
start: {
lineNumber: startLineNumber,
},
end: {
lineNumber: endLineNumber,
}
}
});
}
}}
/> />
{(index !== matches.length - 1 || isMoreContentButtonVisible) && ( {(index !== matches.length - 1 || isMoreContentButtonVisible) && (
<Separator className="bg-accent" /> <Separator className="bg-accent" />

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation"; import { getBrowsePath } from "@/app/[domain]/browse/hooks/useBrowseNavigation";
import { PathHeader } from "@/app/[domain]/components/pathHeader"; import { PathHeader } from "@/app/[domain]/components/pathHeader";
import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter"; import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter";
import { FindRelatedSymbolsResponse } from "@/features/codeNav/types"; import { FindRelatedSymbolsResponse } from "@/features/codeNav/types";
@ -8,6 +8,8 @@ import { RepositoryInfo, SourceRange } from "@/features/search/types";
import { useMemo, useRef } from "react"; import { useMemo, useRef } from "react";
import useCaptureEvent from "@/hooks/useCaptureEvent"; import useCaptureEvent from "@/hooks/useCaptureEvent";
import { useVirtualizer } from "@tanstack/react-virtual"; import { useVirtualizer } from "@tanstack/react-virtual";
import Link from "next/link";
import { useDomain } from "@/hooks/useDomain";
interface ReferenceListProps { interface ReferenceListProps {
data: FindRelatedSymbolsResponse; data: FindRelatedSymbolsResponse;
@ -21,6 +23,7 @@ export const ReferenceList = ({
data, data,
revisionName, revisionName,
}: ReferenceListProps) => { }: ReferenceListProps) => {
const domain = useDomain();
const repoInfoMap = useMemo(() => { const repoInfoMap = useMemo(() => {
return data.repositoryInfo.reduce((acc, repo) => { return data.repositoryInfo.reduce((acc, repo) => {
acc[repo.id] = repo; acc[repo.id] = repo;
@ -28,7 +31,6 @@ export const ReferenceList = ({
}, {} as Record<number, RepositoryInfo>); }, {} as Record<number, RepositoryInfo>);
}, [data.repositoryInfo]); }, [data.repositoryInfo]);
const { navigateToPath } = useBrowseNavigation();
const captureEvent = useCaptureEvent(); const captureEvent = useCaptureEvent();
// Virtualization setup // Virtualization setup
@ -103,22 +105,26 @@ export const ReferenceList = ({
{file.matches {file.matches
.sort((a, b) => a.range.start.lineNumber - b.range.start.lineNumber) .sort((a, b) => a.range.start.lineNumber - b.range.start.lineNumber)
.map((match, index) => ( .map((match, index) => (
<ReferenceListItem <Link
key={index} href={getBrowsePath({
lineContent={match.lineContent} repoName: file.repository,
range={match.range} revisionName,
language={file.language} path: file.fileName,
pathType: 'blob',
highlightRange: match.range,
domain,
})}
onClick={() => { onClick={() => {
captureEvent('wa_explore_menu_reference_clicked', {}); captureEvent('wa_explore_menu_reference_clicked', {});
navigateToPath({
repoName: file.repository,
revisionName,
path: file.fileName,
pathType: 'blob',
highlightRange: match.range,
})
}} }}
/> key={index}
>
<ReferenceListItem
lineContent={match.lineContent}
range={match.range}
language={file.language}
/>
</Link>
))} ))}
</div> </div>
</div> </div>
@ -134,21 +140,18 @@ interface ReferenceListItemProps {
lineContent: string; lineContent: string;
range: SourceRange; range: SourceRange;
language: string; language: string;
onClick: () => void;
} }
const ReferenceListItem = ({ const ReferenceListItem = ({
lineContent, lineContent,
range, range,
language, language,
onClick,
}: ReferenceListItemProps) => { }: ReferenceListItemProps) => {
const highlightRanges = useMemo(() => [range], [range]); const highlightRanges = useMemo(() => [range], [range]);
return ( return (
<div <div
className="w-full hover:bg-accent py-1 cursor-pointer" className="w-full hover:bg-accent py-1 cursor-pointer"
onClick={onClick}
> >
<LightweightCodeHighlighter <LightweightCodeHighlighter
language={language} language={language}

View file

@ -6,6 +6,7 @@ import clsx from "clsx";
import scrollIntoView from 'scroll-into-view-if-needed'; import scrollIntoView from 'scroll-into-view-if-needed';
import { ChevronDownIcon, ChevronRightIcon } from "@radix-ui/react-icons"; import { ChevronDownIcon, ChevronRightIcon } from "@radix-ui/react-icons";
import { FileTreeItemIcon } from "./fileTreeItemIcon"; import { FileTreeItemIcon } from "./fileTreeItemIcon";
import Link from "next/link";
export const FileTreeItemComponent = ({ export const FileTreeItemComponent = ({
node, node,
@ -13,7 +14,9 @@ export const FileTreeItemComponent = ({
depth, depth,
isCollapsed = false, isCollapsed = false,
isCollapseChevronVisible = true, isCollapseChevronVisible = true,
href,
onClick, onClick,
onNavigate,
parentRef, parentRef,
}: { }: {
node: FileTreeItem, node: FileTreeItem,
@ -21,10 +24,12 @@ export const FileTreeItemComponent = ({
depth: number, depth: number,
isCollapsed?: boolean, isCollapsed?: boolean,
isCollapseChevronVisible?: boolean, isCollapseChevronVisible?: boolean,
onClick: () => void, href: string,
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void,
onNavigate?: (e: { preventDefault: () => void }) => void,
parentRef: React.RefObject<HTMLDivElement | null>, parentRef: React.RefObject<HTMLDivElement | null>,
}) => { }) => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLAnchorElement>(null);
useEffect(() => { useEffect(() => {
if (isActive && ref.current) { if (isActive && ref.current) {
@ -51,20 +56,16 @@ export const FileTreeItemComponent = ({
}, [isActive, parentRef]); }, [isActive, parentRef]);
return ( return (
<div <Link
ref={ref} ref={ref}
href={href}
className={clsx("flex flex-row gap-1 items-center hover:bg-accent hover:text-accent-foreground rounded-sm cursor-pointer p-0.5", { className={clsx("flex flex-row gap-1 items-center hover:bg-accent hover:text-accent-foreground rounded-sm cursor-pointer p-0.5", {
'bg-accent': isActive, 'bg-accent': isActive,
})} })}
style={{ paddingLeft: `${depth * 16}px` }} style={{ paddingLeft: `${depth * 16}px` }}
tabIndex={0} tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
onClick();
}
}}
onClick={onClick} onClick={onClick}
onNavigate={onNavigate}
> >
<div <div
className="flex flex-row gap-1 cursor-pointer w-4 h-4 flex-shrink-0" className="flex flex-row gap-1 cursor-pointer w-4 h-4 flex-shrink-0"
@ -79,6 +80,6 @@ export const FileTreeItemComponent = ({
</div> </div>
<FileTreeItemIcon item={node} /> <FileTreeItemIcon item={node} />
<span className="text-sm">{node.name}</span> <span className="text-sm">{node.name}</span>
</div> </Link>
) )
} }

View file

@ -4,9 +4,9 @@ import { FileTreeNode as RawFileTreeNode } from "../actions";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import React, { useCallback, useMemo, useState, useEffect, useRef } from "react"; import React, { useCallback, useMemo, useState, useEffect, useRef } from "react";
import { FileTreeItemComponent } from "./fileTreeItemComponent"; import { FileTreeItemComponent } from "./fileTreeItemComponent";
import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation"; import { getBrowsePath } from "@/app/[domain]/browse/hooks/useBrowseNavigation";
import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams"; import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams";
import { useDomain } from "@/hooks/useDomain";
export type FileTreeNode = Omit<RawFileTreeNode, 'children'> & { export type FileTreeNode = Omit<RawFileTreeNode, 'children'> & {
isCollapsed: boolean; isCollapsed: boolean;
@ -41,8 +41,8 @@ interface PureFileTreePanelProps {
export const PureFileTreePanel = ({ tree: _tree, path }: PureFileTreePanelProps) => { export const PureFileTreePanel = ({ tree: _tree, path }: PureFileTreePanelProps) => {
const [tree, setTree] = useState<FileTreeNode>(buildCollapsibleTree(_tree)); const [tree, setTree] = useState<FileTreeNode>(buildCollapsibleTree(_tree));
const scrollAreaRef = useRef<HTMLDivElement>(null); const scrollAreaRef = useRef<HTMLDivElement>(null);
const { navigateToPath } = useBrowseNavigation();
const { repoName, revisionName } = useBrowseParams(); const { repoName, revisionName } = useBrowseParams();
const domain = useDomain();
// @note: When `_tree` changes, it indicates that a new tree has been loaded. // @note: When `_tree` changes, it indicates that a new tree has been loaded.
// In that case, we need to rebuild the collapsible tree. // In that case, we need to rebuild the collapsible tree.
@ -72,21 +72,6 @@ export const PureFileTreePanel = ({ tree: _tree, path }: PureFileTreePanelProps)
} }
}, [path, setIsCollapsed]); }, [path, setIsCollapsed]);
const onNodeClicked = useCallback((node: FileTreeNode) => {
if (node.type === 'tree') {
setIsCollapsed(node.path, !node.isCollapsed);
}
else if (node.type === 'blob') {
navigateToPath({
repoName: repoName,
revisionName: revisionName,
path: node.path,
pathType: 'blob',
});
}
}, [setIsCollapsed, navigateToPath, repoName, revisionName]);
const renderTree = useCallback((nodes: FileTreeNode, depth = 0): React.ReactNode => { const renderTree = useCallback((nodes: FileTreeNode, depth = 0): React.ReactNode => {
return ( return (
<> <>
@ -94,13 +79,35 @@ export const PureFileTreePanel = ({ tree: _tree, path }: PureFileTreePanelProps)
return ( return (
<React.Fragment key={node.path}> <React.Fragment key={node.path}>
<FileTreeItemComponent <FileTreeItemComponent
href={getBrowsePath({
repoName,
revisionName,
path: node.path,
pathType: node.type === 'tree' ? 'tree' : 'blob',
domain,
})}
key={node.path} key={node.path}
node={node} node={node}
isActive={node.path === path} isActive={node.path === path}
depth={depth} depth={depth}
isCollapsed={node.isCollapsed} isCollapsed={node.isCollapsed}
isCollapseChevronVisible={node.type === 'tree'} isCollapseChevronVisible={node.type === 'tree'}
onClick={() => onNodeClicked(node)} // Only collapse the tree when a regular click happens.
// (i.e., not ctrl/cmd click).
onClick={(e) => {
const isMetaOrCtrlKey = e.metaKey || e.ctrlKey;
if (node.type === 'tree' && !isMetaOrCtrlKey) {
setIsCollapsed(node.path, !node.isCollapsed);
}
}}
// @note: onNavigate _won't_ be called when the user ctrl/cmd clicks on a tree node.
// So when a regular click happens, we want to prevent the navigation from happening
// and instead collapse the tree.
onNavigate={(e) => {
if (node.type === 'tree') {
e.preventDefault();
}
}}
parentRef={scrollAreaRef} parentRef={scrollAreaRef}
/> />
{node.children.length > 0 && !node.isCollapsed && renderTree(node, depth + 1)} {node.children.length > 0 && !node.isCollapsed && renderTree(node, depth + 1)}
@ -109,7 +116,7 @@ export const PureFileTreePanel = ({ tree: _tree, path }: PureFileTreePanelProps)
})} })}
</> </>
); );
}, [path, onNodeClicked]); }, [path]);
const renderedTree = useMemo(() => renderTree(tree), [tree, renderTree]); const renderedTree = useMemo(() => renderTree(tree), [tree, renderTree]);