feat(select): implement searchable select component with server search capabilities
- Enhanced the Select component to support a searchable dropdown, allowing users to filter options by label or value. - Introduced a new useSelectServerSearch hook for server-side searching, enabling dynamic fetching of options based on user input. - Updated TenderDetailsHeader and TenderListFilters to utilize the searchable feature, improving user experience in selecting languages and countries. - Added utility functions for filtering select items and managing search states, ensuring efficient data handling and responsiveness.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import useDebounce from "@/hooks/useDebounce";
|
||||
import type { ILabelValue } from "@/types/shared";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export type SelectSearchMode = "client" | "server";
|
||||
|
||||
export type UseSelectServerSearchArgs = {
|
||||
/** When `"server"`, options are loaded via `searchFn` + React Query. */
|
||||
searchMode: SelectSearchMode;
|
||||
searchable: boolean;
|
||||
/** Whether the dropdown panel is open (queries run only while open). */
|
||||
open: boolean;
|
||||
/** Live search input (debounced before the request). */
|
||||
query: string;
|
||||
debounceMs?: number;
|
||||
/** Base React Query key; debounced search term is appended. */
|
||||
searchQueryKey?: readonly unknown[];
|
||||
searchFn?: (query: string) => Promise<ILabelValue[]>;
|
||||
/** Fallback / initial options (e.g. first page or selected value). */
|
||||
items: readonly ILabelValue[];
|
||||
/** Current selected value — kept visible if missing from server results. */
|
||||
selectedValue?: string;
|
||||
};
|
||||
|
||||
export function useSelectServerSearch({
|
||||
searchMode,
|
||||
searchable,
|
||||
open,
|
||||
query,
|
||||
debounceMs = 300,
|
||||
searchQueryKey,
|
||||
searchFn,
|
||||
items,
|
||||
selectedValue,
|
||||
}: UseSelectServerSearchArgs) {
|
||||
const isServerMode = searchable && searchMode === "server" && !!searchFn;
|
||||
|
||||
const debouncedQuery = useDebounce(query, debounceMs);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [...(searchQueryKey ?? ["select-search"]), debouncedQuery],
|
||||
[searchQueryKey, debouncedQuery],
|
||||
);
|
||||
|
||||
const {
|
||||
data: serverItems,
|
||||
isFetching,
|
||||
isPending,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey,
|
||||
queryFn: () => searchFn!(debouncedQuery),
|
||||
enabled: isServerMode && open,
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
|
||||
const displayItems = useMemo(() => {
|
||||
if (!isServerMode) return null;
|
||||
|
||||
const list = serverItems ?? (debouncedQuery.trim() ? [] : [...items]);
|
||||
|
||||
if (!selectedValue) return list;
|
||||
|
||||
const hasSelected = list.some(
|
||||
(i) => String(i.value) === String(selectedValue),
|
||||
);
|
||||
if (hasSelected) return list;
|
||||
|
||||
const fromItems = items.find(
|
||||
(i) => String(i.value) === String(selectedValue),
|
||||
);
|
||||
if (fromItems) return [fromItems, ...list];
|
||||
|
||||
return [{ value: selectedValue, label: String(selectedValue) }, ...list];
|
||||
}, [
|
||||
isServerMode,
|
||||
serverItems,
|
||||
debouncedQuery,
|
||||
items,
|
||||
selectedValue,
|
||||
]);
|
||||
|
||||
const isSearchDebouncing =
|
||||
isServerMode && open && query.trim() !== debouncedQuery.trim();
|
||||
|
||||
return {
|
||||
isServerMode,
|
||||
displayItems,
|
||||
isSearchLoading:
|
||||
isServerMode &&
|
||||
open &&
|
||||
(isSearchDebouncing || isFetching || isPending),
|
||||
isSearchDebouncing,
|
||||
isSearchError: isServerMode && open && isError,
|
||||
searchError: error,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user