mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 14:31:11 -05:00
Implement persistent lock state
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useImperativeHandle, forwardRef, useMemo, useCallback, useRef, useEffect } from 'react';
|
import { useState, useImperativeHandle, forwardRef, useMemo, useCallback, useRef, useEffect } from 'react';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { AlertCircle } from 'lucide-react';
|
import { AlertCircle, Info } from 'lucide-react';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||||
@@ -88,6 +88,8 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
|
|||||||
const [bulkEditMode, setBulkEditMode] = useState(false);
|
const [bulkEditMode, setBulkEditMode] = useState(false);
|
||||||
const [bulkEditItems, setBulkEditItems] = useState<SubmissionItemWithDeps[]>([]);
|
const [bulkEditItems, setBulkEditItems] = useState<SubmissionItemWithDeps[]>([]);
|
||||||
const [activeLocksCount, setActiveLocksCount] = useState(0);
|
const [activeLocksCount, setActiveLocksCount] = useState(0);
|
||||||
|
const [lockRestored, setLockRestored] = useState(false);
|
||||||
|
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
|
||||||
|
|
||||||
// Confirmation dialog state
|
// Confirmation dialog state
|
||||||
const [confirmDialog, setConfirmDialog] = useState<{
|
const [confirmDialog, setConfirmDialog] = useState<{
|
||||||
@@ -153,6 +155,19 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [isSuperuser, queueManager.queue.queueStats]);
|
}, [isSuperuser, queueManager.queue.queueStats]);
|
||||||
|
|
||||||
|
// Track if lock was restored from database
|
||||||
|
useEffect(() => {
|
||||||
|
if (!initialLoadComplete) {
|
||||||
|
setInitialLoadComplete(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queueManager.queue.currentLock && !lockRestored) {
|
||||||
|
// If we have a lock after initial load but haven't claimed in this session
|
||||||
|
setLockRestored(true);
|
||||||
|
}
|
||||||
|
}, [queueManager.queue.currentLock, lockRestored, initialLoadComplete]);
|
||||||
|
|
||||||
// Virtual scrolling setup
|
// Virtual scrolling setup
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
@@ -359,6 +374,17 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Lock Restored Alert */}
|
||||||
|
{lockRestored && queueManager.queue.currentLock && (
|
||||||
|
<Alert className="border-blue-500/50 bg-blue-500/5">
|
||||||
|
<Info className="h-4 w-4 text-blue-600" />
|
||||||
|
<AlertTitle>Active Claim Restored</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Your previous claim was restored. You still have time to review this submission.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Filter Bar */}
|
{/* Filter Bar */}
|
||||||
<QueueFilters
|
<QueueFilters
|
||||||
activeEntityFilter={queueManager.filters.entityFilter}
|
activeEntityFilter={queueManager.filters.entityFilter}
|
||||||
|
|||||||
@@ -162,6 +162,81 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Restore active lock from database on mount
|
||||||
|
const restoreActiveLock = useCallback(async () => {
|
||||||
|
if (!user?.id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Query for any active lock assigned to current user
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('content_submissions')
|
||||||
|
.select('id, locked_until')
|
||||||
|
.eq('assigned_to', user.id)
|
||||||
|
.gt('locked_until', new Date().toISOString())
|
||||||
|
.in('status', ['pending', 'partially_approved'])
|
||||||
|
.order('locked_until', { ascending: false })
|
||||||
|
.limit(1)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
const expiresAt = new Date(data.locked_until || '');
|
||||||
|
|
||||||
|
// Only restore if lock hasn't expired (race condition check)
|
||||||
|
if (data.locked_until && expiresAt > new Date()) {
|
||||||
|
setCurrentLock({
|
||||||
|
submissionId: data.id,
|
||||||
|
expiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start countdown timer for restored lock
|
||||||
|
startLockTimer(expiresAt);
|
||||||
|
|
||||||
|
console.log('Lock state restored from database', {
|
||||||
|
submissionId: data.id,
|
||||||
|
expiresAt: expiresAt.toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
// Log but don't show user toast (they haven't taken any action yet)
|
||||||
|
console.debug('Failed to restore lock state', {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [user, startLockTimer]);
|
||||||
|
|
||||||
|
// Initialize lock state from database on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
restoreActiveLock();
|
||||||
|
}, [user, restoreActiveLock]);
|
||||||
|
|
||||||
|
// Sync lock state across tabs when user returns to the page
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
// User returned to tab - check if lock state is still valid
|
||||||
|
if (!currentLock) {
|
||||||
|
restoreActiveLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
};
|
||||||
|
}, [user, currentLock, restoreActiveLock]);
|
||||||
|
|
||||||
// Claim a specific submission (CRM-style claim any)
|
// Claim a specific submission (CRM-style claim any)
|
||||||
const extendLock = useCallback(async (submissionId: string): Promise<boolean> => {
|
const extendLock = useCallback(async (submissionId: string): Promise<boolean> => {
|
||||||
if (!user?.id) return false;
|
if (!user?.id) return false;
|
||||||
@@ -323,6 +398,16 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if user already has an active lock on a different submission
|
||||||
|
if (currentLock && currentLock.submissionId !== submissionId) {
|
||||||
|
toast({
|
||||||
|
title: 'Already Have Active Lock',
|
||||||
|
description: 'Release your current lock before claiming another submission',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
// Get submission details FIRST for better toast message
|
// Get submission details FIRST for better toast message
|
||||||
|
|||||||
Reference in New Issue
Block a user