7aa13bb904
- 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.
103 lines
2.7 KiB
TypeScript
103 lines
2.7 KiB
TypeScript
"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,
|
|
};
|
|
}
|