mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix(projects): bulk restore preview + type-to-confirm modal
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import {useState} from "react";
|
||||
import {Loader2} from "lucide-react";
|
||||
import {Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter} from "@/components/ui/dialog";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import type {RestorePreviewRow} from "@/features/database/actions/bulk-restore.action";
|
||||
|
||||
const CONFIRM_WORD = "restore";
|
||||
|
||||
export type BulkRestoreModalProps = {
|
||||
open: boolean;
|
||||
loading: boolean;
|
||||
submitting: boolean;
|
||||
rows: RestorePreviewRow[];
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
export const BulkRestoreModal = (props: BulkRestoreModalProps) => {
|
||||
const {open, loading, submitting, rows, onConfirm, onCancel} = props;
|
||||
const [typed, setTyped] = useState("");
|
||||
|
||||
const restorableCount = rows.filter((r) => r.restorable).length;
|
||||
const canConfirm = restorableCount > 0 && typed.trim().toLowerCase() === CONFIRM_WORD && !submitting;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(o) => (!o ? onCancel() : undefined)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-destructive">
|
||||
Restore {rows.length} database(s) to latest backup
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This overwrites current data and cannot be undone.
|
||||
</p>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8"><Loader2 className="animate-spin" /></div>
|
||||
) : (
|
||||
<div className="max-h-64 overflow-auto rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-left text-muted-foreground border-b">
|
||||
<th className="p-2">Database</th>
|
||||
<th className="p-2">Will restore from</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((r) => (
|
||||
<tr key={r.databaseId} className="border-b last:border-0">
|
||||
<td className={`p-2 ${r.restorable ? "" : "opacity-50"}`}>{r.name}</td>
|
||||
<td className="p-2">
|
||||
{r.restorable
|
||||
? <span className="text-green-600">{r.backupDate ? new Date(r.backupDate).toLocaleString() : "latest backup"}</span>
|
||||
: <span className="text-amber-500">no successful backup — skipped</span>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{restorableCount > 0 && (
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs text-muted-foreground">Type "{CONFIRM_WORD}" to confirm</label>
|
||||
<Input value={typed} onChange={(e) => setTyped(e.target.value)} placeholder={CONFIRM_WORD} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onCancel} disabled={submitting}>Cancel</Button>
|
||||
<Button variant="destructive" onClick={onConfirm} disabled={!canConfirm}>
|
||||
{submitting && <Loader2 className="animate-spin" />}
|
||||
{restorableCount > 0 ? `Restore ${restorableCount} database(s)` : "Nothing to restore"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user