feat: replace browser confirm with gui modal for delete
All checks were successful
Automated Container Build / build-and-push (push) Successful in 40s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 40s
This commit is contained in:
parent
ee36e16dae
commit
ee441dfefd
1 changed files with 58 additions and 10 deletions
|
|
@ -244,6 +244,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
const [draggedFile, setDraggedFile] = useState<string | null>(null);
|
const [draggedFile, setDraggedFile] = useState<string | null>(null);
|
||||||
const [uploads, setUploads] = useState<UploadProgress[]>([]);
|
const [uploads, setUploads] = useState<UploadProgress[]>([]);
|
||||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||||
|
const [deleteConfirm, setDeleteConfirm] = useState<{ type: 'single' | 'multiple'; file?: FileItem; count?: number } | null>(null);
|
||||||
|
|
||||||
function showToast(message: string, type: 'success' | 'error' | 'info' = 'success') {
|
function showToast(message: string, type: 'success' | 'error' | 'info' = 'success') {
|
||||||
const id = Date.now() + Math.random();
|
const id = Date.now() + Math.random();
|
||||||
|
|
@ -384,7 +385,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigateUp();
|
navigateUp();
|
||||||
} else if (e.key === 'Delete') {
|
} else if (e.key === 'Delete') {
|
||||||
deleteSelected();
|
requestDeleteSelected();
|
||||||
} else if (e.key === 'a' && (e.ctrlKey || e.metaKey)) {
|
} else if (e.key === 'a' && (e.ctrlKey || e.metaKey)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
selectAll();
|
selectAll();
|
||||||
|
|
@ -613,18 +614,29 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
setSelectedFiles(new Set(files.map(f => f.path)));
|
setSelectedFiles(new Set(files.map(f => f.path)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteSelected() {
|
function requestDeleteSelected() {
|
||||||
if (selectedFiles.size === 0) return;
|
if (selectedFiles.size === 0) return;
|
||||||
if (!confirm(`Move ${selectedFiles.size} items to trash?`)) return;
|
setDeleteConfirm({ type: 'multiple', count: selectedFiles.size });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeDeleteSelected() {
|
||||||
const count = selectedFiles.size;
|
const count = selectedFiles.size;
|
||||||
for (const path of selectedFiles) {
|
for (const path of selectedFiles) {
|
||||||
await api.deleteFile(path);
|
await api.deleteFile(path);
|
||||||
}
|
}
|
||||||
setSelectedFiles(new Set());
|
setSelectedFiles(new Set());
|
||||||
loadFiles(undefined, true);
|
loadFiles(undefined, true);
|
||||||
|
setDeleteConfirm(null);
|
||||||
showToast(`Deleted ${count} item${count > 1 ? 's' : ''}`);
|
showToast(`Deleted ${count} item${count > 1 ? 's' : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function executeDeleteSingle(file: FileItem) {
|
||||||
|
await api.deleteFile(file.path);
|
||||||
|
loadFiles(undefined, true);
|
||||||
|
setDeleteConfirm(null);
|
||||||
|
showToast(`Deleted ${file.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
async function handlePin(file: FileItem) {
|
async function handlePin(file: FileItem) {
|
||||||
await api.togglePin(file.path);
|
await api.togglePin(file.path);
|
||||||
loadFiles(undefined, true);
|
loadFiles(undefined, true);
|
||||||
|
|
@ -1578,7 +1590,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
<button
|
<button
|
||||||
className="p-2 hover:bg-red-500/10 rounded-lg transition-colors flex items-center gap-2 text-sm text-red-500"
|
className="p-2 hover:bg-red-500/10 rounded-lg transition-colors flex items-center gap-2 text-sm text-red-500"
|
||||||
title="Delete Selected"
|
title="Delete Selected"
|
||||||
onClick={() => deleteSelected()}
|
onClick={() => requestDeleteSelected()}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1693,12 +1705,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
label="Move to Trash"
|
label="Move to Trash"
|
||||||
danger
|
danger
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (confirm(`Move ${contextMenu.file.name} to trash?`)) {
|
setDeleteConfirm({ type: 'single', file: contextMenu.file });
|
||||||
api.deleteFile(contextMenu.file.path).then(() => {
|
|
||||||
loadFiles(undefined, true);
|
|
||||||
showToast(`Deleted ${contextMenu.file.name}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1857,6 +1864,47 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{deleteConfirm && (
|
||||||
|
<div className="fixed inset-0 z-[120] flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95 }}
|
||||||
|
className="w-full max-w-md overflow-hidden rounded-2xl shadow-xl"
|
||||||
|
style={{ backgroundColor: 'var(--color-bg-primary)', border: '1px solid var(--color-border)' }}
|
||||||
|
>
|
||||||
|
<div className="p-6">
|
||||||
|
<h2 className="text-xl font-bold mb-2" style={{ color: 'var(--color-text-primary)' }}>Move to Trash?</h2>
|
||||||
|
<p className="mb-6" style={{ color: 'var(--color-text-secondary)' }}>
|
||||||
|
{deleteConfirm.type === 'multiple'
|
||||||
|
? `Are you sure you want to move ${deleteConfirm.count} items to the trash?`
|
||||||
|
: `Are you sure you want to move ${deleteConfirm.file?.name} to the trash?`}
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setDeleteConfirm(null)}
|
||||||
|
className="px-4 py-2 rounded-lg text-sm font-medium transition-colors hover:opacity-80"
|
||||||
|
style={{ backgroundColor: 'var(--color-bg-tertiary)', color: 'var(--color-text-primary)' }}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (deleteConfirm.type === 'multiple') executeDeleteSelected();
|
||||||
|
else if (deleteConfirm.file) executeDeleteSingle(deleteConfirm.file);
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 rounded-lg text-sm font-medium transition-colors hover:opacity-90 bg-red-500 text-white"
|
||||||
|
>
|
||||||
|
Move to Trash
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showTrashModal && (
|
{showTrashModal && (
|
||||||
<div className="fixed inset-0 z-[120] flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
<div className="fixed inset-0 z-[120] flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||||
|
|
|
||||||
Reference in a new issue