Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
116 lines
3.6 KiB
TypeScript
116 lines
3.6 KiB
TypeScript
import qs from 'qs'
|
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
|
|
import type { SelectOptionItem } from '@/components/formElements/input/inputUtils'
|
|
import axiosInstance from '@/config/axios'
|
|
import { debounce } from '@/helpers'
|
|
|
|
type DebouncedSearch = ((value: string) => void) & { cancel?: () => void }
|
|
|
|
interface Options {
|
|
apiField: string
|
|
disabled?: boolean
|
|
enabled: boolean
|
|
filterName: string
|
|
filterValue: string | number
|
|
getCurrentValue: () => unknown
|
|
isOutsideFilter?: boolean
|
|
pageSize: number
|
|
searchMode: 'local' | 'api'
|
|
selectKey: string
|
|
selectValue: string
|
|
url?: string
|
|
}
|
|
|
|
export function useRemoteSelectOptions({
|
|
apiField,
|
|
disabled,
|
|
enabled,
|
|
filterName,
|
|
filterValue,
|
|
getCurrentValue,
|
|
isOutsideFilter,
|
|
pageSize,
|
|
searchMode,
|
|
selectKey,
|
|
selectValue,
|
|
url,
|
|
}: Options) {
|
|
const [options, setOptions] = useState<SelectOptionItem[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState<unknown>(null)
|
|
const sourceOptions = useRef<SelectOptionItem[]>([])
|
|
const firstFetch = useRef(true)
|
|
const abortController = useRef<AbortController | null>(null)
|
|
const debouncedSearch = useRef<DebouncedSearch | null>(null)
|
|
|
|
const fetchOptions = useCallback(
|
|
async (searchValue?: string) => {
|
|
if (!url) return
|
|
if (!firstFetch.current && searchMode === 'local') {
|
|
setOptions(
|
|
searchValue
|
|
? sourceOptions.current.filter((item) => {
|
|
const value = (item as Record<string, unknown>)[selectValue]
|
|
|
|
return typeof value === 'string' && value.includes(searchValue)
|
|
})
|
|
: sourceOptions.current
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
abortController.current?.abort()
|
|
const controller = new AbortController()
|
|
|
|
abortController.current = controller
|
|
setLoading(true)
|
|
setError(null)
|
|
try {
|
|
const currentValue = getCurrentValue()
|
|
const selectedFilter = searchValue ? { [selectValue]: searchValue } : currentValue ? { [selectKey]: currentValue } : {}
|
|
const commonFilter = filterValue ? { [filterName]: filterValue } : {}
|
|
const params = isOutsideFilter
|
|
? { ...commonFilter, ...selectedFilter }
|
|
: { filters: { ...commonFilter, ...selectedFilter }, page: 1, pageSize }
|
|
const response = await axiosInstance.get(url, {
|
|
params,
|
|
paramsSerializer: (value) => qs.stringify(value, { arrayFormat: 'brackets' }),
|
|
signal: controller.signal,
|
|
})
|
|
const data = response.data?.[apiField]
|
|
const nextOptions = Array.isArray(data) ? (data as SelectOptionItem[]) : []
|
|
|
|
sourceOptions.current = nextOptions
|
|
setOptions(nextOptions)
|
|
firstFetch.current = false
|
|
} catch (requestError) {
|
|
if (!controller.signal.aborted) setError(requestError)
|
|
} finally {
|
|
if (abortController.current === controller) setLoading(false)
|
|
}
|
|
},
|
|
[apiField, filterName, filterValue, getCurrentValue, isOutsideFilter, pageSize, searchMode, selectKey, selectValue, url]
|
|
)
|
|
|
|
useEffect(() => {
|
|
debouncedSearch.current?.cancel?.()
|
|
debouncedSearch.current = debounce((value: string) => void fetchOptions(value), 500)
|
|
|
|
return () => debouncedSearch.current?.cancel?.()
|
|
}, [fetchOptions])
|
|
|
|
useEffect(() => {
|
|
if (!enabled || disabled) return
|
|
firstFetch.current = true
|
|
void fetchOptions()
|
|
|
|
return () => abortController.current?.abort()
|
|
}, [disabled, enabled, fetchOptions, filterValue])
|
|
|
|
const search = useCallback((value: string) => debouncedSearch.current?.(value), [])
|
|
|
|
return { error, loading, options, refresh: fetchOptions, search }
|
|
}
|