refactor: control the flow using react

This commit is contained in:
EnixCoda 2021-05-05 22:28:31 +08:00
parent 5c25d9d0d8
commit 72f825db2e
No known key found for this signature in database
GPG key ID: 0C1A07377913A1DD
23 changed files with 351 additions and 251 deletions

View file

@ -1,4 +1,4 @@
import { Text } from '@primer/components'
import { Label, Text } from '@primer/components'
import { LoadingIndicator } from 'components/LoadingIndicator'
import { Node } from 'components/Node'
import { SearchBar } from 'components/SearchBar'
@ -11,11 +11,14 @@ import * as React from 'react'
import { FixedSizeList, ListChildComponentProps } from 'react-window'
import { cx } from 'utils/cx'
import { focusFileExplorer } from 'utils/DOMHelper'
import { run } from 'utils/general'
import { useLoadedContext } from 'utils/hooks/useLoadedContext'
import { useOnLocationChange } from 'utils/hooks/useOnLocationChange'
import { useOnPJAXDone } from 'utils/hooks/usePJAX'
import { VisibleNodes } from 'utils/VisibleNodesGenerator'
import { Icon } from './Icon'
import { SearchMode, searchModes } from './searchModes'
import { SideBarStateContext } from './SideBarState'
import { SizeObserver } from './SizeObserver'
type renderNodeContext = {
@ -27,7 +30,6 @@ type renderNodeContext = {
const RawFileExplorer: React.FC<Props & ConnectorState> = function RawFileExplorer(props) {
const {
state,
visibleNodes,
visibleNodesGenerator,
freeze,
@ -37,11 +39,9 @@ const RawFileExplorer: React.FC<Props & ConnectorState> = function RawFileExplor
onFocusSearchBar,
goTo,
handleKeyDown,
toggleShowSettings,
metaData,
expandTo,
setUpTree,
treeRoot,
defer,
searched,
} = props
@ -59,18 +59,19 @@ const RawFileExplorer: React.FC<Props & ConnectorState> = function RawFileExplor
[updateSearchKey, visibleNodesGenerator],
)
const stateContext = useLoadedContext(SideBarStateContext)
const state = stateContext.value
React.useEffect(() => {
if (treeRoot) {
setUpTree({
treeRoot,
metaData,
config: {
compressSingletonFolder,
accessToken,
},
})
}
}, [setUpTree, treeRoot, metaData, compressSingletonFolder, accessToken])
setUpTree({
metaData,
config: {
compressSingletonFolder,
accessToken,
},
stateContext,
})
}, [setUpTree, metaData, compressSingletonFolder, accessToken])
React.useEffect(() => {
if (visibleNodes?.focusedNode) focusFileExplorer()
@ -131,52 +132,63 @@ const RawFileExplorer: React.FC<Props & ConnectorState> = function RawFileExplor
)
return (
<div
className={cx(`file-explorer`, { freeze })}
tabIndex={-1}
onKeyDown={handleKeyDown}
onClick={freeze ? toggleShowSettings : undefined}
>
{state !== 'done' ? (
<LoadingIndicator text={'Rendering File List...'} />
) : (
visibleNodes &&
renderNodeContext && (
<>
<SearchBar value={searchKey} onSearch={onSearch} onFocus={onFocusSearchBar} />
{searched && visibleNodes.nodes.length === 0 && (
<>
<Text marginTop={6} textAlign="center" color="text.gray">
No results found.
</Text>
{defer && (
<Text textAlign="center" color="gray.4" fontSize="12px">
Lazy mode is ON. Search results are limited to loaded folders.
</Text>
)}
</>
)}
<SizeObserver className={'files'}>
{({ width = 0, height = 0 }) => (
<ListView
height={height}
width={width}
renderNodeContext={renderNodeContext}
expandTo={expandTo}
metaData={metaData}
/>
)}
</SizeObserver>
</>
)
)}
<div className={cx(`file-explorer`, { freeze })} tabIndex={-1} onKeyDown={handleKeyDown}>
{run(() => {
switch (state) {
case 'tree-loading':
return <LoadingIndicator text={'Fetching File List...'} />
case 'tree-rendering':
return <LoadingIndicator text={'Rendering File List...'} />
case 'tree-rendered':
return (
visibleNodes &&
renderNodeContext && (
<>
<SearchBar value={searchKey} onSearch={onSearch} onFocus={onFocusSearchBar} />
{searched && visibleNodes.nodes.length === 0 && (
<>
<Text marginTop={6} textAlign="center" color="text.gray">
No results found.
</Text>
{defer && (
<Text textAlign="center" color="gray.4" fontSize="12px">
Lazy mode is ON. Search results are limited to loaded folders.
</Text>
)}
</>
)}
<SizeObserver className={'files'}>
{({ width = 0, height = 0 }) => (
<ListView
height={height}
width={width}
renderNodeContext={renderNodeContext}
expandTo={expandTo}
metaData={metaData}
/>
)}
</SizeObserver>
{defer && (
<Label
title="File tree data is loaded on demand. And search results are limited."
bg="yellow.5"
color="gray.6"
>
Lazy Mode
</Label>
)}
</>
)
)
}
})}
</div>
)
}
RawFileExplorer.defaultProps = {
freeze: false,
state: 'rendering',
searchKey: '',
visibleNodes: null,
}

