4.7.2 delete maps
Some checks failed
Test / test (push) Has been cancelled

This commit is contained in:
2026-06-21 09:18:14 +02:00
parent 7a850937b0
commit 199f8c0537
9 changed files with 199 additions and 59 deletions

View File

@@ -334,6 +334,10 @@
"maps.sitesDialog.empty": "Chưa có site.",
"maps.sitesDialog.deleteConfirm": "Xóa site \"{name}\"?",
"maps.deleteConfirm": "Xóa map \"{name}\"?",
"maps.deleteDialog.title": "Xóa map?",
"maps.deleteDialog.text": "Xóa map \"{name}\"? Hành động không hoàn tác.",
"maps.deleteDialog.activeWarning": "Map này đang là map hoạt động trên robot.",
"maps.deleteForbidden": "Bạn không thể xóa map thuộc nhóm người dùng khác.",
"maps.error.nameEmpty": "Tên map không được để trống.",
"maps.error.noImage": "Map chưa có ảnh — upload PNG trước khi kích hoạt.",
"maps.error.pngOnly": "Chỉ chấp nhận file PNG.",
@@ -948,6 +952,10 @@
"maps.sitesDialog.empty": "No sites yet.",
"maps.sitesDialog.deleteConfirm": "Delete site \"{name}\"?",
"maps.deleteConfirm": "Delete map \"{name}\"?",
"maps.deleteDialog.title": "Delete map?",
"maps.deleteDialog.text": "Delete map \"{name}\"? This cannot be undone.",
"maps.deleteDialog.activeWarning": "This map is currently active on the robot.",
"maps.deleteForbidden": "You cannot delete a map from another user group.",
"maps.error.nameEmpty": "Map name is required.",
"maps.error.noImage": "Map has no image — upload a PNG before activating.",
"maps.error.pngOnly": "Only PNG files are accepted.",

View File

@@ -1264,6 +1264,18 @@
</form>
</dialog>
<dialog id="mapsDeleteDialog" class="mapsMirDialog">
<div class="mapsMirDialogPanel">
<h2 class="mapsMirDialogTitle" data-i18n="maps.deleteDialog.title">Delete map?</h2>
<p id="mapsDeleteDialogText" class="mapsMirDialogText"></p>
<p id="mapsDeleteDialogActiveWarn" class="mapsMirDialogHint" hidden data-i18n="maps.deleteDialog.activeWarning">This map is currently active on the robot.</p>
<div class="mapsMirDialogFooter">
<button type="button" class="mapsMirBtn mapsMirBtn--outline" id="mapsDeleteNoBtn" data-i18n="common.no">No</button>
<button type="button" class="mapsMirBtn mapsMirBtn--danger" id="mapsDeleteYesBtn" data-i18n="common.delete">Delete</button>
</div>
</div>
</dialog>
<dialog id="mapEditorMenuDialog" class="mapsMirDialog mapsMirDialog--mapMenu">
<h2 class="mapsMirDialogTitle mapsMirMapMenuTitle" data-i18n="maps.menu.title">Upload, download and record maps</h2>
<div class="mapsMirMapMenuGrid">

View File

