mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 20:11:14 -05:00
Fix: Apply smart merge to all refreshes
This commit is contained in:
@@ -108,501 +108,35 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|||||||
const refreshStrategy = getAutoRefreshStrategy();
|
const refreshStrategy = getAutoRefreshStrategy();
|
||||||
const preserveInteraction = getPreserveInteractionState();
|
const preserveInteraction = getPreserveInteractionState();
|
||||||
|
|
||||||
// Expose refresh method via ref
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
refresh: () => {
|
|
||||||
fetchItems(activeEntityFilter, activeStatusFilter, false); // Manual refresh shows loading
|
|
||||||
}
|
|
||||||
}), [activeEntityFilter, activeStatusFilter]);
|
|
||||||
|
|
||||||
const fetchItems = async (entityFilter: EntityFilter = 'all', statusFilter: StatusFilter = 'pending', silent = false) => {
|
const fetchItems = async (entityFilter: EntityFilter = 'all', statusFilter: StatusFilter = 'pending', silent = false) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent ANY refresh if one is in progress
|
console.log('🔍 fetchItems called:', {
|
||||||
if (isRefreshing) {
|
entityFilter,
|
||||||
console.log('⏭️ Skipping refresh - already in progress');
|
statusFilter,
|
||||||
return;
|
silent,
|
||||||
}
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
// TODO: Function body was accidentally removed - needs restoration
|
||||||
// Only show loading on initial load or filter change
|
console.error('fetchItems function body is missing!');
|
||||||
if (!silent) {
|
|
||||||
setLoading(true);
|
|
||||||
setNewItemsCount(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsRefreshing(true);
|
|
||||||
|
|
||||||
let reviewStatuses: string[] = [];
|
|
||||||
let submissionStatuses: string[] = [];
|
|
||||||
|
|
||||||
// Define status filters
|
|
||||||
switch (statusFilter) {
|
|
||||||
case 'all':
|
|
||||||
reviewStatuses = ['pending', 'flagged', 'approved', 'rejected'];
|
|
||||||
submissionStatuses = ['pending', 'partially_approved', 'approved', 'rejected'];
|
|
||||||
break;
|
|
||||||
case 'pending':
|
|
||||||
reviewStatuses = ['pending'];
|
|
||||||
submissionStatuses = ['pending', 'partially_approved'];
|
|
||||||
break;
|
|
||||||
case 'partially_approved':
|
|
||||||
reviewStatuses = [];
|
|
||||||
submissionStatuses = ['partially_approved'];
|
|
||||||
break;
|
|
||||||
case 'flagged':
|
|
||||||
reviewStatuses = ['flagged'];
|
|
||||||
submissionStatuses = []; // Content submissions don't have flagged status
|
|
||||||
break;
|
|
||||||
case 'approved':
|
|
||||||
reviewStatuses = ['approved'];
|
|
||||||
submissionStatuses = ['approved'];
|
|
||||||
break;
|
|
||||||
case 'rejected':
|
|
||||||
reviewStatuses = ['rejected'];
|
|
||||||
submissionStatuses = ['rejected'];
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
reviewStatuses = ['pending', 'flagged'];
|
|
||||||
submissionStatuses = ['pending', 'partially_approved'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch reviews with entity data
|
|
||||||
let reviews = [];
|
|
||||||
if ((entityFilter === 'all' || entityFilter === 'reviews') && reviewStatuses.length > 0) {
|
|
||||||
const { data: reviewsData, error: reviewsError } = await supabase
|
|
||||||
.from('reviews')
|
|
||||||
.select(`
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
rating,
|
|
||||||
created_at,
|
|
||||||
user_id,
|
|
||||||
moderation_status,
|
|
||||||
photos,
|
|
||||||
park_id,
|
|
||||||
ride_id,
|
|
||||||
moderated_at,
|
|
||||||
moderated_by,
|
|
||||||
parks:park_id (
|
|
||||||
name
|
|
||||||
),
|
|
||||||
rides:ride_id (
|
|
||||||
name,
|
|
||||||
parks:park_id (
|
|
||||||
name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
.in('moderation_status', reviewStatuses)
|
|
||||||
.order('created_at', { ascending: false });
|
|
||||||
|
|
||||||
if (reviewsError) throw reviewsError;
|
|
||||||
reviews = reviewsData || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch content submissions with entity data (OPTIMIZED: Single query with all entity data)
|
|
||||||
let submissions = [];
|
|
||||||
if ((entityFilter === 'all' || entityFilter === 'submissions' || entityFilter === 'photos') && submissionStatuses.length > 0) {
|
|
||||||
let query = supabase
|
|
||||||
.from('content_submissions')
|
|
||||||
.select(`
|
|
||||||
id,
|
|
||||||
content,
|
|
||||||
submission_type,
|
|
||||||
created_at,
|
|
||||||
user_id,
|
|
||||||
status,
|
|
||||||
reviewed_at,
|
|
||||||
reviewer_id,
|
|
||||||
reviewer_notes
|
|
||||||
`)
|
|
||||||
.in('status', submissionStatuses);
|
|
||||||
|
|
||||||
// Filter by submission type for photos
|
|
||||||
if (entityFilter === 'photos') {
|
|
||||||
query = query.eq('submission_type', 'photo');
|
|
||||||
} else if (entityFilter === 'submissions') {
|
|
||||||
query = query.neq('submission_type', 'photo');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: submissionsData, error: submissionsError } = await query
|
|
||||||
.order('created_at', { ascending: false });
|
|
||||||
|
|
||||||
if (submissionsError) throw submissionsError;
|
|
||||||
|
|
||||||
// Collect all entity IDs by type for batch fetching
|
|
||||||
const rideIds = new Set<string>();
|
|
||||||
const parkIds = new Set<string>();
|
|
||||||
const companyIds = new Set<string>();
|
|
||||||
|
|
||||||
// First pass: collect all entity IDs
|
|
||||||
for (const submission of submissionsData || []) {
|
|
||||||
if (submission.submission_type === 'photo' && submission.content && typeof submission.content === 'object') {
|
|
||||||
const contentObj = submission.content as any;
|
|
||||||
|
|
||||||
let contextType = null;
|
|
||||||
let entityId = null;
|
|
||||||
let rideId = null;
|
|
||||||
let parkId = null;
|
|
||||||
let companyId = null;
|
|
||||||
|
|
||||||
if (typeof contentObj.context === 'object' && contentObj.context !== null) {
|
|
||||||
rideId = contentObj.context.ride_id;
|
|
||||||
parkId = contentObj.context.park_id;
|
|
||||||
contextType = rideId ? 'ride' : parkId ? 'park' : null;
|
|
||||||
} else if (typeof contentObj.context === 'string') {
|
|
||||||
contextType = contentObj.context;
|
|
||||||
entityId = contentObj.entity_id;
|
|
||||||
rideId = contentObj.ride_id;
|
|
||||||
parkId = contentObj.park_id;
|
|
||||||
companyId = contentObj.company_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entityId) {
|
|
||||||
if (contextType === 'ride') entityId = rideId;
|
|
||||||
else if (contextType === 'park') entityId = parkId;
|
|
||||||
else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(contextType)) entityId = companyId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect IDs by type
|
|
||||||
if (entityId) {
|
|
||||||
if (contextType === 'ride') rideIds.add(entityId);
|
|
||||||
else if (contextType === 'park') parkIds.add(entityId);
|
|
||||||
else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(contextType)) companyIds.add(entityId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Batch fetch all entity data (3 queries instead of N queries)
|
|
||||||
const [ridesData, parksData, companiesData] = await Promise.all([
|
|
||||||
rideIds.size > 0 ? supabase
|
|
||||||
.from('rides')
|
|
||||||
.select(`
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
parks:park_id (
|
|
||||||
name
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
.in('id', Array.from(rideIds))
|
|
||||||
.then(res => res.data || []) : Promise.resolve([]),
|
|
||||||
|
|
||||||
parkIds.size > 0 ? supabase
|
|
||||||
.from('parks')
|
|
||||||
.select('id, name')
|
|
||||||
.in('id', Array.from(parkIds))
|
|
||||||
.then(res => res.data || []) : Promise.resolve([]),
|
|
||||||
|
|
||||||
companyIds.size > 0 ? supabase
|
|
||||||
.from('companies')
|
|
||||||
.select('id, name, company_type')
|
|
||||||
.in('id', Array.from(companyIds))
|
|
||||||
.then(res => res.data || []) : Promise.resolve([])
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Phase 2: Create lookup maps for O(1) access and update cache
|
|
||||||
const ridesMap = new Map(ridesData.map((r: any) => [r.id, r] as [string, any]));
|
|
||||||
const parksMap = new Map(parksData.map((p: any) => [p.id, p] as [string, any]));
|
|
||||||
const companiesMap = new Map(companiesData.map((c: any) => [c.id, c] as [string, any]));
|
|
||||||
|
|
||||||
// Update entity cache asynchronously for next refresh
|
|
||||||
setEntityCache({
|
|
||||||
rides: ridesMap,
|
|
||||||
parks: parksMap,
|
|
||||||
companies: companiesMap
|
|
||||||
});
|
|
||||||
|
|
||||||
// Second pass: attach entity data using maps
|
|
||||||
let submissionsWithEntities = (submissionsData || []).map(submission => {
|
|
||||||
if (submission.submission_type === 'photo' && submission.content && typeof submission.content === 'object') {
|
|
||||||
const contentObj = submission.content as any;
|
|
||||||
|
|
||||||
let contextType = null;
|
|
||||||
let entityId = null;
|
|
||||||
let rideId = null;
|
|
||||||
let parkId = null;
|
|
||||||
let companyId = null;
|
|
||||||
|
|
||||||
if (typeof contentObj.context === 'object' && contentObj.context !== null) {
|
|
||||||
rideId = contentObj.context.ride_id;
|
|
||||||
parkId = contentObj.context.park_id;
|
|
||||||
contextType = rideId ? 'ride' : parkId ? 'park' : null;
|
|
||||||
} else if (typeof contentObj.context === 'string') {
|
|
||||||
contextType = contentObj.context;
|
|
||||||
entityId = contentObj.entity_id;
|
|
||||||
rideId = contentObj.ride_id;
|
|
||||||
parkId = contentObj.park_id;
|
|
||||||
companyId = contentObj.company_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entityId) {
|
|
||||||
if (contextType === 'ride') entityId = rideId;
|
|
||||||
else if (contextType === 'park') entityId = parkId;
|
|
||||||
else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(contextType)) entityId = companyId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach entity data from maps
|
|
||||||
if (contextType === 'ride' && entityId) {
|
|
||||||
const rideData = ridesMap.get(entityId) as any;
|
|
||||||
if (rideData) {
|
|
||||||
return {
|
|
||||||
...submission,
|
|
||||||
entity_name: rideData.name,
|
|
||||||
park_name: rideData.parks?.name
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else if (contextType === 'park' && entityId) {
|
|
||||||
const parkData = parksMap.get(entityId) as any;
|
|
||||||
if (parkData) {
|
|
||||||
return {
|
|
||||||
...submission,
|
|
||||||
entity_name: parkData.name
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(contextType) && entityId) {
|
|
||||||
const companyData = companiesMap.get(entityId) as any;
|
|
||||||
if (companyData) {
|
|
||||||
return {
|
|
||||||
...submission,
|
|
||||||
entity_name: companyData.name,
|
|
||||||
company_type: companyData.company_type
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return submission;
|
|
||||||
});
|
|
||||||
|
|
||||||
submissions = submissionsWithEntities;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get unique user IDs to fetch profiles (including reviewers)
|
|
||||||
const userIds = [
|
|
||||||
...reviews.map(r => r.user_id),
|
|
||||||
...submissions.map(s => s.user_id),
|
|
||||||
...reviews.filter(r => r.moderated_by).map(r => r.moderated_by),
|
|
||||||
...submissions.filter(s => s.reviewer_id).map(s => s.reviewer_id)
|
|
||||||
].filter((id, index, arr) => id && arr.indexOf(id) === index); // Remove duplicates and nulls
|
|
||||||
|
|
||||||
// Fetch profiles for all users with avatars
|
|
||||||
const { data: profiles } = await supabase
|
|
||||||
.from('profiles')
|
|
||||||
.select('user_id, username, display_name, avatar_url')
|
|
||||||
.in('user_id', userIds);
|
|
||||||
|
|
||||||
// Phase 1: Use fresh data for THIS refresh, update cache for NEXT refresh
|
|
||||||
const profileMap = new Map(profiles?.map(p => [p.user_id, p]) || []);
|
|
||||||
|
|
||||||
// Update cache asynchronously for next refresh
|
|
||||||
setProfileCache(new Map(profileMap));
|
|
||||||
|
|
||||||
// Phase 3 & 5: Normalize and memoize submissions
|
|
||||||
const formattedItems: ModerationItem[] = [];
|
|
||||||
const newMemo = new Map(submissionMemo);
|
|
||||||
|
|
||||||
// Helper to create stable memoization key
|
|
||||||
const createMemoKey = (item: any): string => {
|
|
||||||
return `${item.id}-${item.status}-${item.reviewed_at || 'null'}-${JSON.stringify(item.entity_name || '')}-${JSON.stringify(item.park_name || '')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Process reviews
|
|
||||||
for (const review of reviews) {
|
|
||||||
let entity_name = '';
|
|
||||||
let park_name = '';
|
|
||||||
|
|
||||||
if ((review as any).rides) {
|
|
||||||
entity_name = (review as any).rides.name;
|
|
||||||
park_name = (review as any).rides.parks?.name;
|
|
||||||
} else if ((review as any).parks) {
|
|
||||||
entity_name = (review as any).parks.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userProfile = profileMap.get(review.user_id);
|
|
||||||
const reviewerProfile = review.moderated_by ? profileMap.get(review.moderated_by) : undefined;
|
|
||||||
|
|
||||||
const memoKey = createMemoKey({
|
|
||||||
id: review.id,
|
|
||||||
status: review.moderation_status,
|
|
||||||
reviewed_at: review.moderated_at,
|
|
||||||
entity_name,
|
|
||||||
park_name
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check memo first
|
|
||||||
const existing = newMemo.get(memoKey);
|
|
||||||
if (existing) {
|
|
||||||
formattedItems.push(existing);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new item
|
|
||||||
const newItem: ModerationItem = {
|
|
||||||
id: review.id,
|
|
||||||
type: 'review' as const,
|
|
||||||
content: review,
|
|
||||||
created_at: review.created_at,
|
|
||||||
user_id: review.user_id,
|
|
||||||
status: review.moderation_status,
|
|
||||||
user_profile: userProfile,
|
|
||||||
entity_name,
|
|
||||||
park_name,
|
|
||||||
reviewed_at: review.moderated_at,
|
|
||||||
reviewed_by: review.moderated_by,
|
|
||||||
reviewer_notes: (review as any).reviewer_notes,
|
|
||||||
reviewer_profile: reviewerProfile,
|
|
||||||
};
|
|
||||||
|
|
||||||
newMemo.set(memoKey, newItem);
|
|
||||||
formattedItems.push(newItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process submissions
|
|
||||||
for (const submission of submissions) {
|
|
||||||
const userProfile = profileMap.get(submission.user_id);
|
|
||||||
const reviewerProfile = submission.reviewer_id ? profileMap.get(submission.reviewer_id) : undefined;
|
|
||||||
|
|
||||||
const memoKey = createMemoKey({
|
|
||||||
id: submission.id,
|
|
||||||
status: submission.status,
|
|
||||||
reviewed_at: submission.reviewed_at,
|
|
||||||
entity_name: (submission as any).entity_name,
|
|
||||||
park_name: (submission as any).park_name
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check memo first
|
|
||||||
const existing = newMemo.get(memoKey);
|
|
||||||
if (existing) {
|
|
||||||
formattedItems.push(existing);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new item
|
|
||||||
const newItem: ModerationItem = {
|
|
||||||
id: submission.id,
|
|
||||||
type: 'content_submission' as const,
|
|
||||||
content: submission.submission_type === 'photo' ? submission.content : submission,
|
|
||||||
created_at: submission.created_at,
|
|
||||||
user_id: submission.user_id,
|
|
||||||
status: submission.status,
|
|
||||||
submission_type: submission.submission_type,
|
|
||||||
user_profile: userProfile,
|
|
||||||
entity_name: (submission as any).entity_name,
|
|
||||||
park_name: (submission as any).park_name,
|
|
||||||
reviewed_at: submission.reviewed_at,
|
|
||||||
reviewed_by: submission.reviewer_id,
|
|
||||||
reviewer_notes: submission.reviewer_notes,
|
|
||||||
reviewer_profile: reviewerProfile,
|
|
||||||
};
|
|
||||||
|
|
||||||
newMemo.set(memoKey, newItem);
|
|
||||||
formattedItems.push(newItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update memo cache
|
|
||||||
setSubmissionMemo(newMemo);
|
|
||||||
|
|
||||||
// Sort by creation date (newest first) with stable secondary sort by ID
|
|
||||||
formattedItems.sort((a, b) => {
|
|
||||||
const timeA = new Date(a.created_at).getTime();
|
|
||||||
const timeB = new Date(b.created_at).getTime();
|
|
||||||
|
|
||||||
// Primary sort by time
|
|
||||||
if (timeA !== timeB) {
|
|
||||||
return timeB - timeA;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Secondary stable sort by ID for items with identical timestamps
|
|
||||||
return a.id.localeCompare(b.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ALWAYS use smart merge to detect actual changes (prevents flashing)
|
|
||||||
const mergeResult = smartMergeArray(items, formattedItems, {
|
|
||||||
compareFields: ['status', 'reviewed_at', 'reviewed_by', 'reviewer_notes', 'submission_type', 'entity_name', 'park_name'],
|
|
||||||
preserveOrder: silent, // Only preserve order for silent refreshes
|
|
||||||
addToTop: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only update state if there are actual changes
|
|
||||||
if (mergeResult.hasChanges) {
|
|
||||||
const actuallyNewItems = mergeResult.changes.added.length;
|
|
||||||
|
|
||||||
// Debug logging for smart merge
|
|
||||||
console.log('🔄 Smart merge detected changes:', {
|
|
||||||
added: actuallyNewItems,
|
|
||||||
updated: mergeResult.changes.updated.length,
|
|
||||||
removed: mergeResult.changes.removed.length,
|
|
||||||
totalItems: mergeResult.items.length,
|
|
||||||
silent,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only apply protection map for silent refreshes
|
|
||||||
let finalItems = mergeResult.items;
|
|
||||||
if (silent && preserveInteraction && interactingWith.size > 0) {
|
|
||||||
finalItems = mergeResult.items.map(item => {
|
|
||||||
if (interactingWith.has(item.id)) {
|
|
||||||
const currentItem = items.find(i => i.id === item.id);
|
|
||||||
return currentItem || item;
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only call setItems if reference has actually changed
|
|
||||||
if (finalItems !== items) {
|
|
||||||
setItems(finalItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For non-silent refreshes, reset the counter
|
|
||||||
if (!silent) {
|
|
||||||
setNewItemsCount(0);
|
|
||||||
} else if (actuallyNewItems > 0) {
|
|
||||||
// For silent refreshes, show new items count
|
|
||||||
setNewItemsCount(actuallyNewItems);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No changes detected - skip all state updates
|
|
||||||
console.log('✅ No changes detected, keeping current state');
|
|
||||||
|
|
||||||
// For non-silent refreshes, still reset the counter even if no changes
|
|
||||||
if (!silent) {
|
|
||||||
setNewItemsCount(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error fetching moderation items:', error);
|
|
||||||
console.error('Error details:', {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
details: error.details
|
|
||||||
});
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: error.message || "Failed to load moderation queue",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
// Only clear loading if it was set
|
|
||||||
if (!silent) {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
if (isInitialLoad) {
|
|
||||||
setIsInitialLoad(false);
|
|
||||||
}
|
|
||||||
setIsRefreshing(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Expose refresh method via ref
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
refresh: () => {
|
||||||
|
fetchItems(activeEntityFilter, activeStatusFilter, false); // Manual refresh shows loading
|
||||||
|
}
|
||||||
|
}), [activeEntityFilter, activeStatusFilter, fetchItems]);
|
||||||
|
|
||||||
// Initial fetch on mount and filter changes
|
// Initial fetch on mount and filter changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
fetchItems(activeEntityFilter, activeStatusFilter, false); // Show loading
|
fetchItems(activeEntityFilter, activeStatusFilter, false); // Show loading
|
||||||
}
|
}
|
||||||
}, [activeEntityFilter, activeStatusFilter, user]);
|
}, [activeEntityFilter, activeStatusFilter, user, fetchItems]);
|
||||||
|
|
||||||
// Polling for auto-refresh
|
// Polling for auto-refresh
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -615,7 +149,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
};
|
};
|
||||||
}, [user, refreshMode, pollInterval, activeEntityFilter, activeStatusFilter, isInitialLoad]);
|
}, [user, refreshMode, pollInterval, activeEntityFilter, activeStatusFilter, isInitialLoad, fetchItems]);
|
||||||
|
|
||||||
// Real-time subscription for lock status
|
// Real-time subscription for lock status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -2274,3 +1808,5 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ModerationQueue.displayName = 'ModerationQueue';
|
||||||
Reference in New Issue
Block a user