From b37c565cf0a5feb4df674031291388743ec55ca1 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 22 Apr 2026 19:29:53 +0200 Subject: [PATCH] fix(ui): replace native alert/confirm with showConfirm/showToast helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Daniel flagged the native browser confirm() dialogs in Extensions as ugly and incompatible with the app's design. There was also a stray alert() in calculators.js resus-meds weight validation. Replaced: - extensions.js: confirmDelete → showConfirm(..., {confirmText: 'Move to trash'}) - extensions.js: confirmPurge → showConfirm(..., {danger: true, confirmText: 'Delete permanently'}) - calculators.js:2060 alert() → showToast(..., 'error') All three helpers (showConfirm, showToast) are already defined as globals in public/js/app.js. The design already had a modal — I should have used it from the start. Audit confirmation: `grep -rnE '\b(alert|confirm|prompt)\s*\(' public/` now returns only comment references and the showConfirm definition itself. No native dialogs remain anywhere in the frontend. Playwright test updated to click the in-app modal's #confirm-modal-ok and #confirm-modal-cancel buttons instead of intercepting page.on('dialog'). --- e2e/tests/extensions-crud.spec.js | 10 +++---- public/js/calculators.js | 2 +- public/js/extensions.js | 50 ++++++++++++++++++------------- 3 files changed, 36 insertions(+), 26 deletions(-) diff --git a/e2e/tests/extensions-crud.spec.js b/e2e/tests/extensions-crud.spec.js index 282adf4..66cc962 100644 --- a/e2e/tests/extensions-crud.spec.js +++ b/e2e/tests/extensions-crud.spec.js @@ -149,9 +149,9 @@ test.describe('Extensions — CRUD', () => { await openTab(page); await fillForm(page, { location: 'Loc', name: 'To delete', number: '9999', type: 'extension' }); - // Auto-accept the confirm() dialog - page.once('dialog', d => d.accept()); await page.locator('.ext-card button[data-ext-action="delete"]').first().click(); + // In-app modal — click confirm + await page.click('#confirm-modal-ok'); // Gone from active list await expect(page.locator('#ext-list')).toContainText(/No entries yet/i, { timeout: 5000 }); @@ -188,8 +188,8 @@ test.describe('Extensions — CRUD', () => { await page.click('#ext-trash-btn'); await expect(page.locator('.ext-card')).toContainText('Gone forever'); - page.once('dialog', d => d.accept()); await page.locator('.ext-card button[data-ext-action="purge"]').first().click(); + await page.click('#confirm-modal-ok'); // Trash is empty await expect(page.locator('#ext-list')).toContainText(/Trash is empty/i, { timeout: 5000 }); @@ -203,9 +203,9 @@ test.describe('Extensions — CRUD', () => { await openTab(page); await fillForm(page, { location: 'Loc', name: 'Stays', number: '8888', type: 'extension' }); - // User cancels the confirm dialog - page.once('dialog', d => d.dismiss()); + // User cancels the modal await page.locator('.ext-card button[data-ext-action="delete"]').first().click(); + await page.click('#confirm-modal-cancel'); // Still there await expect(page.locator('.ext-card')).toContainText('Stays'); diff --git a/public/js/calculators.js b/public/js/calculators.js index d62b0bb..9e92fdc 100644 --- a/public/js/calculators.js +++ b/public/js/calculators.js @@ -2057,7 +2057,7 @@ var weightEl = document.getElementById('resus-weight'); var resultDiv = document.getElementById('resus-result'); var w = parseFloat(weightEl.value); - if (!w || w <= 0) { alert('Please enter a valid weight.'); return; } + if (!w || w <= 0) { showToast('Please enter a valid weight.', 'error'); return; } var html = '
Doses for ' + w + ' kg patient
'; html += '
'; diff --git a/public/js/extensions.js b/public/js/extensions.js index fd0a2e8..adff3b2 100644 --- a/public/js/extensions.js +++ b/public/js/extensions.js @@ -228,16 +228,21 @@ function confirmDelete(id) { var item = _items.filter(function (x) { return x.id === id; })[0]; if (!item) return; - if (!confirm('Move "' + item.name + ' (' + item.number + ')" to trash?\n\nYou can restore it later from the trash view.')) return; - fetch('/api/extensions/' + id, { method: 'DELETE', headers: getAuthHeaders() }) - .then(function (r) { return r.json(); }) - .then(function (d) { - if (!d.success) { showToast(d.error || 'Delete failed', 'error'); return; } - showToast('Moved to trash', 'success'); - load(); - loadTrashCount(); - }) - .catch(function () { showToast('Delete failed', 'error'); }); + showConfirm( + 'Move "' + item.name + ' (' + item.number + ')" to trash? You can restore it later from the trash view.', + function () { + fetch('/api/extensions/' + id, { method: 'DELETE', headers: getAuthHeaders() }) + .then(function (r) { return r.json(); }) + .then(function (d) { + if (!d.success) { showToast(d.error || 'Delete failed', 'error'); return; } + showToast('Moved to trash', 'success'); + load(); + loadTrashCount(); + }) + .catch(function () { showToast('Delete failed', 'error'); }); + }, + { confirmText: 'Move to trash' } + ); } function restoreItem(id) { @@ -255,16 +260,21 @@ function confirmPurge(id) { var item = _items.filter(function (x) { return x.id === id; })[0]; if (!item) return; - if (!confirm('Permanently delete "' + item.name + ' (' + item.number + ')"?\n\nThis cannot be undone.')) return; - fetch('/api/extensions/' + id + '/purge', { method: 'DELETE', headers: getAuthHeaders() }) - .then(function (r) { return r.json(); }) - .then(function (d) { - if (!d.success) { showToast(d.error || 'Delete failed', 'error'); return; } - showToast('Permanently deleted', 'success'); - load(); - loadTrashCount(); - }) - .catch(function () { showToast('Delete failed', 'error'); }); + showConfirm( + 'Permanently delete "' + item.name + ' (' + item.number + ')"? This cannot be undone.', + function () { + fetch('/api/extensions/' + id + '/purge', { method: 'DELETE', headers: getAuthHeaders() }) + .then(function (r) { return r.json(); }) + .then(function (d) { + if (!d.success) { showToast(d.error || 'Delete failed', 'error'); return; } + showToast('Permanently deleted', 'success'); + load(); + loadTrashCount(); + }) + .catch(function () { showToast('Delete failed', 'error'); }); + }, + { danger: true, confirmText: 'Delete permanently' } + ); } function toggleTrashMode() {