fix: Make custom feeds table widget-specific instead of generic fallback

Replace generic array-of-objects check with widget-specific check for
'custom-feeds' widget to prevent hardcoded schema from breaking other
plugins with different array-of-objects structures.

Changes:
- Check for x-widget == 'custom-feeds' before rendering custom feeds table
- Add schema validation to ensure required fields (name, url) exist
- Show warning message if schema doesn't match expected structure
- Fall back to generic array input for other array-of-objects schemas
- Add comments for future generic array-of-objects support

This ensures the hardcoded custom feeds table (name, url, logo, enabled)
only renders when explicitly requested via widget type, preventing
breakage for other plugins with different array-of-objects schemas.
This commit is contained in:
Chuck
2026-01-08 12:43:04 -05:00
parent 0eb457fbc3
commit 89f07b8b79

View File

@@ -177,95 +177,105 @@
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
{# Check if it's an array of objects (like custom_feeds) - use simple table interface #} {# Check for custom-feeds widget first #}
{% set items_schema = prop.get('items') or {} %} {% set items_schema = prop.get('items') or {} %}
{% set is_array_of_objects = items_schema.get('type') == 'object' and items_schema.get('properties') %} {% if x_widget == 'custom-feeds' %}
{% if is_array_of_objects %} {# Custom feeds table interface - widget-specific implementation #}
{# Simple table-based interface for custom feeds #} {# Validate that required fields exist in schema #}
{% set item_properties = items_schema.get('properties', {}) %} {% set item_properties = items_schema.get('properties', {}) %}
{% set max_items = prop.get('maxItems', 50) %} {% if not (item_properties.get('name') and item_properties.get('url')) %}
{% set array_value = value if value is not none and value is iterable and value is not string else (prop.default if prop.default is defined and prop.default is iterable and prop.default is not string else []) %} {# Fallback to generic if schema doesn't match expected structure #}
<p class="text-xs text-amber-600 mt-1">
<div class="custom-feeds-table-container mt-1"> <i class="fas fa-exclamation-triangle mr-1"></i>
<table class="min-w-full divide-y divide-gray-200 border border-gray-300 rounded-lg"> Custom feeds widget requires 'name' and 'url' properties in items schema.
<thead class="bg-gray-50"> </p>
<tr> {% else %}
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th> {% set max_items = prop.get('maxItems', 50) %}
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">URL</th> {% set array_value = value if value is not none and value is iterable and value is not string else (prop.default if prop.default is defined and prop.default is iterable and prop.default is not string else []) %}
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Logo</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Enabled</th> <div class="custom-feeds-table-container mt-1">
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th> <table class="min-w-full divide-y divide-gray-200 border border-gray-300 rounded-lg">
</tr> <thead class="bg-gray-50">
</thead> <tr>
<tbody id="{{ field_id }}_tbody" class="bg-white divide-y divide-gray-200"> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
{% for item in array_value %} <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">URL</th>
{% set item_index = loop.index0 %} <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Logo</th>
<tr class="custom-feed-row" data-index="{{ item_index }}"> <th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Enabled</th>
<td class="px-4 py-3 whitespace-nowrap"> <th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
<input type="text" </tr>
name="{{ full_key }}.{{ item_index }}.name" </thead>
value="{{ item.get('name', '') }}" <tbody id="{{ field_id }}_tbody" class="bg-white divide-y divide-gray-200">
class="block w-full px-2 py-1 border border-gray-300 rounded text-sm" {% for item in array_value %}
placeholder="Feed Name" {% set item_index = loop.index0 %}
required> <tr class="custom-feed-row" data-index="{{ item_index }}">
</td> <td class="px-4 py-3 whitespace-nowrap">
<td class="px-4 py-3 whitespace-nowrap"> <input type="text"
<input type="url" name="{{ full_key }}.{{ item_index }}.name"
name="{{ full_key }}.{{ item_index }}.url" value="{{ item.get('name', '') }}"
value="{{ item.get('url', '') }}" class="block w-full px-2 py-1 border border-gray-300 rounded text-sm"
class="block w-full px-2 py-1 border border-gray-300 rounded text-sm" placeholder="Feed Name"
placeholder="https://example.com/feed" required>
required> </td>
</td> <td class="px-4 py-3 whitespace-nowrap">
<td class="px-4 py-3 whitespace-nowrap"> <input type="url"
{% set logo_value = item.get('logo') or {} %} name="{{ full_key }}.{{ item_index }}.url"
{% set logo_path = logo_value.get('path', '') %} value="{{ item.get('url', '') }}"
<div class="flex items-center space-x-2"> class="block w-full px-2 py-1 border border-gray-300 rounded text-sm"
<input type="file" placeholder="https://example.com/feed"
id="{{ field_id }}_logo_{{ item_index }}" required>
accept="image/png,image/jpeg,image/bmp" </td>
style="display: none;" <td class="px-4 py-3 whitespace-nowrap">
onchange="handleCustomFeedLogoUpload(event, '{{ field_id }}', {{ item_index }}, '{{ plugin_id }}', '{{ full_key }}')"> {% set logo_value = item.get('logo') or {} %}
{% set logo_path = logo_value.get('path', '') %}
<div class="flex items-center space-x-2">
<input type="file"
id="{{ field_id }}_logo_{{ item_index }}"
accept="image/png,image/jpeg,image/bmp"
style="display: none;"
onchange="handleCustomFeedLogoUpload(event, '{{ field_id }}', {{ item_index }}, '{{ plugin_id }}', '{{ full_key }}')">
<button type="button"
onclick="document.getElementById('{{ field_id }}_logo_{{ item_index }}').click()"
class="px-2 py-1 text-xs bg-gray-200 hover:bg-gray-300 rounded">
<i class="fas fa-upload mr-1"></i> Upload
</button>
{% if logo_path %}
<img src="/{{ logo_path }}" alt="Logo" class="w-8 h-8 object-cover rounded border" id="{{ field_id }}_logo_preview_{{ item_index }}">
<input type="hidden" name="{{ full_key }}.{{ item_index }}.logo.path" value="{{ logo_path }}">
<input type="hidden" name="{{ full_key }}.{{ item_index }}.logo.id" value="{{ logo_value.get('id', '') }}">
{% else %}
<span class="text-xs text-gray-400">No logo</span>
{% endif %}
</div>
</td>
<td class="px-4 py-3 whitespace-nowrap text-center">
<input type="checkbox"
name="{{ full_key }}.{{ item_index }}.enabled"
{% if item.get('enabled', true) %}checked{% endif %}
value="true"
class="h-4 w-4 text-blue-600">
</td>
<td class="px-4 py-3 whitespace-nowrap text-center">
<button type="button" <button type="button"
onclick="document.getElementById('{{ field_id }}_logo_{{ item_index }}').click()" onclick="removeCustomFeedRow(this)"
class="px-2 py-1 text-xs bg-gray-200 hover:bg-gray-300 rounded"> class="text-red-600 hover:text-red-800 px-2 py-1">
<i class="fas fa-upload mr-1"></i> Upload <i class="fas fa-trash"></i>
</button> </button>
{% if logo_path %} </td>
<img src="/{{ logo_path }}" alt="Logo" class="w-8 h-8 object-cover rounded border" id="{{ field_id }}_logo_preview_{{ item_index }}"> </tr>
<input type="hidden" name="{{ full_key }}.{{ item_index }}.logo.path" value="{{ logo_path }}"> {% endfor %}
<input type="hidden" name="{{ full_key }}.{{ item_index }}.logo.id" value="{{ logo_value.get('id', '') }}"> </tbody>
{% else %} </table>
<span class="text-xs text-gray-400">No logo</span> <button type="button"
{% endif %} onclick="addCustomFeedRow('{{ field_id }}', '{{ full_key }}', {{ max_items }})"
</div> class="mt-3 px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-md"
</td> {% if array_value|length >= max_items %}disabled style="opacity: 0.5;"{% endif %}>
<td class="px-4 py-3 whitespace-nowrap text-center"> <i class="fas fa-plus mr-1"></i> Add Feed
<input type="checkbox" </button>
name="{{ full_key }}.{{ item_index }}.enabled" </div>
{% if item.get('enabled', true) %}checked{% endif %} {% endif %}
value="true"
class="h-4 w-4 text-blue-600">
</td>
<td class="px-4 py-3 whitespace-nowrap text-center">
<button type="button"
onclick="removeCustomFeedRow(this)"
class="text-red-600 hover:text-red-800 px-2 py-1">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<button type="button"
onclick="addCustomFeedRow('{{ field_id }}', '{{ full_key }}', {{ max_items }})"
class="mt-3 px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-md"
{% if array_value|length >= max_items %}disabled style="opacity: 0.5;"{% endif %}>
<i class="fas fa-plus mr-1"></i> Add Feed
</button>
</div>
{% else %} {% else %}
{# Generic array-of-objects would go here if needed in the future #}
{# For now, fall back to regular array input (comma-separated) #}
{# Regular array input (comma-separated) #} {# Regular array input (comma-separated) #}
{% set array_value = value if value is not none else (prop.default if prop.default is defined else []) %} {% set array_value = value if value is not none else (prop.default if prop.default is defined else []) %}
<input type="text" <input type="text"