Skip to content

Commit

Permalink
refactor(ui): add loading debounce (TabbyML#1569)
Browse files Browse the repository at this point in the history
* feat(ui): useDebounce

* update

* update
  • Loading branch information
liangfung authored Feb 28, 2024
1 parent 41f48e7 commit 8162476
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { toast } from 'sonner'
import * as z from 'zod'

import { graphql } from '@/lib/gql/generates'
import { useDebounceCallback } from '@/lib/hooks/use-debounce'
import { useMutation } from '@/lib/tabby/gql'
import { cn } from '@/lib/utils'
import {
Expand Down Expand Up @@ -64,9 +65,34 @@ export function LicenseForm({
resolver: zodResolver(formSchema)
})
const license = form.watch('license')
const { isSubmitting } = form.formState
const [isReseting, setIsDeleting] = React.useState(false)
const [isSubmitting, setIsSubmitting] = React.useState(false)
const [resetDialogOpen, setResetDialogOpen] = React.useState(false)
const [isResetting, setIsResetting] = React.useState(false)

const toggleSubmitting = useDebounceCallback(
(value: boolean, success?: boolean) => {
setIsSubmitting(value)
if (success) {
form.reset({ license: '' })
toast.success('License is uploaded')
onSuccess?.()
}
},
500,
{ leading: true }
)

const toggleResetting = useDebounceCallback(
(value: boolean, success?: boolean) => {
setIsResetting(value)
if (success) {
setResetDialogOpen(false)
onSuccess?.()
}
},
500,
{ leading: true }
)

const uploadLicense = useMutation(uploadLicenseMutation, {
form
Expand All @@ -75,34 +101,27 @@ export function LicenseForm({
const resetLicense = useMutation(resetLicenseMutation)

const onSubmit = (values: FormValues) => {
toggleSubmitting.run(true)
return uploadLicense(values).then(res => {
if (res?.data?.uploadLicense) {
form.reset({ license: '' })
toast.success('License is uploaded')
onSuccess?.()
}
toggleSubmitting.run(false, res?.data?.uploadLicense)
})
}

const onReset: React.MouseEventHandler<HTMLButtonElement> = e => {
e.preventDefault()
setIsDeleting(true)
resetLicense()
.then(res => {
if (res?.data?.resetLicense) {
setResetDialogOpen(false)
onSuccess?.()
} else if (res?.error) {
toast.error(res.error.message ?? 'reset failed')
}
})
.finally(() => {
setIsDeleting(false)
})
toggleResetting.run(true)
resetLicense().then(res => {
const isSuccess = res?.data?.resetLicense
toggleResetting.run(false, isSuccess)

if (res?.error) {
toast.error(res.error.message ?? 'reset failed')
}
})
}

const onResetDialogOpenChange = (v: boolean) => {
if (isReseting) return
if (isResetting) return
setResetDialogOpen(v)
}

Expand All @@ -126,6 +145,7 @@ export function LicenseForm({
</FormItem>
)}
/>
<FormMessage />
<div className="mt-2 flex items-center justify-end gap-4">
<AlertDialog
open={resetDialogOpen}
Expand All @@ -151,9 +171,9 @@ export function LicenseForm({
<AlertDialogAction
className={buttonVariants({ variant: 'destructive' })}
onClick={onReset}
disabled={isReseting}
disabled={isResetting}
>
{isReseting && (
{isResetting && (
<IconSpinner className="mr-2 h-4 w-4 animate-spin" />
)}
Yes, reset it
Expand All @@ -169,7 +189,6 @@ export function LicenseForm({
</Button>
</div>
</form>
<FormMessage className="text-center" />
</Form>
</div>
)
Expand Down
4 changes: 2 additions & 2 deletions ee/tabby-ui/app/files/components/file-directory-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import { find, omit } from 'lodash-es'

import { useDebounce } from '@/lib/hooks/use-debounce'
import { useDebounceValue } from '@/lib/hooks/use-debounce'
import { cn } from '@/lib/utils'
import { IconDirectorySolid, IconFile } from '@/components/ui/icons'
import { Skeleton } from '@/components/ui/skeleton'
Expand All @@ -28,7 +28,7 @@ const DirectoryPanel: React.FC<DirectoryPanelProps> = ({
return getCurrentDirFromTree(fileTreeData, activePath)
}, [fileTreeData, activePath])

const loading = useDebounce(propsLoading, 300)
const [loading] = useDebounceValue(propsLoading, 300)

const showParentEntry = currentFileRoutes?.length > 0

Expand Down
6 changes: 3 additions & 3 deletions ee/tabby-ui/app/files/components/file-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React from 'react'
import { SWRResponse } from 'swr'
import useSWRImmutable from 'swr/immutable'

import { useDebounce } from '@/lib/hooks/use-debounce'
import { useDebounceValue } from '@/lib/hooks/use-debounce'
import fetcher from '@/lib/tabby/fetcher'
import type { ResolveEntriesResponse, TFile } from '@/lib/types'
import { cn } from '@/lib/utils'
Expand Down Expand Up @@ -225,7 +225,7 @@ const DirectoryTreeNode: React.FC<DirectoryTreeNodeProps> = ({
!fileMap?.[node.fullPath]?.treeExpanded &&
expanded

const { data, isValidating }: SWRResponse<ResolveEntriesResponse> =
const { data, isLoading }: SWRResponse<ResolveEntriesResponse> =
useSWRImmutable(
shouldFetchChildren
? `/repositories/${repositoryName}/resolve/${basename}`
Expand Down Expand Up @@ -262,7 +262,7 @@ const DirectoryTreeNode: React.FC<DirectoryTreeNodeProps> = ({
onSelectTreeNode?.(node)
}

const loading = useDebounce(isValidating, 100)
const [loading] = useDebounceValue(isLoading, 300)

const existingChildren = !!node?.children?.length

Expand Down
62 changes: 62 additions & 0 deletions ee/tabby-ui/lib/hooks/use-debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react'
import { debounce, type DebounceSettings } from 'lodash-es'

import { useLatest } from './use-latest'
import { useUnmount } from './use-unmount'

type noop = (...args: any[]) => any

// interface UseDebounceOptions<T = any> extends DebounceSettings {
// onFire?: (value: T) => void
// }

function useDebounceCallback<T extends noop>(
fn: T,
wait: number,
options?: DebounceSettings
) {
const fnRef = useLatest(fn)
const debounced = React.useMemo(
() =>
debounce(
(...args: Parameters<T>): ReturnType<T> => {
return fnRef.current(...args)
},
wait,
options
),
[]
)

useUnmount(() => debounced.cancel())

return {
run: debounced,
cancel: debounced.cancel,
flush: debounced.flush
}
}

function useDebounceValue<T>(
value: T,
wait: number,
options?: DebounceSettings
): [T, React.Dispatch<React.SetStateAction<T>>] {
const [debouncedValue, setDebouncedValue] = React.useState(value)

const { run } = useDebounceCallback(
() => {
setDebouncedValue(value)
},
wait,
options
)

React.useEffect(() => {
run()
}, [value])

return [debouncedValue, setDebouncedValue]
}

export { useDebounceCallback, useDebounceValue }
15 changes: 0 additions & 15 deletions ee/tabby-ui/lib/hooks/use-debounce.tsx

This file was deleted.

10 changes: 10 additions & 0 deletions ee/tabby-ui/lib/hooks/use-latest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'

function useLatest<T>(value: T) {
const ref = React.useRef(value)
ref.current = value

return ref
}

export { useLatest }
16 changes: 16 additions & 0 deletions ee/tabby-ui/lib/hooks/use-unmount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react'

import { useLatest } from './use-latest'

const useUnmount = (fn: () => void) => {
const fnRef = useLatest(fn)

React.useEffect(
() => () => {
fnRef.current()
},
[]
)
}

export { useUnmount }

0 comments on commit 8162476

Please sign in to comment.