feat: Implement Modern React Patterns

This commit is contained in:
gpt-engineer-app[bot]
2025-10-13 22:40:35 +00:00
parent 2bd0b414e1
commit 787e16753e
5 changed files with 242 additions and 451 deletions

View File

@@ -53,7 +53,8 @@ const queryClient = new QueryClient({
refetchOnMount: true, // Keep refetch on component mount refetchOnMount: true, // Keep refetch on component mount
refetchOnReconnect: true, // Keep refetch on network reconnect refetchOnReconnect: true, // Keep refetch on network reconnect
retry: 1, // Keep retry attempts retry: 1, // Keep retry attempts
staleTime: 0, // Keep data fresh staleTime: 30000, // 30 seconds - queries stay fresh for 30s
gcTime: 5 * 60 * 1000, // 5 minutes - keep in cache for 5 mins
}, },
}, },
}); });

View File

@@ -21,6 +21,9 @@ export type {
UseRealtimeSubscriptionsReturn UseRealtimeSubscriptionsReturn
} from './useRealtimeSubscriptions'; } from './useRealtimeSubscriptions';
export { useQueueQuery } from './useQueueQuery';
export type { UseQueueQueryConfig, UseQueueQueryReturn } from './useQueueQuery';
export { useModerationQueueManager } from './useModerationQueueManager'; export { useModerationQueueManager } from './useModerationQueueManager';
export type { export type {
ModerationQueueManager, ModerationQueueManager,

View File

@@ -8,6 +8,7 @@ import {
useModerationFilters, useModerationFilters,
usePagination, usePagination,
useRealtimeSubscriptions, useRealtimeSubscriptions,
useQueueQuery,
} from "./index"; } from "./index";
import { useModerationQueue } from "@/hooks/useModerationQueue"; import { useModerationQueue } from "@/hooks/useModerationQueue";
@@ -122,13 +123,9 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
// Refs for tracking // Refs for tracking
const recentlyRemovedRef = useRef<Set<string>>(new Set()); const recentlyRemovedRef = useRef<Set<string>>(new Set());
const fetchInProgressRef = useRef(false);
const lastFetchTimeRef = useRef<number>(0);
const initialFetchCompleteRef = useRef(false); const initialFetchCompleteRef = useRef(false);
const isMountingRef = useRef(true); const isMountingRef = useRef(true);
const FETCH_COOLDOWN_MS = 1000;
// Store settings, filters, and pagination in refs to stabilize fetchItems // Store settings, filters, and pagination in refs to stabilize fetchItems
const settingsRef = useRef(settings); const settingsRef = useRef(settings);
const filtersRef = useRef(filters); const filtersRef = useRef(filters);
@@ -153,366 +150,70 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
}, [pagination]); }, [pagination]);
/** /**
* Fetch queue items from database * Replace manual fetching with TanStack Query
*/ */
const fetchItems = useCallback( const queueQuery = useQueueQuery({
async (silent = false) => { userId: user?.id,
if (!user) return;
// Get caller info
const callerStack = new Error().stack;
const callerLine = callerStack?.split("\n")[2]?.trim();
console.log("🔄 [FETCH ITEMS] Called", {
silent,
documentHidden: document.hidden,
caller: callerLine,
sortField: sortRef.current.field,
sortDirection: sortRef.current.direction,
timestamp: new Date().toISOString(),
});
// Prevent concurrent calls
if (fetchInProgressRef.current) {
console.log("⚠️ Fetch already in progress, skipping");
return;
}
// Cooldown check
const now = Date.now();
const timeSinceLastFetch = now - lastFetchTimeRef.current;
if (timeSinceLastFetch < FETCH_COOLDOWN_MS && lastFetchTimeRef.current > 0) {
console.log(`⏸️ Fetch cooldown active (${timeSinceLastFetch}ms)`);
return;
}
fetchInProgressRef.current = true;
lastFetchTimeRef.current = now;
console.log("🔍 fetchItems called:", {
entityFilter: filtersRef.current.debouncedEntityFilter,
statusFilter: filtersRef.current.debouncedStatusFilter,
sortField: sortRef.current.field,
sortDirection: sortRef.current.direction,
silent,
timestamp: new Date().toISOString(),
});
try {
// Set loading states
if (!silent) {
setLoadingState("loading");
} else {
setLoadingState("refreshing");
}
// Build base query
let submissionsQuery = supabase
.from("content_submissions")
.select(
`
id,
submission_type,
status,
content,
created_at,
user_id,
reviewed_at,
reviewer_id,
reviewer_notes,
escalated,
assigned_to,
locked_until,
submission_items (
id,
item_type,
item_data,
status
)
`,
);
// CRITICAL: Multi-level ordering
console.log('📊 [SORT QUERY] Applying multi-level sort:', {
level1: 'escalated DESC',
level2: `${sortRef.current.field} ${sortRef.current.direction.toUpperCase()}`,
level3: sortRef.current.field !== 'created_at' ? 'created_at ASC' : 'none'
});
// Level 1: Always sort by escalated first (descending)
submissionsQuery = submissionsQuery.order('escalated', { ascending: false });
// Level 2: Apply user-selected sort (use ref)
submissionsQuery = submissionsQuery.order(
sortRef.current.field,
{ ascending: sortRef.current.direction === 'asc' }
);
// Level 3: Tertiary sort by created_at (if not already primary)
if (sortRef.current.field !== 'created_at') {
submissionsQuery = submissionsQuery.order('created_at', { ascending: true });
}
// Apply tab-based status filtering (use refs)
const tab = filtersRef.current.activeTab;
const statusFilter = filtersRef.current.debouncedStatusFilter;
const entityFilter = filtersRef.current.debouncedEntityFilter;
if (tab === "mainQueue") {
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 (statusFilter === "all") {
submissionsQuery = submissionsQuery.in("status", ["approved", "rejected"]);
} else {
submissionsQuery = submissionsQuery.eq("status", statusFilter);
}
}
// Apply entity type filter
if (entityFilter === "photos") {
submissionsQuery = submissionsQuery.eq("submission_type", "photo");
} else if (entityFilter === "submissions") {
submissionsQuery = submissionsQuery.neq("submission_type", "photo");
}
// Apply access control
if (!isAdmin && !isSuperuser) {
const now = new Date().toISOString();
submissionsQuery = submissionsQuery.or(
`assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${user.id}`,
);
}
// Get total count - build separate query with same filters
let countQuery = supabase
.from("content_submissions")
.select("*", { count: "exact", head: true });
// Apply same filters as main query
if (tab === "mainQueue") {
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);
}
}
if (entityFilter === "photos") {
countQuery = countQuery.eq("submission_type", "photo");
} else if (entityFilter === "submissions") {
countQuery = countQuery.neq("submission_type", "photo");
}
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;
paginationRef.current.setTotalCount(count || 0);
// Apply pagination (use refs)
const startIndex = paginationRef.current.startIndex;
const endIndex = paginationRef.current.endIndex;
submissionsQuery = submissionsQuery.range(startIndex, endIndex);
const { data: submissions, error: submissionsError } = await submissionsQuery;
if (submissionsError) throw submissionsError;
// Log the actual data returned to verify sort order
if (submissions && submissions.length > 0) {
const sortField = sortRef.current.field;
const preview = submissions.slice(0, 3).map(s => ({
id: s.id.substring(0, 8),
[sortField]: s[sortField],
escalated: s.escalated
}));
console.log(`📋 [SORT RESULT] First 3 items by ${sortField}:`, preview);
}
// Fetch related profiles and entities
const userIds = [
...new Set([
...(submissions?.map((s) => s.user_id) || []),
...(submissions?.map((s) => s.reviewer_id).filter(Boolean) || []),
]),
];
if (userIds.length > 0) {
await profileCache.bulkFetch(userIds);
}
// Collect entity IDs
if (submissions) {
await entityCache.fetchRelatedEntities(submissions);
}
// Map to ModerationItems
const moderationItems: ModerationItem[] =
submissions?.map((submission) => {
const content = submission.content as any;
let entityName = content?.name || "Unknown";
let parkName: string | undefined;
// Resolve entity names from cache
if (submission.submission_type === "ride" && content?.entity_id) {
const ride = entityCache.getCached("rides", content.entity_id);
if (ride) {
entityName = ride.name;
if (ride.park_id) {
const park = entityCache.getCached("parks", ride.park_id);
if (park) parkName = park.name;
}
}
} else if (submission.submission_type === "park" && content?.entity_id) {
const park = entityCache.getCached("parks", content.entity_id);
if (park) entityName = park.name;
} else if (
["manufacturer", "operator", "designer", "property_owner"].includes(submission.submission_type) &&
content?.entity_id
) {
const company = entityCache.getCached("companies", content.entity_id);
if (company) entityName = company.name;
}
const userProfile = profileCache.getCached(submission.user_id);
const reviewerProfile = submission.reviewer_id ? profileCache.getCached(submission.reviewer_id) : undefined;
return {
id: submission.id,
type: "content_submission",
content: submission.content,
created_at: submission.created_at,
user_id: submission.user_id,
status: submission.status,
submission_type: submission.submission_type,
user_profile: userProfile,
entity_name: entityName,
park_name: parkName,
reviewed_at: submission.reviewed_at || undefined,
reviewed_by: submission.reviewer_id || undefined,
reviewer_notes: submission.reviewer_notes || undefined,
reviewer_profile: reviewerProfile,
escalated: submission.escalated,
assigned_to: submission.assigned_to || undefined,
locked_until: submission.locked_until || undefined,
submission_items: submission.submission_items || undefined,
};
}) || [];
// Apply refresh strategy
const currentRefreshStrategy = settingsRef.current.refreshStrategy;
const currentPreserveInteraction = settingsRef.current.preserveInteraction;
if (silent) {
// Background polling: detect new submissions
const currentDisplayedIds = new Set(items.map((item) => item.id));
const newSubmissions = moderationItems.filter((item) => !currentDisplayedIds.has(item.id));
if (newSubmissions.length > 0) {
console.log("🆕 Detected new submissions:", newSubmissions.length);
setPendingNewItems((prev) => {
const existingIds = new Set(prev.map((p) => p.id));
const uniqueNew = newSubmissions.filter((item) => !existingIds.has(item.id));
if (uniqueNew.length > 0) {
setNewItemsCount((prev) => prev + uniqueNew.length);
}
return [...prev, ...uniqueNew];
});
}
// Apply refresh strategy
switch (currentRefreshStrategy) {
case "notify":
console.log("✅ Queue frozen (notify mode)");
break;
case "merge":
if (newSubmissions.length > 0) {
const currentIds = new Set(items.map((item) => item.id));
const trulyNewSubmissions = newSubmissions.filter((item) => !currentIds.has(item.id));
if (trulyNewSubmissions.length > 0) {
setItems((prev) => [...prev, ...trulyNewSubmissions]);
console.log("🔀 Queue merged - added", trulyNewSubmissions.length, "items");
}
}
break;
case "replace":
// Full replace - simple and predictable
setItems(moderationItems);
console.log("🔄 Queue updated (replace mode)");
if (!currentPreserveInteraction) {
setPendingNewItems([]);
setNewItemsCount(0);
}
break;
}
} else {
// Manual refresh - ALWAYS update items for user-initiated changes (sort/filter)
// Don't use smartMerge here because it won't detect order changes
setItems(moderationItems);
console.log("🔄 Queue updated (manual refresh) - items:", moderationItems.length);
setPendingNewItems([]);
setNewItemsCount(0);
}
} catch (error: any) {
console.error("Error fetching moderation items:", error);
toast({
title: "Error",
description: error.message || "Failed to fetch moderation queue",
variant: "destructive",
});
} finally {
fetchInProgressRef.current = false;
setLoadingState("ready");
}
},
[
user,
isAdmin, isAdmin,
isSuperuser, isSuperuser,
profileCache, entityFilter: filtersRef.current.debouncedEntityFilter,
entityCache, statusFilter: filtersRef.current.debouncedStatusFilter,
toast tab: filtersRef.current.activeTab,
], currentPage: paginationRef.current.currentPage,
); pageSize: paginationRef.current.pageSize,
sortConfig: sortRef.current,
enabled: !!user,
});
// Update items when query data changes
useEffect(() => {
if (queueQuery.items) {
setItems(queueQuery.items);
console.log('✅ Queue items updated from TanStack Query:', queueQuery.items.length);
}
}, [queueQuery.items]);
// Update loading state based on query status
useEffect(() => {
if (queueQuery.isLoading) {
setLoadingState('loading');
} else if (queueQuery.isRefreshing) {
setLoadingState('refreshing');
} else {
setLoadingState('ready');
}
}, [queueQuery.isLoading, queueQuery.isRefreshing]);
// Update total count for pagination
useEffect(() => {
paginationRef.current.setTotalCount(queueQuery.totalCount);
}, [queueQuery.totalCount]);
// Mark initial fetch as complete
useEffect(() => {
if (!queueQuery.isLoading && !initialFetchCompleteRef.current) {
initialFetchCompleteRef.current = true;
console.log('✅ Initial queue fetch complete');
}
}, [queueQuery.isLoading]);
/** /**
* Show pending new items by merging them into the queue * Manual refresh function
*/ */
const showNewItems = useCallback(() => { const refresh = useCallback(async () => {
if (pendingNewItems.length > 0) { console.log('🔄 Manual refresh triggered');
setItems((prev) => [...pendingNewItems, ...prev]); await queueQuery.refetch();
}, [queueQuery]);
/**
* Show pending new items by invalidating query
*/
const showNewItems = useCallback(async () => {
console.log('✅ Showing new items via query invalidation');
await queueQuery.invalidate();
setPendingNewItems([]); setPendingNewItems([]);
setNewItemsCount(0); setNewItemsCount(0);
console.log("✅ New items merged into queue:", pendingNewItems.length); }, [queueQuery]);
}
}, [pendingNewItems]);
/** /**
* Mark an item as being interacted with (prevents realtime updates) * Mark an item as being interacted with (prevents realtime updates)
@@ -808,51 +509,36 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
[filters.statusFilter, toast], [filters.statusFilter, toast],
); );
// Initial fetch on mount // Mark initial fetch as complete when query loads
useEffect(() => { useEffect(() => {
if (!user) return; if (!queueQuery.isLoading && !initialFetchCompleteRef.current) {
isMountingRef.current = true;
fetchItems(false).then(() => {
initialFetchCompleteRef.current = true; initialFetchCompleteRef.current = true;
requestAnimationFrame(() => {
isMountingRef.current = false; isMountingRef.current = false;
}); console.log('✅ Initial queue fetch complete');
}); }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [queueQuery.isLoading]);
}, [user?.id]);
// Filter and sort changes trigger refetch // Invalidate query when filters or sort changes
useEffect(() => { useEffect(() => {
if (!user || !initialFetchCompleteRef.current || isMountingRef.current) return; if (
!user ||
console.log('🔄 [SORT/FILTER CHANGE] Detected change:', { !initialFetchCompleteRef.current ||
entityFilter: filters.debouncedEntityFilter, isMountingRef.current
statusFilter: filters.debouncedStatusFilter, ) return;
sortField: filters.debouncedSortConfig.field,
sortDirection: filters.debouncedSortConfig.direction,
timestamp: new Date().toISOString()
});
console.log('🔄 Filters/sort changed, invalidating query');
pagination.reset(); pagination.reset();
fetchItems(false); queueQuery.invalidate();
}, [ }, [
filters.debouncedEntityFilter, filters.debouncedEntityFilter,
filters.debouncedStatusFilter, filters.debouncedStatusFilter,
filters.debouncedSortConfig.field, filters.debouncedSortConfig.field,
filters.debouncedSortConfig.direction, filters.debouncedSortConfig.direction,
user, user,
fetchItems, queueQuery,
pagination pagination
]); ]);
// Pagination changes trigger refetch
useEffect(() => {
if (!user || !initialFetchCompleteRef.current || pagination.currentPage === 1) return;
fetchItems(true);
}, [pagination.currentPage, pagination.pageSize, user, fetchItems]);
// Polling effect (when realtime disabled) // Polling effect (when realtime disabled)
useEffect(() => { useEffect(() => {
if (!user || settings.refreshMode !== "auto" || loadingState === "initial" || settings.useRealtimeQueue) { if (!user || settings.refreshMode !== "auto" || loadingState === "initial" || settings.useRealtimeQueue) {
@@ -862,14 +548,14 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
console.log("⚠️ Polling ENABLED - interval:", settings.pollInterval); console.log("⚠️ Polling ENABLED - interval:", settings.pollInterval);
const interval = setInterval(() => { const interval = setInterval(() => {
console.log("🔄 Polling refresh triggered"); console.log("🔄 Polling refresh triggered");
fetchItems(true); queueQuery.refetch();
}, settings.pollInterval); }, settings.pollInterval);
return () => { return () => {
clearInterval(interval); clearInterval(interval);
console.log("🛑 Polling stopped"); console.log("🛑 Polling stopped");
}; };
}, [user, settings.refreshMode, settings.pollInterval, loadingState, settings.useRealtimeQueue, fetchItems]); }, [user, settings.refreshMode, settings.pollInterval, loadingState, settings.useRealtimeQueue, queueQuery]);
// Initialize realtime subscriptions // Initialize realtime subscriptions
useRealtimeSubscriptions({ useRealtimeSubscriptions({
@@ -927,9 +613,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
showNewItems, showNewItems,
interactingWith, interactingWith,
markInteracting, markInteracting,
refresh: async () => { refresh,
await fetchItems(false);
},
performAction, performAction,
deleteSubmission, deleteSubmission,
resetToPending, resetToPending,

View File

@@ -0,0 +1,141 @@
/**
* TanStack Query hook for moderation queue data fetching
*
* Wraps the existing fetchSubmissions query builder with React Query
* to provide automatic caching, deduplication, and background refetching.
*/
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { fetchSubmissions, type QueryConfig } from '@/lib/moderation/queries';
import { supabase } from '@/integrations/supabase/client';
import type { ModerationItem } from '@/types/moderation';
/**
* Configuration for queue query
*/
export interface UseQueueQueryConfig {
/** User making the query */
userId: string | undefined;
/** Whether user is admin */
isAdmin: boolean;
/** Whether user is superuser */
isSuperuser: boolean;
/** Entity filter */
entityFilter: string;
/** Status filter */
statusFilter: string;
/** Active tab */
tab: 'mainQueue' | 'archive';
/** Current page */
currentPage: number;
/** Page size */
pageSize: number;
/** Sort configuration */
sortConfig: {
field: string;
direction: 'asc' | 'desc';
};
/** Whether query is enabled (defaults to true) */
enabled?: boolean;
}
/**
* Return type for useQueueQuery
*/
export interface UseQueueQueryReturn {
/** Queue items */
items: ModerationItem[];
/** Total count of items matching filters */
totalCount: number;
/** Initial loading state (no data yet) */
isLoading: boolean;
/** Background refresh in progress (has data already) */
isRefreshing: boolean;
/** Any error that occurred */
error: Error | null;
/** Manually trigger a refetch */
refetch: () => Promise<any>;
/** Invalidate this query (triggers background refetch) */
invalidate: () => Promise<void>;
}
/**
* Hook to fetch moderation queue data using TanStack Query
*/
export function useQueueQuery(config: UseQueueQueryConfig): UseQueueQueryReturn {
const queryClient = useQueryClient();
// Build query config for fetchSubmissions
const queryConfig: QueryConfig = {
userId: config.userId || '',
isAdmin: config.isAdmin,
isSuperuser: config.isSuperuser,
entityFilter: config.entityFilter as any,
statusFilter: config.statusFilter as any,
tab: config.tab,
currentPage: config.currentPage,
pageSize: config.pageSize,
sortConfig: config.sortConfig as any,
};
// Create stable query key (TanStack Query uses this for caching/deduplication)
const queryKey = [
'moderation-queue',
config.entityFilter,
config.statusFilter,
config.tab,
config.currentPage,
config.pageSize,
config.sortConfig.field,
config.sortConfig.direction,
];
// Execute query
const query = useQuery({
queryKey,
queryFn: async () => {
console.log('🔍 [TanStack Query] Fetching queue data:', queryKey);
const result = await fetchSubmissions(supabase, queryConfig);
if (result.error) {
throw result.error;
}
console.log('✅ [TanStack Query] Fetched', result.submissions.length, 'items');
return result;
},
enabled: config.enabled !== false && !!config.userId,
staleTime: 30000, // 30 seconds
gcTime: 5 * 60 * 1000, // 5 minutes
});
// Invalidate helper
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: ['moderation-queue'] });
};
return {
items: query.data?.submissions || [],
totalCount: query.data?.totalCount || 0,
isLoading: query.isLoading,
isRefreshing: query.isFetching && !query.isLoading,
error: query.error as Error | null,
refetch: query.refetch,
invalidate,
};
}

