mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
feat: search modes
This commit is contained in:
parent
8737611c17
commit
e1d0742de8
9 changed files with 229 additions and 123 deletions
|
|
@ -11,28 +11,49 @@ import * as React from 'react'
|
|||
import { FixedSizeList, ListChildComponentProps } from 'react-window'
|
||||
import { cx } from 'utils/cx'
|
||||
import { focusFileExplorer } from 'utils/DOMHelper'
|
||||
import { isValidRegexpSource } from 'utils/general'
|
||||
import { useOnLocationChange } from 'utils/hooks/useOnLocationChange'
|
||||
import { useOnPJAXDone } from 'utils/hooks/usePJAX'
|
||||
import { VisibleNodes } from 'utils/VisibleNodesGenerator'
|
||||
import { Icon } from './Icon'
|
||||
import { searchModes } from './searchModes'
|
||||
import { SizeObserver } from './SizeObserver'
|
||||
|
||||
type renderNodeContext = {
|
||||
onNodeClick: (event: React.MouseEvent<HTMLElement, MouseEvent>, node: TreeNode) => void
|
||||
renderLabelText: (node: TreeNode) => React.ReactNode
|
||||
renderActions: ((node: TreeNode) => React.ReactNode) | undefined
|
||||
visibleNodes: VisibleNodes
|
||||
}
|
||||
|
||||
const RawFileExplorer: React.FC<Props & ConnectorState> = function RawFileExplorer(props) {
|
||||
const {
|
||||
state,
|
||||
visibleNodes,
|
||||
visibleNodesGenerator,
|
||||
freeze,
|
||||
onNodeClick,
|
||||
searchKey,
|
||||
updateSearchKey,
|
||||
onFocusSearchBar,
|
||||
goTo,
|
||||
handleKeyDown,
|
||||
toggleShowSettings,
|
||||
metaData,
|
||||
expandTo,
|
||||
setUpTree,
|
||||
treeRoot,
|
||||
defer,
|
||||
searched,
|
||||
} = props
|
||||
const { value: config } = useConfigs()
|
||||
const {
|
||||
value: { accessToken, compressSingletonFolder, searchMode },
|
||||
} = useConfigs()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (visibleNodesGenerator) {
|
||||
visibleNodesGenerator.search(searchModes[searchMode].getSearchParams(searchKey))
|
||||
}
|
||||
}, [visibleNodesGenerator, searchKey, searchMode])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (treeRoot) {
|
||||
|
|
@ -40,43 +61,78 @@ const RawFileExplorer: React.FC<Props & ConnectorState> = function RawFileExplor
|
|||
treeRoot,
|
||||
metaData,
|
||||
config: {
|
||||
compressSingletonFolder: config.compressSingletonFolder,
|
||||
accessToken: config.accessToken,
|
||||
compressSingletonFolder,
|
||||
accessToken,
|
||||
searchMode,
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [setUpTree, treeRoot, metaData, config.compressSingletonFolder, config.accessToken])
|
||||
}, [setUpTree, treeRoot, metaData, compressSingletonFolder, accessToken])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (visibleNodes?.focusedNode) focusFileExplorer()
|
||||
})
|
||||
|
||||
const renderActions: ((node: TreeNode) => React.ReactNode) | undefined = React.useMemo(
|
||||
const renderActions: ((node: TreeNode) => React.ReactNode) | undefined = React.useMemo(() => {
|
||||
const renderGoToButton = (node: TreeNode): React.ReactNode => (
|
||||
<button
|
||||
title={'Reveal in file tree'}
|
||||
className={'go-to-button'}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
goTo(node.path.split('/'))
|
||||
}}
|
||||
>
|
||||
<Icon type="go-to" />
|
||||
</button>
|
||||
)
|
||||
const renderFindInFolderButton = (node: TreeNode): React.ReactNode =>
|
||||
node.type === 'tree' ? (
|
||||
<button
|
||||
title={'Find in folder...'}
|
||||
className={'find-in-folder-button'}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
updateSearchKey(node.path + '/')
|
||||
}}
|
||||
>
|
||||
<Icon type="search" />
|
||||
</button>
|
||||
) : undefined
|
||||
|
||||
const renders: ((node: TreeNode) => React.ReactNode)[] = []
|
||||
if (searchMode === 'fuzzy') renders.push(renderFindInFolderButton)
|
||||
if (searched) renders.push(renderGoToButton)
|
||||
|
||||
return renders.length
|
||||
? node => renders.map((render, i) => <React.Fragment key={i}>{render(node)}</React.Fragment>)
|
||||
: undefined
|
||||
}, [goTo, updateSearchKey, searched, searchMode])
|
||||
|
||||
const renderLabelText = React.useCallback(
|
||||
node => searchModes[searchMode].renderNodeLabelText(node, searchKey),
|
||||
[searchKey, searchMode],
|
||||
)
|
||||
|
||||
const renderNodeContext: renderNodeContext | null = React.useMemo(
|
||||
() =>
|
||||
visibleNodes?.lastMatch?.match.searchKey
|
||||
? node => (
|
||||
<button
|
||||
title={'Reveal in file tree'}
|
||||
className={'go-to-button'}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
goTo(node.path.split('/'))
|
||||
}}
|
||||
>
|
||||
<Icon type="go-to" />
|
||||
</button>
|
||||
)
|
||||
: undefined,
|
||||
[visibleNodes, goTo],
|
||||
visibleNodes && {
|
||||
onNodeClick,
|
||||
renderActions,
|
||||
renderLabelText,
|
||||
visibleNodes,
|
||||
},
|
||||
[onNodeClick, renderActions, renderLabelText, visibleNodes],
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(`file-explorer`, { freeze })}
|
||||
tabIndex={-1}
|
||||
onKeyDown={props.handleKeyDown}
|
||||
onClick={freeze ? props.toggleShowSettings : undefined}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={freeze ? toggleShowSettings : undefined}
|
||||
>
|
||||
{state !== 'done' ? (
|
||||
<LoadingIndicator
|
||||
|
|
@ -88,14 +144,15 @@ const RawFileExplorer: React.FC<Props & ConnectorState> = function RawFileExplor
|
|||
}
|
||||
/>
|
||||
) : (
|
||||
visibleNodes && (
|
||||
visibleNodes &&
|
||||
renderNodeContext && (
|
||||
<>
|
||||
<SearchBar
|
||||
searchKey={searchKey}
|
||||
onSearch={props.search}
|
||||
onFocus={props.onFocusSearchBar}
|
||||
value={searchKey}
|
||||
onSearch={value => updateSearchKey(value)}
|
||||
onFocus={onFocusSearchBar}
|
||||
/>
|
||||
{visibleNodes.lastMatch?.match.searchKey !== '' && visibleNodes.nodes.length === 0 && (
|
||||
{searched && visibleNodes.nodes.length === 0 && (
|
||||
<>
|
||||
<Text marginTop={6} textAlign="center" color="text.gray">
|
||||
No results found.
|
||||
|
|
@ -112,9 +169,7 @@ const RawFileExplorer: React.FC<Props & ConnectorState> = function RawFileExplor
|
|||
<ListView
|
||||
height={height}
|
||||
width={width}
|
||||
onNodeClick={onNodeClick}
|
||||
renderActions={renderActions}
|
||||
visibleNodes={visibleNodes}
|
||||
renderNodeContext={renderNodeContext}
|
||||
expandTo={expandTo}
|
||||
metaData={metaData}
|
||||
/>
|
||||
|
|
@ -139,21 +194,13 @@ export const FileExplorer = connect(FileExplorerCore)(RawFileExplorer)
|
|||
const VirtualNode = React.memo(function VirtualNode({
|
||||
index,
|
||||
style,
|
||||
data,
|
||||
}: ListChildComponentProps) {
|
||||
const { onNodeClick, renderActions, visibleNodes } = data
|
||||
data: { onNodeClick, renderLabelText, renderActions, visibleNodes },
|
||||
}: Override<ListChildComponentProps, { data: renderNodeContext }>) {
|
||||
if (!visibleNodes) return null
|
||||
|
||||
const {
|
||||
lastMatch,
|
||||
nodes,
|
||||
focusedNode,
|
||||
expandedNodes,
|
||||
loading,
|
||||
depths,
|
||||
} = visibleNodes as VisibleNodes
|
||||
const { nodes, focusedNode, expandedNodes, loading, depths } = visibleNodes as VisibleNodes
|
||||
const node = nodes[index]
|
||||
const searchKey = lastMatch?.match.searchKey
|
||||
|
||||
return (
|
||||
<Node
|
||||
style={style}
|
||||
|
|
@ -164,8 +211,8 @@ const VirtualNode = React.memo(function VirtualNode({
|
|||
loading={loading.has(node.path)}
|
||||
expanded={expandedNodes.has(node.path)}
|
||||
onClick={onNodeClick}
|
||||
renderLabelText={renderLabelText}
|
||||
renderActions={renderActions}
|
||||
regex={searchKey && isValidRegexpSource(searchKey) ? new RegExp(searchKey, 'gi') : undefined}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
@ -173,31 +220,23 @@ const VirtualNode = React.memo(function VirtualNode({
|
|||
type ListViewProps = {
|
||||
height: number
|
||||
width: number
|
||||
onNodeClick(event: React.MouseEvent<HTMLElement, MouseEvent>, node: TreeNode): void
|
||||
renderActions?(node: TreeNode): React.ReactNode
|
||||
visibleNodes: VisibleNodes
|
||||
}
|
||||
renderNodeContext: renderNodeContext
|
||||
} & Pick<Props, 'metaData'> &
|
||||
Pick<ConnectorState, 'expandTo'>
|
||||
|
||||
function ListView({
|
||||
width,
|
||||
height,
|
||||
metaData,
|
||||
expandTo,
|
||||
onNodeClick,
|
||||
renderActions,
|
||||
visibleNodes,
|
||||
}: ListViewProps & Pick<Props, 'metaData'> & Pick<ConnectorState, 'expandTo'>) {
|
||||
function ListView({ width, height, metaData, expandTo, renderNodeContext }: ListViewProps) {
|
||||
const { visibleNodes } = renderNodeContext
|
||||
const { focusedNode, nodes } = visibleNodes
|
||||
const listRef = React.useRef<FixedSizeList>(null)
|
||||
// the change of depths indicates switch into/from search state
|
||||
React.useEffect(() => {
|
||||
const { focusedNode, nodes } = visibleNodes
|
||||
if (listRef.current && focusedNode?.path) {
|
||||
const index = nodes.findIndex(node => node.path === focusedNode.path)
|
||||
if (index !== -1) {
|
||||
listRef.current.scrollToItem(index, 'smart')
|
||||
}
|
||||
}
|
||||
}, [visibleNodes])
|
||||
}, [focusedNode, nodes])
|
||||
// For some reason, removing the deps array above results in bug:
|
||||
// If scroll fast and far, then clicking on items would result in redirect
|
||||
// Not know the reason :(
|
||||
|
|
@ -210,20 +249,11 @@ function ListView({
|
|||
useOnLocationChange(goToCurrentItem)
|
||||
useOnPJAXDone(goToCurrentItem)
|
||||
|
||||
const itemData = React.useMemo(
|
||||
() => ({
|
||||
onNodeClick,
|
||||
renderActions,
|
||||
visibleNodes,
|
||||
}),
|
||||
[onNodeClick, renderActions, visibleNodes],
|
||||
)
|
||||
|
||||
return (
|
||||
<FixedSizeList
|
||||
ref={listRef}
|
||||
itemKey={(index, { visibleNodes }) => visibleNodes?.nodes[index]?.path}
|
||||
itemData={itemData}
|
||||
itemData={renderNodeContext}
|
||||
itemCount={visibleNodes.nodes.length}
|
||||
itemSize={37}
|
||||
height={height}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
MarkdownIcon as Markdown,
|
||||
OctofaceIcon as Octoface,
|
||||
ReplyIcon as Reply,
|
||||
SearchIcon as Search,
|
||||
XIcon as X,
|
||||
} from '@primer/octicons-react'
|
||||
import * as React from 'react'
|
||||
|
|
@ -27,6 +28,11 @@ function getSVGIconComponent(
|
|||
name: string
|
||||
} {
|
||||
switch (type) {
|
||||
case 'search':
|
||||
return {
|
||||
IconComponent: Search,
|
||||
name: 'Search',
|
||||
}
|
||||
case 'loading':
|
||||
return {
|
||||
IconComponent: Clock,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import * as React from 'react'
|
|||
import { cx } from 'utils/cx'
|
||||
import { OperatingSystems, os } from 'utils/general'
|
||||
import { getFileIconSrc, getFolderIconSrc } from '../utils/parseIconMapCSV'
|
||||
import { Highlight } from './Highlight'
|
||||
import { Icon } from './Icon'
|
||||
|
||||
function getIconType(node: TreeNode) {
|
||||
|
|
@ -25,8 +24,8 @@ type Props = {
|
|||
focused: boolean
|
||||
loading: boolean
|
||||
renderActions?(node: TreeNode): React.ReactNode
|
||||
renderLabelText(node: TreeNode): React.ReactNode
|
||||
style?: React.CSSProperties
|
||||
regex?: RegExp
|
||||
}
|
||||
export function Node({
|
||||
node,
|
||||
|
|
@ -35,11 +34,10 @@ export function Node({
|
|||
focused,
|
||||
loading,
|
||||
renderActions,
|
||||
renderLabelText,
|
||||
style,
|
||||
onClick,
|
||||
regex,
|
||||
}: Props) {
|
||||
const { name, path } = node
|
||||
return (
|
||||
<a
|
||||
href={node.url}
|
||||
|
|
@ -57,20 +55,11 @@ export function Node({
|
|||
}}
|
||||
className={cx(`node-item`, { focused, disabled: node.accessDenied, expanded })}
|
||||
style={{ ...style, paddingLeft: `${10 + 20 * depth}px` }}
|
||||
title={path}
|
||||
title={node.path}
|
||||
>
|
||||
<div className={'node-item-label'}>
|
||||
<NodeItemIcon node={node} open={expanded} loading={loading} />
|
||||
{name.includes('/') ? (
|
||||
name.split('/').map((chunk, index, arr) => (
|
||||
<span key={chunk} className={cx({ prefix: index + 1 !== arr.length })}>
|
||||
<Highlight match={regex} text={chunk} />
|
||||
{index + 1 !== arr.length && '/'}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<Highlight match={regex} text={name} />
|
||||
)}
|
||||
{renderLabelText(node)}
|
||||
</div>
|
||||
{renderActions && <div>{renderActions(node)}</div>}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import { TextInput } from '@primer/components'
|
||||
import { Label, TextInput, TextInputProps } from '@primer/components'
|
||||
import { SearchIcon } from '@primer/octicons-react'
|
||||
import { useConfigs } from 'containers/ConfigsContext'
|
||||
import * as React from 'react'
|
||||
import { cx } from 'utils/cx'
|
||||
import { isValidRegexpSource } from 'utils/general'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
onSearch: (searchKey: string) => void
|
||||
onFocus: React.FocusEventHandler
|
||||
searchKey: string
|
||||
}
|
||||
} & Required<Pick<TextInputProps, 'onFocus'>>
|
||||
|
||||
export function SearchBar({ onSearch, onFocus, value }: Props) {
|
||||
const configs = useConfigs()
|
||||
const { searchMode } = configs.value
|
||||
|
||||
export function SearchBar({ onSearch, onFocus, searchKey }: Props) {
|
||||
return (
|
||||
<div className={'search-input-wrapper'}>
|
||||
<TextInput
|
||||
|
|
@ -22,13 +25,30 @@ export function SearchBar({ onSearch, onFocus, searchKey }: Props) {
|
|||
}}
|
||||
tabIndex={0}
|
||||
className={cx('search-input', {
|
||||
error: !isValidRegexpSource(searchKey),
|
||||
error: searchMode === 'regex' && !isValidRegexpSource(value),
|
||||
})}
|
||||
aria-label="search files"
|
||||
placeholder="Search files (use RegExp)"
|
||||
placeholder={`Search files (${searchMode === 'regex' ? 'use RegExp' : 'match sequence'})`}
|
||||
onChange={({ target: { value } }) => onSearch(value)}
|
||||
value={searchKey}
|
||||
value={value}
|
||||
/>
|
||||
<div className={`actions`}>
|
||||
<Label
|
||||
className={`toggle-mode`}
|
||||
variant="small"
|
||||
outline
|
||||
title="Toggle search mode"
|
||||
onClick={() => {
|
||||
configs.onChange({
|
||||
searchMode: searchMode === 'regex' ? 'fuzzy' : 'regex',
|
||||
})
|
||||
onSearch('')
|
||||
}}
|
||||
aria-label="Toggle search mode"
|
||||
>
|
||||
{searchMode === 'regex' ? '.*?' : 'Seq'}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import * as React from 'react'
|
||||
import { cx } from 'utils/cx'
|
||||
import { ModeShape } from '.'
|
||||
import { Highlight } from '../Highlight'
|
||||
|
||||
export const fuzzyMode: ModeShape = {
|
||||
getSearchParams(searchKey) {
|
||||
if (!searchKey) return null
|
||||
|
||||
const matchNode = (node: TreeNode) => fuzzyMatch(searchKey, node.path)
|
||||
return {
|
||||
matchNode,
|
||||
|
|
@ -11,15 +14,22 @@ export const fuzzyMode: ModeShape = {
|
|||
},
|
||||
renderNodeLabelText(node, searchKey) {
|
||||
const { name, path } = node
|
||||
const indexes = fuzzyMatchIndexes(searchKey, path, path.length - name.length)
|
||||
return (
|
||||
<span>
|
||||
<Highlight
|
||||
match={new RegExp(indexes.map(i => (i === 0 ? `^.` : `(?<=^.{${i}}).`)).join('|'))}
|
||||
text={name}
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
|
||||
const result: React.ReactNode[] = []
|
||||
const chunks = name.split('/')
|
||||
let renderedPath = path.slice(0, path.length - name.length)
|
||||
chunks.forEach((chunk, index, chunks) => {
|
||||
renderedPath += '/' + chunk
|
||||
const indexes = fuzzyMatchIndexes(searchKey, renderedPath, renderedPath.length - chunk.length)
|
||||
const regexp = new RegExp(indexes.map(i => `(?<=^.{${i}}).`).join('|'))
|
||||
result.push(
|
||||
<span key={chunk} className={cx({ prefix: index + 1 !== chunks.length })}>
|
||||
<Highlight match={regexp} text={chunk} />
|
||||
{index + 1 !== chunks.length && '/'}
|
||||
</span>,
|
||||
)
|
||||
})
|
||||
return result
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -27,22 +37,21 @@ function fuzzyMatch(input: string, sample: string) {
|
|||
let i = 0,
|
||||
j = 0
|
||||
while (i < input.length && j < sample.length) {
|
||||
if (input[i] === sample[j]) i++
|
||||
j++
|
||||
if (input[i] === sample[j++]) i++
|
||||
}
|
||||
return i === input.length
|
||||
}
|
||||
|
||||
function fuzzyMatchIndexes(input: string, sample: string, shift: number = 0) {
|
||||
const r: number[] = []
|
||||
const indexes: number[] = []
|
||||
let i = 0,
|
||||
j = 0
|
||||
while (i < input.length && j < sample.length) {
|
||||
if (input[i] === sample[j]) {
|
||||
if (j >= shift) r.push(j - shift)
|
||||
if (j >= shift) indexes.push(j - shift)
|
||||
i++
|
||||
}
|
||||
j++
|
||||
}
|
||||
return r
|
||||
return indexes
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { Highlight } from '../Highlight'
|
|||
|
||||
export const regexMode: ModeShape = {
|
||||
getSearchParams(searchKey) {
|
||||
if (!searchKey) return null
|
||||
|
||||
const regexp = searchKeyToRegexp(searchKey)
|
||||
if (regexp) {
|
||||
const matchNode = (node: TreeNode) => regexp.test(node.name)
|
||||
|
|
@ -22,10 +24,10 @@ export const regexMode: ModeShape = {
|
|||
searchKey && isValidRegexpSource(searchKey) ? new RegExp(searchKey, 'gi') : undefined
|
||||
const { name } = node
|
||||
return name.includes('/') ? (
|
||||
name.split('/').map((chunk, index, arr) => (
|
||||
<span key={chunk} className={cx({ prefix: index + 1 !== arr.length })}>
|
||||
name.split('/').map((chunk, index, chunks) => (
|
||||
<span key={chunk} className={cx({ prefix: index + 1 !== chunks.length })}>
|
||||
<Highlight match={regex} text={chunk} />
|
||||
{index + 1 !== arr.length && '/'}
|
||||
{index + 1 !== chunks.length && '/'}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { searchModes } from 'components/searchModes'
|
||||
import { GetCreatedMethod, MethodCreator } from 'driver/connect'
|
||||
import { platform } from 'platforms'
|
||||
import { Config } from 'utils/configHelper'
|
||||
|
|
@ -17,12 +18,13 @@ export type Props = {
|
|||
|
||||
export type ConnectorState = {
|
||||
state: 'pulling' | 'rendering' | 'done'
|
||||
visibleNodesGenerator: VisibleNodesGenerator | null
|
||||
visibleNodes: VisibleNodes | null
|
||||
searchKey: string
|
||||
searched: boolean // derived state from searchKey, = !!searchKey
|
||||
|
||||
handleKeyDown: GetCreatedMethod<typeof handleKeyDown>
|
||||
search: GetCreatedMethod<typeof search>
|
||||
updateSearchKey: GetCreatedMethod<typeof updateSearchKey>
|
||||
onNodeClick: GetCreatedMethod<typeof onNodeClick>
|
||||
onFocusSearchBar: GetCreatedMethod<typeof onFocusSearchBar>
|
||||
setUpTree: GetCreatedMethod<typeof setUpTree>
|
||||
|
|
@ -40,20 +42,18 @@ function getVisibleParentNode(nodes: TreeNode[], focusedNode: TreeNode) {
|
|||
}
|
||||
}
|
||||
|
||||
let visibleNodesGenerator: VisibleNodesGenerator
|
||||
|
||||
type BoundMethodCreator<Args extends any[] = []> = MethodCreator<Props, ConnectorState, Args>
|
||||
|
||||
export const setUpTree: BoundMethodCreator<
|
||||
[
|
||||
Required<Pick<Props, 'treeRoot' | 'metaData'>> & {
|
||||
config: Pick<Config, 'compressSingletonFolder' | 'accessToken'>
|
||||
config: Pick<Config, 'compressSingletonFolder' | 'accessToken' | 'searchMode'>
|
||||
},
|
||||
]
|
||||
> = dispatch => async ({ treeRoot, metaData, config }) => {
|
||||
dispatch.set({ state: 'rendering' })
|
||||
|
||||
visibleNodesGenerator = new VisibleNodesGenerator({
|
||||
const visibleNodesGenerator = new VisibleNodesGenerator({
|
||||
root: treeRoot,
|
||||
compress: config.compressSingletonFolder,
|
||||
async getTreeData(path) {
|
||||
|
|
@ -61,7 +61,9 @@ export const setUpTree: BoundMethodCreator<
|
|||
return root
|
||||
},
|
||||
})
|
||||
dispatch.set({ visibleNodesGenerator })
|
||||
visibleNodesGenerator.onUpdate(visibleNodes => dispatch.set({ visibleNodes }))
|
||||
|
||||
if (platform.shouldExpandAll?.()) {
|
||||
const unsubscribe = visibleNodesGenerator.onUpdate(visibleNodes => {
|
||||
unsubscribe()
|
||||
|
|
@ -69,11 +71,11 @@ export const setUpTree: BoundMethodCreator<
|
|||
dispatch.call(toggleNodeExpansion, node, { recursive: true }),
|
||||
)
|
||||
})
|
||||
dispatch.call(search, '')
|
||||
visibleNodesGenerator.search(searchModes[config.searchMode].getSearchParams(''))
|
||||
} else {
|
||||
const targetPath = platform.getCurrentPath(metaData.branchName)
|
||||
if (targetPath) dispatch.call(goTo, targetPath)
|
||||
else dispatch.call(search, '')
|
||||
else visibleNodesGenerator.search(searchModes[config.searchMode].getSearchParams(''))
|
||||
}
|
||||
|
||||
dispatch.set({ state: 'done' })
|
||||
|
|
@ -186,12 +188,16 @@ export const handleKeyDown: BoundMethodCreator<[React.KeyboardEvent]> = dispatch
|
|||
|
||||
export const onFocusSearchBar: BoundMethodCreator = dispatch => () => dispatch.call(focusNode, null)
|
||||
|
||||
export const search: BoundMethodCreator<[string]> = dispatch => searchKey => {
|
||||
export const updateSearchKey: BoundMethodCreator<[string]> = dispatch => searchKey => {
|
||||
dispatch.set({ searchKey, searched: searchKey !== '' })
|
||||
visibleNodesGenerator.search({ searchKey })
|
||||
}
|
||||
|
||||
export const goTo: BoundMethodCreator<[string[]]> = dispatch => currentPath => {
|
||||
const {
|
||||
state: { visibleNodesGenerator },
|
||||
} = dispatch.get()
|
||||
if (!visibleNodesGenerator) return
|
||||
|
||||
dispatch.set({ searchKey: '', searched: false })
|
||||
visibleNodesGenerator.search(null)
|
||||
dispatch.call(expandTo, currentPath)
|
||||
|
|
@ -201,6 +207,11 @@ export const setExpand: BoundMethodCreator<[TreeNode, boolean]> = dispatch => as
|
|||
node,
|
||||
expand = false,
|
||||
) => {
|
||||
const {
|
||||
state: { visibleNodesGenerator },
|
||||
} = dispatch.get()
|
||||
if (!visibleNodesGenerator) return
|
||||
|
||||
await visibleNodesGenerator.setExpand(node, expand)
|
||||
dispatch.call(focusNode, node)
|
||||
}
|
||||
|
|
@ -213,6 +224,11 @@ export const toggleNodeExpansion: BoundMethodCreator<
|
|||
},
|
||||
]
|
||||
> = dispatch => async (node, { recursive = false }) => {
|
||||
const {
|
||||
state: { visibleNodesGenerator },
|
||||
} = dispatch.get()
|
||||
if (!visibleNodesGenerator) return
|
||||
|
||||
visibleNodesGenerator.focusNode(node)
|
||||
await visibleNodesGenerator.toggleExpand(node, recursive)
|
||||
}
|
||||
|
|
@ -220,6 +236,11 @@ export const toggleNodeExpansion: BoundMethodCreator<
|
|||
export const focusNode: BoundMethodCreator<[TreeNode | null]> = dispatch => (
|
||||
node: TreeNode | null,
|
||||
) => {
|
||||
const {
|
||||
state: { visibleNodesGenerator },
|
||||
} = dispatch.get()
|
||||
if (!visibleNodesGenerator) return
|
||||
|
||||
visibleNodesGenerator.focusNode(node)
|
||||
}
|
||||
|
||||
|
|
@ -255,6 +276,11 @@ export const onNodeClick: BoundMethodCreator<
|
|||
}
|
||||
|
||||
export const expandTo: BoundMethodCreator<[string[]]> = dispatch => async currentPath => {
|
||||
const {
|
||||
state: { visibleNodesGenerator },
|
||||
} = dispatch.get()
|
||||
if (!visibleNodesGenerator) return
|
||||
|
||||
const nodeExpandedTo = await visibleNodesGenerator.expandTo(currentPath.join('/'))
|
||||
if (nodeExpandedTo) {
|
||||
visibleNodesGenerator.focusNode(nodeExpandedTo)
|
||||
|
|
|
|||
|
|
@ -447,6 +447,8 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
|
|||
|
||||
/* search input */
|
||||
.search-input-wrapper {
|
||||
position: relative;
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
box-shadow: none; // stay low
|
||||
|
|
@ -454,11 +456,33 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
|
|||
border-radius: 0; // its rounded corner not match with nearby elements
|
||||
color: var(--gitako-text-primary);
|
||||
background: var(--gitako-bg-canvas);
|
||||
padding-right: 32px; // save space for actions
|
||||
|
||||
&.error {
|
||||
border-color: var(--gitako-border-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 0 2px;
|
||||
|
||||
.toggle-mode {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--gitako-bg-tertiary);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--gitako-bg-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.files {
|
||||
|
|
@ -543,7 +567,8 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
|
|||
}
|
||||
}
|
||||
|
||||
.go-to-button {
|
||||
.go-to-button,
|
||||
.find-in-folder-button {
|
||||
@include icon-button();
|
||||
@include button-color();
|
||||
width: 28px;
|
||||
|
|
@ -555,7 +580,8 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
|
|||
}
|
||||
}
|
||||
&:not(:hover) {
|
||||
.go-to-button {
|
||||
.go-to-button,
|
||||
.find-in-folder-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -324,7 +324,6 @@ type Options = {
|
|||
|
||||
export type VisibleNodes = {
|
||||
loading: BaseLayer['loading']
|
||||
lastMatch: ShakeLayer['lastSearchParams']
|
||||
depths: CompressLayer['depths']
|
||||
nodes: FlattenLayer['nodes']
|
||||
expandedNodes: FlattenLayer['expandedNodes']
|
||||
|
|
@ -353,7 +352,6 @@ export class VisibleNodesGenerator extends FlattenLayer {
|
|||
get visibleNodes(): VisibleNodes {
|
||||
return {
|
||||
nodes: this.nodes,
|
||||
lastMatch: this.lastSearchParams,
|
||||
depths: this.depths,
|
||||
expandedNodes: this.expandedNodes,
|
||||
focusedNode: this.focusedNode,
|
||||
|
|
|
|||
Loading…
Reference in a new issue