Files
michaelschiemer/resources/js/modules/livecomponent/LiveComponentUIHelper.js
Michael Schiemer 36ef2a1e2c
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
fix: Gitea Traefik routing and connection pool optimization
- Remove middleware reference from Gitea Traefik labels (caused routing issues)
- Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s)
- Add explicit service reference in Traefik labels
- Fix intermittent 504 timeouts by improving PostgreSQL connection handling

Fixes Gitea unreachability via git.michaelschiemer.de
2025-11-09 14:46:15 +01:00

348 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* LiveComponent UI Helper
*
* Provides unified API for UI components (Dialogs, Modals, Notifications)
* integrated with LiveComponents.
*/
import { UIManager } from '../ui/UIManager.js';
export class LiveComponentUIHelper {
constructor(liveComponentManager) {
this.manager = liveComponentManager;
this.uiManager = UIManager;
this.activeDialogs = new Map(); // componentId → dialog instances
this.activeNotifications = new Map(); // componentId → notification instances
}
/**
* Show dialog from LiveComponent action
*
* @param {string} componentId - Component ID
* @param {Object} options - Dialog options
* @returns {Object} Dialog instance
*/
showDialog(componentId, options = {}) {
const {
title = '',
content = '',
size = 'medium',
buttons = [],
closeOnBackdrop = true,
closeOnEscape = true,
onClose = null,
onConfirm = null
} = options;
// Create dialog content
const dialogContent = this.createDialogContent(title, content, buttons, {
onClose,
onConfirm
});
// Show modal via UIManager
const modal = this.uiManager.open('modal', {
content: dialogContent,
className: `livecomponent-dialog livecomponent-dialog--${size}`,
onClose: () => {
if (onClose) {
onClose();
}
this.activeDialogs.delete(componentId);
}
});
// Store reference
this.activeDialogs.set(componentId, modal);
return modal;
}
/**
* Show confirmation dialog
*
* @param {string} componentId - Component ID
* @param {Object} options - Confirmation options
* @returns {Promise<boolean>} Promise resolving to true if confirmed
*/
showConfirm(componentId, options = {}) {
return new Promise((resolve) => {
const {
title = 'Confirm',
message = 'Are you sure?',
confirmText = 'Confirm',
cancelText = 'Cancel',
confirmClass = 'btn-primary',
cancelClass = 'btn-secondary'
} = options;
const buttons = [
{
text: cancelText,
class: cancelClass,
action: () => {
this.closeDialog(componentId);
resolve(false);
}
},
{
text: confirmText,
class: confirmClass,
action: () => {
this.closeDialog(componentId);
resolve(true);
}
}
];
this.showDialog(componentId, {
title,
content: `<p>${message}</p>`,
buttons,
size: 'small'
});
});
}
/**
* Show alert dialog
*
* @param {string} componentId - Component ID
* @param {Object} options - Alert options
*/
showAlert(componentId, options = {}) {
const {
title = 'Alert',
message = '',
buttonText = 'OK',
type = 'info' // info, success, warning, error
} = options;
const buttons = [
{
text: buttonText,
class: `btn-${type}`,
action: () => {
this.closeDialog(componentId);
}
}
];
return this.showDialog(componentId, {
title,
content: `<p class="alert-message alert-message--${type}">${message}</p>`,
buttons,
size: 'small'
});
}
/**
* Close dialog
*
* @param {string} componentId - Component ID
*/
closeDialog(componentId) {
const dialog = this.activeDialogs.get(componentId);
if (dialog && typeof dialog.close === 'function') {
dialog.close();
this.activeDialogs.delete(componentId);
}
}
/**
* Show notification
*
* @param {string} componentId - Component ID
* @param {Object} options - Notification options
*/
showNotification(componentId, options = {}) {
const {
message = '',
type = 'info', // info, success, warning, error
duration = 5000,
position = 'top-right',
action = null
} = options;
// Create notification element
const notification = document.createElement('div');
notification.className = `livecomponent-notification livecomponent-notification--${type} livecomponent-notification--${position}`;
notification.setAttribute('role', 'alert');
notification.setAttribute('aria-live', 'polite');
notification.innerHTML = `
<div class="notification-content">
<span class="notification-message">${message}</span>
${action ? `<button class="notification-action">${action.text}</button>` : ''}
<button class="notification-close" aria-label="Close">×</button>
</div>
`;
// Add styles
notification.style.cssText = `
position: fixed;
${position.includes('top') ? 'top' : 'bottom'}: 1rem;
${position.includes('left') ? 'left' : 'right'}: 1rem;
max-width: 400px;
padding: 1rem;
background: ${this.getNotificationColor(type)};
color: white;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
z-index: 10000;
opacity: 0;
transform: translateY(${position.includes('top') ? '-20px' : '20px'});
transition: opacity 0.3s ease, transform 0.3s ease;
`;
// Add to DOM
document.body.appendChild(notification);
// Animate in
requestAnimationFrame(() => {
notification.style.opacity = '1';
notification.style.transform = 'translateY(0)';
});
// Setup close button
const closeBtn = notification.querySelector('.notification-close');
closeBtn.addEventListener('click', () => {
this.hideNotification(notification);
});
// Setup action button
if (action) {
const actionBtn = notification.querySelector('.notification-action');
actionBtn.addEventListener('click', () => {
if (action.handler) {
action.handler();
}
this.hideNotification(notification);
});
}
// Auto-hide after duration
if (duration > 0) {
setTimeout(() => {
this.hideNotification(notification);
}, duration);
}
// Store reference
this.activeNotifications.set(componentId, notification);
return notification;
}
/**
* Hide notification
*
* @param {HTMLElement|string} notificationOrComponentId - Notification element or component ID
*/
hideNotification(notificationOrComponentId) {
const notification = typeof notificationOrComponentId === 'string'
? this.activeNotifications.get(notificationOrComponentId)
: notificationOrComponentId;
if (!notification) {
return;
}
// Animate out
notification.style.opacity = '0';
notification.style.transform = `translateY(${notification.classList.contains('livecomponent-notification--top') ? '-20px' : '20px'})`;
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
// Remove from map
for (const [componentId, notif] of this.activeNotifications.entries()) {
if (notif === notification) {
this.activeNotifications.delete(componentId);
break;
}
}
}, 300);
}
/**
* Create dialog content HTML
*
* @param {string} title - Dialog title
* @param {string} content - Dialog content
* @param {Array} buttons - Button configurations
* @param {Object} callbacks - Callback functions
* @returns {string} HTML content
*/
createDialogContent(title, content, buttons, callbacks) {
const buttonsHtml = buttons.map((btn, index) => {
const btnClass = btn.class || 'btn-secondary';
const btnAction = btn.action || (() => {});
return `<button class="btn ${btnClass}" data-dialog-action="${index}">${btn.text || 'Button'}</button>`;
}).join('');
return `
<div class="livecomponent-dialog-content">
${title ? `<div class="dialog-header"><h3>${title}</h3></div>` : ''}
<div class="dialog-body">${content}</div>
${buttons.length > 0 ? `<div class="dialog-footer">${buttonsHtml}</div>` : ''}
</div>
`;
}
/**
* Get notification color by type
*
* @param {string} type - Notification type
* @returns {string} Color value
*/
getNotificationColor(type) {
const colors = {
info: '#3b82f6',
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444'
};
return colors[type] || colors.info;
}
/**
* Show loading dialog
*
* @param {string} componentId - Component ID
* @param {string} message - Loading message
* @returns {Object} Dialog instance
*/
showLoadingDialog(componentId, message = 'Loading...') {
return this.showDialog(componentId, {
title: '',
content: `
<div class="loading-dialog">
<div class="spinner"></div>
<p>${message}</p>
</div>
`,
size: 'small',
buttons: [],
closeOnBackdrop: false,
closeOnEscape: false
});
}
/**
* Cleanup all UI components for component
*
* @param {string} componentId - Component ID
*/
cleanup(componentId) {
// Close dialogs
this.closeDialog(componentId);
// Hide notifications
this.hideNotification(componentId);
}
}