import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { IInwinkContextQuery, InwinkContextOption } from '../context/InwinkContextProvider'; interface ContextOptionsState { options: InwinkContextOption[]; isLoading: boolean; error: string | null; /** Relance le loader avec un terme de recherche texte (debouncé). */ search: (query: string) => void; } const DEBOUNCE_MS = 250; /** * Charge une liste d'options de contexte via le loader fourni * (`loadEvents` / `loadCommunities` / `loadTenants` de useInwinkContext). * Mutualise la gestion chargement / erreur des pickers et expose `search()` * pour relancer le loader côté serveur (par nom ou id) en debounçant la saisie. * * `selectedId` (id courant, typiquement issu de l'URL) est fetché par id et * fusionné dans les options : la liste `me//query` étant paginée (1ʳᵉ page * seulement), l'objet sélectionné peut ne pas y figurer alors qu'il existe — sans ce * fetch le picker afficherait l'id brut au lieu de son libellé. * * Les réponses obsolètes (saisie rapide, démontage) sont ignorées. */ export function useContextOptions( load: (query?: IInwinkContextQuery) => Promise, selectedId?: string, ): ContextOptionsState { const [options, setOptions] = useState([]); const [selectedOption, setSelectedOption] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); // `load` provient du contexte et change d'identité à chaque rendu : on le garde // dans une ref pour ne pas relancer le chargement initial à chaque rendu. const loadRef = useRef(load); loadRef.current = load; // Identifiant monotone de requête : seule la dernière en vol met à jour l'état. const reqId = useRef(0); const debounce = useRef | undefined>(undefined); const run = useCallback((query?: IInwinkContextQuery) => { const id = ++reqId.current; setIsLoading(true); setError(null); loadRef.current(query) .then((opts) => { if (id === reqId.current) setOptions(opts); }) .catch((err: unknown) => { if (id === reqId.current) setError(err instanceof Error ? err.message : String(err)); }) .finally(() => { if (id === reqId.current) setIsLoading(false); }); }, []); // Chargement initial (sans filtre) au montage ; invalide les requêtes en vol au démontage. useEffect(() => { run(); return () => { reqId.current++; clearTimeout(debounce.current); }; }, [run]); // Fetch du détail de l'objet sélectionné par id (cf. doc ci-dessus). useEffect(() => { if (!selectedId) { setSelectedOption(null); return; } let active = true; loadRef.current({ id: selectedId }) .then((opts) => { if (active) setSelectedOption(opts.find((o) => o.id === selectedId) ?? opts[0] ?? null); }) .catch(() => { // En cas d'échec, on laisse le picker afficher l'id brut : non bloquant. }); return () => { active = false; }; }, [selectedId]); const search = useCallback( (query: string) => { clearTimeout(debounce.current); const q = query.trim(); debounce.current = setTimeout(() => run(q ? { search: q } : undefined), DEBOUNCE_MS); }, [run], ); // Garantit que l'option sélectionnée est toujours présente (et en tête) même si // elle est absente de la page courante ou d'un résultat de recherche filtré. const mergedOptions = useMemo(() => { if (!selectedOption || options.some((o) => o.id === selectedOption.id)) return options; return [selectedOption, ...options]; }, [options, selectedOption]); return { options: mergedOptions, isLoading, error, search }; }