mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
refactor: control the flow using react
This commit is contained in:
parent
5c25d9d0d8
commit
72f825db2e
23 changed files with 351 additions and 251 deletions
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
8
src/components/IIFC.tsx
Normal 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()}</>
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
98
src/components/RepoContext.tsx
Normal file
98
src/components/RepoContext.tsx
Normal 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
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
26
src/components/SideBarState.tsx
Normal file
26
src/components/SideBarState.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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'} />
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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
4
src/global.d.ts
vendored
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export const Gitea: Platform = {
|
|||
isEnterprise() {
|
||||
return !window.location.host.endsWith('gitea.com')
|
||||
},
|
||||
resolveMeta() {
|
||||
resolvePartialMetaData() {
|
||||
if (!DOMHelper.isInRepoPage()) {
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export const Gitee: Platform = {
|
|||
isEnterprise() {
|
||||
return !window.location.host.endsWith('gitee.com')
|
||||
},
|
||||
resolveMeta() {
|
||||
resolvePartialMetaData() {
|
||||
if (!DOMHelper.isInRepoPage()) {
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ export const dummyPlatformForTypeSafety: Platform = {
|
|||
isEnterprise() {
|
||||
return false
|
||||
},
|
||||
resolveMeta() {
|
||||
resolvePartialMetaData() {
|
||||
return null
|
||||
},
|
||||
getDefaultBranchName: dummyPlatformMethod,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
8
src/platforms/platform.d.ts
vendored
8
src/platforms/platform.d.ts
vendored
|
|
@ -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,
|
||||
|
|
|
|||
9
src/utils/hooks/useEffectOnSerializableUpdates.tsx
Normal file
9
src/utils/hooks/useEffectOnSerializableUpdates.tsx
Normal 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)])
|
||||
}
|
||||
7
src/utils/hooks/useLoadedContext.tsx
Normal file
7
src/utils/hooks/useLoadedContext.tsx
Normal 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
|
||||
}
|
||||
Loading…
Reference in a new issue