View file

@ -1,15 +1,29 @@
import { IIFC } from 'components/IIFC'
import { SideBar } from 'components/SideBar'
import { ConfigsContext, ConfigsContextWrapper } from 'containers/ConfigsContext'
import { ConfigsContextWrapper, useConfigs } from 'containers/ConfigsContext'
import * as React from 'react'
import { useLoadedContext } from 'utils/hooks/useLoadedContext'
import { ErrorBoundary } from './ErrorBoundary'
import { RepoContext, RepoContextWrapper } from './RepoContext'
import { SideBarStateContext, StateBarStateContextWrapper } from './SideBarState'
export function Gitako() {
return (
<ErrorBoundary>
<ConfigsContextWrapper>
<ConfigsContext.Consumer>
{configContext => configContext && <SideBar configContext={configContext} />}
</ConfigsContext.Consumer>
<StateBarStateContextWrapper>
<RepoContextWrapper>
<IIFC>
{() => (
<SideBar
configContext={useConfigs()}
stateContext={useLoadedContext(SideBarStateContext)}
metaData={React.useContext(RepoContext)}
/>
)}
</IIFC>
</RepoContextWrapper>
</StateBarStateContextWrapper>
</ConfigsContextWrapper>
</ErrorBoundary>
)

8
src/components/IIFC.tsx Normal file
View file

@ -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()}</>
}

View file

