diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index dc4fcdaf..d6cdd481 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useImperativeHandle, forwardRef } from 'react'; +import { useState, useEffect, useImperativeHandle, forwardRef, useCallback, useRef } from 'react'; import { CheckCircle, XCircle, Eye, Calendar, User, Filter, MessageSquare, FileText, Image, X, Trash2, ListTree, RefreshCw, AlertCircle, Clock, Lock, Unlock, AlertTriangle, UserCog, Zap } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -26,6 +26,7 @@ import { QueueStatsDashboard } from './QueueStatsDashboard'; import { EscalationDialog } from './EscalationDialog'; import { ReassignDialog } from './ReassignDialog'; import { smartMergeArray } from '@/lib/smartStateUpdate'; +import { useDebounce } from '@/hooks/useDebounce'; interface ModerationItem { id: string; @@ -95,6 +96,7 @@ export const ModerationQueue = forwardRef((props, ref) => { const { isAdmin, isSuperuser } = useUserRole(); const { user } = useAuth(); const queue = useModerationQueue(); + const fetchInProgressRef = useRef(false); // Get admin settings for polling configuration const { @@ -108,11 +110,19 @@ export const ModerationQueue = forwardRef((props, ref) => { const refreshStrategy = getAutoRefreshStrategy(); const preserveInteraction = getPreserveInteractionState(); - const fetchItems = async (entityFilter: EntityFilter = 'all', statusFilter: StatusFilter = 'pending', silent = false) => { + const fetchItems = useCallback(async (entityFilter: EntityFilter = 'all', statusFilter: StatusFilter = 'pending', silent = false) => { if (!user) { return; } + // Prevent concurrent calls - race condition guard + if (fetchInProgressRef.current) { + console.log('⚠️ Fetch already in progress, skipping duplicate call'); + return; + } + + fetchInProgressRef.current = true; + console.log('🔍 fetchItems called:', { entityFilter, statusFilter, @@ -394,11 +404,26 @@ export const ModerationQueue = forwardRef((props, ref) => { variant: 'destructive', }); } finally { + fetchInProgressRef.current = false; setLoading(false); setIsRefreshing(false); setIsInitialLoad(false); } - }; + }, [ + user, + entityCache, + profileCache, + submissionMemo, + items, + refreshStrategy, + preserveInteraction, + interactingWith, + toast + ]); + + // Debounced filters to prevent rapid-fire calls + const debouncedEntityFilter = useDebounce(activeEntityFilter, 300); + const debouncedStatusFilter = useDebounce(activeStatusFilter, 300); // Expose refresh method via ref useImperativeHandle(ref, () => ({ @@ -410,22 +435,22 @@ export const ModerationQueue = forwardRef((props, ref) => { // Initial fetch on mount and filter changes useEffect(() => { if (user) { - fetchItems(activeEntityFilter, activeStatusFilter, false); // Show loading + fetchItems(debouncedEntityFilter, debouncedStatusFilter, false); // Show loading } - }, [activeEntityFilter, activeStatusFilter, user, fetchItems]); + }, [debouncedEntityFilter, debouncedStatusFilter, user, fetchItems]); // Polling for auto-refresh useEffect(() => { if (!user || refreshMode !== 'auto' || isInitialLoad) return; const interval = setInterval(() => { - fetchItems(activeEntityFilter, activeStatusFilter, true); // Silent refresh + fetchItems(debouncedEntityFilter, debouncedStatusFilter, true); // Silent refresh }, pollInterval); return () => { clearInterval(interval); }; - }, [user, refreshMode, pollInterval, activeEntityFilter, activeStatusFilter, isInitialLoad, fetchItems]); + }, [user, refreshMode, pollInterval, debouncedEntityFilter, debouncedStatusFilter, isInitialLoad, fetchItems]); // Real-time subscription for lock status useEffect(() => {