mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-11 21:33:00 +00:00
Add simple table interface for custom feeds
- Replace complex array-of-objects widget with clean table - Table columns: Name, URL, Logo (upload), Enabled checkbox, Delete - Use dot notation for form field names (feeds.custom_feeds.0.name) - Add JavaScript functions for add/remove rows and logo upload - Fix file-upload detection order to prevent breaking static-image plugin
This commit is contained in:
@@ -4839,7 +4839,7 @@
|
|||||||
newRow.innerHTML = `
|
newRow.innerHTML = `
|
||||||
<td class="px-4 py-3 whitespace-nowrap">
|
<td class="px-4 py-3 whitespace-nowrap">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="${fullKey}[${newIndex}].name"
|
name="${fullKey}.${newIndex}.name"
|
||||||
value=""
|
value=""
|
||||||
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="Feed Name"
|
||||||
@@ -4847,7 +4847,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 whitespace-nowrap">
|
<td class="px-4 py-3 whitespace-nowrap">
|
||||||
<input type="url"
|
<input type="url"
|
||||||
name="${fullKey}[${newIndex}].url"
|
name="${fullKey}.${newIndex}.url"
|
||||||
value=""
|
value=""
|
||||||
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="https://example.com/feed"
|
placeholder="https://example.com/feed"
|
||||||
@@ -4859,7 +4859,7 @@
|
|||||||
id="${fieldId}_logo_${newIndex}"
|
id="${fieldId}_logo_${newIndex}"
|
||||||
accept="image/png,image/jpeg,image/bmp"
|
accept="image/png,image/jpeg,image/bmp"
|
||||||
style="display: none;"
|
style="display: none;"
|
||||||
onchange="handleCustomFeedLogoUpload(event, '${fieldId}', ${newIndex}, 'ledmatrix-news')">
|
onchange="handleCustomFeedLogoUpload(event, '${fieldId}', ${newIndex}, 'ledmatrix-news', '${fullKey}')">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
onclick="document.getElementById('${fieldId}_logo_${newIndex}').click()"
|
onclick="document.getElementById('${fieldId}_logo_${newIndex}').click()"
|
||||||
class="px-2 py-1 text-xs bg-gray-200 hover:bg-gray-300 rounded">
|
class="px-2 py-1 text-xs bg-gray-200 hover:bg-gray-300 rounded">
|
||||||
@@ -4870,8 +4870,9 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 whitespace-nowrap text-center">
|
<td class="px-4 py-3 whitespace-nowrap text-center">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
name="${fullKey}[${newIndex}].enabled"
|
name="${fullKey}.${newIndex}.enabled"
|
||||||
checked
|
checked
|
||||||
|
value="true"
|
||||||
class="h-4 w-4 text-blue-600">
|
class="h-4 w-4 text-blue-600">
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 whitespace-nowrap text-center">
|
<td class="px-4 py-3 whitespace-nowrap text-center">
|
||||||
@@ -4898,14 +4899,20 @@
|
|||||||
r.querySelectorAll('input, button').forEach(input => {
|
r.querySelectorAll('input, button').forEach(input => {
|
||||||
const name = input.getAttribute('name');
|
const name = input.getAttribute('name');
|
||||||
if (name) {
|
if (name) {
|
||||||
input.setAttribute('name', name.replace(/\[\d+\]/, `[${index}]`));
|
// Replace pattern like "feeds.custom_feeds.0.name" with "feeds.custom_feeds.1.name"
|
||||||
|
input.setAttribute('name', name.replace(/\.\d+\./, `.${index}.`));
|
||||||
}
|
}
|
||||||
const id = input.id;
|
const id = input.id;
|
||||||
if (id && id.includes('_logo_')) {
|
if (id && id.includes('_logo_')) {
|
||||||
const newId = id.replace(/_logo_\d+/, `_logo_${index}`);
|
const newId = id.replace(/_logo_\d+/, `_logo_${index}`);
|
||||||
input.id = newId;
|
input.id = newId;
|
||||||
if (input.onclick) {
|
const onclick = input.getAttribute('onclick');
|
||||||
input.setAttribute('onclick', input.getAttribute('onclick').replace(/\d+/, index));
|
if (onclick) {
|
||||||
|
input.setAttribute('onclick', onclick.replace(/,\s*\d+\s*,/, `, ${index},`));
|
||||||
|
}
|
||||||
|
const onchange = input.getAttribute('onchange');
|
||||||
|
if (onchange) {
|
||||||
|
input.setAttribute('onchange', onchange.replace(/,\s*\d+\s*,/, `, ${index},`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -4913,7 +4920,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCustomFeedLogoUpload(event, fieldId, index, pluginId) {
|
function handleCustomFeedLogoUpload(event, fieldId, index, pluginId, fullKey) {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
@@ -4932,21 +4939,26 @@
|
|||||||
const row = document.querySelector(`#${fieldId}_tbody tr[data-index="${index}"]`);
|
const row = document.querySelector(`#${fieldId}_tbody tr[data-index="${index}"]`);
|
||||||
if (row) {
|
if (row) {
|
||||||
const logoCell = row.querySelector('td:nth-child(3)');
|
const logoCell = row.querySelector('td:nth-child(3)');
|
||||||
|
const existingPathInput = logoCell.querySelector('input[name*=".logo.path"]');
|
||||||
|
const existingIdInput = logoCell.querySelector('input[name*=".logo.id"]');
|
||||||
|
const pathName = existingPathInput ? existingPathInput.name : `${fullKey}.${index}.logo.path`;
|
||||||
|
const idName = existingIdInput ? existingIdInput.name : `${fullKey}.${index}.logo.id`;
|
||||||
|
|
||||||
logoCell.innerHTML = `
|
logoCell.innerHTML = `
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<input type="file"
|
<input type="file"
|
||||||
id="${fieldId}_logo_${index}"
|
id="${fieldId}_logo_${index}"
|
||||||
accept="image/png,image/jpeg,image/bmp"
|
accept="image/png,image/jpeg,image/bmp"
|
||||||
style="display: none;"
|
style="display: none;"
|
||||||
onchange="handleCustomFeedLogoUpload(event, '${fieldId}', ${index}, '${pluginId}')">
|
onchange="handleCustomFeedLogoUpload(event, '${fieldId}', ${index}, '${pluginId}', '${fullKey}')">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
onclick="document.getElementById('${fieldId}_logo_${index}').click()"
|
onclick="document.getElementById('${fieldId}_logo_${index}').click()"
|
||||||
class="px-2 py-1 text-xs bg-gray-200 hover:bg-gray-300 rounded">
|
class="px-2 py-1 text-xs bg-gray-200 hover:bg-gray-300 rounded">
|
||||||
<i class="fas fa-upload mr-1"></i> Upload
|
<i class="fas fa-upload mr-1"></i> Upload
|
||||||
</button>
|
</button>
|
||||||
<img src="/${uploadedFile.path}" alt="Logo" class="w-8 h-8 object-cover rounded border" id="${fieldId}_logo_preview_${index}">
|
<img src="/${uploadedFile.path}" alt="Logo" class="w-8 h-8 object-cover rounded border" id="${fieldId}_logo_preview_${index}">
|
||||||
<input type="hidden" name="${logoCell.querySelector('input[name*=".logo.path"]')?.name || `feeds.custom_feeds[${index}].logo.path`}" value="${uploadedFile.path}">
|
<input type="hidden" name="${pathName}" value="${uploadedFile.path}">
|
||||||
<input type="hidden" name="${logoCell.querySelector('input[name*=".logo.id"]')?.name || `feeds.custom_feeds[${index}].logo.id`}" value="${uploadedFile.id}">
|
<input type="hidden" name="${idName}" value="${uploadedFile.id}">
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,15 +153,105 @@
|
|||||||
<input type="hidden" id="{{ field_id }}_images_data" name="{{ full_key }}" value='{{ array_value|tojson|safe }}'>
|
<input type="hidden" id="{{ field_id }}_images_data" name="{{ full_key }}" value='{{ array_value|tojson|safe }}'>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{# Regular array input (comma-separated) #}
|
{# Check if it's an array of objects (like custom_feeds) - use simple table interface #}
|
||||||
{% set array_value = value if value is not none else (prop.default if prop.default is defined else []) %}
|
{% set items_schema = prop.get('items') or {} %}
|
||||||
<input type="text"
|
{% set is_array_of_objects = items_schema.get('type') == 'object' and items_schema.get('properties') %}
|
||||||
id="{{ field_id }}"
|
{% if is_array_of_objects %}
|
||||||
name="{{ full_key }}"
|
{# Simple table-based interface for custom feeds #}
|
||||||
value="{{ array_value|join(', ') if array_value is iterable and array_value is not string else '' }}"
|
{% set item_properties = items_schema.get('properties', {}) %}
|
||||||
placeholder="Enter values separated by commas"
|
{% set max_items = prop.get('maxItems', 50) %}
|
||||||
class="form-input w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 bg-white text-black placeholder:text-gray-500">
|
{% 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 []) %}
|
||||||
<p class="text-xs text-gray-400 mt-1">Separate multiple values with commas</p>
|
|
||||||
|
<div class="custom-feeds-table-container mt-1">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 border border-gray-300 rounded-lg">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">URL</th>
|
||||||
|
<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>
|
||||||
|
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="{{ field_id }}_tbody" class="bg-white divide-y divide-gray-200">
|
||||||
|
{% for item in array_value %}
|
||||||
|
{% set item_index = loop.index0 %}
|
||||||
|
<tr class="custom-feed-row" data-index="{{ item_index }}">
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap">
|
||||||
|
<input type="text"
|
||||||
|
name="{{ full_key }}.{{ item_index }}.name"
|
||||||
|
value="{{ item.get('name', '') }}"
|
||||||
|
class="block w-full px-2 py-1 border border-gray-300 rounded text-sm"
|
||||||
|
placeholder="Feed Name"
|
||||||
|
required>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap">
|
||||||
|
<input type="url"
|
||||||
|
name="{{ full_key }}.{{ item_index }}.url"
|
||||||
|
value="{{ item.get('url', '') }}"
|
||||||
|
class="block w-full px-2 py-1 border border-gray-300 rounded text-sm"
|
||||||
|
placeholder="https://example.com/feed"
|
||||||
|
required>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap">
|
||||||
|
{% 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"
|
||||||
|
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 %}
|
||||||
|
{# Regular array input (comma-separated) #}
|
||||||
|
{% set array_value = value if value is not none else (prop.default if prop.default is defined else []) %}
|
||||||
|
<input type="text"
|
||||||
|
id="{{ field_id }}"
|
||||||
|
name="{{ full_key }}"
|
||||||
|
value="{{ array_value|join(', ') if array_value is iterable and array_value is not string else '' }}"
|
||||||
|
placeholder="Enter values separated by commas"
|
||||||
|
class="form-input w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 bg-white text-black placeholder:text-gray-500">
|
||||||
|
<p class="text-xs text-gray-400 mt-1">Separate multiple values with commas</p>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Text input (default) #}
|
{# Text input (default) #}
|
||||||
|
|||||||
Reference in New Issue
Block a user