mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
234 lines
8 KiB
TypeScript
234 lines
8 KiB
TypeScript
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<HTMLElement, MouseEvent>, node: TreeNode) => void
|
|
onNodeFocus: (event: React.FocusEvent<HTMLElement, Element>, 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 <LoadingIndicator text={'Fetching File List...'} />
|
|
case 'tree-rendering':
|
|
return <LoadingIndicator text={'Rendering File List...'} />
|
|
case 'tree-rendered':
|
|
return (
|
|
metaData &&
|
|
visibleNodesGenerator &&
|
|
visibleNodes && (
|
|
<LoadedFileExplorer
|
|
metaData={metaData}
|
|
visibleNodesGenerator={visibleNodesGenerator}
|
|
visibleNodes={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<HTMLDivElement>()
|
|
const { compactFileTree } = useConfigs().value
|
|
const {
|
|
ref: scrollElementRef,
|
|
onScroll,
|
|
visibleRows,
|
|
containerStyle,
|
|
scrollToItem,
|
|
} = useVirtualScroll<HTMLDivElement>({
|
|
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<AlignMode>('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 (
|
|
<div className={`file-explorer`} tabIndex={-1} onKeyDown={handleKeyDown}>
|
|
{visibleNodesGenerator?.defer && (
|
|
<div className={'status'}>
|
|
<Label
|
|
title="This repository is large. Gitako has switched to Lazy Mode to improve performance. Folders will be loaded on demand."
|
|
className={'lazy-mode'}
|
|
variant="attention"
|
|
>
|
|
Lazy Mode is ON
|
|
</Label>
|
|
</div>
|
|
)}
|
|
<SearchBar value={searchKey} onSearch={onSearch} onFocus={handleFocusSearchBar} />
|
|
{searched && visibleNodes.nodes.length === 0 && (
|
|
<>
|
|
<Text marginTop={6} textAlign="center" color="text.gray">
|
|
No results found.
|
|
</Text>
|
|
{visibleNodesGenerator?.defer && (
|
|
<Text textAlign="center" color="gray.4" fontSize="12px">
|
|
Search results are limited to loaded folders in Lazy Mode.
|
|
</Text>
|
|
)}
|
|
</>
|
|
)}
|
|
<div
|
|
className={cx('files', {
|
|
// instead of unmounting, hide the element when not needed, so that the ref can be preserved after search result matches nothing
|
|
hidden: visibleNodes.nodes.length === 0,
|
|
})}
|
|
tabIndex={-1} // prevent getting focus via tab key on GitHub
|
|
ref={filesRef}
|
|
>
|
|
<div
|
|
style={{
|
|
height,
|
|
width: '100%',
|
|
overflow: 'auto',
|
|
position: 'absolute', // This allows reducing `height` on viewport height reduce
|
|
}}
|
|
ref={scrollElementRef}
|
|
onScroll={onScroll}
|
|
>
|
|
<div style={containerStyle}>
|
|
{visibleRows.map(({ row, style }) => {
|
|
const node = nodes[row]
|
|
return (
|
|
<Node
|
|
key={node.path}
|
|
node={node}
|
|
style={style}
|
|
depth={depths.get(node) || 0}
|
|
focused={focusedNode?.path === node.path}
|
|
loading={loading.has(node.path)}
|
|
expanded={expandedNodes.has(node.path)}
|
|
onClick={handleNodeClick}
|
|
onFocus={handleNodeFocus}
|
|
renderLabelText={renderLabelText}
|
|
renderActions={renderActions}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|