Implement superuser lock management

This commit is contained in:
gpt-engineer-app[bot]
2025-11-04 23:08:00 +00:00
parent ae22a48ce2
commit 16386f9894
7 changed files with 412 additions and 26 deletions

View File

@@ -8,6 +8,7 @@ import { useToast } from '@/hooks/use-toast';
import { useUserRole } from '@/hooks/useUserRole';
import { useAuth } from '@/hooks/useAuth';
import { getErrorMessage } from '@/lib/errorHandler';
import { supabase } from '@/lib/supabaseClient';
import { PhotoModal } from './PhotoModal';
import { SubmissionReviewManager } from './SubmissionReviewManager';
import { ItemEditDialog } from './ItemEditDialog';
@@ -29,6 +30,7 @@ import { EnhancedEmptyState } from './EnhancedEmptyState';
import { QueuePagination } from './QueuePagination';
import { ConfirmationDialog } from './ConfirmationDialog';
import { KeyboardShortcutsHelp } from './KeyboardShortcutsHelp';
import { SuperuserQueueControls } from './SuperuserQueueControls';
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
import { fetchSubmissionItems, type SubmissionItemWithDeps } from '@/lib/submissionItemsService';
import type { ModerationQueueRef, ModerationItem } from '@/types/moderation';
@@ -85,6 +87,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
const [availableItems, setAvailableItems] = useState<SubmissionItemWithDeps[]>([]);
const [bulkEditMode, setBulkEditMode] = useState(false);
const [bulkEditItems, setBulkEditItems] = useState<SubmissionItemWithDeps[]>([]);
const [activeLocksCount, setActiveLocksCount] = useState(0);
// Confirmation dialog state
const [confirmDialog, setConfirmDialog] = useState<{
@@ -129,6 +132,27 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
};
}, [queueManager, toast]);
// Fetch active locks count for superusers
useEffect(() => {
if (!isSuperuser()) return;
const fetchActiveLocksCount = async () => {
const { count } = await supabase
.from('content_submissions')
.select('id', { count: 'exact', head: true })
.not('assigned_to', 'is', null)
.gt('locked_until', new Date().toISOString());
setActiveLocksCount(count || 0);
};
fetchActiveLocksCount();
// Refresh count periodically
const interval = setInterval(fetchActiveLocksCount, 30000); // Every 30s
return () => clearInterval(interval);
}, [isSuperuser, queueManager.queue.queueStats]);
// Virtual scrolling setup
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
@@ -154,6 +178,22 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
});
}, [queueManager]);
// Superuser force release lock
const handleSuperuserReleaseLock = useCallback(async (submissionId: string) => {
await queueManager.queue.superuserReleaseLock(submissionId);
// Refresh locks count and queue
setActiveLocksCount(prev => Math.max(0, prev - 1));
queueManager.refresh();
}, [queueManager]);
// Superuser clear all locks
const handleClearAllLocks = useCallback(async () => {
const count = await queueManager.queue.superuserReleaseAllLocks();
setActiveLocksCount(0);
// Force queue refresh
queueManager.refresh();
}, [queueManager]);
// Clear filters handler
const handleClearFilters = useCallback(() => {
queueManager.filters.clearFilters();
@@ -310,6 +350,15 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
</Card>
)}
{/* Superuser Queue Controls */}
{isSuperuser() && (
<SuperuserQueueControls
activeLocksCount={activeLocksCount}
onClearAllLocks={handleClearAllLocks}
isLoading={queueManager.queue.isLoading}
/>
)}
{/* Filter Bar */}
<QueueFilters
activeEntityFilter={queueManager.filters.entityFilter}
@@ -390,6 +439,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
onDeleteSubmission={handleDeleteSubmission}
onInteractionFocus={(id) => queueManager.markInteracting(id, true)}
onInteractionBlur={(id) => queueManager.markInteracting(id, false)}
onSuperuserReleaseLock={isSuperuser() ? handleSuperuserReleaseLock : undefined}
/>
</ModerationErrorBoundary>
))}
@@ -451,6 +501,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
onDeleteSubmission={handleDeleteSubmission}
onInteractionFocus={(id) => queueManager.markInteracting(id, true)}
onInteractionBlur={(id) => queueManager.markInteracting(id, false)}
onSuperuserReleaseLock={isSuperuser() ? handleSuperuserReleaseLock : undefined}
/>
</ModerationErrorBoundary>
</div>

View File

