From d907955707de2fb74362bb38f60f9322ee34e611 Mon Sep 17 00:00:00 2001 From: 5ymb01 <5ymb01@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:05:24 -0500 Subject: [PATCH 1/5] fix(web): resolve file upload config lookup for server-rendered forms The file upload widget's getUploadConfig() function failed to map server-rendered field IDs (e.g., "static-image-images") back to schema property keys ("images"), causing upload config (plugin_id, endpoint, allowed_types) to be lost. This could prevent image uploads from working correctly in the static-image plugin and others. Changes: - Add data-* attributes to the Jinja2 file-upload template so upload config is embedded directly on the file input element - Update getUploadConfig() in both file-upload.js and plugins_manager.js to read config from data attributes first, falling back to schema lookup - Remove duplicate handleFiles/handleFileDrop/handleFileSelect from plugins_manager.js that overwrote the more robust file-upload.js versions - Bump cache-busting version strings so browsers fetch updated JS Co-Authored-By: Claude Opus 4.6 --- .../static/v3/js/widgets/file-upload.js | 41 +++-- web_interface/static/v3/plugins_manager.js | 141 ++++-------------- web_interface/templates/v3/base.html | 4 +- .../templates/v3/partials/plugin_config.html | 12 +- 4 files changed, 70 insertions(+), 128 deletions(-) diff --git a/web_interface/static/v3/js/widgets/file-upload.js b/web_interface/static/v3/js/widgets/file-upload.js index 846dc78a..9120a30f 100644 --- a/web_interface/static/v3/js/widgets/file-upload.js +++ b/web_interface/static/v3/js/widgets/file-upload.js @@ -377,21 +377,40 @@ }; /** - * Get upload configuration from schema + * Get upload configuration for a file upload field. + * Priority: 1) data attributes on the file input element (server-rendered), + * 2) schema lookup via window.currentPluginConfig (client-rendered). * @param {string} fieldId - Field ID * @returns {Object} Upload configuration */ window.getUploadConfig = function(fieldId) { - // Extract config from schema + // Strategy 1: Read from data attributes on the file input element + // This is the most reliable method for server-side rendered forms + const fileInput = document.getElementById(`${fieldId}_file_input`); + if (fileInput && fileInput.dataset.pluginId) { + const config = {}; + if (fileInput.dataset.pluginId) config.plugin_id = fileInput.dataset.pluginId; + if (fileInput.dataset.uploadEndpoint) config.endpoint = fileInput.dataset.uploadEndpoint; + if (fileInput.dataset.fileType) config.file_type = fileInput.dataset.fileType; + if (fileInput.dataset.maxFiles) config.max_files = parseInt(fileInput.dataset.maxFiles, 10); + if (fileInput.dataset.maxSizeMb) config.max_size_mb = parseFloat(fileInput.dataset.maxSizeMb); + if (fileInput.dataset.allowedTypes) { + config.allowed_types = fileInput.dataset.allowedTypes.split(',').map(t => t.trim()); + } + return config; + } + + // Strategy 2: Extract config from schema (client-side rendered forms) const schema = window.currentPluginConfig?.schema; if (!schema || !schema.properties) return {}; - + // Find the property that matches this fieldId - // FieldId is like "image_config_images" for "image_config.images" + // FieldId is like "image_config_images" for "image_config.images" (client-side) + // or "static-image-images" for plugin "static-image", field "images" (server-side) const key = fieldId.replace(/_/g, '.'); const keys = key.split('.'); let prop = schema.properties; - + for (const k of keys) { if (prop && prop[k]) { prop = prop[k]; @@ -404,22 +423,22 @@ } } } - + // If we found an array with x-widget, get its config if (prop && prop.type === 'array' && prop['x-widget'] === 'file-upload') { return prop['x-upload-config'] || {}; } - - // Try to find nested images array - if (schema.properties && schema.properties.image_config && - schema.properties.image_config.properties && + + // Try to find nested images array (legacy fallback) + if (schema.properties && schema.properties.image_config && + schema.properties.image_config.properties && schema.properties.image_config.properties.images) { const imagesProp = schema.properties.image_config.properties.images; if (imagesProp['x-widget'] === 'file-upload') { return imagesProp['x-upload-config'] || {}; } } - + return {}; }; diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js index d122d5fe..941f3af3 100644 --- a/web_interface/static/v3/plugins_manager.js +++ b/web_interface/static/v3/plugins_manager.js @@ -6622,22 +6622,9 @@ window.closeInstructionsModal = function() { } // ==================== File Upload Functions ==================== -// Make these globally accessible for use in base.html - -window.handleFileDrop = function(event, fieldId) { - event.preventDefault(); - const files = event.dataTransfer.files; - if (files.length > 0) { - window.handleFiles(fieldId, Array.from(files)); - } -} - -window.handleFileSelect = function(event, fieldId) { - const files = event.target.files; - if (files.length > 0) { - window.handleFiles(fieldId, Array.from(files)); - } -} +// Note: handleFileDrop, handleFileSelect, and handleFiles are defined in +// file-upload.js widget which loads first. We only define supplementary +// functions here that file-upload.js doesn't provide. window.handleCredentialsUpload = async function(event, fieldId, uploadEndpoint, targetFilename) { const file = event.target.files[0]; @@ -6706,91 +6693,7 @@ window.handleCredentialsUpload = async function(event, fieldId, uploadEndpoint, } } -window.handleFiles = async function(fieldId, files) { - const uploadConfig = window.getUploadConfig(fieldId); - const pluginId = uploadConfig.plugin_id || window.currentPluginConfig?.pluginId || 'static-image'; - const maxFiles = uploadConfig.max_files || 10; - const maxSizeMB = uploadConfig.max_size_mb || 5; - const fileType = uploadConfig.file_type || 'image'; - const customUploadEndpoint = uploadConfig.endpoint || '/api/v3/plugins/assets/upload'; - - // Get current files list (works for both images and JSON) - const currentFiles = window.getCurrentImages ? window.getCurrentImages(fieldId) : []; - if (currentFiles.length + files.length > maxFiles) { - showNotification(`Maximum ${maxFiles} files allowed. You have ${currentFiles.length} and tried to add ${files.length}.`, 'error'); - return; - } - - // Validate file types and sizes - const validFiles = []; - for (const file of files) { - if (file.size > maxSizeMB * 1024 * 1024) { - showNotification(`File ${file.name} exceeds ${maxSizeMB}MB limit`, 'error'); - continue; - } - - if (fileType === 'json') { - // Validate JSON files - if (!file.name.toLowerCase().endsWith('.json')) { - showNotification(`File ${file.name} must be a JSON file (.json)`, 'error'); - continue; - } - } else { - // Validate image files - const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/bmp', 'image/gif']; - if (!allowedTypes.includes(file.type)) { - showNotification(`File ${file.name} is not a valid image type`, 'error'); - continue; - } - } - - validFiles.push(file); - } - - if (validFiles.length === 0) { - return; - } - - // Show upload progress - window.showUploadProgress(fieldId, validFiles.length); - - // Upload files - const formData = new FormData(); - if (fileType !== 'json') { - formData.append('plugin_id', pluginId); - } - validFiles.forEach(file => formData.append('files', file)); - - try { - const response = await fetch(customUploadEndpoint, { - method: 'POST', - body: formData - }); - - const data = await response.json(); - - if (data.status === 'success') { - // Add uploaded files to current list - const currentFiles = window.getCurrentImages ? window.getCurrentImages(fieldId) : []; - const newFiles = [...currentFiles, ...data.uploaded_files]; - window.updateImageList(fieldId, newFiles); - - showNotification(`Successfully uploaded ${data.uploaded_files.length} ${fileType === 'json' ? 'file(s)' : 'image(s)'}`, 'success'); - } else { - showNotification(`Upload failed: ${data.message}`, 'error'); - } - } catch (error) { - console.error('Upload error:', error); - showNotification(`Upload error: ${error.message}`, 'error'); - } finally { - window.hideUploadProgress(fieldId); - // Clear file input - const fileInput = document.getElementById(`${fieldId}_file_input`); - if (fileInput) { - fileInput.value = ''; - } - } -} +// handleFiles is now defined exclusively in file-upload.js widget window.deleteUploadedImage = async function(fieldId, imageId, pluginId) { return window.deleteUploadedFile(fieldId, imageId, pluginId, 'image', null); @@ -6833,16 +6736,30 @@ window.deleteUploadedFile = async function(fieldId, fileId, pluginId, fileType, } window.getUploadConfig = function(fieldId) { - // Extract config from schema + // Strategy 1: Read from data attributes on the file input element + // This is the most reliable method for server-side rendered forms + const fileInput = document.getElementById(`${fieldId}_file_input`); + if (fileInput && fileInput.dataset.pluginId) { + const config = {}; + if (fileInput.dataset.pluginId) config.plugin_id = fileInput.dataset.pluginId; + if (fileInput.dataset.uploadEndpoint) config.endpoint = fileInput.dataset.uploadEndpoint; + if (fileInput.dataset.fileType) config.file_type = fileInput.dataset.fileType; + if (fileInput.dataset.maxFiles) config.max_files = parseInt(fileInput.dataset.maxFiles, 10); + if (fileInput.dataset.maxSizeMb) config.max_size_mb = parseFloat(fileInput.dataset.maxSizeMb); + if (fileInput.dataset.allowedTypes) { + config.allowed_types = fileInput.dataset.allowedTypes.split(',').map(t => t.trim()); + } + return config; + } + + // Strategy 2: Extract config from schema (client-side rendered forms) const schema = window.currentPluginConfig?.schema; if (!schema || !schema.properties) return {}; - - // Find the property that matches this fieldId - // FieldId is like "image_config_images" for "image_config.images" + const key = fieldId.replace(/_/g, '.'); const keys = key.split('.'); let prop = schema.properties; - + for (const k of keys) { if (prop && prop[k]) { prop = prop[k]; @@ -6855,22 +6772,22 @@ window.getUploadConfig = function(fieldId) { } } } - + // If we found an array with x-widget, get its config if (prop && prop.type === 'array' && prop['x-widget'] === 'file-upload') { return prop['x-upload-config'] || {}; } - - // Try to find nested images array - if (schema.properties && schema.properties.image_config && - schema.properties.image_config.properties && + + // Try to find nested images array (legacy fallback) + if (schema.properties && schema.properties.image_config && + schema.properties.image_config.properties && schema.properties.image_config.properties.images) { const imagesProp = schema.properties.image_config.properties.images; if (imagesProp['x-widget'] === 'file-upload') { return imagesProp['x-upload-config'] || {}; } } - + return {}; } diff --git a/web_interface/templates/v3/base.html b/web_interface/templates/v3/base.html index 8e161a86..c1a5139e 100644 --- a/web_interface/templates/v3/base.html +++ b/web_interface/templates/v3/base.html @@ -4988,7 +4988,7 @@

- + @@ -5014,7 +5014,7 @@

- +