chore: resolve eslint issues

This commit is contained in:
EnixCoda 2022-05-17 23:06:05 +08:00
parent 77d9db6043
commit 2c93bc64eb
45 changed files with 150 additions and 215 deletions

View file

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

View file

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

View file

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

View file

@ -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
}

View file

@ -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
}

View file

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

View file

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

View file

@ -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
}
}

View file

@ -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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
}

View file

@ -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 []
}
}

View file

@ -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,

View file

@ -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(

View file

@ -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,

View file

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

View file

@ -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`)
}

View file

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

View file

@ -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
}

View file

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

View file

@ -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]>

View file

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

View file

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

View file

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

View file

@ -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']

View file

@ -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,

View file

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

View file

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

View file

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

View file

@ -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
}
}

View file

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

View file

@ -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
}

View file

@ -43,6 +43,6 @@ export function useCatchNetworkError() {
}
}
},
[accessToken /* , stateContext.value, errorContext.value */],
[accessToken /* , stateContext.value, errorContext.value */], // eslint-disable-line react-hooks/exhaustive-deps
)
}

View file

@ -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
}

View file

@ -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 '),
)
}
}

View file

@ -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
},
}

View file

@ -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 }
}

View file

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

View file

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

View file

@ -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' : '') +

View file

@ -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 }