mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 16:11:12 -05:00
Reverted to commit be92deec43
This commit is contained in:
@@ -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';
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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':
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user