Reverted to commit de2d4a495f

This commit is contained in:
gpt-engineer-app[bot]
2025-10-13 13:05:48 +00:00
parent c129be2e87
commit 4fed307335
6 changed files with 144 additions and 160 deletions

View File

@@ -1,5 +1,6 @@
import { useState, useImperativeHandle, forwardRef, useMemo } from 'react'; import { useState, useImperativeHandle, forwardRef, useMemo } from 'react';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { useUserRole } from '@/hooks/useUserRole'; import { useUserRole } from '@/hooks/useUserRole';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
@@ -19,7 +20,8 @@ import { AutoRefreshIndicator } from './AutoRefreshIndicator';
import { NewItemsAlert } from './NewItemsAlert'; import { NewItemsAlert } from './NewItemsAlert';
import { EmptyQueueState } from './EmptyQueueState'; import { EmptyQueueState } from './EmptyQueueState';
import { QueuePagination } from './QueuePagination'; import { QueuePagination } from './QueuePagination';
import type { ModerationQueueRef } from '@/types/moderation'; import type { ModerationQueueRef, QueueTab } from '@/types/moderation';
import { AlertTriangle, Clock, Archive } from 'lucide-react';
export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => { export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@@ -87,6 +89,28 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{/* Queue Tabs */}
<Tabs
value={queueManager.filters.activeTab}
onValueChange={(value) => queueManager.filters.setActiveTab(value as QueueTab)}
className="w-full"
>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="mainQueue" className="flex items-center gap-2">
<Clock className="w-4 h-4" />
Main Queue
</TabsTrigger>
<TabsTrigger value="escalated" className="flex items-center gap-2">
<AlertTriangle className="w-4 h-4" />
Escalated
</TabsTrigger>
<TabsTrigger value="archive" className="flex items-center gap-2">
<Archive className="w-4 h-4" />
Archive
</TabsTrigger>
</TabsList>
</Tabs>
{/* Queue Statistics & Lock Status */} {/* Queue Statistics & Lock Status */}
{queueManager.queue.queueStats && ( {queueManager.queue.queueStats && (
<Card className="bg-gradient-to-r from-primary/5 to-primary/10 border-primary/20"> <Card className="bg-gradient-to-r from-primary/5 to-primary/10 border-primary/20">

View File

@@ -12,7 +12,7 @@ import {
} from "./index"; } from "./index";
import { useModerationQueue } from "@/hooks/useModerationQueue"; import { useModerationQueue } from "@/hooks/useModerationQueue";
import { smartMergeArray } from "@/lib/smartStateUpdate"; import { smartMergeArray } from "@/lib/smartStateUpdate";
import type { ModerationItem, EntityFilter, StatusFilter, LoadingState, SortConfig, SortField } from "@/types/moderation"; import type { ModerationItem, EntityFilter, StatusFilter, LoadingState, SortConfig } from "@/types/moderation";
/** /**
* Configuration for useModerationQueueManager * Configuration for useModerationQueueManager
@@ -100,29 +100,11 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
}); });
const sort = useModerationSort({ const sort = useModerationSort({
initialConfig: { field: "created_at", direction: "desc" }, initialConfig: { field: "created_at", direction: "asc" },
persist: true, persist: true,
storageKey: "moderationQueue_sortConfig", storageKey: "moderationQueue_sortConfig",
}); });
/**
* Map UI sort field to actual database column
*/
const getSortColumn = (field: SortField): string => {
switch (field) {
case 'created_at':
return 'created_at';
case 'submission_type':
return 'submission_type';
case 'status':
return 'status';
case 'escalated':
return 'escalated';
default:
return 'created_at';
}
};
const queue = useModerationQueue(); const queue = useModerationQueue();
const entityCache = useEntityCache(); const entityCache = useEntityCache();
const profileCache = useProfileCache(); const profileCache = useProfileCache();
@@ -145,7 +127,9 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
const isMountingRef = useRef(true); const isMountingRef = useRef(true);
const fetchItemsRef = useRef<((silent?: boolean) => Promise<void>) | null>(null); const fetchItemsRef = useRef<((silent?: boolean) => Promise<void>) | null>(null);
const FETCH_COOLDOWN_MS = 300; // Match filter debounce delay for responsive sort changes const FETCH_COOLDOWN_MS = 1000;
const EFFECT_DEBOUNCE_MS = 50; // Short debounce to let all effects settle
const effectFetchTimerRef = useRef<NodeJS.Timeout | null>(null);
// Store settings in refs to avoid re-creating fetchItems // Store settings in refs to avoid re-creating fetchItems
const settingsRef = useRef(settings); const settingsRef = useRef(settings);
@@ -162,14 +146,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
* Fetch queue items from database * Fetch queue items from database
*/ */
const fetchItems = useCallback( const fetchItems = useCallback(
async (silent = false, bypassCooldown = false) => { async (silent = false) => {
console.log('🔄 [fetchItems RECREATED]', {
sortField: sort.field,
sortDirection: sort.direction,
bypassCooldown,
timestamp: new Date().toISOString()
});
if (!user) return; if (!user) return;
// Get caller info // Get caller info
@@ -196,10 +173,10 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
return; return;
} }
// Cooldown check (can be bypassed for critical operations like sort changes) // Cooldown check
const now = Date.now(); const now = Date.now();
const timeSinceLastFetch = now - lastFetchTimeRef.current; const timeSinceLastFetch = now - lastFetchTimeRef.current;
if (!bypassCooldown && timeSinceLastFetch < FETCH_COOLDOWN_MS && lastFetchTimeRef.current > 0) { if (timeSinceLastFetch < FETCH_COOLDOWN_MS && lastFetchTimeRef.current > 0) {
console.log(`⏸️ Fetch cooldown active (${timeSinceLastFetch}ms)`); console.log(`⏸️ Fetch cooldown active (${timeSinceLastFetch}ms)`);
return; return;
} }
@@ -245,15 +222,48 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
item_data, item_data,
status status
) )
` `,
); );
// Apply tab-based status filtering FIRST // Validate sort field is an actual column in content_submissions
const validSortFields = ['created_at', 'submission_type', 'status', 'escalated', 'submitted_at'];
let sortField = sort.config.field;
if (!validSortFields.includes(sortField)) {
console.warn('[Query] Invalid sort field:', sortField, '- falling back to created_at');
sortField = 'created_at';
}
console.log('[Query] Sorting by:', {
field: sortField,
direction: sort.config.direction,
ascending: sort.config.direction === 'asc'
});
// Apply sorting by user's chosen field only
submissionsQuery = submissionsQuery
.order(sortField, { ascending: sort.config.direction === 'asc' });
// Apply tab-based status filtering
const tab = filters.activeTab; const tab = filters.activeTab;
const statusFilter = filters.debouncedStatusFilter; const statusFilter = filters.debouncedStatusFilter;
const entityFilter = filters.debouncedEntityFilter; const entityFilter = filters.debouncedEntityFilter;
if (tab === "mainQueue") { if (tab === "mainQueue") {
// Main queue: non-escalated pending items
submissionsQuery = submissionsQuery.eq("escalated", false);
if (statusFilter === "all") {
submissionsQuery = submissionsQuery.in("status", ["pending", "flagged", "partially_approved"]);
} else if (statusFilter === "pending") {
submissionsQuery = submissionsQuery.in("status", ["pending", "partially_approved"]);
} else {
submissionsQuery = submissionsQuery.eq("status", statusFilter);
}
} else if (tab === "escalated") {
// Escalated queue: only escalated items
submissionsQuery = submissionsQuery.eq("escalated", true);
if (statusFilter === "all") { if (statusFilter === "all") {
submissionsQuery = submissionsQuery.in("status", ["pending", "flagged", "partially_approved"]); submissionsQuery = submissionsQuery.in("status", ["pending", "flagged", "partially_approved"]);
} else if (statusFilter === "pending") { } else if (statusFilter === "pending") {
@@ -262,6 +272,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
submissionsQuery = submissionsQuery.eq("status", statusFilter); submissionsQuery = submissionsQuery.eq("status", statusFilter);
} }
} else { } else {
// Archive: completed items (non-escalated and escalated)
if (statusFilter === "all") { if (statusFilter === "all") {
submissionsQuery = submissionsQuery.in("status", ["approved", "rejected"]); submissionsQuery = submissionsQuery.in("status", ["approved", "rejected"]);
} else { } else {
@@ -276,7 +287,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
submissionsQuery = submissionsQuery.neq("submission_type", "photo"); submissionsQuery = submissionsQuery.neq("submission_type", "photo");
} }
// Apply access control (must be AFTER status/entity filters to work as AND) // Apply access control
if (!isAdmin && !isSuperuser) { if (!isAdmin && !isSuperuser) {
const now = new Date().toISOString(); const now = new Date().toISOString();
submissionsQuery = submissionsQuery.or( submissionsQuery = submissionsQuery.or(
@@ -284,41 +295,25 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
); );
} }
// Apply user-selected sort configuration // Get total count - rebuild query with same filters
const sortColumn = getSortColumn(sort.field);
const sortAscending = sort.direction === 'asc';
console.log('[Query] Applying sort:', {
sortLevels: [
'1. escalated DESC (always first)',
`2. ${sortColumn} ${sortAscending ? 'ASC' : 'DESC'} (user selected)`,
sortColumn !== 'created_at' ? '3. created_at ASC (tie-breaker)' : null
].filter(Boolean),
uiField: sort.field,
dbColumn: sortColumn,
direction: sort.direction,
ascending: sortAscending,
timestamp: new Date().toISOString()
});
// Always prioritize escalated submissions first
submissionsQuery = submissionsQuery.order('escalated', { ascending: false });
// Then apply user-selected sort
submissionsQuery = submissionsQuery.order(sortColumn, { ascending: sortAscending });
// Tertiary sort by created_at for consistency (only if not already the user's sort choice)
if (sortColumn !== 'created_at') {
submissionsQuery = submissionsQuery.order('created_at', { ascending: true });
}
// Get total count for pagination (rebuild query with same filters in SAME ORDER)
let countQuery = supabase let countQuery = supabase
.from("content_submissions") .from("content_submissions")
.select("*", { count: "exact", head: true }); .select("*", { count: "exact", head: true });
// Apply same filters as main query - status filter FIRST // Apply the exact same filters as the main query
if (tab === "mainQueue") { if (tab === "mainQueue") {
countQuery = countQuery.eq("escalated", false);
if (statusFilter === "all") {
countQuery = countQuery.in("status", ["pending", "flagged", "partially_approved"]);
} else if (statusFilter === "pending") {
countQuery = countQuery.in("status", ["pending", "partially_approved"]);
} else {
countQuery = countQuery.eq("status", statusFilter);
}
} else if (tab === "escalated") {
countQuery = countQuery.eq("escalated", true);
if (statusFilter === "all") { if (statusFilter === "all") {
countQuery = countQuery.in("status", ["pending", "flagged", "partially_approved"]); countQuery = countQuery.in("status", ["pending", "flagged", "partially_approved"]);
} else if (statusFilter === "pending") { } else if (statusFilter === "pending") {
@@ -334,14 +329,14 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
} }
} }
// Entity type filter // Apply entity type filter
if (entityFilter === "photos") { if (entityFilter === "photos") {
countQuery = countQuery.eq("submission_type", "photo"); countQuery = countQuery.eq("submission_type", "photo");
} else if (entityFilter === "submissions") { } else if (entityFilter === "submissions") {
countQuery = countQuery.neq("submission_type", "photo"); countQuery = countQuery.neq("submission_type", "photo");
} }
// Access control (AFTER status/entity filters) // Apply access control
if (!isAdmin && !isSuperuser) { if (!isAdmin && !isSuperuser) {
const now = new Date().toISOString(); const now = new Date().toISOString();
countQuery = countQuery.or( countQuery = countQuery.or(
@@ -357,40 +352,10 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
const endIndex = pagination.endIndex; const endIndex = pagination.endIndex;
submissionsQuery = submissionsQuery.range(startIndex, endIndex); submissionsQuery = submissionsQuery.range(startIndex, endIndex);
// Log the final query for debugging
console.log('[Query] Final query about to execute:', {
sortApplied: sortColumn,
sortDirection: sortAscending ? 'ASC' : 'DESC',
filtersApplied: {
tab: filters.activeTab,
status: filters.debouncedStatusFilter,
entity: filters.debouncedEntityFilter,
},
pagination: {
startIndex,
endIndex
}
});
const { data: submissions, error: submissionsError } = await submissionsQuery; const { data: submissions, error: submissionsError } = await submissionsQuery;
if (submissionsError) throw submissionsError; if (submissionsError) throw submissionsError;
// VALIDATE: Log first few items to verify sort is working
if (submissions && submissions.length > 0) {
console.log('[Query] Results returned (first 3 items):', {
sortLevels: `escalated DESC → ${sortColumn} ${sortAscending ? 'ASC' : 'DESC'}`,
items: submissions.slice(0, 3).map(s => ({
id: s.id.substring(0, 8),
escalated: s.escalated,
type: s.submission_type,
status: s.status,
created: s.created_at,
sortValue: s[sortColumn as keyof typeof s]
}))
});
}
// Fetch related profiles and entities // Fetch related profiles and entities
const userIds = [ const userIds = [
...new Set([ ...new Set([
@@ -565,7 +530,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
setLoadingState("ready"); setLoadingState("ready");
} }
}, },
[user, isAdmin, isSuperuser, filters, pagination, profileCache, entityCache, toast, sort.field, sort.direction], [user, isAdmin, isSuperuser, filters, pagination, sort, profileCache, entityCache, toast],
); );
// Store fetchItems in ref to avoid re-creating visibility listener // Store fetchItems in ref to avoid re-creating visibility listener
@@ -893,20 +858,53 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [user?.id]); }, [user?.id]);
// Filter changes trigger refetch // Debounced fetch for effects - prevents race conditions
const debouncedEffectFetch = useCallback(() => {
if (effectFetchTimerRef.current) {
clearTimeout(effectFetchTimerRef.current);
}
effectFetchTimerRef.current = setTimeout(() => {
console.log('[Debounced Fetch] Executing after effects settled');
fetchItemsRef.current?.(true);
}, EFFECT_DEBOUNCE_MS);
}, []);
// Filter and tab changes trigger refetch
useEffect(() => { useEffect(() => {
if (!user || !initialFetchCompleteRef.current || isMountingRef.current) return; if (!user || !initialFetchCompleteRef.current || isMountingRef.current) return;
console.log('[Filter/Tab Change] Queuing debounced fetch');
pagination.reset(); pagination.reset();
fetchItems(true); debouncedEffectFetch();
}, [filters.debouncedEntityFilter, filters.debouncedStatusFilter]); }, [filters.activeTab, filters.debouncedEntityFilter, filters.debouncedStatusFilter, user, debouncedEffectFetch, pagination]);
// Sort changes trigger refetch
useEffect(() => {
if (!user || !initialFetchCompleteRef.current || isMountingRef.current) {
return;
}
console.log('[Sort Change] Queuing debounced fetch');
pagination.reset();
debouncedEffectFetch();
}, [sort.field, sort.direction, user, pagination, debouncedEffectFetch]);
// Pagination changes trigger refetch // Pagination changes trigger refetch
useEffect(() => { useEffect(() => {
if (!user || !initialFetchCompleteRef.current || pagination.currentPage === 1) return; if (!user || !initialFetchCompleteRef.current || pagination.currentPage === 1) return;
fetchItemsRef.current?.(true); debouncedEffectFetch();
}, [pagination.currentPage, pagination.pageSize]); }, [pagination.currentPage, pagination.pageSize, debouncedEffectFetch]);
// Cleanup effect timer on unmount
useEffect(() => {
return () => {
if (effectFetchTimerRef.current) {
clearTimeout(effectFetchTimerRef.current);
}
};
}, []);
// Polling effect (when realtime disabled) // Polling effect (when realtime disabled)
useEffect(() => { useEffect(() => {
@@ -988,36 +986,6 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
}; };
}, [settings.refreshOnTabVisible]); }, [settings.refreshOnTabVisible]);
// Refetch when sort configuration changes
useEffect(() => {
console.log('🔄 [Sort Changed]', {
field: sort.field,
direction: sort.direction,
timestamp: new Date().toISOString()
});
// Skip if initial fetch hasn't completed yet
if (!initialFetchCompleteRef.current) {
console.log('⏭️ Skipping sort refetch (initial fetch not complete)');
return;
}
// Skip if mounting
if (isMountingRef.current) {
console.log('⏭️ Skipping sort refetch (mounting)');
return;
}
console.log('✅ Triggering refetch due to sort change', {
willUseField: sort.field,
willUseDirection: sort.direction
});
// Call fetchItems directly (guaranteed to have latest sort values in closure)
// Use bypass to skip cooldown for immediate sort response
fetchItems(false, true);
}, [sort.field, sort.direction, fetchItems]);
// Initialize realtime subscriptions // Initialize realtime subscriptions
useRealtimeSubscriptions({ useRealtimeSubscriptions({
enabled: settings.useRealtimeQueue && !!user, enabled: settings.useRealtimeQueue && !!user,

View File

@@ -4,7 +4,7 @@
* Manages sort configuration for the moderation queue with persistence. * Manages sort configuration for the moderation queue with persistence.
*/ */
import { useState, useCallback, useEffect, useMemo } from 'react'; import { useState, useCallback, useEffect } from 'react';
import type { SortConfig, SortField, SortDirection } from '@/types/moderation'; import type { SortConfig, SortField, SortDirection } from '@/types/moderation';
import { import {
getDefaultSortConfig, getDefaultSortConfig,
@@ -87,24 +87,9 @@ export function useModerationSort(config: ModerationSortConfig = {}): Moderation
// Load persisted or use initial/default config // Load persisted or use initial/default config
const [sortConfig, setSortConfig] = useState<SortConfig>(() => { const [sortConfig, setSortConfig] = useState<SortConfig>(() => {
// Priority order: if (initialConfig) return initialConfig;
// 1. Saved config from localStorage (if persist enabled and exists) if (persist) return loadSortConfig(storageKey);
// 2. initialConfig prop (if provided) return getDefaultSortConfig();
// 3. Global default (fallback)
if (persist) {
try {
const saved = localStorage.getItem(storageKey);
if (saved) {
return JSON.parse(saved);
}
} catch (error) {
console.error('Failed to load sort config:', error);
}
}
// Use initialConfig if provided, otherwise use global default
return initialConfig || getDefaultSortConfig();
}); });
// Persist changes // Persist changes
@@ -146,7 +131,7 @@ export function useModerationSort(config: ModerationSortConfig = {}): Moderation
// Check if using default config // Check if using default config
const isDefault = isDefaultSortConfig(sortConfig); const isDefault = isDefaultSortConfig(sortConfig);
return useMemo(() => ({ return {
config: sortConfig, config: sortConfig,
field: sortConfig.field, field: sortConfig.field,
direction: sortConfig.direction, direction: sortConfig.direction,
@@ -156,5 +141,5 @@ export function useModerationSort(config: ModerationSortConfig = {}): Moderation
setConfig, setConfig,
reset, reset,
isDefault, isDefault,
}), [sortConfig, setField, setDirection, toggleSortDirection, setConfig, reset, isDefault]); };
} }

View File

@@ -73,9 +73,7 @@ export function buildSubmissionQuery(
item_data, item_data,
status status
) )
`) `);
.order('escalated', { ascending: false })
.order('created_at', { ascending: true });
// Apply tab-based status filtering // Apply tab-based status filtering
if (tab === 'mainQueue') { if (tab === 'mainQueue') {

View File

@@ -57,7 +57,7 @@ export function sortModerationItems(
export function getDefaultSortConfig(): SortConfig { export function getDefaultSortConfig(): SortConfig {
return { return {
field: 'created_at', field: 'created_at',
direction: 'desc', // Newest first by default direction: 'asc',
}; };
} }
@@ -71,7 +71,16 @@ export function loadSortConfig(key: string = 'moderationQueue_sortConfig'): Sort
try { try {
const saved = localStorage.getItem(key); const saved = localStorage.getItem(key);
if (saved) { if (saved) {
return JSON.parse(saved); const config = JSON.parse(saved);
// Migrate old 'username' sort to 'created_at'
if (config.field === 'username') {
console.warn('[Sort] Migrating deprecated username sort to created_at');
config.field = 'created_at';
saveSortConfig(config, key); // Save the migrated config
}
return config;
} }
} catch (error) { } catch (error) {
console.error('Failed to load sort config:', error); console.error('Failed to load sort config:', error);

View File

@@ -97,7 +97,7 @@ export type StatusFilter = 'all' | 'pending' | 'partially_approved' | 'flagged'
/** /**
* Available tabs in the moderation interface * Available tabs in the moderation interface
*/ */
export type QueueTab = 'mainQueue' | 'archive'; export type QueueTab = 'mainQueue' | 'escalated' | 'archive';
/** /**
* Fields that can be used for sorting the moderation queue * Fields that can be used for sorting the moderation queue