feat: delete non empty folders
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -31,7 +31,7 @@ async def delete(
|
|||||||
request: DeleteRequest,
|
request: DeleteRequest,
|
||||||
service: FileOpsService = Depends(get_file_ops_service),
|
service: FileOpsService = Depends(get_file_ops_service),
|
||||||
) -> DeleteResponse:
|
) -> DeleteResponse:
|
||||||
return service.delete(path=request.path)
|
return service.delete(path=request.path, recursive=request.recursive)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/upload", response_model=UploadResponse)
|
@router.post("/upload", response_model=UploadResponse)
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class RenameResponse(BaseModel):
|
|||||||
|
|
||||||
class DeleteRequest(BaseModel):
|
class DeleteRequest(BaseModel):
|
||||||
path: str
|
path: str
|
||||||
|
recursive: bool = False
|
||||||
|
|
||||||
|
|
||||||
class DeleteResponse(BaseModel):
|
class DeleteResponse(BaseModel):
|
||||||
|
|||||||
Binary file not shown.
@@ -104,6 +104,9 @@ class FilesystemAdapter:
|
|||||||
def delete_empty_directory(self, path: Path) -> None:
|
def delete_empty_directory(self, path: Path) -> None:
|
||||||
path.rmdir()
|
path.rmdir()
|
||||||
|
|
||||||
|
def delete_directory_recursive(self, path: Path) -> None:
|
||||||
|
shutil.rmtree(path)
|
||||||
|
|
||||||
def copy_file(self, source: str, destination: str, on_progress: callable | None = None) -> None:
|
def copy_file(self, source: str, destination: str, on_progress: callable | None = None) -> None:
|
||||||
src = Path(source)
|
src = Path(source)
|
||||||
dst = Path(destination)
|
dst = Path(destination)
|
||||||
|
|||||||
Binary file not shown.
@@ -158,7 +158,7 @@ class FileOpsService:
|
|||||||
self._record_history_error(operation="rename", source=path, destination=new_name, path=path, error=error)
|
self._record_history_error(operation="rename", source=path, destination=new_name, path=path, error=error)
|
||||||
raise error
|
raise error
|
||||||
|
|
||||||
def delete(self, path: str) -> DeleteResponse:
|
def delete(self, path: str, recursive: bool = False) -> DeleteResponse:
|
||||||
try:
|
try:
|
||||||
resolved_target = self._path_guard.resolve_existing_path(path)
|
resolved_target = self._path_guard.resolve_existing_path(path)
|
||||||
|
|
||||||
@@ -166,12 +166,15 @@ class FileOpsService:
|
|||||||
self._filesystem.delete_file(resolved_target.absolute)
|
self._filesystem.delete_file(resolved_target.absolute)
|
||||||
elif resolved_target.absolute.is_dir():
|
elif resolved_target.absolute.is_dir():
|
||||||
if not self._filesystem.is_directory_empty(resolved_target.absolute):
|
if not self._filesystem.is_directory_empty(resolved_target.absolute):
|
||||||
|
if not recursive:
|
||||||
raise AppError(
|
raise AppError(
|
||||||
code="directory_not_empty",
|
code="directory_not_empty",
|
||||||
message="Directory is not empty",
|
message="Directory is not empty",
|
||||||
status_code=409,
|
status_code=409,
|
||||||
details={"path": resolved_target.relative},
|
details={"path": resolved_target.relative},
|
||||||
)
|
)
|
||||||
|
self._filesystem.delete_directory_recursive(resolved_target.absolute)
|
||||||
|
else:
|
||||||
self._filesystem.delete_empty_directory(resolved_target.absolute)
|
self._filesystem.delete_empty_directory(resolved_target.absolute)
|
||||||
else:
|
else:
|
||||||
raise AppError(
|
raise AppError(
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -300,6 +300,22 @@ class FileOpsApiGoldenTest(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_delete_non_empty_directory_recursive_success(self) -> None:
|
||||||
|
target = self.scope / "non_empty_recursive"
|
||||||
|
target.mkdir()
|
||||||
|
nested = target / "nested"
|
||||||
|
nested.mkdir()
|
||||||
|
(nested / "a.txt").write_text("a", encoding="utf-8")
|
||||||
|
|
||||||
|
response = self._post(
|
||||||
|
"/api/files/delete",
|
||||||
|
{"path": "storage1/scope/non_empty_recursive", "recursive": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), {"path": "storage1/scope/non_empty_recursive"})
|
||||||
|
self.assertFalse(target.exists())
|
||||||
|
|
||||||
def test_delete_invalid_path(self) -> None:
|
def test_delete_invalid_path(self) -> None:
|
||||||
response = self._post(
|
response = self._post(
|
||||||
"/api/files/delete",
|
"/api/files/delete",
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
|||||||
self.assertIn('id="upload-modal-count"', body)
|
self.assertIn('id="upload-modal-count"', body)
|
||||||
self.assertIn('id="upload-modal-status"', body)
|
self.assertIn('id="upload-modal-status"', body)
|
||||||
self.assertIn('id="upload-modal-cancel-btn"', body)
|
self.assertIn('id="upload-modal-cancel-btn"', body)
|
||||||
|
self.assertIn('id="feedback-modal"', body)
|
||||||
|
self.assertIn('id="feedback-message"', body)
|
||||||
|
self.assertIn('id="feedback-close-btn"', body)
|
||||||
self.assertIn('id="settings-btn"', body)
|
self.assertIn('id="settings-btn"', body)
|
||||||
self.assertIn('id="rename-btn"', body)
|
self.assertIn('id="rename-btn"', body)
|
||||||
self.assertIn('id="view-btn"', body)
|
self.assertIn('id="view-btn"', body)
|
||||||
@@ -132,6 +135,10 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
|||||||
self.assertIn(">Target path</label>", body)
|
self.assertIn(">Target path</label>", body)
|
||||||
self.assertIn('id="batch-move-popup"', body)
|
self.assertIn('id="batch-move-popup"', body)
|
||||||
self.assertIn('id="batch-move-apply-btn"', body)
|
self.assertIn('id="batch-move-apply-btn"', body)
|
||||||
|
self.assertIn('id="delete-confirm-modal"', body)
|
||||||
|
self.assertIn('id="delete-confirm-apply-btn"', body)
|
||||||
|
self.assertIn('id="delete-confirm-cancel-btn"', body)
|
||||||
|
self.assertIn("Delete folder and contents?", body)
|
||||||
self.assertIn('id="mkdir-btn"', body)
|
self.assertIn('id="mkdir-btn"', body)
|
||||||
self.assertIn('id="copy-btn"', body)
|
self.assertIn('id="copy-btn"', body)
|
||||||
self.assertIn('id="move-btn"', body)
|
self.assertIn('id="move-btn"', body)
|
||||||
@@ -189,12 +196,18 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
|||||||
self.assertIn("document.documentElement.dataset.theme", app_js)
|
self.assertIn("document.documentElement.dataset.theme", app_js)
|
||||||
self.assertIn('document.getElementById("theme-toggle").onclick = toggleTheme;', app_js)
|
self.assertIn('document.getElementById("theme-toggle").onclick = toggleTheme;', app_js)
|
||||||
self.assertIn('document.getElementById("upload-btn").onclick = openUploadPicker;', app_js)
|
self.assertIn('document.getElementById("upload-btn").onclick = openUploadPicker;', app_js)
|
||||||
|
self.assertIn('function feedbackElements()', app_js)
|
||||||
|
self.assertIn('function openFeedbackModal(message)', app_js)
|
||||||
|
self.assertIn('function closeFeedbackModal()', app_js)
|
||||||
self.assertIn('document.getElementById("upload-menu-toggle").onclick = (event) => {', 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.assertIn('document.getElementById("upload-folder-btn").onclick = openFolderPicker;', app_js)
|
||||||
|
self.assertIn('throw createApiError(response, data);', app_js)
|
||||||
self.assertIn('function closeUploadMenu()', app_js)
|
self.assertIn('function closeUploadMenu()', app_js)
|
||||||
self.assertIn('function toggleUploadMenu()', app_js)
|
self.assertIn('function toggleUploadMenu()', app_js)
|
||||||
self.assertNotIn('if (event.altKey) {', app_js)
|
self.assertNotIn('if (event.altKey) {', app_js)
|
||||||
self.assertIn('document.getElementById("settings-btn").onclick = () => openSettings("general");', app_js)
|
self.assertIn('document.getElementById("settings-btn").onclick = () => openSettings("general");', app_js)
|
||||||
|
self.assertIn('err.code === "directory_not_empty"', app_js)
|
||||||
|
self.assertIn('openDeleteConfirmModal(item.path);', app_js)
|
||||||
self.assertIn('async function loadSettings()', app_js)
|
self.assertIn('async function loadSettings()', app_js)
|
||||||
self.assertIn('await loadSettings();', app_js)
|
self.assertIn('await loadSettings();', app_js)
|
||||||
self.assertIn('settings.showThumbnailsInput.onchange = handleShowThumbnailsChange;', app_js)
|
self.assertIn('settings.showThumbnailsInput.onchange = handleShowThumbnailsChange;', app_js)
|
||||||
@@ -225,6 +238,11 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
|||||||
self.assertIn('async function ensureFolderDirectoryExists(path)', app_js)
|
self.assertIn('async function ensureFolderDirectoryExists(path)', app_js)
|
||||||
self.assertIn('async function executeFolderUploadPlan(plan)', app_js)
|
self.assertIn('async function executeFolderUploadPlan(plan)', app_js)
|
||||||
self.assertIn('async function handleFolderSelection(event)', app_js)
|
self.assertIn('async function handleFolderSelection(event)', app_js)
|
||||||
|
self.assertIn('function deleteConfirmElements()', app_js)
|
||||||
|
self.assertIn('function openDeleteConfirmModal(path)', app_js)
|
||||||
|
self.assertIn('async function submitDeleteConfirmModal()', app_js)
|
||||||
|
self.assertIn('recursive: true', app_js)
|
||||||
|
self.assertIn('err.code === "directory_not_empty"', app_js)
|
||||||
self.assertIn('input.setAttribute("webkitdirectory", "")', app_js)
|
self.assertIn('input.setAttribute("webkitdirectory", "")', app_js)
|
||||||
self.assertIn('await apiRequest("POST", "/api/files/mkdir", {', app_js)
|
self.assertIn('await apiRequest("POST", "/api/files/mkdir", {', app_js)
|
||||||
self.assertIn('await uploadFileRequest(targetPath, entry.file, overwrite);', app_js)
|
self.assertIn('await uploadFileRequest(targetPath, entry.file, overwrite);', app_js)
|
||||||
|
|||||||
+141
-2
@@ -47,6 +47,9 @@ let renameState = {
|
|||||||
source: null,
|
source: null,
|
||||||
name: "",
|
name: "",
|
||||||
};
|
};
|
||||||
|
let deleteConfirmState = {
|
||||||
|
path: null,
|
||||||
|
};
|
||||||
let batchMoveState = {
|
let batchMoveState = {
|
||||||
destinationBase: "",
|
destinationBase: "",
|
||||||
count: 0,
|
count: 0,
|
||||||
@@ -164,6 +167,15 @@ function setStatus(msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setError(id, msg) {
|
function setError(id, msg) {
|
||||||
|
if (id === "actions-error") {
|
||||||
|
document.getElementById(id).textContent = "";
|
||||||
|
if (msg) {
|
||||||
|
openFeedbackModal(msg);
|
||||||
|
} else {
|
||||||
|
closeFeedbackModal();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
document.getElementById(id).textContent = msg || "";
|
document.getElementById(id).textContent = msg || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,6 +293,24 @@ function batchMoveElements() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deleteConfirmElements() {
|
||||||
|
return {
|
||||||
|
overlay: document.getElementById("delete-confirm-modal"),
|
||||||
|
path: document.getElementById("delete-confirm-path"),
|
||||||
|
error: document.getElementById("delete-confirm-error"),
|
||||||
|
applyButton: document.getElementById("delete-confirm-apply-btn"),
|
||||||
|
cancelButton: document.getElementById("delete-confirm-cancel-btn"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function feedbackElements() {
|
||||||
|
return {
|
||||||
|
overlay: document.getElementById("feedback-modal"),
|
||||||
|
message: document.getElementById("feedback-message"),
|
||||||
|
closeButton: document.getElementById("feedback-close-btn"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function settingsElements() {
|
function settingsElements() {
|
||||||
return {
|
return {
|
||||||
overlay: document.getElementById("settings-modal"),
|
overlay: document.getElementById("settings-modal"),
|
||||||
@@ -347,6 +377,28 @@ function uploadModalElements() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isFeedbackModalOpen() {
|
||||||
|
return !feedbackElements().overlay.classList.contains("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function openFeedbackModal(message) {
|
||||||
|
const elements = feedbackElements();
|
||||||
|
if (!elements.overlay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
elements.message.textContent = message || "";
|
||||||
|
elements.overlay.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeFeedbackModal() {
|
||||||
|
const elements = feedbackElements();
|
||||||
|
if (!elements.overlay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
elements.message.textContent = "";
|
||||||
|
elements.overlay.classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
function setUploadModalVisible(visible) {
|
function setUploadModalVisible(visible) {
|
||||||
const elements = uploadModalElements();
|
const elements = uploadModalElements();
|
||||||
if (!elements.overlay) {
|
if (!elements.overlay) {
|
||||||
@@ -486,8 +538,7 @@ async function apiRequest(method, url, body) {
|
|||||||
const response = await fetch(url, options);
|
const response = await fetch(url, options);
|
||||||
const data = await response.json().catch(() => ({}));
|
const data = await response.json().catch(() => ({}));
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = data.error || {};
|
throw createApiError(response, data);
|
||||||
throw new Error(error.message || `HTTP ${response.status}`);
|
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -1831,6 +1882,40 @@ async function renameSelected() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeDeleteConfirmModal() {
|
||||||
|
const elements = deleteConfirmElements();
|
||||||
|
deleteConfirmState.path = null;
|
||||||
|
elements.error.textContent = "";
|
||||||
|
elements.overlay.classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDeleteConfirmModal(path) {
|
||||||
|
const elements = deleteConfirmElements();
|
||||||
|
deleteConfirmState.path = path;
|
||||||
|
elements.path.textContent = path;
|
||||||
|
elements.error.textContent = "";
|
||||||
|
elements.overlay.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitDeleteConfirmModal() {
|
||||||
|
const path = deleteConfirmState.path;
|
||||||
|
if (!path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const elements = deleteConfirmElements();
|
||||||
|
elements.error.textContent = "";
|
||||||
|
try {
|
||||||
|
await apiRequest("POST", "/api/files/delete", { path, recursive: true });
|
||||||
|
closeDeleteConfirmModal();
|
||||||
|
setSelectedItem(state.activePane, null);
|
||||||
|
await loadBrowsePane(state.activePane);
|
||||||
|
setStatus("Delete: 1 success, 0 failed");
|
||||||
|
setError("actions-error", "");
|
||||||
|
} catch (err) {
|
||||||
|
elements.error.textContent = err.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteSelected() {
|
async function deleteSelected() {
|
||||||
const pane = state.activePane;
|
const pane = state.activePane;
|
||||||
const selectedItems = [...paneState(pane).selectedItems];
|
const selectedItems = [...paneState(pane).selectedItems];
|
||||||
@@ -1849,6 +1934,16 @@ async function deleteSelected() {
|
|||||||
await apiRequest("POST", "/api/files/delete", { path: item.path });
|
await apiRequest("POST", "/api/files/delete", { path: item.path });
|
||||||
successes += 1;
|
successes += 1;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (
|
||||||
|
err.code === "directory_not_empty"
|
||||||
|
&& selectedItems.length === 1
|
||||||
|
&& item.kind === "directory"
|
||||||
|
) {
|
||||||
|
failures = 0;
|
||||||
|
firstError = null;
|
||||||
|
openDeleteConfirmModal(item.path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
failures += 1;
|
failures += 1;
|
||||||
if (!firstError) {
|
if (!firstError) {
|
||||||
firstError = `${item.path}: ${err.message}`;
|
firstError = `${item.path}: ${err.message}`;
|
||||||
@@ -2089,6 +2184,10 @@ function isBatchMovePopupOpen() {
|
|||||||
return !batchMoveElements().overlay.classList.contains("hidden");
|
return !batchMoveElements().overlay.classList.contains("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDeleteConfirmModalOpen() {
|
||||||
|
return !deleteConfirmElements().overlay.classList.contains("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
function isUploadConflictModalOpen() {
|
function isUploadConflictModalOpen() {
|
||||||
return isUploadConflictOpen();
|
return isUploadConflictOpen();
|
||||||
}
|
}
|
||||||
@@ -3150,6 +3249,13 @@ function handleKeyboardShortcuts(event) {
|
|||||||
closeUploadMenu();
|
closeUploadMenu();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isFeedbackModalOpen()) {
|
||||||
|
if (event.key === "Escape" || event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
closeFeedbackModal();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isInfoOpen()) {
|
if (isInfoOpen()) {
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -3204,6 +3310,19 @@ function handleKeyboardShortcuts(event) {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isDeleteConfirmModalOpen()) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
closeDeleteConfirmModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
submitDeleteConfirmModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isUploadConflictModalOpen()) {
|
if (isUploadConflictModalOpen()) {
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -3393,6 +3512,17 @@ function setupEvents() {
|
|||||||
if (modalCancel) {
|
if (modalCancel) {
|
||||||
modalCancel.onclick = requestUploadCancel;
|
modalCancel.onclick = requestUploadCancel;
|
||||||
}
|
}
|
||||||
|
const feedback = feedbackElements();
|
||||||
|
if (feedback.closeButton) {
|
||||||
|
feedback.closeButton.onclick = closeFeedbackModal;
|
||||||
|
}
|
||||||
|
if (feedback.overlay) {
|
||||||
|
feedback.overlay.onclick = (event) => {
|
||||||
|
if (event.target === feedback.overlay) {
|
||||||
|
closeFeedbackModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
const elements = uploadElements();
|
const elements = uploadElements();
|
||||||
if (!elements.menu || elements.menu.contains(event.target)) {
|
if (!elements.menu || elements.menu.contains(event.target)) {
|
||||||
@@ -3514,6 +3644,15 @@ function setupEvents() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteConfirm = deleteConfirmElements();
|
||||||
|
deleteConfirm.cancelButton.onclick = closeDeleteConfirmModal;
|
||||||
|
deleteConfirm.applyButton.onclick = submitDeleteConfirmModal;
|
||||||
|
deleteConfirm.overlay.onclick = (event) => {
|
||||||
|
if (event.target === deleteConfirm.overlay) {
|
||||||
|
closeDeleteConfirmModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const viewer = viewerElements();
|
const viewer = viewerElements();
|
||||||
viewer.closeButton.onclick = closeViewer;
|
viewer.closeButton.onclick = closeViewer;
|
||||||
viewer.overlay.onclick = (event) => {
|
viewer.overlay.onclick = (event) => {
|
||||||
|
|||||||
@@ -656,6 +656,10 @@ button:disabled {
|
|||||||
box-shadow: var(--shadow-elevated);
|
box-shadow: var(--shadow-elevated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.feedback-card {
|
||||||
|
width: min(440px, calc(100vw - 24px));
|
||||||
|
}
|
||||||
|
|
||||||
#upload-modal .popup-card {
|
#upload-modal .popup-card {
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
|
|||||||
@@ -108,6 +108,16 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="feedback-modal" class="popup-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="feedback-title">
|
||||||
|
<div class="popup-card feedback-card">
|
||||||
|
<h3 id="feedback-title">Action feedback</h3>
|
||||||
|
<div id="feedback-message" class="popup-meta"></div>
|
||||||
|
<div class="popup-actions">
|
||||||
|
<button id="feedback-close-btn" type="button">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="settings-modal" class="popup-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="settings-title">
|
<div id="settings-modal" class="popup-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="settings-title">
|
||||||
<div class="popup-card settings-card">
|
<div class="popup-card settings-card">
|
||||||
<button id="settings-close-btn" class="viewer-close" type="button" aria-label="Close settings">X</button>
|
<button id="settings-close-btn" class="viewer-close" type="button" aria-label="Close settings">X</button>
|
||||||
@@ -257,6 +267,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="delete-confirm-modal" class="popup-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="delete-confirm-title">
|
||||||
|
<div class="popup-card">
|
||||||
|
<h3 id="delete-confirm-title">Delete folder and contents?</h3>
|
||||||
|
<div class="popup-meta">This will permanently delete the folder and all files and subfolders inside it.</div>
|
||||||
|
<div id="delete-confirm-path" class="popup-meta"></div>
|
||||||
|
<div id="delete-confirm-error" class="error"></div>
|
||||||
|
<div class="popup-actions">
|
||||||
|
<button id="delete-confirm-apply-btn" type="button">Delete</button>
|
||||||
|
<button id="delete-confirm-cancel-btn" type="button">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="viewer-modal" class="popup-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="viewer-title">
|
<div id="viewer-modal" class="popup-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="viewer-title">
|
||||||
<div class="popup-card viewer-card">
|
<div class="popup-card viewer-card">
|
||||||
<button id="viewer-close-btn" class="viewer-close" type="button" aria-label="Close viewer">X</button>
|
<button id="viewer-close-btn" class="viewer-close" type="button" aria-label="Close viewer">X</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user