mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:51:14 -05:00
Fix: Address moderation queue reload issues
This commit is contained in:
@@ -190,11 +190,16 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|||||||
localStorage.setItem('moderationQueue_sortConfig', JSON.stringify(sortConfig));
|
localStorage.setItem('moderationQueue_sortConfig', JSON.stringify(sortConfig));
|
||||||
}, [sortConfig]);
|
}, [sortConfig]);
|
||||||
|
|
||||||
// Only sync itemsRef (not loadedIdsRef) to avoid breaking silent polling logic
|
// Sync itemsRef with items state (after React commits)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
itemsRef.current = items;
|
itemsRef.current = items;
|
||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
|
// Sync loadedIdsRef with items state (after React commits)
|
||||||
|
useEffect(() => {
|
||||||
|
loadedIdsRef.current = new Set(items.map(item => item.id));
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
// Enable transitions after initial render
|
// Enable transitions after initial render
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loadingState === 'ready' && items.length > 0 && !hasRenderedOnce) {
|
if (loadingState === 'ready' && items.length > 0 && !hasRenderedOnce) {
|
||||||
@@ -572,11 +577,8 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|||||||
const existingIds = new Set(prev.map(p => p.id));
|
const existingIds = new Set(prev.map(p => p.id));
|
||||||
const uniqueNew = newSubmissions.filter(item => !existingIds.has(item.id));
|
const uniqueNew = newSubmissions.filter(item => !existingIds.has(item.id));
|
||||||
|
|
||||||
// Track these IDs as loaded to prevent re-counting on next poll
|
// Track count increment (loadedIdsRef will sync automatically via useEffect)
|
||||||
if (uniqueNew.length > 0) {
|
if (uniqueNew.length > 0) {
|
||||||
const newIds = uniqueNew.map(item => item.id);
|
|
||||||
const currentLoadedIds = loadedIdsRef.current;
|
|
||||||
loadedIdsRef.current = new Set([...currentLoadedIds, ...newIds]);
|
|
||||||
setNewItemsCount(prev => prev + uniqueNew.length);
|
setNewItemsCount(prev => prev + uniqueNew.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,8 +619,6 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (mergeResult.hasChanges) {
|
if (mergeResult.hasChanges) {
|
||||||
// Update ref BEFORE setState to prevent race conditions
|
|
||||||
itemsRef.current = mergeResult.items;
|
|
||||||
setItems(mergeResult.items);
|
setItems(mergeResult.items);
|
||||||
console.log('🔄 Queue updated (replace mode):', {
|
console.log('🔄 Queue updated (replace mode):', {
|
||||||
added: mergeResult.changes.added.length,
|
added: mergeResult.changes.added.length,
|
||||||
@@ -660,8 +660,6 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (mergeResult.hasChanges) {
|
if (mergeResult.hasChanges) {
|
||||||
// Update ref BEFORE setState to prevent race conditions
|
|
||||||
itemsRef.current = mergeResult.items;
|
|
||||||
setItems(mergeResult.items);
|
setItems(mergeResult.items);
|
||||||
console.log('🔄 Queue updated (manual refresh):', {
|
console.log('🔄 Queue updated (manual refresh):', {
|
||||||
added: mergeResult.changes.added.length,
|
added: mergeResult.changes.added.length,
|
||||||
@@ -1061,6 +1059,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|||||||
escalated: submission.escalated,
|
escalated: submission.escalated,
|
||||||
assigned_to: submission.assigned_to || undefined,
|
assigned_to: submission.assigned_to || undefined,
|
||||||
locked_until: submission.locked_until || undefined,
|
locked_until: submission.locked_until || undefined,
|
||||||
|
submission_items: submission.submission_items || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update or add to queue
|
// Update or add to queue
|
||||||
@@ -1079,17 +1078,55 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|||||||
currentItem.reviewer_notes !== fullItem.reviewer_notes ||
|
currentItem.reviewer_notes !== fullItem.reviewer_notes ||
|
||||||
currentItem.assigned_to !== fullItem.assigned_to ||
|
currentItem.assigned_to !== fullItem.assigned_to ||
|
||||||
currentItem.locked_until !== fullItem.locked_until ||
|
currentItem.locked_until !== fullItem.locked_until ||
|
||||||
currentItem.escalated !== fullItem.escalated ||
|
currentItem.escalated !== fullItem.escalated;
|
||||||
JSON.stringify(currentItem.content) !== JSON.stringify(fullItem.content);
|
|
||||||
|
|
||||||
if (!hasChanged) {
|
// Only check content if critical fields match (performance optimization)
|
||||||
|
let contentChanged = false;
|
||||||
|
if (!hasChanged && currentItem.content && fullItem.content) {
|
||||||
|
// Compare content reference first
|
||||||
|
if (currentItem.content !== fullItem.content) {
|
||||||
|
// Check each key for actual value changes (one level deep)
|
||||||
|
const currentKeys = Object.keys(currentItem.content).sort();
|
||||||
|
const fullKeys = Object.keys(fullItem.content).sort();
|
||||||
|
|
||||||
|
if (currentKeys.length !== fullKeys.length ||
|
||||||
|
!currentKeys.every((key, i) => key === fullKeys[i])) {
|
||||||
|
contentChanged = true;
|
||||||
|
} else {
|
||||||
|
for (const key of currentKeys) {
|
||||||
|
if (currentItem.content[key] !== fullItem.content[key]) {
|
||||||
|
contentChanged = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasChanged && !contentChanged) {
|
||||||
console.log('✅ Realtime UPDATE: No changes detected for', fullItem.id);
|
console.log('✅ Realtime UPDATE: No changes detected for', fullItem.id);
|
||||||
return prev; // Keep existing array reference - PREVENTS RE-RENDER
|
return prev; // Keep existing array reference - PREVENTS RE-RENDER
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🔄 Realtime UPDATE: Changes detected for', fullItem.id);
|
console.log('🔄 Realtime UPDATE: Changes detected for', fullItem.id);
|
||||||
// Shallow merge to preserve stable references
|
// Update ONLY changed fields to preserve object stability
|
||||||
return prev.map(i => i.id === fullItem.id ? { ...i, ...fullItem } : i);
|
return prev.map(i => {
|
||||||
|
if (i.id !== fullItem.id) return i;
|
||||||
|
|
||||||
|
// Create minimal update object with only changed fields
|
||||||
|
const updates: Partial<ModerationItem> = {};
|
||||||
|
if (i.status !== fullItem.status) updates.status = fullItem.status;
|
||||||
|
if (i.reviewed_at !== fullItem.reviewed_at) updates.reviewed_at = fullItem.reviewed_at;
|
||||||
|
if (i.reviewer_notes !== fullItem.reviewer_notes) updates.reviewer_notes = fullItem.reviewer_notes;
|
||||||
|
if (i.assigned_to !== fullItem.assigned_to) updates.assigned_to = fullItem.assigned_to;
|
||||||
|
if (i.locked_until !== fullItem.locked_until) updates.locked_until = fullItem.locked_until;
|
||||||
|
if (i.escalated !== fullItem.escalated) updates.escalated = fullItem.escalated;
|
||||||
|
if (contentChanged) updates.content = fullItem.content;
|
||||||
|
if (fullItem.submission_items) updates.submission_items = fullItem.submission_items;
|
||||||
|
|
||||||
|
// Only create new object if there are actual updates
|
||||||
|
return Object.keys(updates).length > 0 ? { ...i, ...updates } : i;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('🆕 Realtime UPDATE: Adding new item', fullItem.id);
|
console.log('🆕 Realtime UPDATE: Adding new item', fullItem.id);
|
||||||
return [fullItem, ...prev];
|
return [fullItem, ...prev];
|
||||||
@@ -2226,23 +2263,13 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Smooth merge with loading state
|
// Instant merge without loading state
|
||||||
if (pendingNewItems.length > 0) {
|
if (pendingNewItems.length > 0) {
|
||||||
setLoadingState('loading');
|
|
||||||
|
|
||||||
// After 150ms, merge items
|
|
||||||
setTimeout(() => {
|
|
||||||
setItems(prev => [...pendingNewItems, ...prev]);
|
setItems(prev => [...pendingNewItems, ...prev]);
|
||||||
setPendingNewItems([]);
|
setPendingNewItems([]);
|
||||||
setNewItemsCount(0);
|
setNewItemsCount(0);
|
||||||
|
console.log('✅ New items merged into queue:', pendingNewItems.length);
|
||||||
// Show content again after brief pause
|
|
||||||
setTimeout(() => {
|
|
||||||
setLoadingState('ready');
|
|
||||||
}, 100);
|
|
||||||
}, 150);
|
|
||||||
}
|
}
|
||||||
console.log('✅ New items merged into queue');
|
|
||||||
}}
|
}}
|
||||||
className="ml-4"
|
className="ml-4"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -668,15 +668,16 @@ export const QueueItem = memo(({
|
|||||||
const nextLocked = nextProps.lockedSubmissions.has(nextProps.item.id);
|
const nextLocked = nextProps.lockedSubmissions.has(nextProps.item.id);
|
||||||
if (prevLocked !== nextLocked) return false;
|
if (prevLocked !== nextLocked) return false;
|
||||||
|
|
||||||
// Deep comparison of content and other fields that affect rendering
|
// Deep comparison of critical fields (use strict equality for reference stability)
|
||||||
|
if (prevProps.item.status !== nextProps.item.status) return false;
|
||||||
if (prevProps.item.reviewed_at !== nextProps.item.reviewed_at) return false;
|
if (prevProps.item.reviewed_at !== nextProps.item.reviewed_at) return false;
|
||||||
if (prevProps.item.reviewer_notes !== nextProps.item.reviewer_notes) return false;
|
if (prevProps.item.reviewer_notes !== nextProps.item.reviewer_notes) return false;
|
||||||
if (prevProps.item.assigned_to !== nextProps.item.assigned_to) return false;
|
if (prevProps.item.assigned_to !== nextProps.item.assigned_to) return false;
|
||||||
if (prevProps.item.locked_until !== nextProps.item.locked_until) return false;
|
if (prevProps.item.locked_until !== nextProps.item.locked_until) return false;
|
||||||
if (prevProps.item.escalated !== nextProps.item.escalated) return false;
|
if (prevProps.item.escalated !== nextProps.item.escalated) return false;
|
||||||
|
|
||||||
// Content comparison (most expensive, do last)
|
// Only check content reference, not deep equality (performance)
|
||||||
if (JSON.stringify(prevProps.item.content) !== JSON.stringify(nextProps.item.content)) return false;
|
if (prevProps.item.content !== nextProps.item.content) return false;
|
||||||
|
|
||||||
// All checks passed - items are identical
|
// All checks passed - items are identical
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
Reference in New Issue
Block a user