mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:31:11 -05:00
217 lines
7.8 KiB
TypeScript
217 lines
7.8 KiB
TypeScript
import { useState } from 'react';
|
|
import { Clock, GitBranch, User, FileText, RotateCcw, GitCompare } from 'lucide-react';
|
|
import { formatDistanceToNow } from 'date-fns';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card } from '@/components/ui/card';
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { VersionComparisonDialog } from './VersionComparisonDialog';
|
|
import { RollbackDialog } from './RollbackDialog';
|
|
import { useEntityVersions } from '@/hooks/useEntityVersions';
|
|
import { useUserRole } from '@/hooks/useUserRole';
|
|
import type { EntityType } from '@/types/versioning';
|
|
|
|
interface EntityVersionHistoryProps {
|
|
entityType: EntityType;
|
|
entityId: string;
|
|
entityName: string;
|
|
}
|
|
|
|
const changeTypeColors = {
|
|
created: 'bg-green-500/10 text-green-700 dark:text-green-400',
|
|
updated: 'bg-blue-500/10 text-blue-700 dark:text-blue-400',
|
|
deleted: 'bg-red-500/10 text-red-700 dark:text-red-400',
|
|
restored: 'bg-purple-500/10 text-purple-700 dark:text-purple-400',
|
|
archived: 'bg-gray-500/10 text-gray-700 dark:text-gray-400',
|
|
};
|
|
|
|
export function EntityVersionHistory({ entityType, entityId, entityName }: EntityVersionHistoryProps) {
|
|
const { versions, loading, rollbackToVersion } = useEntityVersions(entityType, entityId);
|
|
const { isModerator } = useUserRole();
|
|
const [selectedVersions, setSelectedVersions] = useState<string[]>([]);
|
|
const [compareDialogOpen, setCompareDialogOpen] = useState(false);
|
|
const [rollbackDialogOpen, setRollbackDialogOpen] = useState(false);
|
|
const [selectedVersionForRollback, setSelectedVersionForRollback] = useState<string | null>(null);
|
|
|
|
const handleVersionSelect = (versionId: string) => {
|
|
setSelectedVersions(prev => {
|
|
if (prev.includes(versionId)) {
|
|
return prev.filter(id => id !== versionId);
|
|
}
|
|
if (prev.length >= 2) {
|
|
return [prev[1], versionId];
|
|
}
|
|
return [...prev, versionId];
|
|
});
|
|
};
|
|
|
|
const handleCompare = () => {
|
|
if (selectedVersions.length === 2) {
|
|
setCompareDialogOpen(true);
|
|
}
|
|
};
|
|
|
|
const handleRollback = (versionId: string) => {
|
|
setSelectedVersionForRollback(versionId);
|
|
setRollbackDialogOpen(true);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<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>
|
|
);
|
|
}
|
|
|
|
if (versions.length === 0) {
|
|
return (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
<FileText className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
|
<p>No version history available</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Header Actions */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-lg font-semibold">Version History</h3>
|
|
<p className="text-sm text-muted-foreground">{versions.length} versions</p>
|
|
</div>
|
|
|
|
{selectedVersions.length === 2 && (
|
|
<Button onClick={handleCompare} size="sm">
|
|
<GitCompare className="h-4 w-4 mr-2" />
|
|
Compare Selected
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Timeline */}
|
|
<ScrollArea className="h-[600px] pr-4">
|
|
<div className="relative space-y-4">
|
|
{/* Timeline line */}
|
|
<div className="absolute left-6 top-0 bottom-0 w-px bg-border" />
|
|
|
|
{versions.map((version, index) => (
|
|
<Card
|
|
key={version.version_id}
|
|
className={`relative pl-16 pr-4 py-4 cursor-pointer transition-colors ${
|
|
selectedVersions.includes(version.version_id)
|
|
? 'border-primary bg-accent'
|
|
: 'hover:border-primary/50'
|
|
} ${version.is_current ? 'border-primary shadow-md' : ''}`}
|
|
onClick={() => handleVersionSelect(version.version_id)}
|
|
>
|
|
{/* Timeline dot */}
|
|
<div
|
|
className={`absolute left-4 top-4 w-5 h-5 rounded-full border-4 border-background ${
|
|
version.is_current
|
|
? 'bg-primary'
|
|
: 'bg-muted-foreground'
|
|
}`}
|
|
/>
|
|
|
|
<div className="space-y-3">
|
|
{/* Header */}
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex items-center gap-2 flex-1">
|
|
<Badge variant="outline" className={changeTypeColors[version.change_type]}>
|
|
{version.change_type}
|
|
</Badge>
|
|
<span className="font-medium">Version {version.version_number}</span>
|
|
{version.is_current && (
|
|
<Badge variant="default">Current</Badge>
|
|
)}
|
|
</div>
|
|
|
|
{!version.is_current && isModerator() && (
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleRollback(version.version_id);
|
|
}}
|
|
title="Moderator only: Restore this version"
|
|
>
|
|
<RotateCcw className="h-4 w-4 mr-1" />
|
|
Restore
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Metadata */}
|
|
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
|
<div className="flex items-center gap-2">
|
|
<Avatar className="h-6 w-6">
|
|
<AvatarImage src={version.profiles?.avatar_url || undefined} />
|
|
<AvatarFallback>
|
|
<User className="h-3 w-3" />
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<span>{version.profiles?.display_name || version.profiles?.username || 'Unknown'}</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
<Clock className="h-4 w-4" />
|
|
<span>{formatDistanceToNow(new Date(version.created_at), { addSuffix: true })}</span>
|
|
</div>
|
|
|
|
{version.submission_id && (
|
|
<div className="flex items-center gap-1">
|
|
<GitBranch className="h-4 w-4" />
|
|
<span>Submission</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Change Reason */}
|
|
{version.change_reason && (
|
|
<>
|
|
<Separator />
|
|
<p className="text-sm italic">{version.change_reason}</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
{/* Dialogs */}
|
|
{selectedVersions.length === 2 && (
|
|
<VersionComparisonDialog
|
|
open={compareDialogOpen}
|
|
onOpenChange={setCompareDialogOpen}
|
|
entityType={entityType}
|
|
entityId={entityId}
|
|
fromVersionId={selectedVersions[0]}
|
|
toVersionId={selectedVersions[1]}
|
|
/>
|
|
)}
|
|
|
|
{selectedVersionForRollback && (
|
|
<RollbackDialog
|
|
open={rollbackDialogOpen}
|
|
onOpenChange={setRollbackDialogOpen}
|
|
versionId={selectedVersionForRollback}
|
|
entityType={entityType}
|
|
entityId={entityId}
|
|
entityName={entityName}
|
|
onRollback={async (reason) => {
|
|
await rollbackToVersion(selectedVersionForRollback, reason);
|
|
setRollbackDialogOpen(false);
|
|
setSelectedVersionForRollback(null);
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|