fix(web): Fix loading issues with references / definitions list (#617)

This commit is contained in:
Brendan Kellam 2025-11-13 17:21:48 -08:00 committed by GitHub
parent 341836a2ed
commit fbe1073d0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 275 additions and 128 deletions

View file

@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Fixed
- Fixed spurious infinite loads with explore panel, file tree, and file search command. [#617](https://github.com/sourcebot-dev/sourcebot/pull/617)
## [4.9.2] - 2025-11-13 ## [4.9.2] - 2025-11-13
### Changed ### Changed

View file

@ -1,10 +1,10 @@
import { getRepoInfoByName } from "@/actions"; import { getRepoInfoByName } from "@/actions";
import { PathHeader } from "@/app/[domain]/components/pathHeader"; import { PathHeader } from "@/app/[domain]/components/pathHeader";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { getFileSource } from "@/features/search/fileSourceApi";
import { cn, getCodeHostInfoForRepo, isServiceError } from "@/lib/utils"; import { cn, getCodeHostInfoForRepo, isServiceError } from "@/lib/utils";
import Image from "next/image"; import Image from "next/image";
import { PureCodePreviewPanel } from "./pureCodePreviewPanel"; import { PureCodePreviewPanel } from "./pureCodePreviewPanel";
import { getFileSource } from "@/features/search/fileSourceApi";
interface CodePreviewPanelProps { interface CodePreviewPanelProps {
path: string; path: string;

View file

@ -1,12 +1,12 @@
'use client'; 'use client';
import { useRef } from "react"; import { useRef } from "react";
import { FileTreeItem } from "@/features/fileTree/actions";
import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeItemComponent"; import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeItemComponent";
import { getBrowsePath } from "../../hooks/utils"; import { getBrowsePath } from "../../hooks/utils";
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"; import { useDomain } from "@/hooks/useDomain";
import { FileTreeItem } from "@/features/fileTree/types";
interface PureTreePreviewPanelProps { interface PureTreePreviewPanelProps {
items: FileTreeItem[]; items: FileTreeItem[];

View file

@ -2,7 +2,7 @@
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { getRepoInfoByName } from "@/actions"; import { getRepoInfoByName } from "@/actions";
import { PathHeader } from "@/app/[domain]/components/pathHeader"; import { PathHeader } from "@/app/[domain]/components/pathHeader";
import { getFolderContents } from "@/features/fileTree/actions"; import { getFolderContents } from "@/features/fileTree/api";
import { isServiceError } from "@/lib/utils"; import { isServiceError } from "@/lib/utils";
import { PureTreePreviewPanel } from "./pureTreePreviewPanel"; import { PureTreePreviewPanel } from "./pureTreePreviewPanel";

View file

@ -5,7 +5,6 @@ import { useState, useRef, useMemo, useEffect, useCallback } from "react";
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from "react-hotkeys-hook";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { unwrapServiceError } from "@/lib/utils"; import { unwrapServiceError } from "@/lib/utils";
import { FileTreeItem, getFiles } from "@/features/fileTree/actions";
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog";
import { useBrowseNavigation } from "../hooks/useBrowseNavigation"; import { useBrowseNavigation } from "../hooks/useBrowseNavigation";
import { useBrowseState } from "../hooks/useBrowseState"; import { useBrowseState } from "../hooks/useBrowseState";
@ -13,6 +12,8 @@ import { useBrowseParams } from "../hooks/useBrowseParams";
import { FileTreeItemIcon } from "@/features/fileTree/components/fileTreeItemIcon"; import { FileTreeItemIcon } from "@/features/fileTree/components/fileTreeItemIcon";
import { useLocalStorage } from "usehooks-ts"; import { useLocalStorage } from "usehooks-ts";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { FileTreeItem } from "@/features/fileTree/types";
import { getFiles } from "@/app/api/(client)/client";
const MAX_RESULTS = 100; const MAX_RESULTS = 100;

View file

@ -55,7 +55,7 @@ export const useSuggestionsData = ({
query: `file:${suggestionQuery}`, query: `file:${suggestionQuery}`,
matches: 15, matches: 15,
contextLines: 1, contextLines: 1,
}, domain), }),
select: (data): Suggestion[] => { select: (data): Suggestion[] => {
if (isServiceError(data)) { if (isServiceError(data)) {
return []; return [];
@ -75,7 +75,7 @@ export const useSuggestionsData = ({
query: `sym:${suggestionQuery.length > 0 ? suggestionQuery : ".*"}`, query: `sym:${suggestionQuery.length > 0 ? suggestionQuery : ".*"}`,
matches: 15, matches: 15,
contextLines: 1, contextLines: 1,
}, domain), }),
select: (data): Suggestion[] => { select: (data): Suggestion[] => {
if (isServiceError(data)) { if (isServiceError(data)) {
return []; return [];

View file

@ -5,8 +5,8 @@ import { CodePreview } from "./codePreview";
import { SearchResultFile } from "@/features/search/types"; import { SearchResultFile } from "@/features/search/types";
import { SymbolIcon } from "@radix-ui/react-icons"; import { SymbolIcon } from "@radix-ui/react-icons";
import { SetStateAction, Dispatch, useMemo } from "react"; import { SetStateAction, Dispatch, useMemo } from "react";
import { getFileSource } from "@/features/search/fileSourceApi";
import { unwrapServiceError } from "@/lib/utils"; import { unwrapServiceError } from "@/lib/utils";
import { getFileSource } from "@/app/api/(client)/client";
interface CodePreviewPanelProps { interface CodePreviewPanelProps {
previewedFile: SearchResultFile; previewedFile: SearchResultFile;

View file

@ -66,7 +66,7 @@ export const SearchResultsPage = ({
matches: maxMatchCount, matches: maxMatchCount,
contextLines: 3, contextLines: 3,
whole: false, whole: false,
}, domain)), "client.search"), })), "client.search"),
select: ({ data, durationMs }) => ({ select: ({ data, durationMs }) => ({
...data, ...data,
totalClientSearchDurationMs: durationMs, totalClientSearchDurationMs: durationMs,

View file

@ -9,13 +9,22 @@ import {
SearchRequest, SearchRequest,
SearchResponse, SearchResponse,
} from "@/features/search/types"; } from "@/features/search/types";
import {
FindRelatedSymbolsRequest,
FindRelatedSymbolsResponse,
} from "@/features/codeNav/types";
import {
GetFilesRequest,
GetFilesResponse,
GetTreeRequest,
GetTreeResponse,
} from "@/features/fileTree/types";
export const search = async (body: SearchRequest, domain: string): Promise<SearchResponse | ServiceError> => { export const search = async (body: SearchRequest): Promise<SearchResponse | ServiceError> => {
const result = await fetch("/api/search", { const result = await fetch("/api/search", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Org-Domain": domain,
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
}).then(response => response.json()); }).then(response => response.json());
@ -27,12 +36,11 @@ export const search = async (body: SearchRequest, domain: string): Promise<Searc
return result as SearchResponse | ServiceError; return result as SearchResponse | ServiceError;
} }
export const fetchFileSource = async (body: FileSourceRequest, domain: string): Promise<FileSourceResponse | ServiceError> => { export const getFileSource = async (body: FileSourceRequest): Promise<FileSourceResponse | ServiceError> => {
const result = await fetch("/api/source", { const result = await fetch("/api/source", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Org-Domain": domain,
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
}).then(response => response.json()); }).then(response => response.json());
@ -60,3 +68,35 @@ export const getVersion = async (): Promise<GetVersionResponse> => {
}).then(response => response.json()); }).then(response => response.json());
return result as GetVersionResponse; return result as GetVersionResponse;
} }
export const findSearchBasedSymbolReferences = async (body: FindRelatedSymbolsRequest): Promise<FindRelatedSymbolsResponse | ServiceError> => {
const result = await fetch("/api/find_references", {
method: "POST",
body: JSON.stringify(body),
}).then(response => response.json());
return result as FindRelatedSymbolsResponse | ServiceError;
}
export const findSearchBasedSymbolDefinitions = async (body: FindRelatedSymbolsRequest): Promise<FindRelatedSymbolsResponse | ServiceError> => {
const result = await fetch("/api/find_definitions", {
method: "POST",
body: JSON.stringify(body),
}).then(response => response.json());
return result as FindRelatedSymbolsResponse | ServiceError;
}
export const getTree = async (body: GetTreeRequest): Promise<GetTreeResponse | ServiceError> => {
const result = await fetch("/api/tree", {
method: "POST",
body: JSON.stringify(body),
}).then(response => response.json());
return result as GetTreeResponse | ServiceError;
}
export const getFiles = async (body: GetFilesRequest): Promise<GetFilesResponse | ServiceError> => {
const result = await fetch("/api/files", {
method: "POST",
body: JSON.stringify(body),
}).then(response => response.json());
return result as GetFilesResponse | ServiceError;
}

View file

@ -0,0 +1,23 @@
'use server';
import { getFiles } from "@/features/fileTree/api";
import { getFilesRequestSchema } from "@/features/fileTree/types";
import { schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";
export const POST = async (request: NextRequest) => {
const body = await request.json();
const parsed = await getFilesRequestSchema.safeParseAsync(body);
if (!parsed.success) {
return serviceErrorResponse(schemaValidationError(parsed.error));
}
const response = await getFiles(parsed.data);
if (isServiceError(response)) {
return serviceErrorResponse(response);
}
return Response.json(response);
}

View file

@ -0,0 +1,22 @@
'use server';
import { findSearchBasedSymbolDefinitions } from "@/features/codeNav/api";
import { findRelatedSymbolsRequestSchema } from "@/features/codeNav/types";
import { schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";
export const POST = async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
if (!parsed.success) {
return serviceErrorResponse(schemaValidationError(parsed.error));
}
const response = await findSearchBasedSymbolDefinitions(parsed.data);
if (isServiceError(response)) {
return serviceErrorResponse(response);
}
return Response.json(response);
}

View file

@ -0,0 +1,20 @@
import { findSearchBasedSymbolReferences } from "@/features/codeNav/api";
import { findRelatedSymbolsRequestSchema } from "@/features/codeNav/types";
import { schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";
export const POST = async (request: NextRequest) => {
const body = await request.json();
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
if (!parsed.success) {
return serviceErrorResponse(schemaValidationError(parsed.error));
}
const response = await findSearchBasedSymbolReferences(parsed.data);
if (isServiceError(response)) {
return serviceErrorResponse(response);
}
return Response.json(response);
}

View file

@ -0,0 +1,23 @@
'use server';
import { getTree } from "@/features/fileTree/api";
import { getTreeRequestSchema } from "@/features/fileTree/types";
import { schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";
export const POST = async (request: NextRequest) => {
const body = await request.json();
const parsed = await getTreeRequestSchema.safeParseAsync(body);
if (!parsed.success) {
return serviceErrorResponse(schemaValidationError(parsed.error));
}
const response = await getTree(parsed.data);
if (isServiceError(response)) {
return serviceErrorResponse(response);
}
return Response.json(response);
}

View file

@ -1,11 +1,11 @@
'use client'; 'use client';
import { useBrowseState } from "@/app/[domain]/browse/hooks/useBrowseState"; import { useBrowseState } from "@/app/[domain]/browse/hooks/useBrowseState";
import { findSearchBasedSymbolReferences, findSearchBasedSymbolDefinitions} from "@/app/api/(client)/client";
import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle"; import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { findSearchBasedSymbolDefinitions, findSearchBasedSymbolReferences } from "@/features/codeNav/actions";
import { useDomain } from "@/hooks/useDomain"; import { useDomain } from "@/hooks/useDomain";
import { unwrapServiceError } from "@/lib/utils"; import { unwrapServiceError } from "@/lib/utils";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
@ -46,7 +46,7 @@ export const ExploreMenu = ({
symbolName: selectedSymbolInfo.symbolName, symbolName: selectedSymbolInfo.symbolName,
language: selectedSymbolInfo.language, language: selectedSymbolInfo.language,
revisionName: selectedSymbolInfo.revisionName, revisionName: selectedSymbolInfo.revisionName,
}, domain) })
), ),
}); });
@ -62,7 +62,7 @@ export const ExploreMenu = ({
symbolName: selectedSymbolInfo.symbolName, symbolName: selectedSymbolInfo.symbolName,
language: selectedSymbolInfo.language, language: selectedSymbolInfo.language,
revisionName: selectedSymbolInfo.revisionName, revisionName: selectedSymbolInfo.revisionName,
}, domain) })
), ),
}); });

