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