@@ -25,8 +25,11 @@
const sitesDialogEl = el("mapsSitesDialog");
const sitesListEl = el("mapsSitesList");
const siteFormDialogEl = el("mapsSiteFormDialog");
const deleteDialogEl = el("mapsDeleteDialog");
const createSiteSelectEl = el("mapsCreateSite");
let deleteDialogResolve = null;
const SITE_ICONS = {
chevron: `<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M4 3l3 4-3 4M8 3l3 4-3 4" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
edit: `<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M9.5 2.5l2 2L5 11H3v-2L9.5 2.5z" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>`,
@@ -48,6 +51,19 @@
return window.AuthApp?.canWrite?.("maps") ?? true;
}
function currentUser() {
return window.AuthApp?.getUser?.() || null;
}
function canDeleteMap(map) {
if (!canWrite() || !map) return false;
const user = currentUser();
if (!user) return true;
const mapGroup = map.created_by_group;
if (mapGroup) return mapGroup === user.group_id;
return true;
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, "&amp;")
@@ -216,7 +232,7 @@
? `<div class="mapsMirRowActions">
<button type="button" class="mapsMirIconBtn" data-edit="${map.id}" data-i18n-title="common.edit" title="${escapeHtml(t("common.edit"))}">${ICONS.edit}</button>
<button type="button" class="mapsMirIconBtn" data-view="${map.id}" data-i18n-title="maps.view" title="${escapeHtml(t("maps.view"))}">${ICONS.view}</button>
<button type="button" class="mapsMirIconBtn mapsMirIconBtn--danger" data-delete="${map.id}" data-i18n-title="common.delete" title="${escapeHtml(t("common.delete"))}">${ICONS.delete}</button>
${canDeleteMap(map) ? `<button type="button" class="mapsMirIconBtn mapsMirIconBtn--danger" data-delete="${map.id}" data-i18n-title="common.delete" title="${escapeHtml(t("common.delete"))}">${ICONS.delete}</button>` : ""}
</div>`
: `<div class="mapsMirRowActions">
<button type="button" class="mapsMirIconBtn" data-view="${map.id}" data-i18n-title="maps.view" title="${escapeHtml(t("maps.view"))}">${ICONS.view}</button>
@@ -310,11 +326,31 @@
});
}
function confirmDeleteMap(map) {
return new Promise((resolve) => {
deleteDialogResolve = resolve;
const textEl = el("mapsDeleteDialogText");
const activeWarnEl = el("mapsDeleteDialogActiveWarn");
if (textEl) textEl.textContent = t("maps.deleteDialog.text", { name: map.name || map.id });
if (activeWarnEl) {
const isActive = map.id === store.activeMapId;
activeWarnEl.hidden = !isActive;
if (isActive) activeWarnEl.textContent = t("maps.deleteDialog.activeWarning");
}
deleteDialogEl?.showModal();
});
}
async function deleteMapFromList(mapId) {
const map = findMap(mapId);
if (!map) return;
if (!confirm(t("maps.deleteConfirm", { name: map.name }))) return;
await api(`/api/maps/${encodeURIComponent(map.id)}`, { method: "DELETE" });
if (!map || !canDeleteMap(map)) return;
if (!(await confirmDeleteMap(map))) return;
try {
await api(`/api/maps/${encodeURIComponent(map.id)}`, { method: "DELETE" });
} catch (e) {
alert(e.message || t("maps.deleteForbidden"));
return;
}
store.maps = store.maps.filter((m) => m.id !== map.id);
if (store.activeMapId === map.id) store.activeMapId = null;
renderList();
@@ -465,6 +501,8 @@
name,
site_id,
created_by: user?.display_name || user?.username || "",
created_by_user: user?.id || "",
created_by_group: user?.group_id || "",
resolution: 0.05,
origin_x: 0,
origin_y: 0,
@@ -515,6 +553,22 @@
el("mapsCreateForm")?.addEventListener("submit", (evt) => {
createMap(evt).catch((e) => alert(e.message));
});
el("mapsDeleteYesBtn")?.addEventListener("click", () => {
deleteDialogEl?.close();
deleteDialogResolve?.(true);
deleteDialogResolve = null;
});
el("mapsDeleteNoBtn")?.addEventListener("click", () => {
deleteDialogEl?.close();
deleteDialogResolve?.(false);
deleteDialogResolve = null;
});
deleteDialogEl?.addEventListener("cancel", (evt) => {
evt.preventDefault();
deleteDialogEl.close();
deleteDialogResolve?.(false);
deleteDialogResolve = null;
});
el("mapsSiteForm")?.addEventListener("submit", (evt) => {
saveSite(evt).catch((e) => alert(e.message));
});

View File

@@ -3259,6 +3259,14 @@ body.auth-readonly-integrations .integrationToolbar .btn.primary { pointer-event
.mapsMirBtn--outline:hover { background: #f5f5f5; }
.mapsMirBtn--danger {
background: #c0392b;
color: #fff;
border: 1px solid #c0392b;
}
.mapsMirBtn--danger:hover { background: #a93226; }
.mapsMirActiveHint {
margin-bottom: 12px;
padding: 8px 12px;