- {state !== 'done' ? (
-
- ) : (
- visibleNodes &&
- renderNodeContext && (
- <>
-
- {searched && visibleNodes.nodes.length === 0 && (
- <>
-
- No results found.
-
- {defer && (
-
- Lazy mode is ON. Search results are limited to loaded folders.
-
- )}
- >
- )}
-
- {({ width = 0, height = 0 }) => (
-
- )}
-
- >
- )
- )}
+
+ {run(() => {
+ switch (state) {
+ case 'tree-loading':
+ return
+ case 'tree-rendering':
+ return
+ case 'tree-rendered':
+ return (
+ visibleNodes &&
+ renderNodeContext && (
+ <>
+
+ {searched && visibleNodes.nodes.length === 0 && (
+ <>
+
+ No results found.
+
+ {defer && (
+
+ Lazy mode is ON. Search results are limited to loaded folders.
+
+ )}
+ >
+ )}
+
+ {({ width = 0, height = 0 }) => (
+
+ )}
+
+
+ {defer && (
+
+ )}
+ >
+ )
+ )
+ }
+ })}
)
}
RawFileExplorer.defaultProps = {
freeze: false,
- state: 'rendering',
searchKey: '',
visibleNodes: null,
}
diff --git a/src/components/Gitako.tsx b/src/components/Gitako.tsx
index 7c68258..13039b3 100644
--- a/src/components/Gitako.tsx
+++ b/src/components/Gitako.tsx
@@ -1,15 +1,25 @@
import { SideBar } from 'components/SideBar'
-import { ConfigsContext, ConfigsContextWrapper } from 'containers/ConfigsContext'
+import { ConfigsContextWrapper } from 'containers/ConfigsContext'
import * as React from 'react'
-import { ErrorBoundary } from './ErrorBoundary'
+import { ErrorBoundary } from '../containers/ErrorBoundary'
+import { StateBarErrorContextWrapper } from '../containers/ErrorContext'
+import { OAuthWrapper } from '../containers/OAuthWrapper'
+import { RepoContextWrapper } from '../containers/RepoContext'
+import { StateBarStateContextWrapper } from '../containers/SideBarState'
export function Gitako() {
return (
-
- {configContext => configContext && }
-
+
+
+
+
+
+
+
+
+
)
diff --git a/src/components/IIFC.tsx b/src/components/IIFC.tsx
new file mode 100644
index 0000000..6a57b87
--- /dev/null
+++ b/src/components/IIFC.tsx
@@ -0,0 +1,8 @@
+import * as React from 'react'
+
+// I tried to install `react-iifc` but that causes TS build errors for unknown reason
+// So here the duplicated code is
+
+export function IIFC({ children }: { children(): React.ReactNode }) {
+ return <>{children()}>
+}
diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx
index ce98f05..d3f8eb4 100644
--- a/src/components/Icon.tsx
+++ b/src/components/Icon.tsx
@@ -11,135 +11,76 @@ import {
GearIcon as Gear,
GrabberIcon as Grabber,
HourglassIcon as Hourglass,
- Icon as OcticonIcon,
IconProps,
MarkdownIcon as Markdown,
OctofaceIcon as Octoface,
+ PinIcon as Pin,
ReplyIcon as Reply,
SearchIcon as Search,
+ TabIcon as Tab,
XIcon as X,
} from '@primer/octicons-react'
import * as React from 'react'
import { cx } from 'utils/cx'
-function getSVGIconComponent(
- type: string,
-): {
- IconComponent: OcticonIcon
- name: string
-} {
- switch (type) {
- case 'comment':
- return {
- IconComponent: Comment,
- name: 'Comment',
- }
- case 'search':
- return {
- IconComponent: Search,
- name: 'Search',
- }
- case 'loading':
- return {
- IconComponent: Clock,
- name: 'Clock',
- }
- case 'hourglass':
- return {
- IconComponent: Hourglass,
- name: 'Hourglass',
- }
- case 'submodule':
- return {
- IconComponent: Submodule,
- name: 'Submodule',
- }
- case 'grabber':
- return {
- IconComponent: Grabber,
- name: 'Grabber',
- }
- case 'octoface':
- return {
- IconComponent: Octoface,
- name: 'Octoface',
- }
- case 'chevron-down':
- return {
- IconComponent: ChevronDown,
- name: 'ChevronDown',
- }
- case 'x':
- return {
- IconComponent: X,
- name: 'X',
- }
- case 'gear':
- return {
- IconComponent: Gear,
- name: 'Gear',
- }
- case 'folder':
- return {
- IconComponent: ChevronRight,
- name: 'ChevronRight',
- }
- case 'go-to':
- return {
- IconComponent: Reply,
- name: 'Reply',
- }
- // not supported in octicon v2 yet
- // case '.pdf':
- // return {
- // IconComponent: FilePdf,
- // name: 'FilePdf',
- // }
- case '.zip':
- case '.rar':
- case '.7z':
- return {
- IconComponent: FileZip,
- name: 'FileZip',
- }
- case '.md':
- return {
- IconComponent: Markdown,
- name: 'Markdown',
- }
- case '.png':
- case '.jpg':
- case '.gif':
- case '.bmp':
- return {
- IconComponent: FileMedia,
- name: 'FileMedia',
- }
- case '.js':
- case '.jsx':
- case '.ts':
- case '.tsx':
- case '.es6':
- case '.coffee':
- case '.css':
- case '.less':
- case '.scss':
- case '.sass':
- return {
- IconComponent: FileCode,
- name: 'FileCode',
- }
- // TODO: adapt to more file types
- // case '': return FileBinary
- // case '': return FileSubmodule
- // case '': return FileSymlinkDirectory
- // case '': return FileSymlinkFile
- default:
- return {
- IconComponent: File,
- name: 'File',
- }
- }
+const iconToComponentMap = {
+ Search,
+ Clock,
+ Comment,
+ Hourglass,
+ Submodule,
+ Grabber,
+ Octoface,
+ ChevronDown,
+ X,
+ Gear,
+ ChevronRight,
+ Reply,
+ FileZip,
+ Markdown,
+ FileMedia,
+ FileCode,
+ File,
+ Pin,
+ Tab,
+}
+
+const defaultIcon = 'File'
+const typeToIconComponentMap: {
+ [type: string]: keyof typeof iconToComponentMap
+} = {
+ search: 'Search',
+ loading: 'Clock',
+ hourglass: 'Hourglass',
+ submodule: 'Submodule',
+ grabber: 'Grabber',
+ octoface: 'Octoface',
+ comment: 'Comment',
+ x: 'X',
+ pin: 'Pin',
+ tab: 'Tab',
+ gear: 'Gear',
+ folder: 'ChevronRight',
+ 'chevron-down': 'ChevronDown',
+ 'go-to': 'Reply',
+ '.zip': 'FileZip',
+ '.rar': 'FileZip',
+ '.7z': 'FileZip',
+ '.md': 'Markdown',
+ '.png': 'FileMedia',
+ '.jpg': 'FileMedia',
+ '.gif': 'FileMedia',
+ '.bmp': 'FileMedia',
+ '.js': 'FileCode',
+ '.jsx': 'FileCode',
+ '.ts': 'FileCode',
+ '.tsx': 'FileCode',
+ '.es6': 'FileCode',
+ '.coffee': 'FileCode',
+ '.css': 'FileCode',
+ '.less': 'FileCode',
+ '.scss': 'FileCode',
+ '.sass': 'FileCode',
}
type Props = {
@@ -157,7 +98,8 @@ export const Icon = React.memo(function Icon({
}: Props) {
let children: React.ReactNode = null
if (!placeholder) {
- const { name, IconComponent } = getSVGIconComponent(type)
+ const name = typeToIconComponentMap[type] || defaultIcon
+ const IconComponent = iconToComponentMap[name]
children =
}
return (
diff --git a/src/components/Node.tsx b/src/components/Node.tsx
index 5fa7942..97adbde 100644
--- a/src/components/Node.tsx
+++ b/src/components/Node.tsx
@@ -2,7 +2,7 @@ import { useConfigs } from 'containers/ConfigsContext'
import * as React from 'react'
import { cx } from 'utils/cx'
import { OperatingSystems, os } from 'utils/general'
-import { getFileIconSrc, getFolderIconSrc } from '../utils/parseIconMapCSV'
+import { getFileIconSrc, getFolderIconSrc } from 'utils/parseIconMapCSV'
import { Icon } from './Icon'
function getIconType(node: TreeNode) {
diff --git a/src/components/ResizeHandler.tsx b/src/components/ResizeHandler.tsx
index 70e983a..177de70 100644
--- a/src/components/ResizeHandler.tsx
+++ b/src/components/ResizeHandler.tsx
@@ -1,14 +1,24 @@
import { Icon } from 'components/Icon'
import * as React from 'react'
-import { Size } from './Resizable'
+import { Size } from './SideBarBodyWrapper'
type Props = {
size: Size
onResize(size: Size): void
+ onResetSize?(): void
+ onResizeStateChange?(state: ResizeState): void
style?: React.CSSProperties
}
-export function HorizontalResizeHandler({ onResize, size, style }: Props) {
+type ResizeState = 'idle' | 'resizing'
+
+export function HorizontalResizeHandler({
+ onResize,
+ onResetSize,
+ onResizeStateChange,
+ size,
+ style,
+}: Props) {
const pointerDown = React.useRef(false)
const startX = React.useRef(0)
const baseSize = React.useRef(size)
@@ -22,6 +32,7 @@ export function HorizontalResizeHandler({ onResize, size, style }: Props) {
startX.current = clientX
pointerDown.current = true
baseSize.current = latestPropSize.current
+ onResizeStateChange?.('resizing')
}, [])
React.useEffect(() => {
@@ -39,6 +50,7 @@ export function HorizontalResizeHandler({ onResize, size, style }: Props) {
if (pointerDown.current) {
pointerDown.current = false
baseSize.current = latestPropSize.current
+ onResizeStateChange?.('idle')
}
}
window.addEventListener('mouseup', onPointerUp)
@@ -46,7 +58,12 @@ export function HorizontalResizeHandler({ onResize, size, style }: Props) {
}, [])
return (
-
+
)
diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx
index 50b3649..62bcf8c 100644
--- a/src/components/SideBar.tsx
+++ b/src/components/SideBar.tsx
@@ -2,106 +2,99 @@ import { AccessDeniedDescription } from 'components/AccessDeniedDescription'
import { FileExplorer } from 'components/FileExplorer'
import { MetaBar } from 'components/MetaBar'
import { Portal } from 'components/Portal'
-import { Resizable } from 'components/Resizable'
import { SettingsBar } from 'components/settings/SettingsBar'
+import { SideBarBodyWrapper } from 'components/SideBarBodyWrapper'
import { ToggleShowButton } from 'components/ToggleShowButton'
-import { connect } from 'driver/connect'
-import { SideBarCore } from 'driver/core'
-import { ConnectorState, Props } from 'driver/core/SideBar'
+import { useConfigs } from 'containers/ConfigsContext'
import { platform } from 'platforms'
import { useGitHubAttachCopyFileButton, useGitHubAttachCopySnippetButton } from 'platforms/GitHub'
import * as React from 'react'
import { cx } from 'utils/cx'
-import { parseURLSearch, run } from 'utils/general'
+import * as DOMHelper from 'utils/DOMHelper'
+import { run } from 'utils/general'
+import { useCatchNetworkError } from 'utils/hooks/useCatchNetworkError'
+import { useLoadedContext } from 'utils/hooks/useLoadedContext'
import { loadWithPJAX, useOnPJAXDone, usePJAX } from 'utils/hooks/usePJAX'
import { useProgressBar } from 'utils/hooks/useProgressBar'
import { useStateIO } from 'utils/hooks/useStateIO'
-import * as keyHelper from 'utils/keyHelper'
+import { SideBarErrorContext } from '../containers/ErrorContext'
+import { RepoContext } from '../containers/RepoContext'
+import { SideBarStateContext } from '../containers/SideBarState'
+import { Theme } from '../containers/Theme'
+import { useToggleSideBarWithKeyboard } from '../utils/hooks/useToggleSideBarWithKeyboard'
import { Icon } from './Icon'
+import { IIFC } from './IIFC'
import { LoadingIndicator } from './LoadingIndicator'
-import { Theme } from './Theme'
-const RawGitako: React.FC
= function RawGitako(props) {
- const {
- metaData,
- treeData,
- state,
- defer,
- error,
- shouldShow,
- showSettings,
- logoContainerElement,
- toggleShowSideBar,
- toggleShowSettings,
- configContext,
- } = props
+export function SideBar() {
+ const metaData = React.useContext(RepoContext)
+ const state = useLoadedContext(SideBarStateContext).value
+ const configContext = useConfigs()
const accessToken = configContext.value.accessToken || ''
const [baseSize] = React.useState(() => configContext.value.sideBarWidth)
+ const $showSettings = useStateIO(false)
+ const showSettings = $showSettings.value
+ const toggleShowSettings = React.useCallback(() => $showSettings.onChange(show => !show), [])
+
+ const $logoContainerElement = useStateIO(null)
+
+ const hasMetaData = state !== 'disabled' // will be true since retrieving data, cannot use Boolean(metaData)
React.useEffect(() => {
- run(async function () {
- if (!accessToken) {
- const accessToken = await trySetUpAccessTokenWithCode()
- if (accessToken) configContext.onChange({ accessToken })
- }
- })
- }, [])
-
- /**
- * Catch unexpected PJAX, force trigger init on scope change.
- */
- const pageScope = useStateIO(platform.resolvePageScope?.())
- useOnPJAXDone(
- React.useCallback(
- () => pageScope.onChange(platform.resolvePageScope?.(metaData?.defaultBranchName)),
- [metaData?.defaultBranchName],
- ),
- )
- React.useEffect(() => {
- props.init()
- }, [accessToken, pageScope.value])
-
- React.useEffect(
- function attachKeyDown() {
- if (state === 'disabled' || !configContext.value.shortcut) return
-
- function onKeyDown(e: KeyboardEvent) {
- const keys = keyHelper.parseEvent(e)
- if (keys === configContext.value.shortcut) {
- toggleShowSideBar()
- }
- }
- window.addEventListener('keydown', onKeyDown)
- return () => window.removeEventListener('keydown', onKeyDown)
- },
- [toggleShowSideBar, state === 'disabled', configContext.value.shortcut],
- )
-
- const intelligentToggle = configContext.value.intelligentToggle
- // Hide sidebar when error due to auth but token is set #128
- const hideSidebarOnInvalidToken: boolean =
- intelligentToggle === null && Boolean(state === 'error-due-to-auth' && accessToken)
- React.useEffect(() => {
- if (hideSidebarOnInvalidToken) {
- props.setShouldShow(false)
+ if (hasMetaData) {
+ DOMHelper.markGitakoReadyState(true)
+ $showSettings.onChange(false)
+ $logoContainerElement.onChange(DOMHelper.insertLogoMountPoint())
} else {
- const shouldShow = intelligentToggle === null ? platform.shouldShow() : intelligentToggle
- props.setShouldShow(shouldShow)
+ DOMHelper.markGitakoReadyState(false)
}
- }, [intelligentToggle, hideSidebarOnInvalidToken, props.metaData])
+ }, [hasMetaData])
- const updateSideBarVisibility = React.useCallback(
- function updateSideBarVisibility() {
- if (hideSidebarOnInvalidToken) {
- props.setShouldShow(false)
- } else if (intelligentToggle === null) {
- props.setShouldShow(platform.shouldShow())
- }
+ const sidebarToggleMode = configContext.value.sidebarToggleMode
+ const intelligentToggle = configContext.value.intelligentToggle
+ const $shouldShow = useStateIO(intelligentToggle === null ? false : intelligentToggle)
+ const shouldShow = $shouldShow.value
+ React.useEffect(() => {
+ if (sidebarToggleMode === 'persistent') {
+ DOMHelper.setBodyIndent(shouldShow)
+ } else {
+ DOMHelper.setBodyIndent(false)
+ }
+
+ if (shouldShow) {
+ DOMHelper.focusFileExplorer() // TODO: verify if it works
+ }
+ }, [shouldShow, sidebarToggleMode])
+
+ // Save expand state on toggle if auto expand if not on
+ React.useEffect(() => {
+ if (intelligentToggle !== null) {
+ configContext.onChange({ intelligentToggle: shouldShow })
+ }
+ }, [shouldShow, intelligentToggle])
+
+ const error = useLoadedContext(SideBarErrorContext).value
+ // Lock shouldShow on error
+ React.useEffect(() => {
+ if (error && shouldShow) {
+ $shouldShow.onChange(false)
+ }
+ }, [error])
+
+ const setShowSideBar = React.useCallback(
+ (show: typeof $shouldShow.value) => {
+ if (!error) $shouldShow.onChange(show)
},
- [props.metaData?.branchName, intelligentToggle, hideSidebarOnInvalidToken],
+ [error],
)
- useOnPJAXDone(updateSideBarVisibility)
+
+ const toggleShowSideBar = React.useCallback(() => {
+ if (!error) $shouldShow.onChange(show => !show)
+ }, [error])
+ useToggleSideBarWithKeyboard(state, configContext, toggleShowSideBar)
+
+ useSetShouldShowOnPJAXDone(setShowSideBar)
useGitHubAttachCopyFileButton(configContext.value.copyFileButton)
useGitHubAttachCopySnippetButton(configContext.value.copySnippetButton)
@@ -109,81 +102,114 @@ const RawGitako: React.FC = function RawGitako(props) {
usePJAX()
useProgressBar()
+ // Hide sidebar when error due to auth but token is set #128
+ const hideSidebarOnInvalidToken: boolean =
+ intelligentToggle === null && Boolean(state === 'error-due-to-auth' && accessToken)
+ React.useEffect(() => {
+ if (hideSidebarOnInvalidToken) {
+ setShowSideBar(false)
+ }
+ }, [hideSidebarOnInvalidToken])
+
return (
-
- {!shouldShow && (
-
- )}
+
+ setShowSideBar(true) : undefined}
+ onClick={toggleShowSideBar}
+ />
-
+ setShowSideBar(false) : undefined}
+ >
-
-
- {(() => {
+
+ {run(() => {
switch (state) {
- case 'loading-meta':
+ case 'disabled':
+ return null
+ case 'getting-access-token':
+ return
+ case 'meta-loading':
return
- case 'loading-tree':
- return
- case 'idle':
+ case 'error-due-to-auth':
+ return
+ default:
return metaData ? (
<>
-
+
+ {() => (
+
+ )}
+
>
) : null
- case 'error-due-to-auth':
- return
}
- })()}
+ })}
-
+
-
+
)
}
-RawGitako.defaultProps = {
- shouldShow: false,
- showSettings: false,
- state: 'loading-meta',
-}
-
-export const SideBar = connect(SideBarCore)(RawGitako)
-
-async function trySetUpAccessTokenWithCode() {
- const search = parseURLSearch()
- if ('code' in search) {
- const accessToken = await platform.setOAuth(search.code)
- if (!accessToken) alert(`Gitako: The OAuth token has expired, please try again.`)
- window.history.replaceState(
- {},
- 'removed search param',
- window.location.pathname.replace(window.location.search, ''),
- )
- return accessToken
- }
+function useSetShouldShowOnPJAXDone(setShouldShow: (value: boolean) => void) {
+ const { intelligentToggle, sidebarToggleMode } = useConfigs().value
+ const updateSideBarVisibility = React.useCallback(() => {
+ if (intelligentToggle === null && sidebarToggleMode === 'persistent') {
+ setShouldShow(platform.shouldShow())
+ }
+ }, [intelligentToggle, sidebarToggleMode])
+ React.useEffect(() => {
+ updateSideBarVisibility()
+ }, [updateSideBarVisibility])
+ useOnPJAXDone(updateSideBarVisibility)
}
diff --git a/src/components/Resizable.tsx b/src/components/SideBarBodyWrapper.tsx
similarity index 61%
rename from src/components/Resizable.tsx
rename to src/components/SideBarBodyWrapper.tsx
index bc1a786..d0b1c10 100644
--- a/src/components/Resizable.tsx
+++ b/src/components/SideBarBodyWrapper.tsx
@@ -2,23 +2,31 @@ import { HorizontalResizeHandler } from 'components/ResizeHandler'
import { useConfigs } from 'containers/ConfigsContext'
import * as React from 'react'
import { useDebounce, useWindowSize } from 'react-use'
+import { defaultConfigs } from 'utils/config/helper'
import { cx } from 'utils/cx'
import { setResizingState } from 'utils/DOMHelper'
import * as features from 'utils/features'
-import { useCSSVariable } from './useCSSVariable'
+import { useCSSVariable } from 'utils/hooks/useCSSVariable'
export type Size = number
type Props = {
baseSize: Size
className?: string
+ onLeave?: React.HTMLAttributes['onMouseLeave']
}
const MINIMAL_CONTENT_VIEWPORT_WIDTH = 100
const MINIMAL_WIDTH = 240
-export function Resizable({ baseSize, className, children }: React.PropsWithChildren) {
+export function SideBarBodyWrapper({
+ baseSize,
+ className,
+ children,
+ onLeave,
+}: React.PropsWithChildren) {
const [size, setSize] = React.useState(baseSize)
const configContext = useConfigs()
+ const blockLeaveRef = React.useRef(false)
React.useEffect(() => {
setSize(baseSize)
@@ -47,10 +55,28 @@ export function Resizable({ baseSize, className, children }: React.PropsWithChil
else if (size < MINIMAL_WIDTH) setSize(MINIMAL_WIDTH)
else setSize(size)
}, [])
+
+ const onMouseLeave = React.useCallback(
+ e => {
+ if (blockLeaveRef.current) return
+ onLeave?.(e)
+ },
+ [onLeave],
+ )
+
return (
-
-
{children}
- {features.resize &&
}
+
+
{children}
+ {features.resize && (
+
setSize(defaultConfigs.sideBarWidth)}
+ onResizeStateChange={state => {
+ blockLeaveRef.current = state === 'resizing'
+ }}
+ size={size}
+ />
+ )}
)
}
diff --git a/src/components/SimpleToggleField.tsx b/src/components/SimpleToggleField.tsx
index 343aae0..7e0ec27 100644
--- a/src/components/SimpleToggleField.tsx
+++ b/src/components/SimpleToggleField.tsx
@@ -1,27 +1,28 @@
import { useConfigs } from 'containers/ConfigsContext'
import * as React from 'react'
-import { Config } from 'utils/configHelper'
+import { Config } from 'utils/config/helper'
import { Field } from './settings/Field'
-export type SimpleField = {
- key: keyof Config
+export type SimpleField
= {
+ key: Key
label: string
wikiLink?: string
tooltip?: string
description?: string
+ disabled?: boolean
overwrite?: {
- value: (value: T) => boolean
- onChange: (checked: boolean) => any
+ value: (value: Config[Key]) => boolean
+ onChange: (checked: boolean) => Config[Key]
}
}
-type Props = {
- field: SimpleField
+type Props = {
+ field: SimpleField
onChange?(): void
}
-export function SimpleToggleField({ field, onChange }: Props) {
+export function SimpleToggleField({ field, onChange }: Props) {
const { overwrite } = field
const configContext = useConfigs()
const value = configContext.value[field.key]
@@ -54,6 +55,7 @@ export function SimpleToggleField({ field, onChange }: Props) {
{
const enabled = e.currentTarget.checked
diff --git a/src/components/ToggleShowButton.tsx b/src/components/ToggleShowButton.tsx
index 4a07863..243e3c7 100644
--- a/src/components/ToggleShowButton.tsx
+++ b/src/components/ToggleShowButton.tsx
@@ -2,13 +2,16 @@ import iconSrc from 'assets/icons/Gitako.png'
import { useConfigs } from 'containers/ConfigsContext'
import * as React from 'react'
import { useDebounce, useWindowSize } from 'react-use'
+import { cx } from 'utils/cx'
import { Icon } from './Icon'
type Props = {
- error?: string
+ error?: string | null
+ className?: React.HTMLAttributes['className']
+ onHover?: React.HTMLAttributes['onMouseEnter']
} & Pick, 'onClick'>
-export function ToggleShowButton({ error, onClick }: Props) {
+export function ToggleShowButton({ error, className, onClick, onHover }: Props) {
const ref = React.useRef(null)
const config = useConfigs()
const [distance, setDistance] = React.useState(config.value.toggleButtonVerticalDistance)
@@ -35,10 +38,13 @@ export function ToggleShowButton({ error, onClick }: Props) {
const toggleIconMode = config.value.toggleButtonContent
return (
-
+
{
hideDragPreview(event)
diff --git a/src/components/settings/FileTreeSettings.tsx b/src/components/settings/FileTreeSettings.tsx
index 72c3ca7..e3cd87f 100644
--- a/src/components/settings/FileTreeSettings.tsx
+++ b/src/components/settings/FileTreeSettings.tsx
@@ -2,7 +2,7 @@ import { wikiLinks } from 'components/settings/SettingsBar'
import { SimpleToggleField } from 'components/SimpleToggleField'
import { useConfigs } from 'containers/ConfigsContext'
import * as React from 'react'
-import { Config } from 'utils/configHelper'
+import { Config } from 'utils/config/helper'
import { Option, SelectInput } from '../SelectInput'
import { Field } from './Field'
import { SettingsSection } from './SettingsSection'
@@ -54,7 +54,7 @@ export function FileTreeSettings(props: React.PropsWithChildren) {
})
}}
value={configContext.value.recursiveToggleFolder}
- >
+ />
diff --git a/src/components/settings/SettingsBar.tsx b/src/components/settings/SettingsBar.tsx
index 70b579e..6f84007 100644
--- a/src/components/settings/SettingsBar.tsx
+++ b/src/components/settings/SettingsBar.tsx
@@ -1,4 +1,4 @@
-import { Label, Link } from '@primer/components'
+import { Link } from '@primer/components'
import { Icon } from 'components/Icon'
import { VERSION } from 'env'
import { platform } from 'platforms'
@@ -21,7 +21,6 @@ export const wikiLinks = {
}
type Props = {
- defer?: boolean
activated: boolean
toggleShowSettings: () => void
}
@@ -30,7 +29,7 @@ function SettingsBarContent() {
const useReloadHint = useStateIO('')
const { value: reloadHint } = useReloadHint
- const moreFields: SimpleField[] =
+ const moreFields: SimpleField<'copyFileButton' | 'copySnippetButton'>[] =
platform === GitHub
? [
{
@@ -78,7 +77,7 @@ function SettingsBarContent() {
}
export function SettingsBar(props: Props) {
- const { defer, toggleShowSettings, activated } = props
+ const { toggleShowSettings, activated } = props
return (
{activated &&
}
@@ -93,15 +92,6 @@ export function SettingsBar(props: Props) {
{VERSION}
- {defer && (
-
- )}
{activated ? (
diff --git a/src/components/settings/SidebarSettings.tsx b/src/components/settings/SidebarSettings.tsx
index 3ac1ed8..69b7955 100644
--- a/src/components/settings/SidebarSettings.tsx
+++ b/src/components/settings/SidebarSettings.tsx
@@ -3,7 +3,7 @@ import { Option, SelectInput } from 'components/SelectInput'
import { SimpleToggleField } from 'components/SimpleToggleField'
import { useConfigs } from 'containers/ConfigsContext'
import * as React from 'react'
-import { Config } from 'utils/configHelper'
+import { Config } from 'utils/config/helper'
import { friendlyFormatShortcut } from 'utils/general'
import { useStateIO } from 'utils/hooks/useStateIO'
import * as keyHelper from 'utils/keyHelper'
@@ -88,16 +88,21 @@ export function SidebarSettings(props: React.PropsWithChildren) {
})
}}
value={configContext.value.toggleButtonContent}
- >
+ />
enabled === null,
+ value: enabled =>
+ configContext.value.sidebarToggleMode === 'float' ? false : enabled === null,
onChange: checked => (checked ? null : true),
},
}}
diff --git a/src/containers/ConfigsContext.tsx b/src/containers/ConfigsContext.tsx
index c5291b6..4710a83 100644
--- a/src/containers/ConfigsContext.tsx
+++ b/src/containers/ConfigsContext.tsx
@@ -1,6 +1,5 @@
import * as React from 'react'
-import * as configsHelper from 'utils/configHelper'
-import { Config } from 'utils/configHelper'
+import { Config, configHelper } from 'utils/config/helper'
type Props = {}
@@ -12,19 +11,19 @@ export const ConfigsContext = React.createContext(null)
export function ConfigsContextWrapper(props: React.PropsWithChildren) {
const [configs, setConfigs] = React.useState(null)
React.useEffect(() => {
- configsHelper.get().then(setConfigs)
+ configHelper.get().then(setConfigs)
}, [])
const onChange = React.useCallback(
(updatedConfigs: Partial) => {
const mergedConfigs = { ...configs, ...updatedConfigs } as Config
- configsHelper.set(mergedConfigs)
+ configHelper.set(mergedConfigs)
setConfigs(mergedConfigs)
},
[configs, setConfigs],
)
if (configs === null) return null
return (
-
+
{props.children}
)
diff --git a/src/components/ErrorBoundary.tsx b/src/containers/ErrorBoundary.tsx
similarity index 100%
rename from src/components/ErrorBoundary.tsx
rename to src/containers/ErrorBoundary.tsx
diff --git a/src/containers/ErrorContext.tsx b/src/containers/ErrorContext.tsx
new file mode 100644
index 0000000..53d9455
--- /dev/null
+++ b/src/containers/ErrorContext.tsx
@@ -0,0 +1,12 @@
+import * as React from 'react'
+import { useStateIO } from 'utils/hooks/useStateIO'
+
+export type SideBarErrorContextShape = IO
+
+export const SideBarErrorContext = React.createContext(null)
+
+export function StateBarErrorContextWrapper({ children }: React.PropsWithChildren<{}>) {
+ const $error = useStateIO(null)
+
+ return {children}
+}
diff --git a/src/containers/OAuthWrapper.tsx b/src/containers/OAuthWrapper.tsx
new file mode 100644
index 0000000..72a058b
--- /dev/null
+++ b/src/containers/OAuthWrapper.tsx
@@ -0,0 +1,65 @@
+import { useConfigs } from 'containers/ConfigsContext'
+import { platform } from 'platforms'
+import * as React from 'react'
+import { parseURLSearch, run } from 'utils/general'
+import { useLoadedContext } from 'utils/hooks/useLoadedContext'
+import { useStateIO } from 'utils/hooks/useStateIO'
+import { SideBarStateContext } from './SideBarState'
+
+/**
+ * Setup access token before sending other requests
+ */
+export function OAuthWrapper({ children }: React.PropsWithChildren<{}>) {
+ const running = useGetAccessToken()
+ const $state = useLoadedContext(SideBarStateContext)
+
+ const firstRenderRef = React.useRef(true)
+ React.useEffect(() => {
+ if (running) {
+ $state.onChange('getting-access-token')
+ } else if (firstRenderRef.current) {
+ firstRenderRef.current = false
+ } else {
+ $state.onChange('after-getting-access-token')
+ }
+ }, [running])
+
+ // block children rendering on the first render if setting token
+ if (running && $state.value !== 'getting-access-token') return null
+ return <>{children}>
+}
+
+function useGetAccessToken() {
+ const $block = useStateIO(() => Boolean(getCodeSearchParam()))
+ const configContext = useConfigs()
+ const { accessToken } = configContext.value
+ React.useEffect(() => {
+ run(async function () {
+ const code = getCodeSearchParam()
+ if (code && !accessToken) {
+ const accessToken = await getAccessTokenWithCode(code)
+ if (accessToken) configContext.onChange({ accessToken })
+ }
+ $block.onChange(false)
+ })
+ }, [])
+
+ return $block.value
+}
+
+function getCodeSearchParam() {
+ return parseURLSearch().get('code')
+}
+
+async function getAccessTokenWithCode(code: string) {
+ const accessToken = await platform.setOAuth(code)
+ if (!accessToken) alert(`Gitako: The OAuth token may have expired, please try again.`)
+ const search = parseURLSearch()
+ search.delete('code')
+ window.history.replaceState(
+ {},
+ 'removed search param',
+ window.location.pathname.replace(window.location.search, '?' + search.toString()),
+ )
+ return accessToken
+}
diff --git a/src/containers/RepoContext.tsx b/src/containers/RepoContext.tsx
new file mode 100644
index 0000000..3e7004a
--- /dev/null
+++ b/src/containers/RepoContext.tsx
@@ -0,0 +1,107 @@
+import { useConfigs } from 'containers/ConfigsContext'
+import { platform } from 'platforms'
+import * as React from 'react'
+import { useEffectOnSerializableUpdates } from 'utils/hooks/useEffectOnSerializableUpdates'
+import { useLoadedContext } from 'utils/hooks/useLoadedContext'
+import { useOnPJAXDone } from 'utils/hooks/usePJAX'
+import { useStateIO } from 'utils/hooks/useStateIO'
+import { useCatchNetworkError } from '../utils/hooks/useCatchNetworkError'
+import { SideBarStateContext } from './SideBarState'
+
+export const RepoContext = React.createContext(null)
+
+export function RepoContextWrapper({ children }: React.PropsWithChildren<{}>) {
+ const partialMetaData = usePartialMetaData()
+ const defaultBranch = useDefaultBranch(partialMetaData)
+ const metaData = useMetaData(partialMetaData, defaultBranch)
+
+ return {children}
+}
+
+function resolvePartialMetaData() {
+ const partialMetaData = platform.resolvePartialMetaData()
+ if (partialMetaData) {
+ const { userName, repoName, type } = partialMetaData
+ return {
+ userName,
+ repoName,
+ type: type === 'pull' ? type : undefined,
+ }
+ } else {
+ return partialMetaData
+ }
+}
+
+function usePartialMetaData(): PartialMetaData | null {
+ const $state = useLoadedContext(SideBarStateContext)
+ const isGettingAccessToken = $state.value === 'getting-access-token' // will be false after getting access token and trigger meta-resolve progress
+ // sync along URL and DOM
+ const $partialMetaData = useStateIO(isGettingAccessToken ? null : resolvePartialMetaData)
+ const $committedPartialMetaData = useStateIO($partialMetaData.value)
+ const setPartialMetaData = () => $partialMetaData.onChange(resolvePartialMetaData())
+ React.useEffect(() => {
+ if (!isGettingAccessToken) setPartialMetaData()
+ }, [isGettingAccessToken])
+ useOnPJAXDone(setPartialMetaData)
+ useEffectOnSerializableUpdates(
+ $partialMetaData.value,
+ JSON.stringify,
+ $committedPartialMetaData.onChange,
+ )
+ React.useEffect(() => {
+ if (!$partialMetaData.value && !isGettingAccessToken) {
+ $state.onChange('disabled')
+ }
+ }, [$partialMetaData.value])
+ return $committedPartialMetaData.value
+}
+
+function useBranchName(): MetaData['branchName'] | null {
+ // sync along URL and DOM
+ const $branchName = useStateIO(() => platform.resolvePartialMetaData()?.branchName || null)
+ useOnPJAXDone(() => $branchName.onChange(platform.resolvePartialMetaData()?.branchName || null))
+ return $branchName.value
+}
+
+function useDefaultBranch(partialMetaData: PartialMetaData | null) {
+ const { accessToken } = useConfigs().value
+ const $state = useLoadedContext(SideBarStateContext)
+ const $defaultBranch = useStateIO(null)
+ const catchNetworkError = useCatchNetworkError()
+ React.useEffect(() => {
+ catchNetworkError(async () => {
+ if (!partialMetaData) return
+ $state.onChange('meta-loading')
+
+ const defaultBranch = await platform.getDefaultBranchName(partialMetaData, accessToken)
+ $defaultBranch.onChange(defaultBranch)
+ })
+ }, [partialMetaData, accessToken])
+ return $defaultBranch.value
+}
+
+function useMetaData(
+ partialMetaData: PartialMetaData | null,
+ defaultBranchName: MetaData['defaultBranchName'] | null,
+) {
+ const $state = useLoadedContext(SideBarStateContext)
+ const $metaData = useStateIO(null)
+ const branchName = useBranchName()
+ const theBranch = branchName && branchName !== defaultBranchName ? branchName : defaultBranchName
+ React.useEffect(() => {
+ if (partialMetaData && defaultBranchName && theBranch) {
+ const { userName, repoName } = partialMetaData
+ const safeMetaData: MetaData = {
+ userName,
+ repoName,
+ branchName: theBranch,
+ defaultBranchName,
+ }
+ $metaData.onChange(safeMetaData)
+ $state.onChange('meta-loaded')
+ } else {
+ $metaData.onChange(null)
+ }
+ }, [partialMetaData, defaultBranchName, theBranch])
+ return $metaData.value
+}
diff --git a/src/containers/SideBarState.tsx b/src/containers/SideBarState.tsx
new file mode 100644
index 0000000..815a122
--- /dev/null
+++ b/src/containers/SideBarState.tsx
@@ -0,0 +1,28 @@
+import * as React from 'react'
+import { useStateIO } from 'utils/hooks/useStateIO'
+
+export type SideBarState =
+ | 'disabled'
+ | 'getting-access-token'
+ | 'after-getting-access-token' // mid-state for a smoother state switch out of 'getting-access-token'
+ | 'meta-loading'
+ | 'meta-loaded'
+ | 'tree-loading'
+ | 'tree-rendering'
+ | 'tree-rendered'
+ | 'idle'
+ | 'error-due-to-auth'
+
+export type SideBarStateContextShape = IO
+
+export const SideBarStateContext = React.createContext(null)
+
+export function StateBarStateContextWrapper({ children }: React.PropsWithChildren<{}>) {
+ const $state = useStateIO('disabled')
+
+ return (
+
+ {$state.value !== null && children}
+
+ )
+}
diff --git a/src/components/Theme.tsx b/src/containers/Theme.tsx
similarity index 100%
rename from src/components/Theme.tsx
rename to src/containers/Theme.tsx
diff --git a/src/content.tsx b/src/content.tsx
index 91c1db0..2d48ef5 100644
--- a/src/content.tsx
+++ b/src/content.tsx
@@ -6,7 +6,7 @@ import * as React from 'react'
import * as ReactDOM from 'react-dom'
import './content.scss'
-if (platform.resolveMeta()) {
+if (platform.resolvePartialMetaData()) {
addMiddleware(withErrorLog)
if (document.readyState === 'loading') {
@@ -26,7 +26,7 @@ async function init() {
// injects a copy of stylesheets so that other extensions(e.g. dark reader) could read
// resolves when style is loaded to prevent render without proper styles
async function injectStyles(url: string) {
- return new Promise(resolve => {
+ return new Promise(resolve => {
const linkElement = document.createElement('link')
linkElement.setAttribute('rel', 'stylesheet')
linkElement.setAttribute('href', url)
diff --git a/src/driver/core/FileExplorer.ts b/src/driver/core/FileExplorer.ts
index ca1df41..8240357 100644
--- a/src/driver/core/FileExplorer.ts
+++ b/src/driver/core/FileExplorer.ts
@@ -1,26 +1,25 @@
+import { SideBarStateContextShape } from 'containers/SideBarState'
import { GetCreatedMethod, MethodCreator } from 'driver/connect'
import { platform } from 'platforms'
-import { Config } from 'utils/configHelper'
+import { Config } from 'utils/config/helper'
import * as DOMHelper from 'utils/DOMHelper'
import { VisibleNodes, VisibleNodesGenerator } from 'utils/VisibleNodesGenerator'
export type Props = {
- treeRoot?: TreeNode
metaData: MetaData
freeze: boolean
accessToken: string | undefined
- toggleShowSettings: React.MouseEventHandler
config: Config
loadWithPJAX(url: string): void
- defer?: boolean
+ catchNetworkErrors: (fn: () => T) => Promise
}
export type ConnectorState = {
- state: 'rendering' | 'done'
visibleNodesGenerator: VisibleNodesGenerator | null
visibleNodes: VisibleNodes | null
searchKey: string
searched: boolean // derived state from searchKey, = !!searchKey
+ defer: boolean
handleKeyDown: GetCreatedMethod
updateSearchKey: GetCreatedMethod
@@ -45,37 +44,58 @@ type BoundMethodCreator = MethodCreator> & {
- config: Pick
- },
+ { stateContext: SideBarStateContextShape } & Required> & {
+ config: Pick
+ },
]
-> = dispatch => async ({ treeRoot, metaData, config }) => {
- dispatch.set({ state: 'rendering' })
+> = dispatch => ({ stateContext, metaData, config }) => {
+ const {
+ props: { catchNetworkErrors },
+ } = dispatch.get()
- const visibleNodesGenerator = new VisibleNodesGenerator({
- root: treeRoot,
- compress: config.compressSingletonFolder,
- async getTreeData(path) {
- const { root } = await platform.getTreeData(metaData, path, false, config.accessToken)
- return root
- },
- })
- dispatch.set({ visibleNodesGenerator })
- visibleNodesGenerator.onUpdate(visibleNodes => dispatch.set({ visibleNodes }))
+ catchNetworkErrors(async () => {
+ const { userName, repoName, branchName } = metaData
- if (platform.shouldExpandAll?.()) {
- const unsubscribe = visibleNodesGenerator.onUpdate(visibleNodes => {
- unsubscribe()
- visibleNodes.nodes.forEach(node =>
- dispatch.call(toggleNodeExpansion, node, { recursive: true }),
- )
+ stateContext.onChange('tree-loading')
+ const { root: treeRoot, defer = false } = await platform.getTreeData(
+ {
+ branchName: branchName,
+ userName,
+ repoName,
+ },
+ '/',
+ true,
+ config.accessToken,
+ )
+
+ stateContext.onChange('tree-rendering')
+ dispatch.set({ defer })
+
+ const visibleNodesGenerator = new VisibleNodesGenerator({
+ root: treeRoot,
+ compress: config.compressSingletonFolder,
+ async getTreeData(path) {
+ const { root } = await platform.getTreeData(metaData, path, false, config.accessToken)
+ return root
+ },
})
- } else {
- const targetPath = platform.getCurrentPath(metaData.branchName)
- if (targetPath) dispatch.call(goTo, targetPath)
- }
+ dispatch.set({ visibleNodesGenerator })
+ visibleNodesGenerator.onUpdate(visibleNodes => dispatch.set({ visibleNodes }))
- dispatch.set({ state: 'done' })
+ if (platform.shouldExpandAll?.()) {
+ const unsubscribe = visibleNodesGenerator.onUpdate(visibleNodes => {
+ unsubscribe()
+ visibleNodes.nodes.forEach(node =>
+ dispatch.call(toggleNodeExpansion, node, { recursive: true }),
+ )
+ })
+ } else {
+ const targetPath = platform.getCurrentPath(metaData.branchName)
+ if (targetPath) dispatch.call(goTo, targetPath)
+ }
+
+ stateContext.onChange('tree-rendered')
+ })
}
export const handleKeyDown: BoundMethodCreator<[React.KeyboardEvent]> = dispatch => event => {
diff --git a/src/driver/core/SideBar.ts b/src/driver/core/SideBar.ts
deleted file mode 100644
index 31d21f8..0000000
--- a/src/driver/core/SideBar.ts
+++ /dev/null
@@ -1,185 +0,0 @@
-import { ConfigsContextShape } from 'containers/ConfigsContext'
-import { GetCreatedMethod, MethodCreator } from 'driver/connect'
-import { errors, platform, platformName } from 'platforms'
-import * as DOMHelper from 'utils/DOMHelper'
-import { createPromiseQueue } from 'utils/general'
-
-export type Props = {
- configContext: ConfigsContextShape
-}
-
-export type ConnectorState = {
- // error message
- error?: string
- // whether Gitako side bar should be shown
- shouldShow: boolean
- // whether show settings pane
- showSettings: boolean
- // meta data for the repository
- metaData?: MetaData
- // file tree data
- treeData?: TreeNode
- state: 'loading-meta' | 'loading-tree' | 'idle' | 'error-due-to-auth' | 'disabled'
- logoContainerElement: Element | null
- defer?: boolean
-} & {
- init: GetCreatedMethod
- setShouldShow: GetCreatedMethod
- toggleShowSideBar: GetCreatedMethod
- toggleShowSettings: GetCreatedMethod
-}
-
-type BoundMethodCreator = MethodCreator
-
-const promiseQueue = createPromiseQueue()
-
-export const init: BoundMethodCreator = dispatch => async () => {
- const leave = await promiseQueue.enter()
-
- try {
- dispatch.set({ state: 'loading-meta' })
- const metaData = platform.resolveMeta()
- if (!metaData) {
- dispatch.set({ state: 'disabled' })
- return
- }
- const { userName, repoName, branchName, defaultBranchName } = metaData
-
- DOMHelper.markGitakoReadyState(true)
- dispatch.set({
- showSettings: false,
- logoContainerElement: DOMHelper.insertLogoMountPoint(),
- })
-
- const {
- props: { configContext },
- } = dispatch.get()
- const { accessToken } = configContext.value
-
- const guessDefaultBranch = 'master' // when to switch to 'main'?
- let getTreeData = platform.getTreeData(
- {
- branchName: branchName || guessDefaultBranch,
- userName,
- repoName,
- },
- '/',
- true,
- accessToken,
- )
- getTreeData.catch(error => error) // catch it early to prevent the error being raised higher
-
- if (branchName) {
- const safeMetaData: MetaData = {
- userName,
- repoName,
- branchName,
- defaultBranchName,
- }
- dispatch.set({ metaData: safeMetaData })
- getTreeData.catch(error => {
- dispatch.call(handleError, error)
- })
- } else {
- const defaultBranchName = await platform.getDefaultBranchName(
- { userName, repoName },
- accessToken,
- )
-
- if (!defaultBranchName) {
- throw new Error(`Failed resolving default branch name`)
- }
-
- const safeMetaData: MetaData = {
- userName,
- repoName,
- branchName: defaultBranchName,
- defaultBranchName,
- }
- dispatch.set({ metaData: safeMetaData })
-
- if (defaultBranchName !== guessDefaultBranch && metaData.type !== 'pull') {
- // 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
- getTreeData = platform.getTreeData(
- {
- branchName: defaultBranchName,
- userName,
- repoName,
- },
- '/',
- true,
- accessToken,
- )
- }
- }
-
- dispatch.set({ state: 'loading-tree' })
- const { root: treeData, defer } = await getTreeData
- dispatch.set({ state: 'idle', treeData, defer })
- } catch (err) {
- dispatch.call(handleError, err)
- }
-
- leave()
-}
-
-export const handleError: BoundMethodCreator<[Error]> = dispatch => async err => {
- if (err.message === errors.EMPTY_PROJECT) {
- dispatch.call(setError, 'This project seems to be empty.')
- } else if (err.message === errors.BLOCKED_PROJECT) {
- dispatch.call(setError, 'Access to the project is blocked.')
- } else if (
- err.message === errors.NOT_FOUND ||
- err.message === errors.BAD_CREDENTIALS ||
- err.message === errors.API_RATE_LIMIT
- ) {
- dispatch.set({ state: 'error-due-to-auth' })
- } else if (err.message === errors.CONNECTION_BLOCKED) {
- const { props } = dispatch.get()
- if (props.configContext.value.accessToken) {
- dispatch.call(setError, `Cannot connect to ${platformName}.`)
- } else {
- dispatch.set({ state: 'error-due-to-auth' })
- }
- } else if (err.message === errors.SERVER_FAULT) {
- dispatch.call(setError, `${platformName} server went down.`)
- } else {
- DOMHelper.markGitakoReadyState(false)
- dispatch.call(setError, 'Some thing went wrong.')
- throw err
- }
-}
-
-export const toggleShowSideBar: BoundMethodCreator = dispatch => () => {
- const {
- state: { shouldShow },
- props: { configContext },
- } = dispatch.get()
- dispatch.call(setShouldShow, !shouldShow)
-
- const {
- value: { intelligentToggle },
- } = configContext
- if (intelligentToggle !== null) {
- configContext.onChange({ intelligentToggle: !shouldShow })
- }
-}
-
-export const setShouldShow: BoundMethodCreator<
- [ConnectorState['shouldShow']]
-> = dispatch => shouldShow => {
- dispatch.set({ shouldShow }, shouldShow ? DOMHelper.focusFileExplorer : undefined)
- DOMHelper.setBodyIndent(shouldShow)
-}
-
-export const setError: BoundMethodCreator<[ConnectorState['error']]> = dispatch => error => {
- dispatch.set({ error })
- dispatch.call(setShouldShow, false)
-}
-
-export const toggleShowSettings: BoundMethodCreator = dispatch => () =>
- dispatch.set(({ showSettings }) => ({
- showSettings: !showSettings,
- }))
diff --git a/src/driver/core/index.ts b/src/driver/core/index.ts
index 4eb403b..dd80f10 100644
--- a/src/driver/core/index.ts
+++ b/src/driver/core/index.ts
@@ -4,8 +4,5 @@ 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/global.d.ts b/src/global.d.ts
index 199ec17..80a88bb 100644
--- a/src/global.d.ts
+++ b/src/global.d.ts
@@ -1,11 +1,13 @@
type MetaData = {
userName: string
repoName: string
+ defaultBranchName: string
branchName: string
- defaultBranchName?: string
type?: EnumString<'tree' | 'blob' | 'pull'>
}
+type PartialMetaData = Omit, 'defaultBranchName'>
+
type TreeNode = {
name: string
contents?: TreeNode[]
diff --git a/src/platforms/GitHub/DOMHelper.ts b/src/platforms/GitHub/DOMHelper.ts
index 0d62f0e..35af789 100644
--- a/src/platforms/GitHub/DOMHelper.ts
+++ b/src/platforms/GitHub/DOMHelper.ts
@@ -239,9 +239,8 @@ export function getPath() {
const pathElement =
document.querySelector(blobPathElementSelector) ||
document.querySelector(folderPathElementSelector)?.nextElementSibling
- if (!pathElement || !pathElement.querySelector('.js-repo-root')) {
- raiseError(new Error(`Path element not found`))
- return ''
+ if (!pathElement?.querySelector('.js-repo-root')) {
+ return []
}
const path = ((pathElement as HTMLDivElement).innerText || '')
.replace(/ \/ Jump to $/, '')
@@ -249,6 +248,5 @@ export function getPath() {
.split('/')
.filter(Boolean)
.slice(1) // the first is the repo's name
- .join('/')
return path
}
diff --git a/src/platforms/GitHub/index.ts b/src/platforms/GitHub/index.ts
index f37bff6..159d1b1 100644
--- a/src/platforms/GitHub/index.ts
+++ b/src/platforms/GitHub/index.ts
@@ -1,4 +1,3 @@
-import { raiseError } from 'analytics'
import { GITHUB_OAUTH } from 'env'
import { Base64 } from 'js-base64'
import { platform } from 'platforms'
@@ -83,26 +82,13 @@ export function isEnterprise() {
return !window.location.host.endsWith('github.com')
}
-function getBranchName() {
- const pathFromDOM = DOMHelper.getPath()
- const pathAndBranchFromURL = URLHelper.parse().path.filter(Boolean).join('/')
- if (pathAndBranchFromURL.endsWith(pathFromDOM)) {
- return pathAndBranchFromURL
- .slice(0, pathAndBranchFromURL.length - pathFromDOM.length)
- .replace(/\/$/, '')
- }
- raiseError(new Error(`Parsed path not end with path from DOM`))
-}
-
function resolvePageScope(defaultBranchName?: string) {
const parsed = URLHelper.parse()
switch (parsed.type) {
- // case undefined:
- // return `branch-${defaultBranchName}`
case 'blob':
case 'tree': {
- // handle URLs like {user}/{repo}/tree/{sha|branch}, issue#131
- const branchName = getBranchName()
+ // handle URLs like {user}/{repo}/{'tree'|'blob'}/{sha|branch}, issue #131
+ const branchName = DOMHelper.getCurrentBranch()
if (branchName && branchName !== defaultBranchName) return `branch-${branchName}`
break
}
@@ -121,7 +107,7 @@ const pathSHAMap = new Map()
export const GitHub: Platform = {
isEnterprise,
- resolveMeta() {
+ resolvePartialMetaData() {
if (!DOMHelper.isInRepoPage()) {
return null
}
@@ -150,14 +136,11 @@ export const GitHub: Platform = {
repoName,
type,
branchName,
- defaultBranchName: type ? undefined : branchName,
}
return metaData
},
- resolvePageScope,
async getDefaultBranchName({ userName, repoName }, accessToken) {
- const data = await API.getRepoMeta(userName, repoName, accessToken)
- return data.default_branch
+ return (await API.getRepoMeta(userName, repoName, accessToken)).default_branch
},
resolveUrlFromMetaData({ userName, repoName }) {
return {
@@ -274,7 +257,11 @@ export const GitHub: Platform = {
return Boolean(URLHelper.isInPullPage())
},
getCurrentPath(branchName) {
- return URLHelper.getCurrentPath(branchName)
+ if (URLHelper.parse().path.length) {
+ return DOMHelper.getPath()
+ } else {
+ return []
+ }
},
setOAuth(code) {
return API.OAuth(code)
@@ -298,33 +285,6 @@ async function createPullFileResolver(userName: string, repoName: string, pullId
}
}
-function findMissingFolders(nodes: TreeNode[]) {
- const folders = new Set()
- const foundFolders = new Set()
- for (const node of nodes) {
- let path = node.path
- if (node.type === 'tree') foundFolders.add(path)
- else {
- while (true) {
- // 'a/b' -> 'a'
- // 'a' -> ''
- path = path.substring(0, path.lastIndexOf('/'))
- if (path === '') break
- folders.add(path)
- }
- }
- }
-
- const missingFolders: string[] = []
- for (const folder of folders) {
- if (!foundFolders.has(folder)) {
- missingFolders.push(folder)
- }
- }
-
- return missingFolders
-}
-
export function useGitHubAttachCopySnippetButton(copySnippetButton: boolean) {
const attachCopySnippetButton = React.useCallback(
function attachCopySnippetButton() {
diff --git a/src/platforms/Gitea/index.ts b/src/platforms/Gitea/index.ts
index 8138794..24cbbdb 100644
--- a/src/platforms/Gitea/index.ts
+++ b/src/platforms/Gitea/index.ts
@@ -77,7 +77,7 @@ export const Gitea: Platform = {
isEnterprise() {
return !window.location.host.endsWith('gitea.com')
},
- resolveMeta() {
+ resolvePartialMetaData() {
if (!DOMHelper.isInRepoPage()) {
return null
}
diff --git a/src/platforms/Gitee/index.ts b/src/platforms/Gitee/index.ts
index 4781db4..8addea1 100644
--- a/src/platforms/Gitee/index.ts
+++ b/src/platforms/Gitee/index.ts
@@ -73,7 +73,7 @@ export const Gitee: Platform = {
isEnterprise() {
return !window.location.host.endsWith('gitee.com')
},
- resolveMeta() {
+ resolvePartialMetaData() {
if (!DOMHelper.isInRepoPage()) {
return null
}
diff --git a/src/platforms/dummyPlatformForTypeSafety.ts b/src/platforms/dummyPlatformForTypeSafety.ts
index 6cf5c8d..9216714 100644
--- a/src/platforms/dummyPlatformForTypeSafety.ts
+++ b/src/platforms/dummyPlatformForTypeSafety.ts
@@ -2,7 +2,7 @@ export const dummyPlatformForTypeSafety: Platform = {
isEnterprise() {
return false
},
- resolveMeta() {
+ resolvePartialMetaData() {
return null
},
getDefaultBranchName: dummyPlatformMethod,
diff --git a/src/platforms/index.ts b/src/platforms/index.ts
index 4e713a4..b5ac56f 100644
--- a/src/platforms/index.ts
+++ b/src/platforms/index.ts
@@ -1,11 +1,9 @@
import { dummyPlatformForTypeSafety } from './dummyPlatformForTypeSafety'
-import { Gitee } from './Gitee'
import { Gitea } from './Gitea'
+import { Gitee } from './Gitee'
import { GitHub } from './GitHub'
-const platforms: {
- [name: string]: Platform
-} = {
+const platforms = {
GitHub: GitHub,
Gitee: Gitee,
Gitea: Gitea,
@@ -13,7 +11,7 @@ const platforms: {
function resolvePlatform() {
for (const platform of Object.values(platforms)) {
- if (platform.resolveMeta()) return platform
+ if (platform.resolvePartialMetaData()) return platform
}
return dummyPlatformForTypeSafety
}
diff --git a/src/platforms/platform.d.ts b/src/platforms/platform.d.ts
index 6bf87ac..fe82adc 100644
--- a/src/platforms/platform.d.ts
+++ b/src/platforms/platform.d.ts
@@ -1,20 +1,20 @@
type Platform = {
isEnterprise(): boolean
// branch name might not be available when resolving from DOM and URL
- resolveMeta(): MakeOptional | null
- resolvePageScope?(branchName?: string): string
+ resolvePartialMetaData(): PartialMetaData | null
+ // resolveMetaData(metaData: PartialMetaData, accessToken?: string): Async
getDefaultBranchName(
metaData: Pick,
accessToken?: string,
): Promise
resolveUrlFromMetaData(
- metaData: MetaData,
+ metaData: Pick,
): {
userUrl: string
repoUrl: string
}
getTreeData(
- metaData: MetaData,
+ metaData: Pick,
path?: string,
recursive?: boolean,
accessToken?: string,
diff --git a/src/resize-observer.d.ts b/src/resize-observer.d.ts
deleted file mode 100644
index 45f7831..0000000
--- a/src/resize-observer.d.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-interface Window {
- ResizeObserver: ResizeObserver
-}
-
-/**
- * The ResizeObserver interface is used to observe changes to Element's content
- * rect.
- *
- * It is modeled after MutationObserver and IntersectionObserver.
- */
-interface ResizeObserver {
- new (callback: ResizeObserverCallback): ResizeObserver
-
- /**
- * Adds target to the list of observed elements.
- */
- observe: (target: Element) => void
-
- /**
- * Removes target from the list of observed elements.
- */
- unobserve: (target: Element) => void
-
- /**
- * Clears both the observationTargets and activeTargets lists.
- */
- disconnect: () => void
-}
-
-/**
- * This callback delivers ResizeObserver's notifications. It is invoked by a
- * broadcast active observations algorithm.
- */
-interface ResizeObserverCallback {
- (entries: ResizeObserverEntry[], observer: ResizeObserver): void
-}
-
-interface ResizeObserverEntry {
- /**
- * @param target The Element whose size has changed.
- */
- new (target: Element): void
-
- /**
- * The Element whose size has changed.
- */
- readonly target: Element
-
- /**
- * Element's content rect when ResizeObserverCallback is invoked.
- */
- readonly contentRect: DOMRectReadOnly
-}
-
-interface DOMRectReadOnly {
- // static fromRect(other: DOMRectInit | undefined): DOMRectReadOnly;
-
- readonly x: number
- readonly y: number
- readonly width: number
- readonly height: number
- readonly top: number
- readonly right: number
- readonly bottom: number
- readonly left: number
-
- toJSON: () => any
-}
diff --git a/src/styles/index.scss b/src/styles/index.scss
index ee47712..ecd8320 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -182,17 +182,30 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
left: 0px;
display: inline-flex;
align-items: center;
+ transition: left 0.25s linear;
+
+ &.hidden {
+ left: -40px;
+ }
.#{$name}-toggle-show-button {
@include icon-button;
background: transparent;
border: none;
padding: 0;
+ position: relative;
+ left: -8px;
+
+ &.error {
+ cursor: not-allowed;
+ }
+
.octoface-icon {
@include flex-center();
width: 32px;
height: 32px;
+ padding: 4px;
border-radius: 6px;
border: 1px solid var(--gitako-border-tertiary);
background: var(--gitako-bg-tertiary);
@@ -202,33 +215,27 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
}
transition: all ease 0.3s;
font-size: 16px;
- transform: translateX(0px);
+ transform: translateX(8px);
&:active,
&:hover {
- transform: translateX(4px);
+ transform: translateX(12px);
}
}
.tentacle {
width: 40px;
height: 40px;
+ object-fit: contain;
transition: all ease 0.4s;
- transform: translateX(-16px);
+ transform: translateX(-8px);
+ filter: drop-shadow(0 0 1px var(--gitako-bg-backdrop));
}
&:active,
&:hover {
.tentacle {
- transform: translateX(-12px);
+ transform: translateX(-4px);
filter: drop-shadow(0 0 2px var(--gitako-bg-backdrop));
}
}
-
- img {
- filter: drop-shadow(0 0 1px var(--gitako-bg-backdrop));
- transition: all ease 0.3s;
- width: 90%;
- height: 90%;
- object-fit: contain;
- }
}
.error-message {
@@ -259,7 +266,8 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
color: var(--gitako-text-link);
}
- .#{$name}-position-wrapper {
+ $resizeHandlerWidth: 1px;
+ .#{$name}-side-bar-body-wrapper {
position: fixed;
top: 0;
left: 0;
@@ -267,11 +275,21 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
z-index: $minimal-z-index;
display: flex;
- &.hidden {
- @extend %hidden;
+ &.toggle-mode-persistent {
+ &.collapsed {
+ @extend %hidden;
+ }
}
- .gitako-position-content {
+ &.toggle-mode-float {
+ left: 0;
+ transition: all 0.25s cubic-bezier(0.55, 0.06, 0.68, 0.19); // values from Chrome devtools
+ &.collapsed {
+ left: calc(0px - #{$resizeHandlerWidth} - var(--gitako-width));
+ }
+ }
+
+ .gitako-side-bar-body-wrapper-content {
width: var(--gitako-width);
}
}
@@ -282,7 +300,7 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
user-select: none;
width: 0;
background: var(--gitako-bg-tertiary);
- border-right: 1px solid var(--gitako-border-tertiary);
+ border-right: $resizeHandlerWidth solid var(--gitako-border-tertiary);
overflow: hidden;
box-sizing: content-box;
&:hover,
@@ -310,10 +328,6 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
border-left: 1px solid var(--gitako-border-overlay);
overflow: hidden;
- &.hidden {
- @extend %hidden;
- }
-
.octicon {
transition: transform 0.3s ease;
color: var(--gitako-icon-tertiary);
@@ -350,6 +364,10 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
transition: background linear 0.3s;
}
+ &.active .octicon {
+ color: var(--gitako-icon-primary);
+ }
+
.action-icon {
color: var(--gitako-icon-tertiary);
width: 20px;
@@ -359,15 +377,22 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
width: 100%;
height: 100%;
}
+ .Pin,
+ .Tab {
+ transform: rotateY(180deg);
+ }
}
}
+
+ .close-side-bar-button + .close-side-bar-button {
+ margin-left: 4px;
+ }
}
.#{$name}-side-bar-content {
display: flex;
flex: 1;
flex-direction: column;
- max-height: calc(100vh - 34px); // temporary fix for layout issue occurred since Chrome v76
min-height: 0; // make content shrinkable
.header {
@@ -378,7 +403,7 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
min-height: 62px; // GitHub header height if login
flex-shrink: 0;
padding: 8px 10px;
- padding-right: $button-size + 8px * 2; // space for toggle button
+ padding-right: $button-size * 2 + 8px; // space for toggle buttons
background: var(--gitako-bg-tertiary);
.user-and-repo a {
@@ -394,8 +419,7 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
}
}
- .description {
- border-top: 1px solid var(--gitako-border-overlay);
+ .description-area {
padding: 4px 10px;
}
diff --git a/src/utils/config/helper.ts b/src/utils/config/helper.ts
new file mode 100644
index 0000000..2bacbcf
--- /dev/null
+++ b/src/utils/config/helper.ts
@@ -0,0 +1,85 @@
+import { SearchMode } from 'components/searchModes'
+import { platform, platformName } from 'platforms'
+import { storageHelper } from 'utils/storageHelper'
+import { migrateConfig } from './migrations'
+
+export type Config = {
+ sideBarWidth: number
+ shortcut: string | undefined
+ accessToken: string | undefined
+ compressSingletonFolder: boolean
+ copyFileButton: boolean
+ copySnippetButton: boolean
+ intelligentToggle: boolean | null // `null` stands for intelligent, boolean for sidebar open state
+ icons: 'rich' | 'dim' | 'native'
+ toggleButtonVerticalDistance: number
+ toggleButtonContent: 'logo' | 'octoface'
+ recursiveToggleFolder: 'shift' | 'alt'
+ searchMode: SearchMode
+ sidebarToggleMode: 'persistent' | 'float'
+ commentToggle: boolean
+}
+
+enum configKeys {
+ sideBarWidth = 'sideBarWidth',
+ shortcut = 'shortcut',
+ accessToken = 'accessToken',
+ compressSingletonFolder = 'compressSingletonFolder',
+ copyFileButton = 'copyFileButton',
+ copySnippetButton = 'copySnippetButton',
+ intelligentToggle = 'intelligentToggle',
+ icons = 'icons',
+ toggleButtonVerticalDistance = 'toggleButtonVerticalDistance',
+ toggleButtonContent = 'toggleButtonContent',
+ recursiveToggleFolder = 'recursiveToggleFolder',
+ searchMode = 'searchMode',
+ sidebarToggleMode = 'sidebarToggleMode',
+ commentToggle = 'commentToggle',
+}
+
+export const defaultConfigs: Config = {
+ sideBarWidth: 260,
+ shortcut: undefined,
+ accessToken: '',
+ compressSingletonFolder: true,
+ copyFileButton: true,
+ copySnippetButton: !(platformName === 'GitHub' && !platform.isEnterprise()), // false when on github.com
+ intelligentToggle: null,
+ icons: 'rich',
+ toggleButtonVerticalDistance: 124, // align with GitHub's navbar items
+ toggleButtonContent: 'logo',
+ recursiveToggleFolder: 'shift',
+ searchMode: 'fuzzy',
+ sidebarToggleMode: 'float',
+ commentToggle: true,
+}
+
+const configKeyArray = Object.values(configKeys)
+
+function applyDefaultConfigs(configs: Partial) {
+ return configKeyArray.reduce((applied, key) => {
+ Object.assign(applied, { [key]: key in configs ? configs[key] : defaultConfigs[key] })
+ return applied
+ }, {} as Config)
+}
+
+export type VersionedConfig = Record & { configVersion: string }
+
+// do NOT use platform name
+const platformStorageKey = `platform_` + window.location.host.toLowerCase()
+const prepareConfig = new Promise(async resolve => {
+ await migrateConfig()
+ resolve()
+})
+
+async function get(): Promise {
+ await prepareConfig
+ const config = await storageHelper.get>([platformStorageKey])
+ return applyDefaultConfigs(config?.[platformStorageKey] || {})
+}
+
+async function set(config: Config) {
+ return await storageHelper.set({ [platformStorageKey]: config })
+}
+
+export const configHelper = { get, set }
diff --git a/src/utils/config/migrations/1.0.1.ts b/src/utils/config/migrations/1.0.1.ts
new file mode 100644
index 0000000..7f16c3d
--- /dev/null
+++ b/src/utils/config/migrations/1.0.1.ts
@@ -0,0 +1,23 @@
+import { storageHelper } from 'utils/storageHelper'
+import { Migration } from '.'
+import { Storage } from '../../storageHelper'
+
+export const migration: Migration = {
+ version: '1.0.1',
+ async migrate(version) {
+ const config: any | void = await storageHelper.get([
+ 'configVersion',
+ 'sideBarWidth',
+ 'shortcut',
+ 'access_token',
+ 'compressSingletonFolder',
+ 'copyFileButton',
+ 'copySnippetButton',
+ 'intelligentToggle',
+ 'icons',
+ ])
+ if (config && (!('configVersion' in config) || config.configVersion < version)) {
+ await storageHelper.set({ platform_GitHub: config, configVersion: version })
+ }
+ },
+}
diff --git a/src/utils/config/migrations/1.3.4.ts b/src/utils/config/migrations/1.3.4.ts
new file mode 100644
index 0000000..9fd8cd9
--- /dev/null
+++ b/src/utils/config/migrations/1.3.4.ts
@@ -0,0 +1,28 @@
+import { storageHelper } from 'utils/storageHelper'
+import { Migration } from '.'
+import { Storage } from '../../storageHelper'
+import { Config, VersionedConfig } from '../helper'
+
+export const migration: Migration = {
+ version: '1.3.4',
+ async migrate(version) {
+ const config: any | void = await storageHelper.get & Storage>([
+ 'configVersion',
+ 'platform_undefined',
+ 'platform_GitHub',
+ 'platform_github.com',
+ ])
+ if (
+ config &&
+ 'configVersion' in config &&
+ config.configVersion < version &&
+ (config.platform_GitHub || config.platform_undefined) &&
+ !config['platform_github.com']
+ ) {
+ await storageHelper.set({
+ ['platform_github.com']: config.platform_GitHub || config.platform_undefined,
+ configVersion: version,
+ })
+ }
+ },
+}
diff --git a/src/utils/config/migrations/2.6.0.ts b/src/utils/config/migrations/2.6.0.ts
new file mode 100644
index 0000000..d5a522f
--- /dev/null
+++ b/src/utils/config/migrations/2.6.0.ts
@@ -0,0 +1,34 @@
+import { storageHelper } from 'utils/storageHelper'
+import { Migration, onConfigOutdated } from '.'
+
+export const migration: Migration = {
+ version: '2.6.0',
+ async migrate(version) {
+ type ConfigBeforeMigrate = {
+ access_token?: string
+ }
+ type ConfigAfterMigrate = {
+ accessToken?: string
+ }
+
+ await onConfigOutdated(version, async configs => {
+ for (const key of Object.keys(configs)) {
+ if (
+ typeof configs[key] === 'object' &&
+ configs[key] !== null &&
+ 'access_token' in configs[key]
+ ) {
+ const configBeforeMigrate: ConfigBeforeMigrate = configs[key]
+ const { access_token: accessToken, ...rest } = configBeforeMigrate
+ const configAfterMigrate: ConfigAfterMigrate = {
+ ...rest,
+ accessToken,
+ }
+ await storageHelper.set({
+ [key]: configAfterMigrate,
+ })
+ }
+ }
+ })
+ },
+}
diff --git a/src/utils/config/migrations/3.0.0.ts b/src/utils/config/migrations/3.0.0.ts
new file mode 100644
index 0000000..4db2fba
--- /dev/null
+++ b/src/utils/config/migrations/3.0.0.ts
@@ -0,0 +1,33 @@
+import { storageHelper } from 'utils/storageHelper'
+import { Migration, onConfigOutdated } from '.'
+
+export const migration: Migration = {
+ version: '3.0.0',
+ async migrate(version) {
+ // disable copy snippet button for github.com
+ type ConfigBeforeMigrate = {
+ copySnippetButton: boolean
+ }
+ type ConfigAfterMigrate = {
+ copySnippetButton: boolean
+ }
+
+ await onConfigOutdated(version, async configs => {
+ const key = 'platform_github.com'
+ const config = configs[key]
+ if (typeof config === 'object' && config !== null && 'copySnippetButton' in config) {
+ const configBeforeMigrate: ConfigBeforeMigrate = config
+ const { copySnippetButton, ...rest } = configBeforeMigrate
+ if (copySnippetButton) {
+ const configAfterMigrate: ConfigAfterMigrate = {
+ ...rest,
+ copySnippetButton: false,
+ }
+ await storageHelper.set({
+ [key]: configAfterMigrate,
+ })
+ }
+ }
+ })
+ },
+}
diff --git a/src/utils/config/migrations/index.ts b/src/utils/config/migrations/index.ts
new file mode 100644
index 0000000..b4b514f
--- /dev/null
+++ b/src/utils/config/migrations/index.ts
@@ -0,0 +1,32 @@
+import { storageHelper } from 'utils/storageHelper'
+import { Storage } from '../../storageHelper'
+import { migration as v1v0v1 } from './1.0.1'
+import { migration as v1v3v4 } from './1.3.4'
+import { migration as v2v6v0 } from './2.6.0'
+import { migration as v3v0v0 } from './3.0.0'
+
+export type Migration = {
+ version: string
+ migrate(version: Migration['version']): Async
+}
+
+export async function migrateConfig() {
+ const migrations: Migration[] = [v1v0v1, v1v3v4, v2v6v0, v3v0v0]
+
+ for (const { version, migrate } of migrations) {
+ await migrate(version)
+ }
+}
+
+export async function onConfigOutdated(
+ configVersion: string,
+ runIfOutdated: (config: T) => Async,
+) {
+ const config = await storageHelper.get()
+
+ if (config && config.configVersion < configVersion) {
+ const { configVersion, ...restConfig } = config
+ await runIfOutdated(restConfig as T)
+ await storageHelper.set({ configVersion })
+ }
+}
diff --git a/src/utils/configHelper.ts b/src/utils/configHelper.ts
deleted file mode 100644
index 01c1869..0000000
--- a/src/utils/configHelper.ts
+++ /dev/null
@@ -1,179 +0,0 @@
-import { SearchMode } from 'components/searchModes'
-import * as storageHelper from 'utils/storageHelper'
-
-export type Config = {
- sideBarWidth: number
- shortcut: string | undefined
- accessToken: string | undefined
- compressSingletonFolder: boolean
- copyFileButton: boolean
- copySnippetButton: boolean
- intelligentToggle: boolean | null // `null` stands for intelligent, boolean for sidebar open state
- icons: 'rich' | 'dim' | 'native'
- toggleButtonVerticalDistance: number
- toggleButtonContent: 'logo' | 'octoface'
- recursiveToggleFolder: 'shift' | 'alt'
- searchMode: SearchMode
- commentToggle: boolean
-}
-
-enum configKeys {
- sideBarWidth = 'sideBarWidth',
- shortcut = 'shortcut',
- accessToken = 'accessToken',
- compressSingletonFolder = 'compressSingletonFolder',
- copyFileButton = 'copyFileButton',
- copySnippetButton = 'copySnippetButton',
- intelligentToggle = 'intelligentToggle',
- icons = 'icons',
- toggleButtonVerticalDistance = 'toggleButtonVerticalDistance',
- toggleButtonContent = 'toggleButtonContent',
- recursiveToggleFolder = 'recursiveToggleFolder',
- searchMode = 'searchMode',
- commentToggle = 'commentToggle',
-}
-
-const defaultConfigs: Config = {
- sideBarWidth: 260,
- shortcut: undefined,
- accessToken: '',
- compressSingletonFolder: true,
- copyFileButton: true,
- copySnippetButton: true,
- intelligentToggle: null,
- icons: 'rich',
- toggleButtonVerticalDistance: 124, // align with GitHub's navbar items
- toggleButtonContent: 'logo',
- recursiveToggleFolder: 'shift',
- searchMode: 'fuzzy',
- commentToggle: true,
-}
-
-const configKeyArray = Object.values(configKeys)
-
-function applyDefaultConfigs(configs: Partial) {
- return configKeyArray.reduce((applied, key) => {
- Object.assign(applied, { [key]: key in configs ? configs[key] : defaultConfigs[key] })
- return applied
- }, {} as Config)
-}
-
-type VersionedConfig = Record & { configVersion: string }
-
-type Storage = {
- // save root level `configVersion` for easier future migrating
- [key in 'configVersion' | string]: string
-
- // separate different platform configs to simplify interactions with browser storage API
- // e.g.
- // platform_github.com?: Config
-}
-
-async function migrateConfig() {
- // not referencing to enum above to prevent migrate future configs
- const migrations: {
- version: string
- migrate(version: string): Promise
- }[] = [
- {
- version: '1.0.1',
- async migrate(version) {
- const config: any | void = await storageHelper.get([
- 'configVersion',
- 'sideBarWidth',
- 'shortcut',
- 'access_token',
- 'compressSingletonFolder',
- 'copyFileButton',
- 'copySnippetButton',
- 'intelligentToggle',
- 'icons',
- 'commentToggle',
- ])
- if (config && (!('configVersion' in config) || config.configVersion < version)) {
- await storageHelper.set({ platform_GitHub: config, configVersion: version })
- }
- },
- },
- {
- version: '1.3.4',
- async migrate(version) {
- const config: any | void = await storageHelper.get & Storage>([
- 'configVersion',
- 'platform_undefined', // this was a mistake :(
- 'platform_GitHub',
- 'platform_github.com',
- ])
- if (
- config &&
- 'configVersion' in config &&
- config.configVersion < version &&
- (config.platform_GitHub || config.platform_undefined) &&
- !config['platform_github.com']
- ) {
- await storageHelper.set({
- ['platform_github.com']: config.platform_GitHub || config.platform_undefined,
- configVersion: version,
- })
- }
- },
- },
- {
- version: '2.6.0',
- async migrate(version) {
- type LegacySiteConfig = {
- access_token?: string
- }
- type MigratedSiteConfig = {
- accessToken?: string
- }
-
- const config = await storageHelper.get & Storage>()
- if (config && config.configVersion < version) {
- const { configVersion, ...restConfig } = config
- for (const key of Object.keys(restConfig)) {
- if (
- typeof restConfig[key] === 'object' &&
- restConfig[key] &&
- 'access_token' in restConfig[key]
- ) {
- const config: LegacySiteConfig = restConfig[key]
- const { access_token: accessToken, ...legacy } = config
- const migrated: MigratedSiteConfig = {
- ...legacy,
- accessToken,
- }
- await storageHelper.set({
- [key]: migrated,
- })
- }
- }
- await storageHelper.set({
- configVersion: version,
- })
- }
- },
- },
- ]
-
- for (const { version, migrate } of migrations) {
- await migrate(version)
- }
-}
-
-// do NOT use platform name
-const platformStorageKey = `platform_` + window.location.host.toLowerCase()
-const prepareConfig = new Promise(async resolve => {
- await migrateConfig()
- resolve()
-})
-
-export async function get(): Promise {
- await prepareConfig
- const config = await storageHelper.get>([platformStorageKey])
- return applyDefaultConfigs(config?.[platformStorageKey] || {})
-}
-
-export async function set(config: Config) {
- return await storageHelper.set({ [platformStorageKey]: config })
-}
diff --git a/src/utils/cx.ts b/src/utils/cx.ts
index 179b6d3..01ea737 100644
--- a/src/utils/cx.ts
+++ b/src/utils/cx.ts
@@ -1,18 +1,28 @@
+type CXValue = string | boolean | null | undefined
+
/**
* cx('class1', { class2: true, class3: false }) --> 'class1 class2'
*/
-export function cx(...classNames: any[]): string {
- return classNames
- .filter(Boolean)
- .map(className => {
- switch (typeof className) {
- case 'string':
- return className
- case 'object':
- return cx(...Object.entries(className).map(([key, value]) => (value ? key : null)))
- default:
- return ''
+export function cx(
+ ...classNames: (
+ | CXValue
+ | {
+ [className: string]: CXValue
}
- })
- .join(' ')
+ )[]
+): string {
+ return classNames.map(handleClassName).filter(Boolean).join(' ')
+}
+
+function handleClassName(className: Parameters[number]): string | false {
+ switch (typeof className) {
+ case 'string':
+ return className
+ case 'object':
+ return className === null
+ ? false
+ : cx(...Object.entries(className).map(([key, value]) => (value ? key : false)))
+ default:
+ return false
+ }
}
diff --git a/src/utils/general.ts b/src/utils/general.ts
index 9bafe4e..f424635 100644
--- a/src/utils/general.ts
+++ b/src/utils/general.ts
@@ -105,23 +105,8 @@ export function setStyleSheetMedia(style: HTMLStyleElement, media: string) {
style.setAttribute('media', media)
}
-export function parseURLSearch(search: string = window.location.search) {
- const parsed: any = {}
- if (search.startsWith('?')) {
- const pairs = search
- .slice(1)
- .split('&')
- .map(rawPair => rawPair.split('=').map(raw => decodeURIComponent(raw))) // [key, value?][]
- pairs.forEach(([key, value]) => {
- if (Object.prototype.hasOwnProperty.call(parsed, key)) {
- if (Array.isArray(parsed[key])) parsed[key].push(value)
- else parsed[key] = [parsed[key], value]
- } else {
- parsed[key] = value
- }
- })
- }
- return parsed
+export function parseURLSearch(search = window.location.search) {
+ return new URLSearchParams(search)
}
export async function JSONRequest(url: string, data: any, extra: RequestInit = { method: 'post' }) {
diff --git a/src/components/useCSSVariable.tsx b/src/utils/hooks/useCSSVariable.ts
similarity index 100%
rename from src/components/useCSSVariable.tsx
rename to src/utils/hooks/useCSSVariable.ts
diff --git a/src/utils/hooks/useCatchNetworkError.ts b/src/utils/hooks/useCatchNetworkError.ts
new file mode 100644
index 0000000..f236641
--- /dev/null
+++ b/src/utils/hooks/useCatchNetworkError.ts
@@ -0,0 +1,45 @@
+import { useConfigs } from 'containers/ConfigsContext'
+import { errors, platformName } from 'platforms'
+import { useCallback } from 'react'
+import { useLoadedContext } from 'utils/hooks/useLoadedContext'
+import { SideBarErrorContext } from '../../containers/ErrorContext'
+import { SideBarStateContext } from '../../containers/SideBarState'
+
+export function useCatchNetworkError() {
+ const { accessToken } = useConfigs().value
+ const stateContext = useLoadedContext(SideBarStateContext)
+ const errorContext = useLoadedContext(SideBarErrorContext)
+
+ return useCallback(
+ async function (fn: () => T) {
+ try {
+ return await fn() // keep the await so that catch block can catch async errors
+ } catch (err) {
+ if (err.message === errors.EMPTY_PROJECT) {
+ errorContext.onChange('This project seems to be empty.')
+ } else if (err.message === errors.BLOCKED_PROJECT) {
+ errorContext.onChange('Access to the project is blocked.')
+ } else if (
+ err.message === errors.NOT_FOUND ||
+ err.message === errors.BAD_CREDENTIALS ||
+ err.message === errors.API_RATE_LIMIT
+ ) {
+ stateContext.onChange('error-due-to-auth')
+ } else if (err.message === errors.CONNECTION_BLOCKED) {
+ if (accessToken) {
+ errorContext.onChange(`Cannot connect to ${platformName}.`)
+ } else {
+ stateContext.onChange('error-due-to-auth')
+ }
+ } else if (err.message === errors.SERVER_FAULT) {
+ errorContext.onChange(`${platformName} server went down.`)
+ } else {
+ stateContext.onChange('disabled')
+ errorContext.onChange('Something unexpected happened.')
+ throw err
+ }
+ }
+ },
+ [accessToken /* , stateContext.value, errorContext.value */],
+ )
+}
diff --git a/src/utils/hooks/useEffectOnSerializableUpdates.ts b/src/utils/hooks/useEffectOnSerializableUpdates.ts
new file mode 100644
index 0000000..ecbcd4c
--- /dev/null
+++ b/src/utils/hooks/useEffectOnSerializableUpdates.ts
@@ -0,0 +1,9 @@
+import * as React from 'react'
+
+export function useEffectOnSerializableUpdates(
+ value: T,
+ serialize: (value: T) => string,
+ onChange: (value: T) => void,
+) {
+ React.useEffect(() => onChange(value), [onChange, serialize(value)])
+}
diff --git a/src/utils/hooks/useLoadedContext.ts b/src/utils/hooks/useLoadedContext.ts
new file mode 100644
index 0000000..7eea096
--- /dev/null
+++ b/src/utils/hooks/useLoadedContext.ts
@@ -0,0 +1,7 @@
+import * as React from 'react'
+
+export function useLoadedContext(context: React.Context): T {
+ const ctx = React.useContext(context)
+ if (ctx === null) throw new Error(`Context not loaded`)
+ return ctx
+}
diff --git a/src/utils/hooks/useToggleSideBarWithKeyboard.ts b/src/utils/hooks/useToggleSideBarWithKeyboard.ts
new file mode 100644
index 0000000..19d0a93
--- /dev/null
+++ b/src/utils/hooks/useToggleSideBarWithKeyboard.ts
@@ -0,0 +1,27 @@
+import { ConfigsContextShape } from 'containers/ConfigsContext'
+import * as React from 'react'
+import * as keyHelper from 'utils/keyHelper'
+import { SideBarState } from '../../containers/SideBarState'
+
+export function useToggleSideBarWithKeyboard(
+ state: SideBarState,
+ configContext: ConfigsContextShape,
+ toggleShowSideBar: () => void,
+) {
+ const isDisabled = state === 'disabled'
+ React.useEffect(
+ function attachKeyDown() {
+ if (isDisabled || !configContext.value.shortcut) return
+
+ function onKeyDown(e: KeyboardEvent) {
+ const keys = keyHelper.parseEvent(e)
+ if (keys === configContext.value.shortcut) {
+ toggleShowSideBar()
+ }
+ }
+ window.addEventListener('keydown', onKeyDown)
+ return () => window.removeEventListener('keydown', onKeyDown)
+ },
+ [toggleShowSideBar, isDisabled, configContext.value.shortcut],
+ )
+}
diff --git a/src/utils/hooks/useUpdateReason.tsx b/src/utils/hooks/useUpdateReason.ts
similarity index 100%
rename from src/utils/hooks/useUpdateReason.tsx
rename to src/utils/hooks/useUpdateReason.ts
diff --git a/src/utils/storageHelper.ts b/src/utils/storageHelper.ts
index 455907a..e3dc40a 100644
--- a/src/utils/storageHelper.ts
+++ b/src/utils/storageHelper.ts
@@ -1,6 +1,15 @@
const localStorage = browser.storage.local
-export async function get<
+export type Storage = {
+ // save root level `configVersion` for easier future migrating
+ [key in EnumString<'configVersion'>]: string
+
+ // separate different platform configs to simplify interactions with browser storage API
+ // e.g.
+ // ['platform_github.com']?: Config
+}
+
+async function get<
T extends {
[key: string]: any
}
@@ -10,8 +19,10 @@ export async function get<
} catch (err) {}
}
-export function set(value: any): Promise | void {
+function set(value: any): Promise | void {
try {
return localStorage.set(value)
} catch (err) {}
}
+
+export const storageHelper = { get, set }
diff --git a/tsconfig.json b/tsconfig.json
index 6fd56e0..837b4ff 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -7,6 +7,7 @@
"strict": true,
"lib": ["dom", "es2017.object", "es2016", "ES2020.String"],
"baseUrl": "src",
+ "allowSyntheticDefaultImports": true,
"outDir": "dist"
},
"include": ["src"]
diff --git a/yarn.lock b/yarn.lock
index 0791ecc..c1e63a0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1428,68 +1428,68 @@
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.3.0.tgz#2098181aab751f2275cacf219b02a6a8b455be85"
integrity sha512-dQA1acyNpwqy5ia5yt1lNjaAhm9rrzSurFtlJPoz7ARVPMV1yZ0yfmNotMwwgVbE5q1HnONqszK7oagH86m7Qw==
-"@sentry/browser@^5.12.1":
- version "5.12.1"
- resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.12.1.tgz#dc1f268595269fb7277f55eb625c7e92d76dc01b"
- integrity sha512-Zl7VdppUxctyaoqMSEhnDJp2rrupx8n8N2n3PSooH74yhB2Z91nt84mouczprBsw3JU1iggGyUw9seRFzDI1hw==
+"@sentry/browser@^6.3.6":
+ version "6.3.6"
+ resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.3.6.tgz#bba07033efded6c844de88dcc47f99548a29afed"
+ integrity sha512-l4323jxuBOArki6Wf+EHes39IEyJ2Zj/CIUaTY7GWh7CntpfHQAfFmZWQw3Ozq+ka1u8lVp25RPhb4Wng3azNA==
dependencies:
- "@sentry/core" "5.12.0"
- "@sentry/types" "5.12.0"
- "@sentry/utils" "5.12.0"
+ "@sentry/core" "6.3.6"
+ "@sentry/types" "6.3.6"
+ "@sentry/utils" "6.3.6"
tslib "^1.9.3"
-"@sentry/cli@^1.51.0":
- version "1.51.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.51.0.tgz#2ce7d03b7aebf3f9e9a7186bac1a777fc3dc0e82"
- integrity sha512-QXdW3smFW9Wjd5gYHuA9u9tCra87VqpeFljRKdD7D2CwnYCnFDeluVk3l9O8Me6IoVBuL9Uwxx8q1vPtob4n3Q==
+"@sentry/cli@^1.64.2":
+ version "1.64.2"
+ resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.64.2.tgz#38a68d31692675061fdedd27d4006a249bc915a9"
+ integrity sha512-hBF8cswn878W7YqAgeg/vcQ51BgtpeiklGV6Q5MEXqGXtbjaKTBYBcKTkhzpkkcUtTu5bFkVcdGzVtpl4oBe+g==
dependencies:
- fs-copy-file-sync "^1.1.1"
- https-proxy-agent "^4.0.0"
- mkdirp "^0.5.1"
- node-fetch "^2.1.2"
- progress "2.0.0"
- proxy-from-env "^1.0.0"
+ https-proxy-agent "^5.0.0"
+ mkdirp "^0.5.5"
+ node-fetch "^2.6.0"
+ npmlog "^4.1.2"
+ progress "^2.0.3"
+ proxy-from-env "^1.1.0"
-"@sentry/core@5.12.0":
- version "5.12.0"
- resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.12.0.tgz#d6380c4ef7beee5f418ac1d0e5be86a2de2af449"
- integrity sha512-wY4rsoX71QsGpcs9tF+OxKgDPKzIFMRvFiSRcJoPMfhFsTilQ/CBMn/c3bDtWQd9Bnr/ReQIL6NbnIjUsPHA4Q==
+"@sentry/core@6.3.6":
+ version "6.3.6"
+ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.3.6.tgz#e2ec6ae7e456e61f28000bab2d8ce85f58c59c66"
+ integrity sha512-w6BRizAqh7BaiM9oeKzO6aACXwRijUPacYaVLX/OfhqCSueF9uDxpMRT7+4D/eCeDVqgJYhBJ4Vsu2NSstkk4A==
dependencies:
- "@sentry/hub" "5.12.0"
- "@sentry/minimal" "5.12.0"
- "@sentry/types" "5.12.0"
- "@sentry/utils" "5.12.0"
+ "@sentry/hub" "6.3.6"
+ "@sentry/minimal" "6.3.6"
+ "@sentry/types" "6.3.6"
+ "@sentry/utils" "6.3.6"
tslib "^1.9.3"
-"@sentry/hub@5.12.0":
- version "5.12.0"
- resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.12.0.tgz#5e8c8f249f5bdbeb8cc4ec02c2ccc53a67f2cc02"
- integrity sha512-3k7yE8BEVJsKx8mR4LcI4IN0O8pngmq44OcJ/fRUUBAPqsT38jsJdP2CaWhdlM1jiNUzUDB1ktBv6/lY+VgcoQ==
+"@sentry/hub@6.3.6":
+ version "6.3.6"
+ resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.3.6.tgz#e7bc6960e30d8731e23c6e77f31af0bfb1d5af3c"
+ integrity sha512-foBZ3ilMnm9Gf9OolrAxYHK8jrA6IF72faDdJ3Al+1H27qcpnBaMdrdEp2/jzwu/dgmwuLmbBaMjEPXaGH/0JQ==
dependencies:
- "@sentry/types" "5.12.0"
- "@sentry/utils" "5.12.0"
+ "@sentry/types" "6.3.6"
+ "@sentry/utils" "6.3.6"
tslib "^1.9.3"
-"@sentry/minimal@5.12.0":
- version "5.12.0"
- resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.12.0.tgz#2611e2aa520c1edb7999e6de51bd65ec66341757"
- integrity sha512-fk73meyz4k4jCg9yzbma+WkggsfEIQWI2e2TWfYsRGcrV3RnlSrXyM4D91/A8Bjx10SNezHPUFHjasjlHXOkyA==
+"@sentry/minimal@6.3.6":
+ version "6.3.6"
+ resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.3.6.tgz#aebcebd2ee9007b0ec505b9fcefd10f10fc5d43d"
+ integrity sha512-uM2/dH0a6zfvI5f+vg+/mST+uTBdN6Jgpm585ipH84ckCYQwIIDRg6daqsen4S1sy/xgg1P1YyC3zdEC4G6b1Q==
dependencies:
- "@sentry/hub" "5.12.0"
- "@sentry/types" "5.12.0"
+ "@sentry/hub" "6.3.6"
+ "@sentry/types" "6.3.6"
tslib "^1.9.3"
-"@sentry/types@5.12.0":
- version "5.12.0"
- resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.12.0.tgz#5367e53c74261beea01502e3f7b6f3d822682a31"
- integrity sha512-aZbBouBLrKB8wXlztriIagZNmsB+wegk1Jkl6eprqRW/w24Sl/47tiwH8c5S4jYTxdAiJk+SAR10AAuYmIN3zg==
+"@sentry/types@6.3.6":
+ version "6.3.6"
+ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.3.6.tgz#aa3687051af1dc04ebc4eaf7f9562872da67aa5c"
+ integrity sha512-93cFJdJkWyCfyZeWFARSU11qnoHVOS/R2h5WIsEf+jbQmkqG2C+TXVz/19s6nHVsfDrwpvYpwALPv4/nrxfU7g==
-"@sentry/utils@5.12.0":
- version "5.12.0"
- resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.12.0.tgz#62967f934a3ee6d21472eac0219084e37225933e"
- integrity sha512-fYUadGLbfTCbs4OG5hKCOtv2jrNE4/8LHNABy9DwNJ/t5DVtGqWAZBnxsC+FG6a3nVqCpxjFI9AHlYsJ2wsf7Q==
+"@sentry/utils@6.3.6":
+ version "6.3.6"
+ resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.3.6.tgz#6f619a525f2a94fa6b160500f63f4bd5bd171055"
+ integrity sha512-HnYlDBf8Dq8MEv7AulH7B6R1D/2LAooVclGdjg48tSrr9g+31kmtj+SAj2WWVHP9+bp29BWaC7i5nkfKrOibWw==
dependencies:
- "@sentry/types" "5.12.0"
+ "@sentry/types" "6.3.6"
tslib "^1.9.3"
"@sheerun/mutationobserver-shim@^0.3.2":
@@ -1759,6 +1759,11 @@
resolved "https://registry.yarnpkg.com/@types/js-base64/-/js-base64-2.3.1.tgz#c39f14f129408a3d96a1105a650d8b2b6eeb4168"
integrity sha512-4RKbhIDGC87s4EBy2Cp2/5S2O6kmCRcZnD5KRCq1q9z2GhBte1+BdsfVKCpG8yKpDGNyEE2G6IqFIh6W2YwWPA==
+"@types/js-cookie@2.2.6":
+ version "2.2.6"
+ resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f"
+ integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw==
+
"@types/minimatch@^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@@ -2044,6 +2049,11 @@
"@webassemblyjs/wast-parser" "1.8.5"
"@xtuc/long" "4.2.2"
+"@xobotyi/scrollbar-width@1.9.5":
+ version "1.9.5"
+ resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d"
+ integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==
+
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@@ -2209,6 +2219,13 @@ agent-base@5:
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c"
integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==
+agent-base@6:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
+ integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
+ dependencies:
+ debug "4"
+
ajv-errors@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
@@ -4898,6 +4915,11 @@ fast-deep-equal@^3.1.1:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
+fast-deep-equal@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+ integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
fast-json-patch@^2.0.6:
version "2.2.1"
resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-2.2.1.tgz#18150d36c9ab65c7209e7d4eb113f4f8eaabe6d9"
@@ -4925,6 +4947,11 @@ fast-safe-stringify@^2.0.7:
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
+fast-shallow-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b"
+ integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==
+
fastest-stable-stringify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-1.0.1.tgz#9122d406d4c9d98bea644a6b6853d5874b87b028"
@@ -5222,11 +5249,6 @@ fs-constants@^1.0.0:
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
-fs-copy-file-sync@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/fs-copy-file-sync/-/fs-copy-file-sync-1.1.1.tgz#11bf32c096c10d126e5f6b36d06eece776062918"
- integrity sha512-2QY5eeqVv4m2PfyMiEuy9adxNP+ajf+8AR05cEi+OAzPcOj90hvFImeZhTmKLBgSd9EvG33jsD7ZRxsx9dThkQ==
-
fs-exists-sync@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add"
@@ -5776,6 +5798,14 @@ https-proxy-agent@^4.0.0:
agent-base "5"
debug "4"
+https-proxy-agent@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
+ integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
+ dependencies:
+ agent-base "6"
+ debug "4"
+
human-signals@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
@@ -6758,6 +6788,11 @@ js-base64@^2.5.1:
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121"
integrity sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==
+js-cookie@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
+ integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
+
js-levenshtein@^1.1.3:
version "1.1.6"
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
@@ -7500,7 +7535,7 @@ mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1:
dependencies:
minimist "0.0.8"
-mkdirp@^0.5.3:
+mkdirp@^0.5.3, mkdirp@^0.5.5:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
@@ -7658,12 +7693,7 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
-node-fetch@^2.1.2:
- version "2.6.0"
- resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
- integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
-
-node-fetch@^2.6.1:
+node-fetch@^2.6.0, node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
@@ -7829,7 +7859,7 @@ npm-run-path@^4.0.0:
dependencies:
path-key "^3.0.0"
-npmlog@^4.0.2:
+npmlog@^4.0.2, npmlog@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@@ -8492,17 +8522,12 @@ process@^0.11.10:
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
-progress@2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
- integrity sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=
-
progress@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=
-progress@^2.0.0, progress@^2.0.1:
+progress@^2.0.0, progress@^2.0.1, progress@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
@@ -8542,6 +8567,11 @@ proxy-from-env@^1.0.0:
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=
+proxy-from-env@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+ integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
prr@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
@@ -8715,19 +8745,14 @@ react-clientside-effect@^1.2.2:
dependencies:
"@babel/runtime" "^7.0.0"
-react-dom@^17.0.1:
- version "17.0.1"
- resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6"
- integrity sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug==
+react-dom@^17.0.2:
+ version "17.0.2"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
+ integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
- scheduler "^0.20.1"
-
-react-fast-compare@^2.0.4:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
- integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
+ scheduler "^0.20.2"
react-focus-lock@^2.1.0:
version "2.2.1"
@@ -8783,20 +8808,30 @@ react-style-singleton@^2.0.0:
invariant "^2.2.4"
tslib "^1.0.0"
-react-use@^13.8.0:
- version "13.8.0"
- resolved "https://registry.yarnpkg.com/react-use/-/react-use-13.8.0.tgz#5e8badaaa5614a1925fd28ad22d01cc1c52e0ff1"
- integrity sha512-J4ZWIqC1h2eSS6ObmHJUWDQeJSKZvHdBEINCu51plz7RS+ijrt6JEHfDWbNXx+aAoR5AmbRK+mzYwd3jiu3xpA==
+react-universal-interface@^0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b"
+ integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==
+
+react-use@^15.3.0:
+ version "15.3.8"
+ resolved "https://registry.yarnpkg.com/react-use/-/react-use-15.3.8.tgz#ca839ac7fb3d696e5ccbeabbc8dadc2698969d30"
+ integrity sha512-GeGcrmGuUvZrY5wER3Lnph9DSYhZt5nEjped4eKDq8BRGr2CnLf9bDQWG9RFc7oCPphnscUUdOovzq0E5F2c6Q==
dependencies:
+ "@types/js-cookie" "2.2.6"
+ "@xobotyi/scrollbar-width" "1.9.5"
copy-to-clipboard "^3.2.0"
+ fast-deep-equal "^3.1.3"
+ fast-shallow-equal "^1.0.0"
+ js-cookie "^2.2.1"
nano-css "^5.2.1"
- react-fast-compare "^2.0.4"
+ react-universal-interface "^0.6.2"
resize-observer-polyfill "^1.5.1"
screenfull "^5.0.0"
set-harmonic-interval "^1.0.1"
throttle-debounce "^2.1.0"
ts-easing "^0.2.0"
- tslib "^1.10.0"
+ tslib "^2.0.0"
react-window@^1.8.5:
version "1.8.5"
@@ -8807,9 +8842,9 @@ react-window@^1.8.5:
memoize-one ">=3.1.1 <6"
react@^16.10.2, react@^17, react@^17.0.1:
- version "17.0.1"
- resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
- integrity sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==
+ version "17.0.2"
+ resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
+ integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
@@ -9354,10 +9389,10 @@ sax@>=0.6.0, sax@^1.2.4:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
-scheduler@^0.20.1:
- version "0.20.1"
- resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.1.tgz#da0b907e24026b01181ecbc75efdc7f27b5a000c"
- integrity sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw==
+scheduler@^0.20.2:
+ version "0.20.2"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
+ integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
@@ -10471,11 +10506,16 @@ tslib@^1.0.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
-tslib@^1.10.0, tslib@^1.9.0, tslib@^1.9.3:
+tslib@^1.9.0, tslib@^1.9.3:
version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
+tslib@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
+ integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
+
tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
@@ -10540,10 +10580,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
-typescript@^4.0.3:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz#153bbd468ef07725c1df9c77e8b453f8d36abba5"
- integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==
+typescript@^4.2.4:
+ version "4.2.4"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961"
+ integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==
uglify-js@^3.6.0:
version "3.6.0"