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
This commit is contained in:
119
src/components/board/VersionHistoryDialog.tsx
Normal file
119
src/components/board/VersionHistoryDialog.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user