mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 12:25:22 +00:00
fix(web): Change buttons into Links in various places (#532)
This commit is contained in:
parent
ef46c0181d
commit
a698afdf13
8 changed files with 124 additions and 132 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue