Reverted to commit be92deec43

This commit is contained in:
gpt-engineer-app[bot]
2025-10-13 12:17:13 +00:00
parent 085375e595
commit 3ec26e23a3
7 changed files with 31 additions and 165 deletions

View File

@@ -22,6 +22,7 @@ const getEntityFilterIcon = (filter: EntityFilter) => {
const getSortFieldLabel = (field: SortField): string => { const getSortFieldLabel = (field: SortField): string => {
switch (field) { switch (field) {
case 'username': return 'Submitter';
case 'submission_type': return 'Type'; case 'submission_type': return 'Type';
case 'escalated': return 'Escalated'; case 'escalated': return 'Escalated';
case 'status': return 'Status'; case 'status': return 'Status';

View File

@@ -1,6 +1,5 @@
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';
@@ -20,8 +19,7 @@ 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, QueueTab } from '@/types/moderation'; import type { ModerationQueueRef } 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();
@@ -89,28 +87,6 @@ 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

@@ -15,6 +15,7 @@ interface QueueSortControlsProps {
const getSortFieldLabel = (field: SortField): string => { const getSortFieldLabel = (field: SortField): string => {
switch (field) { switch (field) {
case 'created_at': return 'Date Created'; case 'created_at': return 'Date Created';
case 'username': return 'Submitter';
case 'submission_type': return 'Type'; case 'submission_type': return 'Type';
case 'status': return 'Status'; case 'status': return 'Status';
case 'escalated': return 'Escalated'; case 'escalated': return 'Escalated';
@@ -57,6 +58,7 @@ export const QueueSortControls = ({
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="created_at">{getSortFieldLabel('created_at')}</SelectItem> <SelectItem value="created_at">{getSortFieldLabel('created_at')}</SelectItem>
<SelectItem value="username">{getSortFieldLabel('username')}</SelectItem>
<SelectItem value="submission_type">{getSortFieldLabel('submission_type')}</SelectItem> <SelectItem value="submission_type">{getSortFieldLabel('submission_type')}</SelectItem>
<SelectItem value="status">{getSortFieldLabel('status')}</SelectItem> <SelectItem value="status">{getSortFieldLabel('status')}</SelectItem>
<SelectItem value="escalated">{getSortFieldLabel('escalated')}</SelectItem> <SelectItem value="escalated">{getSortFieldLabel('escalated')}</SelectItem>

View File

@@ -128,8 +128,6 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
const fetchItemsRef = useRef<((silent?: boolean) => Promise<void>) | null>(null); const fetchItemsRef = useRef<((silent?: boolean) => Promise<void>) | null>(null);
const FETCH_COOLDOWN_MS = 1000; 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);
@@ -223,26 +221,9 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
status status
) )
`, `,
); )
.order("escalated", { ascending: false })
// Validate and log sort configuration .order("created_at", { ascending: true });
console.log('[Query] Sort config received:', {
field: sort.config.field,
direction: sort.config.direction,
ascending: sort.config.direction === 'asc'
});
const validSortFields = ['created_at', 'submission_type', 'status', 'escalated'];
let sortField = sort.config.field;
if (!validSortFields.includes(sortField)) {
console.warn('[Query] Invalid sort field:', sortField, '- falling back to created_at');
sortField = 'created_at';
}
// Apply sorting by user's chosen field only
submissionsQuery = submissionsQuery
.order(sortField, { ascending: sort.config.direction === 'asc' });
// Apply tab-based status filtering // Apply tab-based status filtering
const tab = filters.activeTab; const tab = filters.activeTab;
@@ -250,20 +231,6 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
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") {
@@ -272,7 +239,6 @@ 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 {
@@ -295,56 +261,12 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
); );
} }
// Get total count - rebuild query with same filters // Get total count
let countQuery = supabase const { count } = await supabase
.from("content_submissions") .from("content_submissions")
.select("*", { count: "exact", head: true }); .select("*", { count: "exact", head: true })
.match(submissionsQuery as any);
// Apply the exact same filters as the main query
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") {
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 (statusFilter === "all") {
countQuery = countQuery.in("status", ["approved", "rejected"]);
} else {
countQuery = countQuery.eq("status", statusFilter);
}
}
// Apply entity type filter
if (entityFilter === "photos") {
countQuery = countQuery.eq("submission_type", "photo");
} else if (entityFilter === "submissions") {
countQuery = countQuery.neq("submission_type", "photo");
}
// Apply access control
if (!isAdmin && !isSuperuser) {
const now = new Date().toISOString();
countQuery = countQuery.or(
`assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${user.id}`,
);
}
const { count } = await countQuery;
pagination.setTotalCount(count || 0); pagination.setTotalCount(count || 0);
// Apply pagination // Apply pagination
@@ -530,7 +452,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
setLoadingState("ready"); setLoadingState("ready");
} }
}, },
[user, isAdmin, isSuperuser, filters, pagination, sort, profileCache, entityCache, toast], [user, isAdmin, isSuperuser, filters, pagination, profileCache, entityCache, toast],
); );
// Store fetchItems in ref to avoid re-creating visibility listener // Store fetchItems in ref to avoid re-creating visibility listener
@@ -858,56 +780,20 @@ 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]);
// Debounced fetch for effects - prevents race conditions // Filter changes trigger refetch
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();
debouncedEffectFetch(); fetchItems(true);
}, [filters.activeTab, filters.debouncedEntityFilter, filters.debouncedStatusFilter, user, debouncedEffectFetch, pagination]); }, [filters.debouncedEntityFilter, filters.debouncedStatusFilter]);
// Sort changes trigger refetch
useEffect(() => {
if (!user || !initialFetchCompleteRef.current || isMountingRef.current) {
return;
}
console.log('[Sort Change] Queuing debounced fetch', {
field: sort.config.field,
direction: sort.config.direction
});
pagination.reset();
debouncedEffectFetch();
}, [sort.config.field, sort.config.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;
debouncedEffectFetch(); fetchItemsRef.current?.(true);
}, [pagination.currentPage, pagination.pageSize, debouncedEffectFetch]); }, [pagination.currentPage, pagination.pageSize]);
// 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(() => {

