-
-
-
- {name}
-
- {renderActions &&
{renderActions(node)}
}
+ onClick(node)
+ },
+ [node, onClick],
+ )
+
+ const { name, path } = node
+ return (
+
+
+
+ )
}
diff --git a/src/components/PJAXLink.tsx b/src/components/PJAXLink.tsx
index bc0e7d4..680214e 100644
--- a/src/components/PJAXLink.tsx
+++ b/src/components/PJAXLink.tsx
@@ -1,12 +1,12 @@
import * as React from 'react'
-import DOMHelper from 'utils/DOMHelper'
+import * as DOMHelper from 'utils/DOMHelper'
type Props = {
to: string
children: React.ReactElement
}
-export default function PJAXLink
({ to, children }: Props
) {
+export function PJAXLink
({ to, children }: Props
) {
return React.cloneElement(children, {
...children.props,
onClick: () => DOMHelper.loadWithPJAX(to),
diff --git a/src/components/Portal.tsx b/src/components/Portal.tsx
index 04ebd72..2497954 100644
--- a/src/components/Portal.tsx
+++ b/src/components/Portal.tsx
@@ -5,12 +5,8 @@ type Props = {
into: Element | null
}
-class Portal extends React.PureComponent {
- render() {
- const { into, children } = this.props
- if (!(into instanceof Element)) return null
- return ReactDOM.createPortal(children, into)
- }
+export function Portal(props: React.PropsWithChildren) {
+ const { into, children } = props
+ if (!(into instanceof Element)) return null
+ return ReactDOM.createPortal(children, into)
}
-
-export default Portal
diff --git a/src/components/Resizable.tsx b/src/components/Resizable.tsx
index 39133fb..8a8fc0d 100644
--- a/src/components/Resizable.tsx
+++ b/src/components/Resizable.tsx
@@ -1,10 +1,10 @@
+import { HorizontalResizeHandler } from 'components/ResizeHandler'
+import { useConfigs } from 'containers/ConfigsContext'
import * as React from 'react'
-import HorizontalResizeHandler from 'components/ResizeHandler'
-import cx from 'utils/cx'
-import { useWindowSize, useMediaStyleSheet } from 'utils/hooks'
+import { cx } from 'utils/cx'
import { bodySpacingClassName } from 'utils/DOMHelper'
-import configHelper, { configKeys } from 'utils/configHelper'
import * as features from 'utils/features'
+import { useMediaStyleSheet, useWindowSize } from 'utils/hooks'
export type Size = number
type Props = {
@@ -15,12 +15,9 @@ type Props = {
const MINIMAL_CONTENT_VIEWPORT_WIDTH = 100
const GITHUB_WIDTH = 1020
-export default function Resizable({
- baseSize,
- className,
- children,
-}: React.PropsWithChildren) {
+export function Resizable({ baseSize, className, children }: React.PropsWithChildren) {
const [size, setSize] = React.useState(baseSize)
+ const configContext = useConfigs()
React.useEffect(() => {
setSize(baseSize)
@@ -36,7 +33,7 @@ export default function Resizable({
React.useEffect(() => {
document.documentElement.style.setProperty('--gitako-width', size + 'px')
- configHelper.setOne(configKeys.sideBarWidth, size)
+ configContext.set({ sideBarWidth: size })
}, [size])
useMediaStyleSheet(
diff --git a/src/components/ResizeHandler.tsx b/src/components/ResizeHandler.tsx
index 9886d13..a3b9fd8 100644
--- a/src/components/ResizeHandler.tsx
+++ b/src/components/ResizeHandler.tsx
@@ -1,4 +1,4 @@
-import Icon from 'components/Icon'
+import { Icon } from 'components/Icon'
import * as React from 'react'
import { Size } from './Resizable'
@@ -8,51 +8,46 @@ type Props = {
style?: React.CSSProperties
}
-export default class HorizontalResizeHandler extends React.PureComponent {
- pointerDown = false
- startX = 0
- baseSize = this.props.size
+export function HorizontalResizeHandler({ onResize, size, style }: Props) {
+ const pointerDown = React.useRef(false)
+ const startX = React.useRef(0)
+ const baseSize = React.useRef(size)
+ const latestPropSize = React.useRef(size)
- componentWillReceiveProps(nextProps: Props) {
- if (!this.pointerDown) {
- // update baseSize when not resizing
- this.baseSize = nextProps.size
+ React.useEffect(() => {
+ latestPropSize.current = size
+ }, [size])
+
+ const onPointerDown = React.useCallback(({ clientX }: React.MouseEvent) => {
+ startX.current = clientX
+ pointerDown.current = true
+ baseSize.current = latestPropSize.current
+ }, [])
+
+ React.useEffect(() => {
+ const onPointerMove = ({ clientX }: MouseEvent) => {
+ if (!pointerDown.current) return
+ const shift = clientX - startX.current
+ onResize(baseSize.current + shift)
}
- }
+ window.addEventListener('mousemove', onPointerMove)
+ return () => window.removeEventListener('mousemove', onPointerMove)
+ }, [onResize])
- subscribeEvents = () => {
- window.addEventListener('mousemove', this.onPointerMove)
- window.addEventListener('mouseup', this.onPointerUp)
- }
+ React.useEffect(() => {
+ const onPointerUp = () => {
+ if (pointerDown.current) {
+ pointerDown.current = false
+ baseSize.current = latestPropSize.current
+ }
+ }
+ window.addEventListener('mouseup', onPointerUp)
+ return () => window.removeEventListener('mouseup', onPointerUp)
+ }, [])
- unsubscribeEvents = () => {
- window.removeEventListener('mousemove', this.onPointerMove)
- window.removeEventListener('mouseup', this.onPointerUp)
- }
-
- onPointerDown = ({ clientX }: React.MouseEvent) => {
- this.startX = clientX
- this.pointerDown = true
- this.subscribeEvents()
- }
-
- onPointerMove = ({ clientX }: MouseEvent) => {
- if (!this.pointerDown) return
- this.props.onResize(clientX - this.startX + this.baseSize)
- }
-
- onPointerUp = () => {
- this.pointerDown = false
- this.baseSize = this.props.size
- this.unsubscribeEvents()
- }
-
- render() {
- const { style } = this.props
- return (
-
-
-
- )
- }
+ return (
+
+
+
+ )
}
diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx
index 252b63a..f69895d 100644
--- a/src/components/SearchBar.tsx
+++ b/src/components/SearchBar.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
-import cx from 'utils/cx'
+import { cx } from 'utils/cx'
type Props = {
onSearch: (searchKey: string) => void
@@ -7,7 +7,7 @@ type Props = {
searchKey: string
}
-export default function SearchBar({ onSearch, onFocus, searchKey }: Props) {
+export function SearchBar({ onSearch, onFocus, searchKey }: Props) {
return (
void
- onShortcutChange: (shortcut: string) => void
- 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
- accessTokenHint: React.ReactNode
- shortcutHint: string
- toggleShowSideBarShortcut?: string
- reloadHint: React.ReactNode
- varyOptions: {
- key: string
- label: string
- onChange: (e: React.FormEvent
) => Promise | void
- getValue: () => boolean
- wikiLink?: string
- description?: string
- }[]
}
-export default class SettingsBar extends React.PureComponent {
- state = {
- accessToken: '',
- accessTokenHint: '',
- shortcutHint: '',
- toggleShowSideBarShortcut: this.props.toggleShowSideBarShortcut,
- reloadHint: '',
- varyOptions: [
- {
- key: 'compress-singleton',
- label: 'Compress singleton folder',
- onChange: this.createOnToggleChecked(
- configKeys.compressSingletonFolder,
- this.props.setCompressSingleton,
- ),
- getValue: () => this.props.compressSingletonFolder,
- wikiLink: wikiLinks.compressSingletonFolder,
- },
- {
- key: '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 Shortcut',
- onChange: this.createOnToggleChecked(
- configKeys.copySnippetButton,
- this.props.setCopySnippet,
- ),
- getValue: () => this.props.copySnippetButton,
- wikiLink: wikiLinks.copySnippet,
- },
- {
- key: 'intelligent-toggle',
- label: 'Intelligent Toggle',
- onChange: async (e: React.FormEvent) => {
- 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.`,
- },
- ],
- }
+const moreFields: SimpleField[] = [
+ {
+ key: 'compressSingletonFolder',
+ label: 'Compress singleton folder',
+ wikiLink: wikiLinks.compressSingletonFolder,
+ },
+ {
+ key: 'copyFileButton',
+ label: 'Copy File Shortcut',
+ wikiLink: wikiLinks.copyFileButton,
+ },
+ {
+ key: 'copySnippetButton',
+ label: 'Copy Snippet Shortcut',
+ wikiLink: wikiLinks.copySnippet,
+ },
+ {
+ key: 'intelligentToggle',
+ label: 'Intelligent Toggle',
+ description: `Gitako will open/close automatically according to page content when this is enabled.`,
+ overwrite: {
+ value: enabled => enabled === null,
+ onChange: checked => (checked ? null : true),
+ },
+ },
+]
- componentDidMount() {
- if (!this.props.accessToken) this.trySetUpAccessTokenWithCode()
- }
+function SettingsBarContent() {
+ const configContext = useConfigs()
+ const hasAccessToken = Boolean(configContext.val.access_token)
+ const useAccessToken = useStates('')
+ const useAccessTokenHint = useStates('')
+ const useShortcutHint = useStates('')
+ const useToggleShowSideBarShortcut = useStates(configContext.val.shortcut)
+ const useReloadHint = useStates('')
- componentWillReceiveProps({ toggleShowSideBarShortcut }: Props) {
- if (toggleShowSideBarShortcut !== this.props.toggleShowSideBarShortcut) {
- this.setState({ toggleShowSideBarShortcut })
- }
- }
+ const { val: accessTokenHint } = useAccessTokenHint
+ const { val: toggleShowSideBarShortcut } = useToggleShowSideBarShortcut
+ const { val: shortcutHint } = useShortcutHint
+ const { val: accessToken } = useAccessToken
+ const { val: reloadHint } = useReloadHint
- private async trySetUpAccessTokenWithCode() {
- try {
- const search = parseURLSearch()
- if ('code' in search) {
- const res = await JSONRequest('https://github.com/login/oauth/access_token', {
- code: search.code,
- client_id: oauth.clientId,
- client_secret: oauth.clientSecret,
- })
- const { access_token: accessToken, scope } = res
- if (scope !== 'repo' || !accessToken) {
- throw new Error(`Cannot resolve token response: '${JSON.stringify(res)}'`)
- }
- window.history.pushState({}, 'removed code', window.location.pathname.replace(/#.*$/, ''))
- this.setState({ accessToken }, () => this.saveToken(''))
+ React.useEffect(() => {
+ useToggleShowSideBarShortcut.set(configContext.val.shortcut)
+ }, [configContext.val.shortcut])
+
+ React.useEffect(() => {
+ // clear input when access token updates
+ useAccessToken.set('')
+ }, [configContext.val.access_token])
+
+ const onInputAccessToken = React.useCallback(
+ ({ currentTarget: { value } }: React.FormEvent) => {
+ useAccessToken.set(value)
+ useAccessTokenHint.set(
+ ACCESS_TOKEN_REGEXP.test(value) ? '' : 'This token is in unknown format.',
+ )
+ },
+ [],
+ )
+
+ const onPressAccessToken = React.useCallback(({ key }: React.KeyboardEvent) => {
+ if (key === 'Enter') saveToken()
+ }, [])
+
+ const saveToken = React.useCallback(
+ async (hint?: typeof useAccessTokenHint.val) => {
+ if (accessToken) {
+ configContext.set({ access_token: accessToken })
+ useAccessToken.set('')
+ useAccessTokenHint.set(
+ hint || (
+
+ window.location.reload()}>
+ Reload
+ {' '}
+ to activate!
+
+ ),
+ )
}
- } catch (err) {
- raiseError(err)
- }
- }
+ },
+ [accessToken],
+ )
- onInputAccessToken = (event: React.FormEvent) => {
- const { value } = event.currentTarget
- this.setState({
- accessToken: value,
- accessTokenHint: ACCESS_TOKEN_REGEXP.test(value) ? '' : 'This token is in unknown format.',
- })
- }
-
- onPressAccessToken = (event: React.KeyboardEvent) => {
- const { key } = event
- if (key === 'Enter') {
- this.saveToken()
- }
- }
-
- saveToken = async (
- hint: State['accessTokenHint'] = (
-
- window.location.reload()}>
- Reload
- {' '}
- to activate!
-
- ),
- ) => {
- const { onAccessTokenChange } = this.props
- const { accessToken } = this.state
- if (accessToken) {
- await configHelper.setOne(configKeys.accessToken, accessToken)
- onAccessTokenChange(accessToken)
- this.setState({
- accessToken: '',
- accessTokenHint: hint,
- })
- }
- }
-
- clearToken = async () => {
- const { onAccessTokenChange } = this.props
- await configHelper.setOne(configKeys.accessToken, '')
- onAccessTokenChange('')
- this.setState({ accessToken: '' })
- }
-
- saveShortcut = async () => {
- const { onShortcutChange } = this.props
- const { toggleShowSideBarShortcut } = this.state
- await configHelper.setOne(configKeys.shortcut, toggleShowSideBarShortcut)
+ const saveShortcut = React.useCallback(async () => {
+ const { val: toggleShowSideBarShortcut } = useToggleShowSideBarShortcut
+ configContext.set({ shortcut: toggleShowSideBarShortcut })
if (typeof toggleShowSideBarShortcut === 'string') {
- onShortcutChange(toggleShowSideBarShortcut)
- this.setState({
- shortcutHint: 'Shortcut is saved!',
- })
+ useShortcutHint.set('Shortcut is saved!')
}
- }
+ }, [useToggleShowSideBarShortcut.val])
- onShortCutInputKeyDown = (e: React.KeyboardEvent) => {
+ const onShortCutInputKeyDown = React.useCallback((e: React.KeyboardEvent) => {
e.preventDefault()
+ e.stopPropagation()
// Clear shortcut with backspace
const shortcut = e.key === 'Backspace' ? '' : keyHelper.parseEvent(e)
- this.setState({ toggleShowSideBarShortcut: shortcut })
- }
+ useToggleShowSideBarShortcut.set(shortcut)
+ }, [])
- showReloadHint = () => {
- this.setState({
- reloadHint: (
-
- Saved,{' '}
- window.location.reload()}>
- reload
- {' '}
- to apply.
-
- ),
- })
- }
-
- createOnToggleChecked(
- configKey: configKeys,
- set: (value: boolean) => void,
- ): (e: React.FormEvent) => Promise {
- return async e => {
- const enabled = e.currentTarget.checked
- await configHelper.setOne(configKey, enabled)
- set(enabled)
- this.showReloadHint()
- }
- }
-
- render() {
- const {
- accessTokenHint,
- toggleShowSideBarShortcut,
- shortcutHint,
- accessToken,
- reloadHint,
- varyOptions,
- } = this.state
- const { toggleShowSettings, activated } = this.props
- const hasAccessToken = Boolean(this.props.accessToken)
- return (
-
- {activated && (
-
- Settings
-
-
-
-
-
Toggle Shortcut
-
Set a combination of keys for toggling Gitako sidebar.
-
-
-
-
-
- {shortcutHint &&
{shortcutHint}}
-
-
-
More Options
- {varyOptions.map(option => (
-
-
-
-
- ))}
- {reloadHint &&
{reloadHint}
}
-
-
-
-
- )}
-
-
- v{version}
-
- {activated ? (
-
- ) : (
-
+ return (
+ <>
+
Settings
+
+
+
+
+
Toggle Shortcut
+
Set a combination of keys for toggling Gitako sidebar.
+
+
+
+
+
+ {shortcutHint &&
{shortcutHint}}
+
+
+
More Options
+ {moreFields.map(field => (
+
+
+
+
+ ))}
+
+ {reloadHint &&
{reloadHint}
}
+
+
- )
- }
+ >
+ )
+}
+
+export function SettingsBar(props: Props) {
+ const { toggleShowSettings, activated } = props
+ return (
+
+ )
}
diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx
index 781ac8d..82bdbdc 100644
--- a/src/components/SideBar.tsx
+++ b/src/components/SideBar.tsx
@@ -1,150 +1,198 @@
-import FileExplorer from 'components/FileExplorer'
-import MetaBar from 'components/MetaBar'
-import Portal from 'components/Portal'
-import Resizable from 'components/Resizable'
-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 { raiseError } from 'analytics'
+import { FileExplorer } from 'components/FileExplorer'
+import { MetaBar } from 'components/MetaBar'
+import { Portal } from 'components/Portal'
+import { Resizable } from 'components/Resizable'
+import { SettingsBar } from 'components/SettingsBar'
+import { ToggleShowButton } from 'components/ToggleShowButton'
+import { useConfigs } from 'containers/ConfigsContext'
+import { connect } from 'driver/connect'
+import { SideBarCore } from 'driver/core'
+import { ConnectorState, Props } from 'driver/core/SideBar'
+import { oauth } from 'env'
import * as React from 'react'
-import cx from 'utils/cx'
+import { cx } from 'utils/cx'
+import * as DOMHelper from 'utils/DOMHelper'
+import { JSONRequest, parseURLSearch } from 'utils/general'
+import { useDidUpdate } from 'utils/hooks'
+import * as keyHelper from 'utils/keyHelper'
+import * as URLHelper from 'utils/URLHelper'
-export type Props = {}
+const RawGitako: React.FC
= function RawGitako(props) {
+ const configContext = useConfigs()
+ const accessToken = props.configContext.val.access_token
-class Gitako extends React.PureComponent {
- static defaultProps: Partial = {
- baseSize: 260,
- shouldShow: false,
- showSettings: false,
- errorDueToAuth: false,
- accessToken: '',
- toggleShowSideBarShortcut: '',
- compressSingletonFolder: true,
- copyFileButton: true,
- copySnippetButton: true,
- disabled: false,
- }
+ React.useEffect(() => {
+ const { init } = props
+ ;(async function() {
+ if (!accessToken) {
+ const accessToken = await trySetUpAccessTokenWithCode()
+ configContext.set({ access_token: accessToken })
+ }
+ init()
+ })()
+ }, [])
- componentWillMount() {
- const { init } = this.props
- init()
- }
+ const onKeyDown = React.useCallback(
+ configContext.val.shortcut
+ ? (e: KeyboardEvent) => {
+ const keys = keyHelper.parseEvent(e)
+ if (keys === configContext.val.shortcut) {
+ props.toggleShowSideBar()
+ }
+ }
+ : () => {},
+ [configContext.val.shortcut],
+ )
- componentDidMount() {
- const { useListeners } = this.props
- useListeners(true)
- }
+ React.useEffect(() => {
+ if (props.disabled) return
+ window.addEventListener('keydown', onKeyDown)
+ return () => window.removeEventListener('keydown', onKeyDown)
+ }, [props.disabled, onKeyDown])
- componentWillUnmount() {
- const { useListeners } = this.props
- useListeners(false)
- }
+ const updateMeta = React.useCallback(() => {
+ if (props.disabled) return
+ DOMHelper.unmountTopProgressBar()
+ props.setMetaData({ ...props.metaData, ...URLHelper.parse() })
+ }, [props.disabled, props.metaData, configContext.val])
+ useOnPJAXComplete(updateMeta)
- renderAccessDeniedError() {
- return (
-
-
Access Denied
-
- Due to{' '}
-
- limitation of GitHub
- {' '}
- or{' '}
-
- auth needs
-
- , Gitako needs access token to continue. Please follow the instructions in the settings
- panel below.
-
-
- )
- }
+ const attachCopyFileButton = React.useCallback(() => {
+ if (props.disabled) return
+ if (configContext.val.copyFileButton) return DOMHelper.attachCopyFileBtn()
+ }, [props.disabled, configContext.val.copyFileButton])
+ useOnPJAXComplete(attachCopyFileButton)
- renderContent() {
- const {
- errorDueToAuth,
- metaData,
- treeData,
- showSettings,
- toggleShowSettings,
- compressSingletonFolder,
- accessToken,
- } = this.props
- return (
-
- {metaData && }
- {errorDueToAuth
- ? this.renderAccessDeniedError()
- : metaData && (
-
- )}
-
- )
- }
+ const attachCopySnippetButton = React.useCallback(() => {
+ if (props.disabled) return
+ if (configContext.val.copySnippetButton) return DOMHelper.attachCopySnippet()
+ }, [props.disabled, configContext.val.copySnippetButton])
+ useOnPJAXComplete(attachCopySnippetButton)
- render() {
- const {
- baseSize,
- error,
- shouldShow,
- showSettings,
- accessToken,
- compressSingletonFolder,
- copyFileButton,
- copySnippetButton,
- intelligentToggle,
- toggleShowSideBarShortcut,
- logoContainerElement,
- toggleShowSideBar,
- toggleShowSettings,
- onShortcutChange,
- onAccessTokenChange,
- setCompressSingleton,
- setCopyFile,
- setCopySnippet,
- setIntelligentToggle,
- } = this.props
- return (
-
-
-
-
-
-
- {this.renderContent()}
-
+ React.useEffect(() => {
+ if (configContext.val.intelligentToggle === null) {
+ props.setShouldShow(URLHelper.isInCodePage(props.metaData))
+ }
+ }, [props.metaData, configContext.val.intelligentToggle])
+
+ React.useEffect(() => {
+ if (configContext.val.copyFileButton) return DOMHelper.attachCopyFileBtn() || undefined // undefined is friendlier to React
+ }, [configContext.val.copyFileButton])
+
+ React.useEffect(() => {
+ if (configContext.val.copySnippetButton) return DOMHelper.attachCopySnippet() || undefined // undefined is friendlier to React
+ }, [configContext.val.copySnippetButton])
+
+ // init again when setting new accessToken
+ useDidUpdate(() => {
+ props.init()
+ }, [accessToken])
+
+ const {
+ errorDueToAuth,
+ metaData,
+ treeData,
+ baseSize,
+ error,
+ shouldShow,
+ showSettings,
+ logoContainerElement,
+ toggleShowSideBar,
+ toggleShowSettings,
+ } = props
+ return (
+
+
+
+
+
+
+
+ {metaData && }
+ {errorDueToAuth
+ ? renderAccessDeniedError()
+ : metaData && (
+
+ )}
-
-
- )
- }
+
+
+
+
+ )
}
-export default connect(SideBarCore)(Gitako)
+RawGitako.defaultProps = {
+ baseSize: 260,
+ shouldShow: false,
+ showSettings: false,
+ errorDueToAuth: false,
+ disabled: false,
+}
+
+export const SideBar = connect(SideBarCore)(RawGitako)
+
+function useEvent<
+ T extends {
+ addEventListener: Function
+ }
+>(target: T, event: string, callback: () => void, deps: React.DependencyList = []) {
+ React.useEffect(() => {
+ window.addEventListener('pjax:complete', callback)
+ return () => window.removeEventListener('pjax:complete', callback)
+ }, [callback, ...deps])
+}
+
+const useOnPJAXComplete = useEvent.bind(null, window, 'pjax:complete')
+
+function renderAccessDeniedError() {
+ return (
+
+
Access Denied
+
+ Due to{' '}
+
+ limitation of GitHub
+ {' '}
+ or{' '}
+
+ auth needs
+
+ , Gitako needs access token to continue. Please follow the instructions in the settings
+ panel below.
+
+
+ )
+}
+
+async function trySetUpAccessTokenWithCode() {
+ try {
+ const search = parseURLSearch()
+ if ('code' in search) {
+ const res = await JSONRequest('https://github.com/login/oauth/access_token', {
+ code: search.code,
+ client_id: oauth.clientId,
+ client_secret: oauth.clientSecret,
+ })
+ const { access_token: accessToken, scope } = res
+ if (scope !== 'repo' || !accessToken) {
+ throw new Error(`Cannot resolve token response: '${JSON.stringify(res)}'`)
+ }
+ window.history.pushState({}, 'removed code', window.location.pathname.replace(/#.*$/, ''))
+ return accessToken
+ }
+ } catch (err) {
+ raiseError(err)
+ }
+}
diff --git a/src/components/SizeObserver.tsx b/src/components/SizeObserver.tsx
index c3c443c..ca64c30 100644
--- a/src/components/SizeObserver.tsx
+++ b/src/components/SizeObserver.tsx
@@ -11,7 +11,7 @@ type Props = {
children(size: Partial): React.ReactNode
} & React.HTMLAttributes
-export default function SizeObserver({ type = 'div', children, ...rest }: Props) {
+export function SizeObserver({ type = 'div', children, ...rest }: Props) {
const ref = React.useRef()
const [size, setSize] = React.useState>({
@@ -19,6 +19,16 @@ export default function SizeObserver({ type = 'div', children, ...rest }: Props)
height: undefined,
})
+ const safeSetSize = React.useCallback(function safeSetSize(rect: DOMRectReadOnly) {
+ // requestAnimationFrame fixes "ResizeObserver loop limit exceeded" error
+ requestAnimationFrame(() =>
+ setSize({
+ width: rect.width,
+ height: rect.height,
+ }),
+ )
+ }, [])
+
React.useLayoutEffect(() => {
if (features.resize) {
const observer = new window.ResizeObserver(entries => {
@@ -43,14 +53,4 @@ export default function SizeObserver({ type = 'div', children, ...rest }: Props)
const props: any = { ...rest, ref } // :)
return React.createElement(type, props, children(size))
-
- function safeSetSize(rect: DOMRectReadOnly) {
- // requestAnimationFrame fixes "ResizeObserver loop limit exceeded" error
- requestAnimationFrame(() =>
- setSize({
- width: rect.width,
- height: rect.height,
- }),
- )
- }
}
diff --git a/src/components/ToggleShowButton.tsx b/src/components/ToggleShowButton.tsx
index fa45455..219b06d 100644
--- a/src/components/ToggleShowButton.tsx
+++ b/src/components/ToggleShowButton.tsx
@@ -1,13 +1,14 @@
+import { Icon } from 'components/Icon'
import * as React from 'react'
-import Icon from 'components/Icon'
-import cx from 'utils/cx'
+import { cx } from 'utils/cx'
type Props = {
error?: string
shouldShow: boolean
toggleShowSideBar: React.MouseEventHandler
}
-export default function Logo({ error, shouldShow, toggleShowSideBar }: Props) {
+
+export function ToggleShowButton({ error, shouldShow, toggleShowSideBar }: Props) {
return (
+export type ConfigsContextShape = ContextShape
+
+export const ConfigsContext = React.createContext
(null)
+
+export function ConfigsContextWrapper(props: React.PropsWithChildren) {
+ const [configs, setConfigs] = React.useState(null)
+ React.useEffect(() => {
+ configsHelper.get().then(setConfigs)
+ }, [])
+ const set = React.useCallback(
+ (updatedConfigs: Partial) => {
+ const mergedConfigs = { ...configs, ...updatedConfigs } as Config
+ configsHelper.set(mergedConfigs)
+ setConfigs(mergedConfigs)
+ },
+ [configs, setConfigs],
+ )
+ if (configs === null) return null
+ return (
+
+ {props.children}
+
+ )
+}
+
+export const useConfigs = useNonNullContext(ConfigsContext)
+
+function useNonNullContext>(theContext: React.Context): () => R {
+ return () => {
+ const context = React.useContext(theContext)
+ if (context === null) throw new Error(`Empty context`)
+ return context as R
+ }
+}
diff --git a/src/content.less b/src/content.less
index 1c6fdbc..b0ac42f 100644
--- a/src/content.less
+++ b/src/content.less
@@ -59,14 +59,10 @@
background-image: url('~@primer/octicons/build/svg/clippy.svg?inline');
background-position: center;
background-repeat: no-repeat;
- }
- &.success {
- .icon {
+ &.success {
background-image: url('~@primer/octicons/build/svg/check.svg?inline');
}
- }
- &.fail {
- .icon {
+ &.fail {
background-image: url('~@primer/octicons/build/svg/x.svg?inline');
}
}
@@ -473,7 +469,7 @@
color: #6a737d;
}
}
- .placeholder-row {
+ .header-row {
flex-shrink: 0;
display: flex;
justify-content: space-between;
diff --git a/src/content.tsx b/src/content.tsx
index 06b7c41..c9eb923 100644
--- a/src/content.tsx
+++ b/src/content.tsx
@@ -1,9 +1,8 @@
+import { withErrorLog } from 'analytics'
+import { Gitako } from 'components/Gitako'
+import { addMiddleware } from 'driver/connect'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
-import Gitako from 'components/Gitako'
-import { addMiddleware } from 'driver/connect'
-import { withErrorLog } from 'analytics'
-
import './content.less'
addMiddleware(withErrorLog)
diff --git a/src/driver/connect.ts b/src/driver/connect.ts
index 7eec449..0f31303 100644
--- a/src/driver/connect.ts
+++ b/src/driver/connect.ts
@@ -31,7 +31,7 @@ function run([method, args]: [M, Parameters]) {
}
export type DispatchState = React.Component['setState']
-export type GetState = () => State
+export type GetState = () => [State, Props]
export type TriggerOtherMethod = (
methodCreator: MethodCreator,
...args: Parameters>>
@@ -39,7 +39,7 @@ export type TriggerOtherMethod = (
export type Dispatch = {
set: DispatchState
- get: GetState
+ get: GetState
call: TriggerOtherMethod
}
@@ -47,7 +47,7 @@ export type MethodCreator = (
dispatch: Dispatch,
) => Method
-type Sources = {
+export type Sources
= {
[key: string]: MethodCreator
}
type WrappedMethods = {
@@ -75,7 +75,7 @@ function link
(instance: React.Component
, sources: Sources
): Wr
const dispatchState: DispatchState
= (updater, callback) => {
instance.setState(updater, callback)
}
- const prepareState: GetState = () => instance.state
+ const prepareState: GetState
= () => [instance.state, instance.props]
const dispatch: Dispatch
= {
call: dispatchCall,
get: prepareState,
@@ -93,20 +93,20 @@ function link
(instance: React.Component
, sources: Sources
): Wr
return wrappedMethods
}
-export default function connect(mapping: Sources) {
- return function linkComponent(
- ComponentClass: React.ComponentClass,
- ): React.ComponentClass {
- return class AwesomeApp extends React.PureComponent {
- static displayName = `Connected(${ComponentClass.displayName || ComponentClass.name})`
- static defaultProps = ComponentClass.defaultProps
+export function connect(mapping: Sources) {
+ return function linkComponent>(
+ Component: ComponentType,
+ ) {
+ return class ConnectedComponent extends React.PureComponent {
+ static displayName = `Connected(${Component.displayName || Component.name})`
+ static defaultProps = Component.defaultProps
- state = {} as ExtraP
- connectedMethods = link(this, mapping) as WrappedMethods
+ state: ExtraP = {} as ExtraP
+ connectedMethods: WrappedMethods = link(this, mapping)
render() {
const props = Object.assign({}, this.props, this.connectedMethods, this.state)
- return React.createElement(ComponentClass, props)
+ return React.createElement(Component, props)
}
}
}
diff --git a/src/driver/core/FileExplorer.ts b/src/driver/core/FileExplorer.ts
index ba075c5..1373e83 100644
--- a/src/driver/core/FileExplorer.ts
+++ b/src/driver/core/FileExplorer.ts
@@ -1,13 +1,22 @@
-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 { Config } from 'utils/configHelper'
+import * as 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 * as GitHubHelper from 'utils/GitHubHelper'
+import { BlobData } from 'utils/GitHubHelper'
+import * as treeParser from 'utils/treeParser'
+import * as URLHelper from 'utils/URLHelper'
+import { TreeNode, VisibleNodes, VisibleNodesGenerator } from 'utils/VisibleNodesGenerator'
+
+export type Props = {
+ treeData?: GitHubHelper.TreeData
+ metaData: GitHubHelper.MetaData
+ freeze: boolean
+ accessToken: string | undefined
+ toggleShowSettings: React.MouseEventHandler
+}
export type ConnectorState = {
stateText: string
@@ -49,7 +58,7 @@ let visibleNodesGenerator: VisibleNodesGenerator
type BoundMethodCreator = MethodCreator
-const init: BoundMethodCreator = dispatch => () =>
+export const init: BoundMethodCreator = dispatch => () =>
dispatch.call(setStateText, 'Fetching File List...')
const githubSubModuleURLRegex = {
@@ -127,8 +136,8 @@ function handleParsed(root: TreeNode, parsed: Parsed) {
})
}
-const setUpTree: BoundMethodCreator<
- [Pick]
+export const setUpTree: BoundMethodCreator<
+ [Pick & Pick]
> = dispatch => async ({ treeData, metaData, compressSingletonFolder, accessToken }) => {
if (!treeData) return
dispatch.call(setStateText, 'Rendering File List...')
@@ -158,22 +167,22 @@ const setUpTree: BoundMethodCreator<
dispatch.call(goTo, URLHelper.getCurrentPath(metaData.branchName))
}
-const execAfterRender: BoundMethodCreator = dispatch => () => {
+export const execAfterRender: BoundMethodCreator = dispatch => () => {
for (const task of tasksAfterRender) {
task()
}
tasksAfterRender.length = 0
}
-const setStateText: BoundMethodCreator<[ConnectorState['stateText']]> = dispatch => (
+export const setStateText: BoundMethodCreator<[ConnectorState['stateText']]> = dispatch => (
text: string,
) =>
dispatch.set({
stateText: text,
})
-const handleKeyDown: BoundMethodCreator<[React.KeyboardEvent]> = dispatch => event => {
- const { searched, visibleNodes } = dispatch.get()
+export const handleKeyDown: BoundMethodCreator<[React.KeyboardEvent]> = dispatch => event => {
+ const [{ searched, visibleNodes }] = dispatch.get()
if (!visibleNodes) return
const { nodes, focusedNode, expandedNodes, depths } = visibleNodes
function handleVerticalMove(index: number) {
@@ -277,16 +286,17 @@ const handleKeyDown: BoundMethodCreator<[React.KeyboardEvent]> = dispatch => eve
}
}
-const onFocusSearchBar: BoundMethodCreator = dispatch => () => dispatch.call(focusNode, null, false)
+export const onFocusSearchBar: BoundMethodCreator = dispatch => () =>
+ dispatch.call(focusNode, null, false)
-const search: BoundMethodCreator<[string]> = dispatch => searchKey => {
+export 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 => {
+export const goTo: BoundMethodCreator<[string[]]> = dispatch => async currentPath => {
visibleNodesGenerator.search([])
tasksAfterRender.push(() => {
const nodeExpandedTo = visibleNodesGenerator.expandTo(currentPath.join('/'))
@@ -298,12 +308,15 @@ const goTo: BoundMethodCreator<[string[]]> = dispatch => async currentPath => {
dispatch.set({ searchKey: '', searched: false })
}
-const setExpand: BoundMethodCreator<[TreeNode, boolean]> = dispatch => (node, expand = false) => {
+export const setExpand: BoundMethodCreator<[TreeNode, boolean]> = dispatch => (
+ node,
+ expand = false,
+) => {
visibleNodesGenerator.setExpand(node, expand)
dispatch.call(focusNode, node, false)
}
-const toggleNodeExpansion: BoundMethodCreator<[TreeNode, boolean]> = dispatch => (
+export const toggleNodeExpansion: BoundMethodCreator<[TreeNode, boolean]> = dispatch => (
node,
skipScrollToNode,
) => {
@@ -312,17 +325,16 @@ const toggleNodeExpansion: BoundMethodCreator<[TreeNode, boolean]> = dispatch =>
tasksAfterRender.push(DOMHelper.focusFileExplorer)
}
-const focusNode: BoundMethodCreator<[TreeNode | null, boolean]> = dispatch => (
+export const focusNode: BoundMethodCreator<[TreeNode | null, boolean]> = dispatch => (
node: TreeNode | null,
- skipScroll = false,
) => {
- const { visibleNodes } = dispatch.get()
+ const [{ visibleNodes }] = dispatch.get()
if (!visibleNodes) return
visibleNodesGenerator.focusNode(node)
dispatch.call(updateVisibleNodes)
}
-const onNodeClick: BoundMethodCreator<[TreeNode]> = dispatch => node => {
+export const onNodeClick: BoundMethodCreator<[TreeNode]> = dispatch => node => {
if (node.type === 'tree') {
dispatch.call(toggleNodeExpansion, node, true)
} else if (node.type === 'blob') {
@@ -335,23 +347,7 @@ const onNodeClick: BoundMethodCreator<[TreeNode]> = dispatch => node => {
}
}
-const updateVisibleNodes: BoundMethodCreator = dispatch => () => {
+export const updateVisibleNodes: BoundMethodCreator = dispatch => () => {
const { visibleNodes } = visibleNodesGenerator
dispatch.set({ visibleNodes })
}
-
-export default {
- init,
- setUpTree,
- execAfterRender,
- setStateText,
- handleKeyDown,
- onFocusSearchBar,
- search,
- setExpand,
- goTo,
- toggleNodeExpansion,
- focusNode,
- onNodeClick,
- updateVisibleNodes,
-}
diff --git a/src/driver/core/SideBar.ts b/src/driver/core/SideBar.ts
index d738340..f8f282d 100644
--- a/src/driver/core/SideBar.ts
+++ b/src/driver/core/SideBar.ts
@@ -1,18 +1,13 @@
-import { Props } from 'components/SideBar'
+import { ConfigsContextShape } from 'containers/ConfigsContext'
import { GetCreatedMethod, MethodCreator } from 'driver/connect'
-import configHelper, { Config, configKeys } from 'utils/configHelper'
-import DOMHelper from 'utils/DOMHelper'
-import GitHubHelper, {
- API_RATE_LIMIT,
- BAD_CREDENTIALS,
- BLOCKED_PROJECT,
- EMPTY_PROJECT,
- MetaData,
- NOT_FOUND,
- TreeData,
-} from 'utils/GitHubHelper'
-import keyHelper from 'utils/keyHelper'
-import URLHelper from 'utils/URLHelper'
+import * as DOMHelper from 'utils/DOMHelper'
+import * as GitHubHelper from 'utils/GitHubHelper'
+import { MetaData, TreeData } from 'utils/GitHubHelper'
+import * as URLHelper from 'utils/URLHelper'
+
+export type Props = {
+ configContext: ConfigsContextShape
+}
export type ConnectorState = {
// error message
@@ -32,30 +27,18 @@ export type ConnectorState = {
initializingPromise: Promise | null
} & {
init: GetCreatedMethod
- onPJAXEnd: GetCreatedMethod
- onKeyDown: GetCreatedMethod
+ setMetaData: GetCreatedMethod
+ setShouldShow: GetCreatedMethod
toggleShowSideBar: GetCreatedMethod
toggleShowSettings: GetCreatedMethod
- useListeners: GetCreatedMethod
- onAccessTokenChange: GetCreatedMethod
- onShortcutChange: GetCreatedMethod
- setCopyFile: GetCreatedMethod
- setCopySnippet: GetCreatedMethod
- setCompressSingleton: GetCreatedMethod
- setIntelligentToggle: GetCreatedMethod
} & {
baseSize: number
- toggleShowSideBarShortcut?: string
- accessToken?: string
-} & Pick<
- Config,
- 'compressSingletonFolder' | 'copyFileButton' | 'copySnippetButton' | 'intelligentToggle'
- >
+}
type BoundMethodCreator = MethodCreator
-const init: BoundMethodCreator = dispatch => async () => {
- const { initializingPromise } = dispatch.get()
+export const init: BoundMethodCreator = dispatch => async () => {
+ const [{ initializingPromise }] = dispatch.get()
if (initializingPromise) await initializingPromise
let done: any = null // cannot use type `(() => void) | null` here
@@ -82,24 +65,10 @@ const init: BoundMethodCreator = dispatch => async () => {
}
metaData.branchName = detectedBranchName || 'master'
dispatch.call(setMetaData, metaData)
- const {
- sideBarWidth,
- access_token: accessToken,
- shortcut,
- compressSingletonFolder,
- copyFileButton,
- copySnippetButton,
- intelligentToggle,
- } = await configHelper.getAll()
- DOMHelper.decorateGitHubPageContent({ copyFileButton, copySnippetButton })
+ const [, { configContext }] = dispatch.get()
+ const { sideBarWidth, access_token: accessToken, intelligentToggle } = configContext.val
dispatch.set({
baseSize: sideBarWidth,
- accessToken,
- toggleShowSideBarShortcut: shortcut,
- compressSingletonFolder,
- copyFileButton,
- copySnippetButton,
- intelligentToggle,
})
if (!metaData.branchName || !metaData.userName) return
@@ -159,146 +128,57 @@ const init: BoundMethodCreator = dispatch => async () => {
}
}
-const handleError: BoundMethodCreator<[Error]> = dispatch => async err => {
- if (err.message === EMPTY_PROJECT) {
+export const handleError: BoundMethodCreator<[Error]> = dispatch => async err => {
+ if (err.message === GitHubHelper.EMPTY_PROJECT) {
dispatch.call(setError, 'This project seems to be empty.')
- } else if (err.message === BLOCKED_PROJECT) {
+ } else if (err.message === GitHubHelper.BLOCKED_PROJECT) {
dispatch.call(setError, 'This project is blocked.')
} else if (
- err.message === NOT_FOUND ||
- err.message === BAD_CREDENTIALS ||
- err.message === API_RATE_LIMIT
+ err.message === GitHubHelper.NOT_FOUND ||
+ err.message === GitHubHelper.BAD_CREDENTIALS ||
+ err.message === GitHubHelper.API_RATE_LIMIT
) {
dispatch.set({ errorDueToAuth: true })
dispatch.call(setShowSettings, true)
dispatch.call(setShouldShow, true)
} else {
- dispatch.call(useListeners, false)
dispatch.call(setError, 'Gitako ate a bug, but it should recovery soon!')
throw err
}
}
-const onPJAXEnd: BoundMethodCreator = dispatch => () => {
- const { metaData, copyFileButton, copySnippetButton, intelligentToggle } = dispatch.get()
- DOMHelper.unmountTopProgressBar()
- DOMHelper.decorateGitHubPageContent({ copyFileButton, copySnippetButton })
- const mergedMetaData = { ...metaData, ...URLHelper.parse() }
- dispatch.call(setMetaData, mergedMetaData)
-
- if (intelligentToggle === null) {
- dispatch.call(setShouldShow, URLHelper.isInCodePage(mergedMetaData))
- }
-}
-
-const onKeyDown: BoundMethodCreator<[KeyboardEvent]> = dispatch => e => {
- const { toggleShowSideBarShortcut } = dispatch.get()
- if (toggleShowSideBarShortcut) {
- const keys = keyHelper.parseEvent(e)
- if (keys === toggleShowSideBarShortcut) {
- dispatch.call(toggleShowSideBar)
- }
- }
-}
-
-const toggleShowSideBar: BoundMethodCreator = dispatch => () => {
- const { intelligentToggle } = dispatch.get()
- const shouldShow = !dispatch.get().shouldShow
- dispatch.call(setShouldShow, shouldShow)
+export const toggleShowSideBar: BoundMethodCreator = dispatch => () => {
+ const [{ shouldShow }, { configContext }] = dispatch.get()
+ dispatch.call(setShouldShow, !shouldShow)
+ const {
+ val: { intelligentToggle },
+ } = configContext
if (intelligentToggle !== null) {
- dispatch.call(setIntelligentToggle, shouldShow)
+ configContext.set({ intelligentToggle: !shouldShow })
}
}
-const setShouldShow: BoundMethodCreator<
- [ConnectorState['shouldShow']]
-> = dispatch => shouldShow => {
+export const setShouldShow: BoundMethodCreator<[
+ ConnectorState['shouldShow'],
+]> = dispatch => shouldShow => {
dispatch.set({ shouldShow }, shouldShow ? DOMHelper.focusFileExplorer : undefined)
DOMHelper.setBodyIndent(shouldShow)
}
-const setError: BoundMethodCreator<[ConnectorState['error']]> = dispatch => error => {
+export const setError: BoundMethodCreator<[ConnectorState['error']]> = dispatch => error => {
dispatch.set({ error })
dispatch.call(setShouldShow, false)
}
-const toggleShowSettings: BoundMethodCreator = dispatch => () =>
+export const toggleShowSettings: BoundMethodCreator = dispatch => () =>
dispatch.set(({ showSettings }) => ({
showSettings: !showSettings,
}))
-const setShowSettings: BoundMethodCreator<
- [ConnectorState['showSettings']]
-> = dispatch => showSettings => dispatch.set({ showSettings })
+export const setShowSettings: BoundMethodCreator<[
+ ConnectorState['showSettings'],
+]> = dispatch => showSettings => dispatch.set({ showSettings })
-const onAccessTokenChange: BoundMethodCreator<
- [ConnectorState['accessToken']]
-> = dispatch => accessToken => {
- dispatch.set({ accessToken })
- // reload when setting new accessToken
- if (accessToken) {
- dispatch.call(init)
- }
-}
-
-const onShortcutChange: BoundMethodCreator<
- [ConnectorState['toggleShowSideBarShortcut']]
-> = dispatch => shortcut => dispatch.set({ toggleShowSideBarShortcut: shortcut })
-
-const setMetaData: BoundMethodCreator<[ConnectorState['metaData']]> = dispatch => metaData =>
+export const setMetaData: BoundMethodCreator<[ConnectorState['metaData']]> = dispatch => metaData =>
dispatch.set({ metaData })
-
-const setCompressSingleton: BoundMethodCreator<
- [ConnectorState['compressSingletonFolder']]
-> = dispatch => compressSingletonFolder => dispatch.set({ compressSingletonFolder })
-
-const setCopyFile: BoundMethodCreator<
- [ConnectorState['copyFileButton']]
-> = dispatch => copyFileButton => dispatch.set({ copyFileButton })
-
-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(onPJAXEnd)
- const $onKeyDown = (e: KeyboardEvent) => dispatch.call(onKeyDown, e)
- return on => {
- const { disabled } = dispatch.get()
- if (on && !disabled) {
- window.addEventListener('pjax:complete', $onPJAXEnd)
- window.addEventListener('keydown', $onKeyDown)
- } else {
- window.removeEventListener('pjax:complete', $onPJAXEnd)
- window.removeEventListener('keydown', $onKeyDown)
- }
- }
-}
-
-export default {
- init,
- onPJAXEnd,
- onKeyDown,
- setShouldShow,
- setShowSettings,
- toggleShowSideBar,
- toggleShowSettings,
- onAccessTokenChange,
- onShortcutChange,
- setMetaData,
- setCompressSingleton,
- setCopyFile,
- setCopySnippet,
- setIntelligentToggle,
- setError,
- handleError,
- useListeners,
-}
diff --git a/src/driver/core/index.ts b/src/driver/core/index.ts
index 071ab89..4eb403b 100644
--- a/src/driver/core/index.ts
+++ b/src/driver/core/index.ts
@@ -1,2 +1,11 @@
-export { default as SideBar } from './SideBar'
-export { default as FileExplorer } from './FileExplorer'
+import { Sources } from 'driver/connect'
+import * as FileExplorer from './FileExplorer'
+import {
+ ConnectorState as FileExplorerConnectorState,
+ Props as FileExplorerProps,
+} from './FileExplorer'
+import * as SideBar from './SideBar'
+import { ConnectorState as SideBarConnectorState, Props as SideBarProps } from './SideBar'
+
+export const FileExplorerCore: Sources = FileExplorer
+export const SideBarCore: Sources = SideBar
diff --git a/src/env.ts b/src/env.ts
index dca85ac..bba237d 100644
--- a/src/env.ts
+++ b/src/env.ts
@@ -9,3 +9,5 @@ export const oauth = {
clientId: process.env.GITHUB_OAUTH_CLIENT_ID,
clientSecret: process.env.GITHUB_OAUTH_CLIENT_SECRET,
}
+
+export const VERSION = process.env.VERSION
diff --git a/src/firefox-shim.js b/src/firefox-shim.js
index 8e949c4..205efab 100644
--- a/src/firefox-shim.js
+++ b/src/firefox-shim.js
@@ -1 +1,3 @@
window.requestAnimationFrame = window.requestAnimationFrame.bind(window)
+window.setTimeout = window.setTimeout.bind(window)
+window.clearTimeout = window.clearTimeout.bind(window)
diff --git a/src/global.d.ts b/src/global.d.ts
new file mode 100644
index 0000000..fac9ed3
--- /dev/null
+++ b/src/global.d.ts
@@ -0,0 +1,9 @@
+type ValSet = {
+ val: T
+ set: (val: T) => void
+}
+
+type PartialValSet = {
+ val: T
+ set: (val: Partial) => void
+}
diff --git a/src/utils/DOMHelper.ts b/src/utils/DOMHelper.ts
index 31d681a..613f5c4 100644
--- a/src/utils/DOMHelper.ts
+++ b/src/utils/DOMHelper.ts
@@ -1,17 +1,20 @@
/**
* this helper helps manipulating DOM
*/
-
import { raiseError } from 'analytics'
+import { Clippy, ClippyClassName } from 'components/Clippy'
+import { CopyFileButton, copyFileButtonClassName } from 'components/CopyFileButton'
import * as NProgress from 'nprogress'
import * as PJAX from 'pjax'
+import * as React from 'react'
+import { renderReact } from './general'
NProgress.configure({ showSpinner: false })
/**
* when gitako is ready, make page's header narrower
*/
-function markGitakoReadyState() {
+export function markGitakoReadyState() {
const readyClassName = 'gitako-ready'
document.body.classList.add(readyClassName)
}
@@ -21,7 +24,7 @@ function markGitakoReadyState() {
* otherwise, hide the space
*/
export const bodySpacingClassName = 'with-gitako-spacing'
-function setBodyIndent(shouldShowGitako: boolean) {
+export function setBodyIndent(shouldShowGitako: boolean) {
if (shouldShowGitako) {
document.body.classList.add(bodySpacingClassName)
} else {
@@ -35,10 +38,10 @@ function $ any, O extends () => a
otherwise?: O,
): E extends never
? O extends never
- ? (Element | null)
+ ? Element | null
: ReturnType | null
: O extends never
- ? (ReturnType | null)
+ ? ReturnType | null
: ReturnType | ReturnType {
const element = document.querySelector(selector)
if (element) {
@@ -47,18 +50,18 @@ function $ any, O extends () => a
return otherwise ? otherwise() : null
}
-function isInCodePage() {
+export function isInCodePage() {
const branchListSelector = '#branch-select-menu.branch-select-menu'
return Boolean($(branchListSelector))
}
-function getBranches() {
+export function getBranches() {
const branchSelector = '.branch-select-menu .select-menu-list > div .select-menu-item-text'
const branchElements = Array.from(document.querySelectorAll(branchSelector))
return branchElements.map(element => element.innerHTML.trim())
}
-function getCurrentBranch() {
+export function getCurrentBranch() {
const selectedBranchButtonSelector = '.repository-content .branch-select-menu summary'
const branchButtonElement: HTMLElement = $(selectedBranchButtonSelector)
if (branchButtonElement) {
@@ -92,16 +95,15 @@ function getCurrentBranch() {
/**
* add the logo element into DOM
- *
*/
-function insertLogoMountPoint() {
+export function insertLogoMountPoint() {
const logoSelector = '.gitako .gitako-logo'
return $(logoSelector) || createLogoMountPoint()
}
function createLogoMountPoint() {
const logoMountElement = document.createElement('div')
- logoMountElement.setAttribute('class', 'gitako-logo-mount-point')
+ logoMountElement.classList.add('gitako-logo-mount-point')
document.body.appendChild(logoMountElement)
return logoMountElement
}
@@ -110,7 +112,7 @@ function createLogoMountPoint() {
* content above the file navigation bar is same for all pages of the repo
* use this function to scroll down a bit to hide them
*/
-function scrollToRepoContent() {
+export function scrollToRepoContent() {
const repositoryContentSelector = '.repository-content'
// do NOT use behavior: smooth here as it will scroll horizontally
$(repositoryContentSelector, repositoryContentElement =>
@@ -127,8 +129,8 @@ const pjax = new PJAX({
forceCache: true, // TODO: merge namespace, add forceCache
})
-function loadWithPJAX(URL: string) {
- NProgress.start()
+export function loadWithPJAX(URL: string) {
+ mountTopProgressBar()
pjax.loadUrl(URL, { scrollTo: 0 })
}
@@ -153,8 +155,8 @@ const PAGE_TYPES = {
*
* TODO: distinguish type 'preview'
*/
-function getCurrentPageType() {
- const blobWrapperSelector = '.repository-content .file .blob-wrapper table'
+export function getCurrentPageType() {
+ const blobWrapperSelector = '.repository-content .blob-wrapper table'
const readmeSelector = '.repository-content .readme'
return (
$(blobWrapperSelector, () => PAGE_TYPES.RAW_TEXT) ||
@@ -165,7 +167,7 @@ function getCurrentPageType() {
export const REPO_TYPE_PRIVATE = 'private'
export const REPO_TYPE_PUBLIC = 'public'
-function getRepoPageType() {
+export function getRepoPageType() {
const headerSelector = `#js-repo-pjax-container .pagehead.repohead h1`
return $(headerSelector, header => {
const repoPageTypes = [REPO_TYPE_PRIVATE, REPO_TYPE_PUBLIC]
@@ -178,69 +180,54 @@ function getRepoPageType() {
})
}
+/**
+ * get text content of raw text content
+ */
+export function getCodeElement() {
+ if (getCurrentPageType() === PAGE_TYPES.RAW_TEXT) {
+ const codeContentSelector = '.repository-content .data table'
+ const codeContentElement = $(codeContentSelector)
+ if (!codeContentElement) {
+ raiseError(new Error('cannot find code content element'))
+ }
+ return codeContentElement
+ }
+}
+
/**
* add copy file content buttons to button groups
* click these buttons will copy file content to clipboard
*/
-function attachCopyFileBtn() {
- /**
- * get text content of raw text content
- */
- function getCodeElement() {
- if (getCurrentPageType() === PAGE_TYPES.RAW_TEXT) {
- const codeContentSelector = '.repository-content .file .data table'
- const codeContentElement = $(codeContentSelector)
- if (!codeContentElement) {
- raiseError(new Error('cannot find code content element'))
- }
- return codeContentElement
- }
- }
-
- /**
- * change inner text of copy file button to give feedback
- * @param {element} copyFileBtn
- * @param {string} text
- */
- function setTempCopyFileBtnText(copyFileBtn: HTMLButtonElement, text: string) {
- copyFileBtn.innerText = text
- window.setTimeout(() => (copyFileBtn.innerText = 'Copy file'), 1000)
- }
-
+export function attachCopyFileBtn() {
if (getCurrentPageType() === PAGE_TYPES.RAW_TEXT) {
- const btnGroupSelector = [
- // the button group next to navigation bar
- '.repository-content .file-navigation.js-zeroclipboard-container .BtnGroup',
- // the button group in file content header
- '.repository-content .file .file-header .file-actions .BtnGroup',
- ].join(', ')
- const btnGroups = document.querySelectorAll(btnGroupSelector)
+ // the button group in file content header
+ const buttonGroupSelector = '.repository-content > .Box > .Box-header .BtnGroup'
+ const buttonGroups = document.querySelectorAll(buttonGroupSelector)
- btnGroups.forEach(btnGroup => {
- const copyFileBtn = document.createElement('button')
- copyFileBtn.classList.add('btn', 'btn-sm', 'BtnGroup-item', 'copy-file-btn')
- copyFileBtn.innerText = 'Copy file'
- copyFileBtn.addEventListener('click', () => {
- const codeElement = getCodeElement()
- if (codeElement) {
- if (copyElementContent(codeElement)) {
- setTempCopyFileBtnText(copyFileBtn, 'Success!')
- } else {
- setTempCopyFileBtnText(copyFileBtn, 'Copy failed!')
- }
- }
- })
- btnGroup.insertBefore(copyFileBtn, btnGroup.lastChild)
+ if (buttonGroups.length === 0) {
+ raiseError(new Error(`No button groups found`))
+ }
+
+ buttonGroups.forEach(async buttonGroup => {
+ if (!buttonGroup.lastElementChild) return
+ const button = await renderReact(React.createElement(CopyFileButton))
+ if (button instanceof HTMLElement) {
+ buttonGroup.appendChild(button)
+ }
})
+ return () => {
+ const buttons = document.querySelectorAll(`.${copyFileButtonClassName}`)
+ buttons.forEach(button => {
+ button.parentElement?.removeChild(button)
+ })
+ }
}
}
/**
* copy content of a DOM element to clipboard
- * @param {element} element
- * @returns {boolean} whether copy is successful
*/
-function copyElementContent(element: Element) {
+export function copyElementContent(element: Element): boolean {
let selection = window.getSelection()
if (selection) selection.removeAllRanges()
const range = document.createRange()
@@ -253,67 +240,20 @@ function copyElementContent(element: Element) {
return isCopySuccessful
}
-/**
- * create a copy file content button `clippy`
- * once mouse enters a code snippet of markdown, move clippy into it
- * user can copy the snippet's content by click it
- *
- * TODO: 'reactify' it
- */
-function createClippy() {
- function setTempClippyIconFeedback(clippy: Element, type: 'success' | 'fail') {
- const tempIconClassName = type === 'success' ? 'success' : 'fail'
- clippy.classList.add(tempIconClassName)
- window.setTimeout(() => {
- clippy.classList.remove(tempIconClassName)
- }, 1000)
- }
-
- /**
- *
- *
- *
- */
- const clippyWrapper = document.createElement('div')
- clippyWrapper.classList.add('clippy-wrapper')
- const clippy = document.createElement('button')
- clippy.classList.add('clippy')
- const clippyIcon = document.createElement('i')
- clippyIcon.classList.add('icon')
-
- clippyWrapper.appendChild(clippy)
- clippy.appendChild(clippyIcon)
-
- // set clipboard with current code snippet element's content
- clippy.addEventListener('click', function onClippyClick() {
- if (copyElementContent(currentCodeSnippetElement)) {
- setTempClippyIconFeedback(clippy, 'success')
- } else {
- setTempClippyIconFeedback(clippy, 'fail')
- }
- })
-
- return clippyWrapper
-}
-
-const clippy = createClippy()
-
-let currentCodeSnippetElement: Element
-function attachCopySnippet() {
+export function attachCopySnippet() {
const readmeSelector = '.repository-content div#readme'
return $(readmeSelector, () => {
const readmeArticleSelector = '.repository-content div#readme article'
- $(
+ return $(
readmeArticleSelector,
- readmeElement =>
- readmeElement.addEventListener('mouseover', e => {
- // only move clippy when mouse is over a new snippet()
- const target = e.target as Element
- if (target.nodeName === 'PRE') {
- if (currentCodeSnippetElement !== target) {
- currentCodeSnippetElement = target
+ readmeElement => {
+ const mouseOverCallback = async ({ target }: Event): Promise => {
+ if (target instanceof Element && target.nodeName === 'PRE') {
+ if (
+ target.previousSibling === null ||
+ !(target.previousSibling instanceof Element) ||
+ !target.previousSibling.classList.contains(ClippyClassName)
+ ) {
/**
*
*
@@ -322,10 +262,26 @@ function attachCopySnippet() {
*
*
*/
- if (target.parentNode) target.parentNode.insertBefore(clippy, target)
+ if (target.parentNode) {
+ const clippyElement = await renderReact(
+ React.createElement(Clippy, { codeSnippetElement: target }),
+ )
+ if (clippyElement instanceof HTMLElement) {
+ target.parentNode.insertBefore(clippyElement, target)
+ }
+ }
}
}
- }),
+ }
+ readmeElement.addEventListener('mouseover', mouseOverCallback)
+ return () => {
+ readmeElement.removeEventListener('mouseover', mouseOverCallback)
+ const buttons = document.querySelectorAll(`.${ClippyClassName}`)
+ buttons.forEach(button => {
+ button.parentElement?.removeChild(button)
+ })
+ }
+ },
() => {
const plainReadmeSelector = '.repository-content div#readme .plain'
$(plainReadmeSelector, undefined, () =>
@@ -341,14 +297,14 @@ function attachCopySnippet() {
/**
* focus to side bar, user will be able to manipulate it with keyboard
*/
-function focusFileExplorer() {
+export function focusFileExplorer() {
const sideBarContentSelector = '.gitako-side-bar .file-explorer'
$(sideBarContentSelector, sideBarElement => {
if (sideBarElement instanceof HTMLElement) sideBarElement.focus()
})
}
-function focusSearchInput() {
+export function focusSearchInput() {
const searchInputSelector = '.search-input'
$(searchInputSelector, searchInputElement => {
if (
@@ -360,44 +316,10 @@ function focusSearchInput() {
})
}
-/**
- * a combination of few above functions
- */
-function decorateGitHubPageContent({
- copyFileButton,
- copySnippetButton,
-}: {
- copyFileButton: boolean
- copySnippetButton: boolean
-}) {
- if (copyFileButton) attachCopyFileBtn()
- if (copySnippetButton) attachCopySnippet()
-}
-
-function mountTopProgressBar() {
+export function mountTopProgressBar() {
NProgress.start()
}
-function unmountTopProgressBar() {
+export function unmountTopProgressBar() {
NProgress.done()
}
-
-export default {
- loadWithPJAX,
- attachCopyFileBtn,
- attachCopySnippet,
- decorateGitHubPageContent,
- focusSearchInput,
- focusFileExplorer,
- getCurrentPageType,
- getRepoPageType,
- insertLogoMountPoint,
- markGitakoReadyState,
- setBodyIndent,
- scrollToRepoContent,
- mountTopProgressBar,
- unmountTopProgressBar,
- isInCodePage,
- getBranches,
- getCurrentBranch,
-}
diff --git a/src/utils/GitHubHelper.ts b/src/utils/GitHubHelper.ts
index 42cbf54..140e8b8 100644
--- a/src/utils/GitHubHelper.ts
+++ b/src/utils/GitHubHelper.ts
@@ -70,7 +70,11 @@ type RepoMetaData = {
}
}
-async function getRepoMeta({ userName, repoName, accessToken }: MetaData): Promise {
+export async function getRepoMeta({
+ userName,
+ repoName,
+ accessToken,
+}: MetaData): Promise {
const url = `https://api.github.com/repos/${userName}/${repoName}`
return await request(url, { accessToken })
}
@@ -91,7 +95,7 @@ export type TreeData = {
url: string
}
-async function getTreeData({
+export async function getTreeData({
userName,
repoName,
branchName,
@@ -109,7 +113,7 @@ export type BlobData = {
url: string
}
-async function getBlobData({
+export async function getBlobData({
userName,
repoName,
accessToken,
@@ -121,17 +125,10 @@ async function getBlobData({
return await request(url, { accessToken })
}
-function getUrlForRedirect(
+export function getUrlForRedirect(
{ userName, repoName, branchName }: MetaData,
type = 'blob',
path?: string,
) {
return `https://github.com/${userName}/${repoName}/${type}/${branchName}/${path}`
}
-
-export default {
- getRepoMeta,
- getTreeData,
- getBlobData,
- getUrlForRedirect,
-}
diff --git a/src/utils/URLHelper.ts b/src/utils/URLHelper.ts
index 2e2cac6..6d515bb 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(): MetaData & { path: string[] } {
+export function parse(): MetaData & { path: string[] } {
const { pathname } = window.location
let [
,
@@ -19,12 +19,12 @@ function parse(): MetaData & { path: string[] } {
}
}
-function parseSHA() {
+export function parseSHA() {
const { type, path } = parse()
return type === 'blob' || type === 'tree' ? path[0] : undefined
}
-function isInRepoPage() {
+export function isInRepoPage() {
const repoHeaderSelector = '.repohead'
return Boolean(document.querySelector(repoHeaderSelector))
}
@@ -38,7 +38,7 @@ const TYPES = {
// TODO: record more types
}
-function isInCodePage(metaData: MetaData = {}) {
+export function isInCodePage(metaData: MetaData = {}) {
const mergedRepo = { ...parse(), ...metaData }
const { type, branchName } = mergedRepo
return Boolean(
@@ -57,7 +57,7 @@ function isCompleteCommitSHA(sha?: string) {
return typeof sha === 'string' && /^[abcdef0-9]{40}$/i.test(sha)
}
-function getCurrentPath(branchName = '') {
+export function getCurrentPath(branchName = '') {
const { path, type } = parse()
if (type === 'blob' || type === 'tree') {
if (isCommitPath(path)) {
@@ -90,11 +90,3 @@ function getCurrentPath(branchName = '') {
}
return []
}
-
-export default {
- getCurrentPath,
- isInRepoPage,
- isInCodePage,
- parse,
- parseSHA,
-}
diff --git a/src/utils/configHelper.ts b/src/utils/configHelper.ts
index 9a84d91..470a005 100644
--- a/src/utils/configHelper.ts
+++ b/src/utils/configHelper.ts
@@ -1,4 +1,4 @@
-import storageHelper from 'utils/storageHelper'
+import * as storageHelper from 'utils/storageHelper'
export type Config = {
sideBarWidth: number
@@ -20,7 +20,7 @@ export enum configKeys {
intelligentToggle = 'intelligentToggle',
}
-const defaultConfigs: Config = {
+export const defaultConfigs: Config = {
sideBarWidth: 260,
shortcut: undefined,
access_token: undefined,
@@ -43,27 +43,10 @@ function applyDefaultConfigs(configs: Config) {
)
}
-async function getAll(): Promise {
+export async function get(): Promise {
return applyDefaultConfigs(await storageHelper.get(configKeyArray))
}
-async function getOne(key: configKeys) {
- return (await getAll())[key]
-}
-
-async function setAll(partialConfig: Partial) {
+export async function set(partialConfig: Partial) {
return await storageHelper.set(partialConfig)
}
-
-async function setOne(key: configKeys, value: any) {
- return await setAll({
- [key]: value,
- })
-}
-
-export default {
- getAll,
- getOne,
- setAll,
- setOne,
-}
diff --git a/src/utils/cx.ts b/src/utils/cx.ts
index be8031f..179b6d3 100644
--- a/src/utils/cx.ts
+++ b/src/utils/cx.ts
@@ -1,7 +1,7 @@
/**
* cx('class1', { class2: true, class3: false }) --> 'class1 class2'
*/
-export default function cx(...classNames: any[]): string {
+export function cx(...classNames: any[]): string {
return classNames
.filter(Boolean)
.map(className => {
diff --git a/src/utils/general.ts b/src/utils/general.ts
index a642357..0b4e218 100644
--- a/src/utils/general.ts
+++ b/src/utils/general.ts
@@ -1,16 +1,15 @@
+import { ReactElement } from 'react'
+import * as ReactDOM from 'react-dom'
import { TreeNode } from './VisibleNodesGenerator'
export function pick(source: T, keys: string[]): Partial {
if (keys && typeof keys === 'object') {
- return (Array.isArray(keys) ? keys : Object.keys(keys)).reduce(
- (copy, key) => {
- if (key in source) {
- copy[key as keyof T] = source[key as keyof T]
- }
- return copy
- },
- {} as Partial,
- )
+ return (Array.isArray(keys) ? keys : Object.keys(keys)).reduce((copy, key) => {
+ if (key in source) {
+ copy[key as keyof T] = source[key as keyof T]
+ }
+ return copy
+ }, {} as Partial)
}
return {} as Partial
}
@@ -114,20 +113,22 @@ export function parseURLSearch(search: string = window.location.search) {
}
export async function JSONRequest(url: string, data: any, extra: RequestInit = { method: 'post' }) {
- return (await fetch(url, {
- mode: 'cors',
- cache: 'no-cache',
- credentials: 'same-origin',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- },
- redirect: 'follow',
- referrerPolicy: 'no-referrer',
- method: extra.method || 'post',
- body: JSON.stringify(data),
- ...extra,
- })).json()
+ return (
+ await fetch(url, {
+ mode: 'cors',
+ cache: 'no-cache',
+ credentials: 'same-origin',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
+ redirect: 'follow',
+ referrerPolicy: 'no-referrer',
+ method: extra.method || 'post',
+ body: JSON.stringify(data),
+ ...extra,
+ })
+ ).json()
}
export function searchKeyToRegexps(searchKey: string) {
@@ -141,3 +142,12 @@ export function searchKeyToRegexps(searchKey: string) {
return [/$^/] // matching nothing if failed transforming regexp
}
}
+
+export async function renderReact(element: ReactElement) {
+ return new Promise(resolve => {
+ const mount = document.createElement('div')
+ ReactDOM.render(element, mount, () => {
+ resolve(mount.childNodes[0])
+ })
+ })
+}
diff --git a/src/utils/hooks.ts b/src/utils/hooks.ts
index 578ae1f..fe55030 100644
--- a/src/utils/hooks.ts
+++ b/src/utils/hooks.ts
@@ -47,3 +47,35 @@ export function usePrevious(newValue: T) {
})
return previousRef.current
}
+
+export function useStates(
+ initialState: S | (() => S),
+): { val: S; set: React.Dispatch> } {
+ const [val, set] = React.useState(initialState)
+ return { val, set }
+}
+
+export function useAsyncMemo(
+ factory: (dependencies: D) => T | Promise,
+ deps: D,
+ initialValue: T,
+): T {
+ const firstTime = React.useRef(true)
+ const state = useStates(() => initialValue)
+ React.useEffect(() => {
+ if (firstTime.current) firstTime.current = false
+ Promise.resolve(factory(deps)).then(consumed => state.set(() => consumed))
+ }, deps)
+ return state.val
+}
+
+export function useDidUpdate(effect: React.EffectCallback, deps?: React.DependencyList) {
+ const firstTime = React.useRef(true)
+ React.useEffect(() => {
+ if (firstTime.current) {
+ firstTime.current = false
+ return
+ }
+ return effect()
+ }, deps)
+}
diff --git a/src/utils/keyHelper.ts b/src/utils/keyHelper.ts
index a06850c..3394516 100644
--- a/src/utils/keyHelper.ts
+++ b/src/utils/keyHelper.ts
@@ -36,7 +36,7 @@ function parseKeyCode(code: string) {
return code.toLowerCase().replace(/^control$/, 'ctrl')
}
-function parseEvent(e: KeyboardEvent | React.KeyboardEvent) {
+export function parseEvent(e: KeyboardEvent | React.KeyboardEvent) {
const { altKey: alt, shiftKey: shift, metaKey: meta, ctrlKey: ctrl } = e
try {
const code = parseKeyCode(e.key)
@@ -57,7 +57,3 @@ function parseEvent(e: KeyboardEvent | React.KeyboardEvent) {
throw new Error(`Error parse keyboard event: ${serializedKeyData}`)
}
}
-
-export default {
- parseEvent,
-}
diff --git a/src/utils/storageHelper.ts b/src/utils/storageHelper.ts
index 6f62d20..8c52640 100644
--- a/src/utils/storageHelper.ts
+++ b/src/utils/storageHelper.ts
@@ -1,14 +1,9 @@
const localStorage = browser.storage.local
-function get(mapping: string[] | null): Promise {
+export function get(mapping: string[] | null): Promise {
return localStorage.get(mapping || undefined)
}
-function set(value: any): Promise {
+export function set(value: any): Promise {
return localStorage.set(value)
}
-
-export default {
- get,
- set,
-}
diff --git a/src/utils/treeParser.ts b/src/utils/treeParser.ts
index 0e64998..db9dd8d 100644
--- a/src/utils/treeParser.ts
+++ b/src/utils/treeParser.ts
@@ -1,4 +1,4 @@
-import GitHubHelper, { MetaData, TreeData } from 'utils/GitHubHelper'
+import { getUrlForRedirect, MetaData, TreeData } from 'utils/GitHubHelper'
import { TreeNode } from './VisibleNodesGenerator'
interface RawItem {
@@ -36,7 +36,7 @@ function findGitModules(root: TreeNode) {
return null
}
-function parse(treeData: TreeData, metaData: MetaData) {
+export function parse(treeData: TreeData, metaData: MetaData) {
const { tree } = treeData
// nodes are created from items and put onto tree
@@ -70,7 +70,7 @@ function parse(treeData: TreeData, metaData: MetaData) {
name: item.path && item.path.replace(/^.*\//, ''),
url:
item.url && item.type && item.path
- ? GitHubHelper.getUrlForRedirect(metaData, item.type, item.path)
+ ? getUrlForRedirect(metaData, item.type, item.path)
: null,
contents: item.type === 'tree' ? [] : null,
} as TreeNode
@@ -88,7 +88,3 @@ function parse(treeData: TreeData, metaData: MetaData) {
root: sortFoldersToFront(root),
}
}
-
-export default {
- parse,
-}
diff --git a/src/utils/visibleNodesGenerator.ts b/src/utils/visibleNodesGenerator.ts
index 5212889..bc8dc9e 100644
--- a/src/utils/visibleNodesGenerator.ts
+++ b/src/utils/visibleNodesGenerator.ts
@@ -202,7 +202,7 @@ type Options = {
compress?: boolean
}
-export default class VisibleNodesGenerator {
+export class VisibleNodesGenerator {
l1: L1
l2: L2
l3: L3
diff --git a/tsconfig.json b/tsconfig.json
index e181701..08918c4 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,5 +1,4 @@
{
- "files": ["src/content.tsx"],
"compilerOptions": {
"target": "es2016",
"outDir": "dist",
diff --git a/webpack.config.js b/webpack.config.js
index 02c11e1..a3d34fd 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -38,15 +38,12 @@ if (analyse) {
}
const IN_PRODUCTION_MODE = process.env.NODE_ENV === 'production'
-if (IN_PRODUCTION_MODE) {
- plugins.push(
- new webpack.DefinePlugin({
- 'process.env': {
- NODE_ENV: JSON.stringify('production'),
- },
- }),
- )
-}
+plugins.push(
+ new webpack.DefinePlugin({
+ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
+ 'process.env.VERSION': JSON.stringify(process.env.VERSION),
+ }),
+)
module.exports = {
entry: {
diff --git a/yarn.lock b/yarn.lock
index 4ab28d4..6f77807 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -304,6 +304,14 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
+"@babel/plugin-proposal-optional-chaining@^7.6.0":
+ version "7.6.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.6.0.tgz#e9bf1f9b9ba10c77c033082da75f068389041af8"
+ integrity sha512-kj4gkZ6qUggkprRq3Uh5KP8XnE1MdIO0J7MhdDX8+rAbB6dJ2UrensGIS+0NPZAaaJ1Vr0PN6oLUgXMU1uMcSg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/plugin-syntax-optional-chaining" "^7.2.0"
+
"@babel/plugin-proposal-unicode-property-regex@^7.4.4":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz#501ffd9826c0b91da22690720722ac7cb1ca9c78"
@@ -355,6 +363,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
+"@babel/plugin-syntax-optional-chaining@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.2.0.tgz#a59d6ae8c167e7608eaa443fda9fa8fa6bf21dff"
+ integrity sha512-HtGCtvp5Uq/jH/WNUPkK6b7rufnCPLLlDAFN7cmACoIjaOOiXxUt3SswU5loHqrhtqTsa/WoLQ1OQ1AGuZqaWA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
"@babel/plugin-syntax-typescript@^7.2.0":
version "7.3.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.3.3.tgz#a7cc3f66119a9f7ebe2de5383cce193473d65991"