mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 15:31:12 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
733
src-old/components/moderation/ModerationQueue.tsx
Normal file
733
src-old/components/moderation/ModerationQueue.tsx
Normal file
@@ -0,0 +1,733 @@
|
||||
import { useState, useImperativeHandle, forwardRef, useMemo, useCallback, useRef, useEffect } from 'react';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { AlertCircle, Info } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useUserRole } from '@/hooks/useUserRole';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import * as localStorage from '@/lib/localStorage';
|
||||
import { PhotoModal } from './PhotoModal';
|
||||
import { SubmissionReviewManager } from './SubmissionReviewManager';
|
||||
import { ItemEditDialog } from './ItemEditDialog';
|
||||
import { ItemSelectorDialog } from './ItemSelectorDialog';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { useAdminSettings } from '@/hooks/useAdminSettings';
|
||||
import { useModerationQueueManager } from '@/hooks/moderation';
|
||||
import { QueueItem } from './QueueItem';
|
||||
import { ModerationErrorBoundary } from '@/components/error/ModerationErrorBoundary';
|
||||
import { QueueSkeleton } from './QueueSkeleton';
|
||||
import { EnhancedLockStatusDisplay } from './EnhancedLockStatusDisplay';
|
||||
import { getLockStatus } from '@/lib/moderation/lockHelpers';
|
||||
import { QueueStats } from './QueueStats';
|
||||
import { QueueFilters } from './QueueFilters';
|
||||
import { ActiveFiltersDisplay } from './ActiveFiltersDisplay';
|
||||
import { AutoRefreshIndicator } from './AutoRefreshIndicator';
|
||||
import { NewItemsAlert } from './NewItemsAlert';
|
||||
import { EnhancedEmptyState } from './EnhancedEmptyState';
|
||||
import { QueuePagination } from './QueuePagination';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { KeyboardShortcutsHelp } from './KeyboardShortcutsHelp';
|
||||
import { SuperuserQueueControls } from './SuperuserQueueControls';
|
||||
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
|
||||
import { fetchSubmissionItems, type SubmissionItemWithDeps } from '@/lib/submissionItemsService';
|
||||
import type { ModerationQueueRef, ModerationItem } from '@/types/moderation';
|
||||
import type { PhotoItem } from '@/types/photos';
|
||||
|
||||
interface ModerationQueueProps {
|
||||
optimisticallyUpdateStats?: (delta: Partial<{ pendingSubmissions: number; openReports: number; flaggedContent: number }>) => void;
|
||||
}
|
||||
|
||||
export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueueProps>((props, ref) => {
|
||||
const { optimisticallyUpdateStats } = props;
|
||||
const isMobile = useIsMobile();
|
||||
const { user } = useAuth();
|
||||
const { toast } = useToast();
|
||||
const { isAdmin, isSuperuser } = useUserRole();
|
||||
const adminSettings = useAdminSettings();
|
||||
|
||||
// Extract settings values to stable primitives for memoization
|
||||
const refreshMode = adminSettings.getAdminPanelRefreshMode();
|
||||
const pollInterval = adminSettings.getAdminPanelPollInterval();
|
||||
const refreshStrategy = adminSettings.getAutoRefreshStrategy();
|
||||
const preserveInteraction = adminSettings.getPreserveInteractionState();
|
||||
const useRealtimeQueue = adminSettings.getUseRealtimeQueue();
|
||||
|
||||
// Memoize settings object using stable primitive dependencies
|
||||
const settings = useMemo(() => ({
|
||||
refreshMode,
|
||||
pollInterval,
|
||||
refreshStrategy,
|
||||
preserveInteraction,
|
||||
useRealtimeQueue,
|
||||
}), [refreshMode, pollInterval, refreshStrategy, preserveInteraction, useRealtimeQueue]);
|
||||
|
||||
// Initialize queue manager (replaces all state management, fetchItems, effects)
|
||||
const queueManager = useModerationQueueManager({
|
||||
user,
|
||||
isAdmin: isAdmin(),
|
||||
isSuperuser: isSuperuser(),
|
||||
toast,
|
||||
optimisticallyUpdateStats,
|
||||
settings,
|
||||
});
|
||||
|
||||
// UI-only state
|
||||
const [notes, setNotes] = useState<Record<string, string>>({});
|
||||
const [transactionStatuses, setTransactionStatuses] = useState<Record<string, { status: 'idle' | 'processing' | 'timeout' | 'cached' | 'completed' | 'failed'; message?: string }>>(() => {
|
||||
// Restore from localStorage on mount
|
||||
return localStorage.getJSON('moderation-queue-transaction-statuses', {});
|
||||
});
|
||||
const [photoModalOpen, setPhotoModalOpen] = useState(false);
|
||||
const [selectedPhotos, setSelectedPhotos] = useState<PhotoItem[]>([]);
|
||||
const [selectedPhotoIndex, setSelectedPhotoIndex] = useState(0);
|
||||
const [reviewManagerOpen, setReviewManagerOpen] = useState(false);
|
||||
const [selectedSubmissionId, setSelectedSubmissionId] = useState<string | null>(null);
|
||||
const [showItemEditDialog, setShowItemEditDialog] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState<SubmissionItemWithDeps | null>(null);
|
||||
const [showItemSelector, setShowItemSelector] = useState(false);
|
||||
const [availableItems, setAvailableItems] = useState<SubmissionItemWithDeps[]>([]);
|
||||
const [bulkEditMode, setBulkEditMode] = useState(false);
|
||||
const [bulkEditItems, setBulkEditItems] = useState<SubmissionItemWithDeps[]>([]);
|
||||
const [activeLocksCount, setActiveLocksCount] = useState(0);
|
||||
const [lockRestored, setLockRestored] = useState(false);
|
||||
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
|
||||
|
||||
// Confirmation dialog state
|
||||
const [confirmDialog, setConfirmDialog] = useState<{
|
||||
open: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
onConfirm: () => void;
|
||||
}>({
|
||||
open: false,
|
||||
title: '',
|
||||
description: '',
|
||||
onConfirm: () => {},
|
||||
});
|
||||
|
||||
// Keyboard shortcuts help dialog
|
||||
const [showShortcutsHelp, setShowShortcutsHelp] = useState(false);
|
||||
|
||||
// Offline detection state
|
||||
const [isOffline, setIsOffline] = useState(!navigator.onLine);
|
||||
|
||||
// Persist transaction statuses to localStorage
|
||||
useEffect(() => {
|
||||
localStorage.setJSON('moderation-queue-transaction-statuses', transactionStatuses);
|
||||
}, [transactionStatuses]);
|
||||
|
||||
// Offline detection effect
|
||||
useEffect(() => {
|
||||
const handleOnline = () => {
|
||||
setIsOffline(false);
|
||||
toast({
|
||||
title: 'Connection Restored',
|
||||
description: 'You are back online. Refreshing queue...',
|
||||
});
|
||||
queueManager.refresh();
|
||||
};
|
||||
|
||||
const handleOffline = () => {
|
||||
setIsOffline(true);
|
||||
};
|
||||
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
};
|
||||
}, [queueManager, toast]);
|
||||
|
||||
// Auto-dismiss lock restored banner after 10 seconds
|
||||
useEffect(() => {
|
||||
if (lockRestored && queueManager.queue.currentLock) {
|
||||
const timer = setTimeout(() => {
|
||||
setLockRestored(false);
|
||||
}, 10000); // Auto-dismiss after 10 seconds
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [lockRestored, queueManager.queue.currentLock]);
|
||||
|
||||
// Fetch active locks count for superusers
|
||||
const isSuperuserValue = isSuperuser();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSuperuserValue) return;
|
||||
|
||||
const fetchActiveLocksCount = async () => {
|
||||
const { count } = await supabase
|
||||
.from('content_submissions')
|
||||
.select('id', { count: 'exact', head: true })
|
||||
.not('assigned_to', 'is', null)
|
||||
.gt('locked_until', new Date().toISOString());
|
||||
|
||||
setActiveLocksCount(count || 0);
|
||||
};
|
||||
|
||||
fetchActiveLocksCount();
|
||||
|
||||
// Refresh count periodically
|
||||
const interval = setInterval(fetchActiveLocksCount, 30000); // Every 30s
|
||||
return () => clearInterval(interval);
|
||||
}, [isSuperuserValue]);
|
||||
|
||||
// Track if lock was restored from database
|
||||
useEffect(() => {
|
||||
if (!initialLoadComplete) {
|
||||
setInitialLoadComplete(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (queueManager.queue.currentLock && !lockRestored) {
|
||||
// If we have a lock after initial load but haven't claimed in this session
|
||||
setLockRestored(true);
|
||||
}
|
||||
}, [queueManager.queue.currentLock, lockRestored, initialLoadComplete]);
|
||||
|
||||
// Virtual scrolling setup
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const virtualizer = useVirtualizer({
|
||||
count: queueManager.items.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 420, // Estimated average height of QueueItem (card + spacing)
|
||||
overscan: 3, // Render 3 items above/below viewport for smoother scrolling
|
||||
enabled: queueManager.items.length > 10, // Only enable virtual scrolling for 10+ items
|
||||
});
|
||||
|
||||
// UI-specific handlers
|
||||
const handleNoteChange = (id: string, value: string) => {
|
||||
setNotes(prev => ({ ...prev, [id]: value }));
|
||||
};
|
||||
|
||||
// Transaction status helpers
|
||||
const setTransactionStatus = useCallback((submissionId: string, status: 'idle' | 'processing' | 'timeout' | 'cached' | 'completed' | 'failed', message?: string) => {
|
||||
setTransactionStatuses(prev => ({
|
||||
...prev,
|
||||
[submissionId]: { status, message }
|
||||
}));
|
||||
|
||||
// Auto-clear completed/failed statuses after 5 seconds
|
||||
if (status === 'completed' || status === 'failed') {
|
||||
setTimeout(() => {
|
||||
setTransactionStatuses(prev => {
|
||||
const updated = { ...prev };
|
||||
if (updated[submissionId]?.status === status) {
|
||||
updated[submissionId] = { status: 'idle' };
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Wrap performAction to track transaction status
|
||||
const handlePerformAction = useCallback(async (item: ModerationItem, action: 'approved' | 'rejected', notes?: string) => {
|
||||
setTransactionStatus(item.id, 'processing');
|
||||
try {
|
||||
await queueManager.performAction(item, action, notes);
|
||||
setTransactionStatus(item.id, 'completed');
|
||||
} catch (error: any) {
|
||||
// Check for timeout
|
||||
if (error?.type === 'timeout' || error?.message?.toLowerCase().includes('timeout')) {
|
||||
setTransactionStatus(item.id, 'timeout', error.message);
|
||||
}
|
||||
// Check for cached/409
|
||||
else if (error?.status === 409 || error?.message?.toLowerCase().includes('duplicate')) {
|
||||
setTransactionStatus(item.id, 'cached', 'Using cached result from duplicate request');
|
||||
}
|
||||
// Generic failure
|
||||
else {
|
||||
setTransactionStatus(item.id, 'failed', error.message);
|
||||
}
|
||||
throw error; // Re-throw to allow normal error handling
|
||||
}
|
||||
}, [queueManager, setTransactionStatus]);
|
||||
|
||||
// Wrapped delete with confirmation
|
||||
const handleDeleteSubmission = useCallback((item: ModerationItem) => {
|
||||
setConfirmDialog({
|
||||
open: true,
|
||||
title: 'Delete Submission',
|
||||
description: 'Are you sure you want to permanently delete this submission? This action cannot be undone.',
|
||||
onConfirm: () => queueManager.deleteSubmission(item),
|
||||
});
|
||||
}, [queueManager]);
|
||||
|
||||
// Superuser force release lock
|
||||
const handleSuperuserReleaseLock = useCallback(async (submissionId: string) => {
|
||||
await queueManager.queue.superuserReleaseLock(submissionId);
|
||||
// Refresh locks count and queue
|
||||
setActiveLocksCount(prev => Math.max(0, prev - 1));
|
||||
queueManager.refresh();
|
||||
}, [queueManager]);
|
||||
|
||||
// Superuser clear all locks
|
||||
const handleClearAllLocks = useCallback(async () => {
|
||||
const count = await queueManager.queue.superuserReleaseAllLocks();
|
||||
setActiveLocksCount(0);
|
||||
// Force queue refresh
|
||||
queueManager.refresh();
|
||||
}, [queueManager]);
|
||||
|
||||
// Clear filters handler
|
||||
const handleClearFilters = useCallback(() => {
|
||||
queueManager.filters.clearFilters();
|
||||
}, [queueManager.filters]);
|
||||
|
||||
// Keyboard shortcuts
|
||||
const { shortcuts } = useKeyboardShortcuts({
|
||||
shortcuts: [
|
||||
{
|
||||
key: '?',
|
||||
handler: () => setShowShortcutsHelp(true),
|
||||
description: 'Show keyboard shortcuts',
|
||||
},
|
||||
{
|
||||
key: 'r',
|
||||
handler: () => queueManager.refresh(),
|
||||
description: 'Refresh queue',
|
||||
},
|
||||
{
|
||||
key: 'k',
|
||||
ctrlOrCmd: true,
|
||||
handler: () => {
|
||||
// Focus search/filter (if implemented)
|
||||
document.querySelector<HTMLInputElement>('[data-filter-search]')?.focus();
|
||||
},
|
||||
description: 'Focus filters',
|
||||
},
|
||||
{
|
||||
key: 'e',
|
||||
handler: () => {
|
||||
// Edit first claimed submission
|
||||
const claimedItem = queueManager.items.find(item =>
|
||||
queueManager.queue.isLockedByMe(item.id, item.assigned_to, item.locked_until)
|
||||
);
|
||||
if (claimedItem) {
|
||||
handleOpenItemEditor(claimedItem.id);
|
||||
}
|
||||
},
|
||||
description: 'Edit claimed submission',
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const handleOpenPhotos = (photos: PhotoItem[], index: number) => {
|
||||
setSelectedPhotos(photos);
|
||||
setSelectedPhotoIndex(index);
|
||||
setPhotoModalOpen(true);
|
||||
};
|
||||
|
||||
const handleOpenReviewManager = (submissionId: string) => {
|
||||
setSelectedSubmissionId(submissionId);
|
||||
setReviewManagerOpen(true);
|
||||
};
|
||||
|
||||
const handleOpenItemEditor = async (submissionId: string) => {
|
||||
try {
|
||||
const items = await fetchSubmissionItems(submissionId);
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
toast({
|
||||
title: 'No Items Found',
|
||||
description: 'This submission has no items to edit',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Phase 3: Multi-item selector for submissions with multiple items
|
||||
if (items.length > 1) {
|
||||
setAvailableItems(items);
|
||||
setShowItemSelector(true);
|
||||
} else {
|
||||
// Single item - edit directly
|
||||
setEditingItem(items[0]);
|
||||
setShowItemEditDialog(true);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: getErrorMessage(error),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectItem = (item: SubmissionItemWithDeps) => {
|
||||
setEditingItem(item);
|
||||
setShowItemSelector(false);
|
||||
setShowItemEditDialog(true);
|
||||
};
|
||||
|
||||
const handleBulkEdit = async (submissionId: string) => {
|
||||
try {
|
||||
const items = await fetchSubmissionItems(submissionId);
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
toast({
|
||||
title: 'No Items Found',
|
||||
description: 'This submission has no items to edit',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setBulkEditItems(items);
|
||||
setBulkEditMode(true);
|
||||
setShowItemEditDialog(true);
|
||||
} catch (error: unknown) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: getErrorMessage(error),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Expose imperative API
|
||||
useImperativeHandle(ref, () => ({
|
||||
refresh: async () => {
|
||||
await queueManager.refresh();
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Offline Banner */}
|
||||
{isOffline && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>No Internet Connection</AlertTitle>
|
||||
<AlertDescription>
|
||||
You're offline. The moderation queue will automatically sync when your connection is restored.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Queue Statistics & Lock Status */}
|
||||
{queueManager.queue.queueStats && (
|
||||
<Card className="bg-gradient-to-r from-primary/5 to-primary/10 border-primary/20">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
||||
<QueueStats stats={queueManager.queue.queueStats} isMobile={isMobile} />
|
||||
<EnhancedLockStatusDisplay
|
||||
currentLock={queueManager.queue.currentLock}
|
||||
queueStats={queueManager.queue.queueStats}
|
||||
loading={queueManager.queue.isLoading}
|
||||
onExtendLock={() => queueManager.queue.extendLock(queueManager.queue.currentLock?.submissionId || '')}
|
||||
onReleaseLock={() => queueManager.queue.releaseLock(queueManager.queue.currentLock?.submissionId || '', false)}
|
||||
getCurrentTime={() => new Date()}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Superuser Queue Controls */}
|
||||
{isSuperuser() && (
|
||||
<SuperuserQueueControls
|
||||
activeLocksCount={activeLocksCount}
|
||||
onClearAllLocks={handleClearAllLocks}
|
||||
isLoading={queueManager.queue.isLoading}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Lock Restored Alert */}
|
||||
{lockRestored && queueManager.queue.currentLock && (() => {
|
||||
// Check if restored submission is in current queue
|
||||
const restoredSubmissionInQueue = queueManager.items.some(
|
||||
item => item.id === queueManager.queue.currentLock?.submissionId
|
||||
);
|
||||
|
||||
if (!restoredSubmissionInQueue) return null;
|
||||
|
||||
// Calculate time remaining
|
||||
const timeRemainingMs = queueManager.queue.currentLock.expiresAt.getTime() - Date.now();
|
||||
const timeRemainingSec = Math.max(0, Math.floor(timeRemainingMs / 1000));
|
||||
const isExpiringSoon = timeRemainingSec < 300; // Less than 5 minutes
|
||||
|
||||
return (
|
||||
<Alert className={isExpiringSoon
|
||||
? "border-orange-500/50 bg-orange-500/10"
|
||||
: "border-blue-500/50 bg-blue-500/5"
|
||||
}>
|
||||
<Info className={isExpiringSoon
|
||||
? "h-4 w-4 text-orange-600"
|
||||
: "h-4 w-4 text-blue-600"
|
||||
} />
|
||||
<AlertTitle>
|
||||
{isExpiringSoon
|
||||
? `Lock Expiring Soon (${Math.floor(timeRemainingSec / 60)}m ${timeRemainingSec % 60}s)`
|
||||
: "Active Claim Restored"
|
||||
}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{isExpiringSoon
|
||||
? "Your lock is about to expire. Complete your review or extend the lock."
|
||||
: "Your previous claim was restored. You still have time to review this submission."
|
||||
}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Filter Bar */}
|
||||
<QueueFilters
|
||||
activeEntityFilter={queueManager.filters.entityFilter}
|
||||
activeStatusFilter={queueManager.filters.statusFilter}
|
||||
sortConfig={queueManager.filters.sortConfig}
|
||||
isMobile={isMobile ?? false}
|
||||
isLoading={queueManager.loadingState === 'loading'}
|
||||
onEntityFilterChange={queueManager.filters.setEntityFilter}
|
||||
onStatusFilterChange={queueManager.filters.setStatusFilter}
|
||||
onSortChange={queueManager.filters.setSortConfig}
|
||||
onClearFilters={queueManager.filters.clearFilters}
|
||||
showClearButton={queueManager.filters.hasActiveFilters}
|
||||
onRefresh={queueManager.refresh}
|
||||
isRefreshing={queueManager.loadingState === 'refreshing'}
|
||||
/>
|
||||
|
||||
{/* Active Filters Display */}
|
||||
{queueManager.filters.hasActiveFilters && (
|
||||
<ActiveFiltersDisplay
|
||||
entityFilter={queueManager.filters.entityFilter}
|
||||
statusFilter={queueManager.filters.statusFilter}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Auto-refresh Indicator */}
|
||||
{adminSettings.getAdminPanelRefreshMode() === 'auto' && (
|
||||
<AutoRefreshIndicator
|
||||
enabled={true}
|
||||
intervalSeconds={Math.round(adminSettings.getAdminPanelPollInterval() / 1000)}
|
||||
mode={adminSettings.getUseRealtimeQueue() ? 'realtime' : 'polling'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* New Items Alert */}
|
||||
{queueManager.newItemsCount > 0 && (
|
||||
<NewItemsAlert
|
||||
count={queueManager.newItemsCount}
|
||||
onShowNewItems={queueManager.showNewItems}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Queue Content */}
|
||||
{queueManager.loadingState === 'loading' || queueManager.loadingState === 'initial' ? (
|
||||
<QueueSkeleton count={queueManager.pagination.pageSize} />
|
||||
) : queueManager.items.length === 0 ? (
|
||||
<EnhancedEmptyState
|
||||
entityFilter={queueManager.filters.entityFilter}
|
||||
statusFilter={queueManager.filters.statusFilter}
|
||||
onClearFilters={queueManager.filters.hasActiveFilters ? handleClearFilters : undefined}
|
||||
/>
|
||||
) : (
|
||||
<TooltipProvider>
|
||||
{queueManager.items.length <= 10 ? (
|
||||
// Standard rendering for small lists (no virtual scrolling overhead)
|
||||
<div className="space-y-6">
|
||||
{queueManager.items.map((item) => (
|
||||
<ModerationErrorBoundary key={item.id} submissionId={item.id}>
|
||||
<QueueItem
|
||||
item={item}
|
||||
isMobile={isMobile ?? false}
|
||||
actionLoading={queueManager.actionLoading}
|
||||
isLockedByMe={queueManager.queue.isLockedByMe(item.id, item.assigned_to || null, item.locked_until || null)}
|
||||
isLockedByOther={queueManager.queue.isLockedByOther(item.id, item.assigned_to || null, item.locked_until || null)}
|
||||
lockStatus={getLockStatus({ assigned_to: item.assigned_to || null, locked_until: item.locked_until || null }, user?.id || '')}
|
||||
currentLockSubmissionId={queueManager.queue.currentLock?.submissionId}
|
||||
notes={notes}
|
||||
isAdmin={isAdmin()}
|
||||
isSuperuser={isSuperuser()}
|
||||
queueIsLoading={queueManager.queue.isLoading}
|
||||
transactionStatuses={transactionStatuses}
|
||||
onNoteChange={handleNoteChange}
|
||||
onApprove={handlePerformAction}
|
||||
onResetToPending={queueManager.resetToPending}
|
||||
onRetryFailed={queueManager.retryFailedItems}
|
||||
onOpenPhotos={handleOpenPhotos}
|
||||
onOpenReviewManager={handleOpenReviewManager}
|
||||
onOpenItemEditor={handleOpenItemEditor}
|
||||
onClaimSubmission={queueManager.queue.claimSubmission}
|
||||
onDeleteSubmission={handleDeleteSubmission}
|
||||
onInteractionFocus={(id) => queueManager.markInteracting(id, true)}
|
||||
onInteractionBlur={(id) => queueManager.markInteracting(id, false)}
|
||||
onSuperuserReleaseLock={isSuperuser() ? handleSuperuserReleaseLock : undefined}
|
||||
/>
|
||||
</ModerationErrorBoundary>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
// Virtual scrolling for large lists (10+ items)
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="overflow-auto"
|
||||
style={{
|
||||
height: '70vh',
|
||||
contain: 'strict',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((virtualItem) => {
|
||||
const item = queueManager.items[virtualItem.index];
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
data-index={virtualItem.index}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
transform: `translateY(${virtualItem.start}px)`,
|
||||
}}
|
||||
className="pb-6"
|
||||
>
|
||||
<ModerationErrorBoundary submissionId={item.id}>
|
||||
<QueueItem
|
||||
item={item}
|
||||
isMobile={isMobile ?? false}
|
||||
actionLoading={queueManager.actionLoading}
|
||||
isLockedByMe={queueManager.queue.isLockedByMe(item.id, item.assigned_to || null, item.locked_until || null)}
|
||||
isLockedByOther={queueManager.queue.isLockedByOther(item.id, item.assigned_to || null, item.locked_until || null)}
|
||||
lockStatus={getLockStatus({ assigned_to: item.assigned_to || null, locked_until: item.locked_until || null }, user?.id || '')}
|
||||
currentLockSubmissionId={queueManager.queue.currentLock?.submissionId}
|
||||
notes={notes}
|
||||
isAdmin={isAdmin()}
|
||||
isSuperuser={isSuperuser()}
|
||||
queueIsLoading={queueManager.queue.isLoading}
|
||||
transactionStatuses={transactionStatuses}
|
||||
onNoteChange={handleNoteChange}
|
||||
onApprove={handlePerformAction}
|
||||
onResetToPending={queueManager.resetToPending}
|
||||
onRetryFailed={queueManager.retryFailedItems}
|
||||
onOpenPhotos={handleOpenPhotos}
|
||||
onOpenReviewManager={handleOpenReviewManager}
|
||||
onOpenItemEditor={handleOpenItemEditor}
|
||||
onClaimSubmission={queueManager.queue.claimSubmission}
|
||||
onDeleteSubmission={handleDeleteSubmission}
|
||||
onInteractionFocus={(id) => queueManager.markInteracting(id, true)}
|
||||
onInteractionBlur={(id) => queueManager.markInteracting(id, false)}
|
||||
onSuperuserReleaseLock={isSuperuser() ? handleSuperuserReleaseLock : undefined}
|
||||
/>
|
||||
</ModerationErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{queueManager.loadingState === 'ready' && queueManager.pagination.totalPages > 1 && (
|
||||
<QueuePagination
|
||||
currentPage={queueManager.pagination.currentPage}
|
||||
totalPages={queueManager.pagination.totalPages}
|
||||
pageSize={queueManager.pagination.pageSize}
|
||||
totalCount={queueManager.pagination.totalCount}
|
||||
isMobile={isMobile ?? false}
|
||||
onPageChange={queueManager.pagination.setCurrentPage}
|
||||
onPageSizeChange={queueManager.pagination.setPageSize}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Modals */}
|
||||
<PhotoModal
|
||||
photos={selectedPhotos.map(photo => ({
|
||||
...photo,
|
||||
caption: photo.caption ?? undefined
|
||||
}))}
|
||||
initialIndex={selectedPhotoIndex}
|
||||
isOpen={photoModalOpen}
|
||||
onClose={() => setPhotoModalOpen(false)}
|
||||
/>
|
||||
|
||||
{selectedSubmissionId && (
|
||||
<SubmissionReviewManager
|
||||
submissionId={selectedSubmissionId}
|
||||
open={reviewManagerOpen}
|
||||
onOpenChange={setReviewManagerOpen}
|
||||
onComplete={() => {
|
||||
queueManager.refresh();
|
||||
setSelectedSubmissionId(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Phase 3: Item Selector Dialog */}
|
||||
<ItemSelectorDialog
|
||||
items={availableItems}
|
||||
open={showItemSelector}
|
||||
onOpenChange={setShowItemSelector}
|
||||
onSelectItem={handleSelectItem}
|
||||
onBulkEdit={() => {
|
||||
setShowItemSelector(false);
|
||||
setBulkEditItems(availableItems);
|
||||
setBulkEditMode(true);
|
||||
setShowItemEditDialog(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Phase 4 & 5: Enhanced Item Edit Dialog */}
|
||||
<ItemEditDialog
|
||||
item={bulkEditMode ? null : editingItem}
|
||||
items={bulkEditMode ? bulkEditItems : undefined}
|
||||
open={showItemEditDialog}
|
||||
onOpenChange={(open) => {
|
||||
setShowItemEditDialog(open);
|
||||
if (!open) {
|
||||
setEditingItem(null);
|
||||
setBulkEditMode(false);
|
||||
setBulkEditItems([]);
|
||||
}
|
||||
}}
|
||||
onComplete={() => {
|
||||
queueManager.refresh();
|
||||
setEditingItem(null);
|
||||
setBulkEditMode(false);
|
||||
setBulkEditItems([]);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Confirmation Dialog */}
|
||||
<ConfirmationDialog
|
||||
open={confirmDialog.open}
|
||||
onOpenChange={(open) => setConfirmDialog(prev => ({ ...prev, open }))}
|
||||
title={confirmDialog.title}
|
||||
description={confirmDialog.description}
|
||||
onConfirm={confirmDialog.onConfirm}
|
||||
variant="destructive"
|
||||
confirmLabel="Delete"
|
||||
/>
|
||||
|
||||
{/* Keyboard Shortcuts Help */}
|
||||
<KeyboardShortcutsHelp
|
||||
open={showShortcutsHelp}
|
||||
onOpenChange={setShowShortcutsHelp}
|
||||
shortcuts={shortcuts}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ModerationQueue.displayName = 'ModerationQueue';
|
||||
|
||||
export type { ModerationQueueRef } from '@/types/moderation';
|
||||
Reference in New Issue
Block a user