// Firmware Form Component for add/edit operations in drawer class FirmwareFormComponent extends Component { constructor(container, viewModel, eventBus) { super(container, viewModel, eventBus); this.firmwareData = null; this.onSaveCallback = null; this.onCancelCallback = null; this.isEditMode = false; } setFirmwareData(firmwareData) { this.firmwareData = firmwareData; this.isEditMode = !!firmwareData; } setOnSaveCallback(callback) { this.onSaveCallback = callback; } setOnCancelCallback(callback) { this.onCancelCallback = callback; } setupEventListeners() { // Submit button const submitBtn = this.findElement('button[type="submit"]'); if (submitBtn) { this.addEventListener(submitBtn, 'click', this.handleSubmit.bind(this)); } // Cancel button const cancelBtn = this.findElement('#cancel-btn'); if (cancelBtn) { this.addEventListener(cancelBtn, 'click', this.handleCancel.bind(this)); } // File input const fileInput = this.findElement('#firmware-file'); if (fileInput) { this.addEventListener(fileInput, 'change', this.handleFileSelect.bind(this)); } // Labels management this.setupLabelsManagement(); } setupLabelsManagement() { // Add label button const addLabelBtn = this.findElement('#add-label-btn'); if (addLabelBtn) { this.addEventListener(addLabelBtn, 'click', this.addLabel.bind(this)); } // Remove label buttons (delegated event handling) const labelsContainer = this.findElement('#labels-container'); if (labelsContainer) { this.addEventListener(labelsContainer, 'click', (e) => { const removeBtn = e.target.closest('.remove-label-btn'); if (removeBtn) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); const key = removeBtn.getAttribute('data-label-key'); if (key) { this.removeLabel(key); } } }); } } mount() { super.mount(); this.render(); this.setupEventListeners(); } render() { const container = this.container; if (!container) return; const labels = this.firmwareData?.labels || {}; const labelsHTML = Object.entries(labels).map(([key, value]) => `
${this.escapeHtml(key)} = ${this.escapeHtml(value)}
` ).join(''); container.innerHTML = `
${this.isEditMode ? 'Name cannot be changed after creation' : 'Unique identifier for the firmware'}
${this.isEditMode ? 'Version cannot be changed after creation' : 'Semantic version (e.g., 1.0.0)'}
${this.isEditMode ? 'Select a new firmware file to update, or leave empty to update metadata only' : 'Binary firmware file (.bin or .hex)'}
=
${labelsHTML}
Key-value pairs for categorizing firmware (e.g., platform: esp32, app: base)
`; } handleFileSelect(event) { const file = event.target.files[0]; const fileNameSpan = this.findElement('#file-name'); if (file) { fileNameSpan.textContent = file.name; } else { fileNameSpan.textContent = this.isEditMode ? 'Current file: ' + (this.firmwareData?.name || 'unknown') + '.bin' : 'Choose firmware file...'; } } addLabel() { const keyInput = this.findElement('#label-key'); const valueInput = this.findElement('#label-value'); const labelsContainer = this.findElement('#labels-container'); const key = keyInput.value.trim(); const value = valueInput.value.trim(); if (!key || !value) { this.showError('Please enter both key and value for the label'); return; } // Check if key already exists const existingLabel = labelsContainer.querySelector(`[data-key="${this.escapeHtml(key)}"]`); if (existingLabel) { this.showError('A label with this key already exists'); return; } // Add the label const labelHTML = `
${this.escapeHtml(key)} = ${this.escapeHtml(value)}
`; labelsContainer.insertAdjacentHTML('beforeend', labelHTML); // Clear inputs keyInput.value = ''; valueInput.value = ''; } removeLabel(key) { const removeBtn = this.findElement(`.remove-label-btn[data-label-key="${this.escapeHtml(key)}"]`); if (removeBtn) { const labelItem = removeBtn.closest('.label-item'); if (labelItem) { labelItem.remove(); } } } async handleSubmit(event) { event.preventDefault(); try { const nameInput = this.findElement('#firmware-name'); const versionInput = this.findElement('#firmware-version'); const firmwareFile = this.findElement('#firmware-file').files[0]; const name = nameInput.value.trim(); const version = versionInput.value.trim(); if (!name || !version) { this.showError('Name and version are required'); return; } // Only require file for new uploads, not for edit mode when keeping existing file if (!this.isEditMode && (!firmwareFile || firmwareFile.size === 0)) { this.showError('Please select a firmware file'); return; } // Collect labels const labels = {}; const labelItems = this.findAllElements('.label-item'); labelItems.forEach(item => { const key = item.querySelector('.label-key').textContent; const value = item.querySelector('.label-value').textContent; labels[key] = value; }); // Prepare metadata const metadata = { name, version, labels }; // Handle upload vs metadata-only update if (this.isEditMode && (!firmwareFile || firmwareFile.size === 0)) { // Metadata-only update await window.apiClient.updateFirmwareMetadata(name, version, metadata); } else { // Full upload (new firmware or edit with new file) await window.apiClient.uploadFirmwareToRegistry(metadata, firmwareFile); } this.showSuccess(this.isEditMode ? 'Firmware updated successfully' : 'Firmware uploaded successfully'); if (this.onSaveCallback) { this.onSaveCallback(); } } catch (error) { logger.error('Firmware upload failed:', error); this.showError('Upload failed: ' + error.message); } } handleCancel() { if (this.onCancelCallback) { this.onCancelCallback(); } } showError(message) { this.showNotification(message, 'error'); } showSuccess(message) { this.showNotification(message, 'success'); } showNotification(message, type = 'info') { // Remove any existing notifications const existing = this.findElement('.form-notification'); if (existing) { existing.remove(); } const notification = document.createElement('div'); notification.className = `form-notification notification-${type}`; notification.textContent = message; this.container.insertBefore(notification, this.container.firstChild); setTimeout(() => { notification.classList.add('show'); }, 100); setTimeout(() => { notification.classList.remove('show'); setTimeout(() => { if (notification.parentNode) { notification.remove(); } }, 300); }, 3000); } escapeHtml(text) { if (typeof text !== 'string') return text; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } window.FirmwareFormComponent = FirmwareFormComponent;