Files
tm_panel/src/components/FormElements/use-select-server-search.ts
T
AmirReza Jamali 7aa13bb904 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.
2026-05-16 09:18:12 +03:30

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