Files
openpylon/src/components/board/VersionHistoryDialog.tsx
Your Name 7277bbdc21 feat: Phase 4 - board templates, auto-backup, version history
- BoardTemplate type and storage CRUD (save/list/delete templates)
- createBoardFromTemplate factory function
- "Save as Template" in board card context menu
- User templates shown in NewBoardDialog with delete option
- Auto-backup on save with 5-minute throttle, 10 backup retention
- VersionHistoryDialog with backup list and restore confirmation
- Version History accessible from board settings dropdown
2026-02-16 14:55:58 +02:00

120 lines
4.5 KiB
TypeScript

import { useState, useEffect } from "react";
import { formatDistanceToNow } from "date-fns";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { listBackups, restoreBackupFile, saveBoard, type BackupEntry } from "@/lib/storage";
import { useBoardStore } from "@/stores/board-store";
interface VersionHistoryDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function VersionHistoryDialog({ open, onOpenChange }: VersionHistoryDialogProps) {
const board = useBoardStore((s) => s.board);
const [backups, setBackups] = useState<BackupEntry[]>([]);
const [confirmRestore, setConfirmRestore] = useState<BackupEntry | null>(null);
useEffect(() => {
if (open && board) {
listBackups(board.id).then(setBackups);
}
}, [open, board]);
async function handleRestore(backup: BackupEntry) {
if (!board) return;
// Back up current state before restoring
await saveBoard(board);
const restored = await restoreBackupFile(board.id, backup.filename);
await saveBoard(restored);
// Reload
await useBoardStore.getState().openBoard(board.id);
setConfirmRestore(null);
onOpenChange(false);
}
return (
<>
<Dialog open={open && !confirmRestore} onOpenChange={onOpenChange}>
<DialogContent className="bg-pylon-surface sm:max-w-md">
<DialogHeader>
<DialogTitle className="font-heading text-pylon-text">
Version History
</DialogTitle>
<DialogDescription className="text-pylon-text-secondary">
Browse and restore previous versions of this board.
</DialogDescription>
</DialogHeader>
<OverlayScrollbarsComponent
className="max-h-[300px]"
options={{ scrollbars: { theme: "os-theme-pylon", autoHide: "scroll", autoHideDelay: 600, clickScroll: true }, overflow: { x: "hidden" } }}
defer
>
{backups.length > 0 ? (
<div className="flex flex-col gap-1">
{backups.map((backup) => (
<div
key={backup.filename}
className="flex items-center justify-between rounded px-3 py-2 hover:bg-pylon-column/60"
>
<div className="flex flex-col">
<span className="text-sm text-pylon-text">
{formatDistanceToNow(new Date(backup.timestamp), { addSuffix: true })}
</span>
<span className="font-mono text-xs text-pylon-text-secondary">
{backup.cardCount} cards, {backup.columnCount} columns
</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setConfirmRestore(backup)}
className="text-pylon-accent"
>
Restore
</Button>
</div>
))}
</div>
) : (
<p className="px-3 py-6 text-center text-sm text-pylon-text-secondary">
No backups yet. Backups are created automatically as you work.
</p>
)}
</OverlayScrollbarsComponent>
</DialogContent>
</Dialog>
{/* Restore confirmation */}
<Dialog open={confirmRestore != null} onOpenChange={() => setConfirmRestore(null)}>
<DialogContent className="bg-pylon-surface sm:max-w-sm">
<DialogHeader>
<DialogTitle className="font-heading text-pylon-text">
Restore Version
</DialogTitle>
<DialogDescription className="text-pylon-text-secondary">
This will replace the current board with the selected version. Your current state will be backed up first.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="ghost" onClick={() => setConfirmRestore(null)} className="text-pylon-text-secondary">
Cancel
</Button>
<Button onClick={() => confirmRestore && handleRestore(confirmRestore)}>
Restore
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}