diff --git a/frontend/src/pages/QuickStartPage.tsx b/frontend/src/pages/QuickStartPage.tsx index b3149bea..003c50ab 100644 --- a/frontend/src/pages/QuickStartPage.tsx +++ b/frontend/src/pages/QuickStartPage.tsx @@ -176,7 +176,8 @@ export function QuickStartPage() { return () => window.removeEventListener('focus', onFocus) }, [loadFlows]) - // Debounced search + // Debounced search with staleness guard + const searchRequestId = useRef(0) useEffect(() => { if (debounceRef.current) clearTimeout(debounceRef.current) if (query.length < 2) { @@ -188,13 +189,16 @@ export function QuickStartPage() { setIsSearching(true) setShowResults(true) debounceRef.current = setTimeout(async () => { + const requestId = ++searchRequestId.current try { const results = await treesApi.search(query, 8) + if (requestId !== searchRequestId.current) return setSearchResults(results) } catch { + if (requestId !== searchRequestId.current) return setSearchResults([]) } finally { - setIsSearching(false) + if (requestId === searchRequestId.current) setIsSearching(false) } }, 300) return () => { if (debounceRef.current) clearTimeout(debounceRef.current) } diff --git a/frontend/src/pages/SessionHistoryPage.tsx b/frontend/src/pages/SessionHistoryPage.tsx index c1dc8bfa..d5870751 100644 --- a/frontend/src/pages/SessionHistoryPage.tsx +++ b/frontend/src/pages/SessionHistoryPage.tsx @@ -60,7 +60,59 @@ export function SessionHistoryPage() { // Load sessions when filters change useEffect(() => { + let cancelled = false + + const loadSessions = async () => { + setIsLoading(true) + try { + const params: Record = {} + + // Tab filter (all/active/completed) + if (filter !== 'all') { + params.completed = filter === 'completed' + } + + // Search/filter params + if (filters.ticketNumber) { + params.ticket_number = filters.ticketNumber + } + if (filters.clientName) { + params.client_name = filters.clientName + } + if (filters.treeName) { + params.tree_name = filters.treeName + } + + // Date range params + if (filters.dateRange?.from) { + const fromDate = filters.dateRange.from + const toDate = filters.dateRange.to || filters.dateRange.from + + if (filters.dateType === 'started') { + params.started_after = fromDate.toISOString() + params.started_before = toDate.toISOString() + } else { + params.completed_after = fromDate.toISOString() + params.completed_before = toDate.toISOString() + } + } + + const sessionsData = await sessionsApi.list({ ...params, size: 51 }) + if (cancelled) return + const truncated = sessionsData.length > 50 + setHasMore(truncated) + setSessions(truncated ? sessionsData.slice(0, 50) : sessionsData) + } catch (err) { + if (cancelled) return + toast.error('Failed to load sessions') + console.error(err) + } finally { + if (!cancelled) setIsLoading(false) + } + } + loadSessions() + return () => { cancelled = true } }, [filter, filters]) // Update URL params when filters change @@ -79,53 +131,6 @@ export function SessionHistoryPage() { setSearchParams(params, { replace: true }) }, [filters, setSearchParams]) - const loadSessions = async () => { - setIsLoading(true) - try { - const params: Record = {} - - // Tab filter (all/active/completed) - if (filter !== 'all') { - params.completed = filter === 'completed' - } - - // Search/filter params - if (filters.ticketNumber) { - params.ticket_number = filters.ticketNumber - } - if (filters.clientName) { - params.client_name = filters.clientName - } - if (filters.treeName) { - params.tree_name = filters.treeName - } - - // Date range params - if (filters.dateRange?.from) { - const fromDate = filters.dateRange.from - const toDate = filters.dateRange.to || filters.dateRange.from - - if (filters.dateType === 'started') { - params.started_after = fromDate.toISOString() - params.started_before = toDate.toISOString() - } else { - params.completed_after = fromDate.toISOString() - params.completed_before = toDate.toISOString() - } - } - - const sessionsData = await sessionsApi.list({ ...params, size: 51 }) - const truncated = sessionsData.length > 50 - setHasMore(truncated) - setSessions(truncated ? sessionsData.slice(0, 50) : sessionsData) - } catch (err) { - toast.error('Failed to load sessions') - console.error(err) - } finally { - setIsLoading(false) - } - } - const handleFilterChange = (newFilters: SessionFilterState) => { setFilters(newFilters) } diff --git a/frontend/src/pages/TreeLibraryPage.tsx b/frontend/src/pages/TreeLibraryPage.tsx index 76b0a67d..09141105 100644 --- a/frontend/src/pages/TreeLibraryPage.tsx +++ b/frontend/src/pages/TreeLibraryPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useCallback, useMemo } from 'react' +import { useEffect, useState, useCallback, useMemo, useRef } from 'react' import { useNavigate, useSearchParams } from 'react-router-dom' import { X, RotateCcw, Play, FileUp } from 'lucide-react' import { PageMeta } from '@/components/common/PageMeta' @@ -158,20 +158,11 @@ export function TreeLibraryPage() { .catch((err) => console.error('Failed to load categories:', err)) }, []) - // Load trees when filters change - useEffect(() => { - loadTrees() - }, [selectedCategoryId, selectedTags, selectedFolderId, treeLibrarySortBy, typeFilter]) + // Request ID ref to discard stale responses when filters change rapidly + const loadTreesRequestId = useRef(0) - // Load folders on mount and listen for changes - useEffect(() => { - loadFolders() - const handleFolderChange = () => loadFolders() - window.addEventListener('folder-changed', handleFolderChange) - return () => window.removeEventListener('folder-changed', handleFolderChange) - }, [loadFolders]) - - const loadTrees = async () => { + const loadTrees = useCallback(async () => { + const requestId = ++loadTreesRequestId.current setIsLoading(true) try { const treesData = await treesApi.list({ @@ -181,14 +172,29 @@ export function TreeLibraryPage() { folder_id: selectedFolderId || undefined, sort_by: treeLibrarySortBy, }) + if (requestId !== loadTreesRequestId.current) return setTrees(treesData) } catch (err) { + if (requestId !== loadTreesRequestId.current) return toast.error('Failed to load flows') console.error(err) } finally { - setIsLoading(false) + if (requestId === loadTreesRequestId.current) setIsLoading(false) } - } + }, [selectedCategoryId, selectedTags, selectedFolderId, treeLibrarySortBy, typeFilter]) + + // Load trees when filters change + useEffect(() => { + loadTrees() + }, [loadTrees]) + + // Load folders on mount and listen for changes + useEffect(() => { + loadFolders() + const handleFolderChange = () => loadFolders() + window.addEventListener('folder-changed', handleFolderChange) + return () => window.removeEventListener('folder-changed', handleFolderChange) + }, [loadFolders]) const handleSearch = async () => { if (!searchQuery.trim()) {