diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index 48fa5d30..bb1703a3 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -109,15 +109,17 @@ export const ModerationQueue = forwardRef((props, ref) => { )} {/* Filter Bar */} - + {/* Active Filters Display */} {queueManager.filters.hasActiveFilters && ( diff --git a/src/components/moderation/QueueFilters.tsx b/src/components/moderation/QueueFilters.tsx index 2e3878cb..c98f8b3f 100644 --- a/src/components/moderation/QueueFilters.tsx +++ b/src/components/moderation/QueueFilters.tsx @@ -2,14 +2,17 @@ import { Filter, MessageSquare, FileText, Image, X } from 'lucide-react'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Button } from '@/components/ui/button'; -import type { EntityFilter, StatusFilter } from '@/types/moderation'; +import { QueueSortControls } from './QueueSortControls'; +import type { EntityFilter, StatusFilter, SortConfig } from '@/types/moderation'; interface QueueFiltersProps { activeEntityFilter: EntityFilter; activeStatusFilter: StatusFilter; + sortConfig: SortConfig; isMobile: boolean; onEntityFilterChange: (filter: EntityFilter) => void; onStatusFilterChange: (filter: StatusFilter) => void; + onSortChange: (config: SortConfig) => void; onClearFilters: () => void; showClearButton: boolean; } @@ -26,9 +29,11 @@ const getEntityFilterIcon = (filter: EntityFilter) => { export const QueueFilters = ({ activeEntityFilter, activeStatusFilter, + sortConfig, isMobile, onEntityFilterChange, onStatusFilterChange, + onSortChange, onClearFilters, showClearButton }: QueueFiltersProps) => { @@ -107,6 +112,13 @@ export const QueueFilters = ({ + + {/* Sort Controls */} + {/* Clear Filters Button */} diff --git a/src/components/moderation/QueueSortControls.tsx b/src/components/moderation/QueueSortControls.tsx new file mode 100644 index 00000000..9fdfb5f5 --- /dev/null +++ b/src/components/moderation/QueueSortControls.tsx @@ -0,0 +1,78 @@ +import { ArrowUp, ArrowDown } from 'lucide-react'; +import { Label } from '@/components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Button } from '@/components/ui/button'; +import type { SortConfig, SortField } from '@/types/moderation'; + +interface QueueSortControlsProps { + sortConfig: SortConfig; + onSortChange: (config: SortConfig) => void; + isMobile: boolean; +} + +const SORT_FIELD_LABELS: Record = { + created_at: 'Date Submitted', + submission_type: 'Type', + status: 'Status' +}; + +export const QueueSortControls = ({ + sortConfig, + onSortChange, + isMobile +}: QueueSortControlsProps) => { + const handleFieldChange = (field: SortField) => { + onSortChange({ ...sortConfig, field }); + }; + + const handleDirectionToggle = () => { + onSortChange({ + ...sortConfig, + direction: sortConfig.direction === 'asc' ? 'desc' : 'asc' + }); + }; + + const DirectionIcon = sortConfig.direction === 'asc' ? ArrowUp : ArrowDown; + + return ( +
+
+ + +
+ +
+ +
+
+ ); +}; diff --git a/src/hooks/moderation/index.ts b/src/hooks/moderation/index.ts index e9727013..59baf489 100644 --- a/src/hooks/moderation/index.ts +++ b/src/hooks/moderation/index.ts @@ -17,6 +17,9 @@ export type { ModerationFilters, ModerationFiltersConfig } from './useModeration export { usePagination } from './usePagination'; export type { PaginationState, PaginationConfig } from './usePagination'; +export { useModerationSort } from './useModerationSort'; +export type { UseModerationSortReturn } from './useModerationSort'; + export { useRealtimeSubscriptions } from './useRealtimeSubscriptions'; export type { RealtimeSubscriptionConfig, diff --git a/src/hooks/moderation/useModerationQueueManager.ts b/src/hooks/moderation/useModerationQueueManager.ts index 76e040bb..3280fe5c 100644 --- a/src/hooks/moderation/useModerationQueueManager.ts +++ b/src/hooks/moderation/useModerationQueueManager.ts @@ -7,6 +7,7 @@ import { useProfileCache, useModerationFilters, usePagination, + useModerationSort, useRealtimeSubscriptions, } from "./index"; import { useModerationQueue } from "@/hooks/useModerationQueue"; @@ -43,6 +44,7 @@ export interface ModerationQueueManager { // Sub-hooks (exposed for granular control) filters: ReturnType; pagination: ReturnType; + sort: ReturnType; queue: ReturnType; // Realtime @@ -97,7 +99,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig): }, }); - // Removed - sorting functionality deleted + const sort = useModerationSort(); const queue = useModerationQueue(); const entityCache = useEntityCache(); @@ -217,6 +219,21 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig): `, ); + // CRITICAL: Multi-level ordering + // Level 1: Always sort by escalated first (descending) + submissionsQuery = submissionsQuery.order('escalated', { ascending: false }); + + // Level 2: Apply user-selected sort + submissionsQuery = submissionsQuery.order( + sort.config.field, + { ascending: sort.config.direction === 'asc' } + ); + + // Level 3: Tertiary sort by created_at (if not already primary) + if (sort.config.field !== 'created_at') { + submissionsQuery = submissionsQuery.order('created_at', { ascending: true }); + } + // Apply tab-based status filtering const tab = filters.activeTab; const statusFilter = filters.debouncedStatusFilter; @@ -444,7 +461,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig): setLoadingState("ready"); } }, - [user, isAdmin, isSuperuser, filters, pagination, profileCache, entityCache, toast], + [user, isAdmin, isSuperuser, filters, pagination, sort, profileCache, entityCache, toast], ); // Store fetchItems in ref to avoid re-creating visibility listener @@ -772,13 +789,13 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig): // eslint-disable-next-line react-hooks/exhaustive-deps }, [user?.id]); - // Filter changes trigger refetch + // Filter and sort changes trigger refetch useEffect(() => { if (!user || !initialFetchCompleteRef.current || isMountingRef.current) return; pagination.reset(); fetchItems(true); - }, [filters.debouncedEntityFilter, filters.debouncedStatusFilter]); + }, [filters.debouncedEntityFilter, filters.debouncedStatusFilter, sort.config]); // Pagination changes trigger refetch useEffect(() => { @@ -921,6 +938,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig): actionLoading, filters, pagination, + sort, queue, newItemsCount, pendingNewItems, diff --git a/src/hooks/moderation/useModerationSort.ts b/src/hooks/moderation/useModerationSort.ts new file mode 100644 index 00000000..6b01aef8 --- /dev/null +++ b/src/hooks/moderation/useModerationSort.ts @@ -0,0 +1,104 @@ +import { useState, useCallback, useEffect } from 'react'; +import type { SortConfig, SortField } from '@/types/moderation'; + +const STORAGE_KEY = 'moderationQueue_sortConfig'; + +/** + * Default sort configuration + * Sorts by creation date ascending (oldest first) + */ +const DEFAULT_SORT: SortConfig = { + field: 'created_at', + direction: 'asc' +}; + +/** + * Load sort configuration from localStorage + */ +function loadSortConfig(): SortConfig { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + const parsed = JSON.parse(stored); + // Validate structure + if (parsed.field && parsed.direction) { + return parsed; + } + } + } catch (error) { + console.warn('Failed to load sort config:', error); + } + return DEFAULT_SORT; +} + +/** + * Save sort configuration to localStorage + */ +function saveSortConfig(config: SortConfig): void { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); + } catch (error) { + console.warn('Failed to save sort config:', error); + } +} + +export interface UseModerationSortReturn { + /** Current sort configuration */ + config: SortConfig; + /** Update the sort configuration */ + setConfig: (config: SortConfig) => void; + /** Sort by a specific field, toggling direction if already sorting by that field */ + sortBy: (field: SortField) => void; + /** Toggle the sort direction */ + toggleDirection: () => void; + /** Reset to default sort */ + reset: () => void; +} + +/** + * Hook for managing moderation queue sort state + * + * Provides sort configuration with localStorage persistence + * and convenient methods for updating sort settings. + */ +export function useModerationSort(): UseModerationSortReturn { + const [config, setConfigState] = useState(loadSortConfig); + + // Persist to localStorage whenever config changes + useEffect(() => { + saveSortConfig(config); + }, [config]); + + const setConfig = useCallback((newConfig: SortConfig) => { + setConfigState(newConfig); + }, []); + + const sortBy = useCallback((field: SortField) => { + setConfigState(prev => ({ + field, + // Toggle direction if clicking the same field, otherwise default to ascending + direction: prev.field === field + ? (prev.direction === 'asc' ? 'desc' : 'asc') + : 'asc' + })); + }, []); + + const toggleDirection = useCallback(() => { + setConfigState(prev => ({ + ...prev, + direction: prev.direction === 'asc' ? 'desc' : 'asc' + })); + }, []); + + const reset = useCallback(() => { + setConfigState(DEFAULT_SORT); + }, []); + + return { + config, + setConfig, + sortBy, + toggleDirection, + reset + }; +} diff --git a/src/lib/moderation/queries.ts b/src/lib/moderation/queries.ts index 737b8723..50bc0d18 100644 --- a/src/lib/moderation/queries.ts +++ b/src/lib/moderation/queries.ts @@ -10,6 +10,7 @@ import type { EntityFilter, StatusFilter, QueueTab, + SortConfig, } from '@/types/moderation'; /** @@ -24,6 +25,7 @@ export interface QueryConfig { isSuperuser: boolean; currentPage: number; pageSize: number; + sortConfig?: SortConfig; } /** @@ -73,9 +75,23 @@ export function buildSubmissionQuery( item_data, status ) - `) - .order('escalated', { ascending: false }) - .order('created_at', { ascending: true }); + `); + + // CRITICAL: Multi-level ordering + // Level 1: Always sort by escalated first (descending) - escalated items always appear at top + query = query.order('escalated', { ascending: false }); + + // Level 2: Apply user-selected sort (if provided) + if (config.sortConfig) { + query = query.order(config.sortConfig.field, { + ascending: config.sortConfig.direction === 'asc' + }); + } + + // Level 3: Tertiary sort by created_at as tiebreaker (if not already primary sort) + if (!config.sortConfig || config.sortConfig.field !== 'created_at') { + query = query.order('created_at', { ascending: true }); + } // Apply tab-based status filtering if (tab === 'mainQueue') { diff --git a/src/types/moderation.ts b/src/types/moderation.ts index 33b31b84..a7593e25 100644 --- a/src/types/moderation.ts +++ b/src/types/moderation.ts @@ -99,7 +99,25 @@ export type StatusFilter = 'all' | 'pending' | 'partially_approved' | 'flagged' */ export type QueueTab = 'mainQueue' | 'archive'; -// Removed - sorting functionality deleted +/** + * Available fields for sorting the moderation queue + */ +export type SortField = 'created_at' | 'submission_type' | 'status'; + +/** + * Sort direction + */ +export type SortDirection = 'asc' | 'desc'; + +/** + * Configuration for sorting the moderation queue + */ +export interface SortConfig { + /** Field to sort by */ + field: SortField; + /** Sort direction */ + direction: SortDirection; +} /** * Loading states for the moderation queue