diff --git a/CHANGELOG.md b/CHANGELOG.md index 613ba1b68..db775061b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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 ### Changed diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx index b38d140b3..7fc2af07e 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx @@ -1,10 +1,10 @@ import { getRepoInfoByName } from "@/actions"; 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 { PureCodePreviewPanel } from "./pureCodePreviewPanel"; +import { getFileSource } from "@/features/search/fileSourceApi"; interface CodePreviewPanelProps { path: string; diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx index 83c9528ef..269647360 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx @@ -1,12 +1,12 @@ 'use client'; import { useRef } from "react"; -import { FileTreeItem } from "@/features/fileTree/actions"; import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeItemComponent"; import { getBrowsePath } from "../../hooks/utils"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useBrowseParams } from "../../hooks/useBrowseParams"; import { useDomain } from "@/hooks/useDomain"; +import { FileTreeItem } from "@/features/fileTree/types"; interface PureTreePreviewPanelProps { items: FileTreeItem[]; diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx index 4a0c38576..8d6b335c0 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx @@ -2,7 +2,7 @@ import { Separator } from "@/components/ui/separator"; import { getRepoInfoByName } from "@/actions"; 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 { PureTreePreviewPanel } from "./pureTreePreviewPanel"; diff --git a/packages/web/src/app/[domain]/browse/components/fileSearchCommandDialog.tsx b/packages/web/src/app/[domain]/browse/components/fileSearchCommandDialog.tsx index 0cfe720a4..dd1014d10 100644 --- a/packages/web/src/app/[domain]/browse/components/fileSearchCommandDialog.tsx +++ b/packages/web/src/app/[domain]/browse/components/fileSearchCommandDialog.tsx @@ -5,7 +5,6 @@ import { useState, useRef, useMemo, useEffect, useCallback } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { useQuery } from "@tanstack/react-query"; import { unwrapServiceError } from "@/lib/utils"; -import { FileTreeItem, getFiles } from "@/features/fileTree/actions"; import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"; import { useBrowseNavigation } from "../hooks/useBrowseNavigation"; import { useBrowseState } from "../hooks/useBrowseState"; @@ -13,6 +12,8 @@ import { useBrowseParams } from "../hooks/useBrowseParams"; import { FileTreeItemIcon } from "@/features/fileTree/components/fileTreeItemIcon"; import { useLocalStorage } from "usehooks-ts"; import { Skeleton } from "@/components/ui/skeleton"; +import { FileTreeItem } from "@/features/fileTree/types"; +import { getFiles } from "@/app/api/(client)/client"; const MAX_RESULTS = 100; diff --git a/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts b/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts index 8c48e9d14..69e1040c0 100644 --- a/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts +++ b/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts @@ -55,7 +55,7 @@ export const useSuggestionsData = ({ query: `file:${suggestionQuery}`, matches: 15, contextLines: 1, - }, domain), + }), select: (data): Suggestion[] => { if (isServiceError(data)) { return []; @@ -75,7 +75,7 @@ export const useSuggestionsData = ({ query: `sym:${suggestionQuery.length > 0 ? suggestionQuery : ".*"}`, matches: 15, contextLines: 1, - }, domain), + }), select: (data): Suggestion[] => { if (isServiceError(data)) { return []; diff --git a/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx b/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx index c4aaef20e..d1a7e66f0 100644 --- a/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx +++ b/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx @@ -5,8 +5,8 @@ import { CodePreview } from "./codePreview"; import { SearchResultFile } from "@/features/search/types"; import { SymbolIcon } from "@radix-ui/react-icons"; import { SetStateAction, Dispatch, useMemo } from "react"; -import { getFileSource } from "@/features/search/fileSourceApi"; import { unwrapServiceError } from "@/lib/utils"; +import { getFileSource } from "@/app/api/(client)/client"; interface CodePreviewPanelProps { previewedFile: SearchResultFile; diff --git a/packages/web/src/app/[domain]/search/components/searchResultsPage.tsx b/packages/web/src/app/[domain]/search/components/searchResultsPage.tsx index e1d8062a3..9c33e11d4 100644 --- a/packages/web/src/app/[domain]/search/components/searchResultsPage.tsx +++ b/packages/web/src/app/[domain]/search/components/searchResultsPage.tsx @@ -66,7 +66,7 @@ export const SearchResultsPage = ({ matches: maxMatchCount, contextLines: 3, whole: false, - }, domain)), "client.search"), + })), "client.search"), select: ({ data, durationMs }) => ({ ...data, totalClientSearchDurationMs: durationMs, diff --git a/packages/web/src/app/api/(client)/client.ts b/packages/web/src/app/api/(client)/client.ts index 7e5466d8c..6b4b29772 100644 --- a/packages/web/src/app/api/(client)/client.ts +++ b/packages/web/src/app/api/(client)/client.ts @@ -9,13 +9,22 @@ import { SearchRequest, SearchResponse, } 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 => { +export const search = async (body: SearchRequest): Promise => { const result = await fetch("/api/search", { method: "POST", headers: { "Content-Type": "application/json", - "X-Org-Domain": domain, }, body: JSON.stringify(body), }).then(response => response.json()); @@ -27,12 +36,11 @@ export const search = async (body: SearchRequest, domain: string): Promise => { +export const getFileSource = async (body: FileSourceRequest): Promise => { const result = await fetch("/api/source", { method: "POST", headers: { "Content-Type": "application/json", - "X-Org-Domain": domain, }, body: JSON.stringify(body), }).then(response => response.json()); @@ -60,3 +68,35 @@ export const getVersion = async (): Promise => { }).then(response => response.json()); return result as GetVersionResponse; } + +export const findSearchBasedSymbolReferences = async (body: FindRelatedSymbolsRequest): Promise => { + 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 => { + 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 => { + 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 => { + const result = await fetch("/api/files", { + method: "POST", + body: JSON.stringify(body), + }).then(response => response.json()); + return result as GetFilesResponse | ServiceError; +} \ No newline at end of file diff --git a/packages/web/src/app/api/(server)/files/route.ts b/packages/web/src/app/api/(server)/files/route.ts new file mode 100644 index 000000000..70d1330b9 --- /dev/null +++ b/packages/web/src/app/api/(server)/files/route.ts @@ -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); +} + diff --git a/packages/web/src/app/api/(server)/find_definitions/route.ts b/packages/web/src/app/api/(server)/find_definitions/route.ts new file mode 100644 index 000000000..d8abfa38d --- /dev/null +++ b/packages/web/src/app/api/(server)/find_definitions/route.ts @@ -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); +} \ No newline at end of file diff --git a/packages/web/src/app/api/(server)/find_references/route.ts b/packages/web/src/app/api/(server)/find_references/route.ts new file mode 100644 index 000000000..4e4b729bb --- /dev/null +++ b/packages/web/src/app/api/(server)/find_references/route.ts @@ -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); +} \ No newline at end of file diff --git a/packages/web/src/app/api/(server)/tree/route.ts b/packages/web/src/app/api/(server)/tree/route.ts new file mode 100644 index 000000000..efe63bffe --- /dev/null +++ b/packages/web/src/app/api/(server)/tree/route.ts @@ -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); +} + diff --git a/packages/web/src/ee/features/codeNav/components/exploreMenu/index.tsx b/packages/web/src/ee/features/codeNav/components/exploreMenu/index.tsx index 70b825c4d..eedc54727 100644 --- a/packages/web/src/ee/features/codeNav/components/exploreMenu/index.tsx +++ b/packages/web/src/ee/features/codeNav/components/exploreMenu/index.tsx @@ -1,11 +1,11 @@ 'use client'; import { useBrowseState } from "@/app/[domain]/browse/hooks/useBrowseState"; +import { findSearchBasedSymbolReferences, findSearchBasedSymbolDefinitions} from "@/app/api/(client)/client"; import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle"; import { Badge } from "@/components/ui/badge"; import { ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { findSearchBasedSymbolDefinitions, findSearchBasedSymbolReferences } from "@/features/codeNav/actions"; import { useDomain } from "@/hooks/useDomain"; import { unwrapServiceError } from "@/lib/utils"; import { useQuery } from "@tanstack/react-query"; @@ -46,7 +46,7 @@ export const ExploreMenu = ({ symbolName: selectedSymbolInfo.symbolName, language: selectedSymbolInfo.language, revisionName: selectedSymbolInfo.revisionName, - }, domain) + }) ), }); @@ -62,7 +62,7 @@ export const ExploreMenu = ({ symbolName: selectedSymbolInfo.symbolName, language: selectedSymbolInfo.language, revisionName: selectedSymbolInfo.revisionName, - }, domain) + }) ), }); diff --git a/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo.ts b/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo.ts index f21462b1f..03752820f 100644 --- a/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo.ts +++ b/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo.ts @@ -1,4 +1,4 @@ -import { findSearchBasedSymbolDefinitions } from "@/features/codeNav/actions"; +import { findSearchBasedSymbolDefinitions } from "@/app/api/(client)/client"; import { SourceRange } from "@/features/search/types"; import { useDomain } from "@/hooks/useDomain"; import { unwrapServiceError } from "@/lib/utils"; @@ -56,7 +56,7 @@ export const useHoveredOverSymbolInfo = ({ symbolName: symbolName!, language, revisionName, - }, domain) + }) ), select: ((data) => { return data.files.flatMap((file) => { diff --git a/packages/web/src/features/chat/agent.ts b/packages/web/src/features/chat/agent.ts index 0c722c27c..2da325529 100644 --- a/packages/web/src/features/chat/agent.ts +++ b/packages/web/src/features/chat/agent.ts @@ -251,7 +251,6 @@ const resolveFileSource = async ({ path, repo, revision }: FileSource) => { fileName: path, repository: repo, branch: revision, - // @todo: handle multi-tenancy. }); if (isServiceError(fileSource)) { diff --git a/packages/web/src/features/chat/components/chatBox/useSuggestionsData.ts b/packages/web/src/features/chat/components/chatBox/useSuggestionsData.ts index 59f52b0cd..4adf9694f 100644 --- a/packages/web/src/features/chat/components/chatBox/useSuggestionsData.ts +++ b/packages/web/src/features/chat/components/chatBox/useSuggestionsData.ts @@ -41,7 +41,7 @@ export const useSuggestionsData = ({ query, matches: 10, contextLines: 1, - }, domain)) + })) }, select: (data): FileSuggestion[] => { return data.files.map((file) => { diff --git a/packages/web/src/features/chat/components/chatThread/referencedSourcesListView.tsx b/packages/web/src/features/chat/components/chatThread/referencedSourcesListView.tsx index c40f9fb95..b24085d83 100644 --- a/packages/web/src/features/chat/components/chatThread/referencedSourcesListView.tsx +++ b/packages/web/src/features/chat/components/chatThread/referencedSourcesListView.tsx @@ -1,6 +1,6 @@ 'use client'; -import { fetchFileSource } from "@/app/api/(client)/client"; +import { getFileSource } from "@/app/api/(client)/client"; import { VscodeFileIcon } from "@/app/components/vscodeFileIcon"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Skeleton } from "@/components/ui/skeleton"; @@ -99,11 +99,11 @@ export const ReferencedSourcesListView = ({ const fileSourceQueries = useQueries({ queries: referencedFileSources.map((file) => ({ queryKey: ['fileSource', file.path, file.repo, file.revision, domain], - queryFn: () => unwrapServiceError(fetchFileSource({ + queryFn: () => unwrapServiceError(getFileSource({ fileName: file.path, repository: file.repo, branch: file.revision, - }, domain)), + })), staleTime: Infinity, })), }); diff --git a/packages/web/src/features/chat/tools.ts b/packages/web/src/features/chat/tools.ts index f69c5f341..ab2b2ee61 100644 --- a/packages/web/src/features/chat/tools.ts +++ b/packages/web/src/features/chat/tools.ts @@ -1,10 +1,9 @@ import { z } from "zod" import { search } from "@/features/search/searchApi" -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants" import { InferToolInput, InferToolOutput, InferUITool, tool, ToolUIPart } from "ai"; import { isServiceError } from "@/lib/utils"; import { getFileSource } from "../search/fileSourceApi"; -import { findSearchBasedSymbolDefinitions, findSearchBasedSymbolReferences } from "../codeNav/actions"; +import { findSearchBasedSymbolDefinitions, findSearchBasedSymbolReferences } from "../codeNav/api"; import { FileSourceResponse } from "../search/types"; import { addLineNumbers, buildSearchQuery } from "./utils"; import { toolNames } from "./constants"; @@ -36,8 +35,7 @@ export const findSymbolReferencesTool = tool({ symbolName: symbol, language, revisionName: "HEAD", - // @todo(mt): handle multi-tenancy. - }, SINGLE_TENANT_ORG_DOMAIN); + }); if (isServiceError(response)) { return response; @@ -74,8 +72,7 @@ export const findSymbolDefinitionsTool = tool({ symbolName: symbol, language, revisionName: revision, - // @todo(mt): handle multi-tenancy. - }, SINGLE_TENANT_ORG_DOMAIN); + }); if (isServiceError(response)) { return response; diff --git a/packages/web/src/features/codeNav/actions.ts b/packages/web/src/features/codeNav/api.ts similarity index 59% rename from packages/web/src/features/codeNav/actions.ts rename to packages/web/src/features/codeNav/api.ts index 839ef3819..1865ee532 100644 --- a/packages/web/src/features/codeNav/actions.ts +++ b/packages/web/src/features/codeNav/api.ts @@ -1,60 +1,43 @@ -'use server'; +import 'server-only'; -import { sew, withAuth, withOrgMembership } from "@/actions"; +import { sew } from "@/actions"; import { searchResponseSchema } from "@/features/search/schemas"; import { search } from "@/features/search/searchApi"; -import { isServiceError } from "@/lib/utils"; -import { FindRelatedSymbolsResponse } from "./types"; import { ServiceError } from "@/lib/serviceError"; +import { isServiceError } from "@/lib/utils"; +import { withOptionalAuthV2 } from "@/withAuthV2"; 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. const MAX_REFERENCE_COUNT = 1000; -export const findSearchBasedSymbolReferences = async ( - props: { - symbolName: string, - language: string, - revisionName?: string, - }, - domain: string, -): Promise => sew(() => - withAuth((session) => - withOrgMembership(session, domain, async () => { - const { - symbolName, - language, - revisionName = "HEAD", - } = props; +export const findSearchBasedSymbolReferences = async (props: FindRelatedSymbolsRequest): Promise => sew(() => + withOptionalAuthV2(async () => { + const { + symbolName, + language, + revisionName = "HEAD", + } = props; - const query = `\\b${symbolName}\\b rev:${revisionName} ${getExpandedLanguageFilter(language)} case:yes`; + const query = `\\b${symbolName}\\b rev:${revisionName} ${getExpandedLanguageFilter(language)} case:yes`; - const searchResult = await search({ - query, - matches: MAX_REFERENCE_COUNT, - contextLines: 0, - }); + const searchResult = await search({ + query, + matches: MAX_REFERENCE_COUNT, + contextLines: 0, + }); - if (isServiceError(searchResult)) { - return searchResult; - } + if (isServiceError(searchResult)) { + return searchResult; + } - return parseRelatedSymbolsSearchResponse(searchResult); - }, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true) -); - - -export const findSearchBasedSymbolDefinitions = async ( - props: { - symbolName: string, - language: string, - revisionName?: string, - }, - domain: string, -): Promise => sew(() => - withAuth((session) => - withOrgMembership(session, domain, async () => { + return parseRelatedSymbolsSearchResponse(searchResult); + })); + + +export const findSearchBasedSymbolDefinitions = async (props: FindRelatedSymbolsRequest): Promise => sew(() => + withOptionalAuthV2(async () => { const { symbolName, language, @@ -74,8 +57,7 @@ export const findSearchBasedSymbolDefinitions = async ( } return parseRelatedSymbolsSearchResponse(searchResult); - }, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true) -); + })); const parseRelatedSymbolsSearchResponse = (searchResult: SearchResponse) => { const parser = searchResponseSchema.transform(async ({ files }) => ({ diff --git a/packages/web/src/features/codeNav/schemas.ts b/packages/web/src/features/codeNav/schemas.ts deleted file mode 100644 index 03f207211..000000000 --- a/packages/web/src/features/codeNav/schemas.ts +++ /dev/null @@ -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), -}); \ No newline at end of file diff --git a/packages/web/src/features/codeNav/types.ts b/packages/web/src/features/codeNav/types.ts index bb9a282b6..07f3cefd0 100644 --- a/packages/web/src/features/codeNav/types.ts +++ b/packages/web/src/features/codeNav/types.ts @@ -1,4 +1,29 @@ 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; + +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; diff --git a/packages/web/src/features/fileTree/actions.ts b/packages/web/src/features/fileTree/api.ts similarity index 96% rename from packages/web/src/features/fileTree/actions.ts rename to packages/web/src/features/fileTree/api.ts index a861670d7..e5f34e89d 100644 --- a/packages/web/src/features/fileTree/actions.ts +++ b/packages/web/src/features/fileTree/api.ts @@ -1,4 +1,4 @@ -'use server'; +import 'server-only'; import { sew } from '@/actions'; import { env } from '@sourcebot/shared'; @@ -8,19 +8,10 @@ import { Repo } from '@sourcebot/db'; import { createLogger } from '@sourcebot/shared'; import path from 'path'; import { simpleGit } from 'simple-git'; +import { FileTreeItem, FileTreeNode } from './types'; 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, * at a given revision. @@ -218,7 +209,7 @@ const buildFileTree = (flatList: { type: string, path: string }[]): FileTreeNode const part = parts[i]; const isLeaf = i === parts.length - 1; 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) { next = { @@ -240,7 +231,7 @@ const buildFileTree = (flatList: { type: string, path: string }[]): FileTreeNode const sortedChildren = node.children .map(sortTree) - .sort((a, b) => { + .sort((a: FileTreeNode, b: FileTreeNode) => { if (a.type !== b.type) { return a.type === 'tree' ? -1 : 1; } diff --git a/packages/web/src/features/fileTree/components/fileTreeItemComponent.tsx b/packages/web/src/features/fileTree/components/fileTreeItemComponent.tsx index 17fc1ed30..aa1cb7238 100644 --- a/packages/web/src/features/fileTree/components/fileTreeItemComponent.tsx +++ b/packages/web/src/features/fileTree/components/fileTreeItemComponent.tsx @@ -1,12 +1,12 @@ 'use client'; -import { FileTreeItem } from "../actions"; import { useEffect, useRef } from "react"; 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"; +import { FileTreeItem } from "../types"; export const FileTreeItemComponent = ({ node, diff --git a/packages/web/src/features/fileTree/components/fileTreeItemIcon.tsx b/packages/web/src/features/fileTree/components/fileTreeItemIcon.tsx index e685899a5..921ae1269 100644 --- a/packages/web/src/features/fileTree/components/fileTreeItemIcon.tsx +++ b/packages/web/src/features/fileTree/components/fileTreeItemIcon.tsx @@ -1,9 +1,9 @@ 'use client'; -import { FileTreeItem } from "../actions"; import { useMemo } from "react"; import { VscodeFolderIcon } from "@/app/components/vscodeFolderIcon"; import { VscodeFileIcon } from "@/app/components/vscodeFileIcon"; +import { FileTreeItem } from "../types"; interface FileTreeItemIconProps { item: FileTreeItem; diff --git a/packages/web/src/features/fileTree/components/fileTreePanel.tsx b/packages/web/src/features/fileTree/components/fileTreePanel.tsx index a7579628d..eb751bacf 100644 --- a/packages/web/src/features/fileTree/components/fileTreePanel.tsx +++ b/packages/web/src/features/fileTree/components/fileTreePanel.tsx @@ -1,26 +1,25 @@ 'use client'; -import { getTree } from "../actions"; -import { useQuery } from "@tanstack/react-query"; -import { unwrapServiceError } from "@/lib/utils"; -import { ResizablePanel } from "@/components/ui/resizable"; -import { Skeleton } from "@/components/ui/skeleton"; +import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams"; 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 { 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 { useHotkeys } from "react-hotkeys-hook"; -import { Separator } from "@/components/ui/separator"; import { - GoSidebarCollapse as ExpandIcon, - GoSidebarExpand as CollapseIcon + GoSidebarExpand as CollapseIcon, + GoSidebarCollapse as ExpandIcon } from "react-icons/go"; -import { Tooltip, TooltipContent } from "@/components/ui/tooltip"; -import { TooltipTrigger } from "@/components/ui/tooltip"; -import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint"; -import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams"; -import { SearchIcon } from "lucide-react"; +import { ImperativePanelHandle } from "react-resizable-panels"; +import { PureFileTreePanel } from "./pureFileTreePanel"; interface FileTreePanelProps { diff --git a/packages/web/src/features/fileTree/components/pureFileTreePanel.tsx b/packages/web/src/features/fileTree/components/pureFileTreePanel.tsx index 77b4622a0..9e8811292 100644 --- a/packages/web/src/features/fileTree/components/pureFileTreePanel.tsx +++ b/packages/web/src/features/fileTree/components/pureFileTreePanel.tsx @@ -1,6 +1,6 @@ 'use client'; -import { FileTreeNode as RawFileTreeNode } from "../actions"; +import { FileTreeNode as RawFileTreeNode } from "../types"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import React, { useCallback, useMemo, useState, useEffect, useRef } from "react"; import { FileTreeItemComponent } from "./fileTreeItemComponent"; diff --git a/packages/web/src/features/fileTree/types.ts b/packages/web/src/features/fileTree/types.ts new file mode 100644 index 000000000..0f0318f45 --- /dev/null +++ b/packages/web/src/features/fileTree/types.ts @@ -0,0 +1,44 @@ +import { z } from "zod"; + +export const getTreeRequestSchema = z.object({ + repoName: z.string(), + revisionName: z.string(), +}); +export type GetTreeRequest = z.infer; + +export const getFilesRequestSchema = z.object({ + repoName: z.string(), + revisionName: z.string(), +}); +export type GetFilesRequest = z.infer; + +export const fileTreeItemSchema = z.object({ + type: z.string(), + path: z.string(), + name: z.string(), +}); +export type FileTreeItem = z.infer; + +type FileTreeNodeType = { + type: string; + path: string; + name: string; + children: FileTreeNodeType[]; +}; + +export const fileTreeNodeSchema: z.ZodType = z.lazy(() => z.object({ + type: z.string(), + path: z.string(), + name: z.string(), + children: z.array(fileTreeNodeSchema), +})); +export type FileTreeNode = z.infer; + +export const getTreeResponseSchema = z.object({ + tree: fileTreeNodeSchema, +}); +export type GetTreeResponse = z.infer; + +export const getFilesResponseSchema = z.array(fileTreeItemSchema); +export type GetFilesResponse = z.infer; + diff --git a/packages/web/src/features/search/fileSourceApi.ts b/packages/web/src/features/search/fileSourceApi.ts index 68c6a286b..edc346aaa 100644 --- a/packages/web/src/features/search/fileSourceApi.ts +++ b/packages/web/src/features/search/fileSourceApi.ts @@ -1,5 +1,4 @@ -'use server'; - +import 'server-only'; import escapeStringRegexp from "escape-string-regexp"; import { fileNotFound, ServiceError, unexpectedError } from "../../lib/serviceError"; import { FileSourceRequest, FileSourceResponse } from "./types"; diff --git a/packages/web/src/features/search/searchApi.ts b/packages/web/src/features/search/searchApi.ts index d480c96ab..1ca57ef46 100644 --- a/packages/web/src/features/search/searchApi.ts +++ b/packages/web/src/features/search/searchApi.ts @@ -1,5 +1,4 @@ -'use server'; - +import 'server-only'; import { sew } from "@/actions"; import { withOptionalAuthV2 } from "@/withAuthV2"; import { PrismaClient, Repo } from "@sourcebot/db";