';
- }
-
- // Only log once per plugin to avoid spam (Alpine.js may call this multiple times during rendering)
- if (!this._configFormLogged || this._configFormLogged !== pluginId) {
- debugLog('[DEBUG] generateConfigForm called for', pluginId, 'with', webUiActions?.length || 0, 'actions');
- // Debug: Check if image_config.images has x-widget in schema
- if (schema && schema.properties && schema.properties.image_config) {
- const imgConfig = schema.properties.image_config;
- if (imgConfig.properties && imgConfig.properties.images) {
- const imagesProp = imgConfig.properties.images;
- debugLog('[DEBUG] Schema check - image_config.images:', {
- type: imagesProp.type,
- 'x-widget': imagesProp['x-widget'],
- 'has x-widget': 'x-widget' in imagesProp,
- keys: Object.keys(imagesProp)
- });
- }
- }
- this._configFormLogged = pluginId;
- }
- if (!schema || !schema.properties) {
- return this.generateSimpleConfigForm(config, webUiActions, pluginId);
- }
-
- // Helper function to get schema property by full key path
- const getSchemaProperty = (schemaObj, keyPath) => {
- if (!schemaObj || !schemaObj.properties) return null;
- const keys = keyPath.split('.');
- let current = schemaObj.properties;
- for (let i = 0; i < keys.length; i++) {
- const k = keys[i];
- if (!current || !current[k]) {
- return null;
- }
-
- const prop = current[k];
- // If this is the last key, return the property
- if (i === keys.length - 1) {
- return prop;
- }
-
- // If this property has nested properties, navigate deeper
- if (prop && typeof prop === 'object' && prop.properties) {
- current = prop.properties;
- } else {
- // Can't navigate deeper
- return null;
- }
- }
- return null;
- };
-
- const 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());
- const description = prop.description || '';
- let html = '';
-
- // Debug: Log property structure for arrays to help diagnose file-upload widget issues
- if (prop.type === 'array') {
- // Also check schema directly as fallback
- const schemaProp = getSchemaProperty(schema, fullKey);
- const xWidgetFromSchema = schemaProp ? (schemaProp['x-widget'] || schemaProp['x_widget']) : null;
-
- debugLog('[DEBUG generateFieldHtml] Array property:', fullKey, {
- 'prop.x-widget': prop['x-widget'],
- 'prop.x_widget': prop['x_widget'],
- 'schema.x-widget': xWidgetFromSchema,
- 'hasOwnProperty(x-widget)': prop.hasOwnProperty('x-widget'),
- 'x-widget in prop': 'x-widget' in prop,
- 'all prop keys': Object.keys(prop),
- 'schemaProp keys': schemaProp ? Object.keys(schemaProp) : 'null'
- });
- }
-
- // Handle nested objects
- if (prop.type === 'object' && prop.properties) {
- const sectionId = `section-${fullKey.replace(/\./g, '-')}`;
- const nestedConfig = value || {};
- const sectionLabel = prop.title || key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
- // Calculate nesting depth for better spacing
- const nestingDepth = (fullKey.match(/\./g) || []).length;
- const marginClass = nestingDepth > 1 ? 'mb-6' : 'mb-4';
-
- html += `
-
-
-
- `;
-
- // Recursively generate fields for nested properties
- // Get ordered properties if x-propertyOrder is defined
- let nestedPropertyEntries = Object.entries(prop.properties);
- if (prop['x-propertyOrder'] && Array.isArray(prop['x-propertyOrder'])) {
- const order = prop['x-propertyOrder'];
- const orderedEntries = [];
- const unorderedEntries = [];
-
- // Separate ordered and unordered properties
- nestedPropertyEntries.forEach(([nestedKey, nestedProp]) => {
- const index = order.indexOf(nestedKey);
- if (index !== -1) {
- orderedEntries[index] = [nestedKey, nestedProp];
- } else {
- unorderedEntries.push([nestedKey, nestedProp]);
- }
- });
-
- // Combine ordered entries (filter out undefined from sparse array) with unordered entries
- nestedPropertyEntries = orderedEntries.filter(entry => entry !== undefined).concat(unorderedEntries);
- }
-
- nestedPropertyEntries.forEach(([nestedKey, nestedProp]) => {
- // Use config value if it exists and is not null (including false), otherwise use schema default
- // Check if key exists in config and value is not null/undefined
- const hasValue = nestedKey in nestedConfig && nestedConfig[nestedKey] !== null && nestedConfig[nestedKey] !== undefined;
- // For nested objects, if the value is an empty object, still use it (don't fall back to default)
- const isNestedObject = nestedProp.type === 'object' && nestedProp.properties;
- const nestedValue = hasValue ? nestedConfig[nestedKey] :
- (nestedProp.default !== undefined ? nestedProp.default :
- (isNestedObject ? {} : (nestedProp.type === 'array' ? [] : (nestedProp.type === 'boolean' ? false : ''))));
-
- // Debug logging for file-upload widgets
- if (nestedProp.type === 'array' && (nestedProp['x-widget'] === 'file-upload' || nestedProp['x_widget'] === 'file-upload')) {
- debugLog('[DEBUG] Found file-upload widget in nested property:', nestedKey, 'fullKey:', fullKey + '.' + nestedKey, 'prop:', nestedProp);
- }
-
- html += generateFieldHtml(nestedKey, nestedProp, nestedValue, fullKey);
- });
-
- html += `
-
-
- `;
-
- // Add extra spacing after nested sections to prevent overlap with next section
- if (nestingDepth > 0) {
- html += ``;
- }
-
- return html;
- }
-
- // Regular (non-nested) field
- html += `
`;
- html += ``;
-
- if (description) {
- html += `
${description}
`;
- }
-
- // Generate appropriate input based on type
- if (prop.type === 'boolean') {
- html += ``;
- } else if (prop.type === 'number' || prop.type === 'integer' ||
- (Array.isArray(prop.type) && (prop.type.includes('number') || prop.type.includes('integer')))) {
- // Handle union types like ["integer", "null"]
- const isUnionType = Array.isArray(prop.type);
- const allowsNull = isUnionType && prop.type.includes('null');
- const isInteger = prop.type === 'integer' || (isUnionType && prop.type.includes('integer'));
- const isNumber = prop.type === 'number' || (isUnionType && prop.type.includes('number'));
- const min = prop.minimum !== undefined ? `min="${prop.minimum}"` : '';
- const max = prop.maximum !== undefined ? `max="${prop.maximum}"` : '';
- const step = isInteger ? 'step="1"' : 'step="any"';
-
- // For union types with null, don't show default if value is null (leave empty)
- // This allows users to explicitly set null by leaving it empty
- let fieldValue = '';
- if (value !== undefined && value !== null) {
- fieldValue = value;
- } else if (!allowsNull && prop.default !== undefined) {
- // Only use default if null is not allowed
- fieldValue = prop.default;
- }
-
- // Ensure value respects min/max constraints
- if (fieldValue !== '' && fieldValue !== undefined && fieldValue !== null) {
- const numValue = typeof fieldValue === 'string' ? parseFloat(fieldValue) : fieldValue;
- if (!isNaN(numValue)) {
- // Clamp value to min/max if constraints exist
- if (prop.minimum !== undefined && numValue < prop.minimum) {
- fieldValue = prop.minimum;
- } else if (prop.maximum !== undefined && numValue > prop.maximum) {
- fieldValue = prop.maximum;
- } else {
- fieldValue = numValue;
- }
- }
- }
-
- // Add placeholder/help text for null-able fields
- const placeholder = allowsNull ? 'Leave empty to use current time (random)' : '';
- const helpText = allowsNull && description && description.includes('null') ?
- `
${description}
` : '';
-
- html += ``;
- if (helpText) {
- html += helpText;
- }
- } else if (prop.type === 'array') {
- // AGGRESSIVE file upload widget detection
- // For 'images' field in static-image plugin, always check schema directly
- let isFileUpload = false;
- let uploadConfig = {};
-
- // Direct check: if this is the 'images' field and schema has it with x-widget
- if (fullKey === 'images' && schema && schema.properties && schema.properties.images) {
- const imagesSchema = schema.properties.images;
- if (imagesSchema['x-widget'] === 'file-upload' || imagesSchema['x_widget'] === 'file-upload') {
- isFileUpload = true;
- uploadConfig = imagesSchema['x-upload-config'] || imagesSchema['x_upload_config'] || {};
- debugLog('[DEBUG] ✅ Direct detection: images field has file-upload widget', uploadConfig);
- }
- }
-
- // Fallback: check prop object (should have x-widget if schema loaded correctly)
- if (!isFileUpload) {
- const xWidgetFromProp = prop['x-widget'] || prop['x_widget'] || prop.xWidget;
- if (xWidgetFromProp === 'file-upload') {
- isFileUpload = true;
- uploadConfig = prop['x-upload-config'] || prop['x_upload_config'] || {};
- debugLog('[DEBUG] ✅ Detection via prop object');
- }
- }
-
- // Fallback: schema property lookup
- if (!isFileUpload) {
- let schemaProp = getSchemaProperty(schema, fullKey);
- if (!schemaProp && fullKey === 'images' && schema && schema.properties && schema.properties.images) {
- schemaProp = schema.properties.images;
- }
- const xWidgetFromSchema = schemaProp ? (schemaProp['x-widget'] || schemaProp['x_widget']) : null;
- if (xWidgetFromSchema === 'file-upload') {
- isFileUpload = true;
- uploadConfig = schemaProp['x-upload-config'] || schemaProp['x_upload_config'] || {};
- debugLog('[DEBUG] ✅ Detection via schema lookup');
- }
- }
-
- // Debug logging for ALL array fields to diagnose
- debugLog('[DEBUG] Array field check:', fullKey, {
- 'isFileUpload': isFileUpload,
- 'prop keys': Object.keys(prop),
- 'prop.x-widget': prop['x-widget'],
- 'schema.properties.images exists': !!(schema && schema.properties && schema.properties.images),
- 'schema.properties.images.x-widget': (schema && schema.properties && schema.properties.images) ? schema.properties.images['x-widget'] : null,
- 'uploadConfig': uploadConfig
- });
-
- if (isFileUpload) {
- debugLog('[DEBUG] ✅ Rendering file-upload widget for', fullKey, 'with config:', uploadConfig);
- // Use the file upload widget from plugins.html
- // We'll need to call a function that exists in the global scope
- const maxFiles = uploadConfig.max_files || 10;
- const allowedTypes = uploadConfig.allowed_types || ['image/png', 'image/jpeg', 'image/bmp', 'image/gif'];
- const maxSizeMB = uploadConfig.max_size_mb || 5;
-
- const currentImages = Array.isArray(value) ? value : [];
- const fieldId = fullKey.replace(/\./g, '_');
- const safePluginId = (uploadConfig.plugin_id || pluginId || 'static-image').toString().replace(/[^a-zA-Z0-9_-]/g, '_');
-
- html += `
-
-
-
-
-
-
Drag and drop images here or click to browse
-
Max ${maxFiles} files, ${maxSizeMB}MB each (PNG, JPG, GIF, BMP)