({
accessToken,
selected,
onChange,
}: {
accessToken: string | null;
selected: DirectoryItem[];
onChange: (v: DirectoryItem[]) => void;
})
| 11 | }; |
| 12 | |
| 13 | export function SearchUsers({ |
| 14 | accessToken, |
| 15 | selected, |
| 16 | onChange, |
| 17 | }: { |
| 18 | accessToken: string | null; |
| 19 | selected: DirectoryItem[]; |
| 20 | onChange: (v: DirectoryItem[]) => void; |
| 21 | }) { |
| 22 | const [q, setQ] = useState(""); |
| 23 | const [debouncedQ, setDebouncedQ] = useState(""); |
| 24 | const [includeGroups, setIncludeGroups] = useState(true); |
| 25 | const [results, setResults] = useState<DirectoryItem[]>([]); |
| 26 | const [loading, setLoading] = useState(false); |
| 27 | |
| 28 | // Debounce the query to avoid excessive API calls while typing |
| 29 | useEffect(() => { |
| 30 | const handle = setTimeout(() => setDebouncedQ(q), 300); |
| 31 | return () => clearTimeout(handle); |
| 32 | }, [q]); |
| 33 | |
| 34 | useEffect(() => { |
| 35 | const ctrl = new AbortController(); |
| 36 | let canceled = false; |
| 37 | const run = async () => { |
| 38 | if (!accessToken || debouncedQ.trim().length < 2) { |
| 39 | setResults([]); |
| 40 | return; |
| 41 | } |
| 42 | const url = new URL("/api/search", import.meta.env.VITE_API_URL); |
| 43 | url.searchParams.set("q", debouncedQ); |
| 44 | url.searchParams.set("includeGroups", String(includeGroups)); |
| 45 | try { |
| 46 | setLoading(true); |
| 47 | const res = await fetch(url, { |
| 48 | headers: { Authorization: `Bearer ${accessToken}` }, |
| 49 | signal: ctrl.signal, |
| 50 | }); |
| 51 | if (!res.ok) return; |
| 52 | const data = await res.json(); |
| 53 | if (!canceled) setResults(data); |
| 54 | } finally { |
| 55 | if (!canceled) setLoading(false); |
| 56 | } |
| 57 | }; |
| 58 | run(); |
| 59 | return () => { |
| 60 | canceled = true; |
| 61 | ctrl.abort(); |
| 62 | }; |
| 63 | }, [debouncedQ, includeGroups, accessToken]); |
| 64 | |
| 65 | const add = (item: DirectoryItem) => { |
| 66 | if (selected.some((s) => s.id === item.id && s.type === item.type)) return; |
| 67 | onChange([...selected, item]); |
| 68 | }; |
| 69 | const remove = (id: string, type: "user" | "group") => |
| 70 | onChange(selected.filter((s) => !(s.id === id && s.type === type))); |
nothing calls this directly
no test coverage detected