Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fileTree
  • Loading branch information
brendan-kellam committed Sep 21, 2025
commit 41b2226bebb8ba582ffdb99074429f3d3669d59c
Original file line number Diff line number Diff line change
@@ -1,30 +1,22 @@
'use client';

import { useCallback, useRef } from "react";
import { useRef } from "react";
import { FileTreeItem } from "@/features/fileTree/actions";
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 { useBrowseParams } from "../../hooks/useBrowseParams";
import { useDomain } from "@/hooks/useDomain";

interface PureTreePreviewPanelProps {
items: FileTreeItem[];
}

export const PureTreePreviewPanel = ({ items }: PureTreePreviewPanelProps) => {
const { repoName, revisionName } = useBrowseParams();
const { navigateToPath } = useBrowseNavigation();
const scrollAreaRef = useRef<HTMLDivElement>(null);

const onNodeClicked = useCallback((node: FileTreeItem) => {
navigateToPath({
repoName: repoName,
revisionName: revisionName,
path: node.path,
pathType: node.type === 'tree' ? 'tree' : 'blob',
});
}, [navigateToPath, repoName, revisionName]);

const domain = useDomain();

return (
<ScrollArea
className="flex flex-col p-0.5"
Expand All @@ -37,8 +29,14 @@ export const PureTreePreviewPanel = ({ items }: PureTreePreviewPanelProps) => {
isActive={false}
depth={0}
isCollapseChevronVisible={false}
onClick={() => onNodeClicked(item)}
parentRef={scrollAreaRef}
href={getBrowsePath({
repoName,
revisionName,
path: item.path,
pathType: item.type === 'tree' ? 'tree' : 'blob',
domain,
})}
/>
))}
</ScrollArea>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,30 @@ import clsx from "clsx";
import scrollIntoView from 'scroll-into-view-if-needed';
import { ChevronDownIcon, ChevronRightIcon } from "@radix-ui/react-icons";
import { FileTreeItemIcon } from "./fileTreeItemIcon";
import Link from "next/link";

export const FileTreeItemComponent = ({
node,
isActive,
depth,
isCollapsed = false,
isCollapseChevronVisible = true,
href,
onClick,
onNavigate,
parentRef,
}: {
node: FileTreeItem,
isActive: boolean,
depth: number,
isCollapsed?: boolean,
isCollapseChevronVisible?: boolean,
onClick: () => void,
href: string,
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void,
onNavigate?: (e: { preventDefault: () => void }) => void,
parentRef: React.RefObject<HTMLDivElement | null>,
}) => {
const ref = useRef<HTMLDivElement>(null);
const ref = useRef<HTMLAnchorElement>(null);

useEffect(() => {
if (isActive && ref.current) {
Expand All @@ -51,20 +56,16 @@ export const FileTreeItemComponent = ({
}, [isActive, parentRef]);

return (
<div
<Link
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", {
'bg-accent': isActive,
})}
style={{ paddingLeft: `${depth * 16}px` }}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
onClick();
}
}}
onClick={onClick}
onNavigate={onNavigate}
>
<div
className="flex flex-row gap-1 cursor-pointer w-4 h-4 flex-shrink-0"
Expand All @@ -79,6 +80,6 @@ export const FileTreeItemComponent = ({
</div>
<FileTreeItemIcon item={node} />
<span className="text-sm">{node.name}</span>
</div>
</Link>
)
}
47 changes: 27 additions & 20 deletions packages/web/src/features/fileTree/components/pureFileTreePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { FileTreeNode as RawFileTreeNode } from "../actions";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import React, { useCallback, useMemo, useState, useEffect, useRef } from "react";
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 { useDomain } from "@/hooks/useDomain";

export type FileTreeNode = Omit<RawFileTreeNode, 'children'> & {
isCollapsed: boolean;
Expand Down Expand Up @@ -41,8 +41,8 @@ interface PureFileTreePanelProps {
export const PureFileTreePanel = ({ tree: _tree, path }: PureFileTreePanelProps) => {
const [tree, setTree] = useState<FileTreeNode>(buildCollapsibleTree(_tree));
const scrollAreaRef = useRef<HTMLDivElement>(null);
const { navigateToPath } = useBrowseNavigation();
const { repoName, revisionName } = useBrowseParams();
const domain = useDomain();

// @note: When `_tree` changes, it indicates that a new tree has been loaded.
// In that case, we need to rebuild the collapsible tree.
Expand Down Expand Up @@ -72,35 +72,42 @@ export const PureFileTreePanel = ({ tree: _tree, path }: PureFileTreePanelProps)
}
}, [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 => {
return (
<>
{nodes.children.map((node) => {
return (
<React.Fragment key={node.path}>
<FileTreeItemComponent
href={getBrowsePath({
repoName,
revisionName,
path: node.path,
pathType: node.type === 'tree' ? 'tree' : 'blob',
domain,
})}
key={node.path}
node={node}
isActive={node.path === path}
depth={depth}
isCollapsed={node.isCollapsed}
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}
/>
{node.children.length > 0 && !node.isCollapsed && renderTree(node, depth + 1)}
Expand All @@ -109,7 +116,7 @@ export const PureFileTreePanel = ({ tree: _tree, path }: PureFileTreePanelProps)
})}
</>
);
}, [path, onNodeClicked]);
}, [path]);

const renderedTree = useMemo(() => renderTree(tree), [tree, renderTree]);

Expand Down