feat: delete multiple non empty folders
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -136,6 +136,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertIn('id="batch-move-popup"', body)
|
||||
self.assertIn('id="batch-move-apply-btn"', 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-cancel-btn"', body)
|
||||
self.assertIn("Delete folder and contents?", body)
|
||||
@@ -206,8 +207,10 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertIn('function toggleUploadMenu()', app_js)
|
||||
self.assertNotIn('if (event.altKey) {', app_js)
|
||||
self.assertIn('document.getElementById("settings-btn").onclick = () => openSettings("general");', app_js)
|
||||
self.assertIn('err.code === "directory_not_empty"', app_js)
|
||||
self.assertIn('openDeleteConfirmModal(item.path);', app_js)
|
||||
self.assertIn('function collectDeleteRecursivePaths(selectedItems)', 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('await loadSettings();', 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 handleFolderSelection(event)', 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('recursive: true', app_js)
|
||||
self.assertIn('err.code === "directory_not_empty"', app_js)
|
||||
self.assertIn('recursive: recursivePaths.has(item.path)', app_js)
|
||||
self.assertIn('input.setAttribute("webkitdirectory", "")', app_js)
|
||||
self.assertIn('await apiRequest("POST", "/api/files/mkdir", {', app_js)
|
||||
self.assertIn('await uploadFileRequest(targetPath, entry.file, overwrite);', app_js)
|
||||
|
||||
+95
-45
@@ -48,7 +48,9 @@ let renameState = {
|
||||
name: "",
|
||||
};
|
||||
let deleteConfirmState = {
|
||||
path: null,
|
||||
pane: "left",
|
||||
items: [],
|
||||
recursivePaths: [],
|
||||
};
|
||||
let batchMoveState = {
|
||||
destinationBase: "",
|
||||
@@ -296,6 +298,8 @@ function batchMoveElements() {
|
||||
function deleteConfirmElements() {
|
||||
return {
|
||||
overlay: document.getElementById("delete-confirm-modal"),
|
||||
title: document.getElementById("delete-confirm-title"),
|
||||
message: document.getElementById("delete-confirm-message"),
|
||||
path: document.getElementById("delete-confirm-path"),
|
||||
error: document.getElementById("delete-confirm-error"),
|
||||
applyButton: document.getElementById("delete-confirm-apply-btn"),
|
||||
@@ -1884,65 +1888,54 @@ async function renameSelected() {
|
||||
|
||||
function closeDeleteConfirmModal() {
|
||||
const elements = deleteConfirmElements();
|
||||
deleteConfirmState.path = null;
|
||||
deleteConfirmState.pane = "left";
|
||||
deleteConfirmState.items = [];
|
||||
deleteConfirmState.recursivePaths = [];
|
||||
elements.error.textContent = "";
|
||||
elements.overlay.classList.add("hidden");
|
||||
}
|
||||
|
||||
function openDeleteConfirmModal(path) {
|
||||
function openDeleteConfirmModal(pane, items, recursivePaths) {
|
||||
const elements = deleteConfirmElements();
|
||||
deleteConfirmState.path = path;
|
||||
elements.path.textContent = path;
|
||||
deleteConfirmState.pane = pane;
|
||||
deleteConfirmState.items = items.map((item) => ({ ...item }));
|
||||
deleteConfirmState.recursivePaths = Array.from(recursivePaths);
|
||||
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");
|
||||
}
|
||||
|
||||
async function submitDeleteConfirmModal() {
|
||||
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", "");
|
||||
async function executeDeleteItems(pane, items, recursivePaths) {
|
||||
let successes = 0;
|
||||
let failures = 0;
|
||||
let firstError = null;
|
||||
for (const item of selectedItems) {
|
||||
for (const item of items) {
|
||||
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;
|
||||
} catch (err) {
|
||||
if (
|
||||
err.code === "directory_not_empty"
|
||||
&& selectedItems.length === 1
|
||||
&& item.kind === "directory"
|
||||
) {
|
||||
failures = 0;
|
||||
firstError = null;
|
||||
openDeleteConfirmModal(item.path);
|
||||
return;
|
||||
if (err.code === "directory_not_empty" && recursivePaths.has(item.path)) {
|
||||
try {
|
||||
await apiRequest("POST", "/api/files/delete", {
|
||||
path: item.path,
|
||||
recursive: true,
|
||||
});
|
||||
successes += 1;
|
||||
continue;
|
||||
} catch (retryErr) {
|
||||
err = retryErr;
|
||||
}
|
||||
}
|
||||
failures += 1;
|
||||
if (!firstError) {
|
||||
@@ -1955,6 +1948,63 @@ async function deleteSelected() {
|
||||
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) {
|
||||
const sourceName = baseName(sourcePath);
|
||||
return `${targetBasePath}/${sourceName}`;
|
||||
|
||||
+2
-1
@@ -565,7 +565,8 @@ button:disabled {
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
max-width: 760px;
|
||||
max-width: none;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
#function-bar button {
|
||||
|
||||
@@ -270,7 +270,7 @@
|
||||
<div id="delete-confirm-modal" class="popup-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="delete-confirm-title">
|
||||
<div class="popup-card">
|
||||
<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-error" class="error"></div>
|
||||
<div class="popup-actions">
|
||||
|
||||
Reference in New Issue
Block a user