Gitako/src/driver/core/SideBar.ts
2019-02-21 11:44:26 +08:00

258 lines
8.2 KiB
TypeScript

import DOMHelper, { REPO_TYPE_PRIVATE } from 'utils/DOMHelper'
import GitHubHelper, {
NOT_FOUND,
BAD_CREDENTIALS,
API_RATE_LIMIT,
EMPTY_PROJECT,
MetaData,
TreeData,
} from 'utils/GitHubHelper'
import configHelper from 'utils/configHelper'
import URLHelper from 'utils/URLHelper'
import keyHelper from 'utils/keyHelper'
import { MethodCreator } from 'driver/connect'
import { Props } from 'components/SideBar'
import SettingsBar from 'components/SettingsBar'
export type ConnectorState = {
// initial width of side bar
baseSize?: number
// error message
error?: string
// whether Gitako side bar should be shown
shouldShow?: boolean
// whether show settings pane
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: HTMLElement | null
init: () => void
onPJAXEnd: () => void
toggleShowSideBar: () => void
toggleShowSettings: () => void
onAccessTokenChange: SettingsBar['props']['onAccessTokenChange']
onKeyDown: (e: KeyboardEvent) => string
onShortcutChange: SettingsBar['props']['onShortcutChange']
setCopyFile: SettingsBar['props']['setCopyFile']
setCopySnippet: SettingsBar['props']['setCopySnippet']
setCompressSingleton: SettingsBar['props']['setCompressSingleton']
}
const init: MethodCreator<Props, ConnectorState> = dispatch => async () => {
try {
if (!URLHelper.isInRepoPage()) return
dispatch.set({
logoContainerElement: DOMHelper.insertLogoMountPoint(),
})
let detectedBranchName
const metaData = URLHelper.parse()
if (DOMHelper.isInCodePage()) {
detectedBranchName = DOMHelper.getCurrentBranch() || URLHelper.parseSHA() // not working well with non-branch blob // cannot handle '/' split branch name, should not use when possibly on branch page
}
metaData.branchName = detectedBranchName || 'master'
dispatch.call(setMetaData, metaData)
const {
access_token: accessToken,
shortcut,
compressSingletonFolder,
copyFileButton,
copySnippetButton,
} = await configHelper.get()
DOMHelper.decorateGitHubPageContent({ copyFileButton, copySnippetButton })
dispatch.set({
accessToken,
toggleShowSideBarShortcut: shortcut,
compressSingletonFolder,
copyFileButton,
copySnippetButton,
})
if (!metaData.branchName || !metaData.userName) return
const getTreeDataAggressively = GitHubHelper.getTreeData({
branchName: metaData.branchName,
userName: metaData.userName,
repoName: metaData.repoName,
accessToken,
})
const caughtAggressiveError = getTreeDataAggressively.catch(error => {
// 1. the repo has no master branch
// 2. detect branch name from DOM failed
// 3. not very possible...
// not handle this error immediately
return error
})
let getTreeData = getTreeDataAggressively
const metaDataFromAPI = await GitHubHelper.getRepoMeta({ ...metaData, accessToken })
const projectDefaultBranchName = metaDataFromAPI['default_branch']
if (!detectedBranchName && projectDefaultBranchName !== metaData.branchName) {
// Accessing repository's non-homepage(no branch name in URL, nor in DOM)
// We predicted its default branch to be 'master' and sent aggressive request
// Throw that request due to the repo do not use {defaultBranchName} as default branch
metaData.branchName = projectDefaultBranchName
getTreeData = GitHubHelper.getTreeData({
branchName: metaData.branchName,
userName: metaData.userName,
repoName: metaData.repoName,
accessToken,
})
} else {
caughtAggressiveError.then(error => {
// aggressive requested correct branch but ends in failure (e.g. project is empty)
if (error instanceof Error) {
dispatch.call(handleError, error)
}
})
}
getTreeData
.then(treeData => {
if (treeData) {
// in an unknown rare case this NOT happen
dispatch.set({ treeData })
}
})
.catch(err => dispatch.call(handleError, err))
Object.assign(metaData, { api: metaDataFromAPI })
dispatch.call(setMetaData, metaData)
const shouldShow = URLHelper.isInCodePage(metaData)
dispatch.call(setShouldShow, shouldShow)
DOMHelper.markGitakoReadyState()
} catch (err) {
dispatch.call(handleError, err)
}
}
const handleError: MethodCreator<Props, ConnectorState, [Error]> = dispatch => async err => {
if (err.message === EMPTY_PROJECT) {
dispatch.call(setError, 'This project seems to be empty.')
} else if (
err.message === NOT_FOUND ||
err.message === BAD_CREDENTIALS ||
err.message === API_RATE_LIMIT
) {
dispatch.set({ errorDueToAuth: true })
dispatch.call(setShowSettings, true)
dispatch.call(setShouldShow, true)
} else {
dispatch.call(setError, 'Gitako ate a bug, but it should recovery soon!')
}
}
const onPJAXEnd: MethodCreator<Props, ConnectorState> = dispatch => () => {
dispatch.get(({ metaData, copyFileButton, copySnippetButton }) => {
DOMHelper.unmountTopProgressBar()
DOMHelper.decorateGitHubPageContent({ copyFileButton, copySnippetButton })
const mergedMetaData = { ...metaData, ...URLHelper.parse() }
dispatch.call(setShouldShow, URLHelper.isInCodePage(mergedMetaData))
dispatch.call(setMetaData, mergedMetaData)
})
}
const onKeyDown: MethodCreator<Props, ConnectorState, [KeyboardEvent]> = dispatch => e => {
dispatch.get(({ toggleShowSideBarShortcut }) => {
if (toggleShowSideBarShortcut) {
const keys = keyHelper.parseEvent(e)
if (keys === toggleShowSideBarShortcut) {
dispatch.call(toggleShowSideBar)
}
}
})
}
const toggleShowSideBar: MethodCreator<Props, ConnectorState> = dispatch => () =>
dispatch.get(({ shouldShow }) => dispatch.call(setShouldShow, !shouldShow))
const setShouldShow: MethodCreator<
Props,
ConnectorState,
[ConnectorState['shouldShow']]
> = dispatch => shouldShow => {
dispatch.set({ shouldShow }, shouldShow ? DOMHelper.focusFileExplorer : null)
DOMHelper.setBodyIndent(shouldShow)
}
const setError: MethodCreator<
Props,
ConnectorState,
[ConnectorState['error']]
> = dispatch => error => {
dispatch.set({ error })
dispatch.call(setShouldShow, false)
}
const toggleShowSettings: MethodCreator<Props, ConnectorState> = dispatch => () =>
dispatch.set(({ showSettings }) => ({
showSettings: !showSettings,
}))
const setShowSettings: MethodCreator<
Props,
ConnectorState,
[ConnectorState['showSettings']]
> = dispatch => showSettings => dispatch.set({ showSettings })
const onAccessTokenChange: MethodCreator<
Props,
ConnectorState,
[ConnectorState['accessToken']]
> = dispatch => accessToken => dispatch.set({ accessToken })
const onShortcutChange: MethodCreator<
Props,
ConnectorState,
[ConnectorState['toggleShowSideBarShortcut']]
> = dispatch => shortcut => dispatch.set({ toggleShowSideBarShortcut: shortcut })
const setMetaData: MethodCreator<
Props,
ConnectorState,
[ConnectorState['metaData']]
> = dispatch => metaData => dispatch.set({ metaData })
const setCompressSingleton: MethodCreator<
Props,
ConnectorState,
[ConnectorState['compressSingletonFolder']]
> = dispatch => compressSingletonFolder => dispatch.set({ compressSingletonFolder })
const setCopyFile: MethodCreator<
Props,
ConnectorState,
[ConnectorState['copyFileButton']]
> = dispatch => copyFileButton => dispatch.set({ copyFileButton })
const setCopySnippet: MethodCreator<
Props,
ConnectorState,
[ConnectorState['copySnippetButton']]
> = dispatch => copySnippetButton => dispatch.set({ copySnippetButton })
export default {
init,
onPJAXEnd,
onKeyDown,
setShouldShow,
setShowSettings,
toggleShowSideBar,
toggleShowSettings,
onAccessTokenChange,
onShortcutChange,
setMetaData,
setCompressSingleton,
setCopyFile,
setCopySnippet,
setError,
handleError,
}