This commit is contained in:
bkellam 2025-09-21 13:05:07 -07:00
parent 8ae43fd772
commit 41b2226beb
3 changed files with 50 additions and 44 deletions

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,18 +14,9 @@ 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
className="flex flex-col p-0.5" className="flex flex-col p-0.5"
@ -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

@ -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]);