@@ -48,6 +48,7 @@ interface QueueItemProps {
onDeleteSubmission: (item: ModerationItem) => void;
onInteractionFocus: (id: string) => void;
onInteractionBlur: (id: string) => void;
onSuperuserReleaseLock?: (submissionId: string) => Promise<void>;
}
@@ -74,7 +75,8 @@ export const QueueItem = memo(({
onClaimSubmission,
onDeleteSubmission,
onInteractionFocus,
onInteractionBlur
onInteractionBlur,
onSuperuserReleaseLock,
}: QueueItemProps) => {
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null);
const [isClaiming, setIsClaiming] = useState(false);
@@ -323,29 +325,30 @@ export const QueueItem = memo(({
</div>
)}
<QueueItemActions
item={item}
isMobile={isMobile}
actionLoading={actionLoading}
isLockedByMe={isLockedByMe}
isLockedByOther={isLockedByOther}
currentLockSubmissionId={currentLockSubmissionId}
notes={notes}
isAdmin={isAdmin}
isSuperuser={isSuperuser}
queueIsLoading={queueIsLoading}
isClaiming={isClaiming}
onNoteChange={onNoteChange}
onApprove={onApprove}
onResetToPending={onResetToPending}
onRetryFailed={onRetryFailed}
onOpenReviewManager={onOpenReviewManager}
onOpenItemEditor={onOpenItemEditor}
onDeleteSubmission={onDeleteSubmission}
onInteractionFocus={onInteractionFocus}
onInteractionBlur={onInteractionBlur}
onClaim={handleClaim}
/>
<QueueItemActions
item={item}
isMobile={isMobile}
actionLoading={actionLoading}
isLockedByMe={isLockedByMe}
isLockedByOther={isLockedByOther}
currentLockSubmissionId={currentLockSubmissionId}
notes={notes}
isAdmin={isAdmin}
isSuperuser={isSuperuser}
queueIsLoading={queueIsLoading}
isClaiming={isClaiming}
onNoteChange={onNoteChange}
onApprove={onApprove}
onResetToPending={onResetToPending}
onRetryFailed={onRetryFailed}
onOpenReviewManager={onOpenReviewManager}
onOpenItemEditor={onOpenItemEditor}
onDeleteSubmission={onDeleteSubmission}
onInteractionFocus={onInteractionFocus}
onInteractionBlur={onInteractionBlur}
onClaim={handleClaim}
onSuperuserReleaseLock={onSuperuserReleaseLock}
/>
</CardContent>
{/* Raw Data Modal */}

View File

@@ -0,0 +1,84 @@
import { Shield, Unlock } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
interface SuperuserQueueControlsProps {
activeLocksCount: number;
onClearAllLocks: () => Promise<void>;
isLoading: boolean;
}
export const SuperuserQueueControls = ({
activeLocksCount,
onClearAllLocks,
isLoading
}: SuperuserQueueControlsProps) => {
if (activeLocksCount === 0) return null;
return (
<Alert className="border-purple-500/50 bg-purple-500/5">
<Shield className="h-4 w-4 text-purple-600" />
<AlertTitle className="text-purple-900 dark:text-purple-100">
Superuser Queue Management
</AlertTitle>
<AlertDescription className="text-purple-800 dark:text-purple-200">
<div className="flex items-center justify-between mt-2">
<span className="text-sm">
{activeLocksCount} active lock{activeLocksCount !== 1 ? 's' : ''} in queue
</span>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
size="sm"
variant="outline"
className="border-purple-500 text-purple-700 hover:bg-purple-50 dark:hover:bg-purple-950"
disabled={isLoading}
>
<Unlock className="w-4 h-4 mr-2" />
Clear All Locks
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Clear All Active Locks?</AlertDialogTitle>
<AlertDialogDescription>
This will release {activeLocksCount} active lock{activeLocksCount !== 1 ? 's' : ''},
making all submissions available for claiming again.
This action will be logged in the audit trail.
<br /><br />
<strong>Use this for:</strong>
<ul className="list-disc list-inside mt-2 space-y-1">
<li>Clearing stale locks after system issues</li>
<li>Resetting queue after team changes</li>
<li>Emergency queue management</li>
</ul>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={onClearAllLocks}
className="bg-purple-600 hover:bg-purple-700"
>
Clear All Locks
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</AlertDescription>
</Alert>
);
};

View File

@@ -1,7 +1,7 @@
import { memo, useCallback } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import {
AlertCircle, Edit, Info, ExternalLink, ChevronDown, ListTree, Calendar
AlertCircle, Edit, Info, ExternalLink, ChevronDown, ListTree, Calendar, Crown, Unlock
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ActionButton } from '@/components/ui/action-button';
@@ -37,6 +37,7 @@ interface QueueItemActionsProps {
onInteractionFocus: (id: string) => void;
onInteractionBlur: (id: string) => void;
onClaim: () => void;
onSuperuserReleaseLock?: (submissionId: string) => Promise<void>;
}
export const QueueItemActions = memo(({
@@ -60,7 +61,8 @@ export const QueueItemActions = memo(({
onDeleteSubmission,
onInteractionFocus,
onInteractionBlur,
onClaim
onClaim,
onSuperuserReleaseLock
}: QueueItemActionsProps) => {
// Memoize all handlers to prevent re-renders
const handleNoteChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
@@ -172,6 +174,34 @@ export const QueueItemActions = memo(({
</Alert>
</div>
)}
{/* Superuser Lock Override - Show for locked items */}
{isSuperuser && isLockedByOther && onSuperuserReleaseLock && (
<Alert className="border-purple-500/50 bg-purple-500/10">
<Crown className="h-4 w-4 text-purple-600" />
<AlertTitle className="text-purple-900 dark:text-purple-100">
Superuser Override
</AlertTitle>
<AlertDescription className="text-purple-800 dark:text-purple-200">
<div className="flex flex-col gap-2 mt-2">
<p className="text-sm">
This submission is locked by another moderator.
You can force-release this lock.
</p>
<Button
size="sm"
variant="outline"
className="border-purple-500 text-purple-700 hover:bg-purple-50 dark:hover:bg-purple-950"
onClick={() => onSuperuserReleaseLock(item.id)}
disabled={actionLoading === item.id}
>
<Unlock className="w-4 h-4 mr-2" />
Force Release Lock
</Button>
</div>
</AlertDescription>
</Alert>
)}
<div className={isMobile ? 'space-y-4 mt-4' : 'grid grid-cols-1 lg:grid-cols-[1fr,auto] gap-6 items-start mt-4'}>
{/* Submitter Context - shown before moderator can add their notes */}