feat(confirm-dialog): implement ConfirmDialog component for user confirmations
This commit is contained in:
parent
07bbf00aa9
commit
1234ba30d9
|
|
@ -33,6 +33,7 @@ import {
|
|||
DialogDescription,
|
||||
} from './ui/dialog';
|
||||
import { MediaEntry } from './MediaEntry';
|
||||
import { ConfirmDialog, type ConfirmOptions } from './ui/confirm-dialog';
|
||||
|
||||
interface MediaBrowserProps {
|
||||
currentPath: string;
|
||||
|
|
@ -49,6 +50,7 @@ export default function MediaBrowser({ currentPath, onSelect, onClose }: MediaBr
|
|||
const [newFolderName, setNewFolderName] = useState('');
|
||||
const [actionEntry, setActionEntry] = useState<EntryInfo | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [confirm, setConfirm] = useState<ConfirmOptions | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const initial = normalizePath(currentPath || '/');
|
||||
|
|
@ -91,21 +93,38 @@ export default function MediaBrowser({ currentPath, onSelect, onClose }: MediaBr
|
|||
} catch (e: any) { toast.error(`Failed to create folder: ${e?.message ?? e}`); }
|
||||
};
|
||||
|
||||
const handleDelete = async (entry: EntryInfo) => {
|
||||
if (!window.confirm(entry.type === 'folder' ? `Delete folder "${entry.name}" and all its contents?` : `Delete file "${entry.name}"?`)) return;
|
||||
const handleDelete = (entry: EntryInfo) => {
|
||||
setActionEntry(null);
|
||||
try { await deletePath(entry.path); toast.success(`Deleted ${entry.name}`); if (path) void load(path); }
|
||||
catch (e: any) { toast.error(`Failed to delete: ${e?.message ?? e}`); }
|
||||
setConfirm({
|
||||
title: entry.type === 'folder' ? `Delete folder "${entry.name}"?` : `Delete "${entry.name}"?`,
|
||||
description: entry.type === 'folder' ? 'This will delete the folder and all its contents.' : undefined,
|
||||
confirmLabel: 'Delete',
|
||||
destructive: true,
|
||||
onConfirm: async () => {
|
||||
try { await deletePath(entry.path); toast.success(`Deleted ${entry.name}`); if (path) void load(path); }
|
||||
catch (e: any) { toast.error(`Failed to delete: ${e?.message ?? e}`); }
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpload = async (file: File) => {
|
||||
if (!path) return;
|
||||
const target = joinPath(path, file.name);
|
||||
const doUpload = async () => {
|
||||
try { await putFileContents(target, await file.arrayBuffer()); toast.success(`Uploaded ${file.name}`); void load(path!); }
|
||||
catch (e: any) { toast.error(`Failed to upload ${file.name}: ${e?.message ?? e}`); }
|
||||
};
|
||||
if (await fileExists(target)) {
|
||||
if (!window.confirm(`"${file.name}" already exists. Overwrite?`)) return;
|
||||
setConfirm({
|
||||
title: `"${file.name}" already exists`,
|
||||
description: 'Replace the existing file?',
|
||||
confirmLabel: 'Overwrite',
|
||||
destructive: true,
|
||||
onConfirm: doUpload,
|
||||
});
|
||||
return;
|
||||
}
|
||||
try { await putFileContents(target, await file.arrayBuffer()); toast.success(`Uploaded ${file.name}`); void load(path); }
|
||||
catch (e: any) { toast.error(`Failed to upload ${file.name}: ${e?.message ?? e}`); }
|
||||
await doUpload();
|
||||
};
|
||||
|
||||
const onPickFiles = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
|
@ -200,6 +219,12 @@ export default function MediaBrowser({ currentPath, onSelect, onClose }: MediaBr
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirm !== null}
|
||||
{...(confirm ?? { title: '', onConfirm: () => {} })}
|
||||
onCancel={() => setConfirm(null)}
|
||||
/>
|
||||
|
||||
<Dialog open={actionEntry !== null} onOpenChange={open => !open && setActionEntry(null)}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ import {
|
|||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from './ui/dialog';
|
||||
import { ConfirmDialog, type ConfirmOptions } from './ui/confirm-dialog';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -372,6 +373,7 @@ export default function MediaManager({ initialPath = '/', rootPath, title, confi
|
|||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const renameInputRef = useRef<HTMLInputElement>(null);
|
||||
const dragCounter = useRef(0);
|
||||
const [confirm, setConfirm] = useState<ConfirmOptions | null>(null);
|
||||
|
||||
// ── Directory loading ────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -564,23 +566,34 @@ export default function MediaManager({ initialPath = '/', rootPath, title, confi
|
|||
|
||||
// ── Delete ───────────────────────────────────────────────────────────────
|
||||
|
||||
const deleteEntry = async (entry: EntryInfo) => {
|
||||
if (!window.confirm(entry.type === 'folder'
|
||||
? `Delete folder "${entry.name}" and all its contents?`
|
||||
: `Delete "${entry.name}"?`)) return;
|
||||
try { await deletePath(entry.path); toast.success(`Deleted ${entry.name}`); void load(path); }
|
||||
catch (e: any) { toast.error(`Delete failed: ${e?.message ?? e}`); }
|
||||
const deleteEntry = (entry: EntryInfo) => {
|
||||
setConfirm({
|
||||
title: entry.type === 'folder' ? `Delete folder "${entry.name}"?` : `Delete "${entry.name}"?`,
|
||||
description: entry.type === 'folder' ? 'This will delete the folder and all its contents.' : undefined,
|
||||
confirmLabel: 'Delete',
|
||||
destructive: true,
|
||||
onConfirm: async () => {
|
||||
try { await deletePath(entry.path); toast.success(`Deleted ${entry.name}`); void load(path); }
|
||||
catch (e: any) { toast.error(`Delete failed: ${e?.message ?? e}`); }
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteSelected = async () => {
|
||||
const deleteSelected = () => {
|
||||
const targets = entries.filter(e => selected.has(e.path));
|
||||
if (!window.confirm(`Delete ${targets.length} item${targets.length !== 1 ? 's' : ''}?`)) return;
|
||||
let failed = 0;
|
||||
for (const e of targets) { try { await deletePath(e.path); } catch { failed++; } }
|
||||
if (failed) toast.error(`${failed} item${failed !== 1 ? 's' : ''} failed to delete`);
|
||||
else toast.success(`Deleted ${targets.length} item${targets.length !== 1 ? 's' : ''}`);
|
||||
void load(path);
|
||||
setSelected(new Set());
|
||||
setConfirm({
|
||||
title: `Delete ${targets.length} item${targets.length !== 1 ? 's' : ''}?`,
|
||||
confirmLabel: 'Delete',
|
||||
destructive: true,
|
||||
onConfirm: async () => {
|
||||
let failed = 0;
|
||||
for (const e of targets) { try { await deletePath(e.path); } catch { failed++; } }
|
||||
if (failed) toast.error(`${failed} item${failed !== 1 ? 's' : ''} failed to delete`);
|
||||
else toast.success(`Deleted ${targets.length} item${targets.length !== 1 ? 's' : ''}`);
|
||||
void load(path);
|
||||
setSelected(new Set());
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ── Rename ───────────────────────────────────────────────────────────────
|
||||
|
|
@ -752,14 +765,24 @@ export default function MediaManager({ initialPath = '/', rootPath, title, confi
|
|||
|
||||
const handleUpload = async (file: File) => {
|
||||
const target = joinPath(path, file.name);
|
||||
const doUpload = async () => {
|
||||
try {
|
||||
await putFileContents(target, await file.arrayBuffer());
|
||||
toast.success(`Uploaded ${file.name}`);
|
||||
void load(path);
|
||||
} catch (e: any) { toast.error(`Upload failed for ${file.name}: ${e?.message ?? e}`); }
|
||||
};
|
||||
if (await fileExists(target)) {
|
||||
if (!window.confirm(`"${file.name}" already exists. Overwrite?`)) return;
|
||||
setConfirm({
|
||||
title: `"${file.name}" already exists`,
|
||||
description: 'Replace the existing file?',
|
||||
confirmLabel: 'Overwrite',
|
||||
destructive: true,
|
||||
onConfirm: doUpload,
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await putFileContents(target, await file.arrayBuffer());
|
||||
toast.success(`Uploaded ${file.name}`);
|
||||
void load(path);
|
||||
} catch (e: any) { toast.error(`Upload failed for ${file.name}: ${e?.message ?? e}`); }
|
||||
await doUpload();
|
||||
};
|
||||
|
||||
const onPickFiles = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
|
@ -1128,6 +1151,12 @@ export default function MediaManager({ initialPath = '/', rootPath, title, confi
|
|||
</Suspense>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirm !== null}
|
||||
{...(confirm ?? { title: '', onConfirm: () => {} })}
|
||||
onCancel={() => setConfirm(null)}
|
||||
/>
|
||||
|
||||
{/* ── Mount dialog ── */}
|
||||
<Dialog open={mountEntry !== null} onOpenChange={open => !open && setMountEntry(null)}>
|
||||
<DialogContent className="max-w-sm">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import DeviceDetailOverlay from './DeviceDetailOverlay';
|
|||
import MediaSet from './MediaSet';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
|
||||
import { ConfirmDialog, type ConfirmOptions } from './ui/confirm-dialog';
|
||||
|
||||
interface StatusPageProps {
|
||||
config: any;
|
||||
|
|
@ -88,6 +89,7 @@ export default function StatusPage({ config, setConfig }: StatusPageProps) {
|
|||
const [printFiles, setPrintFiles] = useState<EntryInfo[]>([]);
|
||||
const [printLoading, setPrintLoading] = useState(true);
|
||||
const [printActionEntry, setPrintActionEntry] = useState<EntryInfo | null>(null);
|
||||
const [confirm, setConfirm] = useState<ConfirmOptions | null>(null);
|
||||
|
||||
const loadPrintFiles = () => {
|
||||
setPrintLoading(true);
|
||||
|
|
@ -111,13 +113,19 @@ export default function StatusPage({ config, setConfig }: StatusPageProps) {
|
|||
} catch (e: any) { toast.error(`Download failed: ${e?.message ?? e}`); }
|
||||
};
|
||||
|
||||
const deletePrintFile = async (entry: EntryInfo) => {
|
||||
if (!window.confirm(`Delete "${entry.name}"?`)) return;
|
||||
try {
|
||||
await deletePath(entry.path);
|
||||
toast.success(`Deleted ${entry.name}`);
|
||||
loadPrintFiles();
|
||||
} catch (e: any) { toast.error(`Delete failed: ${e?.message ?? e}`); }
|
||||
const deletePrintFile = (entry: EntryInfo) => {
|
||||
setConfirm({
|
||||
title: `Delete "${entry.name}"?`,
|
||||
confirmLabel: 'Delete',
|
||||
destructive: true,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await deletePath(entry.path);
|
||||
toast.success(`Deleted ${entry.name}`);
|
||||
loadPrintFiles();
|
||||
} catch (e: any) { toast.error(`Delete failed: ${e?.message ?? e}`); }
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -163,7 +171,7 @@ export default function StatusPage({ config, setConfig }: StatusPageProps) {
|
|||
{/* New device info cards */}
|
||||
<div className="mb-4">
|
||||
<div className="bg-neutral-50 rounded-lg p-3 flex flex-col items-start justify-center w-full mb-2">
|
||||
<div className="text-xs text-neutral-500 mb-1">Last File</div>
|
||||
<div className="text-xs text-neutral-500 mb-1">Last File Access</div>
|
||||
<div className="text-sm font-medium break-all w-full text-left">{lastFile}</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between gap-4 w-full">
|
||||
|
|
@ -459,6 +467,11 @@ export default function StatusPage({ config, setConfig }: StatusPageProps) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirm !== null}
|
||||
{...(confirm ?? { title: '', onConfirm: () => {} })}
|
||||
onCancel={() => setConfirm(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
|||
import { Wrench, Download, Upload, RotateCcw, Database, FileText, HardDrive, X, Loader2, Cpu, ChevronRight, AlertCircle } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { getFileContents, putFileContents, humanFileSize } from '../webdav';
|
||||
import { ConfirmDialog, type ConfirmOptions } from './ui/confirm-dialog';
|
||||
|
||||
interface ToolsPageProps {
|
||||
config: any;
|
||||
|
|
@ -139,6 +140,7 @@ function FirmwareOverlay({ onClose }: { onClose: () => void }) {
|
|||
|
||||
export default function ToolsPage({ config }: ToolsPageProps) {
|
||||
const [showFirmware, setShowFirmware] = useState(false);
|
||||
const [confirm, setConfirm] = useState<ConfirmOptions | null>(null);
|
||||
|
||||
const handleBackup = () => {
|
||||
const dataStr = JSON.stringify(config, null, 2);
|
||||
|
|
@ -175,15 +177,23 @@ export default function ToolsPage({ config }: ToolsPageProps) {
|
|||
};
|
||||
|
||||
const handleFactoryReset = () => {
|
||||
if (confirm('Are you sure you want to reset to factory defaults? This cannot be undone.')) {
|
||||
toast.success('Configuration reset to factory defaults');
|
||||
}
|
||||
setConfirm({
|
||||
title: 'Factory Reset',
|
||||
description: 'Reset to default settings? This cannot be undone.',
|
||||
confirmLabel: 'Reset',
|
||||
destructive: true,
|
||||
onConfirm: () => toast.success('Configuration reset to factory defaults'),
|
||||
});
|
||||
};
|
||||
|
||||
const handleFormatSD = () => {
|
||||
if (confirm('Are you sure you want to format the SD card? All data will be lost.')) {
|
||||
toast.success('SD card formatting started');
|
||||
}
|
||||
setConfirm({
|
||||
title: 'Format SD Card',
|
||||
description: 'All data on the SD card will be lost.',
|
||||
confirmLabel: 'Format',
|
||||
destructive: true,
|
||||
onConfirm: () => toast.success('SD card formatting started'),
|
||||
});
|
||||
};
|
||||
|
||||
const handleSystemUpdate = () => {
|
||||
|
|
@ -341,6 +351,12 @@ export default function ToolsPage({ config }: ToolsPageProps) {
|
|||
|
||||
|
||||
{showFirmware && <FirmwareOverlay onClose={() => setShowFirmware(false)} />}
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirm !== null}
|
||||
{...(confirm ?? { title: '', onConfirm: () => {} })}
|
||||
onCancel={() => setConfirm(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
41
src/app/components/ui/confirm-dialog.tsx
Normal file
41
src/app/components/ui/confirm-dialog.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './dialog';
|
||||
|
||||
export interface ConfirmOptions {
|
||||
title: string;
|
||||
description?: string;
|
||||
confirmLabel?: string;
|
||||
destructive?: boolean;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
interface Props extends ConfirmOptions {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function ConfirmDialog({ open, title, description, confirmLabel = 'Confirm', destructive = false, onConfirm, onCancel }: Props) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(o) => { if (!o) onCancel(); }}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
{description && <DialogDescription>{description}</DialogDescription>}
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-sm border border-neutral-200 rounded-lg hover:bg-neutral-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { onConfirm(); onCancel(); }}
|
||||
className={`px-4 py-2 text-sm rounded-lg text-white ${destructive ? 'bg-red-600 hover:bg-red-700' : 'bg-blue-600 hover:bg-blue-700'}`}
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user