Add a visible toast stack, fix presentError double-notify, wire density and auto-refresh settings, gate destructive confirms, improve empty/skeleton/search on resource lists, and enable browser history plus keyboard palette navigation.
This commit is contained in:
+120
-25
@@ -9,36 +9,29 @@ import notificationManager from './notifications.js';
|
||||
* Close all open Bootstrap modals and clean up any lingering backdrops
|
||||
*/
|
||||
export function closeAllModals() {
|
||||
// Find and hide all open modals
|
||||
const modals = document.querySelectorAll('.modal.show, .modal[style*="display"]');
|
||||
modals.forEach(modal => {
|
||||
modals.forEach((modal) => {
|
||||
const modalInstance = bootstrap.Modal.getInstance(modal);
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
} else {
|
||||
// If no instance exists, create one and hide it
|
||||
const newInstance = new bootstrap.Modal(modal);
|
||||
newInstance.hide();
|
||||
}
|
||||
// Also directly hide the modal element as fallback
|
||||
modal.classList.remove('show');
|
||||
modal.style.display = 'none';
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
modal.removeAttribute('aria-modal');
|
||||
});
|
||||
|
||||
// Remove any lingering modal backdrops
|
||||
const backdrops = document.querySelectorAll('.modal-backdrop');
|
||||
backdrops.forEach(backdrop => {
|
||||
|
||||
document.querySelectorAll('.modal-backdrop').forEach((backdrop) => {
|
||||
backdrop.remove();
|
||||
});
|
||||
|
||||
// Clean up body classes and styles
|
||||
|
||||
document.body.classList.remove('modal-open');
|
||||
document.body.style.paddingRight = '';
|
||||
document.body.style.overflow = '';
|
||||
|
||||
// Also handle custom terminal modal if it exists
|
||||
|
||||
const terminalModal = document.getElementById('terminal-modal');
|
||||
if (terminalModal && terminalModal.style.display !== 'none') {
|
||||
terminalModal.style.display = 'none';
|
||||
@@ -47,11 +40,9 @@ export function closeAllModals() {
|
||||
|
||||
/**
|
||||
* Activity / progress indicator — routes to the job/event tray (no full-screen spinner).
|
||||
* Falls back to a non-blocking notification if the ops shell is not ready yet.
|
||||
* @param {string} message - Message to display
|
||||
* @param {string} message
|
||||
*/
|
||||
export function showStatusIndicator(message = 'Processing...') {
|
||||
// Defensive: never leave a legacy full-screen overlay around
|
||||
removeLegacyStatusOverlay();
|
||||
|
||||
if (typeof window !== 'undefined' && window.peardockOps?.showActivity) {
|
||||
@@ -67,8 +58,7 @@ export function showStatusIndicator(message = 'Processing...') {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status / activity message in the job tray
|
||||
* @param {string} message - New message to display
|
||||
* @param {string} message
|
||||
*/
|
||||
export function updateStatusIndicator(message) {
|
||||
if (typeof window !== 'undefined' && window.peardockOps?.updateActivity) {
|
||||
@@ -83,8 +73,7 @@ export function updateStatusIndicator(message) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete / hide activity indicator in the job tray
|
||||
* @param {boolean} [ok=true] - Whether the activity succeeded
|
||||
* @param {boolean} [ok=true]
|
||||
*/
|
||||
export function hideStatusIndicator(ok = true) {
|
||||
if (typeof window !== 'undefined' && window.peardockOps?.completeActivity) {
|
||||
@@ -106,12 +95,118 @@ function removeLegacyStatusOverlay() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Show alert message (now uses notification system)
|
||||
* @param {string} type - Alert type (success, danger, warning, info)
|
||||
* @param {string} message - Message to display
|
||||
* @param {Object} options - Additional options (autoDismiss, duration)
|
||||
* Ensure toast host exists (top-center stack).
|
||||
* @returns {HTMLElement|null}
|
||||
*/
|
||||
function ensureToastStack() {
|
||||
if (typeof document === 'undefined') return null;
|
||||
let stack = document.getElementById('toast-stack');
|
||||
if (stack) return stack;
|
||||
|
||||
// Prefer legacy alert-container if present
|
||||
const legacy = document.getElementById('alert-container');
|
||||
if (legacy) {
|
||||
legacy.id = 'toast-stack';
|
||||
legacy.className = 'toast-stack';
|
||||
legacy.removeAttribute('style');
|
||||
legacy.setAttribute('aria-live', 'polite');
|
||||
legacy.setAttribute('aria-atomic', 'false');
|
||||
return legacy;
|
||||
}
|
||||
|
||||
stack = document.createElement('div');
|
||||
stack.id = 'toast-stack';
|
||||
stack.className = 'toast-stack';
|
||||
stack.setAttribute('aria-live', 'polite');
|
||||
stack.setAttribute('aria-atomic', 'false');
|
||||
document.body.appendChild(stack);
|
||||
return stack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Floating toast (visible feedback) + notification tray history.
|
||||
* @param {string} type - success | danger | warning | info
|
||||
* @param {string} message
|
||||
* @param {{ autoDismiss?: boolean, duration?: number, toast?: boolean, tray?: boolean }} [options]
|
||||
*/
|
||||
export function showAlert(type, message, options = {}) {
|
||||
// Use the new notification system
|
||||
notificationManager.add(type, message, options);
|
||||
const {
|
||||
autoDismiss = true,
|
||||
duration = 4500,
|
||||
toast = true,
|
||||
tray = true,
|
||||
} = options;
|
||||
|
||||
const text = String(message ?? '');
|
||||
if (!text) return;
|
||||
|
||||
// Persist in notification tray (history)
|
||||
if (tray !== false) {
|
||||
try {
|
||||
notificationManager.add(type, text, {
|
||||
autoDismiss: false,
|
||||
...options,
|
||||
});
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Visible toast stack
|
||||
if (toast === false) return;
|
||||
const stack = ensureToastStack();
|
||||
if (!stack) return;
|
||||
|
||||
const tone = ['success', 'danger', 'warning', 'info'].includes(type) ? type : 'info';
|
||||
const iconMap = {
|
||||
success: 'fa-circle-check',
|
||||
danger: 'fa-circle-xmark',
|
||||
warning: 'fa-triangle-exclamation',
|
||||
info: 'fa-circle-info',
|
||||
};
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = `pd-toast pd-toast--${tone}`;
|
||||
el.setAttribute('role', 'status');
|
||||
el.innerHTML = `
|
||||
<i class="fas ${iconMap[tone]} pd-toast-icon" aria-hidden="true"></i>
|
||||
<div class="pd-toast-body">${escapeToast(text)}</div>
|
||||
<button type="button" class="pd-toast-close" aria-label="Dismiss">
|
||||
<i class="fas fa-xmark"></i>
|
||||
</button>`;
|
||||
|
||||
let timer = null;
|
||||
const dismiss = () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
el.classList.add('pd-toast--leaving');
|
||||
setTimeout(() => el.remove(), 220);
|
||||
};
|
||||
|
||||
el.querySelector('.pd-toast-close')?.addEventListener('click', dismiss);
|
||||
el.addEventListener('mouseenter', () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
});
|
||||
el.addEventListener('mouseleave', () => {
|
||||
if (autoDismiss !== false) {
|
||||
timer = setTimeout(dismiss, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
stack.prepend(el);
|
||||
// Cap stack size
|
||||
while (stack.children.length > 5) {
|
||||
stack.lastElementChild?.remove();
|
||||
}
|
||||
|
||||
if (autoDismiss !== false) {
|
||||
timer = setTimeout(dismiss, duration);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeToast(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user