From bd34565e562bc229a20e90e61748c8a9d33915a3 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Fri, 21 Aug 2026 09:20:27 -0700 Subject: [PATCH] admin: keep the copy confirmation in front of the access key modal (#10856) * admin: raise nested modals above the ones already open Bootstrap gives every modal and every backdrop the same z-index, so a modal opened while another is showing paints behind it and its buttons cannot be clicked. Viewing an access key secret and then copying a field left the confirmation stuck behind the details modal with no way to dismiss it. Give each nested modal, and the backdrop Bootstrap creates for it, a z-index above what is already on screen, and put back the scroll lock that Bootstrap drops as soon as any one of them closes. * admin: confirm clipboard copies with a toast The access key details modal offers three copy buttons, and each one raised a modal that had to be dismissed before the next copy. Confirm with a toast instead, so the credentials stay in view and nothing has to be clicked away. --- weed/admin/static/js/admin.js | 10 ++--- weed/admin/static/js/modal-alerts.js | 64 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/weed/admin/static/js/admin.js b/weed/admin/static/js/admin.js index 175a424c5..b09790059 100644 --- a/weed/admin/static/js/admin.js +++ b/weed/admin/static/js/admin.js @@ -634,7 +634,7 @@ function formatDate(date) { function adminCopyToClipboard(text) { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(() => { - showAlert('Copied to clipboard!', 'success'); + showToast('Copied to clipboard!'); }).catch(err => { console.error('Failed to copy text: ', err); fallbackCopyText(text); @@ -660,7 +660,7 @@ function fallbackCopyText(text) { try { const successful = document.execCommand('copy'); if (successful) { - showAlert('Copied to clipboard!', 'success'); + showToast('Copied to clipboard!'); } else { showAlert('Failed to copy to clipboard', 'danger'); } @@ -2265,11 +2265,11 @@ function copyFromInput(inputId) { try { const successful = document.execCommand('copy'); if (successful) { - showAlert('Copied to clipboard!', 'success'); + showToast('Copied to clipboard!'); } else { // Try modern clipboard API as fallback navigator.clipboard.writeText(input.value).then(() => { - showAlert('Copied to clipboard!', 'success'); + showToast('Copied to clipboard!'); }).catch(() => { showAlert('Failed to copy', 'danger'); }); @@ -2277,7 +2277,7 @@ function copyFromInput(inputId) { } catch (err) { // Try modern clipboard API as fallback navigator.clipboard.writeText(input.value).then(() => { - showAlert('Copied to clipboard!', 'success'); + showToast('Copied to clipboard!'); }).catch(() => { showAlert('Failed to copy', 'danger'); }); diff --git a/weed/admin/static/js/modal-alerts.js b/weed/admin/static/js/modal-alerts.js index e8ccd54b8..765561254 100644 --- a/weed/admin/static/js/modal-alerts.js +++ b/weed/admin/static/js/modal-alerts.js @@ -168,6 +168,36 @@ bsModal.show(); }; + /** + * Show a transient message that does not have to be dismissed + * @param {string} message - The message to display + * @param {string} type - Bootstrap contextual color, defaults to 'success' + */ + window.showToast = function (message, type) { + let container = document.getElementById('globalToastContainer'); + if (!container) { + container = document.createElement('div'); + container.id = 'globalToastContainer'; + container.className = 'toast-container position-fixed top-0 end-0 p-3'; + // Above any modal, however deeply they are stacked. + container.style.zIndex = '9999'; + document.body.appendChild(container); + } + + const toast = document.createElement('div'); + toast.className = 'toast align-items-center text-white bg-' + (type || 'success') + ' border-0'; + toast.setAttribute('role', 'alert'); + toast.innerHTML = '
' + escapeHtml(message) + '
' + + '
'; + container.appendChild(toast); + + toast.addEventListener('hidden.bs.toast', function () { + toast.remove(); + }); + + new bootstrap.Toast(toast, { delay: 3000 }).show(); + }; + /** * Show a confirmation dialog using Bootstrap modal * @param {string} message - The confirmation message @@ -290,6 +320,40 @@ return text.replace(/[&<>"']/g, function (m) { return map[m]; }); } + // Bootstrap gives every modal and backdrop the same z-index, so a modal + // opened while another one is showing paints behind it and cannot be + // clicked. Lift each nested modal, and its backdrop, above what is already + // on screen. + const MODAL_Z_INDEX = 1055; + const MODAL_Z_INDEX_STEP = 20; + + document.addEventListener('show.bs.modal', function (event) { + const depth = document.querySelectorAll('.modal.show').length; + if (depth === 0) { + return; + } + + const zIndex = MODAL_Z_INDEX + depth * MODAL_Z_INDEX_STEP; + event.target.style.zIndex = zIndex; + + // Bootstrap only creates the backdrop after this event returns. + setTimeout(function () { + const backdrops = document.querySelectorAll('.modal-backdrop'); + const backdrop = backdrops[backdrops.length - 1]; + if (backdrop) { + backdrop.style.zIndex = zIndex - 1; + } + }, 0); + }); + + document.addEventListener('hidden.bs.modal', function (event) { + event.target.style.zIndex = ''; + // Closing any modal releases the scroll lock the others still need. + if (document.querySelector('.modal.show')) { + document.body.classList.add('modal-open'); + } + }); + // Auto-initialize on DOMContentLoaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', ensureModalsExist);