mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 17:31:12 -05:00
Fix moderation queue flashing
This commit is contained in:
@@ -121,14 +121,22 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
const preserveInteraction = getPreserveInteractionState();
|
||||
const useRealtimeQueue = getUseRealtimeQueue();
|
||||
|
||||
// Store admin settings in refs to avoid triggering fetchItems recreation
|
||||
// Store admin settings and stable refs to avoid triggering fetchItems recreation
|
||||
const refreshStrategyRef = useRef(refreshStrategy);
|
||||
const preserveInteractionRef = useRef(preserveInteraction);
|
||||
const userRef = useRef(user);
|
||||
const toastRef = useRef(toast);
|
||||
const isAdminRef = useRef(isAdmin);
|
||||
const isSuperuserRef = useRef(isSuperuser);
|
||||
|
||||
useEffect(() => {
|
||||
refreshStrategyRef.current = refreshStrategy;
|
||||
preserveInteractionRef.current = preserveInteraction;
|
||||
}, [refreshStrategy, preserveInteraction]);
|
||||
userRef.current = user;
|
||||
toastRef.current = toast;
|
||||
isAdminRef.current = isAdmin;
|
||||
isSuperuserRef.current = isSuperuser;
|
||||
}, [refreshStrategy, preserveInteraction, user, toast, isAdmin, isSuperuser]);
|
||||
|
||||
// Only sync itemsRef (not loadedIdsRef) to avoid breaking silent polling logic
|
||||
useEffect(() => {
|
||||
@@ -136,7 +144,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
}, [items]);
|
||||
|
||||
const fetchItems = useCallback(async (entityFilter: EntityFilter = 'all', statusFilter: StatusFilter = 'pending', silent = false, tab: QueueTab = 'mainQueue') => {
|
||||
if (!user) {
|
||||
if (!userRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -525,12 +533,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
setIsRefreshing(false);
|
||||
setIsInitialLoad(false);
|
||||
}
|
||||
}, [user, toast, isAdmin, isSuperuser]);
|
||||
|
||||
// Log when fetchItems is recreated to help debug refresh issues
|
||||
useEffect(() => {
|
||||
console.log('🔄 fetchItems callback recreated');
|
||||
}, [fetchItems]);
|
||||
}, []); // Empty deps - use refs instead
|
||||
|
||||
// Debounced filters to prevent rapid-fire calls
|
||||
const debouncedEntityFilter = useDebounce(activeEntityFilter, 500);
|
||||
@@ -768,7 +771,10 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
description: "Submission and all items have been reset to pending status",
|
||||
});
|
||||
|
||||
fetchItems(activeEntityFilter, activeStatusFilter);
|
||||
// Silent cleanup after delay
|
||||
setTimeout(() => {
|
||||
fetchItems(activeEntityFilter, activeStatusFilter, true);
|
||||
}, 2000);
|
||||
} catch (error: any) {
|
||||
console.error('Error resetting submission:', error);
|
||||
toast({
|
||||
@@ -819,7 +825,10 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
description: `Processed ${failedItems.length} failed item(s)`,
|
||||
});
|
||||
|
||||
fetchItems(activeEntityFilter, activeStatusFilter);
|
||||
// Silent cleanup after delay
|
||||
setTimeout(() => {
|
||||
fetchItems(activeEntityFilter, activeStatusFilter, true);
|
||||
}, 2000);
|
||||
} catch (error: any) {
|
||||
console.error('Error retrying failed items:', error);
|
||||
toast({
|
||||
@@ -844,6 +853,19 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
|
||||
setActionLoading(item.id);
|
||||
|
||||
// Determine if item should be removed from current view after action
|
||||
const shouldRemove = (activeStatusFilter === 'pending' || activeStatusFilter === 'flagged') &&
|
||||
(action === 'approved' || action === 'rejected');
|
||||
|
||||
// Optimistic update
|
||||
if (shouldRemove) {
|
||||
setItems(prev => prev.filter(i => i.id !== item.id));
|
||||
} else {
|
||||
setItems(prev => prev.map(i =>
|
||||
i.id === item.id ? { ...i, status: action } : i
|
||||
));
|
||||
}
|
||||
|
||||
// Release lock if this submission is claimed by current user
|
||||
if (queue.currentLock?.submissionId === item.id) {
|
||||
await queue.releaseLock(item.id);
|
||||
@@ -952,8 +974,10 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
description: "All entities created successfully",
|
||||
});
|
||||
|
||||
// Refresh the queue
|
||||
fetchItems(activeEntityFilter, activeStatusFilter);
|
||||
// Silent cleanup after delay
|
||||
setTimeout(() => {
|
||||
fetchItems(activeEntityFilter, activeStatusFilter, true);
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1049,8 +1073,10 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
description: `Successfully approved and published ${photoSubmission.items.length} photo(s)`,
|
||||
});
|
||||
|
||||
// Refresh the queue
|
||||
fetchItems(activeEntityFilter, activeStatusFilter);
|
||||
// Silent cleanup after delay
|
||||
setTimeout(() => {
|
||||
fetchItems(activeEntityFilter, activeStatusFilter, true);
|
||||
}, 2000);
|
||||
return;
|
||||
|
||||
} catch (error: any) {
|
||||
@@ -1089,8 +1115,10 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
description: `Successfully processed ${submissionItems.length} item(s)`,
|
||||
});
|
||||
|
||||
// Refresh and return early since edge function already updated parent
|
||||
fetchItems(activeEntityFilter, activeStatusFilter);
|
||||
// Silent cleanup after delay
|
||||
setTimeout(() => {
|
||||
fetchItems(activeEntityFilter, activeStatusFilter, true);
|
||||
}, 2000);
|
||||
return;
|
||||
} else if (action === 'rejected') {
|
||||
// Cascade rejection to all pending items
|
||||
@@ -1156,35 +1184,30 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
description: `The ${item.type} has been ${action}`,
|
||||
});
|
||||
|
||||
// Only update local state if the database update was successful
|
||||
setItems(prev => prev.map(i =>
|
||||
i.id === item.id
|
||||
? { ...i, status: action }
|
||||
: i
|
||||
));
|
||||
|
||||
// Clear notes only after successful update
|
||||
// Clear notes after successful update
|
||||
setNotes(prev => {
|
||||
const newNotes = { ...prev };
|
||||
delete newNotes[item.id];
|
||||
return newNotes;
|
||||
});
|
||||
|
||||
// Refresh if needed based on filter
|
||||
if ((activeStatusFilter === 'pending' && (action === 'approved' || action === 'rejected')) ||
|
||||
(activeStatusFilter === 'flagged' && (action === 'approved' || action === 'rejected'))) {
|
||||
// Item no longer matches filter
|
||||
}
|
||||
// Silent cleanup after delay
|
||||
setTimeout(() => {
|
||||
fetchItems(activeEntityFilter, activeStatusFilter, true);
|
||||
}, 2000);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error moderating content:', error);
|
||||
|
||||
// Revert any optimistic updates
|
||||
setItems(prev => prev.map(i =>
|
||||
i.id === item.id
|
||||
? { ...i, status: item.status } // Revert to original status
|
||||
: i
|
||||
));
|
||||
// Revert optimistic update - restore item to list
|
||||
setItems(prev => {
|
||||
const exists = prev.find(i => i.id === item.id);
|
||||
if (exists) {
|
||||
return prev.map(i => i.id === item.id ? item : i);
|
||||
} else {
|
||||
return [...prev, item];
|
||||
}
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "Error",
|
||||
@@ -1385,7 +1408,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
};
|
||||
|
||||
const QueueContent = () => {
|
||||
if (loading) {
|
||||
if (isInitialLoad && loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
|
||||
@@ -1890,11 +1913,8 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
<span className="text-sm">Claim this submission to lock it for 15 minutes while you review</span>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const success = await queue.claimSubmission(item.id);
|
||||
if (success) {
|
||||
// Refresh to update UI
|
||||
fetchItems(activeEntityFilter, activeStatusFilter, false);
|
||||
}
|
||||
await queue.claimSubmission(item.id);
|
||||
// No refresh needed - realtime handles it
|
||||
}}
|
||||
disabled={queue.isLoading}
|
||||
size="sm"
|
||||
@@ -2154,11 +2174,8 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
|
||||
// Handle claim next action
|
||||
const handleClaimNext = async () => {
|
||||
const claimedId = await queue.claimNext();
|
||||
if (claimedId) {
|
||||
// Scroll to claimed submission or fetch to show it
|
||||
fetchItems(activeEntityFilter, activeStatusFilter);
|
||||
}
|
||||
await queue.claimNext();
|
||||
// No refresh needed - realtime subscription handles updates
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -2403,7 +2420,10 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
open={reviewManagerOpen}
|
||||
onOpenChange={setReviewManagerOpen}
|
||||
onComplete={() => {
|
||||
fetchItems(activeEntityFilter, activeStatusFilter);
|
||||
// Silent cleanup after delay
|
||||
setTimeout(() => {
|
||||
fetchItems(activeEntityFilter, activeStatusFilter, true);
|
||||
}, 2000);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user