diff --git a/txt b/txt deleted file mode 100644 index 3e9f489..0000000 --- a/txt +++ /dev/null @@ -1,2 +0,0 @@ -dd - diff --git a/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc b/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc index 8836536..df2bea2 100644 Binary files a/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc and b/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc differ diff --git a/webui/backend/tests/golden/test_ui_smoke_golden.py b/webui/backend/tests/golden/test_ui_smoke_golden.py index 397bb2f..f85ee70 100644 --- a/webui/backend/tests/golden/test_ui_smoke_golden.py +++ b/webui/backend/tests/golden/test_ui_smoke_golden.py @@ -30,6 +30,9 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('id="footer-bar"', body) self.assertIn('id="left-pane"', body) self.assertIn('id="right-pane"', body) + self.assertIn('id="left-items"', body) + self.assertIn('id="right-items"', body) + self.assertIn('id="mkdir-btn"', body) self.assertIn('id="left-breadcrumbs"', body) self.assertIn('id="right-breadcrumbs"', body) self.assertNotIn('id="bookmarks-panel"', body) diff --git a/webui/html/app.js b/webui/html/app.js index 33b2eb7..d0cb5be 100644 --- a/webui/html/app.js +++ b/webui/html/app.js @@ -41,6 +41,16 @@ function setActionError(action, err) { setError("actions-error", `${action}: ${err.message}`); } +function showActionSummary(action, successes, failures, firstError) { + const base = `${action}: ${successes} success, ${failures} failed`; + if (firstError) { + setError("actions-error", `${base}. First error: ${firstError}`); + } else { + setError("actions-error", ""); + } + setStatus(base); +} + async function apiRequest(method, url, body) { const options = { method, headers: {} }; if (body !== undefined) { @@ -103,13 +113,15 @@ function toggleSelection(pane, item) { } function updateActionButtons() { - const selected = activePaneState().selectedItem; - const hasSelection = Boolean(selected); - const isFile = hasSelection && selected.kind === "file"; - document.getElementById("rename-btn").disabled = !hasSelection; + const selectedItems = activePaneState().selectedItems; + const count = selectedItems.length; + const hasSelection = count > 0; + const exactlyOne = count === 1; + const allFiles = hasSelection && selectedItems.every((item) => item.kind === "file"); + document.getElementById("rename-btn").disabled = !exactlyOne; document.getElementById("delete-btn").disabled = !hasSelection; - document.getElementById("copy-btn").disabled = !isFile; - document.getElementById("move-btn").disabled = !isFile; + document.getElementById("copy-btn").disabled = !allFiles; + document.getElementById("move-btn").disabled = !allFiles; } function currentParentPath(path) { @@ -133,7 +145,6 @@ function renderBreadcrumbs(pane, path) { const crumbPath = aggregate; const crumb = createButton(parts[i], () => { setActivePane(pane); - console.debug("[breadcrumbs] click", { pane, crumbPath }); navigateTo(pane, crumbPath); }); crumb.type = "button"; @@ -141,7 +152,6 @@ function renderBreadcrumbs(pane, path) { ev.preventDefault(); ev.stopPropagation(); setActivePane(pane); - console.debug("[breadcrumbs] click", { pane, crumbPath }); navigateTo(pane, crumbPath); }; nav.append(crumb); @@ -239,11 +249,6 @@ async function loadBrowsePane(pane) { path: model.currentPath, show_hidden: String(model.showHidden), }); - console.debug("[browse] request", { - pane, - path: model.currentPath, - show_hidden: model.showHidden, - }); const data = await apiRequest("GET", `/api/browse?${query.toString()}`); model.currentPath = data.path; document.getElementById(`${pane}-current-path`).textContent = data.path; @@ -299,7 +304,6 @@ async function loadBrowsePane(pane) { } function navigateTo(pane, path) { - console.debug("[navigate] pane-path", { pane, path }); paneState(pane).currentPath = path; setSelectedItem(pane, null); loadBrowsePane(pane); @@ -329,10 +333,11 @@ async function createFolderForActivePane() { async function renameSelected() { const pane = state.activePane; - const selected = paneState(pane).selectedItem; - if (!selected) { + const selectedItems = paneState(pane).selectedItems; + if (selectedItems.length !== 1) { return; } + const selected = selectedItems[0]; const newName = window.prompt("New name", selected.name); if (!newName) { return; @@ -352,21 +357,31 @@ async function renameSelected() { async function deleteSelected() { const pane = state.activePane; - const selected = paneState(pane).selectedItem; - if (!selected) { + const selectedItems = [...paneState(pane).selectedItems]; + if (selectedItems.length === 0) { return; } - if (!window.confirm(`Delete ${selected.path}?`)) { + if (!window.confirm(`Delete ${selectedItems.length} selected item(s)?`)) { return; } setError("actions-error", ""); - try { - await apiRequest("POST", "/api/files/delete", { path: selected.path }); - setSelectedItem(pane, null); - await loadBrowsePane(pane); - } catch (err) { - setActionError("Delete", err); + let successes = 0; + let failures = 0; + let firstError = null; + for (const item of selectedItems) { + try { + await apiRequest("POST", "/api/files/delete", { path: item.path }); + successes += 1; + } catch (err) { + failures += 1; + if (!firstError) { + firstError = `${item.path}: ${err.message}`; + } + } } + setSelectedItem(pane, null); + await loadBrowsePane(pane); + showActionSummary("Delete", successes, failures, firstError); } function defaultDestination(sourcePath, targetBasePath) { @@ -377,58 +392,84 @@ function defaultDestination(sourcePath, targetBasePath) { async function startCopySelected() { const sourcePane = state.activePane; const destinationPane = otherPane(sourcePane); - const selected = paneState(sourcePane).selectedItem; - if (!selected || selected.kind !== "file") { + const selectedItems = [...paneState(sourcePane).selectedItems]; + if (selectedItems.length === 0) { return; } - const destination = window.prompt( - "Copy destination (full path)", - defaultDestination(selected.path, paneState(destinationPane).currentPath), + const baseDestination = window.prompt( + "Copy destination base path (full path)", + paneState(destinationPane).currentPath, ); - if (!destination) { + if (!baseDestination) { return; } setError("actions-error", ""); - try { - const result = await apiRequest("POST", "/api/files/copy", { - source: selected.path, - destination, - }); - state.selectedTaskId = result.task_id; - setStatus(`Copy task queued: ${result.task_id}`); - await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]); - } catch (err) { - setActionError("Copy", err); + let successes = 0; + let failures = 0; + let firstError = null; + for (const item of selectedItems) { + const destination = defaultDestination(item.path, baseDestination); + try { + if (item.kind !== "file") { + throw new Error("Only files are supported for copy"); + } + const result = await apiRequest("POST", "/api/files/copy", { + source: item.path, + destination, + }); + state.selectedTaskId = result.task_id; + successes += 1; + } catch (err) { + failures += 1; + if (!firstError) { + firstError = `${item.path}: ${err.message}`; + } + } } + await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]); + showActionSummary("Copy", successes, failures, firstError); } async function startMoveSelected() { const sourcePane = state.activePane; const destinationPane = otherPane(sourcePane); - const selected = paneState(sourcePane).selectedItem; - if (!selected || selected.kind !== "file") { + const selectedItems = [...paneState(sourcePane).selectedItems]; + if (selectedItems.length === 0) { return; } - const destination = window.prompt( - "Move destination (full path)", - defaultDestination(selected.path, paneState(destinationPane).currentPath), + const baseDestination = window.prompt( + "Move destination base path (full path)", + paneState(destinationPane).currentPath, ); - if (!destination) { + if (!baseDestination) { return; } setError("actions-error", ""); - try { - const result = await apiRequest("POST", "/api/files/move", { - source: selected.path, - destination, - }); - state.selectedTaskId = result.task_id; - setSelectedItem(sourcePane, null); - setStatus(`Move task queued: ${result.task_id}`); - await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]); - } catch (err) { - setActionError("Move", err); + let successes = 0; + let failures = 0; + let firstError = null; + for (const item of selectedItems) { + const destination = defaultDestination(item.path, baseDestination); + try { + if (item.kind !== "file") { + throw new Error("Only files are supported for move"); + } + const result = await apiRequest("POST", "/api/files/move", { + source: item.path, + destination, + }); + state.selectedTaskId = result.task_id; + successes += 1; + } catch (err) { + failures += 1; + if (!firstError) { + firstError = `${item.path}: ${err.message}`; + } + } } + setSelectedItem(sourcePane, null); + await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]); + showActionSummary("Move", successes, failures, firstError); } async function addBookmark() {