polish
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -117,6 +117,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertIn('id="search-results"', body)
|
||||
self.assertIn('id="info-modal"', body)
|
||||
self.assertIn('id="rename-popup"', body)
|
||||
self.assertIn('id="rename-label"', body)
|
||||
self.assertIn('id="rename-input"', body)
|
||||
self.assertIn('id="rename-apply-btn"', body)
|
||||
self.assertIn('id="settings-general-tab"', body)
|
||||
@@ -227,6 +228,8 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertIn('function feedbackElements()', app_js)
|
||||
self.assertIn('function openFeedbackModal(message)', app_js)
|
||||
self.assertIn('function closeFeedbackModal()', app_js)
|
||||
self.assertIn('function openConfirmModal({ title, message, path, applyText = "Confirm" })', app_js)
|
||||
self.assertIn('function openTextInputModal({ title, label, applyText, initialValue = "", onSubmit })', app_js)
|
||||
self.assertIn('function downloadModalElements()', app_js)
|
||||
self.assertIn('function isZipDownloadSelection(items)', app_js)
|
||||
self.assertIn('function singleFileDownloadRequestKey(path)', app_js)
|
||||
@@ -328,6 +331,11 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertIn('startCopySelected();', app_js)
|
||||
self.assertIn('openF6Flow();', app_js)
|
||||
self.assertIn('deleteSelected();', app_js)
|
||||
self.assertIn('const confirmed = await openConfirmModal({', app_js)
|
||||
self.assertIn('title: selectedItems.length === 1 ? "Delete item?" : "Delete selected items?"', app_js)
|
||||
self.assertIn('title: "Discard unsaved changes?"', app_js)
|
||||
self.assertIn('title: "Create Folder"', app_js)
|
||||
self.assertIn('title: "Add Bookmark"', app_js)
|
||||
self.assertIn('openInfo();', app_js)
|
||||
self.assertIn('elements.title.textContent = "Properties";', app_js)
|
||||
self.assertIn('if (selectedItems.length > 1) {', app_js)
|
||||
@@ -338,6 +346,9 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertNotIn('Only files are supported for copy', app_js)
|
||||
self.assertIn('document.getElementById("upload-menu-toggle").onclick = (event) => {', app_js)
|
||||
self.assertIn('document.getElementById("upload-folder-btn").onclick = openFolderPicker;', app_js)
|
||||
self.assertNotIn('window.confirm(', app_js)
|
||||
self.assertNotIn('window.prompt(', app_js)
|
||||
self.assertNotIn('window.alert(', app_js)
|
||||
self.assertIn('throw createApiError(response, data);', app_js)
|
||||
self.assertIn('function closeUploadMenu()', app_js)
|
||||
self.assertIn('function toggleUploadMenu()', app_js)
|
||||
|
||||
+185
-95
@@ -46,11 +46,10 @@ let moveState = {
|
||||
let renameState = {
|
||||
source: null,
|
||||
name: "",
|
||||
submitAction: null,
|
||||
};
|
||||
let deleteConfirmState = {
|
||||
pane: "left",
|
||||
items: [],
|
||||
recursivePaths: [],
|
||||
resolver: null,
|
||||
};
|
||||
let contextMenuState = {
|
||||
open: false,
|
||||
@@ -292,6 +291,8 @@ function moveElements() {
|
||||
function renameElements() {
|
||||
return {
|
||||
overlay: document.getElementById("rename-popup"),
|
||||
title: document.getElementById("rename-title"),
|
||||
label: document.getElementById("rename-label"),
|
||||
input: document.getElementById("rename-input"),
|
||||
error: document.getElementById("rename-error"),
|
||||
applyButton: document.getElementById("rename-apply-btn"),
|
||||
@@ -2583,7 +2584,28 @@ function navigateTo(pane, path) {
|
||||
|
||||
async function createFolderForPane(pane) {
|
||||
setActivePane(pane);
|
||||
const name = window.prompt("Folder name");
|
||||
const name = await new Promise((resolve) => {
|
||||
openTextInputModal({
|
||||
title: "Create Folder",
|
||||
label: "Folder name",
|
||||
applyText: "Create",
|
||||
initialValue: "",
|
||||
onSubmit: async (rawValue, elements, cancelled) => {
|
||||
if (cancelled) {
|
||||
resolve("");
|
||||
return true;
|
||||
}
|
||||
const value = rawValue.trim();
|
||||
elements.error.textContent = "";
|
||||
if (!value) {
|
||||
elements.error.textContent = "Folder name is required";
|
||||
return false;
|
||||
}
|
||||
resolve(value);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
});
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
@@ -2609,49 +2631,36 @@ async function renameSelected() {
|
||||
if (selectedItems.length !== 1) {
|
||||
return;
|
||||
}
|
||||
const selected = selectedItems[0];
|
||||
const newName = window.prompt("New name", selected.name);
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
setError("actions-error", "");
|
||||
try {
|
||||
await apiRequest("POST", "/api/files/rename", {
|
||||
path: selected.path,
|
||||
new_name: newName,
|
||||
});
|
||||
setSelectedItem(pane, null);
|
||||
await loadBrowsePane(pane);
|
||||
} catch (err) {
|
||||
setActionError("Rename", err);
|
||||
}
|
||||
openRenamePopup();
|
||||
}
|
||||
|
||||
function closeDeleteConfirmModal() {
|
||||
const elements = deleteConfirmElements();
|
||||
deleteConfirmState.pane = "left";
|
||||
deleteConfirmState.items = [];
|
||||
deleteConfirmState.recursivePaths = [];
|
||||
const resolver = deleteConfirmState.resolver;
|
||||
deleteConfirmState.resolver = null;
|
||||
elements.error.textContent = "";
|
||||
elements.overlay.classList.add("hidden");
|
||||
}
|
||||
|
||||
function openDeleteConfirmModal(pane, items, recursivePaths) {
|
||||
const elements = deleteConfirmElements();
|
||||
deleteConfirmState.pane = pane;
|
||||
deleteConfirmState.items = items.map((item) => ({ ...item }));
|
||||
deleteConfirmState.recursivePaths = Array.from(recursivePaths);
|
||||
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.path.textContent = "";
|
||||
elements.applyButton.textContent = "Delete";
|
||||
elements.overlay.classList.add("hidden");
|
||||
if (typeof resolver === "function") {
|
||||
resolver(false);
|
||||
}
|
||||
}
|
||||
|
||||
function openConfirmModal({ title, message, path, applyText = "Confirm" }) {
|
||||
const elements = deleteConfirmElements();
|
||||
elements.error.textContent = "";
|
||||
elements.title.textContent = title;
|
||||
elements.message.textContent = message;
|
||||
elements.path.textContent = path || "";
|
||||
elements.applyButton.textContent = applyText;
|
||||
elements.overlay.classList.remove("hidden");
|
||||
elements.applyButton.focus();
|
||||
return new Promise((resolve) => {
|
||||
deleteConfirmState.resolver = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
async function executeDeleteItems(pane, items, recursivePaths) {
|
||||
@@ -2690,20 +2699,19 @@ async function executeDeleteItems(pane, items, recursivePaths) {
|
||||
}
|
||||
|
||||
async function submitDeleteConfirmModal() {
|
||||
const elements = deleteConfirmElements();
|
||||
if (!deleteConfirmState.items.length) {
|
||||
const resolver = deleteConfirmState.resolver;
|
||||
if (typeof resolver !== "function") {
|
||||
return;
|
||||
}
|
||||
deleteConfirmState.resolver = null;
|
||||
const elements = deleteConfirmElements();
|
||||
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;
|
||||
}
|
||||
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 = "";
|
||||
elements.applyButton.textContent = "Delete";
|
||||
elements.overlay.classList.add("hidden");
|
||||
resolver(true);
|
||||
}
|
||||
|
||||
async function collectDeleteRecursivePaths(selectedItems) {
|
||||
@@ -2734,10 +2742,29 @@ async function deleteSelected() {
|
||||
try {
|
||||
const recursivePaths = await collectDeleteRecursivePaths(selectedItems);
|
||||
if (recursivePaths.size > 0) {
|
||||
openDeleteConfirmModal(pane, selectedItems, recursivePaths);
|
||||
const confirmed = await openConfirmModal({
|
||||
title: selectedItems.length === 1 ? "Delete folder and contents?" : "Delete selected items and folder contents?",
|
||||
message: selectedItems.length === 1
|
||||
? "This will permanently delete the folder and all files and subfolders inside it."
|
||||
: `This will permanently delete ${selectedItems.length} selected items, including all files and subfolders inside the selected folders.`,
|
||||
path: selectedItems.length === 1 ? selectedItems[0].path : `${selectedItems.length} selected items`,
|
||||
applyText: "Delete",
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(`Delete ${selectedItems.length} selected item(s)?`)) {
|
||||
await executeDeleteItems(pane, selectedItems, recursivePaths);
|
||||
return;
|
||||
}
|
||||
const confirmed = await openConfirmModal({
|
||||
title: selectedItems.length === 1 ? "Delete item?" : "Delete selected items?",
|
||||
message: selectedItems.length === 1
|
||||
? "This will permanently delete the selected item."
|
||||
: `This will permanently delete ${selectedItems.length} selected items.`,
|
||||
path: selectedItems.length === 1 ? selectedItems[0].path : `${selectedItems.length} selected items`,
|
||||
applyText: "Delete",
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
await executeDeleteItems(pane, selectedItems, new Set());
|
||||
@@ -2841,7 +2868,28 @@ async function executeMoveSelection(baseDestination) {
|
||||
async function addBookmark() {
|
||||
const pane = state.activePane;
|
||||
const path = paneState(pane).currentPath;
|
||||
const label = window.prompt("Bookmark label", path);
|
||||
const label = await new Promise((resolve) => {
|
||||
openTextInputModal({
|
||||
title: "Add Bookmark",
|
||||
label: "Bookmark label",
|
||||
applyText: "Add",
|
||||
initialValue: path,
|
||||
onSubmit: async (rawValue, elements, cancelled) => {
|
||||
if (cancelled) {
|
||||
resolve("");
|
||||
return true;
|
||||
}
|
||||
const value = rawValue.trim();
|
||||
elements.error.textContent = "";
|
||||
if (!value) {
|
||||
elements.error.textContent = "Bookmark label is required";
|
||||
return false;
|
||||
}
|
||||
resolve(value);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
});
|
||||
if (!label) {
|
||||
return;
|
||||
}
|
||||
@@ -3145,15 +3193,43 @@ function resetRenameState() {
|
||||
renameState = {
|
||||
source: null,
|
||||
name: "",
|
||||
submitAction: null,
|
||||
};
|
||||
}
|
||||
|
||||
function closeRenamePopup() {
|
||||
function settleRenamePopup(value = null, cancelled = false, notify = true) {
|
||||
const elements = renameElements();
|
||||
const submitAction = renameState.submitAction;
|
||||
elements.overlay.classList.add("hidden");
|
||||
elements.error.textContent = "";
|
||||
elements.input.value = "";
|
||||
elements.title.textContent = "Rename";
|
||||
elements.label.textContent = "Name";
|
||||
elements.applyButton.textContent = "Rename";
|
||||
resetRenameState();
|
||||
if (notify && typeof submitAction === "function") {
|
||||
return submitAction(value, null, cancelled);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function closeRenamePopup() {
|
||||
settleRenamePopup(null, true);
|
||||
}
|
||||
|
||||
function openTextInputModal({ title, label, applyText, initialValue = "", onSubmit }) {
|
||||
const elements = renameElements();
|
||||
renameState.source = null;
|
||||
renameState.name = initialValue;
|
||||
renameState.submitAction = onSubmit;
|
||||
elements.title.textContent = title;
|
||||
elements.label.textContent = label;
|
||||
elements.applyButton.textContent = applyText;
|
||||
elements.input.value = initialValue;
|
||||
elements.error.textContent = "";
|
||||
elements.overlay.classList.remove("hidden");
|
||||
elements.input.focus();
|
||||
elements.input.select();
|
||||
}
|
||||
|
||||
function openRenamePopup() {
|
||||
@@ -3162,15 +3238,49 @@ function openRenamePopup() {
|
||||
return false;
|
||||
}
|
||||
const source = selectedItems[0];
|
||||
const elements = renameElements();
|
||||
renameState.source = source;
|
||||
renameState.name = source.name;
|
||||
elements.input.value = source.name;
|
||||
elements.error.textContent = "";
|
||||
elements.overlay.classList.remove("hidden");
|
||||
elements.input.focus();
|
||||
elements.input.select();
|
||||
return openTextInputModal({
|
||||
title: "Rename",
|
||||
label: "Name",
|
||||
applyText: "Rename",
|
||||
initialValue: source.name,
|
||||
onSubmit: async (rawValue, elements, cancelled) => {
|
||||
if (cancelled) {
|
||||
return true;
|
||||
}
|
||||
const newName = rawValue.trim();
|
||||
elements.error.textContent = "";
|
||||
if (!newName) {
|
||||
elements.error.textContent = "Name is required";
|
||||
return false;
|
||||
}
|
||||
if (newName === source.name) {
|
||||
elements.error.textContent = "Name must differ from current name";
|
||||
return false;
|
||||
}
|
||||
if (newName.includes("/")) {
|
||||
elements.error.textContent = "Name cannot contain /";
|
||||
return false;
|
||||
}
|
||||
if (newName === "." || newName === "..") {
|
||||
elements.error.textContent = "Invalid name";
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiRequest("POST", "/api/files/rename", {
|
||||
path: source.path,
|
||||
new_name: newName,
|
||||
});
|
||||
setSelectedItem(state.activePane, null);
|
||||
await loadBrowsePane(state.activePane);
|
||||
setStatus(`Renamed ${source.path}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
elements.error.textContent = err.message;
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function resetBatchMoveState() {
|
||||
@@ -3253,40 +3363,12 @@ function openF6Flow() {
|
||||
|
||||
async function submitRenamePopup() {
|
||||
const elements = renameElements();
|
||||
const source = renameState.source;
|
||||
if (!source) {
|
||||
if (typeof renameState.submitAction !== "function") {
|
||||
return;
|
||||
}
|
||||
const newName = elements.input.value.trim();
|
||||
elements.error.textContent = "";
|
||||
if (!newName) {
|
||||
elements.error.textContent = "Name is required";
|
||||
return;
|
||||
}
|
||||
if (newName === source.name) {
|
||||
elements.error.textContent = "Name must differ from current name";
|
||||
return;
|
||||
}
|
||||
if (newName.includes("/")) {
|
||||
elements.error.textContent = "Name cannot contain /";
|
||||
return;
|
||||
}
|
||||
if (newName === "." || newName === "..") {
|
||||
elements.error.textContent = "Invalid name";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiRequest("POST", "/api/files/rename", {
|
||||
path: source.path,
|
||||
new_name: newName,
|
||||
});
|
||||
closeRenamePopup();
|
||||
setSelectedItem(state.activePane, null);
|
||||
await loadBrowsePane(state.activePane);
|
||||
setStatus(`Renamed ${source.path}`);
|
||||
} catch (err) {
|
||||
elements.error.textContent = err.message;
|
||||
const shouldClose = await renameState.submitAction(elements.input.value, elements, false);
|
||||
if (shouldClose !== false) {
|
||||
settleRenamePopup("", false, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3764,10 +3846,18 @@ function resetEditorState() {
|
||||
};
|
||||
}
|
||||
|
||||
function attemptCloseEditor() {
|
||||
if (editorIsDirty() && !window.confirm("Discard unsaved changes?")) {
|
||||
async function attemptCloseEditor() {
|
||||
if (editorIsDirty()) {
|
||||
const discard = await openConfirmModal({
|
||||
title: "Discard unsaved changes?",
|
||||
message: "Your unsaved editor changes will be lost.",
|
||||
path: editorState.path || "",
|
||||
applyText: "Discard",
|
||||
});
|
||||
if (!discard) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
closeEditor();
|
||||
}
|
||||
|
||||
|
||||
@@ -302,7 +302,7 @@
|
||||
<div class="popup-card">
|
||||
<button id="rename-close-btn" class="viewer-close" type="button" aria-label="Close rename">X</button>
|
||||
<h3 id="rename-title">Rename</h3>
|
||||
<label for="rename-input" class="popup-label">Name</label>
|
||||
<label id="rename-label" for="rename-input" class="popup-label">Name</label>
|
||||
<input id="rename-input" type="text" autocomplete="off">
|
||||
<div id="rename-error" class="error"></div>
|
||||
<div class="popup-actions">
|
||||
|
||||
Reference in New Issue
Block a user