diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..0bd8935 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: enixcoda +patreon: enixcoda +open_collective: enixcoda +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/__tests__/cases/homepage.not-render.ts b/__tests__/cases/homepage.not-render.ts index 43cbac6..1a326a7 100644 --- a/__tests__/cases/homepage.not-render.ts +++ b/__tests__/cases/homepage.not-render.ts @@ -4,6 +4,6 @@ describe(`in GitHub homepage`, () => { beforeAll(() => page.goto('https://github.com')) it('should not render Gitako', async () => { - await expectToNotFind('.gitako-side-bar .gitako-position-wrapper') + await expectToNotFind('.gitako-side-bar .gitako-side-bar-body-wrapper') }) }) diff --git a/__tests__/cases/pull-request-page.gitako.ts b/__tests__/cases/pull-request-page.gitako.ts index 4658ea1..00c7c18 100644 --- a/__tests__/cases/pull-request-page.gitako.ts +++ b/__tests__/cases/pull-request-page.gitako.ts @@ -4,7 +4,7 @@ describe(`in Gitako project page`, () => { beforeAll(() => page.goto('https://github.com/EnixCoda/Gitako/pull/71')) it('should render Gitako', async () => { - await expectToFind('.gitako-side-bar .gitako-position-wrapper') + await expectToFind('.gitako-side-bar .gitako-side-bar-body-wrapper') }) it('should render file list', async () => { diff --git a/__tests__/cases/pjax.commits-page.ts b/__tests__/non-parallel-cases/pjax.commits-page.ts similarity index 90% rename from __tests__/cases/pjax.commits-page.ts rename to __tests__/non-parallel-cases/pjax.commits-page.ts index e1d219d..dde9484 100644 --- a/__tests__/cases/pjax.commits-page.ts +++ b/__tests__/non-parallel-cases/pjax.commits-page.ts @@ -4,8 +4,6 @@ describe(`in Gitako project page`, () => { beforeAll(() => page.goto('https://github.com/EnixCoda/Gitako/commits/develop')) it('should not break go back in history', async () => { - // temporarily skip this test as GitHub has unexpected PJAX behaviors - return for (let i = 0; i < 3; i++) { const commitLinks = await page.$$( `#js-repo-pjax-container .TimelineItem-body ol li > div:nth-child(1) a[href*="/commit/"]`, diff --git a/__tests__/cases/pjax.files-page.ts b/__tests__/non-parallel-cases/pjax.files-page.ts similarity index 100% rename from __tests__/cases/pjax.files-page.ts rename to __tests__/non-parallel-cases/pjax.files-page.ts diff --git a/__tests__/cases/pjax.mixed.ts b/__tests__/non-parallel-cases/pjax.general.ts similarity index 54% rename from __tests__/cases/pjax.mixed.ts rename to __tests__/non-parallel-cases/pjax.general.ts index 73912b3..d915bc1 100644 --- a/__tests__/cases/pjax.mixed.ts +++ b/__tests__/non-parallel-cases/pjax.general.ts @@ -1,18 +1,27 @@ -import { patientClick, selectFileTreeItem, sleep, waitForLegacyPJAXRedirect } from '../utils' +import { + collapseFloatModeSidebar, + expandFloatModeSidebar, + patientClick, + selectFileTreeItem, + sleep, + waitForLegacyPJAXRedirect, +} from '../utils' describe(`in Gitako project page`, () => { - beforeAll(() => page.goto('https://github.com/EnixCoda/Gitako/src')) + beforeAll(() => page.goto('https://github.com/EnixCoda/Gitako/tree/develop/src')) it('should work with PJAX', async () => { await sleep(3000) - await patientClick(page, selectFileTreeItem('src/analytics.ts')) + await expandFloatModeSidebar() + await patientClick(selectFileTreeItem('src/analytics.ts')) + await waitForLegacyPJAXRedirect() + await collapseFloatModeSidebar() + + await page.click('a[data-selected-links^="repo_issues "]') await waitForLegacyPJAXRedirect() - await page.click('a[data-tab-item="issues-tab"]') - await waitForLegacyPJAXRedirect() - - await page.click('a[data-tab-item="pull-requests-tab"]') + await page.click('a[data-selected-links^="repo_pulls "]') await waitForLegacyPJAXRedirect() page.goBack() diff --git a/__tests__/cases/pjax.internal.ts b/__tests__/non-parallel-cases/pjax.internal.ts similarity index 84% rename from __tests__/cases/pjax.internal.ts rename to __tests__/non-parallel-cases/pjax.internal.ts index c16412f..b70f5fd 100644 --- a/__tests__/cases/pjax.internal.ts +++ b/__tests__/non-parallel-cases/pjax.internal.ts @@ -1,4 +1,5 @@ import { + expandFloatModeSidebar, expectToFind, expectToNotFind, patientClick, @@ -12,7 +13,9 @@ describe(`in Gitako project page`, () => { it('should work with PJAX', async () => { await sleep(3000) - await patientClick(page, selectFileTreeItem('.babelrc')) + + await expandFloatModeSidebar() + await patientClick(selectFileTreeItem('.babelrc')) await waitForPJAXAPIRedirect() // The selector for file content diff --git a/__tests__/cases/project-page.gitako.ts b/__tests__/non-parallel-cases/project-page.gitako.ts similarity index 78% rename from __tests__/cases/project-page.gitako.ts rename to __tests__/non-parallel-cases/project-page.gitako.ts index 6df7c19..fcd79fe 100644 --- a/__tests__/cases/project-page.gitako.ts +++ b/__tests__/non-parallel-cases/project-page.gitako.ts @@ -1,10 +1,16 @@ -import { expectToFind, expectToNotFind, scroll, selectFileTreeItem } from '../utils' +import { + expandFloatModeSidebar, + expectToFind, + expectToNotFind, + scroll, + selectFileTreeItem, +} from '../utils' describe(`in Gitako project page`, () => { beforeAll(() => page.goto('https://github.com/EnixCoda/Gitako')) it('should render Gitako', async () => { - await expectToFind('.gitako-side-bar .gitako-position-wrapper') + await expectToFind('.gitako-side-bar .gitako-side-bar-body-wrapper') }) it('should render file list', async () => { @@ -12,6 +18,8 @@ describe(`in Gitako project page`, () => { }) it('should render while scroll', async () => { + await expandFloatModeSidebar() + const filesEle = await page.waitForSelector('.gitako-side-bar .files') // node of tsconfig.json should NOT be rendered before scroll down await expectToNotFind(selectFileTreeItem('tsconfig.json')) diff --git a/__tests__/utils.ts b/__tests__/utils.ts index 65b41c7..56e422b 100644 --- a/__tests__/utils.ts +++ b/__tests__/utils.ts @@ -1,5 +1,3 @@ -import { Page } from 'puppeteer' - export async function expectToFind(selector: string) { await expect(page.waitForSelector(selector)).resolves.not.toBeNull() } @@ -82,7 +80,26 @@ export function selectFileTreeItem(path: string): string { return `.gitako-side-bar .files a[title="${path}"]` } -export async function patientClick(page: Page, selector: string) { +export async function patientClick(selector: string) { await page.waitForSelector(selector) await page.click(selector) } + +export async function expandFloatModeSidebar() { + const rect = await (await page.$('.gitako-toggle-show-button'))?.evaluate(button => { + const { x, y, width, height } = button.getBoundingClientRect() + // pass required properties to avoid serialization issues + return { x, y, width, height } + }) + if (rect) { + await page.mouse.move(rect.x + rect.width / 2, rect.y + rect.height / 2) + await sleep(500) + } +} + +export async function collapseFloatModeSidebar() { + await page.mouse.move(600, 600, { + steps: 100, + }) + await sleep(500) +} diff --git a/jest.config.js b/jest.config.js index eafe22d..f13eb4d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -61,7 +61,7 @@ module.exports = { // globals: {}, // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. - maxWorkers: 1, // more than 1 workers may make tabs in background and never renders Gitako's file items. + maxWorkers: 8, // An array of directory names to be searched recursively up from the requiring module's location // moduleDirectories: [ diff --git a/jest.non.parallel.config.js b/jest.non.parallel.config.js new file mode 100644 index 0000000..74dc653 --- /dev/null +++ b/jest.non.parallel.config.js @@ -0,0 +1,7 @@ +const baseConfig = require('./jest.config') + +module.exports = { + ...baseConfig, + maxWorkers: 1, + testMatch: ['**/__tests__/non-parallel-cases/*.ts?(x)'], +} diff --git a/package.json b/package.json index e3fecc6..df90b7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitako", - "version": "2.7.1", + "version": "3.1.0", "description": "File tree for GitHub, and more than that.", "repository": "https://github.com/EnixCoda/Gitako", "author": "EnixCoda", @@ -14,13 +14,15 @@ "postinstall": "rm -rf node_modules/@types/react-native && node scripts/fix-pjax-api", "build": "VERSION=v$(node scripts/get-version.js) NODE_ENV=production webpack", "roll": "make release", - "test": "NODE_ENV=test jest" + "test": "yarn run test:parallel && yarn run test:non-parallel", + "test:parallel": "NODE_ENV=test jest", + "test:non-parallel": "NODE_ENV=test jest --config jest.non.parallel.config.js" }, "dependencies": { "@primer/components": "^22.0.2", "@primer/css": "^15.2.0", "@primer/octicons-react": "^10.0.0", - "@sentry/browser": "^5.12.1", + "@sentry/browser": "^6.3.6", "@types/history": "^4.7.5", "@types/ini": "^1.3.30", "@types/js-base64": "^2.3.1", @@ -35,8 +37,8 @@ "nprogress": "^0.2.0", "pjax-api": "^3.33.0", "react": "^17.0.1", - "react-dom": "^17.0.1", - "react-use": "^13.8.0", + "react-dom": "^17.0.2", + "react-use": "^15.3.0", "react-window": "^1.8.5", "styled-components": "^5.2.0", "webext-domain-permission-toggle": "^1.0.0", @@ -51,7 +53,7 @@ "@babel/preset-env": "^7.3.4", "@babel/preset-react": "^7.0.0", "@babel/preset-typescript": "^7.3.3", - "@sentry/cli": "^1.51.0", + "@sentry/cli": "^1.64.2", "@types/firefox-webext-browser": "^70.0.1", "@types/jest": "^24.0.25", "@types/node": "^11.10.4", @@ -71,7 +73,7 @@ "raw-loader": "^4.0.0", "sass": "^1.26.2", "sass-loader": "^8.0.2", - "typescript": "^4.0.3", + "typescript": "^4.2.4", "uglifyjs-webpack-plugin": "^2.1.2", "url-loader": "^1.1.2", "web-ext": "^4.2.0", diff --git a/src/analytics.ts b/src/analytics.ts index 9309680..1af5065 100644 --- a/src/analytics.ts +++ b/src/analytics.ts @@ -52,6 +52,7 @@ const sentryOptions: Sentry.BrowserOptions = { } return breadcrumb }, + autoSessionTracking: false, // this avoids the request when calling `init` } Sentry.init(sentryOptions) diff --git a/src/components/AccessDeniedDescription.tsx b/src/components/AccessDeniedDescription.tsx index 7071ce9..eb07778 100644 --- a/src/components/AccessDeniedDescription.tsx +++ b/src/components/AccessDeniedDescription.tsx @@ -1,12 +1,16 @@ +import { useConfigs } from 'containers/ConfigsContext' import { GITHUB_OAUTH } from 'env' import { platform } from 'platforms' import { GitHub } from 'platforms/GitHub' import * as React from 'react' -export function AccessDeniedDescription({ hasToken }: { hasToken: boolean }) { +export function AccessDeniedDescription() { + const configContext = useConfigs() + const hasToken = Boolean(configContext.value.accessToken) + return ( -
-
Access Denied
+
+

Access Denied

{hasToken ? ( <>

diff --git a/src/components/Clippy.tsx b/src/components/Clippy.tsx index eacc560..2508ade 100644 --- a/src/components/Clippy.tsx +++ b/src/components/Clippy.tsx @@ -10,13 +10,13 @@ const className = 'clippy-wrapper' export const ClippyClassName = className export function Clippy({ codeSnippetElement }: Props) { - const [status, setStatus] = React.useState<'normal' | 'success' | 'fail'>('normal') + const [state, setState] = React.useState<'normal' | 'success' | 'fail'>('normal') React.useEffect(() => { const timer = window.setTimeout(() => { - setStatus('normal') + setState('normal') }, 1000) return () => window.clearTimeout(timer) - }, [status]) + }, [state]) // Temporary fix: // React moved root node of event delegation since v17 @@ -26,7 +26,7 @@ export function Clippy({ codeSnippetElement }: Props) { const element = elementRef.current if (element) { function onClippyClick() { - setStatus(copyElementContent(codeSnippetElement) ? 'success' : 'fail') + setState(copyElementContent(codeSnippetElement) ? 'success' : 'fail') } element.addEventListener('click', onClippyClick) return () => element.removeEventListener('click', onClippyClick) @@ -36,7 +36,7 @@ export function Clippy({ codeSnippetElement }: Props) { return (

) diff --git a/src/components/FileExplorer.tsx b/src/components/FileExplorer.tsx index fb66e48..8abcc59 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,9 +11,12 @@ 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 { SideBarStateContext } from '../containers/SideBarState' import { Icon } from './Icon' import { SearchMode, searchModes } from './searchModes' import { SizeObserver } from './SizeObserver' @@ -27,7 +30,6 @@ type renderNodeContext = { const RawFileExplorer: React.FC = function RawFileExplorer(props) { const { - state, visibleNodes, visibleNodesGenerator, freeze, @@ -37,16 +39,14 @@ const RawFileExplorer: React.FC = function RawFileExplor onFocusSearchBar, goTo, handleKeyDown, - toggleShowSettings, metaData, expandTo, setUpTree, - treeRoot, defer, searched, } = props const { - value: { accessToken, compressSingletonFolder, searchMode }, + value: { accessToken, compressSingletonFolder, searchMode, commentToggle }, } = useConfigs() const onSearch = React.useCallback( @@ -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() @@ -104,18 +105,26 @@ const RawFileExplorer: React.FC = function RawFileExplor ) : undefined + const renderFileCommentAmounts = (node: TreeNode): React.ReactNode => + node.comments !== undefined && + node.comments > 0 && ( + + {node.comments > 9 ? '9+' : node.comments} + + ) const renders: ((node: TreeNode) => React.ReactNode)[] = [] + if (commentToggle) renders.push(renderFileCommentAmounts) if (searchMode === 'fuzzy') renders.push(renderFindInFolderButton) if (searched) renders.push(renderGoToButton) return renders.length ? node => renders.map((render, i) => {render(node)}) : undefined - }, [goTo, onSearch, searched, searchMode]) + }, [goTo, onSearch, searched, searchMode, commentToggle]) const renderLabelText = React.useCallback( - node => searchModes[searchMode].renderNodeLabelText(node, searchKey), + (node: TreeNode) => searchModes[searchMode].renderNodeLabelText(node, searchKey), [searchKey, searchMode], ) @@ -131,59 +140,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: 'pulling', searchKey: '', visibleNodes: null, } @@ -248,13 +261,15 @@ function ListView({ width, height, metaData, expandTo, renderNodeContext }: List useOnLocationChange(goToCurrentItem) useOnPJAXDone(goToCurrentItem) + const { compactFileTree } = useConfigs().value + return ( visibleNodes?.nodes[index]?.path} itemData={renderNodeContext} itemCount={visibleNodes.nodes.length} - itemSize={37} + itemSize={compactFileTree ? 24 : 37} height={height} width={width} > 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 c5d9f07..d3f8eb4 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -2,6 +2,7 @@ import { ChevronDownIcon as ChevronDown, ChevronRightIcon as ChevronRight, ClockIcon as Clock, + CommentIcon as Comment, FileCodeIcon as FileCode, FileIcon as File, FileMediaIcon as FileMedia, @@ -10,130 +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 '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 = { @@ -151,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/MetaBar.tsx b/src/components/MetaBar.tsx index a210444..7696872 100644 --- a/src/components/MetaBar.tsx +++ b/src/components/MetaBar.tsx @@ -11,14 +11,14 @@ export function MetaBar({ metaData }: Props) { const { userName, repoName, branchName } = metaData const { repoUrl, userUrl } = platform.resolveUrlFromMetaData(metaData) return ( - + <> {userName} {repoName} - +
@@ -26,6 +26,6 @@ export function MetaBar({ metaData }: Props) { {branchName || '...'}
-
+ ) } diff --git a/src/components/Node.tsx b/src/components/Node.tsx index 5fa7942..8fa2ff0 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) { @@ -38,6 +38,7 @@ export function Node({ style, onClick, }: Props) { + const { compactFileTree: compact } = useConfigs().value return (
{renderLabelText(node)}
- {renderActions &&
{renderActions(node)}
} + {renderActions &&
{renderActions(node)}
}
) } 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/SearchBar.tsx b/src/components/SearchBar.tsx index d266f7d..9f21e50 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -35,7 +35,7 @@ export function SearchBar({ onSearch, onFocus, value }: Props) { />
-
-
- {metaData ? ( -
- +
+
+
+ {sidebarToggleMode === 'persistent' && ( + + )} +
- ) : ( - - )} - {errorDueToAuth ? ( - - ) : ( - metaData && ( - - ) - )} + {metaData && } +
+ {run(() => { + switch (state) { + case 'disabled': + return null + case 'getting-access-token': + return + case 'after-getting-access-token': + case 'meta-loading': + return + case 'error-due-to-auth': + return + default: + return ( + metaData && ( + + {() => ( + + )} + + ) + ) + } + })}
- +
- +
) } -RawGitako.defaultProps = { - shouldShow: false, - showSettings: false, - errorDueToAuth: false, - disabled: false, -} - -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 ( -
+
)} - {accessTokenHint && !focusInput.value && {accessTokenHint}} + {accessTokenHint && {accessTokenHint}} ) } diff --git a/src/components/settings/FileTreeSettings.tsx b/src/components/settings/FileTreeSettings.tsx index 72c3ca7..337ab54 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} - > + /> @@ -76,6 +76,20 @@ export function FileTreeSettings(props: React.PropsWithChildren) { tooltip: 'Merge folders and their only child folder to make UI more compact.', }} /> + + ) } diff --git a/src/components/settings/SettingsBar.tsx b/src/components/settings/SettingsBar.tsx index 70b579e..3a29e78 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' @@ -15,13 +15,13 @@ const WIKI_HOME_LINK = 'https://github.com/EnixCoda/Gitako/wiki' export const wikiLinks = { compressSingletonFolder: `${WIKI_HOME_LINK}/Compress-Singleton-Folder`, changeLog: `${WIKI_HOME_LINK}/Change-Log`, + codeFolding: `${WIKI_HOME_LINK}/Code-folding`, copyFileButton: `${WIKI_HOME_LINK}/Copy-file-and-snippet`, copySnippet: `${WIKI_HOME_LINK}/Copy-file-and-snippet`, createAccessToken: `${WIKI_HOME_LINK}/Access-token-for-Gitako`, } type Props = { - defer?: boolean activated: boolean toggleShowSettings: () => void } @@ -30,18 +30,24 @@ function SettingsBarContent() { const useReloadHint = useStateIO('') const { value: reloadHint } = useReloadHint - const moreFields: SimpleField[] = + const moreFields: SimpleField<'copyFileButton' | 'copySnippetButton'|'codeFolding'>[] = platform === GitHub ? [ + { + key: 'codeFolding', + label: 'Fold source code button', + wikiLink: wikiLinks.codeFolding, + tooltip: `Read more in Gitako's Wiki`, + }, { key: 'copyFileButton', - label: 'Copy file shortcut', + label: 'Copy file button', wikiLink: wikiLinks.copyFileButton, tooltip: `Read more in Gitako's Wiki`, }, { key: 'copySnippetButton', - label: 'Copy snippet shortcut', + label: 'Copy snippet button', wikiLink: wikiLinks.copySnippet, tooltip: `Read more in Gitako's Wiki`, }, @@ -78,7 +84,7 @@ function SettingsBarContent() { } export function SettingsBar(props: Props) { - const { defer, toggleShowSettings, activated } = props + const { toggleShowSettings, activated } = props return (
{activated && } @@ -93,15 +99,6 @@ export function SettingsBar(props: Props) { {VERSION}
- {defer && ( - - )}