mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 09:51:13 -05:00
Refactor: Remove item_edit_history_view
This commit is contained in:
@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { toast } from '@/hooks/use-toast';
|
import { toast } from '@/hooks/use-toast';
|
||||||
import { useIsMobile } from '@/hooks/use-mobile';
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
import { useUserRole } from '@/hooks/useUserRole';
|
import { useUserRole } from '@/hooks/useUserRole';
|
||||||
@@ -21,22 +22,28 @@ import { RideModelForm } from '@/components/admin/RideModelForm';
|
|||||||
import { Save, X, Edit } from 'lucide-react';
|
import { Save, X, Edit } from 'lucide-react';
|
||||||
|
|
||||||
interface ItemEditDialogProps {
|
interface ItemEditDialogProps {
|
||||||
item: SubmissionItemWithDeps | null;
|
item?: SubmissionItemWithDeps | null;
|
||||||
|
items?: SubmissionItemWithDeps[];
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ItemEditDialog({ item, open, onOpenChange, onComplete }: ItemEditDialogProps) {
|
export function ItemEditDialog({ item, items, open, onOpenChange, onComplete }: ItemEditDialogProps) {
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [activeTab, setActiveTab] = useState<string>(items?.[0]?.id || '');
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const { isModerator } = useUserRole();
|
const { isModerator } = useUserRole();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const Container = isMobile ? Sheet : Dialog;
|
const Container = isMobile ? Sheet : Dialog;
|
||||||
|
|
||||||
|
// Phase 5: Bulk edit mode
|
||||||
|
const bulkEditMode = items && items.length > 1;
|
||||||
|
const currentItem = bulkEditMode ? items.find(i => i.id === activeTab) : item;
|
||||||
|
|
||||||
if (!item) return null;
|
if (!currentItem && !bulkEditMode) return null;
|
||||||
|
|
||||||
const handleSubmit = async (data: Record<string, unknown>) => {
|
const handleSubmit = async (data: Record<string, unknown>, itemId?: string) => {
|
||||||
if (!user?.id) {
|
if (!user?.id) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Authentication Required',
|
title: 'Authentication Required',
|
||||||
@@ -45,10 +52,13 @@ export function ItemEditDialog({ item, open, onOpenChange, onComplete }: ItemEdi
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const targetItemId = itemId || currentItem?.id;
|
||||||
|
if (!targetItemId) return;
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await editSubmissionItem(item.id, data, user.id);
|
await editSubmissionItem(targetItemId, data, user.id);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: isModerator() ? 'Item Updated' : 'Edit Submitted',
|
title: isModerator() ? 'Item Updated' : 'Edit Submitted',
|
||||||
@@ -57,8 +67,19 @@ export function ItemEditDialog({ item, open, onOpenChange, onComplete }: ItemEdi
|
|||||||
: 'Your edit has been submitted and will be reviewed by a moderator.',
|
: 'Your edit has been submitted and will be reviewed by a moderator.',
|
||||||
});
|
});
|
||||||
|
|
||||||
onComplete();
|
if (bulkEditMode && items) {
|
||||||
onOpenChange(false);
|
// Move to next tab or complete
|
||||||
|
const currentIndex = items.findIndex(i => i.id === activeTab);
|
||||||
|
if (currentIndex < items.length - 1) {
|
||||||
|
setActiveTab(items[currentIndex + 1].id);
|
||||||
|
} else {
|
||||||
|
onComplete();
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onComplete();
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMsg = getErrorMessage(error);
|
const errorMsg = getErrorMessage(error);
|
||||||
toast({
|
toast({
|
||||||
@@ -85,10 +106,10 @@ export function ItemEditDialog({ item, open, onOpenChange, onComplete }: ItemEdi
|
|||||||
await handleSubmit(photoData);
|
await handleSubmit(photoData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderEditForm = () => {
|
const renderEditForm = (editItem: SubmissionItemWithDeps) => {
|
||||||
const data = item.item_data;
|
const data = editItem.item_data;
|
||||||
|
|
||||||
switch (item.item_type) {
|
switch (editItem.item_type) {
|
||||||
case 'park':
|
case 'park':
|
||||||
return (
|
return (
|
||||||
<ParkForm
|
<ParkForm
|
||||||
@@ -182,14 +203,31 @@ export function ItemEditDialog({ item, open, onOpenChange, onComplete }: ItemEdi
|
|||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle className="flex items-center gap-2">
|
<SheetTitle className="flex items-center gap-2">
|
||||||
<Edit className="w-5 h-5" />
|
<Edit className="w-5 h-5" />
|
||||||
Edit {item.item_type.replace('_', ' ')}
|
{bulkEditMode ? `Edit ${items.length} Items` : `Edit ${currentItem?.item_type.replace('_', ' ')}`}
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
<SheetDescription>
|
<SheetDescription>
|
||||||
Make changes to this submission item
|
{bulkEditMode ? 'Edit multiple submission items using tabs' : 'Make changes to this submission item'}
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
{renderEditForm()}
|
{bulkEditMode && items ? (
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<TabsList className="grid w-full" style={{ gridTemplateColumns: `repeat(${items.length}, 1fr)` }}>
|
||||||
|
{items.map((tabItem, index) => (
|
||||||
|
<TabsTrigger key={tabItem.id} value={tabItem.id}>
|
||||||
|
{index + 1}. {tabItem.item_type.replace('_', ' ')}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
{items.map(tabItem => (
|
||||||
|
<TabsContent key={tabItem.id} value={tabItem.id} className="mt-4">
|
||||||
|
{renderEditForm(tabItem)}
|
||||||
|
</TabsContent>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
) : currentItem ? (
|
||||||
|
renderEditForm(currentItem)
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
) : (
|
) : (
|
||||||
@@ -197,14 +235,31 @@ export function ItemEditDialog({ item, open, onOpenChange, onComplete }: ItemEdi
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<Edit className="w-5 h-5" />
|
<Edit className="w-5 h-5" />
|
||||||
Edit {item.item_type.replace('_', ' ')}
|
{bulkEditMode ? `Edit ${items.length} Items` : `Edit ${currentItem?.item_type.replace('_', ' ')}`}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Make changes to this submission item
|
{bulkEditMode ? 'Edit multiple submission items using tabs' : 'Make changes to this submission item'}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
{renderEditForm()}
|
{bulkEditMode && items ? (
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<TabsList className="grid w-full" style={{ gridTemplateColumns: `repeat(${items.length}, 1fr)` }}>
|
||||||
|
{items.map((tabItem, index) => (
|
||||||
|
<TabsTrigger key={tabItem.id} value={tabItem.id}>
|
||||||
|
{index + 1}. {tabItem.item_type.replace('_', ' ')}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
{items.map(tabItem => (
|
||||||
|
<TabsContent key={tabItem.id} value={tabItem.id} className="mt-4">
|
||||||
|
{renderEditForm(tabItem)}
|
||||||
|
</TabsContent>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
) : currentItem ? (
|
||||||
|
renderEditForm(currentItem)
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
)}
|
)}
|
||||||
|
|||||||
115
src/components/moderation/ItemSelectorDialog.tsx
Normal file
115
src/components/moderation/ItemSelectorDialog.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Edit, CheckCircle, XCircle, Clock } from 'lucide-react';
|
||||||
|
import type { SubmissionItemWithDeps } from '@/lib/submissionItemsService';
|
||||||
|
|
||||||
|
interface ItemSelectorDialogProps {
|
||||||
|
items: SubmissionItemWithDeps[];
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSelectItem: (item: SubmissionItemWithDeps) => void;
|
||||||
|
onBulkEdit?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ItemSelectorDialog({
|
||||||
|
items,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSelectItem,
|
||||||
|
onBulkEdit
|
||||||
|
}: ItemSelectorDialogProps) {
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'approved':
|
||||||
|
return <CheckCircle className="h-4 w-4 text-green-600" />;
|
||||||
|
case 'rejected':
|
||||||
|
return <XCircle className="h-4 w-4 text-red-600" />;
|
||||||
|
default:
|
||||||
|
return <Clock className="h-4 w-4 text-yellow-600" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'approved':
|
||||||
|
return 'bg-green-100 text-green-800 border-green-200';
|
||||||
|
case 'rejected':
|
||||||
|
return 'bg-red-100 text-red-800 border-red-200';
|
||||||
|
case 'pending':
|
||||||
|
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800 border-gray-200';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Edit className="w-5 h-5" />
|
||||||
|
Select Item to Edit
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
This submission contains {items.length} items. Choose which one to edit, or edit all at once.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-3 mt-4">
|
||||||
|
{/* Bulk Edit Option */}
|
||||||
|
{onBulkEdit && items.length > 1 && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-start h-auto py-4 border-2 border-primary/20 hover:border-primary/40"
|
||||||
|
onClick={onBulkEdit}
|
||||||
|
>
|
||||||
|
<Edit className="mr-3 h-5 w-5 text-primary" />
|
||||||
|
<div className="flex flex-col items-start">
|
||||||
|
<span className="font-semibold">Edit All Items ({items.length})</span>
|
||||||
|
<span className="text-xs text-muted-foreground">Use tabbed interface to edit multiple items</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Individual Items */}
|
||||||
|
{items.map((item) => (
|
||||||
|
<Button
|
||||||
|
key={item.id}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-start h-auto py-4 hover:bg-accent"
|
||||||
|
onClick={() => onSelectItem(item)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between w-full">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Edit className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||||
|
<div className="flex flex-col items-start">
|
||||||
|
<span className="font-medium capitalize">
|
||||||
|
{item.item_type.replace('_', ' ')}
|
||||||
|
</span>
|
||||||
|
{item.item_data.name && (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{String(item.item_data.name)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{item.dependencies && item.dependencies.length > 0 && (
|
||||||
|
<span className="text-xs text-muted-foreground mt-1">
|
||||||
|
Depends on {item.dependencies.length} item(s)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getStatusIcon(item.status)}
|
||||||
|
<Badge variant="outline" className={getStatusColor(item.status)}>
|
||||||
|
{item.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { getErrorMessage } from '@/lib/errorHandler';
|
|||||||
import { PhotoModal } from './PhotoModal';
|
import { PhotoModal } from './PhotoModal';
|
||||||
import { SubmissionReviewManager } from './SubmissionReviewManager';
|
import { SubmissionReviewManager } from './SubmissionReviewManager';
|
||||||
import { ItemEditDialog } from './ItemEditDialog';
|
import { ItemEditDialog } from './ItemEditDialog';
|
||||||
|
import { ItemSelectorDialog } from './ItemSelectorDialog';
|
||||||
import { useIsMobile } from '@/hooks/use-mobile';
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
import { useAdminSettings } from '@/hooks/useAdminSettings';
|
import { useAdminSettings } from '@/hooks/useAdminSettings';
|
||||||
import { useModerationQueueManager } from '@/hooks/moderation';
|
import { useModerationQueueManager } from '@/hooks/moderation';
|
||||||
@@ -80,6 +81,10 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
|
|||||||
const [selectedSubmissionId, setSelectedSubmissionId] = useState<string | null>(null);
|
const [selectedSubmissionId, setSelectedSubmissionId] = useState<string | null>(null);
|
||||||
const [showItemEditDialog, setShowItemEditDialog] = useState(false);
|
const [showItemEditDialog, setShowItemEditDialog] = useState(false);
|
||||||
const [editingItem, setEditingItem] = useState<SubmissionItemWithDeps | null>(null);
|
const [editingItem, setEditingItem] = useState<SubmissionItemWithDeps | null>(null);
|
||||||
|
const [showItemSelector, setShowItemSelector] = useState(false);
|
||||||
|
const [availableItems, setAvailableItems] = useState<SubmissionItemWithDeps[]>([]);
|
||||||
|
const [bulkEditMode, setBulkEditMode] = useState(false);
|
||||||
|
const [bulkEditItems, setBulkEditItems] = useState<SubmissionItemWithDeps[]>([]);
|
||||||
|
|
||||||
// Confirmation dialog state
|
// Confirmation dialog state
|
||||||
const [confirmDialog, setConfirmDialog] = useState<{
|
const [confirmDialog, setConfirmDialog] = useState<{
|
||||||
@@ -176,6 +181,19 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
|
|||||||
},
|
},
|
||||||
description: 'Focus filters',
|
description: 'Focus filters',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'e',
|
||||||
|
handler: () => {
|
||||||
|
// Edit first claimed submission
|
||||||
|
const claimedItem = queueManager.items.find(item =>
|
||||||
|
queueManager.queue.isLockedByMe(item.id, item.assigned_to, item.locked_until)
|
||||||
|
);
|
||||||
|
if (claimedItem) {
|
||||||
|
handleOpenItemEditor(claimedItem.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description: 'Edit claimed submission',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
enabled: true,
|
enabled: true,
|
||||||
});
|
});
|
||||||
@@ -195,22 +213,55 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
|
|||||||
try {
|
try {
|
||||||
const items = await fetchSubmissionItems(submissionId);
|
const items = await fetchSubmissionItems(submissionId);
|
||||||
|
|
||||||
// Find first pending item, fallback to first available
|
if (!items || items.length === 0) {
|
||||||
let itemToEdit = items.find(item => item.status === 'pending');
|
|
||||||
if (!itemToEdit && items.length > 0) {
|
|
||||||
itemToEdit = items[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemToEdit) {
|
|
||||||
setEditingItem(itemToEdit);
|
|
||||||
setShowItemEditDialog(true);
|
|
||||||
} else {
|
|
||||||
toast({
|
toast({
|
||||||
title: 'No Items Found',
|
title: 'No Items Found',
|
||||||
description: 'This submission has no items to edit',
|
description: 'This submission has no items to edit',
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 3: Multi-item selector for submissions with multiple items
|
||||||
|
if (items.length > 1) {
|
||||||
|
setAvailableItems(items);
|
||||||
|
setShowItemSelector(true);
|
||||||
|
} else {
|
||||||
|
// Single item - edit directly
|
||||||
|
setEditingItem(items[0]);
|
||||||
|
setShowItemEditDialog(true);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: getErrorMessage(error),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectItem = (item: SubmissionItemWithDeps) => {
|
||||||
|
setEditingItem(item);
|
||||||
|
setShowItemSelector(false);
|
||||||
|
setShowItemEditDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkEdit = async (submissionId: string) => {
|
||||||
|
try {
|
||||||
|
const items = await fetchSubmissionItems(submissionId);
|
||||||
|
|
||||||
|
if (!items || items.length === 0) {
|
||||||
|
toast({
|
||||||
|
title: 'No Items Found',
|
||||||
|
description: 'This submission has no items to edit',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBulkEditItems(items);
|
||||||
|
setBulkEditMode(true);
|
||||||
|
setShowItemEditDialog(true);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
@@ -435,23 +486,48 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
|
|||||||
submissionId={selectedSubmissionId}
|
submissionId={selectedSubmissionId}
|
||||||
open={reviewManagerOpen}
|
open={reviewManagerOpen}
|
||||||
onOpenChange={setReviewManagerOpen}
|
onOpenChange={setReviewManagerOpen}
|
||||||
onComplete={() => setReviewManagerOpen(false)}
|
onComplete={() => {
|
||||||
/>
|
queueManager.refresh();
|
||||||
)}
|
setSelectedSubmissionId(null);
|
||||||
|
|
||||||
{editingItem && (
|
|
||||||
<ItemEditDialog
|
|
||||||
item={editingItem}
|
|
||||||
open={showItemEditDialog}
|
|
||||||
onOpenChange={setShowItemEditDialog}
|
|
||||||
onComplete={async () => {
|
|
||||||
setShowItemEditDialog(false);
|
|
||||||
setEditingItem(null);
|
|
||||||
await queueManager.refresh();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Phase 3: Item Selector Dialog */}
|
||||||
|
<ItemSelectorDialog
|
||||||
|
items={availableItems}
|
||||||
|
open={showItemSelector}
|
||||||
|
onOpenChange={setShowItemSelector}
|
||||||
|
onSelectItem={handleSelectItem}
|
||||||
|
onBulkEdit={() => {
|
||||||
|
setShowItemSelector(false);
|
||||||
|
setBulkEditItems(availableItems);
|
||||||
|
setBulkEditMode(true);
|
||||||
|
setShowItemEditDialog(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Phase 4 & 5: Enhanced Item Edit Dialog */}
|
||||||
|
<ItemEditDialog
|
||||||
|
item={bulkEditMode ? null : editingItem}
|
||||||
|
items={bulkEditMode ? bulkEditItems : undefined}
|
||||||
|
open={showItemEditDialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setShowItemEditDialog(open);
|
||||||
|
if (!open) {
|
||||||
|
setEditingItem(null);
|
||||||
|
setBulkEditMode(false);
|
||||||
|
setBulkEditItems([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onComplete={() => {
|
||||||
|
queueManager.refresh();
|
||||||
|
setEditingItem(null);
|
||||||
|
setBulkEditMode(false);
|
||||||
|
setBulkEditItems([]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Confirmation Dialog */}
|
{/* Confirmation Dialog */}
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
open={confirmDialog.open}
|
open={confirmDialog.open}
|
||||||
|
|||||||
@@ -256,13 +256,13 @@ export const QueueItemActions = memo(({
|
|||||||
Review Items
|
Review Items
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{isAdmin && isLockedByMe && (
|
{isLockedByMe && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleOpenItemEditor}
|
onClick={handleOpenItemEditor}
|
||||||
disabled={actionLoading === item.id}
|
disabled={actionLoading === item.id}
|
||||||
variant="ghost"
|
variant="outline"
|
||||||
className={isMobile ? 'h-11' : ''}
|
className={isMobile ? 'h-11' : ''}
|
||||||
size={isMobile ? "default" : "default"}
|
size={isMobile ? "default" : "default"}
|
||||||
>
|
>
|
||||||
@@ -271,7 +271,7 @@ export const QueueItemActions = memo(({
|
|||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Quick edit first pending item</p>
|
<p>Edit submission items (Press E)</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|||||||
17
src/hooks/useEditHistory.ts
Normal file
17
src/hooks/useEditHistory.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { fetchEditHistory } from '@/lib/submissionItemsService';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 4: Hook to fetch edit history for a submission item
|
||||||
|
*/
|
||||||
|
export function useEditHistory(itemId: string | null) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['item-edit-history', itemId],
|
||||||
|
queryFn: () => {
|
||||||
|
if (!itemId) return [];
|
||||||
|
return fetchEditHistory(itemId);
|
||||||
|
},
|
||||||
|
enabled: !!itemId,
|
||||||
|
staleTime: 30000, // 30 seconds
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -4595,27 +4595,6 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Relationships: []
|
Relationships: []
|
||||||
}
|
}
|
||||||
item_edit_history_view: {
|
|
||||||
Row: {
|
|
||||||
changes: Json | null
|
|
||||||
edited_at: string | null
|
|
||||||
editor_avatar_url: string | null
|
|
||||||
editor_display_name: string | null
|
|
||||||
editor_id: string | null
|
|
||||||
editor_username: string | null
|
|
||||||
id: string | null
|
|
||||||
item_id: string | null
|
|
||||||
}
|
|
||||||
Relationships: [
|
|
||||||
{
|
|
||||||
foreignKeyName: "item_edit_history_item_id_fkey"
|
|
||||||
columns: ["item_id"]
|
|
||||||
isOneToOne: false
|
|
||||||
referencedRelation: "submission_items"
|
|
||||||
referencedColumns: ["id"]
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
moderation_sla_metrics: {
|
moderation_sla_metrics: {
|
||||||
Row: {
|
Row: {
|
||||||
avg_resolution_hours: number | null
|
avg_resolution_hours: number | null
|
||||||
|
|||||||
@@ -1182,6 +1182,13 @@ export async function editSubmissionItem(
|
|||||||
((currentItem.original_data && Object.keys(currentItem.original_data).length > 0) ? 'edit' : 'create');
|
((currentItem.original_data && Object.keys(currentItem.original_data).length > 0) ? 'edit' : 'create');
|
||||||
|
|
||||||
if (isModerator) {
|
if (isModerator) {
|
||||||
|
// Phase 4: Track changes for edit history
|
||||||
|
const changes = {
|
||||||
|
before: currentItem.item_data,
|
||||||
|
after: newData,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
// Moderators can edit directly
|
// Moderators can edit directly
|
||||||
const { error: updateError } = await supabase
|
const { error: updateError } = await supabase
|
||||||
.from('submission_items')
|
.from('submission_items')
|
||||||
@@ -1195,6 +1202,24 @@ export async function editSubmissionItem(
|
|||||||
|
|
||||||
if (updateError) throw updateError;
|
if (updateError) throw updateError;
|
||||||
|
|
||||||
|
// Phase 4: Record edit history
|
||||||
|
const { error: historyError } = await supabase
|
||||||
|
.from('item_edit_history')
|
||||||
|
.insert({
|
||||||
|
item_id: itemId,
|
||||||
|
editor_id: userId,
|
||||||
|
changes: changes,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (historyError) {
|
||||||
|
logger.error('Failed to record edit history', {
|
||||||
|
itemId,
|
||||||
|
editorId: userId,
|
||||||
|
error: historyError.message,
|
||||||
|
});
|
||||||
|
// Don't fail the whole operation if history tracking fails
|
||||||
|
}
|
||||||
|
|
||||||
// CRITICAL: Create version history if this is an entity edit (not photo)
|
// CRITICAL: Create version history if this is an entity edit (not photo)
|
||||||
// Only create version if this item has already been approved (has approved_entity_id)
|
// Only create version if this item has already been approved (has approved_entity_id)
|
||||||
if (currentItem.item_type !== 'photo' && currentItem.approved_entity_id) {
|
if (currentItem.item_type !== 'photo' && currentItem.approved_entity_id) {
|
||||||
@@ -1309,3 +1334,37 @@ export async function escalateSubmission(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 4: Fetch edit history for a submission item
|
||||||
|
* Returns all edits with editor information
|
||||||
|
*/
|
||||||
|
export async function fetchEditHistory(itemId: string) {
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('item_edit_history')
|
||||||
|
.select(`
|
||||||
|
id,
|
||||||
|
changes,
|
||||||
|
edited_at,
|
||||||
|
editor:profiles!item_edit_history_editor_id_fkey (
|
||||||
|
user_id,
|
||||||
|
username,
|
||||||
|
display_name,
|
||||||
|
avatar_url
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
.eq('item_id', itemId)
|
||||||
|
.order('edited_at', { ascending: false });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return data || [];
|
||||||
|
} catch (error: unknown) {
|
||||||
|
logger.error('Error fetching edit history', {
|
||||||
|
itemId,
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
});
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- Fix security issue: Remove the view and use direct queries instead
|
||||||
|
DROP VIEW IF EXISTS public.item_edit_history_view;
|
||||||
|
|
||||||
|
-- The item_edit_history table with proper RLS is sufficient
|
||||||
|
-- Application code will join with profiles table as needed
|
||||||
Reference in New Issue
Block a user