mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-29 07:47:06 -05:00
Compare commits
2 Commits
5531376edf
...
09c320f508
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09c320f508 | ||
|
|
8422bc378f |
@@ -1,30 +1,70 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { AlertTriangle } from 'lucide-react';
|
import { AlertTriangle, Plus, Minus, Edit } from 'lucide-react';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { useEntityVersions } from '@/hooks/useEntityVersions';
|
||||||
|
import type { EntityType } from '@/types/versioning';
|
||||||
|
|
||||||
interface RollbackDialogProps {
|
interface RollbackDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
versionId: string;
|
versionId: string;
|
||||||
entityType: string;
|
entityType: EntityType;
|
||||||
entityId: string;
|
entityId: string;
|
||||||
entityName: string;
|
entityName: string;
|
||||||
onRollback: (reason: string) => Promise<void>;
|
onRollback: (reason: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface VersionDiff {
|
||||||
|
[fieldName: string]: {
|
||||||
|
from: unknown;
|
||||||
|
to: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function RollbackDialog({
|
export function RollbackDialog({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
versionId,
|
versionId,
|
||||||
|
entityType,
|
||||||
|
entityId,
|
||||||
entityName,
|
entityName,
|
||||||
onRollback,
|
onRollback,
|
||||||
}: RollbackDialogProps) {
|
}: RollbackDialogProps) {
|
||||||
const [reason, setReason] = useState('');
|
const [reason, setReason] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [diff, setDiff] = useState<VersionDiff | null>(null);
|
||||||
|
const [diffLoading, setDiffLoading] = useState(false);
|
||||||
|
|
||||||
|
const { versions, compareVersions } = useEntityVersions(entityType, entityId);
|
||||||
|
const currentVersion = versions[0]; // Most recent version
|
||||||
|
|
||||||
|
// Fetch diff when dialog opens
|
||||||
|
useEffect(() => {
|
||||||
|
const loadDiff = async () => {
|
||||||
|
if (open && versionId && currentVersion?.version_id) {
|
||||||
|
setDiffLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await compareVersions(versionId, currentVersion.version_id);
|
||||||
|
setDiff(result as VersionDiff);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load diff:', error);
|
||||||
|
setDiff(null);
|
||||||
|
} finally {
|
||||||
|
setDiffLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadDiff();
|
||||||
|
}, [open, versionId, currentVersion?.version_id, compareVersions]);
|
||||||
|
|
||||||
const handleRollback = async () => {
|
const handleRollback = async () => {
|
||||||
if (!reason.trim()) return;
|
if (!reason.trim()) return;
|
||||||
@@ -33,15 +73,32 @@ export function RollbackDialog({
|
|||||||
try {
|
try {
|
||||||
await onRollback(reason);
|
await onRollback(reason);
|
||||||
setReason('');
|
setReason('');
|
||||||
|
setDiff(null);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatValue = (value: any): string => {
|
||||||
|
if (value === null || value === undefined) return 'null';
|
||||||
|
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
||||||
|
if (typeof value === 'object') return JSON.stringify(value, null, 2);
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFieldName = (fieldName: string): string => {
|
||||||
|
return fieldName
|
||||||
|
.split('_')
|
||||||
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(' ');
|
||||||
|
};
|
||||||
|
|
||||||
|
const changedFieldCount = diff ? Object.keys(diff).length : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent>
|
<DialogContent className="max-w-3xl max-h-[85vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Restore Previous Version (Moderator Action)</DialogTitle>
|
<DialogTitle>Restore Previous Version (Moderator Action)</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -56,6 +113,100 @@ export function RollbackDialog({
|
|||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
|
{/* Preview Changes Section */}
|
||||||
|
<Accordion type="single" collapsible defaultValue="preview" className="border rounded-lg">
|
||||||
|
<AccordionItem value="preview" className="border-none">
|
||||||
|
<AccordionTrigger className="px-4 hover:no-underline">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium">Preview Changes</span>
|
||||||
|
{changedFieldCount > 0 && (
|
||||||
|
<Badge variant="outline" className="bg-blue-500/10 text-blue-700 dark:text-blue-400">
|
||||||
|
{changedFieldCount} field{changedFieldCount !== 1 ? 's' : ''} will change
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="px-4 pb-4">
|
||||||
|
<ScrollArea className="h-[300px] pr-4">
|
||||||
|
{diffLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||||
|
</div>
|
||||||
|
) : diff && Object.keys(diff).length > 0 ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{Object.entries(diff).map(([fieldName, changes]: [string, any]) => {
|
||||||
|
const isAdded = changes.from === null;
|
||||||
|
const isRemoved = changes.to === null;
|
||||||
|
const isModified = changes.from !== null && changes.to !== null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={fieldName} className="border rounded-md p-3 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{isAdded && <Plus className="h-4 w-4 text-green-500" />}
|
||||||
|
{isRemoved && <Minus className="h-4 w-4 text-red-500" />}
|
||||||
|
{isModified && <Edit className="h-4 w-4 text-blue-500" />}
|
||||||
|
<span className="font-medium text-sm">{formatFieldName(fieldName)}</span>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={
|
||||||
|
isAdded
|
||||||
|
? 'bg-green-500/10 text-green-700 dark:text-green-400'
|
||||||
|
: isRemoved
|
||||||
|
? 'bg-red-500/10 text-red-700 dark:text-red-400'
|
||||||
|
: 'bg-blue-500/10 text-blue-700 dark:text-blue-400'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isAdded ? 'Added' : isRemoved ? 'Removed' : 'Modified'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{/* Current value (will be replaced) */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-xs text-muted-foreground font-medium">Current</div>
|
||||||
|
<div
|
||||||
|
className={`p-2 rounded-md font-mono text-xs whitespace-pre-wrap ${
|
||||||
|
isRemoved
|
||||||
|
? 'bg-red-500/10 text-red-700 dark:text-red-400 line-through'
|
||||||
|
: 'bg-muted'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{formatValue(changes.to)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Restored value */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-xs text-muted-foreground font-medium">After Restore</div>
|
||||||
|
<div
|
||||||
|
className={`p-2 rounded-md font-mono text-xs whitespace-pre-wrap ${
|
||||||
|
isAdded
|
||||||
|
? 'bg-green-500/10 text-green-700 dark:text-green-400 font-semibold'
|
||||||
|
: isModified
|
||||||
|
? 'bg-blue-500/10 text-blue-700 dark:text-blue-400 font-semibold'
|
||||||
|
: 'bg-muted'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{formatValue(changes.from)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
<p className="text-sm">No differences found</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="rollback-reason">Reason for rollback *</Label>
|
<Label htmlFor="rollback-reason">Reason for rollback *</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { History, Clock } from 'lucide-react';
|
import { History } from 'lucide-react';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import { EntityVersionHistory } from './EntityVersionHistory';
|
import { EntityVersionHistory } from './EntityVersionHistory';
|
||||||
import { useEntityVersions } from '@/hooks/useEntityVersions';
|
import { useEntityVersions } from '@/hooks/useEntityVersions';
|
||||||
@@ -43,7 +42,7 @@ export function VersionIndicator({
|
|||||||
>
|
>
|
||||||
<History className="h-4 w-4" />
|
<History className="h-4 w-4" />
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
v{currentVersion.version_number}
|
History
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -66,10 +65,6 @@ export function VersionIndicator({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Badge variant="outline" className="gap-1.5">
|
|
||||||
<Clock className="h-3 w-3" />
|
|
||||||
Version {currentVersion.version_number}
|
|
||||||
</Badge>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Last edited {timeAgo}
|
Last edited {timeAgo}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user