Merge branch 'develop'

This commit is contained in:
EnixCoda 2019-10-24 23:12:30 +08:00
commit 9d004278ed
No known key found for this signature in database
GPG key ID: 0C1A07377913A1DD
25 changed files with 1812 additions and 2502 deletions

View file

@ -1,6 +1,6 @@
{
"name": "gitako",
"version": "0.5.8",
"version": "0.5.13",
"description": "The missing part of GitHub.",
"repository": "https://github.com/EnixCoda/Gitako",
"author": "EnixCoda",
@ -53,7 +53,7 @@
"less": "^3.9.0",
"less-loader": "^4.0.5",
"style-loader": "^0.23.1",
"typescript": "3.3",
"typescript": "^3.6.3",
"uglifyjs-webpack-plugin": "^2.1.2",
"url-loader": "^1.1.2",
"webpack": "^4.29.6",

View file

@ -26,19 +26,19 @@
],
"types": "index.d.ts",
"devDependencies": {
"browserify": "^15.0.0",
"browserify": "^16.5.0",
"jscs": "^3.0.7",
"jsdom": "^11.5.1",
"jsdom": "^15.2.0",
"jsdom-global": "^3.0.2",
"jshint": "^2.5.6",
"npmpub": "^3.1.0",
"nyc": "^11.4.1",
"opn-cli": "^3.1.0",
"serve": "^11.0.0",
"jshint": "^2.10.2",
"npmpub": "^5.0.0",
"nyc": "^14.1.1",
"opn-cli": "^5.0.0",
"serve": "^11.2.0",
"tap-nyc": "^1.0.3",
"tap-spec": "^4.1.1",
"tape": "^4.8.0",
"uglify-js": "^3.3.8"
"tap-spec": "^5.0.0",
"tape": "^4.11.0",
"uglify-js": "^3.6.4"
},
"scripts": {
"lint": "jscs . && jshint . --exclude-path .gitignore",

File diff suppressed because it is too large Load diff

View file

@ -2,13 +2,16 @@
rm -rf dist
yarn build
GITAKO_VERSION=v$(node scripts/get-version.js)
echo "Got version $GITAKO_VERSION"
GIT_SHA=$(git rev-parse HEAD)
VERSION=v$(node scripts/get-version.js)
echo "Got version $VERSION"
# sentry
yarn sentry-cli releases new "$GITAKO_VERSION"
yarn sentry-cli releases files "$GITAKO_VERSION" upload-sourcemaps dist --no-rewrite
yarn sentry-cli releases finalize "$GITAKO_VERSION"
yarn sentry-cli releases new "$VERSION"
git push # make sure sentry can retrieve current commit on remote
yarn sentry-cli releases set-commits $VERSION --commit "EnixCoda/Gitako@$GIT_SHA"
yarn sentry-cli releases files "$VERSION" upload-sourcemaps dist --no-rewrite
yarn sentry-cli releases finalize "$VERSION"
cd dist
rm -f ./gitako.zip

View file

@ -1,6 +1,6 @@
import * as Sentry from '@sentry/browser'
import { Middleware } from 'driver/connect.js'
import { IN_PRODUCTION_MODE } from 'env'
import * as Sentry from '@sentry/browser'
import { version } from '../package.json'
const PUBLIC_KEY = 'd22ec5c9cc874539a51c78388c12e3b0'
@ -29,19 +29,24 @@ export const withErrorLog: Middleware = function withErrorLog(method, args) {
]
}
function reportError(error: Error, extra?: any) {
function reportError(
error: Error,
extra?: {
[key: string]: any
},
) {
if (!IN_PRODUCTION_MODE) {
console.error(error)
console.error('Extra:\n', extra)
return
}
Sentry.captureException(error)
if (extra) {
Sentry.withScope(scope => {
Sentry.withScope(scope => {
if (extra) {
Object.keys(extra).forEach(key => {
scope.setExtra(key, extra[key])
})
})
}
}
Sentry.captureException(error)
})
}

View file

@ -1,17 +1,17 @@
import * as React from 'react'
import { FixedSizeList as List, ListChildComponentProps } from 'react-window'
import LoadingIndicator from 'components/LoadingIndicator'
import Node from 'components/Node'
import SearchBar from 'components/SearchBar'
import connect from 'driver/connect'
import { FileExplorer as FileExplorerCore } from 'driver/core'
import SearchBar from 'components/SearchBar'
import Node from 'components/Node'
import LoadingIndicator from 'components/LoadingIndicator'
import cx from 'utils/cx'
import { ConnectorState } from 'driver/core/FileExplorer'
import { TreeData, MetaData } from 'utils/GitHubHelper'
import { VisibleNodes, TreeNode } from 'utils/VisibleNodesGenerator'
import * as React from 'react'
import { FixedSizeList as List, ListChildComponentProps } from 'react-window'
import cx from 'utils/cx'
import { MetaData, TreeData } from 'utils/GitHubHelper'
import { usePrevious } from 'utils/hooks'
import { TreeNode, VisibleNodes } from 'utils/VisibleNodesGenerator'
import Icon from './Icon'
import SizeObserver from './SizeObserver'
import { usePrevious } from 'utils/hooks'
export type Props = {
treeData?: TreeData
@ -26,7 +26,6 @@ class FileExplorer extends React.Component<Props & ConnectorState> {
static defaultProps: Partial<Props & ConnectorState> = {
freeze: false,
searchKey: '',
searched: false,
visibleNodes: null,
}
@ -126,16 +125,16 @@ class FileExplorer extends React.Component<Props & ConnectorState> {
})
private renderActions: Node['props']['renderActions'] = node => {
const { searchKey, searched, goTo } = this.props
const { searchKey, goTo } = this.props
return (
searchKey &&
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>
searchKey && (
<button
title={'Reveal in file tree'}
className={'go-to-button'}
onClick={this.revealNode(goTo, node)}
>
<Icon type="go-to" />
</button>
)
)
}
@ -157,7 +156,7 @@ class FileExplorer extends React.Component<Props & ConnectorState> {
visibleNodes,
freeze,
handleKeyDown,
handleSearchKeyChange,
search,
toggleShowSettings,
onFocusSearchBar,
searchKey,
@ -174,11 +173,7 @@ class FileExplorer extends React.Component<Props & ConnectorState> {
) : (
visibleNodes && (
<React.Fragment>
<SearchBar
searchKey={searchKey}
onSearchKeyChange={handleSearchKeyChange}
onFocus={onFocusSearchBar}
/>
<SearchBar searchKey={searchKey} onSearch={search} onFocus={onFocusSearchBar} />
{this.renderFiles(visibleNodes)}
</React.Fragment>
)

View file

@ -1,8 +1,8 @@
import * as React from 'react'
import Icon from 'components/Icon'
import * as React from 'react'
import cx from 'utils/cx'
import { OperatingSystems, os } from 'utils/general'
import { TreeNode } from 'utils/VisibleNodesGenerator'
import { os, OperatingSystems } from 'utils/general'
function getIconType(node: TreeNode) {
switch (node.type) {
@ -52,7 +52,7 @@ export default class Node extends React.PureComponent<Props> {
className={cx('node-item', { expanded })}
style={{ paddingLeft: `${10 + 20 * depth}px` }}
>
<div>
<div className={'node-item-label'}>
<Icon type={getIconType(node)} />
<span className={'node-item-name'}>{name}</span>
</div>

View file

@ -1,24 +1,36 @@
import * as React from 'react'
import cx from 'utils/cx'
type Props = {
onSearchKeyChange: React.FormEventHandler
onSearch: (searchKey: string) => void
onFocus: React.FocusEventHandler
searchKey: string
}
export default function SearchBar({ onSearchKeyChange, onFocus, searchKey }: Props) {
export default function SearchBar({ onSearch, onFocus, searchKey }: Props) {
return (
<div className={'search-input-wrapper'}>
<input
onFocus={onFocus}
tabIndex={0}
className="form-control search-input"
className={cx('form-control search-input', {
error: !isValidRegexpSource(searchKey),
})}
aria-label="search files"
placeholder="Search files (RegEx)"
placeholder="Search files (use RegExp)"
type="text"
onChange={onSearchKeyChange}
onChange={({ target: { value } }) => onSearch(value)}
value={searchKey}
/>
</div>
)
}
function isValidRegexpSource(source: string) {
try {
new RegExp(source)
return true
} catch (err) {
return false
}
}

View file

@ -1,17 +1,17 @@
import Icon from 'components/Icon'
import * as React from 'react'
import configHelper, { configKeys } from 'utils/configHelper'
import configHelper, { Config, configKeys } from 'utils/configHelper'
import { friendlyFormatShortcut, JSONRequest, parseURLSearch } from 'utils/general'
import keyHelper from 'utils/keyHelper'
import { version } from '../../package.json'
const WIKI_HOME_LINK = 'https://github.com/EnixCoda/Gitako/wiki'
const wikiLinks = {
compressSingletonFolder: 'https://github.com/EnixCoda/Gitako/wiki/Compress-Singleton-Folder',
changeLog: 'https://github.com/EnixCoda/Gitako/wiki/Change-Log',
copyFileButton: 'https://github.com/EnixCoda/Gitako/wiki/Copy-file-and-snippet',
copySnippet: 'https://github.com/EnixCoda/Gitako/wiki/Copy-file-and-snippet',
createAccessToken:
'https://github.com/EnixCoda/Gitako/wiki/How-to-create-access-token-for-Gitako%3F',
compressSingletonFolder: `${WIKI_HOME_LINK}/Compress-Singleton-Folder`,
changeLog: `${WIKI_HOME_LINK}/Change-Log`,
copyFileButton: `${WIKI_HOME_LINK}/Copy-file-and-snippet`,
copySnippet: `${WIKI_HOME_LINK}/Copy-file-and-snippet`,
createAccessToken: `${WIKI_HOME_LINK}/How-to-create-access-token-for-Gitako%3F`,
}
const oauth = {
@ -26,15 +26,16 @@ type Props = {
activated: boolean
onAccessTokenChange: (accessToken: string) => void
onShortcutChange: (shortcut: string) => void
compressSingletonFolder: boolean
copyFileButton: boolean
copySnippetButton: boolean
setCopyFile: (copyFileButton: Props['copyFileButton']) => void
setCopySnippet: (copySnippetButton: Props['copySnippetButton']) => void
setCompressSingleton: (compressSingletonFolder: Props['compressSingletonFolder']) => void
setIntelligentToggle: (intelligentToggle: Props['intelligentToggle']) => void
toggleShowSettings: () => void
toggleShowSideBarShortcut?: string
}
} & Pick<
Config,
'compressSingletonFolder' | 'copyFileButton' | 'copySnippetButton' | 'intelligentToggle'
>
type State = {
accessToken?: string
@ -47,7 +48,8 @@ type State = {
label: string
onChange: (e: React.FormEvent<HTMLInputElement>) => Promise<void> | void
getValue: () => boolean
wikiLink: string
wikiLink?: string
description?: string
}[]
}
@ -71,14 +73,14 @@ export default class SettingsBar extends React.PureComponent<Props, State> {
},
{
key: 'copy-file',
label: 'Copy File',
label: 'Copy File Shortcut',
onChange: this.createOnToggleChecked(configKeys.copyFileButton, this.props.setCopyFile),
getValue: () => this.props.copyFileButton,
wikiLink: wikiLinks.copyFileButton,
},
{
key: 'copy-snippet',
label: 'Copy Snippet',
label: 'Copy Snippet Shortcut',
onChange: this.createOnToggleChecked(
configKeys.copySnippetButton,
this.props.setCopySnippet,
@ -86,6 +88,18 @@ export default class SettingsBar extends React.PureComponent<Props, State> {
getValue: () => this.props.copySnippetButton,
wikiLink: wikiLinks.copySnippet,
},
{
key: 'intelligent-toggle',
label: 'Intelligent Toggle',
onChange: async (e: React.FormEvent<HTMLInputElement>) => {
const { checked } = e.currentTarget
const intelligentToggle = checked ? null : true
await configHelper.setOne(configKeys.intelligentToggle, intelligentToggle)
this.props.setIntelligentToggle(intelligentToggle)
},
getValue: () => this.props.intelligentToggle === null,
description: `Gitako will open/close automatically according to page content when this is enabled.`,
},
],
}
@ -300,9 +314,17 @@ export default class SettingsBar extends React.PureComponent<Props, State> {
checked={option.getValue()}
/>
&nbsp;{option.label}&nbsp;
<a href={option.wikiLink} target={'_blank'}>
(?)
</a>
{option.wikiLink ? (
<a href={option.wikiLink} target={'_blank'}>
(?)
</a>
) : (
option.description && (
<span className={'description'} title={option.description}>
(?)
</span>
)
)}
</label>
<br />
</React.Fragment>

View file

@ -1,14 +1,14 @@
import * as React from 'react'
import { SideBar as SideBarCore } from 'driver/core'
import connect from 'driver/connect'
import FileExplorer from 'components/FileExplorer'
import ToggleShowButton from 'components/ToggleShowButton'
import MetaBar from 'components/MetaBar'
import SettingsBar from 'components/SettingsBar'
import Portal from 'components/Portal'
import Resizable from 'components/Resizable'
import cx from 'utils/cx'
import SettingsBar from 'components/SettingsBar'
import ToggleShowButton from 'components/ToggleShowButton'
import connect from 'driver/connect'
import { SideBar as SideBarCore } from 'driver/core'
import { ConnectorState } from 'driver/core/SideBar'
import * as React from 'react'
import cx from 'utils/cx'
export type Props = {}
@ -100,6 +100,7 @@ class Gitako extends React.PureComponent<Props & ConnectorState> {
compressSingletonFolder,
copyFileButton,
copySnippetButton,
intelligentToggle,
toggleShowSideBarShortcut,
logoContainerElement,
toggleShowSideBar,
@ -109,6 +110,7 @@ class Gitako extends React.PureComponent<Props & ConnectorState> {
setCompressSingleton,
setCopyFile,
setCopySnippet,
setIntelligentToggle,
} = this.props
return (
<div className={'gitako-side-bar'}>
@ -132,9 +134,11 @@ class Gitako extends React.PureComponent<Props & ConnectorState> {
compressSingletonFolder={compressSingletonFolder}
copyFileButton={copyFileButton}
copySnippetButton={copySnippetButton}
intelligentToggle={intelligentToggle}
setCompressSingleton={setCompressSingleton}
setCopyFile={setCopyFile}
setCopySnippet={setCopySnippet}
setIntelligentToggle={setIntelligentToggle}
/>
</div>
</Resizable>

View file

@ -306,6 +306,10 @@
input[type='text'].form-control {
box-shadow: none;
width: 100%;
&.error {
border-color: #d73a49;
}
}
}
@ -352,30 +356,32 @@
transform: rotate(90deg);
}
.node-item-name {
padding-left: 6px;
vertical-align: middle;
}
.node-item-label {
overflow: hidden;
text-overflow: ellipsis;
.node-item-name {
padding-left: 6px;
vertical-align: middle;
}
}
.node-item:hover .node-item-name {
text-decoration: underline;
}
}
}
}
.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;
.go-to-button {
display: none;
white-space: nowrap;
background: transparent;
border: none;
padding: 0;
}
&:hover {
.go-to-button {
display: inline-block;
}
}
}
}
}
}
@ -409,6 +415,10 @@
}
&-section {
padding-bottom: 16px;
.description:hover {
cursor: help;
}
}
/* inputs for access token & shortcut were too wide */

View file

@ -1,9 +1,6 @@
import * as React from 'react'
import { raiseError } from 'analytics'
export type Method<Args = any[]> = (
...args: Args extends any[] ? Args : any[]
) => void | Promise<void>
export type Method<Args extends any[] = any[]> = (...args: Args) => void | Promise<void>
export type Middleware = <M extends Method, MM extends Method>(
method: M,
args: Parameters<M>,
@ -35,9 +32,9 @@ function run<M extends Method>([method, args]: [M, Parameters<M>]) {
export type DispatchState<Props, State> = React.Component<Props, State>['setState']
export type GetState<State> = () => State
export type TriggerOtherMethod<Props, State> = <Args, MC extends MethodCreator<Props, State, Args>>(
methodCreator: MC,
...args: Parameters<ReturnType<MC>>
export type TriggerOtherMethod<Props, State> = <Args extends any[]>(
methodCreator: MethodCreator<Props, State, Args>,
...args: Parameters<ReturnType<MethodCreator<Props, State, Args>>>
) => void
export type Dispatch<Props, State> = {
@ -46,7 +43,7 @@ export type Dispatch<Props, State> = {
call: TriggerOtherMethod<Props, State>
}
export type MethodCreator<Props, State, Args = []> = (
export type MethodCreator<Props, State, Args extends any[] = []> = (
dispatch: Dispatch<Props, State>,
) => Method<Args>

View file

@ -1,25 +1,25 @@
import { raiseError } from 'analytics'
import { Props } from 'components/FileExplorer'
import { GetCreatedMethod, MethodCreator } from 'driver/connect'
import * as ini from 'ini'
import { Base64 } from 'js-base64'
import DOMHelper from 'utils/DOMHelper'
import { findNode, searchKeyToRegexps } from 'utils/general'
import GitHubHelper, { BlobData } from 'utils/GitHubHelper'
import treeParser from 'utils/treeParser'
import URLHelper from 'utils/URLHelper'
import VisibleNodesGenerator, { TreeNode, VisibleNodes } from 'utils/VisibleNodesGenerator'
import GitHubHelper, { BlobData } from 'utils/GitHubHelper'
import { findNode } from 'utils/general'
import { MethodCreator, GetCreatedMethod } from 'driver/connect'
import { Props } from 'components/FileExplorer'
import { raiseError } from 'analytics'
export type ConnectorState = {
stateText: string
visibleNodes: VisibleNodes | null
searchKey: string
searched: boolean
searched: boolean // derived state from searchKey, = !!searchKey
init: GetCreatedMethod<typeof init>
execAfterRender: GetCreatedMethod<typeof execAfterRender>
handleKeyDown: GetCreatedMethod<typeof handleKeyDown>
handleSearchKeyChange: GetCreatedMethod<typeof handleSearchKeyChange>
search: GetCreatedMethod<typeof search>
onNodeClick: GetCreatedMethod<typeof onNodeClick>
onFocusSearchBar: GetCreatedMethod<typeof onFocusSearchBar>
setUpTree: GetCreatedMethod<typeof setUpTree>
@ -48,7 +48,7 @@ type Task = () => void
const tasksAfterRender: (Task)[] = []
let visibleNodesGenerator: VisibleNodesGenerator
type BoundMethodCreator<Args = []> = MethodCreator<Props, ConnectorState, Args>
type BoundMethodCreator<Args extends any[] = []> = MethodCreator<Props, ConnectorState, Args>
const init: BoundMethodCreator = dispatch => () =>
dispatch.call(setStateText, 'Fetching File List...')
@ -118,7 +118,7 @@ function handleParsed(root: TreeNode, parsed: Parsed) {
node.accessDenied = true
}
} else {
raiseError(Error(`Sub-module node ${path} not found`))
raiseError(new Error(`Sub-module node not found`), { path })
}
} else {
handleParsed(root, value as Parsed)
@ -150,7 +150,7 @@ const setUpTree: BoundMethodCreator<
compress: compressSingletonFolder,
})
await visibleNodesGenerator.init()
visibleNodesGenerator.init()
tasksAfterRender.push(DOMHelper.focusSearchInput)
dispatch.call(setStateText, '')
@ -278,28 +278,15 @@ const handleKeyDown: BoundMethodCreator<[React.KeyboardEvent]> = dispatch => eve
const onFocusSearchBar: BoundMethodCreator = dispatch => () => dispatch.call(focusNode, null, false)
const handleSearchKeyChange: BoundMethodCreator<
[React.FormEvent<HTMLInputElement>]
> = dispatch => async event => {
const searchKey = event.currentTarget.value
await dispatch.call(search, searchKey)
}
const search: BoundMethodCreator<[string]> = dispatch => {
let i = 0
return async searchKey => {
dispatch.set({ searchKey })
const j = (i += 1)
await visibleNodesGenerator.search(searchKey)
if (i === j) {
dispatch.set(({ searched }) => ({ searched: !(searched && searchKey === '') }))
dispatch.call(updateVisibleNodes)
}
}
const search: BoundMethodCreator<[string]> = dispatch => searchKey => {
dispatch.set({ searchKey, searched: searchKey !== '' })
const regexps = searchKeyToRegexps(searchKey)
visibleNodesGenerator.search(regexps)
dispatch.call(updateVisibleNodes)
}
const goTo: BoundMethodCreator<[string[]]> = dispatch => async currentPath => {
await visibleNodesGenerator.search('')
visibleNodesGenerator.search([])
tasksAfterRender.push(() => {
const nodeExpandedTo = visibleNodesGenerator.expandTo(currentPath.join('/'))
if (nodeExpandedTo) {
@ -360,7 +347,6 @@ export default {
handleKeyDown,
onFocusSearchBar,
search,
handleSearchKeyChange,
setExpand,
goTo,
toggleNodeExpansion,

View file

@ -1,22 +1,20 @@
import { Props } from 'components/SideBar'
import { GetCreatedMethod, MethodCreator } from 'driver/connect'
import configHelper, { Config, configKeys } from 'utils/configHelper'
import DOMHelper from 'utils/DOMHelper'
import GitHubHelper, {
NOT_FOUND,
BAD_CREDENTIALS,
API_RATE_LIMIT,
BAD_CREDENTIALS,
BLOCKED_PROJECT,
EMPTY_PROJECT,
MetaData,
NOT_FOUND,
TreeData,
BLOCKED_PROJECT,
} from 'utils/GitHubHelper'
import configHelper from 'utils/configHelper'
import URLHelper from 'utils/URLHelper'
import keyHelper from 'utils/keyHelper'
import { MethodCreator, GetCreatedMethod } from 'driver/connect'
import { Props } from 'components/SideBar'
import URLHelper from 'utils/URLHelper'
export type ConnectorState = {
// initial width of side bar
baseSize: number
// error message
error?: string
// whether Gitako side bar should be shown
@ -25,22 +23,14 @@ export type ConnectorState = {
showSettings: boolean
// whether failed loading the repo due to it is private
errorDueToAuth: boolean
// access token for GitHub
accessToken?: string
// the shortcut string for toggle sidebar
toggleShowSideBarShortcut?: string
// meta data for the repository
metaData?: MetaData
// file tree data
treeData?: TreeData
// few settings
compressSingletonFolder: boolean
copyFileButton: boolean
copySnippetButton: boolean
logoContainerElement: Element | null
disabled: boolean
initializingPromise: Promise<void> | null
} & {
init: GetCreatedMethod<typeof init>
onPJAXEnd: GetCreatedMethod<typeof onPJAXEnd>
onKeyDown: GetCreatedMethod<typeof onKeyDown>
@ -52,9 +42,17 @@ export type ConnectorState = {
setCopyFile: GetCreatedMethod<typeof setCopyFile>
setCopySnippet: GetCreatedMethod<typeof setCopySnippet>
setCompressSingleton: GetCreatedMethod<typeof setCompressSingleton>
}
setIntelligentToggle: GetCreatedMethod<typeof setIntelligentToggle>
} & {
baseSize: number
toggleShowSideBarShortcut?: string
accessToken?: string
} & Pick<
Config,
'compressSingletonFolder' | 'copyFileButton' | 'copySnippetButton' | 'intelligentToggle'
>
type BoundMethodCreator<Args = []> = MethodCreator<Props, ConnectorState, Args>
type BoundMethodCreator<Args extends any[] = []> = MethodCreator<Props, ConnectorState, Args>
const init: BoundMethodCreator = dispatch => async () => {
const { initializingPromise } = dispatch.get()
@ -91,6 +89,7 @@ const init: BoundMethodCreator = dispatch => async () => {
compressSingletonFolder,
copyFileButton,
copySnippetButton,
intelligentToggle,
} = await configHelper.getAll()
DOMHelper.decorateGitHubPageContent({ copyFileButton, copySnippetButton })
dispatch.set({
@ -100,6 +99,7 @@ const init: BoundMethodCreator = dispatch => async () => {
compressSingletonFolder,
copyFileButton,
copySnippetButton,
intelligentToggle,
})
if (!metaData.branchName || !metaData.userName) return
@ -148,7 +148,8 @@ const init: BoundMethodCreator = dispatch => async () => {
.catch(err => dispatch.call(handleError, err))
Object.assign(metaData, { api: metaDataFromAPI })
dispatch.call(setMetaData, metaData)
const shouldShow = URLHelper.isInCodePage(metaData)
const shouldShow =
intelligentToggle === null ? URLHelper.isInCodePage(metaData) : intelligentToggle
dispatch.call(setShouldShow, shouldShow)
DOMHelper.markGitakoReadyState()
} catch (err) {
@ -179,12 +180,15 @@ const handleError: BoundMethodCreator<[Error]> = dispatch => async err => {
}
const onPJAXEnd: BoundMethodCreator = dispatch => () => {
const { metaData, copyFileButton, copySnippetButton } = dispatch.get()
const { metaData, copyFileButton, copySnippetButton, intelligentToggle } = dispatch.get()
DOMHelper.unmountTopProgressBar()
DOMHelper.decorateGitHubPageContent({ copyFileButton, copySnippetButton })
const mergedMetaData = { ...metaData, ...URLHelper.parse() }
dispatch.call(setShouldShow, URLHelper.isInCodePage(mergedMetaData))
dispatch.call(setMetaData, mergedMetaData)
if (intelligentToggle === null) {
dispatch.call(setShouldShow, URLHelper.isInCodePage(mergedMetaData))
}
}
const onKeyDown: BoundMethodCreator<[KeyboardEvent]> = dispatch => e => {
@ -197,8 +201,15 @@ const onKeyDown: BoundMethodCreator<[KeyboardEvent]> = dispatch => e => {
}
}
const toggleShowSideBar: BoundMethodCreator = dispatch => () =>
dispatch.call(setShouldShow, !dispatch.get().shouldShow)
const toggleShowSideBar: BoundMethodCreator = dispatch => () => {
const { intelligentToggle } = dispatch.get()
const shouldShow = !dispatch.get().shouldShow
dispatch.call(setShouldShow, shouldShow)
if (intelligentToggle !== null) {
dispatch.call(setIntelligentToggle, shouldShow)
}
}
const setShouldShow: BoundMethodCreator<
[ConnectorState['shouldShow']]
@ -250,9 +261,16 @@ const setCopySnippet: BoundMethodCreator<
[ConnectorState['copySnippetButton']]
> = dispatch => copySnippetButton => dispatch.set({ copySnippetButton })
const setIntelligentToggle: BoundMethodCreator<
[ConnectorState['intelligentToggle']]
> = dispatch => intelligentToggle => {
configHelper.setOne(configKeys.intelligentToggle, intelligentToggle)
dispatch.set({ intelligentToggle })
}
const useListeners: BoundMethodCreator<[boolean]> = dispatch => {
const $onPJAXEnd = dispatch.call.bind(dispatch, onPJAXEnd)
const $onKeyDown = dispatch.call.bind(dispatch, onKeyDown)
const $onPJAXEnd = () => dispatch.call(onPJAXEnd)
const $onKeyDown = (e: KeyboardEvent) => dispatch.call(onKeyDown, e)
return on => {
const { disabled } = dispatch.get()
if (on && !disabled) {
@ -279,6 +297,7 @@ export default {
setCompressSingleton,
setCopyFile,
setCopySnippet,
setIntelligentToggle,
setError,
handleError,
useListeners,

5
src/global.d.ts vendored
View file

@ -4,8 +4,3 @@ declare module '*.svg?svgr' {
const component: SvgrComponent
export default component
}
type Omit<T, K extends keyof T> = Pick<
T,
({ [P in keyof T]: P } & { [P in K]: never } & { [x: string]: never })[keyof T]
>

View file

@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Gitako - Github file tree",
"version": "0.5.8",
"version": "0.5.13",
"description": "The missing part of GitHub.",
"author": "EnixCoda",
"icons": {

View file

@ -2,9 +2,9 @@
* this helper helps manipulating DOM
*/
import * as PJAX from 'pjax'
import * as NProgress from 'nprogress'
import { raiseError } from 'analytics'
import * as NProgress from 'nprogress'
import * as PJAX from 'pjax'
NProgress.configure({ showSpinner: false })
@ -48,7 +48,7 @@ function $<EE extends Element, E extends (element: EE) => any, O extends () => a
}
function isInCodePage() {
const branchListSelector = '.branch-select-menu'
const branchListSelector = '#branch-select-menu.branch-select-menu'
return Boolean($(branchListSelector))
}
@ -69,7 +69,7 @@ function getCurrentBranch() {
}
const defaultTitle = 'Switch branches or tags'
const title = branchButtonElement.title.trim()
if (title !== defaultTitle) return title
if (title !== defaultTitle && !title.includes(' ')) return title
}
const findFileButtonSelector =
@ -83,7 +83,7 @@ function getCurrentBranch() {
const result = urlFromFindFileButton.match(commitPathRegex)
if (result) {
const [_, userName, repoName, branchName] = result
return branchName
if (!branchName.includes(' ')) return branchName
}
}

View file

@ -16,7 +16,7 @@ function isEmptyProject(content: any /* examined any */) {
}
function isBlockedProject(content: any /* examined any */) {
return content && content['message'] === "Repository access blocked"
return content && content['message'] === 'Repository access blocked'
}
type Options = {
@ -45,7 +45,7 @@ async function request(url: string, { accessToken }: Options = {}) {
if (isEmptyProject(content)) throw new Error(EMPTY_PROJECT)
if (isBlockedProject(content)) throw new Error(BLOCKED_PROJECT)
// Unknown type of error, report it!
raiseError(new Error(`Got ${res.statusText} when requesting ${url}`))
raiseError(new Error(res.statusText))
throw new Error(content && content.message)
}
}

View file

@ -1,13 +1,13 @@
import storageHelper from 'utils/storageHelper'
import { pick } from 'utils/general'
type Config = {
export type Config = {
sideBarWidth: number
shortcut: string | undefined
access_token: string | undefined
compressSingletonFolder: boolean
copyFileButton: boolean
copySnippetButton: boolean
intelligentToggle: boolean | null // `null` stands for intelligent, boolean for sidebar open status
}
export enum configKeys {
@ -17,6 +17,7 @@ export enum configKeys {
compressSingletonFolder = 'compressSingletonFolder',
copyFileButton = 'copyFileButton',
copySnippetButton = 'copySnippetButton',
intelligentToggle = 'intelligentToggle',
}
const defaultConfigs: Config = {
@ -26,6 +27,7 @@ const defaultConfigs: Config = {
compressSingletonFolder: true,
copyFileButton: true,
copySnippetButton: true,
intelligentToggle: null,
}
const configKeyArray = Object.values(configKeys)
@ -34,11 +36,7 @@ function applyDefaultConfigs(configs: Config) {
return configKeyArray.reduce(
(applied, configKey) => {
const key = configKey as keyof Config
if (!(key in configs)) {
applied[key] = defaultConfigs[key]
} else {
applied[key] = configs[key]
}
Object.assign(applied, { [key]: key in configs ? configs[key] : defaultConfigs[key] })
return applied
},
{} as Config,
@ -49,16 +47,16 @@ async function getAll(): Promise<Config> {
return applyDefaultConfigs(await storageHelper.get(configKeyArray))
}
async function getOne(key: keyof Config) {
async function getOne(key: configKeys) {
return (await getAll())[key]
}
async function set(partialConfig: Partial<Config>) {
return await storageHelper.set(pick(partialConfig, configKeyArray))
async function setAll(partialConfig: Partial<Config>) {
return await storageHelper.set(partialConfig)
}
async function setOne(key: configKeys, value: any) {
return await set({
return await setAll({
[key]: value,
})
}
@ -66,6 +64,6 @@ async function setOne(key: configKeys, value: any) {
export default {
getAll,
getOne,
set,
setAll,
setOne,
}

View file

@ -9,9 +9,7 @@ export default function cx(...classNames: any[]): string {
case 'string':
return className
case 'object':
return cx(
...Object.entries(className).map(([key, value]) => (Boolean(value) ? key : null)),
)
return cx(...Object.entries(className).map(([key, value]) => (value ? key : null)))
default:
return ''
}

View file

@ -128,3 +128,14 @@ export async function JSONRequest(url: string, data: any, method = 'post') {
body: JSON.stringify(data),
})).json()
}
export function searchKeyToRegexps(searchKey: string) {
if (!searchKey) return []
try {
// case-sensitive when searchKey contains uppercase char
return [new RegExp(searchKey, /[A-Z]/i.test(searchKey) ? '' : 'i')]
} catch (err) {
return [/$^/] // matching nothing if failed transforming regexp
}
}

View file

@ -38,15 +38,24 @@ function parseKeyCode(code: string) {
function parseEvent(e: KeyboardEvent | React.KeyboardEvent) {
const { altKey: alt, shiftKey: shift, metaKey: meta, ctrlKey: ctrl } = e
const code = parseKeyCode(e.key)
const keys = { meta, ctrl, shift, alt, [code]: true }
const combination = parse(
Object.entries(keys)
.filter(([key, pressed]) => pressed)
.map(([key, pressed]) => key)
.join('+')
)
return combination
try {
const code = parseKeyCode(e.key)
const keys = { meta, ctrl, shift, alt, [code]: true }
const combination = parse(
Object.entries(keys)
.filter(([key, pressed]) => pressed)
.map(([key, pressed]) => key)
.join('+'),
)
return combination
} catch (err) {
const serializedKeyData = JSON.stringify({
keyCode: e.keyCode,
key: e.key,
charCode: e.charCode,
})
throw new Error(`Error parse keyboard event: ${serializedKeyData}`)
}
}
export default {

View file

@ -1,11 +1,10 @@
const localStorage = chrome.storage.local
function get(mapping: string | string[] | object): Promise<any> {
function get(mapping: string[] | null): Promise<any> {
return new Promise(resolve => localStorage.get(mapping, resolve))
}
function set(value: any): Promise<void> {
// it's ok
return new Promise(resolve => localStorage.set(value, resolve))
}

View file

@ -31,41 +31,15 @@ export type TreeNode = {
accessDenied?: boolean
}
function getFilterFunc(keyRegex: RegExp) {
return function filterFunc({ name }: TreeNode) {
return keyRegex.test(name)
}
}
function filterDuplications<T>(arr: T[]) {
return Array.from(new Set(arr))
}
function search(treeNodes: TreeNode[], searchKey: string): TreeNode[] {
if (!searchKey) return treeNodes
/**
* if searchKey is 'abcd'
* then keyRegex will be /abcd/i and /a.*?b.*?c.*?d/i
*/
const regexpGenerators: ((raw: string) => RegExp)[] = [
raw => new RegExp(raw, 'i'),
raw => new RegExp(raw.split('').join('.*?'), 'i'),
]
const keyRegexes: RegExp[] = []
for (const generator of regexpGenerators) {
try {
const regExp = generator(searchKey)
if (keyRegexes.find(keyRegex => keyRegex.source === regExp.source)) continue
// prevent duplicated regExp
keyRegexes.push(regExp)
} catch (err) {
// ignore invalid regexp
}
}
function search(treeNodes: TreeNode[], regexps: RegExp[]): TreeNode[] {
if (!regexps.length) return treeNodes
const searchResults = ([] as TreeNode[]).concat(
...keyRegexes.map(keyRegex => treeNodes.filter(getFilterFunc(keyRegex))),
...regexps.map(keyRegex => treeNodes.filter(({ name }) => keyRegex.test(name))),
)
return filterDuplications(searchResults)
}
@ -121,10 +95,10 @@ class L2 {
this.searchedNodes = null
}
search = async (searchKey: string) => {
this.compressed = !searchKey
this.searchedNodes = searchKey
? search(this.l1.nodes, searchKey)
search = (regexps: RegExp[]) => {
this.compressed = !regexps.length
this.searchedNodes = regexps.length
? search(this.l1.nodes, regexps)
: this.getRoot().contents || []
}
@ -246,8 +220,8 @@ export default class VisibleNodesGenerator {
this.l3 = new L3(this.l1, this.l2)
this.l4 = new L4(this.l1, this.l2, this.l3)
this.search = async (...args) => {
const r = await this.l2.search(...args)
this.search = (...args) => {
const r = this.l2.search(...args)
this.l3.generateVisibleNodes()
this.l4.focusNode(null)
return r
@ -258,8 +232,8 @@ export default class VisibleNodesGenerator {
this.focusNode = (...args) => this.l4.focusNode(...args)
}
async init() {
await this.l2.search('')
init() {
this.l2.search([])
this.l3.generateVisibleNodes()
}

624
yarn.lock

File diff suppressed because it is too large Load diff