// 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 = `
`;
}
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-label-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;