Files

968 lines
35 KiB
JavaScript

/**
* ComfyUI Queue Manager Frontend JavaScript
* Handles the user interface interactions and API communication
*/
class QueueManager {
constructor() {
this.apiBase = '/queue';
this.queueItems = [];
this.selectedItems = new Set();
this.queueState = 'running';
this.updateInterval = null;
this.currentFilters = {
search: '',
status: '',
dateRange: null
};
this.init();
}
init() {
this.bindEvents();
this.loadQueueItems();
this.updateQueueState();
this.startRealTimeUpdates();
}
bindEvents() {
// Control buttons
document.getElementById('pause-resume-btn').addEventListener('click', () => {
this.toggleQueueState();
});
document.getElementById('refresh-btn').addEventListener('click', () => {
this.loadQueueItems();
});
document.getElementById('import-export-btn').addEventListener('click', () => {
this.showModal('import-export-modal');
});
document.getElementById('help-btn').addEventListener('click', () => {
this.showModal('help-modal');
});
// Bulk actions
document.getElementById('archive-selected-btn').addEventListener('click', () => {
this.archiveSelected();
});
document.getElementById('delete-selected-btn').addEventListener('click', () => {
this.deleteSelected();
});
document.getElementById('restore-selected-btn').addEventListener('click', () => {
this.restoreSelected();
});
// Filters
document.getElementById('search-input').addEventListener('input', (e) => {
this.currentFilters.search = e.target.value;
this.filterItems();
});
document.getElementById('status-filter').addEventListener('change', (e) => {
this.currentFilters.status = e.target.value;
this.filterItems();
});
// Modal events
this.bindModalEvents();
// Keyboard shortcuts
this.bindKeyboardShortcuts();
}
bindModalEvents() {
// Modal overlay click to close
document.getElementById('modal-overlay').addEventListener('click', (e) => {
if (e.target.id === 'modal-overlay') {
this.hideModal();
}
});
// Modal close buttons
document.querySelectorAll('.modal-close').forEach(btn => {
btn.addEventListener('click', () => {
this.hideModal();
});
});
// Tab switching
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
this.switchTab(e.target.dataset.tab);
});
});
// Import/Export modal actions
document.getElementById('export-btn').addEventListener('click', () => {
this.exportQueue();
});
document.getElementById('import-btn').addEventListener('click', () => {
this.importQueue();
});
// Archive modal actions
document.getElementById('archive-confirm-btn').addEventListener('click', () => {
this.confirmArchive();
});
// Confirmation modal
document.getElementById('confirm-btn').addEventListener('click', () => {
this.executeConfirmedAction();
});
}
async loadQueueItems() {
try {
this.showLoading();
// Placeholder for API call - will be implemented in later tasks
// const response = await fetch(`${this.apiBase}/items`);
// this.queueItems = await response.json();
// Mock data for demonstration
this.queueItems = this.generateMockData();
this.renderQueueItems();
} catch (error) {
console.error('Failed to load queue items:', error);
this.showError('Failed to load queue items');
}
}
generateMockData() {
// Generate some mock queue items for demonstration
const statuses = ['pending', 'running', 'completed', 'failed', 'archived'];
const workflowNames = [
'Text to Image Generation',
'Image Upscaling',
'Style Transfer',
'Background Removal',
'Face Enhancement'
];
const mockItems = [];
for (let i = 0; i < 10; i++) {
const createdAt = new Date(Date.now() - Math.random() * 86400000 * 7); // Last 7 days
const updatedAt = new Date(createdAt.getTime() + Math.random() * 3600000); // Up to 1 hour later
mockItems.push({
id: `item-${i + 1}`,
workflow_name: workflowNames[Math.floor(Math.random() * workflowNames.length)],
status: statuses[Math.floor(Math.random() * statuses.length)],
created_at: createdAt.toISOString(),
updated_at: updatedAt.toISOString(),
started_at: Math.random() > 0.5 ? updatedAt.toISOString() : null,
completed_at: Math.random() > 0.7 ? new Date(updatedAt.getTime() + Math.random() * 1800000).toISOString() : null,
error_message: Math.random() > 0.8 ? 'Sample error message for demonstration' : null,
workflow_data: { nodes: [], connections: [] }
});
}
return mockItems;
}
startRealTimeUpdates() {
// Update queue items every 5 seconds
this.updateInterval = setInterval(() => {
this.loadQueueItems();
}, 5000);
}
stopRealTimeUpdates() {
if (this.updateInterval) {
clearInterval(this.updateInterval);
this.updateInterval = null;
}
}
async updateQueueState() {
try {
// Placeholder for API call - will be implemented in later tasks
// const response = await fetch(`${this.apiBase}/status`);
// const data = await response.json();
// this.queueState = data.state;
this.updatePauseResumeButton();
} catch (error) {
console.error('Failed to update queue state:', error);
}
}
async toggleQueueState() {
try {
const newState = this.queueState === 'running' ? 'paused' : 'running';
// Placeholder for API call - will be implemented in later tasks
// await fetch(`${this.apiBase}/${newState}`, { method: 'POST' });
this.queueState = newState;
this.updatePauseResumeButton();
} catch (error) {
console.error('Failed to toggle queue state:', error);
}
}
updatePauseResumeButton() {
const btn = document.getElementById('pause-resume-btn');
const header = document.querySelector('header h1');
if (this.queueState === 'running') {
btn.textContent = '⏸️ Pause Queue';
btn.className = 'btn btn-warning';
header.style.color = '#4CAF50';
this.showStatusIndicator('Queue is running', 'success');
} else {
btn.textContent = '▶️ Resume Queue';
btn.className = 'btn btn-primary';
header.style.color = '#ff9800';
this.showStatusIndicator('Queue is paused', 'warning');
}
}
showStatusIndicator(message, type) {
// Remove existing status indicator
const existing = document.querySelector('.status-indicator');
if (existing) {
existing.remove();
}
// Create new status indicator
const indicator = document.createElement('div');
indicator.className = `status-indicator status-${type}`;
indicator.textContent = message;
const header = document.querySelector('header');
header.appendChild(indicator);
// Auto-hide after 3 seconds
setTimeout(() => {
if (indicator.parentNode) {
indicator.remove();
}
}, 3000);
}
renderQueueItems() {
const container = document.getElementById('queue-items');
if (this.queueItems.length === 0) {
container.innerHTML = `
<div class="empty-state">
<h3>No queue items</h3>
<p>Queue items will appear here when workflows are added to the queue.</p>
</div>
`;
return;
}
container.innerHTML = this.queueItems.map(item => this.renderQueueItem(item)).join('');
this.bindItemEvents();
}
renderQueueItem(item) {
const createdAt = new Date(item.created_at).toLocaleString();
const updatedAt = new Date(item.updated_at).toLocaleString();
const startedAt = item.started_at ? new Date(item.started_at).toLocaleString() : null;
const completedAt = item.completed_at ? new Date(item.completed_at).toLocaleString() : null;
// Calculate duration if applicable
let duration = '';
if (item.started_at && item.completed_at) {
const start = new Date(item.started_at);
const end = new Date(item.completed_at);
const durationMs = end - start;
duration = this.formatDuration(durationMs);
} else if (item.started_at && item.status === 'running') {
const start = new Date(item.started_at);
const now = new Date();
const durationMs = now - start;
duration = this.formatDuration(durationMs) + ' (running)';
}
return `
<div class="queue-item ${item.status}" data-id="${item.id}" data-status="${item.status}">
<div class="queue-item-header">
<div class="item-header-left">
<input type="checkbox" class="checkbox item-checkbox" data-id="${item.id}"
${this.selectedItems.has(item.id) ? 'checked' : ''}>
<span class="queue-item-title">${item.workflow_name || 'Unnamed Workflow'}</span>
</div>
<div class="item-header-right">
<span class="queue-item-status status-${item.status}">
${item.status === 'running' ? '<span class="spinner"></span>' : ''}
${item.status.toUpperCase()}
</span>
</div>
</div>
<div class="queue-item-meta">
<span><strong>Created:</strong> ${createdAt}</span>
<span><strong>Updated:</strong> ${updatedAt}</span>
${startedAt ? `<span><strong>Started:</strong> ${startedAt}</span>` : ''}
${completedAt ? `<span><strong>Completed:</strong> ${completedAt}</span>` : ''}
${duration ? `<span><strong>Duration:</strong> ${duration}</span>` : ''}
<span><strong>ID:</strong> ${item.id}</span>
</div>
${item.error_message ? `<div class="error-message"><strong>Error:</strong> ${item.error_message}</div>` : ''}
<div class="queue-item-details" style="display: none;">
<h4>Workflow Data:</h4>
<pre>${JSON.stringify(item.workflow_data, null, 2)}</pre>
</div>
<div class="queue-item-actions">
<button class="btn btn-secondary toggle-details-btn" data-id="${item.id}">Show Details</button>
${this.getItemActions(item)}
</div>
</div>
`;
}
formatDuration(milliseconds) {
const seconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
} else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
} else {
return `${seconds}s`;
}
}
getItemActions(item) {
const actions = [];
if (item.status === 'archived') {
actions.push('<button class="btn btn-secondary restore-btn" data-id="' + item.id + '">Restore</button>');
} else {
actions.push('<button class="btn btn-warning archive-btn" data-id="' + item.id + '">Archive</button>');
}
if (item.status === 'pending' || item.status === 'failed') {
actions.push('<button class="btn btn-primary retry-btn" data-id="' + item.id + '">Retry</button>');
}
actions.push('<button class="btn btn-danger delete-btn" data-id="' + item.id + '">Delete</button>');
return actions.join('');
}
bindItemEvents() {
// Checkbox selection
document.querySelectorAll('.item-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const itemId = e.target.dataset.id;
if (e.target.checked) {
this.selectedItems.add(itemId);
} else {
this.selectedItems.delete(itemId);
}
this.updateBulkActionButtons();
});
});
// Toggle details
document.querySelectorAll('.toggle-details-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
this.toggleItemDetails(e.target.dataset.id);
});
});
// Individual item actions
document.querySelectorAll('.archive-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
this.archiveItem(e.target.dataset.id);
});
});
document.querySelectorAll('.restore-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
this.restoreItem(e.target.dataset.id);
});
});
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
this.deleteItem(e.target.dataset.id);
});
});
document.querySelectorAll('.retry-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
this.retryItem(e.target.dataset.id);
});
});
// Item click selection (excluding buttons and checkboxes)
document.querySelectorAll('.queue-item').forEach(item => {
item.addEventListener('click', (e) => {
if (!e.target.matches('button, input, .btn, .checkbox')) {
this.toggleItemSelection(item.dataset.id);
}
});
});
}
toggleItemDetails(itemId) {
const item = document.querySelector(`[data-id="${itemId}"]`);
const details = item.querySelector('.queue-item-details');
const btn = item.querySelector('.toggle-details-btn');
if (details.style.display === 'none') {
details.style.display = 'block';
btn.textContent = 'Hide Details';
} else {
details.style.display = 'none';
btn.textContent = 'Show Details';
}
}
toggleItemSelection(itemId) {
const checkbox = document.querySelector(`[data-id="${itemId}"].item-checkbox`);
checkbox.checked = !checkbox.checked;
checkbox.dispatchEvent(new Event('change'));
}
updateBulkActionButtons() {
const archiveBtn = document.getElementById('archive-selected-btn');
const restoreBtn = document.getElementById('restore-selected-btn');
const deleteBtn = document.getElementById('delete-selected-btn');
const selectedCount = this.selectedItems.size;
// Count archived items in selection
const archivedCount = Array.from(this.selectedItems).filter(itemId => {
const item = this.queueItems.find(i => i.id === itemId);
return item && item.status === 'archived';
}).length;
archiveBtn.disabled = selectedCount === 0;
restoreBtn.disabled = archivedCount === 0;
deleteBtn.disabled = selectedCount === 0;
if (selectedCount > 0) {
archiveBtn.textContent = `Archive Selected (${selectedCount})`;
deleteBtn.textContent = `Delete Selected (${selectedCount})`;
} else {
archiveBtn.textContent = 'Archive Selected';
deleteBtn.textContent = 'Delete Selected';
}
if (archivedCount > 0) {
restoreBtn.textContent = `Restore Selected (${archivedCount})`;
} else {
restoreBtn.textContent = 'Restore Selected';
}
}
filterItems() {
const searchTerm = this.currentFilters.search.toLowerCase();
const statusFilter = this.currentFilters.status;
let visibleCount = 0;
let totalCount = 0;
document.querySelectorAll('.queue-item').forEach(item => {
totalCount++;
const title = item.querySelector('.queue-item-title').textContent.toLowerCase();
const status = item.dataset.status;
const itemId = item.dataset.id;
// Search in multiple fields
const searchableText = [
title,
itemId.toLowerCase(),
item.querySelector('.queue-item-meta').textContent.toLowerCase()
].join(' ');
const matchesSearch = !searchTerm || searchableText.includes(searchTerm);
const matchesStatus = !statusFilter || status === statusFilter;
const isVisible = matchesSearch && matchesStatus;
item.style.display = isVisible ? 'block' : 'none';
if (isVisible) {
visibleCount++;
}
});
this.updateFilterStatus(visibleCount, totalCount);
}
updateFilterStatus(visibleCount, totalCount) {
// Update the queue header to show filter results
const queueHeader = document.querySelector('.queue-header h2');
if (visibleCount === totalCount) {
queueHeader.textContent = `Queue Items (${totalCount})`;
} else {
queueHeader.textContent = `Queue Items (${visibleCount} of ${totalCount})`;
}
// Show/hide empty state for filtered results
if (visibleCount === 0 && totalCount > 0) {
this.showFilteredEmptyState();
}
}
showFilteredEmptyState() {
const container = document.getElementById('queue-items');
const existingEmpty = container.querySelector('.filtered-empty-state');
if (!existingEmpty) {
const emptyState = document.createElement('div');
emptyState.className = 'filtered-empty-state empty-state';
emptyState.innerHTML = `
<h3>No items match your filters</h3>
<p>Try adjusting your search terms or status filter.</p>
<button class="btn btn-secondary" onclick="queueManager.clearFilters()">Clear Filters</button>
`;
container.appendChild(emptyState);
}
}
clearFilters() {
document.getElementById('search-input').value = '';
document.getElementById('status-filter').value = '';
this.currentFilters = { search: '', status: '', dateRange: null };
// Remove filtered empty state
const filteredEmpty = document.querySelector('.filtered-empty-state');
if (filteredEmpty) {
filteredEmpty.remove();
}
this.filterItems();
}
async archiveSelected() {
if (this.selectedItems.size === 0) {
alert('Please select items to archive');
return;
}
this.showModal('archive-modal');
}
async deleteSelected() {
if (this.selectedItems.size === 0) {
alert('Please select items to delete');
return;
}
this.showConfirmation(
'Delete Selected Items',
`Are you sure you want to delete ${this.selectedItems.size} selected item(s)? This action cannot be undone.`,
() => {
try {
// Placeholder for API call - will be implemented in later tasks
console.log('Deleting items:', Array.from(this.selectedItems));
this.selectedItems.clear();
this.loadQueueItems();
} catch (error) {
console.error('Failed to delete items:', error);
}
}
);
}
async archiveItem(itemId) {
try {
// Placeholder for API call - will be implemented in later tasks
console.log('Archiving item:', itemId);
this.loadQueueItems();
} catch (error) {
console.error('Failed to archive item:', error);
}
}
async restoreItem(itemId) {
this.showConfirmation(
'Restore Item',
'Are you sure you want to restore this item to the active queue?',
() => {
try {
// Restore item (in real implementation, this would be an API call)
const item = this.queueItems.find(i => i.id === itemId);
if (item) {
item.status = 'pending';
item.restored_at = new Date().toISOString();
delete item.archived_at;
delete item.archive_options;
}
this.renderQueueItems();
this.showStatusIndicator('Item restored successfully', 'success');
} catch (error) {
console.error('Failed to restore item:', error);
this.showStatusIndicator('Failed to restore item', 'error');
}
}
);
}
// Add bulk restore functionality
restoreSelected() {
if (this.selectedItems.size === 0) {
this.showStatusIndicator('Please select items to restore', 'warning');
return;
}
const archivedItems = Array.from(this.selectedItems).filter(itemId => {
const item = this.queueItems.find(i => i.id === itemId);
return item && item.status === 'archived';
});
if (archivedItems.length === 0) {
this.showStatusIndicator('No archived items selected', 'warning');
return;
}
this.showConfirmation(
'Restore Selected Items',
`Are you sure you want to restore ${archivedItems.length} archived item(s) to the active queue?`,
() => {
try {
archivedItems.forEach(itemId => {
const item = this.queueItems.find(i => i.id === itemId);
if (item) {
item.status = 'pending';
item.restored_at = new Date().toISOString();
delete item.archived_at;
delete item.archive_options;
}
});
this.selectedItems.clear();
this.renderQueueItems();
this.updateBulkActionButtons();
this.showStatusIndicator(`Restored ${archivedItems.length} items`, 'success');
} catch (error) {
console.error('Failed to restore items:', error);
this.showStatusIndicator('Failed to restore items', 'error');
}
}
);
}
async deleteItem(itemId) {
this.showConfirmation(
'Delete Item',
'Are you sure you want to delete this item? This action cannot be undone.',
() => {
try {
// Placeholder for API call - will be implemented in later tasks
console.log('Deleting item:', itemId);
this.loadQueueItems();
} catch (error) {
console.error('Failed to delete item:', error);
}
}
);
}
async retryItem(itemId) {
try {
// Placeholder for API call - will be implemented in later tasks
console.log('Retrying item:', itemId);
this.loadQueueItems();
} catch (error) {
console.error('Failed to retry item:', error);
}
}
showLoading() {
document.getElementById('queue-items').innerHTML = `
<div class="loading">
<p>Loading queue items...</p>
</div>
`;
}
showError(message) {
document.getElementById('queue-items').innerHTML = `
<div class="empty-state">
<h3>Error</h3>
<p>${message}</p>
</div>
`;
}
// Modal management methods
showModal(modalId) {
document.getElementById('modal-overlay').classList.add('active');
document.querySelectorAll('.modal').forEach(modal => {
modal.style.display = 'none';
});
document.getElementById(modalId).style.display = 'block';
}
hideModal() {
document.getElementById('modal-overlay').classList.remove('active');
document.querySelectorAll('.modal').forEach(modal => {
modal.style.display = 'none';
});
}
switchTab(tabName) {
// Update tab buttons
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`${tabName}-tab`).classList.add('active');
}
showConfirmation(title, message, action) {
document.getElementById('confirmation-title').textContent = title;
document.getElementById('confirmation-message').textContent = message;
this.pendingAction = action;
this.showModal('confirmation-modal');
}
executeConfirmedAction() {
if (this.pendingAction) {
this.pendingAction();
this.pendingAction = null;
}
this.hideModal();
}
// Enhanced import/export functionality
exportQueue() {
const exportAll = document.getElementById('export-all').checked;
const exportSelected = document.getElementById('export-selected').checked;
let itemsToExport = [];
if (exportAll) {
itemsToExport = this.queueItems;
} else if (exportSelected && this.selectedItems.size > 0) {
itemsToExport = this.queueItems.filter(item => this.selectedItems.has(item.id));
} else {
this.showStatusIndicator('Please select items to export or choose "Export all"', 'error');
return;
}
if (itemsToExport.length === 0) {
this.showStatusIndicator('No items to export', 'warning');
return;
}
try {
const exportData = {
version: '1.0',
exported_at: new Date().toISOString(),
items: itemsToExport,
total_items: itemsToExport.length
};
const dataStr = JSON.stringify(exportData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(dataBlob);
link.download = `queue-export-${new Date().toISOString().split('T')[0]}.json`;
link.click();
this.showStatusIndicator(`Exported ${itemsToExport.length} items successfully`, 'success');
this.hideModal();
} catch (error) {
console.error('Export failed:', error);
this.showStatusIndicator('Export failed: ' + error.message, 'error');
}
}
importQueue() {
const fileInput = document.getElementById('import-file');
const mergeImport = document.getElementById('merge-import').checked;
if (!fileInput.files.length) {
this.showStatusIndicator('Please select a file to import', 'warning');
return;
}
const file = fileInput.files[0];
const reader = new FileReader();
reader.onload = (e) => {
try {
const importData = JSON.parse(e.target.result);
// Validate import data structure
if (!importData.items || !Array.isArray(importData.items)) {
throw new Error('Invalid file format: missing items array');
}
// Process import
let importedCount = 0;
let skippedCount = 0;
importData.items.forEach(item => {
// Check for duplicates if merging
if (mergeImport) {
const exists = this.queueItems.some(existing => existing.id === item.id);
if (exists) {
skippedCount++;
return;
}
}
// Add imported item (in real implementation, this would be an API call)
this.queueItems.push({
...item,
id: mergeImport ? item.id : `imported-${Date.now()}-${importedCount}`,
imported_at: new Date().toISOString()
});
importedCount++;
});
// Update display
this.renderQueueItems();
let message = `Imported ${importedCount} items`;
if (skippedCount > 0) {
message += `, skipped ${skippedCount} duplicates`;
}
this.showStatusIndicator(message, 'success');
this.hideModal();
// Clear file input
fileInput.value = '';
} catch (error) {
console.error('Import failed:', error);
this.showStatusIndicator('Import failed: ' + error.message, 'error');
}
};
reader.onerror = () => {
this.showStatusIndicator('Failed to read file', 'error');
};
reader.readAsText(file);
}
confirmArchive() {
const withResults = document.getElementById('archive-with-results').checked;
const compress = document.getElementById('archive-compress').checked;
if (this.selectedItems.size === 0) {
this.showStatusIndicator('No items selected for archiving', 'warning');
return;
}
try {
// Archive selected items (in real implementation, this would be an API call)
const itemsToArchive = Array.from(this.selectedItems);
itemsToArchive.forEach(itemId => {
const item = this.queueItems.find(i => i.id === itemId);
if (item) {
item.status = 'archived';
item.archived_at = new Date().toISOString();
item.archive_options = {
with_results: withResults,
compressed: compress
};
}
});
// Clear selection and update display
this.selectedItems.clear();
this.renderQueueItems();
this.updateBulkActionButtons();
this.showStatusIndicator(`Archived ${itemsToArchive.length} items`, 'success');
this.hideModal();
} catch (error) {
console.error('Archive failed:', error);
this.showStatusIndicator('Archive failed: ' + error.message, 'error');
}
}
bindKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
// Only handle shortcuts when not in input fields
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
return;
}
switch (e.key) {
case 'r':
case 'R':
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
this.loadQueueItems();
}
break;
case ' ':
e.preventDefault();
this.toggleQueueState();
break;
case 'a':
case 'A':
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
this.selectAllItems();
}
break;
case 'Escape':
this.hideModal();
this.clearSelection();
break;
case 'Delete':
case 'Backspace':
if (this.selectedItems.size > 0) {
e.preventDefault();
this.deleteSelected();
}
break;
}
});
}
selectAllItems() {
const visibleItems = document.querySelectorAll('.queue-item:not([style*="display: none"])');
visibleItems.forEach(item => {
const itemId = item.dataset.id;
const checkbox = item.querySelector('.item-checkbox');
this.selectedItems.add(itemId);
checkbox.checked = true;
});
this.updateBulkActionButtons();
this.showStatusIndicator(`Selected ${visibleItems.length} items`, 'success');
}
clearSelection() {
this.selectedItems.clear();
document.querySelectorAll('.item-checkbox').forEach(checkbox => {
checkbox.checked = false;
});
this.updateBulkActionButtons();
}
// Cleanup method for when the component is destroyed
destroy() {
this.stopRealTimeUpdates();
document.removeEventListener('keydown', this.bindKeyboardShortcuts);
}
}
// Initialize the queue manager when the page loads
let queueManager;
document.addEventListener('DOMContentLoaded', () => {
queueManager = new QueueManager();
});