Skip to content

Commit 1384dd8

Browse files
fix(browse): Fix issue where files would sometimes never load (#365)
1 parent d9d0146 commit 1384dd8

File tree

15 files changed

+381
-320
lines changed

15 files changed

+381
-320
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111
- Fixed issue with external source code links being broken for paths with spaces. [#364](https://github.com/sourcebot-dev/sourcebot/pull/364)
1212
- Makes base retry indexing configuration configurable and move from a default of `5s` to `60s`. [#377](https://github.com/sourcebot-dev/sourcebot/pull/377)
13+
- Fixed issue where files would sometimes never load in the code browser. [#365](https://github.com/sourcebot-dev/sourcebot/pull/365)
1314

1415
## [4.5.0] - 2025-06-21
1516

packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx

Lines changed: 25 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,39 @@
1-
'use client';
2-
3-
import { getCodeHostInfoForRepo, unwrapServiceError } from "@/lib/utils";
4-
import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams";
5-
import { useQuery } from "@tanstack/react-query";
6-
import { getFileSource } from "@/features/search/fileSourceApi";
7-
import { useDomain } from "@/hooks/useDomain";
8-
import { Loader2 } from "lucide-react";
9-
import { Separator } from "@/components/ui/separator";
101
import { getRepoInfoByName } from "@/actions";
11-
import { cn } from "@/lib/utils";
2+
import { PathHeader } from "@/app/[domain]/components/pathHeader";
3+
import { Separator } from "@/components/ui/separator";
4+
import { getFileSource } from "@/features/search/fileSourceApi";
5+
import { cn, getCodeHostInfoForRepo, isServiceError } from "@/lib/utils";
126
import Image from "next/image";
13-
import { useMemo } from "react";
147
import { PureCodePreviewPanel } from "./pureCodePreviewPanel";
15-
import { PathHeader } from "@/app/[domain]/components/pathHeader";
168

17-
export const CodePreviewPanel = () => {
18-
const { path, repoName, revisionName } = useBrowseParams();
19-
const domain = useDomain();
9+
interface CodePreviewPanelProps {
10+
path: string;
11+
repoName: string;
12+
revisionName?: string;
13+
domain: string;
14+
}
2015

21-
const { data: fileSourceResponse, isPending: isFileSourcePending, isError: isFileSourceError } = useQuery({
22-
queryKey: ['fileSource', repoName, revisionName, path, domain],
23-
queryFn: () => unwrapServiceError(getFileSource({
16+
export const CodePreviewPanel = async ({ path, repoName, revisionName, domain }: CodePreviewPanelProps) => {
17+
const [fileSourceResponse, repoInfoResponse] = await Promise.all([
18+
getFileSource({
2419
fileName: path,
2520
repository: repoName,
26-
branch: revisionName
27-
}, domain)),
28-
});
21+
branch: revisionName,
22+
}, domain),
23+
getRepoInfoByName(repoName, domain),
24+
]);
2925

30-
const { data: repoInfoResponse, isPending: isRepoInfoPending, isError: isRepoInfoError } = useQuery({
31-
queryKey: ['repoInfo', repoName, domain],
32-
queryFn: () => unwrapServiceError(getRepoInfoByName(repoName, domain)),
33-
});
34-
35-
const codeHostInfo = useMemo(() => {
36-
if (!repoInfoResponse) {
37-
return undefined;
38-
}
39-
40-
return getCodeHostInfoForRepo({
41-
codeHostType: repoInfoResponse.codeHostType,
42-
name: repoInfoResponse.name,
43-
displayName: repoInfoResponse.displayName,
44-
webUrl: repoInfoResponse.webUrl,
45-
});
46-
}, [repoInfoResponse]);
47-
48-
if (isFileSourcePending || isRepoInfoPending) {
49-
return (
50-
<div className="flex flex-col w-full min-h-full items-center justify-center">
51-
<Loader2 className="w-4 h-4 animate-spin" />
52-
Loading...
53-
</div>
54-
)
55-
}
56-
57-
if (isFileSourceError || isRepoInfoError) {
26+
if (isServiceError(fileSourceResponse) || isServiceError(repoInfoResponse)) {
5827
return <div>Error loading file source</div>
5928
}
6029

30+
const codeHostInfo = getCodeHostInfoForRepo({
31+
codeHostType: repoInfoResponse.codeHostType,
32+
name: repoInfoResponse.name,
33+
displayName: repoInfoResponse.displayName,
34+
webUrl: repoInfoResponse.webUrl,
35+
});
36+
6137
return (
6238
<>
6339
<div className="flex flex-row py-1 px-2 items-center justify-between">
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use client';
2+
3+
import { useCallback, useRef } from "react";
4+
import { FileTreeItem } from "@/features/fileTree/actions";
5+
import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeItemComponent";
6+
import { useBrowseNavigation } from "../../hooks/useBrowseNavigation";
7+
import { ScrollArea } from "@/components/ui/scroll-area";
8+
import { useBrowseParams } from "../../hooks/useBrowseParams";
9+
10+
interface PureTreePreviewPanelProps {
11+
items: FileTreeItem[];
12+
}
13+
14+
export const PureTreePreviewPanel = ({ items }: PureTreePreviewPanelProps) => {
15+
const { repoName, revisionName } = useBrowseParams();
16+
const { navigateToPath } = useBrowseNavigation();
17+
const scrollAreaRef = useRef<HTMLDivElement>(null);
18+
19+
const onNodeClicked = useCallback((node: FileTreeItem) => {
20+
navigateToPath({
21+
repoName: repoName,
22+
revisionName: revisionName,
23+
path: node.path,
24+
pathType: node.type === 'tree' ? 'tree' : 'blob',
25+
});
26+
}, [navigateToPath, repoName, revisionName]);
27+
28+
return (
29+
<ScrollArea
30+
className="flex flex-col p-0.5"
31+
ref={scrollAreaRef}
32+
>
33+
{items.map((item) => (
34+
<FileTreeItemComponent
35+
key={item.path}
36+
node={item}
37+
isActive={false}
38+
depth={0}
39+
isCollapseChevronVisible={false}
40+
onClick={() => onNodeClicked(item)}
41+
parentRef={scrollAreaRef}
42+
/>
43+
))}
44+
</ScrollArea>
45+
)
46+
}
Lines changed: 24 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,30 @@
1-
'use client';
21

3-
import { Loader2 } from "lucide-react";
42
import { Separator } from "@/components/ui/separator";
53
import { getRepoInfoByName } from "@/actions";
64
import { PathHeader } from "@/app/[domain]/components/pathHeader";
7-
import { useCallback, useRef } from "react";
8-
import { FileTreeItem, getFolderContents } from "@/features/fileTree/actions";
9-
import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeItemComponent";
10-
import { useBrowseNavigation } from "../../hooks/useBrowseNavigation";
11-
import { ScrollArea } from "@/components/ui/scroll-area";
12-
import { unwrapServiceError } from "@/lib/utils";
13-
import { useBrowseParams } from "../../hooks/useBrowseParams";
14-
import { useDomain } from "@/hooks/useDomain";
15-
import { useQuery } from "@tanstack/react-query";
16-
import { usePrefetchFileSource } from "@/hooks/usePrefetchFileSource";
17-
import { usePrefetchFolderContents } from "@/hooks/usePrefetchFolderContents";
18-
19-
export const TreePreviewPanel = () => {
20-
const { path } = useBrowseParams();
21-
const { repoName, revisionName } = useBrowseParams();
22-
const domain = useDomain();
23-
const { navigateToPath } = useBrowseNavigation();
24-
const { prefetchFileSource } = usePrefetchFileSource();
25-
const { prefetchFolderContents } = usePrefetchFolderContents();
26-
const scrollAreaRef = useRef<HTMLDivElement>(null);
27-
28-
const { data: repoInfoResponse, isPending: isRepoInfoPending, isError: isRepoInfoError } = useQuery({
29-
queryKey: ['repoInfo', repoName, domain],
30-
queryFn: () => unwrapServiceError(getRepoInfoByName(repoName, domain)),
31-
});
32-
33-
const { data, isPending: isFolderContentsPending, isError: isFolderContentsError } = useQuery({
34-
queryKey: ['tree', repoName, revisionName, path, domain],
35-
queryFn: () => unwrapServiceError(
36-
getFolderContents({
37-
repoName,
38-
revisionName: revisionName ?? 'HEAD',
39-
path,
40-
}, domain)
41-
),
42-
});
43-
44-
const onNodeClicked = useCallback((node: FileTreeItem) => {
45-
navigateToPath({
46-
repoName: repoName,
47-
revisionName: revisionName,
48-
path: node.path,
49-
pathType: node.type === 'tree' ? 'tree' : 'blob',
50-
});
51-
}, [navigateToPath, repoName, revisionName]);
52-
53-
const onNodeMouseEnter = useCallback((node: FileTreeItem) => {
54-
if (node.type === 'blob') {
55-
prefetchFileSource(repoName, revisionName ?? 'HEAD', node.path);
56-
} else if (node.type === 'tree') {
57-
prefetchFolderContents(repoName, revisionName ?? 'HEAD', node.path);
58-
}
59-
}, [prefetchFileSource, prefetchFolderContents, repoName, revisionName]);
60-
61-
if (isFolderContentsPending || isRepoInfoPending) {
62-
return (
63-
<div className="flex flex-col w-full min-h-full items-center justify-center">
64-
<Loader2 className="w-4 h-4 animate-spin" />
65-
Loading...
66-
</div>
67-
)
68-
}
69-
70-
if (isFolderContentsError || isRepoInfoError) {
71-
return <div>Error loading tree</div>
5+
import { getFolderContents } from "@/features/fileTree/actions";
6+
import { isServiceError } from "@/lib/utils";
7+
import { PureTreePreviewPanel } from "./pureTreePreviewPanel";
8+
9+
interface TreePreviewPanelProps {
10+
path: string;
11+
repoName: string;
12+
revisionName?: string;
13+
domain: string;
14+
}
15+
16+
export const TreePreviewPanel = async ({ path, repoName, revisionName, domain }: TreePreviewPanelProps) => {
17+
const [repoInfoResponse, folderContentsResponse] = await Promise.all([
18+
getRepoInfoByName(repoName, domain),
19+
getFolderContents({
20+
repoName,
21+
revisionName: revisionName ?? 'HEAD',
22+
path,
23+
}, domain)
24+
]);
25+
26+
if (isServiceError(folderContentsResponse) || isServiceError(repoInfoResponse)) {
27+
return <div>Error loading tree preview</div>
7228
}
7329

7430
return (
@@ -86,23 +42,7 @@ export const TreePreviewPanel = () => {
8642
/>
8743
</div>
8844
<Separator />
89-
<ScrollArea
90-
className="flex flex-col p-0.5"
91-
ref={scrollAreaRef}
92-
>
93-
{data.map((item) => (
94-
<FileTreeItemComponent
95-
key={item.path}
96-
node={item}
97-
isActive={false}
98-
depth={0}
99-
isCollapseChevronVisible={false}
100-
onClick={() => onNodeClicked(item)}
101-
onMouseEnter={() => onNodeMouseEnter(item)}
102-
parentRef={scrollAreaRef}
103-
/>
104-
))}
105-
</ScrollArea>
45+
<PureTreePreviewPanel items={folderContentsResponse} />
10646
</>
10747
)
10848
}

packages/web/src/app/[domain]/browse/[...path]/page.tsx

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,44 @@
1-
'use client';
2-
3-
import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams";
1+
import { Suspense } from "react";
2+
import { getBrowseParamsFromPathParam } from "../hooks/utils";
43
import { CodePreviewPanel } from "./components/codePreviewPanel";
4+
import { Loader2 } from "lucide-react";
55
import { TreePreviewPanel } from "./components/treePreviewPanel";
66

7-
export default function BrowsePage() {
8-
const { pathType } = useBrowseParams();
7+
interface BrowsePageProps {
8+
params: {
9+
path: string[];
10+
domain: string;
11+
};
12+
}
13+
14+
export default async function BrowsePage({ params: { path: _rawPath, domain } }: BrowsePageProps) {
15+
const rawPath = decodeURIComponent(_rawPath.join('/'));
16+
const { repoName, revisionName, path, pathType } = getBrowseParamsFromPathParam(rawPath);
17+
918
return (
1019
<div className="flex flex-col h-full">
11-
12-
{pathType === 'blob' ? (
13-
<CodePreviewPanel />
14-
) : (
15-
<TreePreviewPanel />
16-
)}
20+
<Suspense fallback={
21+
<div className="flex flex-col w-full min-h-full items-center justify-center">
22+
<Loader2 className="w-4 h-4 animate-spin" />
23+
Loading...
24+
</div>
25+
}>
26+
{pathType === 'blob' ? (
27+
<CodePreviewPanel
28+
path={path}
29+
repoName={repoName}
30+
revisionName={revisionName}
31+
domain={domain}
32+
/>
33+
) : (
34+
<TreePreviewPanel
35+
path={path}
36+
repoName={repoName}
37+
revisionName={revisionName}
38+
domain={domain}
39+
/>
40+
)}
41+
</Suspense>
1742
</div>
1843
)
1944
}

packages/web/src/app/[domain]/browse/components/fileSearchCommandDialog.tsx

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { useDomain } from "@/hooks/useDomain";
1010
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog";
1111
import { useBrowseNavigation } from "../hooks/useBrowseNavigation";
1212
import { useBrowseState } from "../hooks/useBrowseState";
13-
import { usePrefetchFileSource } from "@/hooks/usePrefetchFileSource";
1413
import { useBrowseParams } from "../hooks/useBrowseParams";
1514
import { FileTreeItemIcon } from "@/features/fileTree/components/fileTreeItemIcon";
1615
import { useLocalStorage } from "usehooks-ts";
@@ -36,7 +35,6 @@ export const FileSearchCommandDialog = () => {
3635
const inputRef = useRef<HTMLInputElement>(null);
3736
const [searchQuery, setSearchQuery] = useState('');
3837
const { navigateToPath } = useBrowseNavigation();
39-
const { prefetchFileSource } = usePrefetchFileSource();
4038

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

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

125-
const onMouseEnter = useCallback((file: FileTreeItem) => {
126-
prefetchFileSource(
127-
repoName,
128-
revisionName ?? 'HEAD',
129-
file.path
130-
);
131-
}, [prefetchFileSource, repoName, revisionName]);
132-
133123
// @note: We were hitting issues when the user types into the input field while the files are still
134124
// loading. The workaround was to set `disabled` when loading and then focus the input field when
135125
// the files are loaded, hence the `useEffect` below.
@@ -181,7 +171,6 @@ export const FileSearchCommandDialog = () => {
181171
key={file.path}
182172
file={file}
183173
onSelect={() => onSelect(file)}
184-
onMouseEnter={() => onMouseEnter(file)}
185174
/>
186175
);
187176
})}
@@ -196,7 +185,6 @@ export const FileSearchCommandDialog = () => {
196185
file={file}
197186
match={match}
198187
onSelect={() => onSelect(file)}
199-
onMouseEnter={() => onMouseEnter(file)}
200188
/>
201189
);
202190
})}
@@ -223,20 +211,17 @@ interface SearchResultComponentProps {
223211
to: number;
224212
};
225213
onSelect: () => void;
226-
onMouseEnter: () => void;
227214
}
228215

229216
const SearchResultComponent = ({
230217
file,
231218
match,
232219
onSelect,
233-
onMouseEnter,
234220
}: SearchResultComponentProps) => {
235221
return (
236222
<CommandItem
237223
key={file.path}
238224
onSelect={onSelect}
239-
onMouseEnter={onMouseEnter}
240225
>
241226
<div className="flex flex-row gap-2 w-full cursor-pointer relative">
242227
<FileTreeItemIcon item={file} className="mt-1" />

0 commit comments

Comments
 (0)