diff --git a/ee/tabby-ui/app/files/components/chat-side-bar.tsx b/ee/tabby-ui/app/files/components/chat-side-bar.tsx index 0153943b06c2..647011d90c10 100644 --- a/ee/tabby-ui/app/files/components/chat-side-bar.tsx +++ b/ee/tabby-ui/app/files/components/chat-side-bar.tsx @@ -49,7 +49,6 @@ export const ChatSideBar: React.FC = ({ const fullPath = `${repositorySpecifier}/${rev}/${context.filepath}` if (!fullPath) return updateActivePath(fullPath, { - shouldFetchAllEntries: true, params: { line: String(context.range.start) } diff --git a/ee/tabby-ui/app/files/components/error-view.tsx b/ee/tabby-ui/app/files/components/error-view.tsx new file mode 100644 index 000000000000..76845d2672d4 --- /dev/null +++ b/ee/tabby-ui/app/files/components/error-view.tsx @@ -0,0 +1,39 @@ +import React from 'react' + +import { cn } from '@/lib/utils' +import { IconFileSearch } from '@/components/ui/icons' + +import { SourceCodeBrowserContext } from './source-code-browser' +import { Errors } from './utils' + +// import { Errors } from "./utils"; + +interface ErrorViewProps extends React.HTMLAttributes { + error: Error | undefined +} + +export const ErrorView: React.FC = ({ className, error }) => { + const { activeEntryInfo, activeRepo, activeRepoRef } = React.useContext( + SourceCodeBrowserContext + ) + const basename = activeEntryInfo?.basename + const isNotFound = error?.message === Errors.NOT_FOUND + const isEmptyRepository = error?.message === Errors?.EMPTY_REPOSITORY + // const isEmptyRepository = !!basename && (!activeRepo || !activeRepoRef?.name) + + let errorMessge = 'Not found' + if (isEmptyRepository) { + errorMessge = 'Empty repository' + } + + return ( +
+
+ +
{errorMessge}
+
+
+ ) +} diff --git a/ee/tabby-ui/app/files/components/file-directory-breadcrumb.tsx b/ee/tabby-ui/app/files/components/file-directory-breadcrumb.tsx index 9ec71c29de1f..920d82d12a63 100644 --- a/ee/tabby-ui/app/files/components/file-directory-breadcrumb.tsx +++ b/ee/tabby-ui/app/files/components/file-directory-breadcrumb.tsx @@ -1,4 +1,5 @@ import React from 'react' +import Link from 'next/link' import { RepositoryKind } from '@/lib/gql/generates/graphql' import { cn } from '@/lib/utils' @@ -19,7 +20,6 @@ const FileDirectoryBreadcrumb: React.FC = ({ }) => { const { currentFileRoutes, - updateActivePath, activePath, activeRepo, activeRepoRef, @@ -61,36 +61,32 @@ const FileDirectoryBreadcrumb: React.FC = ({ return (
-
updateActivePath(undefined)} + Repositories -
+
/
{routes?.map((route, idx) => { const isRepo = idx === 0 && routes?.length > 1 const isActiveFile = idx === routes.length - 1 + const classname = cn( + 'whitespace-nowrap', + isRepo || isActiveFile ? 'font-bold' : 'font-medium', + isActiveFile ? '' : 'text-primary cursor-pointer hover:underline', + isRepo ? 'hover:underline' : undefined + ) - // todo use link return ( -
{ - if (isActiveFile) return - updateActivePath(route.href) - }} - > - {route.name} -
+ {isActiveFile ? ( +
{route.name}
+ ) : ( + + {route.name} + + )} {!isActiveFile &&
/
}
) diff --git a/ee/tabby-ui/app/files/components/file-directory-view.tsx b/ee/tabby-ui/app/files/components/file-directory-view.tsx index 9a999479a024..e32b298b2614 100644 --- a/ee/tabby-ui/app/files/components/file-directory-view.tsx +++ b/ee/tabby-ui/app/files/components/file-directory-view.tsx @@ -36,7 +36,8 @@ const DirectoryView: React.FC = ({ fileTreeData, activeRepo, activeRepoRef, - repoMap + repoMap, + activeEntryInfo } = React.useContext(SourceCodeBrowserContext) const files: TFileTreeNode[] = React.useMemo(() => { @@ -66,28 +67,28 @@ const DirectoryView: React.FC = ({ const [loading] = useDebounceValue(propsLoading, 300) - const showParentEntry = currentFileRoutes?.length > 0 + const showParentEntry = !!activeEntryInfo?.basename const parentNode = currentFileRoutes[currentFileRoutes?.length - 2] return (
- {loading || !initialized ? ( + {(loading && !files?.length) || !initialized ? ( ) : files?.length ? ( {showParentEntry && ( - - + +
= ({
..
-
- + +
)} <> @@ -153,8 +154,7 @@ const DirectoryView: React.FC = ({
No indexed repository yet
- ) : //
404
- null} + ) : null} ) } 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 c25d4f781e04..1412e7af5037 100644 --- a/ee/tabby-ui/app/files/components/file-tree-header.tsx +++ b/ee/tabby-ui/app/files/components/file-tree-header.tsx @@ -51,8 +51,11 @@ import { RepositoryKindIcon } from './repository-kind-icon' import { SourceCodeBrowserContext } from './source-code-browser' import { generateEntryPath, + getDefaultRepoRef, + repositoryMap2List, resolveRepoRef, - resolveRepositoryInfoFromPath + resolveRepositoryInfoFromPath, + resolveRepoSpecifierFromRepoInfo } from './utils' interface FileTreeHeaderProps extends React.HTMLAttributes {} @@ -87,13 +90,22 @@ const FileTreeHeader: React.FC = ({ }) => { const { activePath, - fileTreeData, updateActivePath, initialized, activeRepo, activeRepoRef, - fileMap + fileMap, + repoMap } = useContext(SourceCodeBrowserContext) + const repoList = React.useMemo(() => { + return repositoryMap2List(repoMap).map(repo => { + const repoSpecifier = resolveRepoSpecifierFromRepoInfo(repo) as string + return { + repo, + repoSpecifier + } + }) + }, [repoMap]) const [refSelectVisible, setRefSelectVisible] = React.useState(false) const [activeRefKind, setActiveRefKind] = React.useState( activeRepoRef?.kind ?? 'branch' @@ -117,7 +129,7 @@ const FileTreeHeader: React.FC = ({ const [options, setOptions] = React.useState>() const [optionsVisible, setOptionsVisible] = React.useState(false) - const noIndexedRepo = initialized && !fileTreeData?.length + const noIndexedRepo = initialized && !repoList?.length const [{ data: repositorySearchData }] = useQuery({ query: repositorySearch, @@ -138,6 +150,8 @@ const FileTreeHeader: React.FC = ({ const { basename = '' } = resolveRepositoryInfoFromPath(activePath) const kind = fileMap?.[basename]?.file?.kind ?? 'dir' + // clear repository search + setInput(undefined) updateActivePath(generateEntryPath(activeRepo, nextRev, basename, kind)) } @@ -151,8 +165,16 @@ const FileTreeHeader: React.FC = ({ setOptionsVisible(!!repositorySearchPattern) }, [repositorySearchData?.repositorySearch]) - const onSelectRepo = (path: string) => { - updateActivePath(path) + const onSelectRepo = (repoSpecifier: string) => { + const repo = repoList.find(o => o.repoSpecifier === repoSpecifier)?.repo + if (repo) { + const path = `${repoSpecifier}/tree/${ + resolveRepoRef(getDefaultRepoRef(repo.refs)).name + }` + // clear repository search + setInput(undefined) + updateActivePath(path) + } } const onInputValueChange = useDebounceCallback((v: string | undefined) => { @@ -171,14 +193,14 @@ const FileTreeHeader: React.FC = ({ } const onSelectFile = async (value: SearchOption) => { - const path = value.path - if (!path) return - - // todo generate entry path - const fullPath = `${repositorySpecifier}/${ - activeRepoRef?.name ?? '' - }/${path}` - await updateActivePath(fullPath) + if (!value.path) return + const path = generateEntryPath( + activeRepo, + activeRepoRef?.name, + value.path, + value.type as any + ) + updateActivePath(path) } // shortcut 't' @@ -244,16 +266,18 @@ const FileTreeHeader: React.FC = ({ ) : ( <> - {/* todo use default */} - {fileTreeData?.map(repo => { + {repoList?.map(repo => { return ( - +
} /> - {repo.name} + {repo.repo.name}
) @@ -297,7 +321,6 @@ const FileTreeHeader: React.FC = ({ value={activeRefKind} onValueChange={v => setActiveRefKind(v as RepositoryRefKind)} > - {/* todo style */} Branches Tags diff --git a/ee/tabby-ui/app/files/components/file-tree-panel.tsx b/ee/tabby-ui/app/files/components/file-tree-panel.tsx index a46b3780a3a2..02f99051113e 100644 --- a/ee/tabby-ui/app/files/components/file-tree-panel.tsx +++ b/ee/tabby-ui/app/files/components/file-tree-panel.tsx @@ -9,9 +9,13 @@ import { FileTreeHeader } from './file-tree-header' import { SourceCodeBrowserContext } from './source-code-browser' import { generateEntryPath } from './utils' -interface FileTreePanelProps extends React.HTMLAttributes {} +interface FileTreePanelProps extends React.HTMLAttributes { + fetchingTreeEntries: boolean +} -export const FileTreePanel: React.FC = () => { +export const FileTreePanel: React.FC = ({ + fetchingTreeEntries +}) => { const { activePath, updateActivePath, @@ -36,18 +40,6 @@ export const FileTreePanel: React.FC = () => { updateActivePath(nextPath) } - // const currentFileTreeData = React.useMemo(() => { - // const { repositorySpecifier, rev, basename } = - // resolveRepositoryInfoFromPath(activePath) - - // if (!basename) return fileTreeData - - // const repo = fileTreeData.find( - // treeNode => treeNode.fullPath === basename - // ) - // return repo?.children ?? [] - // }, [activePath, fileTreeData]) - return (
@@ -64,6 +56,7 @@ export const FileTreePanel: React.FC = () => { toggleExpandedKey={toggleExpandedKey} initialized={initialized} fileTreeData={fileTreeData} + fetchingTreeEntries={fetchingTreeEntries} />
diff --git a/ee/tabby-ui/app/files/components/file-tree.tsx b/ee/tabby-ui/app/files/components/file-tree.tsx index 070729afeea4..765a99d1f8fa 100644 --- a/ee/tabby-ui/app/files/components/file-tree.tsx +++ b/ee/tabby-ui/app/files/components/file-tree.tsx @@ -46,6 +46,7 @@ interface FileTreeProps extends React.HTMLAttributes { toggleExpandedKey: (key: string) => void initialized: boolean fileTreeData: TFileTreeNode[] + fetchingTreeEntries: boolean } interface FileTreeProviderProps extends FileTreeProps {} @@ -59,6 +60,7 @@ type FileTreeContextValue = { toggleExpandedKey: (key: string) => void activePath: string | undefined initialized: boolean + fetchingTreeEntries: boolean } type DirectoryTreeNodeProps = { @@ -97,7 +99,8 @@ const FileTreeProvider: React.FC< expandedKeys, toggleExpandedKey, initialized, - fileTreeData + fileTreeData, + fetchingTreeEntries }) => { return ( {children} @@ -340,25 +344,13 @@ const DirectoryTreeNode: React.FC = ({ } const FileTreeRenderer: React.FC = () => { - const { activeEntryInfo, repoMap } = React.useContext( - SourceCodeBrowserContext - ) - const { initialized, activePath, fileMap, fileTreeData } = + const { repoMap } = React.useContext(SourceCodeBrowserContext) + const { initialized, activePath, fileTreeData, fetchingTreeEntries } = React.useContext(FileTreeContext) const { repositorySpecifier } = resolveRepositoryInfoFromPath(activePath) - // todo const hasSelectedRepo = !!repositorySpecifier const hasNoRepoEntries = hasSelectedRepo && !fileTreeData?.length - const activeEntryPath = activeEntryInfo?.basename - // const fetchingRepoEntries = - // activeEntryPath && - // fileMap?.[activeEntryPath]?.isRepository && - // !fileMap?.[activeEntryPath]?.treeExpanded - const fetchingRepoEntries = - activeEntryPath && - fileMap?.[activeEntryPath]?.isRepository && - !fileMap?.[activeEntryPath]?.treeExpanded if (!initialized) return @@ -374,7 +366,7 @@ const FileTreeRenderer: React.FC = () => { } if (hasNoRepoEntries) { - if (fetchingRepoEntries) { + if (fetchingTreeEntries) { return } 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 61830e45ec3d..574da3977a0d 100644 --- a/ee/tabby-ui/app/files/components/source-code-browser.tsx +++ b/ee/tabby-ui/app/files/components/source-code-browser.tsx @@ -5,8 +5,7 @@ import { usePathname } from 'next/navigation' import { createRequest } from '@urql/core' import { compact, isEmpty, toNumber } from 'lodash-es' import { ImperativePanelHandle } from 'react-resizable-panels' -import { SWRResponse } from 'swr' -import useSWRImmutable from 'swr/immutable' +import useSWR from 'swr' import { graphql } from '@/lib/gql/generates' import { RepositoryListQuery } from '@/lib/gql/generates/graphql' @@ -28,6 +27,7 @@ import { useTopbarProgress } from '@/components/topbar-progress-indicator' import { emitter, QuickActionEventPayload } from '../lib/event-emitter' import { ChatSideBar } from './chat-side-bar' +import { ErrorView } from './error-view' import { FileDirectoryBreadcrumb } from './file-directory-breadcrumb' import { DirectoryView } from './file-directory-view' import { mapToFileTree, sortFileTree, type TFileTreeNode } from './file-tree' @@ -35,7 +35,7 @@ import { FileTreePanel } from './file-tree-panel' import { RawFileView } from './raw-file-view' import { TextFileView } from './text-file-view' import { - fetchEntriesFromPath, + Errors, getDefaultRepoRef, getDirectoriesFromBasename, repositoryList2Map, @@ -90,14 +90,13 @@ type SourceCodeBrowserContextValue = { path: string | undefined, options?: { params?: Record<'line', string> - shouldFetchAllEntries?: boolean replace?: boolean } ) => Promise repoMap: Record setRepoMap: (map: Record) => void fileMap: TFileMap - updateFileMap: (map: TFileMap) => void + updateFileMap: (map: TFileMap, replace?: boolean) => void expandedKeys: Set setExpandedKeys: React.Dispatch>> toggleExpandedKey: (key: string) => void @@ -116,8 +115,7 @@ type SourceCodeBrowserContextValue = { | undefined isPathInitialized: boolean activeEntryInfo: ReturnType - fetchingTreeEntries: boolean - setFetchingTreeEntries: React.Dispatch> + prevActivePath: React.MutableRefObject } const SourceCodeBrowserContext = @@ -132,7 +130,6 @@ const SourceCodeBrowserContextProvider: React.FC = ({ const { updatePathnameAndSearch } = useRouterStuff() const [isPathInitialized, setIsPathInitialized] = React.useState(false) const [activePath, setActivePath] = React.useState() - const [fetchingTreeEntries, setFetchingTreeEntries] = React.useState(false) const activeEntryInfo = React.useMemo(() => { return resolveRepositoryInfoFromPath(activePath) }, [activePath]) @@ -150,106 +147,40 @@ const SourceCodeBrowserContextProvider: React.FC = ({ >() const prevActivePath = React.useRef() - // todo fetch all entries should use swr - const fetchAllEntries = React.useCallback( - async (fullPath: string) => { - if (!fullPath) return - const { repositorySpecifier, basename, rev } = - resolveRepositoryInfoFromPath(fullPath) - const { repositorySpecifier: prevRepositorySpecifier, rev: prevRev } = - resolveRepositoryInfoFromPath(prevActivePath.current) - try { - setFetchingTreeEntries(true) - // fetch dirs - const entries = await fetchEntriesFromPath( - fullPath, - repositorySpecifier ? repoMap?.[repositorySpecifier] : undefined - ) - const expandedDirs = getDirectoriesFromBasename(basename) - const patchMap: TFileMap = {} - if (entries.length) { - for (const entry of entries) { - const path = entry.basename - patchMap[path] = { - file: entry, - name: resolveFileNameFromPath(path), - fullPath: path, - treeExpanded: expandedDirs.includes(entry.basename) - } - } - } - - // todo remove '' - const expandedKeys = expandedDirs.filter(Boolean) - if (patchMap) { - if ( - repositorySpecifier !== prevRepositorySpecifier || - rev !== prevRev - ) { - setFileMap(patchMap) - } else { - setFileMap(prev => ({ - ...prev, - ...patchMap - })) - } - } - if (expandedKeys?.length) { - setExpandedKeys(keys => { - const newSet = new Set(keys) - for (const k of expandedKeys) { - newSet.add(k) - } - return newSet - }) - } - } catch (e) { - } finally { - setFetchingTreeEntries(false) - } - }, - [repoMap, activeEntryInfo?.viewMode] - ) - const updateActivePath: SourceCodeBrowserContextValue['updateActivePath'] = - React.useCallback( - async (path, options) => { - const replace = options?.replace - if (!path) { - // To maintain compatibility with older versions, remove the path params - updatePathnameAndSearch('/files', { - del: ['path', 'plain', 'line'], - replace - }) - } else { - const setParams: Record = {} - let delList = [ - 'plain', - 'line', - 'redirect_filepath', - 'redirect_git_url' - ] - if (options?.params?.line) { - setParams['line'] = options.params.line - delList = delList.filter(o => o !== 'line') - } - updatePathnameAndSearch(`/files/${path}`, { - set: setParams, - del: delList, - replace - }) + React.useCallback(async (path, options) => { + const replace = options?.replace + if (!path) { + // To maintain compatibility with older versions, remove the path params + updatePathnameAndSearch('/files', { + del: ['path', 'plain', 'line'], + replace + }) + } else { + const setParams: Record = {} + let delList = ['plain', 'line', 'redirect_filepath', 'redirect_git_url'] + if (options?.params?.line) { + setParams['line'] = options.params.line + delList = delList.filter(o => o !== 'line') } - }, - [fetchAllEntries] - ) + updatePathnameAndSearch(`/files/${path}`, { + set: setParams, + del: delList, + replace + }) + } + }, []) - const updateFileMap = (map: TFileMap) => { + const updateFileMap = (map: TFileMap, replace?: boolean) => { if (!map) return - - setFileMap(prevMap => ({ - ...prevMap, - ...map - })) + if (replace) { + setFileMap(map) + } else { + setFileMap(prevMap => ({ + ...prevMap, + ...map + })) + } } const toggleExpandedKey = (key: string) => { @@ -321,18 +252,6 @@ const SourceCodeBrowserContextProvider: React.FC = ({ } }, [pathname]) - React.useEffect(() => { - if (!isPathInitialized || isEmpty(repoMap)) return - - const update = async (activePath: string) => { - await fetchAllEntries(activePath) - } - - if (activePath) { - update(activePath) - } - }, [activeEntryInfo, repoMap]) - return ( = ({ activeRepoRef, isPathInitialized, activeEntryInfo, - fetchingTreeEntries, - setFetchingTreeEntries + prevActivePath }} > {children} @@ -379,25 +297,24 @@ const SourceCodeBrowserRenderer: React.FC = ({ const { activePath, updateActivePath, - updateFileMap, - fileMap, initialized, setInitialized, - setExpandedKeys, chatSideBarVisible, setChatSideBarVisible, setPendingEvent, + repoMap, setRepoMap, activeRepo, activeRepoRef, isPathInitialized, activeEntryInfo, - fetchingTreeEntries + prevActivePath, + updateFileMap, + setExpandedKeys } = React.useContext(SourceCodeBrowserContext) const { searchParams } = useRouterStuff() - const activeEntryPath = activeEntryInfo?.basename const initializing = React.useRef(false) - const { setProgress } = useTopbarProgress() + const { progress, setProgress } = useTopbarProgress() const chatSideBarPanelRef = React.useRef(null) const [chatSideBarPanelSize, setChatSideBarPanelSize] = React.useState(35) @@ -410,39 +327,51 @@ const SourceCodeBrowserRenderer: React.FC = ({ const [fileViewType, setFileViewType] = React.useState() const isBlobMode = activeEntryInfo?.viewMode === 'blob' + const shouldFetchTree = + !!isPathInitialized && !isEmpty(repoMap) && !!activePath - // todo handle entries error - const shouldFetchSubDir = React.useMemo(() => { - if (!initialized || fetchingTreeEntries) return false - - const isDir = activeEntryInfo?.viewMode === 'tree' - return isDir && activeEntryPath && !fileMap?.[activeEntryPath]?.treeExpanded - }, [ - activeEntryInfo?.viewMode, - activeEntryPath, - fileMap, - initialized, - fetchingTreeEntries - ]) + // fetch tree + const { + data: entriesResponse, + isLoading: fetchingTreeEntries, + error: entriesError + } = useSWR<{ + entries: TFile[] + requestPathname: string + }>( + shouldFetchTree ? activePath : null, + (path: string) => { + const { repositorySpecifier } = resolveRepositoryInfoFromPath(path) + return fetchEntriesFromPath( + path, + repositorySpecifier ? repoMap?.[repositorySpecifier] : undefined + ).then(data => ({ entries: data, requestPathname: path })) + }, + { + revalidateOnFocus: false, + shouldRetryOnError: false + } + ) // fetch raw file - // todo handle raw file error const { data: rawFileResponse, isLoading: fetchingRawFile, error: rawFileError - } = useSWRImmutable<{ + } = useSWR<{ blob?: Blob contentLength?: number }>( - isBlobMode + isBlobMode && activeRepo ? toEntryRequestUrl(activeRepo, activeRepoRef?.name, activeBasename) : null, (url: string) => fetcher(url, { responseFormatter: async response => { - if (!response.ok) return undefined - + const contentType = response.headers.get('Content-Type') + if (contentType === 'application/vnd.directory+json') { + throw new Error(Errors.INCORRECT_VIEW_MODE) + } const contentLength = toNumber(response.headers.get('Content-Length')) // todo abort big size request and truncate const blob = await response.blob() @@ -451,30 +380,27 @@ const SourceCodeBrowserRenderer: React.FC = ({ blob } }, - errorHandler(response) { - if (!response?.ok) { - throw new Error() - } + errorHandler() { + throw new Error(Errors.NOT_FOUND) } - }) + }), + { + revalidateOnFocus: false, + shouldRetryOnError: false + } ) const fileBlob = rawFileResponse?.blob const contentLength = rawFileResponse?.contentLength + const error = rawFileError || entriesError + + const showErrorView = !!error - // fetch active dir - const { - data: subTree, - isLoading: fetchingSubTree - }: SWRResponse = useSWRImmutable( - shouldFetchSubDir - ? toEntryRequestUrl(activeRepo, activeRepoRef?.name, activeBasename) - : null, - fetcher - ) const showDirectoryView = activeEntryInfo?.viewMode === 'tree' || !activeEntryInfo?.viewMode + const showTextFileView = isBlobMode && fileViewType === 'text' + const showRawFileView = isBlobMode && (fileViewType === 'image' || fileViewType === 'raw') @@ -484,7 +410,6 @@ const SourceCodeBrowserRenderer: React.FC = ({ } } - // todo check if params is valid React.useEffect(() => { const init = async () => { if (initializing.current) return @@ -522,53 +447,60 @@ const SourceCodeBrowserRenderer: React.FC = ({ }, [activePath, initialized, isPathInitialized]) React.useEffect(() => { - if (!initialized) return - if (fetchingSubTree || fetchingRawFile || fetchingTreeEntries) { - setProgress(true) - } else if (!fetchingSubTree && !fetchingRawFile && !fetchingTreeEntries) { - setProgress(false) + if (!entriesResponse) return + + const { entries, requestPathname } = entriesResponse + const { repositorySpecifier, viewMode, basename, rev } = + resolveRepositoryInfoFromPath(requestPathname) + const { repositorySpecifier: prevRepositorySpecifier, rev: prevRev } = + resolveRepositoryInfoFromPath(prevActivePath.current) + const expandedDirs = getDirectoriesFromBasename( + basename, + viewMode === 'tree' + ) + const patchMap: TFileMap = {} + if (entries.length) { + for (const entry of entries) { + const _basename = entry.basename + patchMap[_basename] = { + file: entry, + name: resolveFileNameFromPath(_basename), + // custom pathmane + fullPath: _basename, + treeExpanded: expandedDirs.includes(entry.basename) + } + } } - }, [fetchingSubTree, fetchingRawFile, fetchingTreeEntries]) - React.useEffect(() => { - const onFetchSubTree = () => { - if (Array.isArray(subTree?.entries) && activeEntryPath) { - const { basename } = resolveRepositoryInfoFromPath(activePath) - let patchMap: TFileMap = {} - if (fileMap?.[activeEntryPath]) { - patchMap[activeEntryPath] = { - ...fileMap[activeEntryPath], - treeExpanded: true - } - } - if (subTree?.entries?.length) { - for (const entry of subTree.entries) { - // const path = `${repositorySpecifier}/${rev}/${entry.basename}` - const path = entry.basename - patchMap[path] = { - file: entry, - name: resolveFileNameFromPath(path), - fullPath: path, - treeExpanded: false - } + const expandedKeys = expandedDirs.filter(Boolean) + const shouldReplace = + repositorySpecifier !== prevRepositorySpecifier || rev !== prevRev + if (patchMap) { + updateFileMap(patchMap, shouldReplace) + } + if (expandedKeys?.length) { + if (shouldReplace) { + setExpandedKeys(new Set(expandedKeys)) + } else { + setExpandedKeys(keys => { + const newSet = new Set(keys) + for (const k of expandedKeys) { + newSet.add(k) } - } - updateFileMap(patchMap) - const expandedKeysToAdd = getDirectoriesFromBasename(basename, true) - if (expandedKeysToAdd?.length) { - setExpandedKeys(keys => { - const newSet = new Set(keys) - for (const k of expandedKeysToAdd) { - newSet.add(k) - } - return newSet - }) - } + return newSet + }) } } + }, [entriesResponse]) - onFetchSubTree() - }, [subTree]) + React.useEffect(() => { + if (!initialized) return + if (!progress && (fetchingRawFile || fetchingTreeEntries)) { + setProgress(true) + } else if (!fetchingRawFile && !fetchingTreeEntries) { + setProgress(false) + } + }, [fetchingRawFile, fetchingTreeEntries]) React.useEffect(() => { const calculateViewType = async () => { @@ -616,7 +548,7 @@ const SourceCodeBrowserRenderer: React.FC = ({ maxSize={40} className="hidden lg:block" > - + @@ -624,11 +556,16 @@ const SourceCodeBrowserRenderer: React.FC = ({ {!initialized ? ( + ) : showErrorView ? ( + ) : (
{showDirectoryView && ( @@ -738,6 +675,46 @@ async function getFileViewType( return isReadableText ? 'text' : 'raw' } +async function fetchEntriesFromPath( + path: string | undefined, + repository: RepositoryListQuery['repositoryList'][0] | undefined +) { + if (!path) return [] + if (!repository) throw new Error(Errors.EMPTY_REPOSITORY) + + const { basename, rev, viewMode } = resolveRepositoryInfoFromPath(path) + // array of dir basename that do not include the repo name. + const directoryPaths = getDirectoriesFromBasename( + basename, + viewMode === 'tree' + ) + // fetch all dirs from path + const requests: Array<() => Promise> = + directoryPaths.map( + dir => () => + fetcher(toEntryRequestUrl(repository, rev ?? 'main', dir) as string, { + responseFormatter(response) { + const contentType = response.headers.get('Content-Type') + if (contentType !== 'application/vnd.directory+json') { + throw new Error(Errors.INCORRECT_VIEW_MODE) + } + return response.json() + }, + errorHandler() { + throw new Error(Errors.NOT_FOUND) + } + }) + ) + 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 type { TFileMap, TFileMapItem } export { SourceCodeBrowserContext, SourceCodeBrowser } diff --git a/ee/tabby-ui/app/files/components/text-file-view.tsx b/ee/tabby-ui/app/files/components/text-file-view.tsx index a6098e20e921..234dab578951 100644 --- a/ee/tabby-ui/app/files/components/text-file-view.tsx +++ b/ee/tabby-ui/app/files/components/text-file-view.tsx @@ -79,7 +79,7 @@ export const TextFileView: React.FC = ({ )}
- }> + }> {showMarkdown ? ( ) : ( diff --git a/ee/tabby-ui/app/files/components/utils.ts b/ee/tabby-ui/app/files/components/utils.ts index 82d6a599a272..1f89e62e3b66 100644 --- a/ee/tabby-ui/app/files/components/utils.ts +++ b/ee/tabby-ui/app/files/components/utils.ts @@ -4,12 +4,16 @@ import { RepositoryKind, RepositoryListQuery } from '@/lib/gql/generates/graphql' -import fetcher from '@/lib/tabby/fetcher' -import { ResolveEntriesResponse, TFile } from '@/lib/types' export type ViewMode = 'tree' | 'blob' type RepositoryItem = RepositoryListQuery['repositoryList'][0] +export enum Errors { + NOT_FOUND = 'NOT_FOUND', + INCORRECT_VIEW_MODE = 'INCORRECT_VIEW_MODE', + EMPTY_REPOSITORY = 'EMPTY_REPOSITORY' +} + function getProviderVariantFromKind(kind: RepositoryKind) { return kind.toLowerCase().replaceAll('_', '') } @@ -19,7 +23,6 @@ function resolveRepositoryInfoFromPath(path: string | undefined): { repositoryName?: string basename?: string repositorySpecifier?: string - // todo viewMode?: string rev?: string } { @@ -105,38 +108,6 @@ function getDirectoriesFromBasename( return result } -async function fetchEntriesFromPath( - path: string | undefined, - repository: RepositoryListQuery['repositoryList'][0] | undefined -) { - if (!path || !repository) return [] - - const { basename, rev, viewMode } = resolveRepositoryInfoFromPath(path) - // array of dir basename that do not include the repo name. - const directoryPaths = getDirectoriesFromBasename( - basename, - viewMode === 'tree' - ) - // fetch all dirs from path - const requests: Array<() => Promise> = - directoryPaths.map( - dir => () => - fetcher( - `/repositories/${getProviderVariantFromKind(repository.kind)}/${ - repository.id - }/rev/${rev ?? 'main'}/${encodeURIComponentIgnoringSlash(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 resolveRepoSpecifierFromRepoInfo( repo: | { kind: RepositoryKind | undefined; name: string | undefined } @@ -214,7 +185,6 @@ function getDefaultRepoRef(refs: string[]) { return mainRef || masterRef || firstHeadRef || firstTagRef } -// todo encode & decode function generateEntryPath( repo: | { kind: RepositoryKind | undefined; name: string | undefined } @@ -224,7 +194,7 @@ function generateEntryPath( kind: 'dir' | 'file' ) { const specifier = resolveRepoSpecifierFromRepoInfo(repo) - // todo use 'main' as fallback + // use 'main' as fallback const finalRev = rev ?? 'main' return `${specifier}/${ kind === 'dir' ? 'tree' : 'blob' @@ -251,7 +221,6 @@ export { resolveRepoSpecifierFromRepoInfo, resolveFileNameFromPath, getDirectoriesFromBasename, - fetchEntriesFromPath, resolveRepositoryInfoFromPath, repositoryList2Map, repositoryMap2List, diff --git a/ee/tabby-ui/components/topbar-progress-indicator.tsx b/ee/tabby-ui/components/topbar-progress-indicator.tsx index 8caaa252d3cd..b3a0b51a6dc1 100644 --- a/ee/tabby-ui/components/topbar-progress-indicator.tsx +++ b/ee/tabby-ui/components/topbar-progress-indicator.tsx @@ -23,15 +23,7 @@ const TopbarProgressProvider: React.FC = ({ children }) => { const [progress, setProgress] = React.useState(false) - const updateProgress = React.useCallback( - (v: boolean) => { - if (v !== progress) { - setProgress(v) - } - }, - [progress, setProgress] - ) - const [debouncedProgress] = useDebounceValue(progress, 300, { leading: true }) + const [debouncedProgress] = useDebounceValue(progress, 200, { leading: true }) const { theme } = useTheme() React.useEffect(() => { TopBarProgress.config({