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
- 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
348 lines
10 KiB
JavaScript
348 lines
10 KiB
JavaScript
/**
|
||
* 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);
|
||
}
|
||
}
|
||
|