diff --git a/ee/tabby-ui/app/files/components/file-tree-header.tsx b/ee/tabby-ui/app/files/components/file-tree-header.tsx index a24ad4e88148..7deda53049e4 100644 --- a/ee/tabby-ui/app/files/components/file-tree-header.tsx +++ b/ee/tabby-ui/app/files/components/file-tree-header.tsx @@ -1,9 +1,10 @@ 'use client' import React, { useContext } from 'react' +import { useQuery } from 'urql' +import { graphql } from '@/lib/gql/generates' import { useDebounceCallback } from '@/lib/hooks/use-debounce' -import useRouterStuff from '@/lib/hooks/use-router-stuff' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { @@ -27,12 +28,15 @@ import { SelectValue } from '@/components/ui/select' -import { SourceCodeBrowserContext } from './source-code-browser' -import { resolveRepoNameFromPath } from './utils' -import { graphql } from '@/lib/gql/generates' -import { useQuery } from 'urql' +import { SourceCodeBrowserContext, TFileMap } from './source-code-browser' +import { + fetchEntriesFromPath, + getDirectoriesFromBasename, + resolveFileNameFromPath, + resolveRepoNameFromPath +} from './utils' -interface FileTreeHeaderProps extends React.HTMLAttributes { } +interface FileTreeHeaderProps extends React.HTMLAttributes {} type SearchOption = { path: string; type: string; id: string } @@ -49,16 +53,21 @@ const FileTreeHeader: React.FC = ({ className, ...props }) => { - const { activePath, fileTreeData, setActivePath, initialized } = useContext( - SourceCodeBrowserContext - ) - const { updateSearchParams } = useRouterStuff() + const { + activePath, + fileTreeData, + setActivePath, + initialized, + updateFileMap, + setExpandedKeys + } = useContext(SourceCodeBrowserContext) const curerntRepoName = resolveRepoNameFromPath(activePath) const inputRef = React.useRef(null) const ignoreFetchResultRef = React.useRef(false) const [input, setInput] = React.useState() - const [searchFileter, setSearchFilter] = React.useState() + const [repositorySearchFilter, setRepositorySearchFilter] = + React.useState() const [options, setOptions] = React.useState>() const [optionsVisible, setOptionsVisible] = React.useState(false) @@ -66,23 +75,25 @@ const FileTreeHeader: React.FC = ({ const noIndexedRepo = initialized && !fileTreeData?.length - - const [{ data }] = useQuery({ + const [{ data: repositorySearchData }] = useQuery({ query: repositorySearch, - // todo - pause: !searchFileter, - variables: { repository: curerntRepoName, filter: searchFileter, topN: 20 } + variables: { + repository: curerntRepoName, + filter: repositorySearchFilter, + topN: 20 + }, + pause: !repositorySearchFilter }) React.useEffect(() => { - const _options = data?.repositorySearch?.map(option => ({ - ...option, - id: option.path - })) ?? [] + const _options = + repositorySearchData?.repositorySearch?.map(option => ({ + ...option, + id: option.path + })) ?? [] setOptions(_options) setOptionsVisible(!!_options?.length) - }, [data?.repositorySearch]) - + }, [repositorySearchData?.repositorySearch]) const onSelectRepo = (name: string) => { setActivePath(name) @@ -90,46 +101,63 @@ const FileTreeHeader: React.FC = ({ const memoizedMatchedIndices = React.useMemo(() => { return options?.map(option => - searchFileter ? getMatchedIndices(searchFileter, option.path) : [] + repositorySearchFilter + ? getMatchedIndices(repositorySearchFilter, option.path) + : [] ) - }, [options, searchFileter]) + }, [options, repositorySearchFilter]) const onInputValueChange = useDebounceCallback((v: string | undefined) => { if (!v) { ignoreFetchResultRef.current = true - setSearchFilter('') + setRepositorySearchFilter('') setOptionsVisible(false) } else { ignoreFetchResultRef.current = false - setSearchFilter(v) - // if (v === 'test-not-found') { - // ignoreFetchResultRef.current = true - // setOptions([]) - // setOptionsVisible(true) - // } - // setTimeout(() => { - // if (!ignoreFetchResultRef.current) { - // let ops: SearchOption[] = [ - // { entry: 'test1', type: 'file', id: 'test1' }, - // { entry: 'test2', type: 'file', id: 'test2' }, - // { entry: 'path/to/test', type: 'dir', id: 'path/to/test' } - // ] - // setSearchFilter(v) - // setOptions(ops) - // setOptionsVisible(true) - // } - // }, 100) + setRepositorySearchFilter(v) } - }, 300) + }, 500) const onClearInput = () => { onInputValueChange.run('') onInputValueChange.flush() } - const onSelectFile = (value: SearchOption) => { - // todo fetch dirs and then update activePath, or implement this logic in code borwser fetcher - console.log(value) + const onSelectFile = async (value: SearchOption) => { + const path = value.path + if (!path) return + + const fullPath = `${repoName}/${path}` + const entries = await fetchEntriesFromPath(fullPath) + const initialExpandedDirs = getDirectoriesFromBasename(path) + + const patchMap: TFileMap = {} + // fetch dirs + for (const entry of entries) { + const path = `${repoName}/${entry.basename}` + patchMap[path] = { + file: entry, + name: resolveFileNameFromPath(path), + fullPath: path, + treeExpanded: initialExpandedDirs.includes(entry.basename) + } + } + const expandedKeys = initialExpandedDirs.map(dir => + [repoName, dir].filter(Boolean).join('/') + ) + if (patchMap) { + updateFileMap(patchMap) + } + if (expandedKeys?.length) { + setExpandedKeys(prevKeys => { + const newSet = new Set(prevKeys) + for (const k of expandedKeys) { + newSet.add(k) + } + return newSet + }) + } + setActivePath(fullPath) } // shortcut 't' @@ -210,10 +238,16 @@ const FileTreeHeader: React.FC = ({
{ let value = e.target.value setInput(value) diff --git a/ee/tabby-ui/app/files/components/source-code-browser.tsx b/ee/tabby-ui/app/files/components/source-code-browser.tsx index 680930a83dce..e83f66c58957 100644 --- a/ee/tabby-ui/app/files/components/source-code-browser.tsx +++ b/ee/tabby-ui/app/files/components/source-code-browser.tsx @@ -25,6 +25,7 @@ import { FileTreePanel } from './file-tree-panel' import { RawFileView } from './raw-file-view' import { TextFileView } from './text-file-view' import { + fetchEntriesFromPath, getDirectoriesFromBasename, resolveBasenameFromPath, resolveFileNameFromPath, @@ -261,7 +262,7 @@ const SourceCodeBrowserRenderer: React.FC = ({ React.useEffect(() => { const init = async () => { - const { patchMap, expandedKeys } = await getInitialFileMap(activePath) + const { patchMap, expandedKeys } = await getInitialFileData(activePath) if (patchMap) { updateFileMap(patchMap) } @@ -364,7 +365,7 @@ const SourceCodeBrowser: React.FC = props => { ) } -async function getInitialFileMap(path?: string) { +async function getInitialFileData(path?: string) { const initialRepositoryName = resolveRepoNameFromPath(path) const initialBasename = resolveBasenameFromPath(path) @@ -442,27 +443,6 @@ async function getInitialFileMap(path?: string) { } } -async function fetchEntriesFromPath(path: string | undefined) { - if (!path) return [] - const repoName = resolveRepoNameFromPath(path) - const basename = resolveBasenameFromPath(path) - // array of dir basename that do not include the repo name. - const directoryPaths = getDirectoriesFromBasename(basename) - // fetch all dirs from path - const requests: Array<() => Promise> = - directoryPaths.map( - dir => () => fetcher(`/repositories/${repoName}/resolve/${dir}`).catch(e => []) - ) - const entries = await Promise.all(requests.map(fn => fn())) - let result: TFile[] = [] - for (let entry of entries) { - if (entry?.entries?.length) { - result = [...result, ...entry.entries] - } - } - return result -} - function isReadableTextFile(blob: Blob) { return new Promise((resolve, reject) => { const chunkSize = 1024 diff --git a/ee/tabby-ui/app/files/components/utils.ts b/ee/tabby-ui/app/files/components/utils.ts index 971751d1064e..32b83390574a 100644 --- a/ee/tabby-ui/app/files/components/utils.ts +++ b/ee/tabby-ui/app/files/components/utils.ts @@ -1,5 +1,8 @@ import { isNil } from 'lodash-es' +import fetcher from '@/lib/tabby/fetcher' +import { ResolveEntriesResponse, TFile } from '@/lib/types' + function resolveRepoNameFromPath(path: string | undefined) { if (!path) return '' return path.split('/')?.[0] @@ -29,9 +32,32 @@ function getDirectoriesFromBasename(path: string, isDir?: boolean): string[] { return result } +async function fetchEntriesFromPath(path: string | undefined) { + if (!path) return [] + const repoName = resolveRepoNameFromPath(path) + const basename = resolveBasenameFromPath(path) + // array of dir basename that do not include the repo name. + const directoryPaths = getDirectoriesFromBasename(basename) + // fetch all dirs from path + const requests: Array<() => Promise> = + directoryPaths.map( + dir => () => + fetcher(`/repositories/${repoName}/resolve/${dir}`).catch(e => []) + ) + const entries = await Promise.all(requests.map(fn => fn())) + let result: TFile[] = [] + for (let entry of entries) { + if (entry?.entries?.length) { + result = [...result, ...entry.entries] + } + } + return result +} + export { resolveRepoNameFromPath, resolveBasenameFromPath, resolveFileNameFromPath, - getDirectoriesFromBasename + getDirectoriesFromBasename, + fetchEntriesFromPath }