diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js index 59ccb49f..b8c55568 100644 --- a/web_interface/static/v3/plugins_manager.js +++ b/web_interface/static/v3/plugins_manager.js @@ -2222,15 +2222,16 @@ function handlePluginConfigSubmit(e) { // Process form data with type conversion (using dot notation for nested fields) for (const [key, value] of formData.entries()) { - // Check if this is a patternProperties hidden input (contains JSON data) + // Check if this is a patternProperties or array-of-objects hidden input (contains JSON data) if (key.endsWith('_data') || key.includes('_data')) { try { const baseKey = key.replace(/_data$/, ''); const jsonValue = JSON.parse(value); - if (typeof jsonValue === 'object' && !Array.isArray(jsonValue)) { + // Handle both objects (patternProperties) and arrays (array-of-objects) + if (typeof jsonValue === 'object') { flatConfig[baseKey] = jsonValue; - console.log(`PatternProperties field ${baseKey}: parsed JSON object`, jsonValue); - continue; // Skip normal processing for patternProperties + console.log(`JSON data field ${baseKey}: parsed ${Array.isArray(jsonValue) ? 'array' : 'object'}`, jsonValue); + continue; // Skip normal processing for JSON data fields } } catch (e) { // Not valid JSON, continue with normal processing @@ -2464,6 +2465,113 @@ function flattenConfig(obj, prefix = '') { } // Generate field HTML for a single property (used recursively) +// Helper function to render a single item in an array of objects +function renderArrayObjectItem(fieldId, fullKey, itemProperties, itemValue, index, itemsSchema) { + const item = itemValue || {}; + const itemId = `${fieldId}_item_${index}`; + let html = `
`; + + // Render each property of the object + const propertyOrder = itemsSchema['x-propertyOrder'] || Object.keys(itemProperties); + propertyOrder.forEach(propKey => { + if (!itemProperties[propKey]) return; + + const propSchema = itemProperties[propKey]; + const propValue = item[propKey] !== undefined ? item[propKey] : propSchema.default; + const propLabel = propSchema.title || propKey.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); + const propDescription = propSchema.description || ''; + const propFullKey = `${fullKey}[${index}].${propKey}`; + + html += `
`; + + // Handle file-upload widget (for logo field) + if (propSchema['x-widget'] === 'file-upload') { + html += ``; + if (propDescription) { + html += `

${escapeHtml(propDescription)}

`; + } + const uploadConfig = propSchema['x-upload-config'] || {}; + const pluginId = uploadConfig.plugin_id || (typeof currentPluginConfig !== 'undefined' ? currentPluginConfig?.pluginId : null) || (typeof window.currentPluginConfig !== 'undefined' ? window.currentPluginConfig?.pluginId : null) || 'ledmatrix-news'; + const logoValue = propValue || {}; + + html += ` +
+ + + `; + + if (logoValue.path) { + html += ` +
+ Logo + +
+ `; + } + + html += `
`; + } else if (propSchema.type === 'boolean') { + // Boolean checkbox + html += ` + + `; + } else { + // Regular text/string input + html += ` + + `; + if (propDescription) { + html += `

${escapeHtml(propDescription)}

`; + } + html += ` + + `; + } + + html += `
`; + }); + + html += ` + +
`; + + return html; +} + function generateFieldHtml(key, prop, value, prefix = '') { const fullKey = prefix ? `${prefix}.${key}` : key; const label = prop.title || key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); @@ -2907,6 +3015,36 @@ function generateFieldHtml(key, prop, value, prefix = '') { `; }); html += ``; + } else if (prop.items && prop.items.type === 'object' && prop.items.properties) { + // Array of objects widget (like custom_feeds with name, url, enabled, logo) + console.log(`[DEBUG] ✅ Detected array-of-objects widget for ${fullKey}`); + const fieldId = fullKey.replace(/\./g, '_'); + const itemsSchema = prop.items; + const itemProperties = itemsSchema.properties || {}; + const maxItems = prop.maxItems || 50; + const currentItems = Array.isArray(value) ? value : []; + + html += ` +
+
+ `; + + // Render existing items + currentItems.forEach((item, index) => { + html += renderArrayObjectItem(fieldId, fullKey, itemProperties, item, index, itemsSchema); + }); + + html += ` +
+ + +
+ `; } else { // Regular array input console.log(`[DEBUG] ❌ NOT a file upload widget for ${fullKey}, using regular array input`); @@ -3296,6 +3434,153 @@ window.updateKeyValuePairData = function(fieldId, fullKey) { hiddenInput.value = JSON.stringify(pairs); }; +// Functions to handle array-of-objects +window.addArrayObjectItem = function(fieldId, fullKey, maxItems) { + const itemsContainer = document.getElementById(fieldId + '_items'); + const hiddenInput = document.getElementById(fieldId + '_data'); + if (!itemsContainer || !hiddenInput) return; + + const currentItems = itemsContainer.querySelectorAll('.array-object-item'); + if (currentItems.length >= maxItems) { + alert(`Maximum ${maxItems} items allowed`); + return; + } + + // Get schema for item properties from the hidden input's data attribute or currentPluginConfig + const schema = (typeof currentPluginConfig !== 'undefined' && currentPluginConfig?.schema) || (typeof window.currentPluginConfig !== 'undefined' && window.currentPluginConfig?.schema); + if (!schema) return; + + // Navigate to the items schema + const keys = fullKey.split('.'); + let itemsSchema = schema.properties; + for (const key of keys) { + if (itemsSchema && itemsSchema[key]) { + itemsSchema = itemsSchema[key]; + if (itemsSchema.type === 'array' && itemsSchema.items) { + itemsSchema = itemsSchema.items; + break; + } + } + } + + if (!itemsSchema || !itemsSchema.properties) return; + + const newIndex = currentItems.length; + const itemHtml = renderArrayObjectItem(fieldId, fullKey, itemsSchema.properties, {}, newIndex, itemsSchema); + itemsContainer.insertAdjacentHTML('beforeend', itemHtml); + updateArrayObjectData(fieldId); + + // Update add button state + const addButton = itemsContainer.nextElementSibling; + if (addButton && currentItems.length + 1 >= maxItems) { + addButton.disabled = true; + addButton.style.opacity = '0.5'; + addButton.style.cursor = 'not-allowed'; + } +}; + +window.removeArrayObjectItem = function(fieldId, index) { + const itemsContainer = document.getElementById(fieldId + '_items'); + if (!itemsContainer) return; + + const item = itemsContainer.querySelector(`.array-object-item[data-index="${index}"]`); + if (item) { + item.remove(); + // Re-index remaining items + const remainingItems = itemsContainer.querySelectorAll('.array-object-item'); + remainingItems.forEach((itemEl, newIndex) => { + itemEl.setAttribute('data-index', newIndex); + // Update all inputs within this item - need to update name/id attributes + itemEl.querySelectorAll('input, select, textarea').forEach(input => { + const name = input.getAttribute('name') || input.id; + if (name) { + // Update name/id attribute with new index + const newName = name.replace(/\[\d+\]/, `[${newIndex}]`); + if (input.getAttribute('name')) input.setAttribute('name', newName); + if (input.id) input.id = input.id.replace(/\d+/, newIndex); + } + }); + // Update button onclick attributes + itemEl.querySelectorAll('button[onclick]').forEach(button => { + const onclick = button.getAttribute('onclick'); + if (onclick) { + button.setAttribute('onclick', onclick.replace(/\d+/, newIndex)); + } + }); + }); + updateArrayObjectData(fieldId); + + // Update add button state + const addButton = itemsContainer.nextElementSibling; + if (addButton) { + const maxItems = parseInt(addButton.getAttribute('onclick').match(/\d+/)[0]); + if (remainingItems.length < maxItems) { + addButton.disabled = false; + addButton.style.opacity = '1'; + addButton.style.cursor = 'pointer'; + } + } + } +}; + +window.updateArrayObjectData = function(fieldId) { + const itemsContainer = document.getElementById(fieldId + '_items'); + const hiddenInput = document.getElementById(fieldId + '_data'); + if (!itemsContainer || !hiddenInput) return; + + const items = []; + const itemElements = itemsContainer.querySelectorAll('.array-object-item'); + + itemElements.forEach((itemEl, index) => { + const item = {}; + // Get all text inputs in this item + itemEl.querySelectorAll('input[type="text"], input[type="url"], input[type="number"]').forEach(input => { + const propKey = input.getAttribute('data-prop-key'); + if (propKey && propKey !== 'logo_file') { + item[propKey] = input.value.trim(); + } + }); + // Handle checkboxes + itemEl.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { + const propKey = checkbox.getAttribute('data-prop-key'); + if (propKey) { + item[propKey] = checkbox.checked; + } + }); + // Handle file upload data (stored in data attributes) + itemEl.querySelectorAll('[data-file-data]').forEach(fileEl => { + const fileData = fileEl.getAttribute('data-file-data'); + if (fileData) { + try { + const data = JSON.parse(fileData); + const propKey = fileEl.getAttribute('data-prop-key'); + if (propKey) { + item[propKey] = data; + } + } catch (e) { + console.error('Error parsing file data:', e); + } + } + }); + items.push(item); + }); + + hiddenInput.value = JSON.stringify(items); +}; + +window.handleArrayObjectFileUpload = function(event, fieldId, itemIndex, propKey, pluginId) { + // TODO: Implement file upload handling for array object items + // This is a placeholder - file upload in nested objects needs special handling + console.log('File upload for array object item:', { fieldId, itemIndex, propKey, pluginId }); + updateArrayObjectData(fieldId); +}; + +window.removeArrayObjectFile = function(fieldId, itemIndex, propKey) { + // TODO: Implement file removal for array object items + console.log('File removal for array object item:', { fieldId, itemIndex, propKey }); + updateArrayObjectData(fieldId); +}; + // Function to toggle nested sections window.toggleNestedSection = function(sectionId, event) { // Prevent event bubbling if event is provided