fix(array-objects): Fix schema lookup, reindexing, and disable file upload

Address PR review feedback for array-of-objects helpers:

1. Schema resolution: Use getSchemaProperty() instead of manual traversal
   - Fixes nested array-of-objects schema lookup (e.g., news.custom_feeds)
   - Now properly descends through .properties for nested objects

2. Reindexing: Replace brittle regex with targeted patterns
   - Only replace index in bracket notation [0], [1], etc. for names
   - Only replace _item_<digits> pattern for IDs (not arbitrary digits)
   - Use specific function parameter patterns for onclick handlers
   - Prevents corruption of fieldId, pluginId, or other numeric values

3. File upload: Disable widget until properly implemented
   - Hide/disable upload button with clear message
   - Show existing logos if present but disable upload functionality
   - Prevents silent failures when users attempt to upload files
   - Added TODO comments for future implementation

Also fixes exit code handling in one-shot-install.sh to properly capture
first_time_install.sh exit status before error trap fires.
This commit is contained in:
Chuck
2026-01-08 12:26:08 -05:00
parent 38ffaf0188
commit b88842e672
2 changed files with 78 additions and 45 deletions

View File

@@ -343,14 +343,16 @@ main() {
echo "" echo ""
# Execute with proper error handling # Execute with proper error handling
# Temporarily disable errexit to capture exit code instead of exiting immediately
set +e
# Use sudo if we're not root, otherwise run directly # Use sudo if we're not root, otherwise run directly
if [ "$EUID" -eq 0 ]; then if [ "$EUID" -eq 0 ]; then
bash ./first_time_install.sh bash ./first_time_install.sh
else else
sudo bash ./first_time_install.sh sudo bash ./first_time_install.sh
fi fi
INSTALL_EXIT_CODE=$? INSTALL_EXIT_CODE=$?
set -e # Re-enable errexit
if [ $INSTALL_EXIT_CODE -eq 0 ]; then if [ $INSTALL_EXIT_CODE -eq 0 ]; then
echo "" echo ""

View File

@@ -2485,6 +2485,10 @@ function renderArrayObjectItem(fieldId, fullKey, itemProperties, itemValue, inde
html += `<div class="mb-3">`; html += `<div class="mb-3">`;
// Handle file-upload widget (for logo field) // Handle file-upload widget (for logo field)
// NOTE: File upload for array-of-objects items is not yet implemented.
// The widget is disabled to prevent silent failures when users try to upload files.
// TODO: Implement handleArrayObjectFileUpload and removeArrayObjectFile with proper
// endpoint support and [data-file-data] attribute updates before enabling this widget.
if (propSchema['x-widget'] === 'file-upload') { if (propSchema['x-widget'] === 'file-upload') {
html += `<label class="block text-sm font-medium text-gray-700 mb-1">${escapeHtml(propLabel)}</label>`; html += `<label class="block text-sm font-medium text-gray-700 mb-1">${escapeHtml(propLabel)}</label>`;
if (propDescription) { if (propDescription) {
@@ -2494,29 +2498,26 @@ function renderArrayObjectItem(fieldId, fullKey, itemProperties, itemValue, inde
const pluginId = uploadConfig.plugin_id || (typeof currentPluginConfig !== 'undefined' ? currentPluginConfig?.pluginId : null) || (typeof window.currentPluginConfig !== 'undefined' ? window.currentPluginConfig?.pluginId : null) || 'ledmatrix-news'; const pluginId = uploadConfig.plugin_id || (typeof currentPluginConfig !== 'undefined' ? currentPluginConfig?.pluginId : null) || (typeof window.currentPluginConfig !== 'undefined' ? window.currentPluginConfig?.pluginId : null) || 'ledmatrix-news';
const logoValue = propValue || {}; const logoValue = propValue || {};
html += ` // Display existing logo if present, but disable upload functionality
<div class="file-upload-widget-inline">
<input type="file"
id="${itemId}_logo_file"
accept="${(uploadConfig.allowed_types || ['image/png', 'image/jpeg', 'image/bmp']).join(',')}"
style="display: none;"
onchange="handleArrayObjectFileUpload(event, '${fieldId}', ${index}, '${propKey}', '${pluginId}')">
<button type="button"
onclick="document.getElementById('${itemId}_logo_file').click()"
class="px-3 py-2 text-sm bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-md transition-colors">
<i class="fas fa-upload mr-1"></i> Upload Logo
</button>
`;
if (logoValue.path) { if (logoValue.path) {
html += ` html += `
<div class="mt-2 flex items-center space-x-2"> <div class="file-upload-widget-inline">
<img src="/${logoValue.path}" alt="Logo" class="w-16 h-16 object-cover rounded border"> <div class="mt-2 flex items-center space-x-2">
<img src="/${logoValue.path}" alt="Logo" class="w-16 h-16 object-cover rounded border">
<span class="text-sm text-gray-500 italic">File upload not yet available for array items</span>
</div>
</div>
`;
} else {
html += `
<div class="file-upload-widget-inline">
<button type="button" <button type="button"
onclick="removeArrayObjectFile('${fieldId}', ${index}, '${propKey}')" disabled
class="text-red-600 hover:text-red-800"> class="px-3 py-2 text-sm bg-gray-200 text-gray-400 rounded-md cursor-not-allowed opacity-50"
<i class="fas fa-trash"></i> Remove title="File upload for array items is not yet implemented">
<i class="fas fa-upload mr-1"></i> Upload Logo (Not Available)
</button> </button>
<p class="text-xs text-gray-500 mt-1 italic">File upload functionality for array items is coming soon</p>
</div> </div>
`; `;
} }
@@ -6433,19 +6434,13 @@ if (typeof window !== 'undefined') {
const schema = (typeof currentPluginConfig !== 'undefined' && currentPluginConfig?.schema) || (typeof window.currentPluginConfig !== 'undefined' && window.currentPluginConfig?.schema); const schema = (typeof currentPluginConfig !== 'undefined' && currentPluginConfig?.schema) || (typeof window.currentPluginConfig !== 'undefined' && window.currentPluginConfig?.schema);
if (!schema) return; if (!schema) return;
// Navigate to the items schema // Use getSchemaProperty to properly handle nested schemas (e.g., news.custom_feeds)
const keys = fullKey.split('.'); const arraySchema = getSchemaProperty(schema, fullKey);
let itemsSchema = schema.properties; if (!arraySchema || arraySchema.type !== 'array' || !arraySchema.items) {
for (const key of keys) { return;
if (itemsSchema && itemsSchema[key]) {
itemsSchema = itemsSchema[key];
if (itemsSchema.type === 'array' && itemsSchema.items) {
itemsSchema = itemsSchema.items;
break;
}
}
} }
const itemsSchema = arraySchema.items;
if (!itemsSchema || !itemsSchema.properties) return; if (!itemsSchema || !itemsSchema.properties) return;
const newIndex = currentItems.length; const newIndex = currentItems.length;
@@ -6489,24 +6484,55 @@ if (typeof window !== 'undefined') {
if (item) { if (item) {
item.remove(); item.remove();
// Re-index remaining items // Re-index remaining items
// Use data-index for index storage - no need to encode index in onclick strings or IDs
const remainingItems = itemsContainer.querySelectorAll('.array-object-item'); const remainingItems = itemsContainer.querySelectorAll('.array-object-item');
remainingItems.forEach((itemEl, newIndex) => { remainingItems.forEach((itemEl, newIndex) => {
itemEl.setAttribute('data-index', newIndex); itemEl.setAttribute('data-index', newIndex);
// Update all inputs within this item - need to update name/id attributes // Update all inputs within this item - only update index in array bracket notation
itemEl.querySelectorAll('input, select, textarea').forEach(input => { itemEl.querySelectorAll('input, select, textarea').forEach(input => {
const name = input.getAttribute('name') || input.id; const name = input.getAttribute('name');
const id = input.id;
if (name) { if (name) {
// Update name/id attribute with new index // Only replace index in bracket notation like [0], [1], etc.
const newName = name.replace(/\[\d+\]/, `[${newIndex}]`); // Match pattern: field_name[index] but not field_name123
if (input.getAttribute('name')) input.setAttribute('name', newName); const newName = name.replace(/\[(\d+)\]/, `[${newIndex}]`);
if (input.id) input.id = input.id.replace(/\d+/, newIndex); input.setAttribute('name', newName);
}
if (id) {
// Only update index in specific patterns like _item_0, _item_1
// Match pattern: _item_<digits> but be careful not to break other numeric IDs
const newId = id.replace(/_item_(\d+)/, `_item_${newIndex}`);
input.id = newId;
} }
}); });
// Update button onclick attributes // Update button onclick attributes - only update the index parameter
// Since we use data-index for tracking, we can compute index from closest('.array-object-item')
// For now, update onclick strings but be more careful with the regex
itemEl.querySelectorAll('button[onclick]').forEach(button => { itemEl.querySelectorAll('button[onclick]').forEach(button => {
const onclick = button.getAttribute('onclick'); const onclick = button.getAttribute('onclick');
if (onclick) { if (onclick) {
button.setAttribute('onclick', onclick.replace(/\d+/, newIndex)); // Match patterns like:
// removeArrayObjectItem('fieldId', 0)
// handleArrayObjectFileUpload(event, 'fieldId', 0, 'propKey', 'pluginId')
// removeArrayObjectFile('fieldId', 0, 'propKey')
// Only replace the numeric index parameter (second or third argument depending on function)
let newOnclick = onclick;
// For removeArrayObjectItem('fieldId', index) - second param
newOnclick = newOnclick.replace(
/removeArrayObjectItem\s*\(\s*['"]([^'"]+)['"]\s*,\s*\d+\s*\)/g,
`removeArrayObjectItem('$1', ${newIndex})`
);
// For handleArrayObjectFileUpload(event, 'fieldId', index, ...) - third param
newOnclick = newOnclick.replace(
/handleArrayObjectFileUpload\s*\(\s*event\s*,\s*['"]([^'"]+)['"]\s*,\s*\d+\s*,/g,
`handleArrayObjectFileUpload(event, '$1', ${newIndex},`
);
// For removeArrayObjectFile('fieldId', index, ...) - second param
newOnclick = newOnclick.replace(
/removeArrayObjectFile\s*\(\s*['"]([^'"]+)['"]\s*,\s*\d+\s*,/g,
`removeArrayObjectFile('$1', ${newIndex},`
);
button.setAttribute('onclick', newOnclick);
} }
}); });
}); });
@@ -6514,12 +6540,17 @@ if (typeof window !== 'undefined') {
// Update add button state // Update add button state
const addButton = itemsContainer.nextElementSibling; const addButton = itemsContainer.nextElementSibling;
if (addButton) { if (addButton && addButton.getAttribute('onclick')) {
const maxItems = parseInt(addButton.getAttribute('onclick').match(/\d+/)[0]); // Extract maxItems from onclick attribute more safely
if (remainingItems.length < maxItems) { // Pattern: addArrayObjectItem('fieldId', 'fullKey', maxItems)
addButton.disabled = false; const onclickMatch = addButton.getAttribute('onclick').match(/addArrayObjectItem\s*\([^,]+,\s*[^,]+,\s*(\d+)\)/);
addButton.style.opacity = '1'; if (onclickMatch && onclickMatch[1]) {
addButton.style.cursor = 'pointer'; const maxItems = parseInt(onclickMatch[1]);
if (remainingItems.length < maxItems) {
addButton.disabled = false;
addButton.style.opacity = '1';
addButton.style.cursor = 'pointer';
}
} }
} }
} }