import { Query, QueryKey } from "@tanstack/react-query"; import { JsModal, type ModalMode, ModalHeader, TabSelector, } from "@react-buoy/shared-ui"; import { useGetQueryByQueryKey } from "../../hooks/useSelectedQuery"; import { ReactQueryModalHeader } from "./ReactQueryModalHeader"; import { QueryBrowserMode } from "../QueryBrowserMode"; import { QueryBrowserFooter } from "./QueryBrowserFooter"; import { QueryFilterViewV3 } from "../QueryFilterViewV3"; import { useState, useCallback, useEffect } from "react"; import { View, StyleSheet } from "react-native"; import { devToolsStorageKeys, useSafeAsyncStorage, macOSColors } from "@react-buoy/shared-ui"; import useAllQueries from "../../hooks/useAllQueries"; interface QueryBrowserModalProps { visible: boolean; selectedQueryKey?: QueryKey; onQuerySelect: (query: Query | undefined) => void; onClose: () => void; onMinimize?: (modalState: any) => void; activeFilter?: string | null; onFilterChange?: (filter: string | null) => void; enableSharedModalDimensions?: boolean; onTabChange: (tab: "queries" | "mutations") => void; searchText?: string; onSearchChange?: (text: string) => void; } /** * Specialized modal for query browsing following "Decompose by Responsibility" * Single purpose: Display query browser when no query is selected */ export function QueryBrowserModal({ visible, selectedQueryKey, onQuerySelect, onClose, onMinimize, activeFilter: externalActiveFilter, onFilterChange: externalOnFilterChange, enableSharedModalDimensions = false, onTabChange, searchText = "", onSearchChange, }: QueryBrowserModalProps) { const selectedQuery = useGetQueryByQueryKey(selectedQueryKey); const allQueries = useAllQueries(); // Use external filter state if provided (for persistence), otherwise use internal state const [internalActiveFilter, setInternalActiveFilter] = useState< string | null >(null); const activeFilter = externalActiveFilter ?? internalActiveFilter; const setActiveFilter = externalOnFilterChange ?? setInternalActiveFilter; // Filter modal state const [showFilterView, setShowFilterView] = useState(false); const [ignoredPatterns, setIgnoredPatterns] = useState>(new Set()); const [includedPatterns, setIncludedPatterns] = useState>(new Set()); // AsyncStorage for persisting ignored patterns const { getItem: safeGetItem, setItem: safeSetItem } = useSafeAsyncStorage(); // Load ignored patterns from storage on mount useEffect(() => { const loadFilters = async () => { try { const stored = await safeGetItem( devToolsStorageKeys.reactQuery.ignoredPatterns() ); if (stored) { const patterns = JSON.parse(stored); if (Array.isArray(patterns)) { setIgnoredPatterns(new Set(patterns)); } } } catch (error) { console.error("Failed to load ignored patterns:", error); } }; loadFilters(); }, [safeGetItem]); // Load included patterns from storage on mount useEffect(() => { const loadFilters = async () => { try { const stored = await safeGetItem( devToolsStorageKeys.reactQuery.includedPatterns() ); if (stored) { const patterns = JSON.parse(stored); if (Array.isArray(patterns)) { setIncludedPatterns(new Set(patterns)); } } } catch (error) { console.error("Failed to load included patterns:", error); } }; loadFilters(); }, [safeGetItem]); // Save ignored patterns to storage when they change useEffect(() => { const saveFilters = async () => { try { const patterns = Array.from(ignoredPatterns); await safeSetItem( devToolsStorageKeys.reactQuery.ignoredPatterns(), JSON.stringify(patterns) ); } catch (error) { console.error("Failed to save ignored patterns:", error); } }; saveFilters(); }, [ignoredPatterns, safeSetItem]); // Save included patterns to storage when they change useEffect(() => { const saveFilters = async () => { try { const patterns = Array.from(includedPatterns); await safeSetItem( devToolsStorageKeys.reactQuery.includedPatterns(), JSON.stringify(patterns) ); } catch (error) { console.error("Failed to save included patterns:", error); } }; saveFilters(); }, [includedPatterns, safeSetItem]); // Toggle pattern in ignored set const handlePatternToggle = useCallback((pattern: string) => { setIgnoredPatterns((prev) => { const newSet = new Set(prev); if (newSet.has(pattern)) { newSet.delete(pattern); } else { newSet.add(pattern); } return newSet; }); }, []); // Toggle pattern in included set const handleIncludedPatternToggle = useCallback((pattern: string) => { setIncludedPatterns((prev) => { const newSet = new Set(prev); if (newSet.has(pattern)) { newSet.delete(pattern); } else { newSet.add(pattern); } return newSet; }); }, []); // Track modal mode for conditional styling const [modalMode, setModalMode] = useState("bottomSheet"); const storagePrefix = enableSharedModalDimensions ? devToolsStorageKeys.reactQuery.modal() : devToolsStorageKeys.reactQuery.browserModal(); const handleModeChange = useCallback((mode: ModalMode) => { setModalMode(mode); }, []); if (!visible) return null; const renderHeaderContent = () => { // Filter view header with back button if (showFilterView) { const tabs = [{ key: "filters" as const, label: "Filters" }]; return ( setShowFilterView(false)} /> {}} /> ); } // Normal query browser header return ( onQuerySelect(undefined)} searchText={searchText} onSearchChange={onSearchChange} onFilterPress={() => setShowFilterView(true)} hasActiveFilters={activeFilter !== null || ignoredPatterns.size > 0 || includedPatterns.size > 0} /> ); }; const footerNode = ( ); return ( {showFilterView ? ( /* Show filter view */ ) : ( /* Show query browser */ setShowFilterView(true)} /> )} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: macOSColors.background.base, }, });