import { useMemo, useCallback } from "react"; import { View, StyleSheet, Text, ScrollView, ViewStyle } from "react-native"; import { Query } from "@tanstack/react-query"; import QueryRow from "./QueryRow"; import useAllQueries from "../../hooks/useAllQueries"; import { getQueryStatusLabel } from "../../utils/getQueryStatusLabel"; import { gameUIColors } from "@react-buoy/shared-ui"; import { macOSColors } from "@react-buoy/shared-ui"; interface Props { selectedQuery: Query | undefined; onQuerySelect: (query: Query | undefined) => void; activeFilter?: string | null; emptyStateMessage?: string; contentContainerStyle?: ViewStyle; queries?: Query[]; // Optional external queries to override useAllQueries searchText?: string; ignoredPatterns?: Set; includedPatterns?: Set; } /** * Scrollable list view of cached queries with filtering support and selection handling. */ export default function QueryBrowser({ selectedQuery, onQuerySelect, activeFilter, emptyStateMessage, contentContainerStyle, queries: externalQueries, searchText = "", ignoredPatterns = new Set(), includedPatterns = new Set(), }: Props) { // Holds all queries using the working hook, or use external queries if provided const internalQueries = useAllQueries(); const allQueries = externalQueries ?? internalQueries; // Filter queries based on active filter, search text, and ignored patterns const filteredQueries = useMemo(() => { let filtered = allQueries; // Apply status filter if (activeFilter) { filtered = filtered.filter((query: Query) => { const status = getQueryStatusLabel(query); return status === activeFilter; }); } // Apply search filter on query keys if (searchText) { const searchLower = searchText.toLowerCase(); filtered = filtered.filter((query: Query) => { if (!query?.queryKey) return false; const keys = Array.isArray(query.queryKey) ? query.queryKey : [query.queryKey]; const keyString = keys .filter((k) => k != null) .map((k) => String(k)) .join(" ") .toLowerCase(); return keyString.includes(searchLower); }); } // Apply included patterns filter (show ONLY queries matching any pattern) // This acts as a whitelist - if any include patterns exist, query must match at least one if (includedPatterns.size > 0) { filtered = filtered.filter((query: Query) => { if (!query?.queryKey) return false; const keys = Array.isArray(query.queryKey) ? query.queryKey : [query.queryKey]; const keyString = keys .filter((k) => k != null) .map((k) => String(k)) .join(" ") .toLowerCase(); // Return true if query matches ANY included pattern return Array.from(includedPatterns).some((pattern) => keyString.includes(pattern.toLowerCase()) ); }); } // Apply ignored patterns filter (hide queries matching any pattern) if (ignoredPatterns.size > 0) { filtered = filtered.filter((query: Query) => { if (!query?.queryKey) return true; const keys = Array.isArray(query.queryKey) ? query.queryKey : [query.queryKey]; const keyString = keys .filter((k) => k != null) .map((k) => String(k)) .join(" ") .toLowerCase(); // Return true if NO patterns match (i.e., keep the query) return !Array.from(ignoredPatterns).some((pattern) => keyString.includes(pattern.toLowerCase()) ); }); } // Sort by most recently updated (dataUpdatedAt descending) filtered.sort((a, b) => { const aTime = a.state.dataUpdatedAt || 0; const bTime = b.state.dataUpdatedAt || 0; return bTime - aTime; // Most recent first }); return filtered; }, [allQueries, activeFilter, searchText, includedPatterns, ignoredPatterns]); // Function to handle query selection with stable comparison const handleQuerySelect = useCallback( (query: Query) => { // Compare queries by their queryKey and queryHash for stable selection const isCurrentlySelected = selectedQuery?.queryHash === query.queryHash; if (isCurrentlySelected) { onQuerySelect(undefined); // Deselect return; } onQuerySelect(query); }, [selectedQuery?.queryHash, onQuerySelect] ); if (filteredQueries.length === 0) { return ( {emptyStateMessage || (activeFilter ? `No ${activeFilter} queries found` : "No queries found")} ); } return ( {filteredQueries.map((query) => ( ))} ); } const styles = StyleSheet.create({ listWrapper: { flexGrow: 1, }, listContent: { paddingBottom: 16, backgroundColor: macOSColors.background.base, }, emptyContainer: { flex: 1, justifyContent: "center", alignItems: "center", padding: 32, backgroundColor: macOSColors.background.card, margin: 16, borderRadius: 8, borderWidth: 1, borderColor: macOSColors.border.default, }, emptyText: { color: macOSColors.text.muted, fontSize: 14, textAlign: "center", fontFamily: "monospace", letterSpacing: 0.5, }, });