From 05816751b1f0877ffd611a0d4a0cce0d276bc71b Mon Sep 17 00:00:00 2001 From: kodi Date: Wed, 11 Mar 2026 10:19:40 +0100 Subject: [PATCH] feat (ui): multiselect toegevoegd --- txt | 2 - .../test_ui_smoke_golden.cpython-313.pyc | Bin 3730 -> 3934 bytes .../tests/golden/test_ui_smoke_golden.py | 3 + webui/html/app.js | 157 +++++++++++------- 4 files changed, 102 insertions(+), 60 deletions(-) delete mode 100644 txt 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 88365365ec8fbd343a94db372ce7f048072ba3d7..df2bea2f1330d9757f6bf106b2ea87d70a1ca460 100644 GIT binary patch delta 253 zcmbOvdryw{GcPX}0}x~yZOojtk@r6{(*&l;jGQW)jaYUwPL5?2VdR?Z$EwW84JLUe z-vhFFL8KsGD6=UONGk(_0z*23CjVqJwq!&8%oJOtoYb@u-OQ5I++w9F0gymZW_ku( zgbyT=o1Kzbq?=Tdw|P2S79$f6<78%zRVwN$M6Ph?-QeKq=k4UZAf~ZGqe^=Wq#p}hWr_5+MgX6k{=0yq3FAPA=2OmZT7M_l( a%gmBDSa{lfn|xdSKQJ?}NEL|zoeTi#HbwIQ delta 143 zcmca7H%XTFGcPX}0}#y6*_g?@k@r6{qsnGpmIsWJYuQ8?xhChaDGPFkGMh4i)G#n8 zFr+hR@=RXDmb}@5J%f?yKEvci9IG~0a!N8Ys%@UkOyFGf8DM#~u- s*CjMBN@#vz0CGOKF*305bW~ktmb}5j)9%~k+v@*;nSn*BNDOE;068rs2mk;8 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() {