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-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
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user