${currentFiles.map((file, idx) => {
const fileId = file.id || file.category_name || idx;
const fileName = file.original_filename || file.filename || (fileType === 'json' ? 'JSON File' : 'Image');
const entryCount = file.entry_count ? `${file.entry_count} entries` : '';
-
+
return `
@@ -3280,8 +3280,8 @@ function generateFieldHtml(key, prop, value, prefix = '') {
` : `
-
@@ -3301,14 +3301,14 @@ function generateFieldHtml(key, prop, value, prefix = '') {
${fileType === 'image' ? `
-
` : ''}
-
-
+
`;
enumItems.forEach((option) => {
const isChecked = arrayValue.includes(option);
@@ -3347,13 +3347,13 @@ function generateFieldHtml(key, prop, value, prefix = '') {
const checkboxId = `${fieldId}_${escapeHtml(option)}`;
html += `
`;
-
+
return html;
}
@@ -3518,19 +3518,19 @@ async function loadCustomHtmlWidget(fieldId, pluginId, htmlFile) {
console.warn(`[Custom HTML Widget] Container not found: ${fieldId}_custom_html`);
return;
}
-
+
// Fetch HTML from plugin static files endpoint
const response = await fetch(`/api/v3/plugins/${pluginId}/static/${htmlFile}`);
-
+
if (!response.ok) {
throw new Error(`Failed to load custom HTML: ${response.statusText}`);
}
-
+
const html = await response.text();
-
+
// Inject HTML into container
container.innerHTML = html;
-
+
// Execute any script tags in the loaded HTML
const scripts = container.querySelectorAll('script');
scripts.forEach(oldScript => {
@@ -3541,7 +3541,7 @@ async function loadCustomHtmlWidget(fieldId, pluginId, htmlFile) {
newScript.appendChild(document.createTextNode(oldScript.innerHTML));
oldScript.parentNode.replaceChild(newScript, oldScript);
});
-
+
console.log(`[Custom HTML Widget] Loaded ${htmlFile} for plugin ${pluginId}`);
} catch (error) {
console.error(`[Custom HTML Widget] Error loading ${htmlFile} for plugin ${pluginId}:`, error);
@@ -3570,7 +3570,7 @@ function generateFormFromSchema(schema, config, webUiActions = []) {
const order = schema['x-propertyOrder'];
const orderedEntries = [];
const unorderedEntries = [];
-
+
// Separate ordered and unordered properties
propertyEntries.forEach(([key, prop]) => {
const index = order.indexOf(key);
@@ -3580,20 +3580,20 @@ function generateFormFromSchema(schema, config, webUiActions = []) {
unorderedEntries.push([key, prop]);
}
});
-
+
// Combine ordered entries (filter out undefined from sparse array) with unordered entries
propertyEntries = orderedEntries.filter(entry => entry !== undefined).concat(unorderedEntries);
}
-
+
propertyEntries.forEach(([key, prop]) => {
// Skip the 'enabled' property - it's managed separately via the header toggle
if (key === 'enabled') return;
let value = config[key] !== undefined ? config[key] : prop.default;
-
+
// Special handling: use uploaded_files from config if available (populated by backend from disk)
// No need to populate from categories here since backend does it
-
+
formHtml += generateFieldHtml(key, prop, value);
});
}
@@ -3606,15 +3606,15 @@ function generateFormFromSchema(schema, config, webUiActions = []) {
Actions
${webUiActions[0].section_description || 'Perform actions for this plugin'}
-
+
`;
-
+
webUiActions.forEach((action, index) => {
const actionId = `action-${action.id}-${index}`;
const statusId = `action-status-${action.id}-${index}`;
const bgColor = action.color || 'blue';
-
+
// Map color names to explicit Tailwind classes to ensure they're included
const colorMap = {
'blue': { bg: 'bg-blue-50', border: 'border-blue-200', text: 'text-blue-900', textLight: 'text-blue-700', btn: 'bg-blue-600 hover:bg-blue-700' },
@@ -3623,9 +3623,9 @@ function generateFormFromSchema(schema, config, webUiActions = []) {
'yellow': { bg: 'bg-yellow-50', border: 'border-yellow-200', text: 'text-yellow-900', textLight: 'text-yellow-700', btn: 'bg-yellow-600 hover:bg-yellow-700' },
'purple': { bg: 'bg-purple-50', border: 'border-purple-200', text: 'text-purple-900', textLight: 'text-purple-700', btn: 'bg-purple-600 hover:bg-purple-700' }
};
-
+
const colors = colorMap[bgColor] || colorMap['blue'];
-
+
formHtml += `
@@ -3635,9 +3635,9 @@ function generateFormFromSchema(schema, config, webUiActions = []) {
${action.description || ''}
-
`;
});
-
+
formHtml += `
@@ -3676,33 +3676,33 @@ function generateFormFromSchema(schema, config, webUiActions = []) {
window.addKeyValuePair = function(fieldId, fullKey, maxProperties) {
const pairsContainer = document.getElementById(fieldId + '_pairs');
if (!pairsContainer) return;
-
+
const currentPairs = pairsContainer.querySelectorAll('.key-value-pair');
if (currentPairs.length >= maxProperties) {
alert(`Maximum ${maxProperties} entries allowed`);
return;
}
-
+
const newIndex = currentPairs.length;
const valueType = 'string'; // Default to string, could be determined from schema
-
+
const pairHtml = `
-
-
-
`;
-
+
pairsContainer.insertAdjacentHTML('beforeend', pairHtml);
updateKeyValuePairData(fieldId, fullKey);
-
+
// Update add button state
const addButton = pairsContainer.nextElementSibling;
if (addButton && currentPairs.length + 1 >= maxProperties) {
@@ -3726,7 +3726,7 @@ window.addKeyValuePair = function(fieldId, fullKey, maxProperties) {
window.removeKeyValuePair = function(fieldId, index) {
const pairsContainer = document.getElementById(fieldId + '_pairs');
if (!pairsContainer) return;
-
+
const pair = pairsContainer.querySelector(`.key-value-pair[data-index="${index}"]`);
if (pair) {
pair.remove();
@@ -3756,7 +3756,7 @@ window.removeKeyValuePair = function(fieldId, index) {
const hiddenName = hiddenInput.getAttribute('name').replace(/_data$/, '');
updateKeyValuePairData(fieldId, hiddenName);
}
-
+
// Update add button state
const addButton = pairsContainer.nextElementSibling;
if (addButton) {
@@ -3774,11 +3774,11 @@ window.updateKeyValuePairData = function(fieldId, fullKey) {
const pairsContainer = document.getElementById(fieldId + '_pairs');
const hiddenInput = document.getElementById(fieldId + '_data');
if (!pairsContainer || !hiddenInput) return;
-
+
const pairs = {};
const keyInputs = pairsContainer.querySelectorAll('[data-key-index]');
const valueInputs = pairsContainer.querySelectorAll('[data-value-index]');
-
+
keyInputs.forEach((keyInput, idx) => {
const key = keyInput.value.trim();
const valueInput = Array.from(valueInputs).find(v => v.getAttribute('data-value-index') === keyInput.getAttribute('data-key-index'));
@@ -3789,7 +3789,7 @@ window.updateKeyValuePairData = function(fieldId, fullKey) {
}
}
});
-
+
hiddenInput.value = JSON.stringify(pairs);
};
@@ -3798,17 +3798,17 @@ 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;
@@ -3821,14 +3821,14 @@ window.addArrayObjectItem = function(fieldId, fullKey, maxItems) {
}
}
}
-
+
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) {
@@ -3841,7 +3841,7 @@ window.addArrayObjectItem = function(fieldId, fullKey, maxItems) {
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();
@@ -3871,7 +3871,7 @@ window.removeArrayObjectItem = function(fieldId, index) {
});
});
updateArrayObjectData(fieldId);
-
+
// Update add button state
const addButton = itemsContainer.nextElementSibling;
if (addButton) {
@@ -3889,7 +3889,7 @@ window.updateArrayObjectData = function(fieldId) {
const itemsContainer = document.getElementById(fieldId + '_items');
const hiddenInput = document.getElementById(fieldId + '_data');
if (!itemsContainer || !hiddenInput) return;
-
+
// Get existing items from hidden input to preserve non-editable properties
let existingItems = [];
try {
@@ -3900,10 +3900,10 @@ window.updateArrayObjectData = function(fieldId) {
} catch (e) {
console.error('Error parsing existing items data:', e);
}
-
+
const items = [];
const itemElements = itemsContainer.querySelectorAll('.array-object-item');
-
+
itemElements.forEach((itemEl, index) => {
// Start with original item data from data attribute to preserve non-editable properties
// This avoids index-based corruption after deletions/reindexing
@@ -3927,21 +3927,21 @@ window.updateArrayObjectData = function(fieldId) {
}
}
const item = Object.assign({}, existingItem); // Copy existing item
-
+
// Get all text inputs in this item and overlay their values with type coercion
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') {
let value = input.value.trim();
-
+
// Type coercion: check input type or data-prop-type attribute
const inputType = input.type;
const propType = input.getAttribute('data-prop-type');
-
+
if (inputType === 'number' || propType === 'number') {
// Use valueAsNumber if available, fallback to Number()
- const numValue = input.valueAsNumber !== undefined && !isNaN(input.valueAsNumber)
- ? input.valueAsNumber
+ const numValue = input.valueAsNumber !== undefined && !isNaN(input.valueAsNumber)
+ ? input.valueAsNumber
: Number(value);
item[propKey] = isNaN(numValue) ? value : numValue;
} else if (propType === 'array' || input.getAttribute('data-prop-is-list') === 'true') {
@@ -3984,7 +3984,7 @@ window.updateArrayObjectData = function(fieldId) {
}
});
items.push(item);
-
+
// Update data-item-data attribute with the merged item to keep it in sync
try {
const itemDataJson = JSON.stringify(item);
@@ -3994,28 +3994,28 @@ window.updateArrayObjectData = function(fieldId) {
console.error('Error updating data-item-data attribute:', e);
}
});
-
+
hiddenInput.value = JSON.stringify(items);
};
window.handleArrayObjectFileUpload = async function(event, fieldId, itemIndex, propKey, pluginId) {
const file = event.target.files[0];
if (!file) return;
-
+
// Derive item element from event instead of constructing ID (works after reindexing)
const itemEl = event.target.closest('.array-object-item');
if (!itemEl) {
console.error('Array object item element not found');
return;
}
-
+
// Find file upload container within the item element, scoped to propKey
const fileUploadContainer = itemEl.querySelector(`.file-upload-widget-inline[data-prop-key="${propKey}"]`);
if (!fileUploadContainer) {
console.error('File upload container not found for propKey:', propKey);
return;
}
-
+
// Get upload config from data attribute
let uploadConfig = { allowed_types: ['image/png', 'image/jpeg', 'image/jpg', 'image/bmp'], max_size_mb: 5 };
const uploadConfigBase64 = fileUploadContainer.getAttribute('data-upload-config');
@@ -4027,7 +4027,7 @@ window.handleArrayObjectFileUpload = async function(event, fieldId, itemIndex, p
console.error('Error parsing upload config from data attribute:', e);
}
}
-
+
// Validate file type using uploadConfig
const allowedTypes = uploadConfig.allowed_types || ['image/png', 'image/jpeg', 'image/jpg', 'image/bmp'];
if (!allowedTypes.includes(file.type)) {
@@ -4036,7 +4036,7 @@ window.handleArrayObjectFileUpload = async function(event, fieldId, itemIndex, p
}
return;
}
-
+
// Validate file size using uploadConfig
const maxSizeMB = uploadConfig.max_size_mb || 5;
if (file.size > maxSizeMB * 1024 * 1024) {
@@ -4045,7 +4045,7 @@ window.handleArrayObjectFileUpload = async function(event, fieldId, itemIndex, p
}
return;
}
-
+
// Validate pluginId before upload (fail fast)
if (!pluginId || pluginId === 'null' || pluginId === 'undefined' || (typeof pluginId === 'string' && pluginId.trim() === '')) {
if (typeof showNotification === 'function') {
@@ -4054,18 +4054,18 @@ window.handleArrayObjectFileUpload = async function(event, fieldId, itemIndex, p
console.error('File upload failed: pluginId is required');
return;
}
-
+
// Upload file
const formData = new FormData();
formData.append('plugin_id', pluginId);
formData.append('files', file);
-
+
try {
const response = await fetch('/api/v3/plugins/assets/upload', {
method: 'POST',
body: formData
});
-
+
// Check response.ok before parsing JSON to avoid parsing errors on HTTP errors
if (!response.ok) {
const errorText = await response.text();
@@ -4084,24 +4084,24 @@ window.handleArrayObjectFileUpload = async function(event, fieldId, itemIndex, p
}
return;
}
-
+
const data = await response.json();
-
+
if (data.status === 'success' && data.uploaded_files && data.uploaded_files.length > 0) {
const uploadedFile = data.uploaded_files[0];
-
+
// Store file data in data-file-data attribute on the container (base64-encoded)
const fileDataJson = JSON.stringify(uploadedFile);
const fileDataBase64 = btoa(unescape(encodeURIComponent(fileDataJson)));
fileUploadContainer.setAttribute('data-file-data', fileDataBase64);
fileUploadContainer.setAttribute('data-prop-key', propKey);
-
+
// Update the display to show the uploaded image
const existingImage = fileUploadContainer.querySelector('.uploaded-image-container');
if (existingImage) {
existingImage.remove();
}
-
+
const imageContainer = document.createElement('div');
imageContainer.className = 'mt-2 flex items-center space-x-2 uploaded-image-container';
const escapedPath = escapeAttribute(uploadedFile.path.replace(/^\/+/, ''));
@@ -4111,17 +4111,17 @@ window.handleArrayObjectFileUpload = async function(event, fieldId, itemIndex, p
const currentItemIndex = itemEl.getAttribute('data-index') || itemIndex;
imageContainer.innerHTML = `

-
`;
fileUploadContainer.appendChild(imageContainer);
-
+
// Update the hidden input with the new file data
updateArrayObjectData(fieldId);
-
+
if (typeof showNotification === 'function') {
showNotification('Logo uploaded successfully', 'success');
}
@@ -4136,7 +4136,7 @@ window.handleArrayObjectFileUpload = async function(event, fieldId, itemIndex, p
showNotification(`Upload error: ${error.message}`, 'error');
}
}
-
+
// Clear file input
event.target.value = '';
};
@@ -4148,19 +4148,19 @@ window.removeArrayObjectFile = function(fieldId, itemIndex, propKey) {
console.error('File upload container not found');
return;
}
-
+
// Remove file data from data attribute
fileUploadContainer.removeAttribute('data-file-data');
-
+
// Remove the image display
const imageContainer = fileUploadContainer.querySelector('.uploaded-image-container');
if (imageContainer) {
imageContainer.remove();
}
-
+
// Update the hidden input to remove the file data
updateArrayObjectData(fieldId);
-
+
if (typeof showNotification === 'function') {
showNotification('Logo removed', 'success');
}
@@ -4173,43 +4173,43 @@ window.toggleNestedSection = function(sectionId, event) {
event.stopPropagation();
event.preventDefault();
}
-
+
const content = document.getElementById(sectionId);
const icon = document.getElementById(sectionId + '-icon');
-
+
if (!content || !icon) return;
-
+
// Prevent multiple simultaneous toggles
if (content.dataset.toggling === 'true') {
return;
}
-
+
// Mark as toggling
content.dataset.toggling = 'true';
-
+
// Check current state before making changes
const hasCollapsed = content.classList.contains('collapsed');
const hasExpanded = content.classList.contains('expanded');
const displayStyle = content.style.display;
const computedDisplay = window.getComputedStyle(content).display;
-
+
// Check if content is currently collapsed - prioritize class over display style
const isCollapsed = hasCollapsed || (!hasExpanded && (displayStyle === 'none' || computedDisplay === 'none'));
-
+
if (isCollapsed) {
// Expand the section
content.classList.remove('collapsed');
content.classList.add('expanded');
content.style.display = 'block';
content.style.overflow = 'hidden'; // Prevent content jumping during animation
-
+
// CRITICAL FIX: Use setTimeout to ensure browser has time to layout the element
// When element goes from display:none to display:block, scrollHeight might be 0
// We need to wait for the browser to calculate the layout
setTimeout(() => {
// Force reflow to ensure transition works
void content.offsetHeight;
-
+
// Now measure the actual content height after layout
const scrollHeight = content.scrollHeight;
if (scrollHeight > 0) {
@@ -4222,16 +4222,16 @@ window.toggleNestedSection = function(sectionId, event) {
}, 10);
}
}, 10);
-
+
icon.classList.remove('fa-chevron-right');
icon.classList.add('fa-chevron-down');
-
+
// Allow parent section to show overflow when expanded
const sectionElement = content.closest('.nested-section');
if (sectionElement) {
sectionElement.style.overflow = 'visible';
}
-
+
// After animation completes, remove max-height constraint to allow natural expansion
// This allows parent sections to automatically expand
setTimeout(() => {
@@ -4243,7 +4243,7 @@ window.toggleNestedSection = function(sectionId, event) {
// Clear toggling flag
content.dataset.toggling = 'false';
}, 320); // Slightly longer than transition duration
-
+
// Scroll the expanded content into view after a short delay to allow animation
setTimeout(() => {
if (sectionElement) {
@@ -4266,25 +4266,25 @@ window.toggleNestedSection = function(sectionId, event) {
content.classList.add('collapsed');
content.classList.remove('expanded');
content.style.overflow = 'hidden'; // Prevent content jumping during animation
-
+
// Set max-height to current scroll height first (required for smooth animation)
const currentHeight = content.scrollHeight;
content.style.maxHeight = currentHeight + 'px';
-
+
// Force reflow to apply the height
void content.offsetHeight;
-
+
// Then animate to 0
setTimeout(() => {
content.style.maxHeight = '0';
}, 10);
-
+
// Restore parent section overflow when collapsed
const sectionElement = content.closest('.nested-section');
if (sectionElement) {
sectionElement.style.overflow = 'hidden';
}
-
+
// Use setTimeout to set display:none after transition completes
setTimeout(() => {
if (content.classList.contains('collapsed')) {
@@ -4309,7 +4309,7 @@ function generateSimpleConfigForm(config, webUiActions = []) {
Actions
`;
-
+
// Map color names to explicit Tailwind classes
const colorMap = {
'blue': { bg: 'bg-blue-50', border: 'border-blue-200', text: 'text-blue-900', textLight: 'text-blue-700', btn: 'bg-blue-600 hover:bg-blue-700' },
@@ -4318,13 +4318,13 @@ function generateSimpleConfigForm(config, webUiActions = []) {
'yellow': { bg: 'bg-yellow-50', border: 'border-yellow-200', text: 'text-yellow-900', textLight: 'text-yellow-700', btn: 'bg-yellow-600 hover:bg-yellow-700' },
'purple': { bg: 'bg-purple-50', border: 'border-purple-200', text: 'text-purple-900', textLight: 'text-purple-700', btn: 'bg-purple-600 hover:bg-purple-700' }
};
-
+
webUiActions.forEach((action, index) => {
const actionId = `action-${action.id}-${index}`;
const statusId = `action-status-${action.id}-${index}`;
const bgColor = action.color || 'blue';
const colors = colorMap[bgColor] || colorMap['blue'];
-
+
actionsHtml += `
@@ -4334,9 +4334,9 @@ function generateSimpleConfigForm(config, webUiActions = []) {
${action.description || ''}
-
`;
}
-
+
return `