feat: delete multiple non empty folders

This commit is contained in:
kodi
2026-03-14 08:36:47 +01:00
parent d84b3da561
commit 3987de27e0
6 changed files with 106 additions and 52 deletions
Binary file not shown.
@@ -136,6 +136,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('id="batch-move-popup"', body) self.assertIn('id="batch-move-popup"', body)
self.assertIn('id="batch-move-apply-btn"', body) self.assertIn('id="batch-move-apply-btn"', body)
self.assertIn('id="delete-confirm-modal"', body) self.assertIn('id="delete-confirm-modal"', body)
self.assertIn('id="delete-confirm-message"', body)
self.assertIn('id="delete-confirm-apply-btn"', body) self.assertIn('id="delete-confirm-apply-btn"', body)
self.assertIn('id="delete-confirm-cancel-btn"', body) self.assertIn('id="delete-confirm-cancel-btn"', body)
self.assertIn("Delete folder and contents?", body) self.assertIn("Delete folder and contents?", body)
@@ -206,8 +207,10 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('function toggleUploadMenu()', app_js) self.assertIn('function toggleUploadMenu()', app_js)
self.assertNotIn('if (event.altKey) {', app_js) self.assertNotIn('if (event.altKey) {', app_js)
self.assertIn('document.getElementById("settings-btn").onclick = () => openSettings("general");', app_js) self.assertIn('document.getElementById("settings-btn").onclick = () => openSettings("general");', app_js)
self.assertIn('err.code === "directory_not_empty"', app_js) self.assertIn('function collectDeleteRecursivePaths(selectedItems)', app_js)
self.assertIn('openDeleteConfirmModal(item.path);', app_js) self.assertIn('openDeleteConfirmModal(pane, selectedItems, recursivePaths);', app_js)
self.assertIn('recursivePaths.has(item.path)', app_js)
self.assertIn('Delete selected items and folder contents?', app_js)
self.assertIn('async function loadSettings()', app_js) self.assertIn('async function loadSettings()', app_js)
self.assertIn('await loadSettings();', app_js) self.assertIn('await loadSettings();', app_js)
self.assertIn('settings.showThumbnailsInput.onchange = handleShowThumbnailsChange;', app_js) self.assertIn('settings.showThumbnailsInput.onchange = handleShowThumbnailsChange;', app_js)
@@ -239,10 +242,10 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('async function executeFolderUploadPlan(plan)', app_js) self.assertIn('async function executeFolderUploadPlan(plan)', app_js)
self.assertIn('async function handleFolderSelection(event)', app_js) self.assertIn('async function handleFolderSelection(event)', app_js)
self.assertIn('function deleteConfirmElements()', app_js) self.assertIn('function deleteConfirmElements()', app_js)
self.assertIn('function openDeleteConfirmModal(path)', app_js) self.assertIn('function openDeleteConfirmModal(pane, items, recursivePaths)', app_js)
self.assertIn('async function executeDeleteItems(pane, items, recursivePaths)', app_js)
self.assertIn('async function submitDeleteConfirmModal()', app_js) self.assertIn('async function submitDeleteConfirmModal()', app_js)
self.assertIn('recursive: true', app_js) self.assertIn('recursive: recursivePaths.has(item.path)', app_js)
self.assertIn('err.code === "directory_not_empty"', app_js)
self.assertIn('input.setAttribute("webkitdirectory", "")', app_js) self.assertIn('input.setAttribute("webkitdirectory", "")', app_js)
self.assertIn('await apiRequest("POST", "/api/files/mkdir", {', app_js) self.assertIn('await apiRequest("POST", "/api/files/mkdir", {', app_js)
self.assertIn('await uploadFileRequest(targetPath, entry.file, overwrite);', app_js) self.assertIn('await uploadFileRequest(targetPath, entry.file, overwrite);', app_js)
+95 -45
View File
@@ -48,7 +48,9 @@ let renameState = {
name: "", name: "",
}; };
let deleteConfirmState = { let deleteConfirmState = {
path: null, pane: "left",
items: [],
recursivePaths: [],
}; };
let batchMoveState = { let batchMoveState = {
destinationBase: "", destinationBase: "",
@@ -296,6 +298,8 @@ function batchMoveElements() {
function deleteConfirmElements() { function deleteConfirmElements() {
return { return {
overlay: document.getElementById("delete-confirm-modal"), overlay: document.getElementById("delete-confirm-modal"),
title: document.getElementById("delete-confirm-title"),
message: document.getElementById("delete-confirm-message"),
path: document.getElementById("delete-confirm-path"), path: document.getElementById("delete-confirm-path"),
error: document.getElementById("delete-confirm-error"), error: document.getElementById("delete-confirm-error"),
applyButton: document.getElementById("delete-confirm-apply-btn"), applyButton: document.getElementById("delete-confirm-apply-btn"),
@@ -1884,65 +1888,54 @@ async function renameSelected() {
function closeDeleteConfirmModal() { function closeDeleteConfirmModal() {
const elements = deleteConfirmElements(); const elements = deleteConfirmElements();
deleteConfirmState.path = null; deleteConfirmState.pane = "left";
deleteConfirmState.items = [];
deleteConfirmState.recursivePaths = [];
elements.error.textContent = ""; elements.error.textContent = "";
elements.overlay.classList.add("hidden"); elements.overlay.classList.add("hidden");
} }
function openDeleteConfirmModal(path) { function openDeleteConfirmModal(pane, items, recursivePaths) {
const elements = deleteConfirmElements(); const elements = deleteConfirmElements();
deleteConfirmState.path = path; deleteConfirmState.pane = pane;
elements.path.textContent = path; deleteConfirmState.items = items.map((item) => ({ ...item }));
deleteConfirmState.recursivePaths = Array.from(recursivePaths);
elements.error.textContent = ""; elements.error.textContent = "";
if (items.length === 1) {
elements.title.textContent = "Delete folder and contents?";
elements.message.textContent = "This will permanently delete the folder and all files and subfolders inside it.";
elements.path.textContent = items[0].path;
} else {
elements.title.textContent = "Delete selected items and folder contents?";
elements.message.textContent = `This will permanently delete ${items.length} selected items, including all files and subfolders inside the selected folders.`;
elements.path.textContent = `${items.length} selected items`;
}
elements.overlay.classList.remove("hidden"); elements.overlay.classList.remove("hidden");
} }
async function submitDeleteConfirmModal() { async function executeDeleteItems(pane, items, recursivePaths) {
const path = deleteConfirmState.path;
if (!path) {
return;
}
const elements = deleteConfirmElements();
elements.error.textContent = "";
try {
await apiRequest("POST", "/api/files/delete", { path, recursive: true });
closeDeleteConfirmModal();
setSelectedItem(state.activePane, null);
await loadBrowsePane(state.activePane);
setStatus("Delete: 1 success, 0 failed");
setError("actions-error", "");
} catch (err) {
elements.error.textContent = err.message;
}
}
async function deleteSelected() {
const pane = state.activePane;
const selectedItems = [...paneState(pane).selectedItems];
if (selectedItems.length === 0) {
return;
}
if (!window.confirm(`Delete ${selectedItems.length} selected item(s)?`)) {
return;
}
setError("actions-error", "");
let successes = 0; let successes = 0;
let failures = 0; let failures = 0;
let firstError = null; let firstError = null;
for (const item of selectedItems) { for (const item of items) {
try { try {
await apiRequest("POST", "/api/files/delete", { path: item.path }); await apiRequest("POST", "/api/files/delete", {
path: item.path,
recursive: recursivePaths.has(item.path),
});
successes += 1; successes += 1;
} catch (err) { } catch (err) {
if ( if (err.code === "directory_not_empty" && recursivePaths.has(item.path)) {
err.code === "directory_not_empty" try {
&& selectedItems.length === 1 await apiRequest("POST", "/api/files/delete", {
&& item.kind === "directory" path: item.path,
) { recursive: true,
failures = 0; });
firstError = null; successes += 1;
openDeleteConfirmModal(item.path); continue;
return; } catch (retryErr) {
err = retryErr;
}
} }
failures += 1; failures += 1;
if (!firstError) { if (!firstError) {
@@ -1955,6 +1948,63 @@ async function deleteSelected() {
showActionSummary("Delete", successes, failures, firstError); showActionSummary("Delete", successes, failures, firstError);
} }
async function submitDeleteConfirmModal() {
const elements = deleteConfirmElements();
if (!deleteConfirmState.items.length) {
return;
}
elements.error.textContent = "";
try {
const pane = deleteConfirmState.pane;
const items = [...deleteConfirmState.items];
const recursivePaths = new Set(deleteConfirmState.recursivePaths);
closeDeleteConfirmModal();
await executeDeleteItems(pane, items, recursivePaths);
} catch (err) {
elements.error.textContent = err.message;
}
}
async function collectDeleteRecursivePaths(selectedItems) {
const recursivePaths = new Set();
for (const item of selectedItems) {
if (item.kind !== "directory") {
continue;
}
const query = new URLSearchParams({
path: item.path,
show_hidden: "true",
});
const data = await apiRequest("GET", `/api/browse?${query.toString()}`);
if ((data.directories && data.directories.length > 0) || (data.files && data.files.length > 0)) {
recursivePaths.add(item.path);
}
}
return recursivePaths;
}
async function deleteSelected() {
const pane = state.activePane;
const selectedItems = [...paneState(pane).selectedItems];
if (selectedItems.length === 0) {
return;
}
setError("actions-error", "");
try {
const recursivePaths = await collectDeleteRecursivePaths(selectedItems);
if (recursivePaths.size > 0) {
openDeleteConfirmModal(pane, selectedItems, recursivePaths);
return;
}
if (!window.confirm(`Delete ${selectedItems.length} selected item(s)?`)) {
return;
}
await executeDeleteItems(pane, selectedItems, new Set());
} catch (err) {
setActionError("Delete", err);
}
}
function defaultDestination(sourcePath, targetBasePath) { function defaultDestination(sourcePath, targetBasePath) {
const sourceName = baseName(sourcePath); const sourceName = baseName(sourcePath);
return `${targetBasePath}/${sourceName}`; return `${targetBasePath}/${sourceName}`;
+2 -1
View File
@@ -565,7 +565,8 @@ button:disabled {
justify-content: center; justify-content: center;
gap: 5px; gap: 5px;
width: 100%; width: 100%;
max-width: 760px; max-width: none;
flex-wrap: nowrap;
} }
#function-bar button { #function-bar button {
+1 -1
View File
@@ -270,7 +270,7 @@
<div id="delete-confirm-modal" class="popup-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="delete-confirm-title"> <div id="delete-confirm-modal" class="popup-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="delete-confirm-title">
<div class="popup-card"> <div class="popup-card">
<h3 id="delete-confirm-title">Delete folder and contents?</h3> <h3 id="delete-confirm-title">Delete folder and contents?</h3>
<div class="popup-meta">This will permanently delete the folder and all files and subfolders inside it.</div> <div id="delete-confirm-message" class="popup-meta">This will permanently delete the folder and all files and subfolders inside it.</div>
<div id="delete-confirm-path" class="popup-meta"></div> <div id="delete-confirm-path" class="popup-meta"></div>
<div id="delete-confirm-error" class="error"></div> <div id="delete-confirm-error" class="error"></div>
<div class="popup-actions"> <div class="popup-actions">