diff --git a/src/analytics.ts b/src/analytics.ts index 5cde36d..da4522b 100644 --- a/src/analytics.ts +++ b/src/analytics.ts @@ -1,4 +1,5 @@ import { version } from '../package.json' +import { Middleware } from 'driver/connect.js' // TODO: set this through ENV or something else const LOG_ENDPOINT = 'https://enix.one/gitako/log' @@ -6,7 +7,7 @@ export function raiseError(error: Error) { return reportError(error) } -export function withErrorLog(method: Function, args: any[]) { +export const withErrorLog: Middleware = function withErrorLog(method, args) { return [ function() { try { diff --git a/src/components/Gitako.tsx b/src/components/Gitako.tsx index 8d5b658..ad37b77 100644 --- a/src/components/Gitako.tsx +++ b/src/components/Gitako.tsx @@ -8,6 +8,6 @@ export default class Gitako extends React.PureComponent { } render() { - return React.createElement(SideBar as any) + return } } diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index 0cba7b4..ff6e0c2 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -9,51 +9,16 @@ import Portal from 'components/Portal' import Resizable from 'components/Resizable' import cx from 'utils/cx' import VisibleNodesGenerator from 'utils/VisibleNodesGenerator' +import { ConnectorState } from 'driver/core/SideBar' export type TreeData = any export type MetaData = any export type VisibleNodes = VisibleNodesGenerator['visibleNodes'] -type Props = { - // 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 +export type Props = {} - init: () => void - onPJAXEnd: () => void - toggleShowSideBar: () => void - toggleShowSettings: () => void - onAccessTokenChange: SettingsBar['props']['onAccessTokenChange'] - onKeyDown: EventListener - onShortcutChange: SettingsBar['props']['onShortcutChange'] - setCopyFile: SettingsBar['props']['setCopyFile'] - setCopySnippet: SettingsBar['props']['setCopySnippet'] - setCompressSingleton: SettingsBar['props']['setCompressSingleton'] -} - -@(connect(SideBarCore) as any) -export default class Gitako extends React.PureComponent { - static defaultProps: Partial = { +class Gitako extends React.PureComponent { + static defaultProps: Partial = { baseSize: 260, shouldShow: false, showSettings: false, @@ -105,7 +70,15 @@ export default class Gitako extends React.PureComponent { } renderContent() { - const { errorDueToAuth, metaData, treeData, showSettings, toggleShowSettings } = this.props + const { + errorDueToAuth, + metaData, + treeData, + showSettings, + toggleShowSettings, + compressSingletonFolder, + accessToken, + } = this.props return (
{metaData && } @@ -113,10 +86,12 @@ export default class Gitako extends React.PureComponent { ? this.renderAccessDeniedError() : metaData && ( )}
@@ -175,3 +150,5 @@ export default class Gitako extends React.PureComponent { ) } } + +export default connect(SideBarCore)(Gitako) diff --git a/src/driver/core/SideBar.js b/src/driver/core/SideBar.js deleted file mode 100644 index 6f49333..0000000 --- a/src/driver/core/SideBar.js +++ /dev/null @@ -1,172 +0,0 @@ -import DOMHelper, { REPO_TYPE_PRIVATE } from 'utils/DOMHelper' -import GitHubHelper, { - NOT_FOUND, - BAD_CREDENTIALS, - API_RATE_LIMIT, - EMPTY_PROJECT, -} from 'utils/GitHubHelper' -import configHelper from 'utils/configHelper' -import URLHelper from 'utils/URLHelper' -import keyHelper from 'utils/keyHelper' - -const init = dispatch => async () => { - try { - if (!URLHelper.isInRepoPage()) return - dispatch({ logoContainerElement: DOMHelper.insertLogoMountPoint() }) - let detectedBranchName - const metaData = URLHelper.parse() - if (DOMHelper.isInCodePage()) { - detectedBranchName = DOMHelper.getCurrentBranch() // not working well with non-branch blob - || URLHelper.parseSHA() // cannot handle '/' split branch name, should not use when possibly on branch page - } - metaData.branchName = detectedBranchName || 'master' - dispatch(setMetaData, metaData) - const { - access_token: accessToken, - shortcut, - compressSingletonFolder, - copyFileButton, - copySnippetButton, - } = await configHelper.get() - DOMHelper.decorateGitHubPageContent({ copyFileButton, copySnippetButton }) - dispatch({ - accessToken, - toggleShowSideBarShortcut: shortcut, - compressSingletonFolder, - copyFileButton, - copySnippetButton, - }) - - const getTreeDataAggressively = GitHubHelper.getTreeData({ - ...metaData, - 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 repo'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({ - ...metaData, - accessToken, - }) - } else { - caughtAggressiveError.then(error => { - // aggressive requested correct branch but ends in failure (e.g. project is empty) - if (error instanceof Error) { - dispatch(handleError, error) - } - }) - } - getTreeData - .then(treeData => { - if (treeData) { - // in an unknown rare case this NOT happen - dispatch({ treeData }) - } - }) - .catch(err => dispatch(handleError, err)) - Object.assign(metaData, { api: metaDataFromAPI }) - dispatch(setMetaData, metaData) - const shouldShow = URLHelper.isInCodePage(metaData) - dispatch(setShouldShow, shouldShow) - } catch (err) { - dispatch(handleError, err) - } -} - -const handleError = dispatch => async err => { - if (err.message === EMPTY_PROJECT) { - dispatch(setError, 'This project seems to be empty.') - } else if ( - err.message === NOT_FOUND || - err.message === BAD_CREDENTIALS || - err.message === API_RATE_LIMIT - ) { - dispatch({ errorDueToAuth: true }) - dispatch(setShowSettings, true) - dispatch(setShouldShow, true) - } else { - dispatch(setError, 'Gitako ate a bug, but it should recovery soon!') - } -} - -const onPJAXEnd = dispatch => () => { - dispatch(({ metaData, copyFileButton, copySnippetButton }) => { - DOMHelper.unmountTopProgressBar() - DOMHelper.decorateGitHubPageContent({ copyFileButton, copySnippetButton }) - const mergedMetaData = { ...metaData, ...URLHelper.parse() } - dispatch(setShouldShow, URLHelper.isInCodePage(mergedMetaData)) - dispatch(setMetaData, mergedMetaData) - }) -} - -const onKeyDown = dispatch => e => { - dispatch(({ toggleShowSideBarShortcut }) => { - if (toggleShowSideBarShortcut) { - const keys = keyHelper.parseEvent(e) - if (keys === toggleShowSideBarShortcut) { - dispatch(toggleShowSideBar) - } - } - }) -} - -const toggleShowSideBar = dispatch => () => - dispatch(({ shouldShow }) => dispatch(setShouldShow, !shouldShow)) - -const setShouldShow = dispatch => shouldShow => { - dispatch({ shouldShow }, shouldShow ? DOMHelper.focusFileExplorer : null) - DOMHelper.setBodyIndent(shouldShow) -} - -const setError = dispatch => error => { - dispatch({ error }) - dispatch(setShouldShow, false) -} - -const toggleShowSettings = dispatch => () => - dispatch(({ showSettings }) => ({ showSettings: !showSettings })) - -const setShowSettings = dispatch => showSettings => dispatch({ showSettings }) - -const onAccessTokenChange = dispatch => accessToken => dispatch({ accessToken }) - -const onShortcutChange = dispatch => shortcut => dispatch({ toggleShowSideBarShortcut: shortcut }) - -const setMetaData = dispatch => metaData => dispatch({ metaData }) - -const setCompressSingleton = dispatch => compressSingletonFolder => - dispatch({ compressSingletonFolder }) - -const setCopyFile = dispatch => copyFileButton => dispatch({ copyFileButton }) - -const setCopySnippet = dispatch => copySnippetButton => dispatch({ copySnippetButton }) - -export default { - init, - onPJAXEnd, - onKeyDown, - setShouldShow, - setShowSettings, - toggleShowSideBar, - toggleShowSettings, - onAccessTokenChange, - onShortcutChange, - setMetaData, - setCompressSingleton, - setCopyFile, - setCopySnippet, - setError, - handleError, -} diff --git a/src/driver/core/SideBar.ts b/src/driver/core/SideBar.ts new file mode 100644 index 0000000..558a7cf --- /dev/null +++ b/src/driver/core/SideBar.ts @@ -0,0 +1,226 @@ +import DOMHelper, { REPO_TYPE_PRIVATE } from 'utils/DOMHelper' +import GitHubHelper, { + NOT_FOUND, + BAD_CREDENTIALS, + API_RATE_LIMIT, + EMPTY_PROJECT, + MetaData, +} from 'utils/GitHubHelper' +import configHelper from 'utils/configHelper' +import URLHelper from 'utils/URLHelper' +import keyHelper from 'utils/keyHelper' +import { MethodCreator, DispatchState, TriggerOtherMethod } from 'driver/connect' +import { Props, TreeData } 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: EventListener + onShortcutChange: SettingsBar['props']['onShortcutChange'] + setCopyFile: SettingsBar['props']['setCopyFile'] + setCopySnippet: SettingsBar['props']['setCopySnippet'] + setCompressSingleton: SettingsBar['props']['setCompressSingleton'] +} + +const init: MethodCreator = dispatch => async () => { + try { + if (!URLHelper.isInRepoPage()) return + dispatch.state({ + 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.for(setMetaData, metaData) + const { + access_token: accessToken, + shortcut, + compressSingletonFolder, + copyFileButton, + copySnippetButton, + } = await configHelper.get() + DOMHelper.decorateGitHubPageContent({ copyFileButton, copySnippetButton }) + dispatch.state({ + 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.for(handleError, error) + } + }) + } + getTreeData + .then(treeData => { + if (treeData) { + // in an unknown rare case this NOT happen + dispatch.state({ treeData }) + } + }) + .catch(err => dispatch.for(handleError, err)) + Object.assign(metaData, { api: metaDataFromAPI }) + dispatch.for(setMetaData, metaData) + const shouldShow = URLHelper.isInCodePage(metaData) + dispatch.for(setShouldShow, shouldShow) + } catch (err) { + dispatch.for(handleError, err) + } +} + +const handleError: MethodCreator = dispatch => async err => { + if (err.message === EMPTY_PROJECT) { + dispatch.for(setError, 'This project seems to be empty.') + } else if ( + err.message === NOT_FOUND || + err.message === BAD_CREDENTIALS || + err.message === API_RATE_LIMIT + ) { + dispatch.state({ errorDueToAuth: true }) + dispatch.for(setShowSettings, true) + dispatch.for(setShouldShow, true) + } else { + dispatch.for(setError, 'Gitako ate a bug, but it should recovery soon!') + } +} + +const onPJAXEnd: MethodCreator = dispatch => () => { + dispatch.prepare(({ metaData, copyFileButton, copySnippetButton }) => { + DOMHelper.unmountTopProgressBar() + DOMHelper.decorateGitHubPageContent({ copyFileButton, copySnippetButton }) + const mergedMetaData = { ...metaData, ...URLHelper.parse() } + dispatch.for(setShouldShow, URLHelper.isInCodePage(mergedMetaData)) + dispatch.for(setMetaData, mergedMetaData) + }) +} + +const onKeyDown: MethodCreator = dispatch => e => { + dispatch.state(({ toggleShowSideBarShortcut }: { toggleShowSideBarShortcut: string }) => { + if (toggleShowSideBarShortcut) { + const keys = keyHelper.parseEvent(e) + if (keys === toggleShowSideBarShortcut) { + dispatch.for(toggleShowSideBar) + } + } + }) +} + +const toggleShowSideBar: MethodCreator = dispatch => () => + dispatch.state(({ shouldShow }: { shouldShow: boolean }) => + dispatch.for(setShouldShow, !shouldShow) + ) + +const setShouldShow: MethodCreator = dispatch => shouldShow => { + dispatch.state({ shouldShow }, shouldShow ? DOMHelper.focusFileExplorer : null) + DOMHelper.setBodyIndent(shouldShow) +} + +const setError: MethodCreator = dispatch => error => { + dispatch.state({ error }) + dispatch.for(setShouldShow, false) +} + +const toggleShowSettings: MethodCreator = dispatch => () => + dispatch.state(({ showSettings }: { showSettings: boolean }) => ({ + showSettings: !showSettings, + })) + +const setShowSettings: MethodCreator = dispatch => showSettings => dispatch.state({ showSettings }) + +const onAccessTokenChange: MethodCreator = dispatch => accessToken => + dispatch.state({ accessToken }) + +const onShortcutChange: MethodCreator = dispatch => shortcut => + dispatch.state({ toggleShowSideBarShortcut: shortcut }) + +const setMetaData: MethodCreator = dispatch => metaData => dispatch.state({ metaData }) + +const setCompressSingleton: MethodCreator = dispatch => compressSingletonFolder => + dispatch.state({ compressSingletonFolder }) + +const setCopyFile: MethodCreator = dispatch => copyFileButton => dispatch.state({ copyFileButton }) + +const setCopySnippet: MethodCreator = dispatch => copySnippetButton => + dispatch.state({ copySnippetButton }) + +export default { + init, + onPJAXEnd, + onKeyDown, + setShouldShow, + setShowSettings, + toggleShowSideBar, + toggleShowSettings, + onAccessTokenChange, + onShortcutChange, + setMetaData, + setCompressSingleton, + setCopyFile, + setCopySnippet, + setError, + handleError, +} diff --git a/src/utils/GitHubHelper.ts b/src/utils/GitHubHelper.ts index 0aae21c..5d1f970 100644 --- a/src/utils/GitHubHelper.ts +++ b/src/utils/GitHubHelper.ts @@ -38,11 +38,14 @@ async function request(url: string, { accessToken }: Options = {}) { } } +type PageType = 'blob' | 'tree' | string + export type MetaData = { userName?: string repoName?: string branchName?: string accessToken?: string + type?: PageType api?: any } @@ -63,7 +66,7 @@ export type TreeData = { tree: TreeItem[] } -async function getTreeData({ userName, repoName, branchName, accessToken }: TreeData) { +async function getTreeData({ userName, repoName, branchName, accessToken }: MetaData) { const url = `https://api.github.com/repos/${userName}/${repoName}/git/trees/${branchName}?recursive=1` return await request(url, { accessToken }) } diff --git a/src/utils/URLHelper.ts b/src/utils/URLHelper.ts index 32cf4c7..367a25e 100644 --- a/src/utils/URLHelper.ts +++ b/src/utils/URLHelper.ts @@ -1,7 +1,7 @@ import { raiseError } from 'analytics' import { MetaData } from './GitHubHelper' -function parse() { +function parse(): MetaData & { path: string[] } { const { pathname } = window.location let [ , diff --git a/src/utils/treeParser.ts b/src/utils/treeParser.ts index ef2b542..c0af52c 100644 --- a/src/utils/treeParser.ts +++ b/src/utils/treeParser.ts @@ -1,4 +1,4 @@ -import GitHubHelper, { TreeData, MetaData } from 'utils/GitHubHelper' +import GitHubHelper, { MetaData, TreeData } from 'utils/GitHubHelper' interface BasicItem { name: string | null