mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 09:11:12 -05:00
Remove version number badge from VersionIndicator
Replace the badge with unchanged Last edited and View History UI, keeping dialog and history functionality intact. Update RollbackDialog to support preview of changes and wider dialog, and integrate existing diff logic to preview before rollback. Adjust VersionIndicator to stop displaying version number while preserving UI elements.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user