/** * useSearchIndex Hook * Manages the searchable field registry and filters settings based on query. * * Supports two modes: * 1. Schema-based: Auto-populates from settingsSchema.ts (recommended) * 2. Manual registration: Fields register themselves via SearchableField component * * The schema-based approach ensures search stays in sync with available settings, * similar to iOS Settings search. Fields with requiresToggle will redirect to * their parent toggle when the associated setting is disabled. */ import { useState, useCallback, useMemo, useEffect } from "react"; import type { SearchableFieldData } from "../components/SearchResultsView"; import { schemaToSearchableFields } from "../settingsSchema"; import type { RequiresToggle } from "../settingsSchema"; import { calculateSearchRelevance } from "../searchUtils"; /** Extended field data that includes keywords and requiresToggle for search */ export interface SearchableFieldDataWithKeywords extends SearchableFieldData { keywords?: string[]; requiresToggle?: RequiresToggle; } export interface UseSearchIndexOptions { /** Enable schema-based auto-population (default: true) */ useSchema?: boolean; } export interface UseSearchIndexReturn { /** Current searchable fields registry */ fields: SearchableFieldDataWithKeywords[]; /** Register a new searchable field (for manual mode) */ registerField: (field: SearchableFieldDataWithKeywords) => void; /** Unregister a field by ID */ unregisterField: (id: string) => void; /** Clear all registered fields */ clearFields: () => void; /** Filter fields by query, sorted by relevance */ search: (query: string) => SearchableFieldDataWithKeywords[]; } /** * Hook to manage searchable field registry and perform searches. * All fields are indexed for search discovery. Fields with requiresToggle * will be handled by the search result handler to redirect to parent toggles. * * @example * // Schema-based (recommended) * const { search } = useSearchIndex({ useSchema: true }); * * // Manual registration (legacy) * const { registerField, unregisterField, search } = useSearchIndex({ useSchema: false }); */ export function useSearchIndex( options: UseSearchIndexOptions = {}, ): UseSearchIndexReturn { const { useSchema = true } = options; const [fieldsMap, setFieldsMap] = useState< Map >(new Map()); // Initialize from schema on mount. useEffect(() => { if (!useSchema) return; const schemaFields = schemaToSearchableFields(); const newMap = new Map(); for (const field of schemaFields) { newMap.set(field.id, field); } setFieldsMap(newMap); }, [useSchema]); const registerField = useCallback( (field: SearchableFieldDataWithKeywords) => { setFieldsMap((prev) => { const next = new Map(prev); next.set(field.id, field); return next; }); }, [], ); const unregisterField = useCallback((id: string) => { setFieldsMap((prev) => { const next = new Map(prev); next.delete(id); return next; }); }, []); const clearFields = useCallback(() => { setFieldsMap(new Map()); }, []); const fields = useMemo(() => Array.from(fieldsMap.values()), [fieldsMap]); const search = useCallback( (query: string): SearchableFieldDataWithKeywords[] => { if (!query.trim()) return []; // Score all fields and filter to those with positive scores const scoredFields = fields .map((field) => ({ field, score: calculateSearchRelevance(query, field), })) .filter(({ score }) => score > 0); // Sort by score descending (most relevant first) scoredFields.sort((a, b) => b.score - a.score); return scoredFields.map(({ field }) => field); }, [fields], ); return { fields, registerField, unregisterField, clearFields, search, }; }