feat (ui): multiselect toegevoegd
This commit is contained in:
Binary file not shown.
@@ -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)
|
||||
|
||||
+85
-44
@@ -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", "");
|
||||
let successes = 0;
|
||||
let failures = 0;
|
||||
let firstError = null;
|
||||
for (const item of selectedItems) {
|
||||
try {
|
||||
await apiRequest("POST", "/api/files/delete", { path: selected.path });
|
||||
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);
|
||||
} catch (err) {
|
||||
setActionError("Delete", err);
|
||||
}
|
||||
showActionSummary("Delete", successes, failures, firstError);
|
||||
}
|
||||
|
||||
function defaultDestination(sourcePath, targetBasePath) {
|
||||
@@ -377,59 +392,85 @@ 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", "");
|
||||
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: selected.path,
|
||||
source: item.path,
|
||||
destination,
|
||||
});
|
||||
state.selectedTaskId = result.task_id;
|
||||
setStatus(`Copy task queued: ${result.task_id}`);
|
||||
await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
|
||||
successes += 1;
|
||||
} catch (err) {
|
||||
setActionError("Copy", 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", "");
|
||||
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: selected.path,
|
||||
source: item.path,
|
||||
destination,
|
||||
});
|
||||
state.selectedTaskId = result.task_id;
|
||||
setSelectedItem(sourcePane, null);
|
||||
setStatus(`Move task queued: ${result.task_id}`);
|
||||
await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
|
||||
successes += 1;
|
||||
} catch (err) {
|
||||
setActionError("Move", 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() {
|
||||
const pane = state.activePane;
|
||||
|
||||
Reference in New Issue
Block a user