Fix admin dashboard auto-refresh

This commit is contained in:
gpt-engineer-app[bot]
2025-10-06 18:36:43 +00:00
parent b1112e6261
commit f5c59aa072
5 changed files with 176 additions and 10 deletions

View File

@@ -24,6 +24,7 @@ import { Progress } from '@/components/ui/progress';
import { QueueStatsDashboard } from './QueueStatsDashboard'; import { QueueStatsDashboard } from './QueueStatsDashboard';
import { EscalationDialog } from './EscalationDialog'; import { EscalationDialog } from './EscalationDialog';
import { ReassignDialog } from './ReassignDialog'; import { ReassignDialog } from './ReassignDialog';
import { smartMergeArray } from '@/lib/smartStateUpdate';
interface ModerationItem { interface ModerationItem {
id: string; id: string;
@@ -75,15 +76,24 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
const [escalationDialogOpen, setEscalationDialogOpen] = useState(false); const [escalationDialogOpen, setEscalationDialogOpen] = useState(false);
const [reassignDialogOpen, setReassignDialogOpen] = useState(false); const [reassignDialogOpen, setReassignDialogOpen] = useState(false);
const [selectedItemForAction, setSelectedItemForAction] = useState<ModerationItem | null>(null); const [selectedItemForAction, setSelectedItemForAction] = useState<ModerationItem | null>(null);
const [interactingWith, setInteractingWith] = useState<Set<string>>(new Set());
const [newItemsCount, setNewItemsCount] = useState(0);
const { toast } = useToast(); const { toast } = useToast();
const { isAdmin, isSuperuser } = useUserRole(); const { isAdmin, isSuperuser } = useUserRole();
const { user } = useAuth(); const { user } = useAuth();
const queue = useModerationQueue(); const queue = useModerationQueue();
// Get admin settings for polling configuration // Get admin settings for polling configuration
const { getAdminPanelRefreshMode, getAdminPanelPollInterval } = useAdminSettings(); const {
getAdminPanelRefreshMode,
getAdminPanelPollInterval,
getAutoRefreshStrategy,
getPreserveInteractionState
} = useAdminSettings();
const refreshMode = getAdminPanelRefreshMode(); const refreshMode = getAdminPanelRefreshMode();
const pollInterval = getAdminPanelPollInterval(); const pollInterval = getAdminPanelPollInterval();
const refreshStrategy = getAutoRefreshStrategy();
const preserveInteraction = getPreserveInteractionState();
// Expose refresh method via ref // Expose refresh method via ref
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
@@ -346,7 +356,39 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
// Sort by creation date (newest first for better UX) // Sort by creation date (newest first for better UX)
formattedItems.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); formattedItems.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
// Use smart merging for silent refreshes if strategy is 'merge'
if (silent && refreshStrategy === 'merge') {
const mergeResult = smartMergeArray(items, formattedItems, {
compareFields: ['status', 'reviewed_at', 'reviewer_notes'],
preserveOrder: true,
addToTop: true,
});
// If there are changes and we should preserve interaction
if (mergeResult.hasChanges) {
// Filter out items user is interacting with
const protectedIds = preserveInteraction ? interactingWith : new Set<string>();
const mergedWithProtection = mergeResult.items.map(item => {
if (protectedIds.has(item.id)) {
// Find and preserve the current version of this item
const currentItem = items.find(i => i.id === item.id);
return currentItem || item;
}
return item;
});
setItems(mergedWithProtection);
// Update new items count
if (mergeResult.changes.added.length > 0) {
setNewItemsCount(prev => prev + mergeResult.changes.added.length);
}
}
} else {
// Full replacement for non-silent refreshes or 'replace' strategy
setItems(formattedItems); setItems(formattedItems);
setNewItemsCount(0);
}
} catch (error: any) { } catch (error: any) {
console.error('Error fetching moderation items:', error); console.error('Error fetching moderation items:', error);
console.error('Error details:', { console.error('Error details:', {
@@ -1985,6 +2027,24 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
</div> </div>
)} )}
{/* New Items Notification */}
{newItemsCount > 0 && (
<div className="flex items-center justify-center">
<Button
variant="outline"
size="sm"
onClick={() => {
setNewItemsCount(0);
fetchItems(activeEntityFilter, activeStatusFilter, false);
}}
className="flex items-center gap-2 border-primary/50 bg-primary/5 hover:bg-primary/10"
>
<RefreshCw className="w-4 h-4" />
Show {newItemsCount} new {newItemsCount === 1 ? 'item' : 'items'}
</Button>
</div>
)}
{/* Queue Content */} {/* Queue Content */}
<QueueContent /> <QueueContent />

View File

@@ -5,6 +5,8 @@ import { useToast } from '@/hooks/use-toast';
import { ActivityCard } from './ActivityCard'; import { ActivityCard } from './ActivityCard';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { Activity as ActivityIcon } from 'lucide-react'; import { Activity as ActivityIcon } from 'lucide-react';
import { smartMergeArray } from '@/lib/smartStateUpdate';
import { useAdminSettings } from '@/hooks/useAdminSettings';
interface ActivityItem { interface ActivityItem {
id: string; id: string;
@@ -28,18 +30,25 @@ export interface RecentActivityRef {
export const RecentActivity = forwardRef<RecentActivityRef>((props, ref) => { export const RecentActivity = forwardRef<RecentActivityRef>((props, ref) => {
const [activities, setActivities] = useState<ActivityItem[]>([]); const [activities, setActivities] = useState<ActivityItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isSilentRefresh, setIsSilentRefresh] = useState(false);
const { user } = useAuth(); const { user } = useAuth();
const { toast } = useToast(); const { toast } = useToast();
const { getAutoRefreshStrategy } = useAdminSettings();
const refreshStrategy = getAutoRefreshStrategy();
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
refresh: fetchRecentActivity refresh: () => fetchRecentActivity(false)
})); }));
const fetchRecentActivity = async () => { const fetchRecentActivity = async (silent = false) => {
if (!user) return; if (!user) return;
try { try {
if (!silent) {
setLoading(true); setLoading(true);
} else {
setIsSilentRefresh(true);
}
// Fetch recent approved/rejected submissions // Fetch recent approved/rejected submissions
const { data: submissions, error: submissionsError } = await supabase const { data: submissions, error: submissionsError } = await supabase
@@ -124,7 +133,23 @@ export const RecentActivity = forwardRef<RecentActivityRef>((props, ref) => {
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
); );
setActivities(allActivities.slice(0, 20)); // Keep top 20 most recent const recentActivities = allActivities.slice(0, 20); // Keep top 20 most recent
// Use smart merging for silent refreshes if strategy is 'merge'
if (silent && refreshStrategy === 'merge') {
const mergeResult = smartMergeArray(activities, recentActivities, {
compareFields: ['timestamp', 'action'],
preserveOrder: false,
addToTop: true,
});
if (mergeResult.hasChanges) {
setActivities(mergeResult.items);
}
} else {
// Full replacement for non-silent refreshes or 'replace' strategy
setActivities(recentActivities);
}
} catch (error: any) { } catch (error: any) {
console.error('Error fetching recent activity:', error); console.error('Error fetching recent activity:', error);
toast({ toast({
@@ -133,12 +158,15 @@ export const RecentActivity = forwardRef<RecentActivityRef>((props, ref) => {
variant: "destructive", variant: "destructive",
}); });
} finally { } finally {
if (!silent) {
setLoading(false); setLoading(false);
} }
setIsSilentRefresh(false);
}
}; };
useEffect(() => { useEffect(() => {
fetchRecentActivity(); fetchRecentActivity(false);
}, [user]); }, [user]);
if (loading) { if (loading) {

View File

@@ -10,6 +10,7 @@ import { useToast } from '@/hooks/use-toast';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { useAdminSettings } from '@/hooks/useAdminSettings'; import { useAdminSettings } from '@/hooks/useAdminSettings';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { smartMergeArray } from '@/lib/smartStateUpdate';
interface Report { interface Report {
id: string; id: string;
@@ -49,13 +50,19 @@ export const ReportsQueue = forwardRef<ReportsQueueRef>((props, ref) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isInitialLoad, setIsInitialLoad] = useState(true); const [isInitialLoad, setIsInitialLoad] = useState(true);
const [actionLoading, setActionLoading] = useState<string | null>(null); const [actionLoading, setActionLoading] = useState<string | null>(null);
const [newReportsCount, setNewReportsCount] = useState(0);
const { toast } = useToast(); const { toast } = useToast();
const { user } = useAuth(); const { user } = useAuth();
// Get admin settings for polling configuration // Get admin settings for polling configuration
const { getAdminPanelRefreshMode, getAdminPanelPollInterval } = useAdminSettings(); const {
getAdminPanelRefreshMode,
getAdminPanelPollInterval,
getAutoRefreshStrategy
} = useAdminSettings();
const refreshMode = getAdminPanelRefreshMode(); const refreshMode = getAdminPanelRefreshMode();
const pollInterval = getAdminPanelPollInterval(); const pollInterval = getAdminPanelPollInterval();
const refreshStrategy = getAutoRefreshStrategy();
// Expose refresh method via ref // Expose refresh method via ref
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
@@ -120,7 +127,27 @@ export const ReportsQueue = forwardRef<ReportsQueueRef>((props, ref) => {
}) })
); );
// Use smart merging for silent refreshes if strategy is 'merge'
if (silent && refreshStrategy === 'merge') {
const mergeResult = smartMergeArray(reports, reportsWithContent, {
compareFields: ['status'],
preserveOrder: true,
addToTop: true,
});
if (mergeResult.hasChanges) {
setReports(mergeResult.items);
// Update new reports count
if (mergeResult.changes.added.length > 0) {
setNewReportsCount(prev => prev + mergeResult.changes.added.length);
}
}
} else {
// Full replacement for non-silent refreshes or 'replace' strategy
setReports(reportsWithContent); setReports(reportsWithContent);
setNewReportsCount(0);
}
} catch (error) { } catch (error) {
console.error('Error fetching reports:', error); console.error('Error fetching reports:', error);
toast({ toast({
@@ -213,6 +240,24 @@ export const ReportsQueue = forwardRef<ReportsQueueRef>((props, ref) => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* New Reports Notification */}
{newReportsCount > 0 && (
<div className="flex items-center justify-center">
<Button
variant="outline"
size="sm"
onClick={() => {
setNewReportsCount(0);
fetchReports(false);
}}
className="flex items-center gap-2 border-destructive/50 bg-destructive/5 hover:bg-destructive/10"
>
<Flag className="w-4 h-4" />
Show {newReportsCount} new {newReportsCount === 1 ? 'report' : 'reports'}
</Button>
</div>
)}
{reports.map((report) => ( {reports.map((report) => (
<Card key={report.id} className="border-l-4 border-l-red-500"> <Card key={report.id} className="border-l-4 border-l-red-500">
<CardHeader className="pb-4"> <CardHeader className="pb-4">

View File

@@ -126,6 +126,30 @@ export function useAdminSettings() {
return parseInt(value?.toString() || '30') * 1000; // Convert to milliseconds return parseInt(value?.toString() || '30') * 1000; // Convert to milliseconds
}; };
/**
* Get auto-refresh strategy setting
* Returns: 'merge' | 'replace' | 'notify'
*/
const getAutoRefreshStrategy = (): 'merge' | 'replace' | 'notify' => {
const value = getSettingValue('auto_refresh_strategy', 'merge');
const cleanValue = typeof value === 'string' ? value.replace(/"/g, '') : value;
return cleanValue as 'merge' | 'replace' | 'notify';
};
/**
* Get preserve interaction state setting
* Returns: boolean
*/
const getPreserveInteractionState = (): boolean => {
const value = getSettingValue('preserve_interaction_state', 'true');
const cleanValue = typeof value === 'string' ? value.replace(/"/g, '') : value;
return cleanValue === 'true' || cleanValue === true;
};
const getNotificationRecipients = () => {
return getSettingValue('notifications.recipients', []);
};
return { return {
settings, settings,
isLoading, isLoading,
@@ -140,10 +164,13 @@ export function useAdminSettings() {
getRequireApproval, getRequireApproval,
getBanDurations, getBanDurations,
getEmailAlertsEnabled, getEmailAlertsEnabled,
getNotificationRecipients,
getReportThreshold, getReportThreshold,
getAuditRetentionDays, getAuditRetentionDays,
getAutoCleanupEnabled, getAutoCleanupEnabled,
getAdminPanelRefreshMode, getAdminPanelRefreshMode,
getAdminPanelPollInterval, getAdminPanelPollInterval,
getAutoRefreshStrategy,
getPreserveInteractionState,
}; };
} }

View File

@@ -0,0 +1,6 @@
-- Add admin settings for non-disruptive auto-refresh
INSERT INTO admin_settings (setting_key, setting_value, category, description)
VALUES
('auto_refresh_strategy', '"merge"', 'admin_panel', 'Auto-refresh strategy: merge (silently merge new items), replace (current behavior - full replacement), notify (show notification only)'),
('preserve_interaction_state', '"true"', 'admin_panel', 'Whether to preserve user interaction state during auto-refresh (expanded items, typing in notes, etc.)')
ON CONFLICT (setting_key) DO NOTHING;