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="search-results"', body)
|
||||||
self.assertIn('id="info-modal"', body)
|
self.assertIn('id="info-modal"', body)
|
||||||
self.assertIn('id="rename-popup"', body)
|
self.assertIn('id="rename-popup"', body)
|
||||||
|
self.assertIn('id="rename-label"', body)
|
||||||
self.assertIn('id="rename-input"', body)
|
self.assertIn('id="rename-input"', body)
|
||||||
self.assertIn('id="rename-apply-btn"', body)
|
self.assertIn('id="rename-apply-btn"', body)
|
||||||
self.assertIn('id="settings-general-tab"', 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 feedbackElements()', app_js)
|
||||||
self.assertIn('function openFeedbackModal(message)', app_js)
|
self.assertIn('function openFeedbackModal(message)', app_js)
|
||||||
self.assertIn('function closeFeedbackModal()', 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 downloadModalElements()', app_js)
|
||||||
self.assertIn('function isZipDownloadSelection(items)', app_js)
|
self.assertIn('function isZipDownloadSelection(items)', app_js)
|
||||||
self.assertIn('function singleFileDownloadRequestKey(path)', app_js)
|
self.assertIn('function singleFileDownloadRequestKey(path)', app_js)
|
||||||
@@ -328,6 +331,11 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
|||||||
self.assertIn('startCopySelected();', app_js)
|
self.assertIn('startCopySelected();', app_js)
|
||||||
self.assertIn('openF6Flow();', app_js)
|
self.assertIn('openF6Flow();', app_js)
|
||||||
self.assertIn('deleteSelected();', 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('openInfo();', app_js)
|
||||||
self.assertIn('elements.title.textContent = "Properties";', app_js)
|
self.assertIn('elements.title.textContent = "Properties";', app_js)
|
||||||
self.assertIn('if (selectedItems.length > 1) {', 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.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-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.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('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)
|
||||||
|
|||||||
+185
-95
@@ -46,11 +46,10 @@ let moveState = {
|
|||||||
let renameState = {
|
let renameState = {
|
||||||
source: null,
|
source: null,
|
||||||
name: "",
|
name: "",
|
||||||
|
submitAction: null,
|
||||||
};
|
};
|
||||||
let deleteConfirmState = {
|
let deleteConfirmState = {
|
||||||
pane: "left",
|
resolver: null,
|
||||||
items: [],
|
|
||||||
recursivePaths: [],
|
|
||||||
};
|
};
|
||||||
let contextMenuState = {
|
let contextMenuState = {
|
||||||
open: false,
|
open: false,
|
||||||
@@ -292,6 +291,8 @@ function moveElements() {
|
|||||||
function renameElements() {
|
function renameElements() {
|
||||||
return {
|
return {
|
||||||
overlay: document.getElementById("rename-popup"),
|
overlay: document.getElementById("rename-popup"),
|
||||||
|
title: document.getElementById("rename-title"),
|
||||||
|
label: document.getElementById("rename-label"),
|
||||||
input: document.getElementById("rename-input"),
|
input: document.getElementById("rename-input"),
|
||||||
error: document.getElementById("rename-error"),
|
error: document.getElementById("rename-error"),
|
||||||
applyButton: document.getElementById("rename-apply-btn"),
|
applyButton: document.getElementById("rename-apply-btn"),
|
||||||
@@ -2583,7 +2584,28 @@ function navigateTo(pane, path) {
|
|||||||
|
|
||||||
async function createFolderForPane(pane) {
|
async function createFolderForPane(pane) {
|
||||||
setActivePane(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) {
|
if (!name) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2609,49 +2631,36 @@ async function renameSelected() {
|
|||||||
if (selectedItems.length !== 1) {
|
if (selectedItems.length !== 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const selected = selectedItems[0];
|
openRenamePopup();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDeleteConfirmModal() {
|
function closeDeleteConfirmModal() {
|
||||||
const elements = deleteConfirmElements();
|
const elements = deleteConfirmElements();
|
||||||
deleteConfirmState.pane = "left";
|
const resolver = deleteConfirmState.resolver;
|
||||||
deleteConfirmState.items = [];
|
deleteConfirmState.resolver = null;
|
||||||
deleteConfirmState.recursivePaths = [];
|
|
||||||
elements.error.textContent = "";
|
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.title.textContent = "Delete folder and contents?";
|
||||||
elements.message.textContent = "This will permanently delete the folder and all files and subfolders inside it.";
|
elements.message.textContent = "This will permanently delete the folder and all files and subfolders inside it.";
|
||||||
elements.path.textContent = items[0].path;
|
elements.path.textContent = "";
|
||||||
} else {
|
elements.applyButton.textContent = "Delete";
|
||||||
elements.title.textContent = "Delete selected items and folder contents?";
|
elements.overlay.classList.add("hidden");
|
||||||
elements.message.textContent = `This will permanently delete ${items.length} selected items, including all files and subfolders inside the selected folders.`;
|
if (typeof resolver === "function") {
|
||||||
elements.path.textContent = `${items.length} selected items`;
|
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.overlay.classList.remove("hidden");
|
||||||
|
elements.applyButton.focus();
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
deleteConfirmState.resolver = resolve;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeDeleteItems(pane, items, recursivePaths) {
|
async function executeDeleteItems(pane, items, recursivePaths) {
|
||||||
@@ -2690,20 +2699,19 @@ async function executeDeleteItems(pane, items, recursivePaths) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submitDeleteConfirmModal() {
|
async function submitDeleteConfirmModal() {
|
||||||
const elements = deleteConfirmElements();
|
const resolver = deleteConfirmState.resolver;
|
||||||
if (!deleteConfirmState.items.length) {
|
if (typeof resolver !== "function") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
deleteConfirmState.resolver = null;
|
||||||
|
const elements = deleteConfirmElements();
|
||||||
elements.error.textContent = "";
|
elements.error.textContent = "";
|
||||||
try {
|
elements.title.textContent = "Delete folder and contents?";
|
||||||
const pane = deleteConfirmState.pane;
|
elements.message.textContent = "This will permanently delete the folder and all files and subfolders inside it.";
|
||||||
const items = [...deleteConfirmState.items];
|
elements.path.textContent = "";
|
||||||
const recursivePaths = new Set(deleteConfirmState.recursivePaths);
|
elements.applyButton.textContent = "Delete";
|
||||||
closeDeleteConfirmModal();
|
elements.overlay.classList.add("hidden");
|
||||||
await executeDeleteItems(pane, items, recursivePaths);
|
resolver(true);
|
||||||
} catch (err) {
|
|
||||||
elements.error.textContent = err.message;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function collectDeleteRecursivePaths(selectedItems) {
|
async function collectDeleteRecursivePaths(selectedItems) {
|
||||||
@@ -2734,10 +2742,29 @@ async function deleteSelected() {
|
|||||||
try {
|
try {
|
||||||
const recursivePaths = await collectDeleteRecursivePaths(selectedItems);
|
const recursivePaths = await collectDeleteRecursivePaths(selectedItems);
|
||||||
if (recursivePaths.size > 0) {
|
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;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
await executeDeleteItems(pane, selectedItems, new Set());
|
await executeDeleteItems(pane, selectedItems, new Set());
|
||||||
@@ -2841,7 +2868,28 @@ async function executeMoveSelection(baseDestination) {
|
|||||||
async function addBookmark() {
|
async function addBookmark() {
|
||||||
const pane = state.activePane;
|
const pane = state.activePane;
|
||||||
const path = paneState(pane).currentPath;
|
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) {
|
if (!label) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3145,15 +3193,43 @@ function resetRenameState() {
|
|||||||
renameState = {
|
renameState = {
|
||||||
source: null,
|
source: null,
|
||||||
name: "",
|
name: "",
|
||||||
|
submitAction: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeRenamePopup() {
|
function settleRenamePopup(value = null, cancelled = false, notify = true) {
|
||||||
const elements = renameElements();
|
const elements = renameElements();
|
||||||
|
const submitAction = renameState.submitAction;
|
||||||
elements.overlay.classList.add("hidden");
|
elements.overlay.classList.add("hidden");
|
||||||
elements.error.textContent = "";
|
elements.error.textContent = "";
|
||||||
elements.input.value = "";
|
elements.input.value = "";
|
||||||
|
elements.title.textContent = "Rename";
|
||||||
|
elements.label.textContent = "Name";
|
||||||
|
elements.applyButton.textContent = "Rename";
|
||||||
resetRenameState();
|
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() {
|
function openRenamePopup() {
|
||||||
@@ -3162,16 +3238,50 @@ function openRenamePopup() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const source = selectedItems[0];
|
const source = selectedItems[0];
|
||||||
const elements = renameElements();
|
return openTextInputModal({
|
||||||
renameState.source = source;
|
title: "Rename",
|
||||||
renameState.name = source.name;
|
label: "Name",
|
||||||
elements.input.value = source.name;
|
applyText: "Rename",
|
||||||
elements.error.textContent = "";
|
initialValue: source.name,
|
||||||
elements.overlay.classList.remove("hidden");
|
onSubmit: async (rawValue, elements, cancelled) => {
|
||||||
elements.input.focus();
|
if (cancelled) {
|
||||||
elements.input.select();
|
|
||||||
return true;
|
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() {
|
function resetBatchMoveState() {
|
||||||
batchMoveState = {
|
batchMoveState = {
|
||||||
@@ -3253,40 +3363,12 @@ function openF6Flow() {
|
|||||||
|
|
||||||
async function submitRenamePopup() {
|
async function submitRenamePopup() {
|
||||||
const elements = renameElements();
|
const elements = renameElements();
|
||||||
const source = renameState.source;
|
if (typeof renameState.submitAction !== "function") {
|
||||||
if (!source) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newName = elements.input.value.trim();
|
const shouldClose = await renameState.submitAction(elements.input.value, elements, false);
|
||||||
elements.error.textContent = "";
|
if (shouldClose !== false) {
|
||||||
if (!newName) {
|
settleRenamePopup("", false, false);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3764,10 +3846,18 @@ function resetEditorState() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function attemptCloseEditor() {
|
async function attemptCloseEditor() {
|
||||||
if (editorIsDirty() && !window.confirm("Discard unsaved changes?")) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
closeEditor();
|
closeEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -302,7 +302,7 @@
|
|||||||
<div class="popup-card">
|
<div class="popup-card">
|
||||||
<button id="rename-close-btn" class="viewer-close" type="button" aria-label="Close rename">X</button>
|
<button id="rename-close-btn" class="viewer-close" type="button" aria-label="Close rename">X</button>
|
||||||
<h3 id="rename-title">Rename</h3>
|
<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">
|
<input id="rename-input" type="text" autocomplete="off">
|
||||||
<div id="rename-error" class="error"></div>
|
<div id="rename-error" class="error"></div>
|
||||||
<div class="popup-actions">
|
<div class="popup-actions">
|
||||||
|
|||||||
Reference in New Issue
Block a user