import { Label, Text } from '@primer/react' import { LoadingIndicator } from 'components/LoadingIndicator' import { SearchBar } from 'components/SearchBar' import { useConfigs } from 'containers/ConfigsContext' import { RepoContext } from 'containers/RepoContext' import { platform } from 'platforms' import * as React from 'react' import { usePrevious } from 'react-use' import { cx } from 'utils/cx' import { run } from 'utils/general' import { useElementSize } from 'utils/hooks/useElementSize' import { useAfterRedirect } from 'utils/hooks/useFastRedirect' import { useLoadedContext } from 'utils/hooks/useLoadedContext' import { useOnLocationChange } from 'utils/hooks/useOnLocationChange' import { VisibleNodes, VisibleNodesGenerator } from 'utils/VisibleNodesGenerator' import { SideBarStateContext } from '../../containers/SideBarState' import { useFocusFileExplorerOnFirstRender } from './hooks/useFocusFileExplorerOnFirstRender' import { useGetCurrentPath } from './hooks/useGetCurrentPath' import { useHandleKeyDown } from './hooks/useHandleKeyDown' import { NodeRenderer, useNodeRenderers, useRenderFileCommentAmounts, useRenderFileStatus, useRenderFindInFolderButton, useRenderGoToButton } from './hooks/useNodeRenderers' import { useHandleNodeClick } from './hooks/useOnNodeClick' import { useOnSearch } from './hooks/useOnSearch' import { useRenderLabelText } from './hooks/useRenderLabelText' import { useVisibleNodesGenerator } from './hooks/useVisibleNodesGenerator' import { useVisibleNodesGeneratorMethods } from './hooks/useVisibleNodesGeneratorMethods' import { Node } from './Node' import { useHandleNodeFocus } from './useHandleNodeFocus' import { AlignMode, useVirtualScroll } from './useVirtualScroll' import { useVisibleNodes } from './useVisibleNodes' export type NodeRendererContext = { onNodeClick: (event: React.MouseEvent, node: TreeNode) => void onNodeFocus: (event: React.FocusEvent, node: TreeNode) => void renderLabelText: NodeRenderer renderActions: NodeRenderer | undefined visibleNodes: VisibleNodes } export function FileExplorer() { const metaData = React.useContext(RepoContext) const visibleNodesGenerator = useVisibleNodesGenerator(metaData) const visibleNodes = useVisibleNodes(visibleNodesGenerator) const state = useLoadedContext(SideBarStateContext).value return ( <> {run(() => { switch (state) { case 'tree-loading': return case 'tree-rendering': return case 'tree-rendered': return ( metaData && visibleNodesGenerator && visibleNodes && ( ) ) } })} ) } function LoadedFileExplorer({ metaData, visibleNodesGenerator, visibleNodes, }: { metaData: MetaData visibleNodesGenerator: VisibleNodesGenerator visibleNodes: VisibleNodes }) { const [searchKey, updateSearchKey] = React.useState('') const searched = !!searchKey const onSearch = useOnSearch(updateSearchKey, visibleNodesGenerator) const { focusedNode, nodes, expandedNodes, depths, loading } = visibleNodes const { ref: filesRef, size: [, height], } = useElementSize() const { compactFileTree } = useConfigs().value const { ref: scrollElementRef, onScroll, visibleRows, containerStyle, scrollToItem, } = useVirtualScroll({ totalAmount: visibleNodes.nodes.length, rowHeight: compactFileTree ? 24 : 37, viewportHeight: height, overScan: 10, }) // - init loading // - "top" // - jump to file // - "top" // - tab to file // - "lazy" // - click file/folder // - "lazy" // - navigate with keyboard // - "lazy" const [alignMode, setAlignMode] = React.useState('top') const index = React.useMemo( () => (focusedNode?.path ? nodes.findIndex(node => node.path === focusedNode.path) : -1), [focusedNode?.path, nodes], ) React.useLayoutEffect(() => { if (index !== -1) scrollToItem(index, alignMode) }, [index, scrollToItem, alignMode]) const prevSearchKey = usePrevious(searchKey) React.useEffect(() => { // when start searching or stop searching if (!prevSearchKey !== !searchKey) scrollToItem(0, alignMode) }, [prevSearchKey, searchKey, scrollToItem, alignMode]) const getCurrentPath = useGetCurrentPath(metaData) const methods = useVisibleNodesGeneratorMethods( visibleNodesGenerator, getCurrentPath, updateSearchKey, ) const { expandTo, goTo, focusNode } = methods const handleNodeFocus = useHandleNodeFocus(methods, setAlignMode) const handleNodeClick = useHandleNodeClick(methods, setAlignMode) const handleKeyDown = useHandleKeyDown(visibleNodes, methods, searched, setAlignMode) const handleFocusSearchBar = () => focusNode(null) const renderActions = useNodeRenderers([ useRenderGoToButton(searched, goTo), useRenderFindInFolderButton(onSearch), useRenderFileCommentAmounts(), useRenderFileStatus(), ]) const renderLabelText = useRenderLabelText(searchKey) useFocusFileExplorerOnFirstRender() const goToCurrentItem = React.useCallback(() => { const targetPath = platform.getCurrentPath(metaData.branchName) if (targetPath) expandTo(targetPath) }, [metaData.branchName, expandTo]) useOnLocationChange(goToCurrentItem) useAfterRedirect(goToCurrentItem) return (
{visibleNodesGenerator?.defer && (
)} {searched && visibleNodes.nodes.length === 0 && ( <> No results found. {visibleNodesGenerator?.defer && ( Search results are limited to loaded folders in Lazy Mode. )} )}
{visibleRows.map(({ row, style }) => { const node = nodes[row] return ( ) })}
) }