Skip to content

fix(browse): Fix issue where files would sometimes never load #365

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed
- Fixed issue with external source code links being broken for paths with spaces. [#364](https://github.com/sourcebot-dev/sourcebot/pull/364)
- Fixed issue where files would sometimes never load in the code browser. [#365](https://github.com/sourcebot-dev/sourcebot/pull/365)

## [4.5.0] - 2025-06-21

Expand Down
Original file line number Diff line number Diff line change
@@ -1,63 +1,39 @@
'use client';

import { getCodeHostInfoForRepo, unwrapServiceError } from "@/lib/utils";
import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams";
import { useQuery } from "@tanstack/react-query";
import { getFileSource } from "@/features/search/fileSourceApi";
import { useDomain } from "@/hooks/useDomain";
import { Loader2 } from "lucide-react";
import { Separator } from "@/components/ui/separator";
import { getRepoInfoByName } from "@/actions";
import { cn } from "@/lib/utils";
import { PathHeader } from "@/app/[domain]/components/pathHeader";
import { Separator } from "@/components/ui/separator";
import { getFileSource } from "@/features/search/fileSourceApi";
import { cn, getCodeHostInfoForRepo, isServiceError } from "@/lib/utils";
import Image from "next/image";
import { useMemo } from "react";
import { PureCodePreviewPanel } from "./pureCodePreviewPanel";
import { PathHeader } from "@/app/[domain]/components/pathHeader";

export const CodePreviewPanel = () => {
const { path, repoName, revisionName } = useBrowseParams();
const domain = useDomain();
interface CodePreviewPanelProps {
path: string;
repoName: string;
revisionName?: string;
domain: string;
}

const { data: fileSourceResponse, isPending: isFileSourcePending, isError: isFileSourceError } = useQuery({
queryKey: ['fileSource', repoName, revisionName, path, domain],
queryFn: () => unwrapServiceError(getFileSource({
export const CodePreviewPanel = async ({ path, repoName, revisionName, domain }: CodePreviewPanelProps) => {
const [fileSourceResponse, repoInfoResponse] = await Promise.all([
getFileSource({
fileName: path,
repository: repoName,
branch: revisionName
}, domain)),
});
branch: revisionName,
}, domain),
getRepoInfoByName(repoName, domain),
]);

const { data: repoInfoResponse, isPending: isRepoInfoPending, isError: isRepoInfoError } = useQuery({
queryKey: ['repoInfo', repoName, domain],
queryFn: () => unwrapServiceError(getRepoInfoByName(repoName, domain)),
});

const codeHostInfo = useMemo(() => {
if (!repoInfoResponse) {
return undefined;
}

return getCodeHostInfoForRepo({
codeHostType: repoInfoResponse.codeHostType,
name: repoInfoResponse.name,
displayName: repoInfoResponse.displayName,
webUrl: repoInfoResponse.webUrl,
});
}, [repoInfoResponse]);

if (isFileSourcePending || isRepoInfoPending) {
return (
<div className="flex flex-col w-full min-h-full items-center justify-center">
<Loader2 className="w-4 h-4 animate-spin" />
Loading...
</div>
)
}

if (isFileSourceError || isRepoInfoError) {
if (isServiceError(fileSourceResponse) || isServiceError(repoInfoResponse)) {
return <div>Error loading file source</div>
}

const codeHostInfo = getCodeHostInfoForRepo({
codeHostType: repoInfoResponse.codeHostType,
name: repoInfoResponse.name,
displayName: repoInfoResponse.displayName,
webUrl: repoInfoResponse.webUrl,
});

return (
<>
<div className="flex flex-row py-1 px-2 items-center justify-between">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use client';

import { useCallback, useRef } from "react";
import { FileTreeItem } from "@/features/fileTree/actions";
import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeItemComponent";
import { useBrowseNavigation } from "../../hooks/useBrowseNavigation";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useBrowseParams } from "../../hooks/useBrowseParams";

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

return (
<ScrollArea
className="flex flex-col p-0.5"
ref={scrollAreaRef}
>
{items.map((item) => (
<FileTreeItemComponent
key={item.path}
node={item}
isActive={false}
depth={0}
isCollapseChevronVisible={false}
onClick={() => onNodeClicked(item)}
parentRef={scrollAreaRef}
/>
))}
</ScrollArea>
)
}
Original file line number Diff line number Diff line change
@@ -1,74 +1,30 @@
'use client';

import { Loader2 } from "lucide-react";
import { Separator } from "@/components/ui/separator";
import { getRepoInfoByName } from "@/actions";
import { PathHeader } from "@/app/[domain]/components/pathHeader";
import { useCallback, useRef } from "react";
import { FileTreeItem, getFolderContents } from "@/features/fileTree/actions";
import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeItemComponent";
import { useBrowseNavigation } from "../../hooks/useBrowseNavigation";
import { ScrollArea } from "@/components/ui/scroll-area";
import { unwrapServiceError } from "@/lib/utils";
import { useBrowseParams } from "../../hooks/useBrowseParams";
import { useDomain } from "@/hooks/useDomain";
import { useQuery } from "@tanstack/react-query";
import { usePrefetchFileSource } from "@/hooks/usePrefetchFileSource";
import { usePrefetchFolderContents } from "@/hooks/usePrefetchFolderContents";

export const TreePreviewPanel = () => {
const { path } = useBrowseParams();
const { repoName, revisionName } = useBrowseParams();
const domain = useDomain();
const { navigateToPath } = useBrowseNavigation();
const { prefetchFileSource } = usePrefetchFileSource();
const { prefetchFolderContents } = usePrefetchFolderContents();
const scrollAreaRef = useRef<HTMLDivElement>(null);

const { data: repoInfoResponse, isPending: isRepoInfoPending, isError: isRepoInfoError } = useQuery({
queryKey: ['repoInfo', repoName, domain],
queryFn: () => unwrapServiceError(getRepoInfoByName(repoName, domain)),
});

const { data, isPending: isFolderContentsPending, isError: isFolderContentsError } = useQuery({
queryKey: ['tree', repoName, revisionName, path, domain],
queryFn: () => unwrapServiceError(
getFolderContents({
repoName,
revisionName: revisionName ?? 'HEAD',
path,
}, domain)
),
});

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

const onNodeMouseEnter = useCallback((node: FileTreeItem) => {
if (node.type === 'blob') {
prefetchFileSource(repoName, revisionName ?? 'HEAD', node.path);
} else if (node.type === 'tree') {
prefetchFolderContents(repoName, revisionName ?? 'HEAD', node.path);
}
}, [prefetchFileSource, prefetchFolderContents, repoName, revisionName]);

if (isFolderContentsPending || isRepoInfoPending) {
return (
<div className="flex flex-col w-full min-h-full items-center justify-center">
<Loader2 className="w-4 h-4 animate-spin" />
Loading...
</div>
)
}

if (isFolderContentsError || isRepoInfoError) {
return <div>Error loading tree</div>
import { getFolderContents } from "@/features/fileTree/actions";
import { isServiceError } from "@/lib/utils";
import { PureTreePreviewPanel } from "./pureTreePreviewPanel";

interface TreePreviewPanelProps {
path: string;
repoName: string;
revisionName?: string;
domain: string;
}

export const TreePreviewPanel = async ({ path, repoName, revisionName, domain }: TreePreviewPanelProps) => {
const [repoInfoResponse, folderContentsResponse] = await Promise.all([
getRepoInfoByName(repoName, domain),
getFolderContents({
repoName,
revisionName: revisionName ?? 'HEAD',
path,
}, domain)
]);

if (isServiceError(folderContentsResponse) || isServiceError(repoInfoResponse)) {
return <div>Error loading tree preview</div>
}

return (
Expand All @@ -86,23 +42,7 @@ export const TreePreviewPanel = () => {
/>
</div>
<Separator />
<ScrollArea
className="flex flex-col p-0.5"
ref={scrollAreaRef}
>
{data.map((item) => (
<FileTreeItemComponent
key={item.path}
node={item}
isActive={false}
depth={0}
isCollapseChevronVisible={false}
onClick={() => onNodeClicked(item)}
onMouseEnter={() => onNodeMouseEnter(item)}
parentRef={scrollAreaRef}
/>
))}
</ScrollArea>
<PureTreePreviewPanel items={folderContentsResponse} />
</>
)
}
47 changes: 36 additions & 11 deletions packages/web/src/app/[domain]/browse/[...path]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,44 @@
'use client';

import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams";
import { Suspense } from "react";
import { getBrowseParamsFromPathParam } from "../hooks/utils";
import { CodePreviewPanel } from "./components/codePreviewPanel";
import { Loader2 } from "lucide-react";
import { TreePreviewPanel } from "./components/treePreviewPanel";

export default function BrowsePage() {
const { pathType } = useBrowseParams();
interface BrowsePageProps {
params: {
path: string[];
domain: string;
};
}

export default async function BrowsePage({ params: { path: _rawPath, domain } }: BrowsePageProps) {
const rawPath = decodeURIComponent(_rawPath.join('/'));
const { repoName, revisionName, path, pathType } = getBrowseParamsFromPathParam(rawPath);

return (
<div className="flex flex-col h-full">

{pathType === 'blob' ? (
<CodePreviewPanel />
) : (
<TreePreviewPanel />
)}
<Suspense fallback={
<div className="flex flex-col w-full min-h-full items-center justify-center">
<Loader2 className="w-4 h-4 animate-spin" />
Loading...
</div>
}>
{pathType === 'blob' ? (
<CodePreviewPanel
path={path}
repoName={repoName}
revisionName={revisionName}
domain={domain}
/>
) : (
<TreePreviewPanel
path={path}
repoName={repoName}
revisionName={revisionName}
domain={domain}
/>
)}
</Suspense>
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { useDomain } from "@/hooks/useDomain";
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog";
import { useBrowseNavigation } from "../hooks/useBrowseNavigation";
import { useBrowseState } from "../hooks/useBrowseState";
import { usePrefetchFileSource } from "@/hooks/usePrefetchFileSource";
import { useBrowseParams } from "../hooks/useBrowseParams";
import { FileTreeItemIcon } from "@/features/fileTree/components/fileTreeItemIcon";
import { useLocalStorage } from "usehooks-ts";
Expand All @@ -36,7 +35,6 @@ export const FileSearchCommandDialog = () => {
const inputRef = useRef<HTMLInputElement>(null);
const [searchQuery, setSearchQuery] = useState('');
const { navigateToPath } = useBrowseNavigation();
const { prefetchFileSource } = usePrefetchFileSource();

const [recentlyOpened, setRecentlyOpened] = useLocalStorage<FileTreeItem[]>(`recentlyOpenedFiles-${repoName}`, []);

Expand Down Expand Up @@ -122,14 +120,6 @@ export const FileSearchCommandDialog = () => {
});
}, [navigateToPath, repoName, revisionName, setRecentlyOpened, updateBrowseState]);

const onMouseEnter = useCallback((file: FileTreeItem) => {
prefetchFileSource(
repoName,
revisionName ?? 'HEAD',
file.path
);
}, [prefetchFileSource, repoName, revisionName]);

// @note: We were hitting issues when the user types into the input field while the files are still
// loading. The workaround was to set `disabled` when loading and then focus the input field when
// the files are loaded, hence the `useEffect` below.
Expand Down Expand Up @@ -181,7 +171,6 @@ export const FileSearchCommandDialog = () => {
key={file.path}
file={file}
onSelect={() => onSelect(file)}
onMouseEnter={() => onMouseEnter(file)}
/>
);
})}
Expand All @@ -196,7 +185,6 @@ export const FileSearchCommandDialog = () => {
file={file}
match={match}
onSelect={() => onSelect(file)}
onMouseEnter={() => onMouseEnter(file)}
/>
);
})}
Expand All @@ -223,20 +211,17 @@ interface SearchResultComponentProps {
to: number;
};
onSelect: () => void;
onMouseEnter: () => void;
}

const SearchResultComponent = ({
file,
match,
onSelect,
onMouseEnter,
}: SearchResultComponentProps) => {
return (
<CommandItem
key={file.path}
onSelect={onSelect}
onMouseEnter={onMouseEnter}
>
<div className="flex flex-row gap-2 w-full cursor-pointer relative">
<FileTreeItemIcon item={file} className="mt-1" />
Expand Down
Loading