feat: reveal item from search result

This commit is contained in:
EnixCoda 2019-02-21 15:42:19 +08:00
parent 9f3ee8b5b5
commit a09bba0a36
No known key found for this signature in database
GPG key ID: 6825847C88AA329A
6 changed files with 101 additions and 23 deletions

View file

@ -7,7 +7,8 @@ import LoadingIndicator from 'components/LoadingIndicator'
import cx from 'utils/cx'
import { ConnectorState } from 'driver/core/FileExplorer'
import { TreeData, MetaData } from 'utils/GitHubHelper'
import { VisibleNodes } from 'utils/VisibleNodesGenerator'
import { VisibleNodes, TreeNode } from 'utils/VisibleNodesGenerator'
import Icon from './Icon'
export type Props = {
treeData: TreeData
@ -21,6 +22,8 @@ export type Props = {
class FileExplorer extends React.Component<Props & ConnectorState> {
static defaultProps: Partial<Props & ConnectorState> = {
freeze: false,
searchKey: '',
searched: false,
}
componentWillMount() {
@ -48,7 +51,9 @@ class FileExplorer extends React.Component<Props & ConnectorState> {
renderFiles(visibleNodes: VisibleNodes, onNodeClick: Node['props']['onClick']) {
const { nodes, depths, focusedNode, expandedNodes } = visibleNodes
if (nodes.length === 0) {
const { goTo, searchKey, searched } = this.props
const inSearch = searchKey !== ''
if (inSearch && nodes.length === 0) {
return <label className={'no-results'}>No results found.</label>
}
return (
@ -61,12 +66,34 @@ class FileExplorer extends React.Component<Props & ConnectorState> {
focused={focusedNode === node}
expanded={expandedNodes.has(node)}
onClick={onNodeClick}
renderActions={() =>
inSearch &&
searched && (
<div className={'go-to-wrapper'}>
<button className={'go-to-button'} onClick={this.revealNode(goTo, node)}>
<Icon type="go-to" />
&nbsp;Reveal in file tree
</button>
</div>
)
}
/>
))}
</div>
)
}
revealNode(
goTo: (path: string[]) => void,
node: TreeNode,
): (event: React.MouseEvent<HTMLElement, MouseEvent>) => void {
return e => {
e.stopPropagation()
e.preventDefault()
goTo(node.path.split('/'))
}
}
render() {
const {
stateText,
@ -77,6 +104,7 @@ class FileExplorer extends React.Component<Props & ConnectorState> {
onNodeClick,
toggleShowSettings,
onFocusSearchBar,
searchKey,
} = this.props
return (
<div
@ -90,7 +118,11 @@ class FileExplorer extends React.Component<Props & ConnectorState> {
) : (
visibleNodes && (
<React.Fragment>
<SearchBar onSearchKeyChange={handleSearchKeyChange} onFocus={onFocusSearchBar} />
<SearchBar
searchKey={searchKey}
onSearchKeyChange={handleSearchKeyChange}
onFocus={onFocusSearchBar}
/>
{this.renderFiles(visibleNodes, onNodeClick)}
</React.Fragment>
)

View file

@ -10,6 +10,7 @@ import FileZip from 'assets/icons/octicons/file-zip.svg?svgr'
import Markdown from 'assets/icons/octicons/markdown.svg?svgr'
import FileMedia from 'assets/icons/octicons/file-media.svg?svgr'
import FileCode from 'assets/icons/octicons/file-code.svg?svgr'
import Reply from 'assets/icons/octicons/reply.svg?svgr'
// import FileBinary from 'assets/icons/octicons/file-binary.svg?svgr'
// import FileSymlinkDirectory from 'assets/icons/octicons/file-symlink-directory.svg?svgr'
// import FileSymlinkFile from 'assets/icons/octicons/file-symlink-file.svg?svgr'
@ -33,6 +34,8 @@ function getSVGIconComponent(type: string) {
return Gear
case 'folder':
return TriangleRight
case 'go-to':
return Reply
case '.pdf':
return FilePdf
case '.zip':
@ -72,7 +75,7 @@ const iconStyle = { width: '100%', height: '100%' }
type Props = {
type: string
className?: string
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void
onClick?: (event: React.MouseEvent<HTMLElement>) => void
}
const Icon: React.SFC<Props> = function Icon({ type, className = undefined, ...otherProps }) {

View file

@ -21,6 +21,7 @@ type Props = {
depth: number
expanded: boolean
focused: boolean
renderActions?(): React.ReactNode
}
export default class Node extends React.PureComponent<Props> {
onClick: React.MouseEventHandler = event => {
@ -31,7 +32,7 @@ export default class Node extends React.PureComponent<Props> {
}
render() {
const { node, depth, expanded, focused } = this.props
const { node, depth, expanded, focused, renderActions } = this.props
const { name, path, virtual } = node
if (virtual) {
// this is not a real node
@ -51,8 +52,11 @@ export default class Node extends React.PureComponent<Props> {
className={cx('node-item', { expanded })}
style={{ paddingLeft: `${10 + 20 * depth}px` }}
>
<Icon type={getIconType(node)} />
<span className={'node-item-name'}>{name}</span>
<div>
<Icon type={getIconType(node)} />
<span className={'node-item-name'}>{name}</span>
</div>
{renderActions && <div>{renderActions()}</div>}
</div>
</a>
</div>

View file

@ -3,9 +3,10 @@ import * as React from 'react'
type Props = {
onSearchKeyChange: React.FormEventHandler
onFocus: React.FocusEventHandler
searchKey: string
}
export default function SearchBar({ onSearchKeyChange, onFocus }: Props) {
export default function SearchBar({ onSearchKeyChange, onFocus, searchKey }: Props) {
return (
<div className={'search-input-wrapper'}>
<input
@ -15,7 +16,8 @@ export default function SearchBar({ onSearchKeyChange, onFocus }: Props) {
aria-label="search files"
placeholder="Search files (RegEx)"
type="text"
onInput={onSearchKeyChange}
onChange={onSearchKeyChange}
value={searchKey}
/>
</div>
)

View file

@ -304,12 +304,14 @@
}
.node-item {
display: flex;
justify-content: space-between;
word-wrap: normal;
word-break: break-all;
margin: 0;
color: #0366d6;
line-height: 20px;
padding: 6px 0;
padding: 6px 10px;
cursor: pointer;
border-top: 1px solid #eaecef;
transition: all 0.5s ease;
@ -330,6 +332,21 @@
}
}
}
.go-to-wrapper {
max-width: 16px;
overflow: hidden;
transition: max-width 0.5s linear;
&:hover {
max-width: 160px;
}
.go-to-button {
white-space: nowrap;
background: transparent;
border: none;
padding: 0;
}
}
}
.@{name}-settings-bar {

View file

@ -11,6 +11,8 @@ import Node from 'components/Node'
export type ConnectorState = {
stateText: string
visibleNodes: VisibleNodes
searchKey: string
searched: boolean
init: () => void
execAfterRender: () => void
@ -19,6 +21,7 @@ export type ConnectorState = {
onNodeClick: Node['props']['onClick']
onFocusSearchBar: React.FocusEventHandler
setUpTree: (treeData?: TreeData) => void
goTo: (path: string[]) => void
}
type DepthMap = Map<TreeNode, number>
@ -90,15 +93,7 @@ const setUpTree: MethodCreator<Props, ConnectorState> = dispatch => () =>
tasksAfterRender.push(DOMHelper.focusSearchInput)
dispatch.call(setStateText, '')
const currentPath = URLHelper.getCurrentPath(metaData.branchName)
if (currentPath.length) {
const nodeExpandedTo = visibleNodesGenerator.expandTo(currentPath.join('/'))
if (nodeExpandedTo) {
visibleNodesGenerator.focusNode(nodeExpandedTo)
const { nodes } = visibleNodesGenerator.visibleNodes
tasksAfterRender.push(() => DOMHelper.scrollToNodeElement(nodes.indexOf(nodeExpandedTo)))
}
}
dispatch.call(goTo, URLHelper.getCurrentPath(metaData.branchName))
dispatch.call(updateVisibleNodes)
})
@ -228,17 +223,40 @@ const handleSearchKeyChange: MethodCreator<
Props,
ConnectorState,
[React.FormEvent<HTMLInputElement>]
> = dispatch => {
> = dispatch => async event => {
const searchKey = event.currentTarget.value
await dispatch.call(search, searchKey)
}
const search: MethodCreator<Props, ConnectorState, [string]> = dispatch => {
let i = 0
return async event => {
const searchKey = event.currentTarget.value
return async searchKey => {
dispatch.set({ searchKey })
const j = (i += 1)
await visibleNodesGenerator.search(searchKey)
if (i === j) dispatch.call(updateVisibleNodes)
if (i === j) {
dispatch.set(({ searched }) => ({ searched: !(searched && searchKey === '') }))
dispatch.call(updateVisibleNodes)
}
}
}
const delayExpandThreshold = 400
const goTo: MethodCreator<Props, ConnectorState, [string[]]> = dispatch => async currentPath => {
if (currentPath.length) {
await visibleNodesGenerator.search('')
dispatch.set({ searchKey: '', searched: false })
const nodeExpandedTo = visibleNodesGenerator.expandTo(currentPath.join('/'))
if (nodeExpandedTo) {
visibleNodesGenerator.focusNode(nodeExpandedTo)
const { nodes } = visibleNodesGenerator.visibleNodes
tasksAfterRender.push(() => DOMHelper.scrollToNodeElement(nodes.indexOf(nodeExpandedTo)))
dispatch.call(updateVisibleNodes)
}
}
}
function shouldDelayExpand(node: TreeNode) {
return (
visibleNodesGenerator.visibleNodes.expandedNodes.has(node) &&
@ -336,8 +354,10 @@ export default {
setStateText,
handleKeyDown,
onFocusSearchBar,
search,
handleSearchKeyChange,
setExpand,
goTo,
toggleNodeExpansion,
focusNode,
onNodeClick,