View File

@@ -73,7 +73,9 @@ 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

@@ -27,6 +27,12 @@ export function sortModerationItems(
comparison = new Date(a.created_at).getTime() - new Date(b.created_at).getTime(); comparison = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
break; break;
case 'username':
const usernameA = a.user_profile?.username || a.user_profile?.display_name || '';
const usernameB = b.user_profile?.username || b.user_profile?.display_name || '';
comparison = usernameA.localeCompare(usernameB);
break;
case 'submission_type': case 'submission_type':
comparison = (a.submission_type || '').localeCompare(b.submission_type || ''); comparison = (a.submission_type || '').localeCompare(b.submission_type || '');
break; break;
@@ -71,16 +77,7 @@ export function loadSortConfig(key: string = 'moderationQueue_sortConfig'): Sort
try { try {
const saved = localStorage.getItem(key); const saved = localStorage.getItem(key);
if (saved) { if (saved) {
const config = JSON.parse(saved); return 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);
@@ -126,6 +123,8 @@ export function getSortFieldLabel(field: SortField): string {
switch (field) { switch (field) {
case 'created_at': case 'created_at':
return 'Date Created'; return 'Date Created';
case 'username':
return 'Submitter';
case 'submission_type': case 'submission_type':
return 'Type'; return 'Type';
case 'status': case 'status':

View File

@@ -97,12 +97,12 @@ export type StatusFilter = 'all' | 'pending' | 'partially_approved' | 'flagged'
/** /**
* Available tabs in the moderation interface * Available tabs in the moderation interface
*/ */
export type QueueTab = 'mainQueue' | 'escalated' | 'archive'; export type QueueTab = 'mainQueue' | 'archive';
/** /**
* Fields that can be used for sorting the moderation queue * Fields that can be used for sorting the moderation queue
*/ */
export type SortField = 'created_at' | 'submission_type' | 'status' | 'escalated'; export type SortField = 'created_at' | 'username' | 'submission_type' | 'status' | 'escalated';
/** /**
* Direction for sorting (ascending or descending) * Direction for sorting (ascending or descending)