From 716e852544ca73299c08451adcaf1aadef158bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82o=C5=84ski?= Date: Wed, 29 Apr 2026 23:33:18 +0200 Subject: [PATCH] feat(ui): replace confirm inline input with html dialog --- internal/template/functions_test.go | 20 ++++++ .../template/templates/common/layout.html | 9 +++ internal/ui/static/css/common.css | 30 +++++---- internal/ui/static/js/app.js | 63 +++++++------------ 4 files changed, 71 insertions(+), 51 deletions(-) diff --git a/internal/template/functions_test.go b/internal/template/functions_test.go index f31f1b166e2..644a1717b79 100644 --- a/internal/template/functions_test.go +++ b/internal/template/functions_test.go @@ -272,6 +272,26 @@ func TestQueryString(t *testing.T) { } } +func TestConfirmationModalDefaultsToCancelAction(t *testing.T) { + contents, err := commonTemplateFiles.ReadFile("templates/common/layout.html") + if err != nil { + t.Fatalf(`Unable to read layout template: %v`, err) + } + + layout := string(contents) + if !strings.Contains(layout, ``) { + t.Fatal(`The confirmation modal should be present in the base layout`) + } + + if !strings.Contains(layout, ` + +
+

+
+ + +
+
+
diff --git a/internal/ui/static/css/common.css b/internal/ui/static/css/common.css index 0de01917298..3cfda684bac 100644 --- a/internal/ui/static/css/common.css +++ b/internal/ui/static/css/common.css @@ -675,10 +675,18 @@ template { } dialog { - max-height: none; - height: 100vh; + box-sizing: border-box; + inline-size: fit-content; + max-inline-size: calc(100vw - 2rem); + max-block-size: 80vh; border: none; - padding: 1%; + margin: 10vh auto auto; + padding: 1rem; + overflow: auto; +} + +dialog::backdrop { + background-color: rgba(0, 0, 0, 0.5); } .btn-close-modal { @@ -1291,18 +1299,16 @@ details.entry-enclosures { word-wrap: break-word; } -/* Confirmation */ -.confirm { +#confirmation-modal-question { + margin: 0 0 0.75rem; font-weight: 500; - color: #ed2d04; + text-align: center; } -.confirm button { - color: #ed2d04; - border: none; - background-color: transparent; - cursor: pointer; - font-size: inherit; +.confirmation-modal-actions { + display: flex; + gap: 0.75rem; + justify-content: center; } .loading { diff --git a/internal/ui/static/js/app.js b/internal/ui/static/js/app.js index 7a2b7d65aed..45235464c66 100644 --- a/internal/ui/static/js/app.js +++ b/internal/ui/static/js/app.js @@ -901,68 +901,53 @@ function updateUnreadCounterValue(delta) { } /** - * Handle confirmation messages for actions that require user confirmation. + * Handle confirmation dialogs for actions that require user confirmation. * - * This function modifies the link element to show a confirmation question with "Yes" and "No" buttons. * If the user clicks "Yes", it calls the provided callback with the URL and redirect URL. - * If the user clicks "No", it either redirects to a no-action URL or restores the link element. + * If the user clicks "No", it either redirects to a no-action URL or cancels the action. * * @param {Element} linkElement - The link or button element that triggered the confirmation. * @param {function} callback - The callback function to execute if the user confirms the action. * @returns {void} */ function handleConfirmationMessage(linkElement, callback) { - if (linkElement.tagName !== 'A' && linkElement.tagName !== "BUTTON") { - linkElement = linkElement.parentNode; - } - - linkElement.style.display = "none"; - - const containerElement = linkElement.parentNode; - const questionElement = document.createElement("span"); + linkElement = linkElement.closest("a, button"); + if (!linkElement) return; function createLoadingElement() { const loadingElement = document.createElement("span"); loadingElement.className = "loading"; loadingElement.appendChild(document.createTextNode(linkElement.dataset.labelLoading)); - questionElement.remove(); - containerElement.appendChild(loadingElement); + linkElement.style.display = "none"; + linkElement.parentNode.appendChild(loadingElement); } - const yesElement = document.createElement("button"); - yesElement.appendChild(document.createTextNode(linkElement.dataset.labelYes)); - yesElement.onclick = (event) => { - event.preventDefault(); + const dialogElement = document.getElementById("confirmation-modal"); + const questionElement = document.getElementById("confirmation-modal-question"); + const yesElement = document.getElementById("confirmation-modal-yes"); + const noElement = document.getElementById("confirmation-modal-no"); - createLoadingElement(); + questionElement.textContent = linkElement.dataset.labelQuestion; + yesElement.textContent = linkElement.dataset.labelYes; + noElement.textContent = linkElement.dataset.labelNo; + dialogElement.returnValue = ""; - callback(linkElement.dataset.url, linkElement.dataset.redirectUrl); - }; - - const noElement = document.createElement("button"); - noElement.appendChild(document.createTextNode(linkElement.dataset.labelNo)); - noElement.onclick = (event) => { - event.preventDefault(); - - const noActionUrl = linkElement.dataset.noActionUrl; - if (noActionUrl) { + dialogElement.onclose = () => { + dialogElement.onclose = null; + if (dialogElement.returnValue === "yes") { createLoadingElement(); + callback(linkElement.dataset.url, linkElement.dataset.redirectUrl); + return; + } - callback(noActionUrl, linkElement.dataset.redirectUrl); - } else { - linkElement.style.display = "inline"; - questionElement.remove(); + if (dialogElement.returnValue === "no" && linkElement.dataset.noActionUrl) { + createLoadingElement(); + callback(linkElement.dataset.noActionUrl, linkElement.dataset.redirectUrl); } }; - questionElement.className = "confirm"; - questionElement.appendChild(document.createTextNode(`${linkElement.dataset.labelQuestion} `)); - questionElement.appendChild(yesElement); - questionElement.appendChild(document.createTextNode(", ")); - questionElement.appendChild(noElement); - - containerElement.appendChild(questionElement); + dialogElement.showModal(); } /**