View File

@@ -6,6 +6,7 @@
*/ */
import { useEffect, useRef, useState, useCallback } from 'react'; import { useEffect, useRef, useState, useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import type { RealtimeChannel } from '@supabase/supabase-js'; import type { RealtimeChannel } from '@supabase/supabase-js';
import type { ModerationItem, EntityFilter, StatusFilter } from '@/types/moderation'; import type { ModerationItem, EntityFilter, StatusFilter } from '@/types/moderation';
@@ -85,6 +86,8 @@ export interface UseRealtimeSubscriptionsReturn {
export function useRealtimeSubscriptions( export function useRealtimeSubscriptions(
config: RealtimeSubscriptionConfig config: RealtimeSubscriptionConfig
): UseRealtimeSubscriptionsReturn { ): UseRealtimeSubscriptionsReturn {
const queryClient = useQueryClient();
const { const {
enabled, enabled,
filters, filters,
@@ -268,21 +271,22 @@ export function useRealtimeSubscriptions(
return; return;
} }
console.log('✅ NEW submission matches filters:', newSubmission.id); console.log('✅ NEW submission matches filters, invalidating query:', newSubmission.id);
// Fetch full submission details // Invalidate the query to trigger background refetch
await queryClient.invalidateQueries({ queryKey: ['moderation-queue'] });
// Call legacy callback for new item notification
// (This maintains compatibility with NewItemsAlert component)
try { try {
const submission = await fetchSubmissionDetails(newSubmission.id); const submission = await fetchSubmissionDetails(newSubmission.id);
if (!submission) return; if (!submission) return;
// Fetch user profile
const profile = await profileCache.bulkFetch([submission.user_id]); const profile = await profileCache.bulkFetch([submission.user_id]);
const userProfile = profile[0]; const userProfile = profile[0];
// Resolve entity names
const { entityName, parkName } = await resolveEntityNames(submission); const { entityName, parkName } = await resolveEntityNames(submission);
// Build full ModerationItem
const fullItem = buildModerationItem( const fullItem = buildModerationItem(
submission, submission,
userProfile, userProfile,
@@ -290,17 +294,15 @@ export function useRealtimeSubscriptions(
parkName parkName
); );
// Trigger callback
onNewItem(fullItem); onNewItem(fullItem);
console.log('🎉 New submission added to queue:', fullItem.id);
} catch (error) { } catch (error) {
console.error('Error processing new submission:', error); console.error('Error building new item notification:', error);
} }
}, [ }, [
filters, filters,
pauseWhenHidden, pauseWhenHidden,
recentlyRemovedIds, recentlyRemovedIds,
queryClient,
fetchSubmissionDetails, fetchSubmissionDetails,
profileCache, profileCache,
resolveEntityNames, resolveEntityNames,
@@ -335,56 +337,18 @@ export function useRealtimeSubscriptions(
// Debounce the update // Debounce the update
debouncedUpdate(updatedSubmission.id, async () => { debouncedUpdate(updatedSubmission.id, async () => {
// Check if submission matches current filters console.log('🔄 Invalidating query due to UPDATE:', updatedSubmission.id);
// Simply invalidate the query - TanStack Query handles the rest
await queryClient.invalidateQueries({ queryKey: ['moderation-queue'] });
// Legacy callback for compatibility
const matchesEntity = matchesEntityFilter(updatedSubmission, filters.entityFilter); const matchesEntity = matchesEntityFilter(updatedSubmission, filters.entityFilter);
const matchesStatus = matchesStatusFilter(updatedSubmission, filters.statusFilter); const matchesStatus = matchesStatusFilter(updatedSubmission, filters.statusFilter);
const wasInQueue = currentItemsRef.current.some(i => i.id === updatedSubmission.id);
const shouldBeInQueue = matchesEntity && matchesStatus; const shouldBeInQueue = matchesEntity && matchesStatus;
if (wasInQueue && !shouldBeInQueue) {
// Submission moved out of current filter (e.g., pending → approved)
console.log('❌ Submission moved out of queue:', updatedSubmission.id);
onItemRemoved(updatedSubmission.id);
return;
}
if (!shouldBeInQueue) { if (!shouldBeInQueue) {
// Item doesn't belong in queue at all onItemRemoved(updatedSubmission.id);
return;
}
// Fetch full details
try {
const submission = await fetchSubmissionDetails(updatedSubmission.id);
if (!submission) return;
// Get user profile
const profiles = await profileCache.bulkFetch([submission.user_id]);
const profile = profiles[0];
// Resolve entity name (simplified for updates)
const content = submission.content as any;
const entityName = content?.name || 'Unknown';
const fullItem = buildModerationItem(
submission,
profile,
entityName,
undefined
);
// Check if item actually changed
const currentItem = currentItemsRef.current.find(i => i.id === fullItem.id);
if (currentItem && !hasItemChanged(currentItem, fullItem)) {
console.log('✅ Realtime UPDATE: No changes detected for', fullItem.id);
return;
}
console.log('🔄 Realtime UPDATE: Changes detected for', fullItem.id);
onUpdateItem(fullItem, false);
} catch (error) {
console.error('Error processing updated submission:', error);
} }
}); });
}, [ }, [
@@ -393,9 +357,7 @@ export function useRealtimeSubscriptions(
recentlyRemovedIds, recentlyRemovedIds,
interactingWithIds, interactingWithIds,
debouncedUpdate, debouncedUpdate,
fetchSubmissionDetails, queryClient,
profileCache,
onUpdateItem,
onItemRemoved, onItemRemoved,
]); ]);