View file

@ -1,4 +1,4 @@
import { findSearchBasedSymbolDefinitions } from "@/features/codeNav/actions"; import { findSearchBasedSymbolDefinitions } from "@/app/api/(client)/client";
import { SourceRange } from "@/features/search/types"; import { SourceRange } from "@/features/search/types";
import { useDomain } from "@/hooks/useDomain"; import { useDomain } from "@/hooks/useDomain";
import { unwrapServiceError } from "@/lib/utils"; import { unwrapServiceError } from "@/lib/utils";
@ -56,7 +56,7 @@ export const useHoveredOverSymbolInfo = ({
symbolName: symbolName!, symbolName: symbolName!,
language, language,
revisionName, revisionName,
}, domain) })
), ),
select: ((data) => { select: ((data) => {
return data.files.flatMap((file) => { return data.files.flatMap((file) => {

View file

@ -251,7 +251,6 @@ const resolveFileSource = async ({ path, repo, revision }: FileSource) => {
fileName: path, fileName: path,
repository: repo, repository: repo,
branch: revision, branch: revision,
// @todo: handle multi-tenancy.
}); });
if (isServiceError(fileSource)) { if (isServiceError(fileSource)) {

View file

@ -41,7 +41,7 @@ export const useSuggestionsData = ({
query, query,
matches: 10, matches: 10,
contextLines: 1, contextLines: 1,
}, domain)) }))
}, },
select: (data): FileSuggestion[] => { select: (data): FileSuggestion[] => {
return data.files.map((file) => { return data.files.map((file) => {

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { fetchFileSource } from "@/app/api/(client)/client"; import { getFileSource } from "@/app/api/(client)/client";
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon"; import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
@ -99,11 +99,11 @@ export const ReferencedSourcesListView = ({
const fileSourceQueries = useQueries({ const fileSourceQueries = useQueries({
queries: referencedFileSources.map((file) => ({ queries: referencedFileSources.map((file) => ({
queryKey: ['fileSource', file.path, file.repo, file.revision, domain], queryKey: ['fileSource', file.path, file.repo, file.revision, domain],
queryFn: () => unwrapServiceError(fetchFileSource({ queryFn: () => unwrapServiceError(getFileSource({
fileName: file.path, fileName: file.path,
repository: file.repo, repository: file.repo,
branch: file.revision, branch: file.revision,
}, domain)), })),
staleTime: Infinity, staleTime: Infinity,
})), })),
}); });

View file

@ -1,10 +1,9 @@
import { z } from "zod" import { z } from "zod"
import { search } from "@/features/search/searchApi" import { search } from "@/features/search/searchApi"
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"
import { InferToolInput, InferToolOutput, InferUITool, tool, ToolUIPart } from "ai"; import { InferToolInput, InferToolOutput, InferUITool, tool, ToolUIPart } from "ai";
import { isServiceError } from "@/lib/utils"; import { isServiceError } from "@/lib/utils";
import { getFileSource } from "../search/fileSourceApi"; import { getFileSource } from "../search/fileSourceApi";
import { findSearchBasedSymbolDefinitions, findSearchBasedSymbolReferences } from "../codeNav/actions"; import { findSearchBasedSymbolDefinitions, findSearchBasedSymbolReferences } from "../codeNav/api";
import { FileSourceResponse } from "../search/types"; import { FileSourceResponse } from "../search/types";
import { addLineNumbers, buildSearchQuery } from "./utils"; import { addLineNumbers, buildSearchQuery } from "./utils";
import { toolNames } from "./constants"; import { toolNames } from "./constants";
@ -36,8 +35,7 @@ export const findSymbolReferencesTool = tool({
symbolName: symbol, symbolName: symbol,
language, language,
revisionName: "HEAD", revisionName: "HEAD",
// @todo(mt): handle multi-tenancy. });
}, SINGLE_TENANT_ORG_DOMAIN);
if (isServiceError(response)) { if (isServiceError(response)) {
return response; return response;
@ -74,8 +72,7 @@ export const findSymbolDefinitionsTool = tool({
symbolName: symbol, symbolName: symbol,
language, language,
revisionName: revision, revisionName: revision,
// @todo(mt): handle multi-tenancy. });
}, SINGLE_TENANT_ORG_DOMAIN);
if (isServiceError(response)) { if (isServiceError(response)) {
return response; return response;

View file

@ -1,27 +1,19 @@
'use server'; import 'server-only';
import { sew, withAuth, withOrgMembership } from "@/actions"; import { sew } from "@/actions";
import { searchResponseSchema } from "@/features/search/schemas"; import { searchResponseSchema } from "@/features/search/schemas";
import { search } from "@/features/search/searchApi"; import { search } from "@/features/search/searchApi";
import { isServiceError } from "@/lib/utils";
import { FindRelatedSymbolsResponse } from "./types";
import { ServiceError } from "@/lib/serviceError"; import { ServiceError } from "@/lib/serviceError";
import { isServiceError } from "@/lib/utils";
import { withOptionalAuthV2 } from "@/withAuthV2";
import { SearchResponse } from "../search/types"; import { SearchResponse } from "../search/types";
import { OrgRole } from "@sourcebot/db"; import { FindRelatedSymbolsRequest, FindRelatedSymbolsResponse } from "./types";
// The maximum number of matches to return from the search API. // The maximum number of matches to return from the search API.
const MAX_REFERENCE_COUNT = 1000; const MAX_REFERENCE_COUNT = 1000;
export const findSearchBasedSymbolReferences = async ( export const findSearchBasedSymbolReferences = async (props: FindRelatedSymbolsRequest): Promise<FindRelatedSymbolsResponse | ServiceError> => sew(() =>
props: { withOptionalAuthV2(async () => {
symbolName: string,
language: string,
revisionName?: string,
},
domain: string,
): Promise<FindRelatedSymbolsResponse | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async () => {
const { const {
symbolName, symbolName,
language, language,
@ -41,20 +33,11 @@ export const findSearchBasedSymbolReferences = async (
} }
return parseRelatedSymbolsSearchResponse(searchResult); return parseRelatedSymbolsSearchResponse(searchResult);
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true) }));
);
export const findSearchBasedSymbolDefinitions = async ( export const findSearchBasedSymbolDefinitions = async (props: FindRelatedSymbolsRequest): Promise<FindRelatedSymbolsResponse | ServiceError> => sew(() =>
props: { withOptionalAuthV2(async () => {
symbolName: string,
language: string,
revisionName?: string,
},
domain: string,
): Promise<FindRelatedSymbolsResponse | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async () => {
const { const {
symbolName, symbolName,
language, language,
@ -74,8 +57,7 @@ export const findSearchBasedSymbolDefinitions = async (
} }
return parseRelatedSymbolsSearchResponse(searchResult); return parseRelatedSymbolsSearchResponse(searchResult);
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true) }));
);
const parseRelatedSymbolsSearchResponse = (searchResult: SearchResponse) => { const parseRelatedSymbolsSearchResponse = (searchResult: SearchResponse) => {
const parser = searchResponseSchema.transform(async ({ files }) => ({ const parser = searchResponseSchema.transform(async ({ files }) => ({

View file

@ -1,20 +0,0 @@
import { rangeSchema, repositoryInfoSchema } from "../search/schemas";
import { z } from "zod";
export const findRelatedSymbolsResponseSchema = z.object({
stats: z.object({
matchCount: z.number(),
}),
files: z.array(z.object({
fileName: z.string(),
repository: z.string(),
repositoryId: z.number(),
webUrl: z.string().optional(),
language: z.string(),
matches: z.array(z.object({
lineContent: z.string(),
range: rangeSchema,
}))
})),
repositoryInfo: z.array(repositoryInfoSchema),
});

View file

@ -1,4 +1,29 @@
import { z } from "zod"; import { z } from "zod";
import { findRelatedSymbolsResponseSchema } from "./schemas"; import { rangeSchema, repositoryInfoSchema } from "../search/schemas";
export const findRelatedSymbolsRequestSchema = z.object({
symbolName: z.string(),
language: z.string(),
revisionName: z.string().optional(),
});
export type FindRelatedSymbolsRequest = z.infer<typeof findRelatedSymbolsRequestSchema>;
export const findRelatedSymbolsResponseSchema = z.object({
stats: z.object({
matchCount: z.number(),
}),
files: z.array(z.object({
fileName: z.string(),
repository: z.string(),
repositoryId: z.number(),
webUrl: z.string().optional(),
language: z.string(),
matches: z.array(z.object({
lineContent: z.string(),
range: rangeSchema,
}))
})),
repositoryInfo: z.array(repositoryInfoSchema),
});
export type FindRelatedSymbolsResponse = z.infer<typeof findRelatedSymbolsResponseSchema>; export type FindRelatedSymbolsResponse = z.infer<typeof findRelatedSymbolsResponseSchema>;

View file

@ -1,4 +1,4 @@
'use server'; import 'server-only';
import { sew } from '@/actions'; import { sew } from '@/actions';
import { env } from '@sourcebot/shared'; import { env } from '@sourcebot/shared';
@ -8,19 +8,10 @@ import { Repo } from '@sourcebot/db';
import { createLogger } from '@sourcebot/shared'; import { createLogger } from '@sourcebot/shared';
import path from 'path'; import path from 'path';
import { simpleGit } from 'simple-git'; import { simpleGit } from 'simple-git';
import { FileTreeItem, FileTreeNode } from './types';
const logger = createLogger('file-tree'); const logger = createLogger('file-tree');
export type FileTreeItem = {
type: string;
path: string;
name: string;
}
export type FileTreeNode = FileTreeItem & {
children: FileTreeNode[];
}
/** /**
* Returns the tree of files (blobs) and directories (trees) for a given repository, * Returns the tree of files (blobs) and directories (trees) for a given repository,
* at a given revision. * at a given revision.
@ -218,7 +209,7 @@ const buildFileTree = (flatList: { type: string, path: string }[]): FileTreeNode
const part = parts[i]; const part = parts[i];
const isLeaf = i === parts.length - 1; const isLeaf = i === parts.length - 1;
const nodeType = isLeaf ? item.type : 'tree'; const nodeType = isLeaf ? item.type : 'tree';
let next = current.children.find(child => child.name === part && child.type === nodeType); let next = current.children.find((child: FileTreeNode) => child.name === part && child.type === nodeType);
if (!next) { if (!next) {
next = { next = {
@ -240,7 +231,7 @@ const buildFileTree = (flatList: { type: string, path: string }[]): FileTreeNode
const sortedChildren = node.children const sortedChildren = node.children
.map(sortTree) .map(sortTree)
.sort((a, b) => { .sort((a: FileTreeNode, b: FileTreeNode) => {
if (a.type !== b.type) { if (a.type !== b.type) {
return a.type === 'tree' ? -1 : 1; return a.type === 'tree' ? -1 : 1;
} }

View file

@ -1,12 +1,12 @@
'use client'; 'use client';
import { FileTreeItem } from "../actions";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import clsx from "clsx"; 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"; import Link from "next/link";
import { FileTreeItem } from "../types";
export const FileTreeItemComponent = ({ export const FileTreeItemComponent = ({
node, node,

View file

@ -1,9 +1,9 @@
'use client'; 'use client';
import { FileTreeItem } from "../actions";
import { useMemo } from "react"; import { useMemo } from "react";
import { VscodeFolderIcon } from "@/app/components/vscodeFolderIcon"; import { VscodeFolderIcon } from "@/app/components/vscodeFolderIcon";
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon"; import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
import { FileTreeItem } from "../types";
interface FileTreeItemIconProps { interface FileTreeItemIconProps {
item: FileTreeItem; item: FileTreeItem;

View file

@ -1,26 +1,25 @@
'use client'; 'use client';
import { getTree } from "../actions"; import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams";
import { useQuery } from "@tanstack/react-query";
import { unwrapServiceError } from "@/lib/utils";
import { ResizablePanel } from "@/components/ui/resizable";
import { Skeleton } from "@/components/ui/skeleton";
import { useBrowseState } from "@/app/[domain]/browse/hooks/useBrowseState"; import { useBrowseState } from "@/app/[domain]/browse/hooks/useBrowseState";
import { PureFileTreePanel } from "./pureFileTreePanel"; import { getTree } from "@/app/api/(client)/client";
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ImperativePanelHandle } from "react-resizable-panels"; import { ResizablePanel } from "@/components/ui/resizable";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { unwrapServiceError } from "@/lib/utils";
import { useQuery } from "@tanstack/react-query";
import { SearchIcon } from "lucide-react";
import { useRef } from "react"; import { useRef } from "react";
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from "react-hotkeys-hook";
import { Separator } from "@/components/ui/separator";
import { import {
GoSidebarCollapse as ExpandIcon, GoSidebarExpand as CollapseIcon,
GoSidebarExpand as CollapseIcon GoSidebarCollapse as ExpandIcon
} from "react-icons/go"; } from "react-icons/go";
import { Tooltip, TooltipContent } from "@/components/ui/tooltip"; import { ImperativePanelHandle } from "react-resizable-panels";
import { TooltipTrigger } from "@/components/ui/tooltip"; import { PureFileTreePanel } from "./pureFileTreePanel";
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams";
import { SearchIcon } from "lucide-react";
interface FileTreePanelProps { interface FileTreePanelProps {

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { FileTreeNode as RawFileTreeNode } from "../actions"; import { FileTreeNode as RawFileTreeNode } from "../types";
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";

View file

@ -0,0 +1,44 @@
import { z } from "zod";
export const getTreeRequestSchema = z.object({
repoName: z.string(),
revisionName: z.string(),
});
export type GetTreeRequest = z.infer<typeof getTreeRequestSchema>;
export const getFilesRequestSchema = z.object({
repoName: z.string(),
revisionName: z.string(),
});
export type GetFilesRequest = z.infer<typeof getFilesRequestSchema>;
export const fileTreeItemSchema = z.object({
type: z.string(),
path: z.string(),
name: z.string(),
});
export type FileTreeItem = z.infer<typeof fileTreeItemSchema>;
type FileTreeNodeType = {
type: string;
path: string;
name: string;
children: FileTreeNodeType[];
};
export const fileTreeNodeSchema: z.ZodType<FileTreeNodeType> = z.lazy(() => z.object({
type: z.string(),
path: z.string(),
name: z.string(),
children: z.array(fileTreeNodeSchema),
}));
export type FileTreeNode = z.infer<typeof fileTreeNodeSchema>;
export const getTreeResponseSchema = z.object({
tree: fileTreeNodeSchema,
});
export type GetTreeResponse = z.infer<typeof getTreeResponseSchema>;
export const getFilesResponseSchema = z.array(fileTreeItemSchema);
export type GetFilesResponse = z.infer<typeof getFilesResponseSchema>;

View file

@ -1,5 +1,4 @@
'use server'; import 'server-only';
import escapeStringRegexp from "escape-string-regexp"; import escapeStringRegexp from "escape-string-regexp";
import { fileNotFound, ServiceError, unexpectedError } from "../../lib/serviceError"; import { fileNotFound, ServiceError, unexpectedError } from "../../lib/serviceError";
import { FileSourceRequest, FileSourceResponse } from "./types"; import { FileSourceRequest, FileSourceResponse } from "./types";

View file

@ -1,5 +1,4 @@
'use server'; import 'server-only';
import { sew } from "@/actions"; import { sew } from "@/actions";
import { withOptionalAuthV2 } from "@/withAuthV2"; import { withOptionalAuthV2 } from "@/withAuthV2";
import { PrismaClient, Repo } from "@sourcebot/db"; import { PrismaClient, Repo } from "@sourcebot/db";