mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 16:58:06 +00:00
fix(web): resolve Codacy findings — DOM building over innerHTML, Map callbacks, misc lint
Verified each of the 27 reported findings against current code; all fixed except one rule class skipped with reason below. - app-early.js: plugin tab buttons are now built with createElement/ createTextNode instead of innerHTML template strings (icon class and name come from plugin manifests - semi-trusted input; the old code escaped the name but interpolated the icon class). Both construction sites. Also the forEach arrow no longer returns tab.remove()'s value. - plugin-order-list.js: rows, empty state, and error state all built with DOM APIs - the file no longer contains innerHTML at all (the now-unneeded escapeHtml/escapeAttr helpers are removed); MODE_LABELS is a Map so the vegas-mode lookup can't hit prototype properties. - notification.js: actionCallbacks is a Map (get/set/delete) instead of a plain object - resolves the object-injection-sink and dynamic-delete findings; triggerAction also type-checks the callback. - htmx-config.js: unused catch binding dropped; var -> const in the afterSettle handler; the swapped-<script> re-execution reads/writes textContent instead of innerHTML; the diagnostic form payload uses a null-prototype object so a field named __proto__ can't pollute. - custom-feeds-helpers.js DELETED (with its script tag): all three of its functions (addCustomFeedRow, removeCustomFeedRow, handleCustomFeedLogoUpload) are shadowed by the deferred widgets/custom-feeds.js window assignments, which always win at call time - the copies were dead even when they lived inline in base.html. This also resolves the unused-function and unused-variable findings there. Skipped: 4x "Non-serializable expression must be wrapped with $(...)" in app-early.js - that rule targets code crossing a browser-automation serialization boundary (e.g. page.evaluate); these are ordinary arrow functions in plain browser code with no such boundary. Validation: all 40 web tests pass (incl. the static-asset reference audit, which confirms no template still points at the deleted file); Jinja parse OK. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e1e26e8eab
commit
e00d10ce60
@@ -51,7 +51,7 @@
|
||||
if (pluginTabsRow && pluginTabsNav && plugins.length > 0) {
|
||||
// Clear existing plugin tabs (except Plugin Manager)
|
||||
const existingTabs = pluginTabsNav.querySelectorAll('.plugin-tab');
|
||||
existingTabs.forEach(tab => tab.remove());
|
||||
existingTabs.forEach(tab => { tab.remove(); });
|
||||
|
||||
// Add tabs for each installed plugin
|
||||
plugins.forEach(plugin => {
|
||||
@@ -72,8 +72,14 @@
|
||||
}
|
||||
}
|
||||
};
|
||||
const iconClass = (plugin.icon || 'fas fa-puzzle-piece').replace(/"/g, '"');
|
||||
tabButton.innerHTML = `<i class="${iconClass}"></i>${(plugin.name || plugin.id).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')}`;
|
||||
// Built with DOM APIs (no innerHTML): the icon class and
|
||||
// name come from plugin manifests, which are only
|
||||
// semi-trusted input.
|
||||
const tabIcon = document.createElement('i');
|
||||
tabIcon.className = plugin.icon || 'fas fa-puzzle-piece';
|
||||
tabButton.textContent = '';
|
||||
tabButton.appendChild(tabIcon);
|
||||
tabButton.appendChild(document.createTextNode(plugin.name || plugin.id));
|
||||
pluginTabsNav.appendChild(tabButton);
|
||||
});
|
||||
debugLog('[GLOBAL] Updated plugin tabs directly:', plugins.length, 'tabs added');
|
||||
@@ -331,10 +337,13 @@
|
||||
this.updatePluginTabStates();
|
||||
}
|
||||
};
|
||||
const div = document.createElement('div');
|
||||
div.textContent = plugin.name || plugin.id;
|
||||
const iconClass = (plugin.icon || 'fas fa-puzzle-piece').replace(/"/g, '"');
|
||||
tabButton.innerHTML = `<i class="${iconClass}"></i>${div.innerHTML}`;
|
||||
// DOM APIs instead of innerHTML: manifest
|
||||
// icon/name are semi-trusted input.
|
||||
const tabIcon = document.createElement('i');
|
||||
tabIcon.className = plugin.icon || 'fas fa-puzzle-piece';
|
||||
tabButton.textContent = '';
|
||||
tabButton.appendChild(tabIcon);
|
||||
tabButton.appendChild(document.createTextNode(plugin.name || plugin.id));
|
||||
pluginTabsNav.appendChild(tabButton);
|
||||
});
|
||||
debugLog('[STUB] updatePluginTabs: Added', this.installedPlugins.length, 'plugin tabs');
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
/* global debugLog */
|
||||
// Custom feeds table helper functions
|
||||
// Extracted from templates/v3/base.html so browsers cache it as a static asset.
|
||||
function addCustomFeedRow(fieldId, fullKey, maxItems, pluginId) {
|
||||
const tbody = document.getElementById(fieldId + '_tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
const currentRows = tbody.querySelectorAll('.custom-feed-row');
|
||||
if (currentRows.length >= maxItems) {
|
||||
alert(`Maximum ${maxItems} feeds allowed`);
|
||||
return;
|
||||
}
|
||||
|
||||
const newIndex = currentRows.length;
|
||||
const newRow = document.createElement('tr');
|
||||
newRow.className = 'custom-feed-row';
|
||||
newRow.setAttribute('data-index', newIndex);
|
||||
|
||||
// Create name cell
|
||||
const nameCell = document.createElement('td');
|
||||
nameCell.className = 'px-4 py-3 whitespace-nowrap';
|
||||
const nameInput = document.createElement('input');
|
||||
nameInput.type = 'text';
|
||||
nameInput.name = `${fullKey}.${newIndex}.name`;
|
||||
nameInput.value = '';
|
||||
nameInput.className = 'block w-full px-2 py-1 border border-gray-300 rounded text-sm';
|
||||
nameInput.placeholder = 'Feed Name';
|
||||
nameInput.required = true;
|
||||
nameCell.appendChild(nameInput);
|
||||
|
||||
// Create URL cell
|
||||
const urlCell = document.createElement('td');
|
||||
urlCell.className = 'px-4 py-3 whitespace-nowrap';
|
||||
const urlInput = document.createElement('input');
|
||||
urlInput.type = 'url';
|
||||
urlInput.name = `${fullKey}.${newIndex}.url`;
|
||||
urlInput.value = '';
|
||||
urlInput.className = 'block w-full px-2 py-1 border border-gray-300 rounded text-sm';
|
||||
urlInput.placeholder = 'https://example.com/feed';
|
||||
urlInput.required = true;
|
||||
urlCell.appendChild(urlInput);
|
||||
|
||||
// Create logo cell
|
||||
const logoCell = document.createElement('td');
|
||||
logoCell.className = 'px-4 py-3 whitespace-nowrap';
|
||||
const logoContainer = document.createElement('div');
|
||||
logoContainer.className = 'flex items-center space-x-2';
|
||||
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.id = `${fieldId}_logo_${newIndex}`;
|
||||
fileInput.accept = 'image/png,image/jpeg,image/bmp,image/gif';
|
||||
fileInput.style.display = 'none';
|
||||
fileInput.dataset.index = String(newIndex);
|
||||
// Use addEventListener with dataset index to allow reindexing
|
||||
fileInput.addEventListener('change', function(e) {
|
||||
const idx = parseInt(e.target.dataset.index || '0', 10);
|
||||
handleCustomFeedLogoUpload(e, fieldId, idx, pluginId, fullKey);
|
||||
});
|
||||
|
||||
const uploadButton = document.createElement('button');
|
||||
uploadButton.type = 'button';
|
||||
uploadButton.className = 'px-2 py-1 text-xs bg-gray-200 hover:bg-gray-300 rounded';
|
||||
// Use fileInput directly instead of getElementById for reindexing compatibility
|
||||
uploadButton.addEventListener('click', function() {
|
||||
fileInput.click();
|
||||
});
|
||||
const uploadIcon = document.createElement('i');
|
||||
uploadIcon.className = 'fas fa-upload mr-1';
|
||||
uploadButton.appendChild(uploadIcon);
|
||||
uploadButton.appendChild(document.createTextNode(' Upload'));
|
||||
|
||||
const noLogoSpan = document.createElement('span');
|
||||
noLogoSpan.className = 'text-xs text-gray-400';
|
||||
noLogoSpan.textContent = 'No logo';
|
||||
|
||||
logoContainer.appendChild(fileInput);
|
||||
logoContainer.appendChild(uploadButton);
|
||||
logoContainer.appendChild(noLogoSpan);
|
||||
logoCell.appendChild(logoContainer);
|
||||
|
||||
// Create enabled cell
|
||||
const enabledCell = document.createElement('td');
|
||||
enabledCell.className = 'px-4 py-3 whitespace-nowrap text-center';
|
||||
const enabledInput = document.createElement('input');
|
||||
enabledInput.type = 'checkbox';
|
||||
enabledInput.name = `${fullKey}.${newIndex}.enabled`;
|
||||
enabledInput.checked = true;
|
||||
enabledInput.value = 'true';
|
||||
enabledInput.className = 'h-4 w-4 text-blue-600';
|
||||
enabledCell.appendChild(enabledInput);
|
||||
|
||||
// Create remove cell
|
||||
const removeCell = document.createElement('td');
|
||||
removeCell.className = 'px-4 py-3 whitespace-nowrap text-center';
|
||||
const removeButton = document.createElement('button');
|
||||
removeButton.type = 'button';
|
||||
removeButton.className = 'text-red-600 hover:text-red-800 px-2 py-1';
|
||||
// Use addEventListener instead of string-based onclick to prevent injection
|
||||
removeButton.addEventListener('click', function() {
|
||||
removeCustomFeedRow(this);
|
||||
});
|
||||
const removeIcon = document.createElement('i');
|
||||
removeIcon.className = 'fas fa-trash';
|
||||
removeButton.appendChild(removeIcon);
|
||||
removeCell.appendChild(removeButton);
|
||||
|
||||
// Append all cells to row
|
||||
newRow.appendChild(nameCell);
|
||||
newRow.appendChild(urlCell);
|
||||
newRow.appendChild(logoCell);
|
||||
newRow.appendChild(enabledCell);
|
||||
newRow.appendChild(removeCell);
|
||||
tbody.appendChild(newRow);
|
||||
}
|
||||
|
||||
function removeCustomFeedRow(button) {
|
||||
const row = button.closest('tr');
|
||||
if (!row) return;
|
||||
|
||||
if (confirm('Remove this feed?')) {
|
||||
const tbody = row.parentElement;
|
||||
if (!tbody) return;
|
||||
|
||||
row.remove();
|
||||
|
||||
// Re-index remaining rows
|
||||
const rows = tbody.querySelectorAll('.custom-feed-row');
|
||||
rows.forEach((r, index) => {
|
||||
const oldIndex = r.getAttribute('data-index');
|
||||
r.setAttribute('data-index', index);
|
||||
// Update all input names with new index
|
||||
r.querySelectorAll('input, button').forEach(input => {
|
||||
const name = input.getAttribute('name');
|
||||
if (name) {
|
||||
// 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;
|
||||
if (id) {
|
||||
// Keep IDs aligned after reindex (supports both _logo_<n> and _logo_preview_<n>)
|
||||
input.id = id
|
||||
.replace(/_logo_preview_\d+$/, `_logo_preview_${index}`)
|
||||
.replace(/_logo_\d+$/, `_logo_${index}`);
|
||||
}
|
||||
// Keep dataset index aligned so event handlers remain correct after reindex
|
||||
if (input.dataset && 'index' in input.dataset) {
|
||||
input.dataset.index = String(index);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleCustomFeedLogoUpload(event, fieldId, index, pluginId, fullKey) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('plugin_id', pluginId);
|
||||
|
||||
fetch('/api/v3/plugins/assets/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
// Check HTTP status before parsing JSON
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
throw new Error(`Upload failed: ${response.status} ${response.statusText}${text ? ': ' + text : ''}`);
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.data && data.data.files && data.data.files.length > 0) {
|
||||
const uploadedFile = data.data.files[0];
|
||||
const row = document.querySelector(`#${fieldId}_tbody tr[data-index="${index}"]`);
|
||||
if (row) {
|
||||
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`;
|
||||
|
||||
// Normalize path: remove leading slashes, then add single leading slash
|
||||
const normalizedPath = String(uploadedFile.path || '').replace(/^\/+/, '');
|
||||
const imageSrc = '/' + normalizedPath;
|
||||
|
||||
// Clear logoCell and build DOM safely to prevent XSS
|
||||
logoCell.textContent = ''; // Clear existing content
|
||||
|
||||
// Create container div
|
||||
const container = document.createElement('div');
|
||||
container.className = 'flex items-center space-x-2';
|
||||
|
||||
// Create file input
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.id = `${fieldId}_logo_${index}`;
|
||||
fileInput.accept = 'image/png,image/jpeg,image/bmp,image/gif';
|
||||
fileInput.style.display = 'none';
|
||||
fileInput.dataset.index = String(index);
|
||||
// Use addEventListener with dataset index to allow reindexing
|
||||
fileInput.addEventListener('change', function(e) {
|
||||
const idx = parseInt(e.target.dataset.index || '0', 10);
|
||||
handleCustomFeedLogoUpload(e, fieldId, idx, pluginId, fullKey);
|
||||
});
|
||||
|
||||
// Create upload button
|
||||
const uploadButton = document.createElement('button');
|
||||
uploadButton.type = 'button';
|
||||
uploadButton.className = 'px-2 py-1 text-xs bg-gray-200 hover:bg-gray-300 rounded';
|
||||
// Use fileInput directly instead of getElementById for reindexing compatibility
|
||||
uploadButton.addEventListener('click', function() {
|
||||
fileInput.click();
|
||||
});
|
||||
const uploadIcon = document.createElement('i');
|
||||
uploadIcon.className = 'fas fa-upload mr-1';
|
||||
uploadButton.appendChild(uploadIcon);
|
||||
uploadButton.appendChild(document.createTextNode(' Upload'));
|
||||
|
||||
// Create img element - use normalized path, set src via property to prevent XSS
|
||||
const img = document.createElement('img');
|
||||
img.src = imageSrc; // Use property assignment with normalized path
|
||||
img.alt = 'Logo';
|
||||
img.className = 'w-8 h-8 object-cover rounded border';
|
||||
img.id = `${fieldId}_logo_preview_${index}`;
|
||||
|
||||
// Create hidden input for path - set value via property to prevent XSS
|
||||
const pathInput = document.createElement('input');
|
||||
pathInput.type = 'hidden';
|
||||
pathInput.name = pathName;
|
||||
pathInput.value = imageSrc;
|
||||
|
||||
// Create hidden input for id - set value via property to prevent XSS
|
||||
const idInput = document.createElement('input');
|
||||
idInput.type = 'hidden';
|
||||
idInput.name = idName;
|
||||
idInput.value = String(uploadedFile.id); // Ensure it's a string
|
||||
|
||||
// Append all elements to container
|
||||
container.appendChild(fileInput);
|
||||
container.appendChild(uploadButton);
|
||||
container.appendChild(img);
|
||||
container.appendChild(pathInput);
|
||||
container.appendChild(idInput);
|
||||
|
||||
// Append container to logoCell
|
||||
logoCell.appendChild(container);
|
||||
}
|
||||
// Allow re-uploading the same file (change event won't fire otherwise)
|
||||
event.target.value = '';
|
||||
} else {
|
||||
alert('Upload failed: ' + (data.message || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Upload error:', error);
|
||||
alert('Upload failed: ' + error.message);
|
||||
});
|
||||
}
|
||||
@@ -133,7 +133,7 @@
|
||||
// For form submissions, log the form data
|
||||
if (target && target.tagName === 'FORM') {
|
||||
const formData = new FormData(target);
|
||||
const formPayload = {};
|
||||
const formPayload = Object.create(null);
|
||||
for (const [key, value] of formData.entries()) {
|
||||
formPayload[key] = value;
|
||||
}
|
||||
@@ -149,7 +149,7 @@
|
||||
validation_errors: errorData.validation_errors,
|
||||
context: errorData.context
|
||||
});
|
||||
} catch (e) {
|
||||
} catch {
|
||||
console.error('Error response (non-JSON):', xhr.responseText.substring(0, 500));
|
||||
}
|
||||
}
|
||||
@@ -167,11 +167,11 @@
|
||||
const scripts = event.detail.target.querySelectorAll('script');
|
||||
scripts.forEach(function(oldScript) {
|
||||
try {
|
||||
if (oldScript.innerHTML.trim() || oldScript.src) {
|
||||
if (oldScript.textContent.trim() || oldScript.src) {
|
||||
const newScript = document.createElement('script');
|
||||
if (oldScript.src) newScript.src = oldScript.src;
|
||||
if (oldScript.type) newScript.type = oldScript.type;
|
||||
if (oldScript.innerHTML) newScript.textContent = oldScript.innerHTML;
|
||||
if (oldScript.textContent) newScript.textContent = oldScript.textContent;
|
||||
if (oldScript.parentNode) {
|
||||
oldScript.parentNode.insertBefore(newScript, oldScript);
|
||||
oldScript.parentNode.removeChild(oldScript);
|
||||
@@ -195,8 +195,8 @@
|
||||
// containers only) so modals and plugin config panels can still reload.
|
||||
document.body.addEventListener('htmx:afterSettle', function(event) {
|
||||
if (event.detail && event.detail.target) {
|
||||
var target = event.detail.target;
|
||||
var trigger = target.getAttribute('hx-trigger') || '';
|
||||
const target = event.detail.target;
|
||||
const trigger = target.getAttribute('hx-trigger') || '';
|
||||
if (trigger.includes('loadtab')) {
|
||||
target.setAttribute('data-loaded', 'true');
|
||||
}
|
||||
|
||||
@@ -59,8 +59,9 @@
|
||||
// Track active notifications
|
||||
let activeNotifications = [];
|
||||
// onAction callbacks for notifications with an inline action button,
|
||||
// keyed by notification id (cleaned up on dismiss).
|
||||
const actionCallbacks = {};
|
||||
// keyed by notification id (cleaned up on dismiss). A Map rather than a
|
||||
// plain object so ids can never collide with prototype properties.
|
||||
const actionCallbacks = new Map();
|
||||
let notificationCounter = 0;
|
||||
|
||||
/**
|
||||
@@ -116,7 +117,7 @@
|
||||
|
||||
// Remove from tracking array
|
||||
activeNotifications = activeNotifications.filter(id => id !== notificationId);
|
||||
delete actionCallbacks[notificationId];
|
||||
actionCallbacks.delete(notificationId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,7 +167,7 @@
|
||||
// The callback is stored by id and invoked via triggerAction, which
|
||||
// also dismisses the notification.
|
||||
if (options.actionLabel && typeof options.onAction === 'function') {
|
||||
actionCallbacks[notificationId] = options.onAction;
|
||||
actionCallbacks.set(notificationId, options.onAction);
|
||||
html += `
|
||||
<button type="button"
|
||||
onclick="window.LEDMatrixWidgets.get('notification').triggerAction('${notificationId}')"
|
||||
@@ -251,9 +252,9 @@
|
||||
* @param {string} notificationId - Notification ID whose action to run
|
||||
*/
|
||||
triggerAction: function(notificationId) {
|
||||
const cb = actionCallbacks[notificationId];
|
||||
const cb = actionCallbacks.get(notificationId);
|
||||
removeNotification(notificationId);
|
||||
if (cb) cb();
|
||||
if (typeof cb === 'function') cb();
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,21 +21,11 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text == null ? '' : String(text);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function escapeAttr(text) {
|
||||
return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
const MODE_LABELS = {
|
||||
'scroll': { label: 'Scroll', icon: 'fa-scroll', color: 'text-blue-600' },
|
||||
'fixed': { label: 'Fixed', icon: 'fa-square', color: 'text-green-600' },
|
||||
'static': { label: 'Static', icon: 'fa-pause', color: 'text-orange-600' }
|
||||
};
|
||||
const MODE_LABELS = new Map([
|
||||
['scroll', { label: 'Scroll', icon: 'fa-scroll', color: 'text-blue-600' }],
|
||||
['fixed', { label: 'Fixed', icon: 'fa-square', color: 'text-green-600' }],
|
||||
['static', { label: 'Static', icon: 'fa-pause', color: 'text-orange-600' }]
|
||||
]);
|
||||
|
||||
function init(options) {
|
||||
const container = document.getElementById(options.containerId);
|
||||
@@ -109,7 +99,11 @@
|
||||
const allPlugins = (data.data && data.data.plugins) || data.plugins || [];
|
||||
const plugins = allPlugins.filter(p => p.enabled);
|
||||
if (plugins.length === 0) {
|
||||
container.innerHTML = '<p class="text-sm text-gray-500 italic">No enabled plugins</p>';
|
||||
const empty = document.createElement('p');
|
||||
empty.className = 'text-sm text-gray-500 italic';
|
||||
empty.textContent = 'No enabled plugins';
|
||||
container.textContent = '';
|
||||
container.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -132,40 +126,55 @@
|
||||
if (!orderedPlugins.find(p => p.id === plugin.id)) orderedPlugins.push(plugin);
|
||||
});
|
||||
|
||||
let html = '';
|
||||
// Rows are built with DOM APIs rather than innerHTML — plugin
|
||||
// ids/names come from installed manifests (semi-trusted).
|
||||
container.textContent = '';
|
||||
orderedPlugins.forEach(plugin => {
|
||||
const safePluginId = escapeAttr(plugin.id);
|
||||
const safePluginName = escapeHtml(plugin.name || plugin.id);
|
||||
let rowInner;
|
||||
const row = document.createElement('div');
|
||||
row.className = 'flex items-center p-2 bg-gray-50 rounded border border-gray-200 cursor-move plugin-order-item';
|
||||
row.dataset.pluginId = plugin.id;
|
||||
row.draggable = true;
|
||||
|
||||
const grip = document.createElement('i');
|
||||
grip.className = 'fas fa-grip-vertical text-gray-400 mr-3';
|
||||
row.appendChild(grip);
|
||||
|
||||
if (excludedInput) {
|
||||
const isExcluded = excluded.includes(plugin.id);
|
||||
rowInner = `
|
||||
<label class="flex items-center flex-1">
|
||||
<input type="checkbox"
|
||||
class="plugin-order-include h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2"
|
||||
${!isExcluded ? 'checked' : ''}>
|
||||
<span class="text-sm font-medium text-gray-700">${safePluginName}</span>
|
||||
</label>`;
|
||||
const label = document.createElement('label');
|
||||
label.className = 'flex items-center flex-1';
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.className = 'plugin-order-include h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2';
|
||||
checkbox.checked = !isExcluded;
|
||||
const name = document.createElement('span');
|
||||
name.className = 'text-sm font-medium text-gray-700';
|
||||
name.textContent = plugin.name || plugin.id;
|
||||
label.appendChild(checkbox);
|
||||
label.appendChild(name);
|
||||
row.appendChild(label);
|
||||
} else {
|
||||
rowInner = `<span class="text-sm font-medium text-gray-700 flex-1">${safePluginName}</span>`;
|
||||
const name = document.createElement('span');
|
||||
name.className = 'text-sm font-medium text-gray-700 flex-1';
|
||||
name.textContent = plugin.name || plugin.id;
|
||||
row.appendChild(name);
|
||||
}
|
||||
let badge = '';
|
||||
|
||||
if (options.showVegasModeBadge) {
|
||||
const vegasMode = plugin.vegas_mode || plugin.vegas_content_type || 'fixed';
|
||||
const modeInfo = MODE_LABELS[vegasMode] || MODE_LABELS['fixed'];
|
||||
badge = `
|
||||
<span class="text-xs ${modeInfo.color} ml-2" title="Vegas display mode: ${modeInfo.label}">
|
||||
<i class="fas ${modeInfo.icon} mr-1"></i>${modeInfo.label}
|
||||
</span>`;
|
||||
const modeInfo = MODE_LABELS.get(vegasMode) || MODE_LABELS.get('fixed');
|
||||
const badge = document.createElement('span');
|
||||
badge.className = `text-xs ${modeInfo.color} ml-2`;
|
||||
badge.title = `Vegas display mode: ${modeInfo.label}`;
|
||||
const badgeIcon = document.createElement('i');
|
||||
badgeIcon.className = `fas ${modeInfo.icon} mr-1`;
|
||||
badge.appendChild(badgeIcon);
|
||||
badge.appendChild(document.createTextNode(modeInfo.label));
|
||||
row.appendChild(badge);
|
||||
}
|
||||
html += `
|
||||
<div class="flex items-center p-2 bg-gray-50 rounded border border-gray-200 cursor-move plugin-order-item"
|
||||
data-plugin-id="${safePluginId}" draggable="true">
|
||||
<i class="fas fa-grip-vertical text-gray-400 mr-3"></i>
|
||||
${rowInner}${badge}
|
||||
</div>`;
|
||||
|
||||
container.appendChild(row);
|
||||
});
|
||||
container.innerHTML = html;
|
||||
|
||||
setupDragAndDrop();
|
||||
container.querySelectorAll('.plugin-order-include').forEach(checkbox => {
|
||||
@@ -175,7 +184,11 @@
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching plugins:', error);
|
||||
container.innerHTML = '<p class="text-sm text-red-500">Error loading plugins</p>';
|
||||
const err = document.createElement('p');
|
||||
err.className = 'text-sm text-red-500';
|
||||
err.textContent = 'Error loading plugins';
|
||||
container.textContent = '';
|
||||
container.appendChild(err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1005,8 +1005,9 @@
|
||||
<!-- Legacy plugins_manager.js (for backward compatibility during migration) -->
|
||||
<script src="{{ url_for('static', filename='v3/plugins_manager.js') }}?v=20260307" defer></script>
|
||||
|
||||
<!-- Custom feeds table helper functions -->
|
||||
<script src="{{ url_for('static', filename='v3/js/custom-feeds-helpers.js') }}"></script>
|
||||
<!-- Custom feeds table helpers live in js/widgets/custom-feeds.js (the
|
||||
deferred widget's window assignments always shadowed the inline
|
||||
copies that used to sit here, so the duplicates were removed) -->
|
||||
|
||||
<!-- On-Demand Modal (moved here from plugins.html so it's always available) -->
|
||||
<div id="on-demand-modal" class="fixed inset-0 modal-backdrop flex items-center justify-center z-50" style="display: none;">
|
||||
|
||||
Reference in New Issue
Block a user