@ -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) {

View file

@ -0,0 +1,98 @@
import { useConfigs } from 'containers/ConfigsContext'
import { platform } from 'platforms'
import * as React from 'react'
import { run } from 'utils/general'
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 { SideBarStateContext } from './SideBarState'
export const RepoContext = React.createContext<MetaData | null>(null)
export function RepoContextWrapper({ children }: React.PropsWithChildren<{}>) {
const partialMetaData = usePartialMetaData()
const defaultBranch = useDefaultBranch(partialMetaData)
const metaData = useMetaData(partialMetaData, defaultBranch)
return <RepoContext.Provider value={metaData}>{metaData && children}</RepoContext.Provider>
}
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 {
// sync along URL and DOM
const $partialMetaData = useStateIO(resolvePartialMetaData)
const $committedPartialMetaData = useStateIO($partialMetaData.value)
useOnPJAXDone(() => $partialMetaData.onChange(resolvePartialMetaData()))
useEffectOnSerializableUpdates(
$partialMetaData.value,
JSON.stringify,
$committedPartialMetaData.onChange,
)
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 $defaultBranch = useStateIO<string | null>(null)
React.useEffect(() => {
run(async () => {
if (!partialMetaData) return
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<MetaData | null>(null)
const branchName = useBranchName()
React.useEffect(() => {
if (!partialMetaData) {
$state.onChange('disabled')
} else if (!defaultBranchName) {
$state.onChange('meta-loading')
}
if (partialMetaData && defaultBranchName) {
const { userName, repoName } = partialMetaData
const safeMetaData: MetaData = {
userName,
repoName,
branchName: branchName || defaultBranchName,
defaultBranchName,
}
$metaData.onChange(safeMetaData)
} else {
$metaData.onChange(null)
}
$state.onChange('meta-loaded')
}, [partialMetaData, branchName, defaultBranchName])
return $metaData.value
}

View file

@ -5,7 +5,7 @@ import { useDebounce, useWindowSize } from 'react-use'
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 = {

View file

@ -5,6 +5,7 @@ import { Portal } from 'components/Portal'
import { Resizable } from 'components/Resizable'
import { SettingsBar } from 'components/settings/SettingsBar'
import { ToggleShowButton } from 'components/ToggleShowButton'
import { useConfigs } from 'containers/ConfigsContext'
import { connect } from 'driver/connect'
import { SideBarCore } from 'driver/core'
import { ConnectorState, Props } from 'driver/core/SideBar'
@ -12,33 +13,46 @@ import { platform } from 'platforms'
import { useGitHubAttachCopyFileButton, useGitHubAttachCopySnippetButton } from 'platforms/GitHub'
import * as React from 'react'
import { cx } from 'utils/cx'
import * as DOMHelper from 'utils/DOMHelper'
import { parseURLSearch, run } from 'utils/general'
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 { Icon } from './Icon'
import { LoadingIndicator } from './LoadingIndicator'
import { SideBarStateContext } from './SideBarState'
import { Theme } from './Theme'
const RawGitako: React.FC<Props & ConnectorState> = function RawGitako(props) {
const {
metaData,
treeData,
state,
defer,
error,
shouldShow,
showSettings,
logoContainerElement,
toggleShowSideBar,
toggleShowSettings,
configContext,
} = props
const RawSideBar: React.FC<Props & ConnectorState> = function RawGitako(props) {
const { metaData, error, shouldShow, toggleShowSideBar } = props
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(function toggleShowSettings() {
$showSettings.onChange(show => !show)
}, [])
const $logoContainerElement = useStateIO<HTMLElement | null>(null)
const hasMetaData = state !== 'disabled'
React.useEffect(() => {
if (hasMetaData) {
DOMHelper.markGitakoReadyState(true)
$showSettings.onChange(false)
$logoContainerElement.onChange(DOMHelper.insertLogoMountPoint())
} else {
DOMHelper.markGitakoReadyState(false)
}
}, [hasMetaData])
React.useEffect(() => {
run(async function () {
if (!accessToken) {
@ -48,20 +62,6 @@ const RawGitako: React.FC<Props & ConnectorState> = function RawGitako(props) {
})
}, [])
/**
* 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
@ -89,7 +89,7 @@ const RawGitako: React.FC<Props & ConnectorState> = function RawGitako(props) {
const shouldShow = intelligentToggle === null ? platform.shouldShow() : intelligentToggle
props.setShouldShow(shouldShow)
}
}, [intelligentToggle, hideSidebarOnInvalidToken, props.metaData])
}, [intelligentToggle, hideSidebarOnInvalidToken, metaData])
const updateSideBarVisibility = React.useCallback(
function updateSideBarVisibility() {
@ -99,7 +99,7 @@ const RawGitako: React.FC<Props & ConnectorState> = function RawGitako(props) {
props.setShouldShow(platform.shouldShow())
}
},
[props.metaData?.branchName, intelligentToggle, hideSidebarOnInvalidToken],
[metaData?.branchName, intelligentToggle, hideSidebarOnInvalidToken],
)
useOnPJAXDone(updateSideBarVisibility)
@ -112,7 +112,7 @@ const RawGitako: React.FC<Props & ConnectorState> = function RawGitako(props) {
return (
<Theme>
<div className={'gitako-side-bar'}>
<Portal into={logoContainerElement}>
<Portal into={$logoContainerElement.value}>
{!shouldShow && (
<ToggleShowButton error={error} onClick={error ? undefined : toggleShowSideBar} />
)}
@ -124,41 +124,37 @@ const RawGitako: React.FC<Props & ConnectorState> = function RawGitako(props) {
<Icon className={'action-icon'} type={'x'} />
</button>
</div>
<div className={'gitako-side-bar-content'}>
<div
className={'gitako-side-bar-content'}
onClick={showSettings ? toggleShowSettings : undefined}
>
{run(() => {
switch (state) {
case 'loading-meta':
case 'disabled':
return null
case 'meta-loading':
return <LoadingIndicator text={'Fetching repo meta...'} />
case 'loading-tree':
return <LoadingIndicator text={'Fetching File List...'} />
case 'idle':
case 'error-due-to-auth':
return <AccessDeniedDescription hasToken={Boolean(accessToken)} />
default:
return metaData ? (
<>
<div className={'header'}>
<MetaBar metaData={metaData} />
</div>
<FileExplorer
toggleShowSettings={toggleShowSettings}
metaData={metaData}
treeRoot={treeData}
freeze={showSettings}
accessToken={accessToken}
loadWithPJAX={loadWithPJAX}
config={configContext.value}
defer={defer}
/>
</>
) : null
case 'error-due-to-auth':
return <AccessDeniedDescription hasToken={Boolean(accessToken)} />
}
})}
</div>
<SettingsBar
defer={defer}
toggleShowSettings={toggleShowSettings}
activated={showSettings}
/>
<SettingsBar toggleShowSettings={toggleShowSettings} activated={showSettings} />
</div>
</Resizable>
</div>
@ -166,13 +162,11 @@ const RawGitako: React.FC<Props & ConnectorState> = function RawGitako(props) {
)
}
RawGitako.defaultProps = {
RawSideBar.defaultProps = {
shouldShow: false,
showSettings: false,
state: 'loading-meta',
}
export const SideBar = connect(SideBarCore)(RawGitako)
export const SideBar = connect(SideBarCore)(RawSideBar)
async function trySetUpAccessTokenWithCode() {
const search = parseURLSearch()

View file

@ -0,0 +1,26 @@
import * as React from 'react'
import { useStateIO } from 'utils/hooks/useStateIO'
export type SideBarState =
| 'disabled'
| 'meta-loading'
| 'meta-loaded'
| 'tree-loading'
| 'tree-rendering'
| 'tree-rendered'
| 'idle'
| 'error-due-to-auth'
export type SideBarStateContextShape = IO<SideBarState>
export const SideBarStateContext = React.createContext<SideBarStateContextShape | null>(null)
export function StateBarStateContextWrapper({ children }: React.PropsWithChildren<{}>) {
const $state = useStateIO<SideBarState>('disabled')
return (
<SideBarStateContext.Provider value={$state}>
{$state.value !== null && children}
</SideBarStateContext.Provider>
)
}

View file

@ -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
}
@ -78,7 +77,7 @@ function SettingsBarContent() {
}
export function SettingsBar(props: Props) {
const { defer, toggleShowSettings, activated } = props
const { toggleShowSettings, activated } = props
return (
<div className={'gitako-settings-bar'}>
{activated && <SettingsBarContent />}
@ -93,15 +92,6 @@ export function SettingsBar(props: Props) {
{VERSION}
</Link>
<div className={'header-right'}>
{defer && (
<Label
title="File tree data is loaded on demand. And search results are limited."
bg="yellow.5"
color="gray.6"
>
Lazy Mode
</Label>
)}
<button className={'settings-button'} onClick={toggleShowSettings}>
{activated ? (
<Icon type={'chevron-down'} className={'hide-settings-icon'} />

View file

@ -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') {

View file

@ -1,3 +1,4 @@
import { SideBarStateContextShape } from 'components/SideBarState'
import { GetCreatedMethod, MethodCreator } from 'driver/connect'
import { platform } from 'platforms'
import { Config } from 'utils/configHelper'
@ -5,22 +6,19 @@ 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
}
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<typeof handleKeyDown>
updateSearchKey: GetCreatedMethod<typeof updateSearchKey>
@ -45,12 +43,27 @@ type BoundMethodCreator<Args extends any[] = []> = MethodCreator<Props, Connecto
export const setUpTree: BoundMethodCreator<
[
Required<Pick<Props, 'treeRoot' | 'metaData'>> & {
config: Pick<Config, 'compressSingletonFolder' | 'accessToken'>
},
{ stateContext: SideBarStateContextShape } & Required<Pick<Props, 'metaData'>> & {
config: Pick<Config, 'compressSingletonFolder' | 'accessToken'>
},
]
> = dispatch => async ({ treeRoot, metaData, config }) => {
dispatch.set({ state: 'rendering' })
> = dispatch => async ({ stateContext, metaData, config }) => {
const { userName, repoName, branchName } = metaData
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,
@ -75,7 +88,7 @@ export const setUpTree: BoundMethodCreator<
if (targetPath) dispatch.call(goTo, targetPath)
}
dispatch.set({ state: 'done' })
stateContext.onChange('tree-rendered')
}
export const handleKeyDown: BoundMethodCreator<[React.KeyboardEvent]> = dispatch => event => {

View file

@ -1,11 +1,14 @@
import { SideBarStateContextShape } from 'components/SideBarState'
import { ConfigsContextShape } from 'containers/ConfigsContext'
import { GetCreatedMethod, MethodCreator } from 'driver/connect'
import { errors, platform, platformName } from 'platforms'
import { errors, platformName } from 'platforms'
import * as DOMHelper from 'utils/DOMHelper'
import { createPromiseQueue } from 'utils/general'
export type Props = {
// meta data for the repository
metaData: MetaData | null
configContext: ConfigsContextShape
stateContext: SideBarStateContextShape
}
export type ConnectorState = {
@ -13,108 +16,17 @@ export type ConnectorState = {
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<typeof init>
setShouldShow: GetCreatedMethod<typeof setShouldShow>
toggleShowSideBar: GetCreatedMethod<typeof toggleShowSideBar>
toggleShowSettings: GetCreatedMethod<typeof toggleShowSettings>
}
type BoundMethodCreator<Args extends any[] = []> = MethodCreator<Props, ConnectorState, Args>
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 } = 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
const defaultBranchName = await platform.getDefaultBranchName(
{ userName, repoName },
accessToken,
)
if (!defaultBranchName) {
throw new Error(`Failed resolving default branch name`)
}
const safeMetaData: MetaData = {
userName,
repoName,
branchName: branchName || defaultBranchName,
defaultBranchName,
}
dispatch.set({ metaData: safeMetaData })
if (branchName) {
getTreeData.catch(error => {
dispatch.call(handleError, error)
})
} else 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 => {
const {
props: { stateContext },
} = dispatch.get()
if (err.message === errors.EMPTY_PROJECT) {
dispatch.call(setError, 'This project seems to be empty.')
} else if (err.message === errors.BLOCKED_PROJECT) {
@ -124,13 +36,13 @@ export const handleError: BoundMethodCreator<[Error]> = dispatch => async err =>
err.message === errors.BAD_CREDENTIALS ||
err.message === errors.API_RATE_LIMIT
) {
dispatch.set({ state: 'error-due-to-auth' })
stateContext.onChange('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' })
stateContext.onChange('error-due-to-auth')
}
} else if (err.message === errors.SERVER_FAULT) {
dispatch.call(setError, `${platformName} server went down.`)
@ -167,8 +79,3 @@ export const setError: BoundMethodCreator<[ConnectorState['error']]> = dispatch
dispatch.set({ error })
dispatch.call(setShouldShow, false)
}
export const toggleShowSettings: BoundMethodCreator = dispatch => () =>
dispatch.set(({ showSettings }) => ({
showSettings: !showSettings,
}))

4
src/global.d.ts vendored
View file

@ -1,11 +1,13 @@
type MetaData = {
userName: string
repoName: string
defaultBranchName: string
branchName: string
defaultBranchName?: string
type?: EnumString<'tree' | 'blob' | 'pull'>
}
type PartialMetaData = Omit<MakeOptional<MetaData, 'branchName'>, 'defaultBranchName'>
type TreeNode = {
name: string
contents?: TreeNode[]

View file

@ -232,3 +232,21 @@ export function attachCopySnippet() {
)
})
}
export function getPath() {
const folderPathElementSelector = '.file-navigation .position-relative' // available when in path like '/tree/...'
const blobPathElementSelector = '#blob-path' // available when in path like '/blob/...'
const pathElement =
document.querySelector(blobPathElementSelector) ||
document.querySelector(folderPathElementSelector)?.nextElementSibling
if (!pathElement?.querySelector('.js-repo-root')) {
return []
}
const path = ((pathElement as HTMLDivElement).innerText || '')
.replace(/ \/ Jump to $/, '')
.trim()
.split('/')
.filter(Boolean)
.slice(1) // the first is the repo's name
return path
}

View file

@ -107,7 +107,7 @@ const pathSHAMap = new Map<string, string>()
export const GitHub: Platform = {
isEnterprise,
resolveMeta() {
resolvePartialMetaData() {
if (!DOMHelper.isInRepoPage()) {
return null
}
@ -139,10 +139,8 @@ export const GitHub: Platform = {
}
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 {
@ -257,7 +255,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)

View file

@ -77,7 +77,7 @@ export const Gitea: Platform = {
isEnterprise() {
return !window.location.host.endsWith('gitea.com')
},
resolveMeta() {
resolvePartialMetaData() {
if (!DOMHelper.isInRepoPage()) {
return null
}

View file

@ -73,7 +73,7 @@ export const Gitee: Platform = {
isEnterprise() {
return !window.location.host.endsWith('gitee.com')
},
resolveMeta() {
resolvePartialMetaData() {
if (!DOMHelper.isInRepoPage()) {
return null
}

View file

@ -2,7 +2,7 @@ export const dummyPlatformForTypeSafety: Platform = {
isEnterprise() {
return false
},
resolveMeta() {
resolvePartialMetaData() {
return null
},
getDefaultBranchName: dummyPlatformMethod,

View file

@ -13,7 +13,7 @@ const platforms: {
function resolvePlatform() {
for (const platform of Object.values(platforms)) {
if (platform.resolveMeta()) return platform
if (platform.resolvePartialMetaData()) return platform
}
return dummyPlatformForTypeSafety
}

View file

@ -1,20 +1,20 @@
type Platform = {
isEnterprise(): boolean
// branch name might not be available when resolving from DOM and URL
resolveMeta(): MakeOptional<MetaData, 'branchName'> | null
resolvePageScope?(branchName?: string): string
resolvePartialMetaData(): PartialMetaData | null
// resolveMetaData(metaData: PartialMetaData, accessToken?: string): Async<MetaData>
getDefaultBranchName(
metaData: Pick<MetaData, 'userName' | 'repoName'>,
accessToken?: string,
): Promise<string>
resolveUrlFromMetaData(
metaData: MetaData,
metaData: Pick<MetaData, 'userName' | 'repoName'>,
): {
userUrl: string
repoUrl: string
}
getTreeData(
metaData: MetaData,
metaData: Pick<MetaData, 'userName' | 'repoName' | 'branchName'>,
path?: string,
recursive?: boolean,
accessToken?: string,

View file

@ -0,0 +1,9 @@
import * as React from 'react'
export function useEffectOnSerializableUpdates<T>(
value: T,
serialize: (value: T) => string,
onChange: (value: T) => void,
) {
React.useEffect(() => onChange(value), [onChange, serialize(value)])
}

View file

@ -0,0 +1,7 @@
import * as React from 'react'
export function useLoadedContext<T>(context: React.Context<T | null>): T {
const ctx = React.useContext(context)
if (ctx === null) throw new Error(`Context not loaded`)
return ctx
}