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="footer-bar"', body)
|
||||||
self.assertIn('id="left-pane"', body)
|
self.assertIn('id="left-pane"', body)
|
||||||
self.assertIn('id="right-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="left-breadcrumbs"', body)
|
||||||
self.assertIn('id="right-breadcrumbs"', body)
|
self.assertIn('id="right-breadcrumbs"', body)
|
||||||
self.assertNotIn('id="bookmarks-panel"', body)
|
self.assertNotIn('id="bookmarks-panel"', body)
|
||||||
|
|||||||
+99
-58
@@ -41,6 +41,16 @@ function setActionError(action, err) {
|
|||||||
setError("actions-error", `${action}: ${err.message}`);
|
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) {
|
async function apiRequest(method, url, body) {
|
||||||
const options = { method, headers: {} };
|
const options = { method, headers: {} };
|
||||||
if (body !== undefined) {
|
if (body !== undefined) {
|
||||||
@@ -103,13 +113,15 @@ function toggleSelection(pane, item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateActionButtons() {
|
function updateActionButtons() {
|
||||||
const selected = activePaneState().selectedItem;
|
const selectedItems = activePaneState().selectedItems;
|
||||||
const hasSelection = Boolean(selected);
|
const count = selectedItems.length;
|
||||||
const isFile = hasSelection && selected.kind === "file";
|
const hasSelection = count > 0;
|
||||||
document.getElementById("rename-btn").disabled = !hasSelection;
|
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("delete-btn").disabled = !hasSelection;
|
||||||
document.getElementById("copy-btn").disabled = !isFile;
|
document.getElementById("copy-btn").disabled = !allFiles;
|
||||||
document.getElementById("move-btn").disabled = !isFile;
|
document.getElementById("move-btn").disabled = !allFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentParentPath(path) {
|
function currentParentPath(path) {
|
||||||
@@ -133,7 +145,6 @@ function renderBreadcrumbs(pane, path) {
|
|||||||
const crumbPath = aggregate;
|
const crumbPath = aggregate;
|
||||||
const crumb = createButton(parts[i], () => {
|
const crumb = createButton(parts[i], () => {
|
||||||
setActivePane(pane);
|
setActivePane(pane);
|
||||||
console.debug("[breadcrumbs] click", { pane, crumbPath });
|
|
||||||
navigateTo(pane, crumbPath);
|
navigateTo(pane, crumbPath);
|
||||||
});
|
});
|
||||||
crumb.type = "button";
|
crumb.type = "button";
|
||||||
@@ -141,7 +152,6 @@ function renderBreadcrumbs(pane, path) {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
setActivePane(pane);
|
setActivePane(pane);
|
||||||
console.debug("[breadcrumbs] click", { pane, crumbPath });
|
|
||||||
navigateTo(pane, crumbPath);
|
navigateTo(pane, crumbPath);
|
||||||
};
|
};
|
||||||
nav.append(crumb);
|
nav.append(crumb);
|
||||||
@@ -239,11 +249,6 @@ async function loadBrowsePane(pane) {
|
|||||||
path: model.currentPath,
|
path: model.currentPath,
|
||||||
show_hidden: String(model.showHidden),
|
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()}`);
|
const data = await apiRequest("GET", `/api/browse?${query.toString()}`);
|
||||||
model.currentPath = data.path;
|
model.currentPath = data.path;
|
||||||
document.getElementById(`${pane}-current-path`).textContent = data.path;
|
document.getElementById(`${pane}-current-path`).textContent = data.path;
|
||||||
@@ -299,7 +304,6 @@ async function loadBrowsePane(pane) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function navigateTo(pane, path) {
|
function navigateTo(pane, path) {
|
||||||
console.debug("[navigate] pane-path", { pane, path });
|
|
||||||
paneState(pane).currentPath = path;
|
paneState(pane).currentPath = path;
|
||||||
setSelectedItem(pane, null);
|
setSelectedItem(pane, null);
|
||||||
loadBrowsePane(pane);
|
loadBrowsePane(pane);
|
||||||
@@ -329,10 +333,11 @@ async function createFolderForActivePane() {
|
|||||||
|
|
||||||
async function renameSelected() {
|
async function renameSelected() {
|
||||||
const pane = state.activePane;
|
const pane = state.activePane;
|
||||||
const selected = paneState(pane).selectedItem;
|
const selectedItems = paneState(pane).selectedItems;
|
||||||
if (!selected) {
|
if (selectedItems.length !== 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const selected = selectedItems[0];
|
||||||
const newName = window.prompt("New name", selected.name);
|
const newName = window.prompt("New name", selected.name);
|
||||||
if (!newName) {
|
if (!newName) {
|
||||||
return;
|
return;
|
||||||
@@ -352,21 +357,31 @@ async function renameSelected() {
|
|||||||
|
|
||||||
async function deleteSelected() {
|
async function deleteSelected() {
|
||||||
const pane = state.activePane;
|
const pane = state.activePane;
|
||||||
const selected = paneState(pane).selectedItem;
|
const selectedItems = [...paneState(pane).selectedItems];
|
||||||
if (!selected) {
|
if (selectedItems.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!window.confirm(`Delete ${selected.path}?`)) {
|
if (!window.confirm(`Delete ${selectedItems.length} selected item(s)?`)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError("actions-error", "");
|
setError("actions-error", "");
|
||||||
try {
|
let successes = 0;
|
||||||
await apiRequest("POST", "/api/files/delete", { path: selected.path });
|
let failures = 0;
|
||||||
setSelectedItem(pane, null);
|
let firstError = null;
|
||||||
await loadBrowsePane(pane);
|
for (const item of selectedItems) {
|
||||||
} catch (err) {
|
try {
|
||||||
setActionError("Delete", err);
|
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) {
|
function defaultDestination(sourcePath, targetBasePath) {
|
||||||
@@ -377,58 +392,84 @@ function defaultDestination(sourcePath, targetBasePath) {
|
|||||||
async function startCopySelected() {
|
async function startCopySelected() {
|
||||||
const sourcePane = state.activePane;
|
const sourcePane = state.activePane;
|
||||||
const destinationPane = otherPane(sourcePane);
|
const destinationPane = otherPane(sourcePane);
|
||||||
const selected = paneState(sourcePane).selectedItem;
|
const selectedItems = [...paneState(sourcePane).selectedItems];
|
||||||
if (!selected || selected.kind !== "file") {
|
if (selectedItems.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const destination = window.prompt(
|
const baseDestination = window.prompt(
|
||||||
"Copy destination (full path)",
|
"Copy destination base path (full path)",
|
||||||
defaultDestination(selected.path, paneState(destinationPane).currentPath),
|
paneState(destinationPane).currentPath,
|
||||||
);
|
);
|
||||||
if (!destination) {
|
if (!baseDestination) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError("actions-error", "");
|
setError("actions-error", "");
|
||||||
try {
|
let successes = 0;
|
||||||
const result = await apiRequest("POST", "/api/files/copy", {
|
let failures = 0;
|
||||||
source: selected.path,
|
let firstError = null;
|
||||||
destination,
|
for (const item of selectedItems) {
|
||||||
});
|
const destination = defaultDestination(item.path, baseDestination);
|
||||||
state.selectedTaskId = result.task_id;
|
try {
|
||||||
setStatus(`Copy task queued: ${result.task_id}`);
|
if (item.kind !== "file") {
|
||||||
await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
|
throw new Error("Only files are supported for copy");
|
||||||
} catch (err) {
|
}
|
||||||
setActionError("Copy", err);
|
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() {
|
async function startMoveSelected() {
|
||||||
const sourcePane = state.activePane;
|
const sourcePane = state.activePane;
|
||||||
const destinationPane = otherPane(sourcePane);
|
const destinationPane = otherPane(sourcePane);
|
||||||
const selected = paneState(sourcePane).selectedItem;
|
const selectedItems = [...paneState(sourcePane).selectedItems];
|
||||||
if (!selected || selected.kind !== "file") {
|
if (selectedItems.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const destination = window.prompt(
|
const baseDestination = window.prompt(
|
||||||
"Move destination (full path)",
|
"Move destination base path (full path)",
|
||||||
defaultDestination(selected.path, paneState(destinationPane).currentPath),
|
paneState(destinationPane).currentPath,
|
||||||
);
|
);
|
||||||
if (!destination) {
|
if (!baseDestination) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError("actions-error", "");
|
setError("actions-error", "");
|
||||||
try {
|
let successes = 0;
|
||||||
const result = await apiRequest("POST", "/api/files/move", {
|
let failures = 0;
|
||||||
source: selected.path,
|
let firstError = null;
|
||||||
destination,
|
for (const item of selectedItems) {
|
||||||
});
|
const destination = defaultDestination(item.path, baseDestination);
|
||||||
state.selectedTaskId = result.task_id;
|
try {
|
||||||
setSelectedItem(sourcePane, null);
|
if (item.kind !== "file") {
|
||||||
setStatus(`Move task queued: ${result.task_id}`);
|
throw new Error("Only files are supported for move");
|
||||||
await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
|
}
|
||||||
} catch (err) {
|
const result = await apiRequest("POST", "/api/files/move", {
|
||||||
setActionError("Move", err);
|
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() {
|
async function addBookmark() {
|
||||||
|
|||||||
Reference in New Issue
Block a user