diff --git a/src/analytics.ts b/src/analytics.ts index 9938eb4..5cde36d 100644 --- a/src/analytics.ts +++ b/src/analytics.ts @@ -2,11 +2,11 @@ import { version } from '../package.json' // TODO: set this through ENV or something else const LOG_ENDPOINT = 'https://enix.one/gitako/log' -export function raiseError(error) { +export function raiseError(error: Error) { return reportError(error) } -export function withErrorLog(method, args) { +export function withErrorLog(method: Function, args: any[]) { return [ function() { try { @@ -19,13 +19,13 @@ export function withErrorLog(method, args) { ] } -function encodeParams(params) { +function encodeParams(params: any) { return Object.keys(params) .map(key => `${key}=${encodeURIComponent(JSON.stringify(params[key]))}`) .join('&') } -function reportError(error) { +function reportError(error: Error) { return fetch( `${LOG_ENDPOINT}?${encodeParams({ error: (error && error.message) || error, diff --git a/src/components/AutoSizer.jsx b/src/components/AutoSizer.tsx similarity index 62% rename from src/components/AutoSizer.jsx rename to src/components/AutoSizer.tsx index 4535a96..e7e9db5 100644 --- a/src/components/AutoSizer.jsx +++ b/src/components/AutoSizer.tsx @@ -1,11 +1,19 @@ -import React from 'react' -import PropTypes from 'prop-types' +import * as React from 'react' -export default class AutoSizer extends React.Component { - static propTypes = {} +type Props = { + children(size: Size): React.ReactNode +} - static defaultProps = {} +type Size = { + width: number + height: number +} +type State = { + size: Size +} + +export default class AutoSizer extends React.Component { state = { size: { width: 0, @@ -13,7 +21,7 @@ export default class AutoSizer extends React.Component { }, } - ref = React.createRef() + ref = React.createRef() componentDidMount() { const container = this.ref.current diff --git a/src/components/Icon.jsx b/src/components/Icon.jsx index 4f952e4..b9ed481 100644 --- a/src/components/Icon.jsx +++ b/src/components/Icon.jsx @@ -69,7 +69,7 @@ function getSVGIconComponent(type) { const iconStyle = { width: '100%', height: '100%' } -export default function Icon({ type, className, ...otherProps }) { +export default function Icon({ type, className = undefined, ...otherProps }) { return (
{React.createElement(getSVGIconComponent(type), iconStyle)} diff --git a/src/components/Node.jsx b/src/components/Node.tsx similarity index 74% rename from src/components/Node.jsx rename to src/components/Node.tsx index 3b6130f..0ec8d3a 100644 --- a/src/components/Node.jsx +++ b/src/components/Node.tsx @@ -1,10 +1,10 @@ -import React from 'react' +import * as React from 'react' import Icon from 'components/Icon' import cx from 'utils/cx' -import DOMHelper from 'utils/DOMHelper' -import LoadingIndicator from 'components/LoadingIndicator'; +import LoadingIndicator from 'components/LoadingIndicator' +import { TreeNode } from 'utils/VisibleNodesGenerator' -function getIconType(node) { +function getIconType(node: TreeNode) { switch (node.type) { case 'tree': return 'folder' @@ -15,8 +15,15 @@ function getIconType(node) { } } -export default class Node extends React.PureComponent { - onClick = (event) => { +type Props = { + node: TreeNode + onClick(node: TreeNode): void + depth: number + expanded: boolean + focused: boolean +} +export default class Node extends React.PureComponent { + onClick: React.MouseEventHandler = event => { if (event.metaKey) return event.preventDefault() const { node, onClick } = this.props diff --git a/src/utils/visibleNodesGenerator.js b/src/utils/visibleNodesGenerator.ts similarity index 69% rename from src/utils/visibleNodesGenerator.js rename to src/utils/visibleNodesGenerator.ts index 367938e..4f92f34 100644 --- a/src/utils/visibleNodesGenerator.js +++ b/src/utils/visibleNodesGenerator.ts @@ -19,19 +19,28 @@ * v stable */ +export type TreeNode = { + name: string + contents?: TreeNode[] + path: string + url?: string + virtual?: boolean + type: 'tree' | 'blob' | 'commit' | 'virtual' +} + const SEARCH_DELAY = 250 -function getFilterFunc(keyRegex) { - return function filterFunc({ name }) { +function getFilterFunc(keyRegex: RegExp) { + return function filterFunc({ name }: TreeNode) { return keyRegex.test(name) } } -function filterDuplications(arr) { +function filterDuplications(arr: T[]) { return Array.from(new Set(arr)) } -function search(treeNodes, searchKey) { +function search(treeNodes: TreeNode[], searchKey: string): TreeNode[] { if (!searchKey) return treeNodes /** * if searchKey is 'abcd' @@ -53,17 +62,21 @@ function search(treeNodes, searchKey) { return filterDuplications(searchResults) } -function debounce(func, delay) { - let timer - return (...args) => new Promise(resolve => { - window.clearTimeout(timer) - timer = window.setTimeout(() => resolve(func(...args)), delay) - }) +function debounce( + func: (...args: Arg[]) => T, + delay: number +): (...args: Arg[]) => Promise { + let timer: number + return (...args) => + new Promise(resolve => { + window.clearTimeout(timer) + timer = window.setTimeout(() => resolve(func(...args)), delay) + }) } export const debouncedSearch = debounce(search, SEARCH_DELAY) -function getNodes(root, nodes = []) { +function getNodes(root: TreeNode, nodes: TreeNode[] = []) { if (!root.contents) return root.contents.forEach(node => { nodes.push(node) @@ -72,7 +85,7 @@ function getNodes(root, nodes = []) { return nodes } -function compressTree(root, prefix = []) { +function compressTree(root: TreeNode, prefix: string[] = []): TreeNode { if (root.contents) { if (root.contents.length === 1) { const singleton = root.contents[0] @@ -84,35 +97,33 @@ function compressTree(root, prefix = []) { return { ...root, name: [...prefix, root.name].join('/'), - contents: root.contents - ? root.contents.map(node => compressTree(node)) - : undefined, + contents: root.contents ? root.contents.map(node => compressTree(node)) : undefined, } } export default class VisibleNodesGenerator { // LEVEL 1 - root = null - nodes = null + root: TreeNode = null + nodes: TreeNode[] = null + compress = false compressed = false + compressedRoot: TreeNode | undefined getRoot() { - return this.compress && this.compressed - ? this.compressedRoot - : this.root + return this.compress && this.compressed ? this.compressedRoot : this.root } - async plantTree(root) { + async plantTree(root: TreeNode) { this.root = root this.nodes = getNodes(root) this.compressedRoot = compressTree(root) - await this.search() + await this.search('') } // LEVEL 2 - searchedNodes = null - async search(searchKey) { + searchedNodes: TreeNode[] | null = null + async search(searchKey: string) { this.compressed = !Boolean(searchKey) this.searchedNodes = searchKey ? await debouncedSearch(this.nodes, searchKey) @@ -122,18 +133,18 @@ export default class VisibleNodesGenerator { this.generateVisibleNodes() } - setCompress(compress) { + setCompress(compress: VisibleNodesGenerator['compress']) { this.compress = compress } // LEVEL 3 - expandedNodes = new Set() + expandedNodes = new Set() depths = new Map() - toggleExpand(node) { + toggleExpand(node: TreeNode) { this.setExpand(node, !this.expandedNodes.has(node)) } - setExpand(node, expand) { + setExpand(node: TreeNode, expand: boolean) { if (expand && node.contents) { // only node with contents is expandable this.expandedNodes.add(node) @@ -143,9 +154,9 @@ export default class VisibleNodesGenerator { this.generateVisibleNodes() } - expandTo(path) { + expandTo(path: string) { let root = this.getRoot() - const findNode = (root) => { + const findNode: (root: TreeNode) => TreeNode = root => { if (path.indexOf(root.path) === 0) { if (root.path === path) return root this.setExpand(root, true) @@ -165,13 +176,20 @@ export default class VisibleNodesGenerator { return node } - visibleNodes = null + visibleNodes: { + nodes: VisibleNodesGenerator['nodes'] + depths: VisibleNodesGenerator['depths'] + expandedNodes: VisibleNodesGenerator['expandedNodes'] + focusedNode: TreeNode + } = null generateVisibleNodes() { this.focusedNode = null this.depths.clear() const nodesSet = new Set() // prevent duplication - const nodes = [], stack = this.searchedNodes.slice().reverse() - let current, depth = 0 + const nodes = [], + stack = this.searchedNodes.slice().reverse() + let current, + depth = 0 while (stack.length) { current = stack.pop() if (current === null) { @@ -192,13 +210,14 @@ export default class VisibleNodesGenerator { nodes, depths: this.depths, expandedNodes: this.expandedNodes, + focusedNode: null, } this.focusNode(null) } // LEVEL 4 - focusedNode = null - focusNode(node) { + focusedNode: TreeNode = null + focusNode(node: TreeNode) { this.focusedNode = node this.visibleNodes = { ...this.visibleNodes,