feat: Implement timeline manager

This commit is contained in:
gpt-engineer-app[bot]
2025-11-05 18:44:57 +00:00
parent 18d28a1fc8
commit 0d6d3fb2cc
3 changed files with 247 additions and 40 deletions

View File

@@ -1,4 +1,4 @@
import { memo, useCallback } from 'react';
import { memo, useCallback, useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import {
AlertCircle, Edit, Info, ExternalLink, ChevronDown, ListTree, Calendar, Crown, Unlock
@@ -14,6 +14,7 @@ import { UserAvatar } from '@/components/ui/user-avatar';
import { format } from 'date-fns';
import type { ModerationItem } from '@/types/moderation';
import { sanitizeURL, sanitizePlainText } from '@/lib/sanitize';
import { getErrorMessage } from '@/lib/errorHandler';
interface QueueItemActionsProps {
item: ModerationItem;
@@ -64,30 +65,50 @@ export const QueueItemActions = memo(({
onClaim,
onSuperuserReleaseLock
}: QueueItemActionsProps) => {
// Error state for retry functionality
const [actionError, setActionError] = useState<{
message: string;
errorId?: string;
action: 'approve' | 'reject';
} | null>(null);
// Memoize all handlers to prevent re-renders
const handleNoteChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
onNoteChange(item.id, e.target.value);
}, [onNoteChange, item.id]);
// Debounced handlers to prevent duplicate submissions
// Debounced handlers with error tracking
const handleApprove = useDebouncedCallback(
() => {
// Extra guard against race conditions
if (actionLoading === item.id) {
return;
async () => {
if (actionLoading === item.id) return;
try {
setActionError(null);
await onApprove(item, 'approved', notes[item.id]);
} catch (error: any) {
setActionError({
message: getErrorMessage(error),
errorId: error.errorId,
action: 'approve',
});
}
onApprove(item, 'approved', notes[item.id]);
},
300, // 300ms debounce
{ leading: true, trailing: false } // Only fire on first click
300,
{ leading: true, trailing: false }
);
const handleReject = useDebouncedCallback(
() => {
if (actionLoading === item.id) {
return;
async () => {
if (actionLoading === item.id) return;
try {
setActionError(null);
await onApprove(item, 'rejected', notes[item.id]);
} catch (error: any) {
setActionError({
message: getErrorMessage(error),
errorId: error.errorId,
action: 'reject',
});
}
onApprove(item, 'rejected', notes[item.id]);
},
300,
{ leading: true, trailing: false }
@@ -149,6 +170,40 @@ export const QueueItemActions = memo(({
return (
<>
{/* Error Display with Retry */}
{actionError && (
<Alert variant="destructive" className="mb-4">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Action Failed: {actionError.action}</AlertTitle>
<AlertDescription>
<div className="space-y-2">
<p className="text-sm">{actionError.message}</p>
{actionError.errorId && (
<p className="text-xs font-mono bg-destructive/10 px-2 py-1 rounded">
Reference ID: {actionError.errorId.slice(0, 8)}
</p>
)}
<div className="flex gap-2 mt-3">
<Button
size="sm"
variant="outline"
onClick={() => {
setActionError(null);
if (actionError.action === 'approve') handleApprove();
else if (actionError.action === 'reject') handleReject();
}}
>
Retry {actionError.action}
</Button>
<Button size="sm" variant="ghost" onClick={() => setActionError(null)}>
Dismiss
</Button>
</div>
</div>
</AlertDescription>
</Alert>
)}
{/* Action buttons based on status */}
{(item.status === 'pending' || item.status === 'flagged') && (
<>