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