admin/components/formElements/input/useRemoteSelectOptions.ts
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

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