diff --git a/Web App/web/src/App.vue b/Web App/web/src/App.vue index 75015de..87ce59b 100644 --- a/Web App/web/src/App.vue +++ b/Web App/web/src/App.vue @@ -7,6 +7,7 @@ import { servers } from "./servers"; import { api } from "./api"; import { t } from "./i18n"; import Logo from "./components/Logo.vue"; +import ConfirmDialog from "./components/ConfirmDialog.vue"; import ServerSwitcher from "./components/ServerSwitcher.vue"; const router = useRouter(); @@ -161,4 +162,7 @@ onBeforeUnmount(() => themeObserver?.disconnect()); + + + diff --git a/Web App/web/src/components/AdminUsers.vue b/Web App/web/src/components/AdminUsers.vue index 4a24b87..7caf67d 100644 --- a/Web App/web/src/components/AdminUsers.vue +++ b/Web App/web/src/components/AdminUsers.vue @@ -6,6 +6,7 @@ import { api } from "../api"; import { state } from "../auth"; import { formatDate } from "../lib/format.js"; import { t } from "../i18n"; +import { askConfirm } from "../lib/confirm.js"; import Modal from "./Modal.vue"; const users = ref([]); @@ -121,7 +122,7 @@ async function submitResetPassword() { } async function removeUser(u) { - if (!confirm(t("admin.confirmDelete", { name: u.name || u.email }))) return; + if (!(await askConfirm(t("admin.confirmDelete", { name: u.name || u.email })))) return; error.value = ""; try { await api.deleteUser(u.id); diff --git a/Web App/web/src/components/ConfirmDialog.vue b/Web App/web/src/components/ConfirmDialog.vue new file mode 100644 index 0000000..82789d3 --- /dev/null +++ b/Web App/web/src/components/ConfirmDialog.vue @@ -0,0 +1,28 @@ + + + + + + {{ confirmPrompt.message }} + + + {{ t("common.cancel") }} + + + {{ confirmPrompt.confirmLabel || t("common.yes") }} + + + + diff --git a/Web App/web/src/components/Modal.vue b/Web App/web/src/components/Modal.vue index f524f68..5e0fef7 100644 --- a/Web App/web/src/components/Modal.vue +++ b/Web App/web/src/components/Modal.vue @@ -1,10 +1,19 @@ - + {{ title }} diff --git a/Web App/web/src/components/OrgManager.vue b/Web App/web/src/components/OrgManager.vue index 7b88062..8626d7a 100644 --- a/Web App/web/src/components/OrgManager.vue +++ b/Web App/web/src/components/OrgManager.vue @@ -3,6 +3,7 @@ import { ref, computed, onMounted } from "vue"; import { api } from "../api"; import { state, refreshProfile } from "../auth"; import { t } from "../i18n"; +import { askConfirm } from "../lib/confirm.js"; // Organization management, adapting to who is looking: // - a user with no organization gets a "create your own" form, and becomes the @@ -97,7 +98,7 @@ async function remove(o) { const msg = mine ? t("settings.org.confirmDeleteOwn", { name: o.name }) : t("settings.org.confirmDelete", { name: o.name }); - if (!confirm(msg)) return; + if (!(await askConfirm(msg))) return; busy.value = true; error.value = ""; try { diff --git a/Web App/web/src/components/ProviderPanel.vue b/Web App/web/src/components/ProviderPanel.vue index fc48c2a..31ad23b 100644 --- a/Web App/web/src/components/ProviderPanel.vue +++ b/Web App/web/src/components/ProviderPanel.vue @@ -13,6 +13,7 @@ import { ref, computed, watch, onMounted } from "vue"; import { api } from "../api"; import { prefs } from "../prefs"; import { t } from "../i18n"; +import { askConfirm } from "../lib/confirm.js"; import { formatDateTime, formatKm } from "../lib/format.js"; const props = defineProps({ @@ -112,7 +113,7 @@ async function connect() { } async function disconnect() { - if (!confirm(t("car.provider.unlinkConfirm", { label: label.value }))) return; + if (!(await askConfirm(t("car.provider.unlinkConfirm", { label: label.value })))) return; error.value = ""; try { const car = await api.linkCarProvider(props.car.id, { provider: "", vehicleId: "" }); diff --git a/Web App/web/src/components/ServerConnectModal.vue b/Web App/web/src/components/ServerConnectModal.vue index 2a53779..a99f923 100644 --- a/Web App/web/src/components/ServerConnectModal.vue +++ b/Web App/web/src/components/ServerConnectModal.vue @@ -14,6 +14,7 @@ import { } from "../servers"; import { connect, disconnect } from "../auth"; import { t } from "../i18n"; +import { askConfirm } from "../lib/confirm.js"; import Modal from "./Modal.vue"; const props = defineProps({ @@ -70,8 +71,8 @@ function onDisconnect() { emit("done"); } -function onRemove() { - if (!confirm(t("servers.removeConfirm", { name: displayName(props.server) }))) return; +async function onRemove() { + if (!(await askConfirm(t("servers.removeConfirm", { name: displayName(props.server) })))) return; removeServer(props.server.id); emit("done"); } diff --git a/Web App/web/src/lib/confirm.js b/Web App/web/src/lib/confirm.js new file mode 100644 index 0000000..436bebc --- /dev/null +++ b/Web App/web/src/lib/confirm.js @@ -0,0 +1,43 @@ +// One confirmation prompt for the whole app, asked inside the page. +// +// window.confirm() reads like the obvious tool and is not: a browser that +// suppresses native dialogs — an embedded webview, a blocked-dialogs setting — +// never shows it and hands back false, so the action silently does not happen +// and nothing says why. That is indistinguishable from a broken button, and it +// was one: removing a home charger did nothing at all until the prompt moved +// into the page. +// +// askConfirm() renders the question as part of the page (ConfirmDialog.vue, +// mounted once in App.vue) and resolves to what the user actually pressed: +// +// if (!(await askConfirm(t("car.parts.confirmDelete")))) return; +import { reactive } from "vue"; + +export const confirmPrompt = reactive({ + open: false, + title: "", + message: "", + confirmLabel: "", // blank falls back to the dialog's own wording +}); + +let settle = null; + +export function askConfirm(message, { title = "", confirmLabel = "" } = {}) { + // Asking a second question while one is open answers the first with no. + // Nothing in the app asks two at once, and an abandoned promise would hang + // whoever awaited it. + if (settle) settle(false); + Object.assign(confirmPrompt, { open: true, title, message, confirmLabel }); + return new Promise((resolve) => { + settle = resolve; + }); +} + +// Answers the open question. Closing the dialog any other way — the backdrop, +// Cancel — is a no, never a dropped promise. +export function answerConfirm(value) { + confirmPrompt.open = false; + const done = settle; + settle = null; + if (done) done(value); +} diff --git a/Web App/web/src/views/CarDetail.vue b/Web App/web/src/views/CarDetail.vue index d83647d..a40916f 100644 --- a/Web App/web/src/views/CarDetail.vue +++ b/Web App/web/src/views/CarDetail.vue @@ -20,6 +20,7 @@ import { } from "../lib/format.js"; import { SERVICE_PARTS, changedParts, visibleParts } from "../lib/serviceParts.js"; import { t, tSplit } from "../i18n"; +import { askConfirm } from "../lib/confirm.js"; import CarFormModal from "../components/CarFormModal.vue"; import ServiceFormModal from "../components/ServiceFormModal.vue"; import TechnicalCheckFormModal from "../components/TechnicalCheckFormModal.vue"; @@ -704,7 +705,7 @@ async function onServiceSaved() { await load(); } async function deleteService(id) { - if (!confirm(t("car.services.confirmDelete"))) return; + if (!(await askConfirm(t("car.services.confirmDelete")))) return; try { await api.deleteService(id); await load(); @@ -728,7 +729,7 @@ async function onTechnicalCheckSaved() { await load(); } async function deleteTechnicalCheck(id) { - if (!confirm(t("car.technical.confirmDelete"))) return; + if (!(await askConfirm(t("car.technical.confirmDelete")))) return; try { await api.deleteTechnicalCheck(id); await load(); @@ -751,7 +752,7 @@ async function onPartSaved() { parts.value = await api.listCarParts(props.id); } async function deletePart(id) { - if (!confirm(t("car.parts.confirmDelete"))) return; + if (!(await askConfirm(t("car.parts.confirmDelete")))) return; try { await api.deletePart(id); parts.value = await api.listCarParts(props.id); @@ -786,7 +787,7 @@ async function onFuelSaved() { await reloadFuel(); } async function deleteFuel(id) { - if (!confirm(t("car.fuel.confirmDelete"))) return; + if (!(await askConfirm(t("car.fuel.confirmDelete")))) return; try { await api.deleteFuel(id); await reloadFuel(); @@ -822,7 +823,7 @@ async function onChargingSaved() { await reloadCharging(); } async function deleteCharging(id) { - if (!confirm(t("car.charging.confirmDelete"))) return; + if (!(await askConfirm(t("car.charging.confirmDelete")))) return; try { await api.deleteCharging(id); await reloadCharging(); @@ -851,7 +852,7 @@ async function onMaintenanceSaved() { ]); } async function deleteMaintenance(id) { - if (!confirm(t("car.maintenance.confirmDelete"))) return; + if (!(await askConfirm(t("car.maintenance.confirmDelete")))) return; try { await api.deleteMaintenance(id); maintenance.value = await api.listCarMaintenance(props.id); @@ -879,7 +880,7 @@ async function onDocumentSaved() { ]); } async function deleteDocument(id) { - if (!confirm(t("car.documents.confirmDelete"))) return; + if (!(await askConfirm(t("car.documents.confirmDelete")))) return; try { await api.deleteDocument(id); [documents.value, reminders.value] = await Promise.all([ @@ -948,7 +949,7 @@ async function reopenReminder(r) { } } async function deleteReminder(id) { - if (!confirm(t("car.reminders.confirmDelete"))) return; + if (!(await askConfirm(t("car.reminders.confirmDelete")))) return; try { await api.deleteReminder(id); reminders.value = await api.listCarReminders(props.id); diff --git a/Web App/web/src/views/Settings.vue b/Web App/web/src/views/Settings.vue index 6a10b87..42ec9a0 100644 --- a/Web App/web/src/views/Settings.vue +++ b/Web App/web/src/views/Settings.vue @@ -6,6 +6,7 @@ import { state, isAdmin, logout, refreshProfile } from "../auth"; import { prefs, applyProfilePrefs } from "../prefs"; import { formatDate, formatMoney } from "../lib/format.js"; import { t, tSplit, TRANSLATED_LANGUAGES } from "../i18n"; +import { askConfirm } from "../lib/confirm.js"; import OrgManager from "../components/OrgManager.vue"; import AdminUsers from "../components/AdminUsers.vue"; @@ -546,7 +547,7 @@ async function generateAnkerToken() { async function revokeAnkerToken() { const sn = ankerCtlSerial.value.trim(); if (!sn) return; - if (!confirm(t("settings.integrations.controlRevokeConfirm"))) return; + if (!(await askConfirm(t("settings.integrations.controlRevokeConfirm")))) return; ankerCtlError.value = ""; ankerNewToken.value = ""; try { @@ -873,7 +874,7 @@ async function onImportFileChosen(e) { importError.value = t("settings.advanced.notExport"); return; } - if (!confirm(t("settings.advanced.confirmImport", { count: payload.cars.length }))) { + if (!(await askConfirm(t("settings.advanced.confirmImport", { count: payload.cars.length })))) { return; } @@ -934,7 +935,7 @@ async function cancelDeletion() { } async function finalizeDeletion() { - if (!confirm(t("settings.danger.confirmFinalize"))) return; + if (!(await askConfirm(t("settings.danger.confirmFinalize")))) return; deleteError.value = ""; try { await api.finalizeAccountDeletion();
{{ confirmPrompt.message }}