From 72f825db2e83dd1330d88b72946f65b1d902958f Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Wed, 5 May 2021 22:28:31 +0800 Subject: [PATCH] refactor: control the flow using react --- src/components/FileExplorer.tsx | 122 ++++++++++-------- src/components/Gitako.tsx | 22 +++- src/components/IIFC.tsx | 8 ++ src/components/Node.tsx | 2 +- src/components/RepoContext.tsx | 98 ++++++++++++++ src/components/Resizable.tsx | 2 +- src/components/SideBar.tsx | 94 +++++++------- src/components/SideBarState.tsx | 26 ++++ src/components/settings/SettingsBar.tsx | 14 +- src/content.tsx | 2 +- src/driver/core/FileExplorer.ts | 33 +++-- src/driver/core/SideBar.ts | 113 ++-------------- src/global.d.ts | 4 +- src/platforms/GitHub/DOMHelper.ts | 18 +++ src/platforms/GitHub/index.ts | 12 +- src/platforms/Gitea/index.ts | 2 +- src/platforms/Gitee/index.ts | 2 +- src/platforms/dummyPlatformForTypeSafety.ts | 2 +- src/platforms/index.ts | 2 +- src/platforms/platform.d.ts | 8 +- .../hooks}/useCSSVariable.tsx | 0 .../hooks/useEffectOnSerializableUpdates.tsx | 9 ++ src/utils/hooks/useLoadedContext.tsx | 7 + 23 files changed, 351 insertions(+), 251 deletions(-) create mode 100644 src/components/IIFC.tsx create mode 100644 src/components/RepoContext.tsx create mode 100644 src/components/SideBarState.tsx rename src/{components => utils/hooks}/useCSSVariable.tsx (100%) create mode 100644 src/utils/hooks/useEffectOnSerializableUpdates.tsx create mode 100644 src/utils/hooks/useLoadedContext.tsx diff --git a/src/components/FileExplorer.tsx b/src/components/FileExplorer.tsx index 81002d4..ef43db4 100644 --- a/src/components/FileExplorer.tsx +++ b/src/components/FileExplorer.tsx @@ -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 = function RawFileExplorer(props) { const { - state, visibleNodes, visibleNodesGenerator, freeze, @@ -37,11 +39,9 @@ const RawFileExplorer: React.FC = function RawFileExplor onFocusSearchBar, goTo, handleKeyDown, - toggleShowSettings, metaData, expandTo, setUpTree, - treeRoot, defer, searched, } = props @@ -59,18 +59,19 @@ const RawFileExplorer: React.FC = 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 = function RawFileExplor ) return ( -
- {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..9b386fa 100644 --- a/src/components/Gitako.tsx +++ b/src/components/Gitako.tsx @@ -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 ( - - {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/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/RepoContext.tsx b/src/components/RepoContext.tsx new file mode 100644 index 0000000..3b88e4b --- /dev/null +++ b/src/components/RepoContext.tsx @@ -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(null) + +export function RepoContextWrapper({ children }: React.PropsWithChildren<{}>) { + const partialMetaData = usePartialMetaData() + const defaultBranch = useDefaultBranch(partialMetaData) + const metaData = useMetaData(partialMetaData, defaultBranch) + + return {metaData && 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 { + // 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(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(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 +} diff --git a/src/components/Resizable.tsx b/src/components/Resizable.tsx index bc1a786..c7758cd 100644 --- a/src/components/Resizable.tsx +++ b/src/components/Resizable.tsx @@ -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 = { diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index 414063f..2876dec 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -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 = function RawGitako(props) { - const { - metaData, - treeData, - state, - defer, - error, - shouldShow, - showSettings, - logoContainerElement, - toggleShowSideBar, - toggleShowSettings, - configContext, - } = props +const RawSideBar: React.FC = 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(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 = 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 = 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 = 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 = function RawGitako(props) { return (
- + {!shouldShow && ( )} @@ -124,41 +124,37 @@ const RawGitako: React.FC = function RawGitako(props) {
-
+
{run(() => { switch (state) { - case 'loading-meta': + case 'disabled': + return null + 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 } })}
- +
@@ -166,13 +162,11 @@ const RawGitako: React.FC = 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() diff --git a/src/components/SideBarState.tsx b/src/components/SideBarState.tsx new file mode 100644 index 0000000..f41e30f --- /dev/null +++ b/src/components/SideBarState.tsx @@ -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 + +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/settings/SettingsBar.tsx b/src/components/settings/SettingsBar.tsx index 70b579e..d7f146d 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 } @@ -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 && ( - - )}