mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
chore: resolve eslint issues
This commit is contained in:
parent
77d9db6043
commit
2c93bc64eb
45 changed files with 150 additions and 215 deletions
|
|
@ -2,6 +2,7 @@ import * as Sentry from '@sentry/browser'
|
|||
import { Middleware } from 'driver/connect.js'
|
||||
import { IN_PRODUCTION_MODE, VERSION } from 'env'
|
||||
import { platform } from 'platforms'
|
||||
import { forOf } from 'utils/general'
|
||||
|
||||
const PUBLIC_KEY = 'd22ec5c9cc874539a51c78388c12e3b0'
|
||||
const PROJECT_ID = '1406497'
|
||||
|
|
@ -69,12 +70,7 @@ export const withErrorLog: Middleware = function withErrorLog(method, args) {
|
|||
]
|
||||
}
|
||||
|
||||
export function raiseError(
|
||||
error: Error,
|
||||
extra?: {
|
||||
[key: string]: any
|
||||
},
|
||||
) {
|
||||
export function raiseError(error: Error, extra?: unknown) {
|
||||
if (!IN_PRODUCTION_MODE || platform.isEnterprise()) {
|
||||
// ignore errors from enterprise to get less noise on Sentry
|
||||
console.error(error)
|
||||
|
|
@ -83,10 +79,8 @@ export function raiseError(
|
|||
}
|
||||
|
||||
Sentry.withScope(scope => {
|
||||
if (extra) {
|
||||
Object.keys(extra).forEach(key => {
|
||||
scope.setExtra(key, extra[key])
|
||||
})
|
||||
if (typeof extra === 'object' && extra) {
|
||||
forOf(extra, (key, value) => scope.setExtra(key, value))
|
||||
}
|
||||
Sentry.captureException(error)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { cx } from 'utils/cx'
|
|||
import { setCSSVariable } from 'utils/DOMHelper'
|
||||
import * as features from 'utils/features'
|
||||
import { detectBrowser } from 'utils/general'
|
||||
import { ResizeState } from 'utils/hooks/useResizeHandler'
|
||||
import { useConditionalHook } from '../utils/hooks/useConditionalHook'
|
||||
|
||||
type Size = number
|
||||
|
|
@ -107,6 +108,10 @@ export function SideBarBodyWrapper({
|
|||
)
|
||||
|
||||
const dummySize: [number, number] = React.useMemo(() => [size, size], [size])
|
||||
const onResizeStateChange = React.useCallback((state: ResizeState) => {
|
||||
blockLeaveRef.current = state === 'resizing'
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={bodyWrapperRef}
|
||||
|
|
@ -122,9 +127,7 @@ export function SideBarBodyWrapper({
|
|||
setSize(defaultConfigs.sideBarWidth)
|
||||
applySizeToCSSVariables(sizeVariableMountPoint, defaultConfigs.sideBarWidth)
|
||||
}}
|
||||
onResizeStateChange={state => {
|
||||
blockLeaveRef.current = state === 'resizing'
|
||||
}}
|
||||
onResizeStateChange={onResizeStateChange}
|
||||
size={dummySize}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -29,9 +29,9 @@ export function ConfigsContextWrapper(props: React.PropsWithChildren<Props>) {
|
|||
)
|
||||
}
|
||||
|
||||
export const useConfigs = useNonNullContext(ConfigsContext)
|
||||
export const useConfigs = createUseNonNullContext(ConfigsContext)
|
||||
|
||||
function useNonNullContext<T>(theContext: React.Context<T | null>): () => T {
|
||||
function createUseNonNullContext<T>(theContext: React.Context<T | null>): () => T {
|
||||
return () => {
|
||||
const context = React.useContext(theContext)
|
||||
if (context === null) throw new Error(`Empty context`)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export function OAuthWrapper({ children }: React.PropsWithChildren<{}>) {
|
|||
if (needGetAccessTokenRef.current) {
|
||||
$state.onChange(running ? 'getting-access-token' : 'after-getting-access-token')
|
||||
}
|
||||
}, [running])
|
||||
}, [running]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// block children rendering on the first render if setting token
|
||||
if (running && $state.value !== 'getting-access-token') return null
|
||||
|
|
@ -38,7 +38,7 @@ function useGetAccessToken() {
|
|||
}
|
||||
$block.onChange(false)
|
||||
})
|
||||
}, [])
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return $block.value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,10 +38,13 @@ function usePartialMetaData(): PartialMetaData | null {
|
|||
// sync along URL and DOM
|
||||
const $partialMetaData = useStateIO(isGettingAccessToken ? null : resolvePartialMetaData)
|
||||
const $committedPartialMetaData = useStateIO($partialMetaData.value)
|
||||
const setPartialMetaData = () => $partialMetaData.onChange(resolvePartialMetaData())
|
||||
const setPartialMetaData = React.useCallback(
|
||||
() => $partialMetaData.onChange(resolvePartialMetaData()),
|
||||
[], // eslint-disable-line react-hooks/exhaustive-deps
|
||||
)
|
||||
React.useEffect(() => {
|
||||
if (!isGettingAccessToken) setPartialMetaData()
|
||||
}, [isGettingAccessToken])
|
||||
}, [isGettingAccessToken, setPartialMetaData])
|
||||
useOnPJAXDone(setPartialMetaData)
|
||||
useEffectOnSerializableUpdates(
|
||||
$partialMetaData.value,
|
||||
|
|
@ -52,7 +55,7 @@ function usePartialMetaData(): PartialMetaData | null {
|
|||
if (!$partialMetaData.value && !isGettingAccessToken) {
|
||||
$state.onChange('disabled')
|
||||
}
|
||||
}, [$partialMetaData.value])
|
||||
}, [$partialMetaData.value]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
return $committedPartialMetaData.value
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +79,7 @@ function useDefaultBranch(partialMetaData: PartialMetaData | null) {
|
|||
const defaultBranch = await platform.getDefaultBranchName(partialMetaData, accessToken)
|
||||
$defaultBranch.onChange(defaultBranch)
|
||||
})
|
||||
}, [partialMetaData, accessToken])
|
||||
}, [partialMetaData, accessToken]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
return $defaultBranch.value
|
||||
}
|
||||
|
||||
|
|
@ -102,6 +105,6 @@ function useMetaData(
|
|||
} else {
|
||||
$metaData.onChange(null)
|
||||
}
|
||||
}, [partialMetaData, defaultBranchName, theBranch])
|
||||
}, [partialMetaData, defaultBranchName, theBranch]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
return $metaData.value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
import { errors } from 'platforms'
|
||||
import { isEnterprise } from '.'
|
||||
import { is } from '../../utils/is'
|
||||
import { continuousLoadPages, getDOM, resolveHeaderLink } from './utils'
|
||||
|
||||
function isAPIRateLimitExceeded(content: any /* safe any */) {
|
||||
return content?.['documentation_url'] === 'https://developer.github.com/v3/#rate-limiting'
|
||||
function isAPIRateLimitExceeded(content: JSONValue) {
|
||||
return (
|
||||
is.JSON.object(content) &&
|
||||
content?.['documentation_url'] === 'https://developer.github.com/v3/#rate-limiting'
|
||||
)
|
||||
}
|
||||
|
||||
function isEmptyProject(content: any /* safe any */) {
|
||||
return content?.['message'] === 'Git Repository is empty.'
|
||||
function isEmptyProject(content: JSONValue) {
|
||||
return is.JSON.object(content) && content?.['message'] === 'Git Repository is empty.'
|
||||
}
|
||||
|
||||
function isBlockedProject(content: any /* safe any */) {
|
||||
return content?.['message'] === 'Repository access blocked'
|
||||
function isBlockedProject(content: JSONValue) {
|
||||
return is.JSON.object(content) && content?.['message'] === 'Repository access blocked'
|
||||
}
|
||||
|
||||
export const responseBodyResolvers = {
|
||||
|
|
@ -151,11 +155,10 @@ export async function getPullPageDocuments(
|
|||
)
|
||||
}
|
||||
|
||||
export async function getCommitPageDocuments(
|
||||
userName: string,
|
||||
export async function getCommitPageDocuments(): Promise<Document[]> {
|
||||
/* userName: string,
|
||||
repoName: string,
|
||||
commitId: string,
|
||||
): Promise<Document[]> {
|
||||
commitId: string, */
|
||||
// arguments are not used because info are collected from DOM directly
|
||||
return continuousLoadPages(document)
|
||||
}
|
||||
|
|
@ -191,7 +194,7 @@ export async function requestCommitTreeData(
|
|||
userName: string,
|
||||
repoName: string,
|
||||
sha: string,
|
||||
page: number = 1,
|
||||
page = 1,
|
||||
accessToken?: string,
|
||||
): Promise<Response> {
|
||||
const search = new URLSearchParams({
|
||||
|
|
@ -205,6 +208,7 @@ export async function requestCommitTreeData(
|
|||
export async function getPaginatedData<T>(sendRequest: (page: number) => Promise<Response>) {
|
||||
const responses: Response[] = []
|
||||
let page = 1
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const response = await sendRequest(page)
|
||||
responses.push(response)
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import { cx } from 'utils/cx'
|
|||
import { copyElementContent } from 'utils/DOMHelper'
|
||||
import { getCodeElement } from './DOMHelper'
|
||||
|
||||
type Props = {}
|
||||
|
||||
const className = 'gitako-copy-file-button'
|
||||
export const copyFileButtonClassName = className
|
||||
|
||||
|
|
@ -13,7 +11,8 @@ const contents = {
|
|||
error: 'Copy failed!',
|
||||
normal: 'Copy file',
|
||||
}
|
||||
export function CopyFileButton(props: React.PropsWithChildren<Props>) {
|
||||
|
||||
export function CopyFileButton() {
|
||||
const [content, setContent] = React.useState(contents.normal)
|
||||
React.useEffect(() => {
|
||||
if (content !== contents.normal) {
|
||||
|
|
@ -31,7 +30,7 @@ export function CopyFileButton(props: React.PropsWithChildren<Props>) {
|
|||
// onClick on <a /> won't work when rendered with `renderReact`
|
||||
const element = elementRef.current
|
||||
if (element) {
|
||||
function copyCode() {
|
||||
const copyCode = () => {
|
||||
const codeElement = getCodeElement()
|
||||
if (codeElement) {
|
||||
setContent(copyElementContent(codeElement, true) ? contents.success : contents.error)
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export function getCurrentBranch(passive = false) {
|
|||
const commitPathRegex = /^(.*?)\/(.*?)\/find\/(.*?)$/
|
||||
const result = urlFromFindFileButton.match(commitPathRegex)
|
||||
if (result) {
|
||||
const [_, userName, repoName, branchName] = result
|
||||
const [_, userName, repoName, branchName] = result // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
if (!branchName.includes(' ')) return branchName
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export function parse(): Partial<Pick<MetaData, 'userName' | 'repoName' | 'type'
|
|||
path: string[]
|
||||
} {
|
||||
const { pathname } = window.location
|
||||
let [
|
||||
const [
|
||||
,
|
||||
// ignore content before the first '/'
|
||||
userName,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export async function getCommitTreeData(
|
|||
.map(({ files }) => files)
|
||||
.flat()
|
||||
|
||||
const documents = await API.getCommitPageDocuments(userName, repoName, commitSHA)
|
||||
const documents = await API.getCommitPageDocuments(/* userName, repoName, commitSHA */)
|
||||
|
||||
const getItemURL = (path: string) => {
|
||||
for (const doc of documents) {
|
||||
|
|
|
|||
|
|
@ -7,11 +7,10 @@ import { GitHub } from '../index'
|
|||
export function useGitHubAttachCopyFileButton(copyFileButton: boolean) {
|
||||
const attachCopyFileButton = React.useCallback(
|
||||
function attachCopyFileButton() {
|
||||
if (platform !== GitHub) return
|
||||
if (copyFileButton) return DOMHelper.attachCopyFileBtn() || undefined // for the sake of react effect
|
||||
if (platform === GitHub && copyFileButton) DOMHelper.attachCopyFileBtn()
|
||||
},
|
||||
[copyFileButton],
|
||||
)
|
||||
React.useEffect(attachCopyFileButton, [copyFileButton])
|
||||
React.useEffect(attachCopyFileButton, [attachCopyFileButton])
|
||||
useOnPJAXDone(attachCopyFileButton)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,10 @@ import { GitHub } from '../index'
|
|||
export function useGitHubAttachCopySnippetButton(copySnippetButton: boolean) {
|
||||
const attachCopySnippetButton = React.useCallback(
|
||||
function attachCopySnippetButton() {
|
||||
if (platform !== GitHub) return
|
||||
if (copySnippetButton) return DOMHelper.attachCopySnippet() || undefined // for the sake of react effect
|
||||
if (platform === GitHub && copySnippetButton) DOMHelper.attachCopySnippet()
|
||||
},
|
||||
[copySnippetButton],
|
||||
)
|
||||
React.useEffect(attachCopySnippetButton, [copySnippetButton])
|
||||
React.useEffect(attachCopySnippetButton, [attachCopySnippetButton])
|
||||
useOnPJAXDone(attachCopySnippetButton)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,10 +51,11 @@ function init() {
|
|||
type Level = number // measured by leading whitespace amount
|
||||
const stack: [Level, LineNumber][] = []
|
||||
|
||||
function trySeal(lineNumber: number, level: number) {
|
||||
const trySeal = (lineNumber: number, level: number) => {
|
||||
let ignoredTheHighestLevelItem = false
|
||||
while (stack.length) {
|
||||
const top = stack.pop()! // safe
|
||||
const top = stack.pop()
|
||||
if (top === undefined) throw new Error()
|
||||
const [$LineNumber, $level] = top
|
||||
|
||||
if ($level < level) {
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export async function continuousLoadPages(doc: Document, onReceivePage?: (doc: D
|
|||
*/
|
||||
const fragmentSelector = 'include-fragment[data-targets="diff-file-filter.progressiveLoaders"]'
|
||||
const documents: Document[] = [doc]
|
||||
while (true) {
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const fragment = doc.querySelector(fragmentSelector) as HTMLElement
|
||||
if (!fragment) break
|
||||
const src = fragment.getAttribute('src')
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { raiseError } from 'analytics'
|
||||
import { errors } from 'platforms'
|
||||
import { is } from 'utils/is'
|
||||
|
||||
function isEmptyProject(content: any /* safe any */) {
|
||||
return content?.['message'] === 'Git Repository is empty.'
|
||||
function isEmptyProject(content: JSONValue) {
|
||||
return is.JSON.object(content) && content?.['message'] === 'Git Repository is empty.'
|
||||
}
|
||||
|
||||
function isBlockedProject(content: any /* safe any */) {
|
||||
return content?.['message'] === 'Repository access blocked'
|
||||
function isBlockedProject(content: JSONValue) {
|
||||
return is.JSON.object(content) && content?.['message'] === 'Repository access blocked'
|
||||
}
|
||||
|
||||
async function request(
|
||||
|
|
@ -77,8 +78,7 @@ export async function getTreeData(
|
|||
): Promise<GiteaAPI.TreeData> {
|
||||
const search = new URLSearchParams()
|
||||
if (recursive) search.set('recursive', '1')
|
||||
const url =
|
||||
`${API_ENDPOINT}/repos/${userName}/${repoName}/git/trees/${branchName}?` + search
|
||||
const url = `${API_ENDPOINT}/repos/${userName}/${repoName}/git/trees/${branchName}?` + search
|
||||
return await request(url, { accessToken })
|
||||
}
|
||||
|
||||
|
|
@ -94,14 +94,14 @@ export async function getBlobData(
|
|||
|
||||
export async function OAuth(code: string): Promise<string | null> {
|
||||
const endpoint = `https://gitako.enix.one/oauth/gitea?`
|
||||
const res = await fetch(endpoint + new URLSearchParams({ code }).toString(), {
|
||||
method: 'post',
|
||||
})
|
||||
const res = await fetch(endpoint + new URLSearchParams({ code }).toString(), {
|
||||
method: 'post',
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
const body = await res.json()
|
||||
const accessToken = body?.accessToken
|
||||
if (typeof accessToken === 'string') return accessToken
|
||||
}
|
||||
return null
|
||||
if (res.ok) {
|
||||
const body = await res.json()
|
||||
const accessToken = body?.accessToken
|
||||
if (typeof accessToken === 'string') return accessToken
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { raiseError } from 'analytics'
|
|||
|
||||
export function parse(): Partial<MetaData> & { path: string[] } {
|
||||
const { pathname } = window.location
|
||||
let [
|
||||
const [
|
||||
,
|
||||
// ignore content before the first '/'
|
||||
userName,
|
||||
|
|
@ -67,4 +67,4 @@ export function getCurrentPath(branchName = '') {
|
|||
return path.map(decodeURIComponent)
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ function getUrlForRedirect(
|
|||
userName: string,
|
||||
repoName: string,
|
||||
branchName: string,
|
||||
type = 'blob',
|
||||
type = 'blob', // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
path = '',
|
||||
) {
|
||||
// Modern browsers have great support for handling unsafe URL,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { raiseError } from 'analytics'
|
||||
import { errors } from 'platforms'
|
||||
import { is } from 'utils/is'
|
||||
|
||||
function isEmptyProject(content: any /* safe any */) {
|
||||
return content?.['message'] === 'Git Repository is empty.'
|
||||
function isEmptyProject(content: JSONValue) {
|
||||
return is.JSON.object(content) && content?.['message'] === 'Git Repository is empty.'
|
||||
}
|
||||
|
||||
function isBlockedProject(content: any /* safe any */) {
|
||||
return content?.['message'] === 'Repository access blocked'
|
||||
function isBlockedProject(content: JSONValue) {
|
||||
return is.JSON.object(content) && content?.['message'] === 'Repository access blocked'
|
||||
}
|
||||
|
||||
async function request(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { raiseError } from 'analytics'
|
|||
|
||||
export function parse(): Partial<MetaData> & { path: string[] } {
|
||||
const { pathname } = window.location
|
||||
let [
|
||||
const [
|
||||
,
|
||||
// ignore content before the first '/'
|
||||
userName,
|
||||
|
|
|
|||
|
|
@ -184,11 +184,10 @@ export const Gitee: Platform = {
|
|||
export function useGiteeAttachCopySnippetButton(copySnippetButton: boolean) {
|
||||
const attachCopySnippetButton = React.useCallback(
|
||||
function attachCopySnippetButton() {
|
||||
if (platform !== Gitee) return
|
||||
if (copySnippetButton) return DOMHelper.attachCopySnippet() || undefined // for the sake of react effect
|
||||
if (platform === Gitee && copySnippetButton) DOMHelper.attachCopySnippet()
|
||||
},
|
||||
[copySnippetButton],
|
||||
)
|
||||
React.useEffect(attachCopySnippetButton, [copySnippetButton])
|
||||
React.useEffect(attachCopySnippetButton, [attachCopySnippetButton])
|
||||
useOnPJAXDone(attachCopySnippetButton)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,6 @@ export const dummyPlatformForTypeSafety: Platform = {
|
|||
getOAuthLink: dummyPlatformMethod,
|
||||
}
|
||||
|
||||
function dummyPlatformMethod(): any {
|
||||
function dummyPlatformMethod(): any { // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
throw new Error(`Do not call dummy platform methods`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { forOf } from 'utils/general'
|
||||
import { dummyPlatformForTypeSafety } from './dummyPlatformForTypeSafety'
|
||||
import { Gitea } from './Gitea'
|
||||
import { Gitee } from './Gitee'
|
||||
|
|
@ -17,10 +18,9 @@ function resolvePlatform() {
|
|||
}
|
||||
|
||||
function getPlatformName() {
|
||||
const keys = Object.keys(platforms) as (keyof typeof platforms)[]
|
||||
for (const key of keys) {
|
||||
if (platform === platforms[key]) return key
|
||||
}
|
||||
return forOf(platforms, (name, $platform) => {
|
||||
if (platform === $platform) return name
|
||||
})
|
||||
}
|
||||
|
||||
export const platform = resolvePlatform()
|
||||
|
|
|
|||
6
src/platforms/platform.d.ts
vendored
6
src/platforms/platform.d.ts
vendored
|
|
@ -23,9 +23,9 @@ type Platform = {
|
|||
getCurrentPath(branchName: string): string[] | null
|
||||
setOAuth(code: string): Promise<string | null>
|
||||
getOAuthLink(): string
|
||||
delegatePJAXProps?(options?: {
|
||||
node?: TreeNode
|
||||
}): void | (React.DOMAttributes<HTMLElement> & Record<string, any>) // support data-* attributes
|
||||
delegatePJAXProps?(options?: { node?: TreeNode }):
|
||||
| (React.DOMAttributes<HTMLElement> & Record<string, unknown>) // support data-* attributes
|
||||
| void
|
||||
loadWithPJAX?(url: string, element: HTMLElement): void
|
||||
usePlatformHooks?(): void
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export function $<T2>(
|
|||
existCallback: undefined | null,
|
||||
otherwise: () => T2,
|
||||
): HTMLElement | null | T2
|
||||
export function $(selector: string, existCallback?: any, otherwise?: any) {
|
||||
export function $(selector: string, existCallback?: any, otherwise?: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
const element = document.querySelector(selector)
|
||||
if (element) {
|
||||
return existCallback ? existCallback(element) : element
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ export class EventSubscription<Data, Listener extends VoidFN<Data> = VoidFN<Data
|
|||
|
||||
export class EventHub<
|
||||
Shape extends {
|
||||
[event: string]: any
|
||||
}
|
||||
[event: string]: unknown
|
||||
},
|
||||
> {
|
||||
ports: {
|
||||
[key in keyof Shape]: EventSubscription<Shape[key]>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ function recursiveMarkDiff(
|
|||
const [path] = node
|
||||
markDiff(path, state)
|
||||
} else {
|
||||
const [name, children] = node
|
||||
const [/* name */, children] = node
|
||||
for (const child of children) {
|
||||
recursiveMarkDiff(child, state, markDiff)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,10 +73,7 @@ function applyDefaultConfigs(configs: Partial<Config>) {
|
|||
|
||||
export type VersionedConfig<SiteConfig> = Record<string, SiteConfig> & { configVersion: string }
|
||||
|
||||
const prepareConfig = new Promise<void>(async resolve => {
|
||||
await migrateConfig()
|
||||
resolve()
|
||||
})
|
||||
const prepareConfig = new Promise((resolve, reject) => migrateConfig().then(resolve, reject))
|
||||
|
||||
async function get(): Promise<Config> {
|
||||
await prepareConfig
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { is } from 'utils/is'
|
||||
import { storageHelper } from 'utils/storageHelper'
|
||||
import { Migration } from '.'
|
||||
import { Storage } from '../../storageHelper'
|
||||
|
|
@ -5,7 +6,7 @@ import { Storage } from '../../storageHelper'
|
|||
export const migration: Migration = {
|
||||
version: '1.0.1',
|
||||
async migrate(version) {
|
||||
const config: any | void = await storageHelper.get<Storage>([
|
||||
const config: JSONObject | void = await storageHelper.get<Storage>([
|
||||
'configVersion',
|
||||
'sideBarWidth',
|
||||
'shortcut',
|
||||
|
|
@ -20,6 +21,7 @@ export const migration: Migration = {
|
|||
config &&
|
||||
(!('configVersion' in config) ||
|
||||
config.configVersion === null ||
|
||||
!is.string(config.configVersion) ||
|
||||
config.configVersion < version)
|
||||
) {
|
||||
await storageHelper.set({ platform_GitHub: config, configVersion: version })
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { is } from 'utils/is'
|
||||
import { storageHelper } from 'utils/storageHelper'
|
||||
import { Migration } from '.'
|
||||
import { Storage } from '../../storageHelper'
|
||||
|
|
@ -6,7 +7,7 @@ import { Config, VersionedConfig } from '../helper'
|
|||
export const migration: Migration = {
|
||||
version: '1.3.4',
|
||||
async migrate(version) {
|
||||
const config: any | void = await storageHelper.get<VersionedConfig<Config> & Storage>([
|
||||
const config: JSONObject | void = await storageHelper.get<VersionedConfig<Config> & Storage>([
|
||||
'configVersion',
|
||||
'platform_undefined',
|
||||
'platform_GitHub',
|
||||
|
|
@ -15,6 +16,7 @@ export const migration: Migration = {
|
|||
if (
|
||||
config &&
|
||||
'configVersion' in config &&
|
||||
is.string(config.configVersion) &&
|
||||
config.configVersion < version &&
|
||||
(config.platform_GitHub || config.platform_undefined) &&
|
||||
!config['platform_github.com']
|
||||
|
|
|
|||
|
|
@ -13,12 +13,9 @@ export const migration: Migration = {
|
|||
|
||||
await onConfigOutdated(version, async configs => {
|
||||
for (const key of Object.keys(configs)) {
|
||||
if (
|
||||
typeof configs[key] === 'object' &&
|
||||
configs[key] !== null &&
|
||||
'access_token' in configs[key]
|
||||
) {
|
||||
const configBeforeMigrate: ConfigBeforeMigrate = configs[key]
|
||||
const target = configs[key]
|
||||
if (typeof target === 'object' && target !== null && 'access_token' in target) {
|
||||
const configBeforeMigrate: ConfigBeforeMigrate = target
|
||||
const { access_token: accessToken, ...rest } = configBeforeMigrate
|
||||
const configAfterMigrate: ConfigAfterMigrate = {
|
||||
...rest,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const migration: Migration = {
|
|||
const key = 'platform_github.com'
|
||||
const config = configs[key]
|
||||
if (typeof config === 'object' && config !== null && 'copySnippetButton' in config) {
|
||||
const configBeforeMigrate: ConfigBeforeMigrate = config
|
||||
const configBeforeMigrate = config as ConfigBeforeMigrate
|
||||
const { copySnippetButton, ...rest } = configBeforeMigrate
|
||||
if (copySnippetButton) {
|
||||
const configAfterMigrate: ConfigAfterMigrate = {
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ export const migration: Migration = {
|
|||
copyFileButton: boolean
|
||||
}
|
||||
type ConfigAfterMigrate = {
|
||||
copyFileButton: boolean
|
||||
copyFileButton: false
|
||||
}
|
||||
|
||||
await onConfigOutdated(version, async configs => {
|
||||
const key = 'platform_github.com'
|
||||
const config = configs[key]
|
||||
if (typeof config === 'object' && config !== null && 'copyFileButton' in config) {
|
||||
const configBeforeMigrate: ConfigBeforeMigrate = config
|
||||
const configBeforeMigrate = config as ConfigBeforeMigrate
|
||||
const { copyFileButton, ...rest } = configBeforeMigrate
|
||||
if (copyFileButton) {
|
||||
const configAfterMigrate: ConfigAfterMigrate = {
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@ export async function migrateConfig() {
|
|||
}
|
||||
}
|
||||
|
||||
export async function onConfigOutdated<T extends { [key: string]: any }>(
|
||||
export async function onConfigOutdated<T extends JSONObject>(
|
||||
configVersion: string,
|
||||
runIfOutdated: (config: T) => Async<void>,
|
||||
) {
|
||||
const config = await storageHelper.get<Storage>()
|
||||
|
||||
if (config && config.configVersion < configVersion) {
|
||||
const { configVersion: $configVersion, ...restConfig } = config
|
||||
const { configVersion: $configVersion, ...restConfig } = config // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
await runIfOutdated(restConfig as T)
|
||||
await storageHelper.set({ configVersion })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,21 +60,6 @@ export function friendlyFormatShortcut(shortcut?: string) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* if item's name matches path, return the depth of the item
|
||||
* else return 0
|
||||
*/
|
||||
function measureDistance(item: TreeNode, path: TreeNode['name'][]): number {
|
||||
const pathString = path.join('/')
|
||||
if (item.name.startsWith(pathString + '/')) {
|
||||
// If accessing a leading item of compressed node, path will be shorter than item.name
|
||||
return path.length
|
||||
} else if (pathString === item.name || pathString.startsWith(item.name + '/')) {
|
||||
return item.name.split('/').length
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
export async function traverse<T>(
|
||||
range: T[] = [],
|
||||
conditionAndEffect: (node: T) => Async<boolean>,
|
||||
|
|
@ -121,7 +106,11 @@ export function parseURLSearch(search = window.location.search) {
|
|||
return new URLSearchParams(search)
|
||||
}
|
||||
|
||||
export async function JSONRequest(url: string, data: any, extra: RequestInit = { method: 'post' }) {
|
||||
export async function JSONRequest<D>(
|
||||
url: string,
|
||||
data: D,
|
||||
extra: RequestInit = { method: 'post' },
|
||||
) {
|
||||
return (
|
||||
await fetch(url, {
|
||||
mode: 'cors',
|
||||
|
|
@ -171,12 +160,12 @@ export function isValidRegexpSource(source: string) {
|
|||
return Boolean(safeRegexp(source))
|
||||
}
|
||||
|
||||
export function withEffect<Method extends (...args: any[]) => any>(
|
||||
export function withEffect<Method extends (...args: any[]) => any>( // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
method: Method,
|
||||
effect: (payload: ReturnType<Method>) => void,
|
||||
): (...args: Parameters<Method>) => ReturnType<Method> {
|
||||
return (...args) => {
|
||||
const returnValue = method.apply(null, args)
|
||||
const returnValue = method(...args)
|
||||
Promise.resolve(returnValue).then(effect)
|
||||
return returnValue
|
||||
}
|
||||
|
|
@ -186,22 +175,6 @@ export function run<T>(fn: () => T) {
|
|||
return fn()
|
||||
}
|
||||
|
||||
export function createPromiseQueue() {
|
||||
let promise: Promise<void>
|
||||
return {
|
||||
async enter() {
|
||||
let leave: () => void
|
||||
const current = new Promise<void>(resolve => (leave = () => resolve()))
|
||||
|
||||
const lastPromise = promise
|
||||
promise = current!
|
||||
if (lastPromise) await lastPromise
|
||||
|
||||
return leave!
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function isOpenInNewWindowClick(event: React.MouseEvent<HTMLElement, MouseEvent>) {
|
||||
return (
|
||||
(os === OperatingSystems.macOS && (event.metaKey || event.shiftKey)) ||
|
||||
|
|
@ -228,3 +201,11 @@ export function formatHash(hash?: string) {
|
|||
export function isNotFalsy<T>(value: T | undefined | null): value is T {
|
||||
return value !== undefined && value !== null
|
||||
}
|
||||
|
||||
export function forOf<T, R>(target: T, callback: <K extends keyof T>(key: K, value: T[K]) => R) {
|
||||
for (const key of Object.keys(target)) {
|
||||
const $key = key as keyof typeof target
|
||||
const r = callback($key, target[$key])
|
||||
if (r !== undefined) return r
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const subModuleURLRegex = {
|
|||
function transformModuleGitURL(node: TreeNode, URL: string) {
|
||||
const matched = URL.match(subModuleURLRegex.git)
|
||||
if (!matched) return
|
||||
const [_, userName, repoName] = matched
|
||||
const [, userName, repoName] = matched
|
||||
return appendCommitPath(`https://${window.location.host}/${userName}/${repoName}`, node)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import { useStateIO } from './useStateIO'
|
||||
|
||||
export function useAsyncMemo<T, D extends any[] | readonly any[]>(
|
||||
factory: (dependencies: D) => T | Promise<T>,
|
||||
deps: D,
|
||||
initialValue: T,
|
||||
): T {
|
||||
const firstTime = React.useRef(true)
|
||||
const state = useStateIO<T>(() => initialValue)
|
||||
React.useEffect(() => {
|
||||
if (firstTime.current) firstTime.current = false
|
||||
Promise.resolve(factory(deps)).then(consumed => state.onChange(() => consumed))
|
||||
}, deps)
|
||||
return state.value
|
||||
}
|
||||
|
|
@ -43,6 +43,6 @@ export function useCatchNetworkError() {
|
|||
}
|
||||
}
|
||||
},
|
||||
[accessToken /* , stateContext.value, errorContext.value */],
|
||||
[accessToken /* , stateContext.value, errorContext.value */], // eslint-disable-line react-hooks/exhaustive-deps
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@ export function useEffectOnSerializableUpdates<T>(
|
|||
serialize: (value: T) => string,
|
||||
onChange: (value: T) => void,
|
||||
) {
|
||||
React.useEffect(() => onChange(value), [onChange, serialize(value)])
|
||||
const serialized = React.useMemo(() => serialize(value), [value, serialize])
|
||||
React.useEffect(() => onChange(value), [onChange, serialized]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import { createStyleSheet, setStyleSheetMedia } from '../general'
|
||||
|
||||
export function useMediaStyleSheet(
|
||||
content: string,
|
||||
getMediaQuery: (width: number) => string[],
|
||||
size: number,
|
||||
) {
|
||||
const style = React.useRef<HTMLStyleElement>()
|
||||
// this order may prevent first effect actually occur at first time
|
||||
React.useEffect(() => {
|
||||
setSheetMedia()
|
||||
}, [size])
|
||||
React.useEffect(() => {
|
||||
style.current = createStyleSheet(content)
|
||||
setSheetMedia()
|
||||
}, [])
|
||||
function setSheetMedia() {
|
||||
if (style.current)
|
||||
setStyleSheetMedia(
|
||||
style.current,
|
||||
getMediaQuery(size)
|
||||
.map(query => `(${query})`)
|
||||
.join(' and '),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ const config: Config = {
|
|||
},
|
||||
link: 'a:not(a)', // this helps fixing the go-back-in-history issue
|
||||
form: 'form:not(form)', // prevent blocking form submissions
|
||||
fallback(target, reason) {
|
||||
fallback(/* target, reason */) {
|
||||
// prevent unexpected reload
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,13 +31,14 @@ export function useResizeHandler(
|
|||
if (!pointerDown.current) return
|
||||
const [x0, y0] = initialSizeRef.current
|
||||
// Allow minor movement, this happened unintentionally for few times when I use track pad
|
||||
pointerMoved.current = pointerMoved.current || (clientX - x0) ** 2 + (clientY - y0) ** 2 > distanceTolerance ** 2
|
||||
pointerMoved.current =
|
||||
pointerMoved.current || (clientX - x0) ** 2 + (clientY - y0) ** 2 > distanceTolerance ** 2
|
||||
const [x1, y1] = baseSize.current
|
||||
onResize([x1 + clientX - x0, y1 + clientY - y0])
|
||||
}
|
||||
window.addEventListener('pointermove', onPointerMove)
|
||||
return () => window.removeEventListener('pointermove', onPointerMove)
|
||||
}, [onResize])
|
||||
}, [onResize, distanceTolerance])
|
||||
|
||||
React.useEffect(() => {
|
||||
const onPointerUp = (e: PointerEvent) => {
|
||||
|
|
@ -53,16 +54,19 @@ export function useResizeHandler(
|
|||
}
|
||||
window.addEventListener('pointerup', onPointerUp)
|
||||
return () => window.removeEventListener('pointerup', onPointerUp)
|
||||
}, [])
|
||||
}, [onClick, onResizeStateChange])
|
||||
|
||||
const onPointerDown = React.useCallback((e: React.PointerEvent) => {
|
||||
e.preventDefault() // Prevent unexpected selection when dragging in Safari
|
||||
const { clientX, clientY } = e
|
||||
pointerDown.current = true
|
||||
initialSizeRef.current = [clientX, clientY]
|
||||
baseSize.current = latestPropSize.current
|
||||
onResizeStateChange?.('resizing')
|
||||
}, [])
|
||||
const onPointerDown = React.useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
e.preventDefault() // Prevent unexpected selection when dragging in Safari
|
||||
const { clientX, clientY } = e
|
||||
pointerDown.current = true
|
||||
initialSizeRef.current = [clientX, clientY]
|
||||
baseSize.current = latestPropSize.current
|
||||
onResizeStateChange?.('resizing')
|
||||
},
|
||||
[onResizeStateChange],
|
||||
)
|
||||
|
||||
return { onPointerDown }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,19 +5,19 @@ export function useUpdateReason<P>(props: P) {
|
|||
const lastPropsRef = React.useRef<P>(props)
|
||||
React.useEffect(() => {
|
||||
if (IN_PRODUCTION_MODE) return
|
||||
let output: unknown[][] = []
|
||||
const output: ([string, keyof P, P[keyof P]] | [string, keyof P, P[keyof P], P[keyof P]])[] = []
|
||||
for (const key of Object.keys(props)) {
|
||||
if (key === 'children') continue
|
||||
const $key = key as keyof P
|
||||
if (!(key in lastPropsRef.current)) output.push([`[Added]`, key, props[$key]])
|
||||
if (!(key in lastPropsRef.current)) output.push([`[Added]`, $key, props[$key]])
|
||||
if (lastPropsRef.current[$key] !== props[$key])
|
||||
output.push([`[Updated]`, key, lastPropsRef.current[$key], props[$key]])
|
||||
output.push([`[Updated]`, $key, lastPropsRef.current[$key], props[$key]])
|
||||
}
|
||||
|
||||
for (const key of Object.keys(lastPropsRef.current)) {
|
||||
if (key === 'children') continue
|
||||
const $key = key as keyof P
|
||||
if (!(key in props)) output.push([`[Removed]`, key, props[$key]])
|
||||
if (!(key in props)) output.push([`[Removed]`, $key, props[$key]])
|
||||
}
|
||||
|
||||
if (output.length) {
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ export function parseEvent(e: KeyboardEvent | React.KeyboardEvent) {
|
|||
const keys = { meta, ctrl, shift, alt, [code]: true }
|
||||
const combination = parse(
|
||||
Object.entries(keys)
|
||||
.filter(([key, pressed]) => pressed)
|
||||
.map(([key, pressed]) => key)
|
||||
.filter(([, pressed]) => pressed)
|
||||
.map(([key]) => key)
|
||||
.join('+'),
|
||||
)
|
||||
return combination
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export function getFileIconURL(node: TreeNode) {
|
|||
// 1. swap time with space
|
||||
// 2. prevent app crash on when extension context invalidates
|
||||
const extensionURL = browser.runtime.getURL('').replace(/\/$/, '')
|
||||
export function getIconURL(type: 'folder' | 'file', name: string = 'default', open?: boolean) {
|
||||
export function getIconURL(type: 'folder' | 'file', name = 'default', open?: boolean) {
|
||||
const filename =
|
||||
(name === 'default' ? 'default_' + type : type + '_type_' + name) +
|
||||
(open ? '_opened' : '') +
|
||||
|
|
|
|||
|
|
@ -9,20 +9,12 @@ export type Storage = {
|
|||
// ['platform_github.com']?: Config
|
||||
}
|
||||
|
||||
async function get<
|
||||
T extends {
|
||||
[key: string]: any
|
||||
}
|
||||
>(mapping: string | string[] | null = null): Promise<T | undefined> {
|
||||
try {
|
||||
return (await localStorage.get(mapping || undefined)) as T
|
||||
} catch (err) {}
|
||||
async function get<T extends JSONObject>(mapping: string | string[] | null = null) {
|
||||
return (await localStorage.get(mapping || undefined)) as T | undefined
|
||||
}
|
||||
|
||||
function set(value: any): Promise<void> | void {
|
||||
try {
|
||||
return localStorage.set(value)
|
||||
} catch (err) {}
|
||||
function set<T>(value: T): Promise<void> | void {
|
||||
return localStorage.set(value)
|
||||
}
|
||||
|
||||
export const storageHelper = { get, set }
|
||||
|
|
|
|||
Loading…
Reference in a new issue