fix: Use JSON encoding for bracket-notation arrays and add sentinel for clearing

Fix bracket-notation array handling to prevent data loss:

1. Use JSON encoding instead of comma-join (lines 3358-3359):
   - Comma-join breaks if option values contain commas
   - Switch to json.dumps() to encode array values as JSON strings
   - _parse_form_value_with_schema() already handles JSON arrays correctly
   - Preserves values with commas, special characters, etc.

2. Add sentinel hidden input for clearing arrays:
   - Add hidden input with name="field[]" value="" in checkbox-group template
   - Ensures field is always submitted, even when all checkboxes unchecked
   - Backend filters out sentinel empty strings to detect empty array
   - Allows users to clear array to [] by unchecking all options

3. Update backend to handle sentinel:
   - Filter out sentinel empty strings from bracket notation values
   - Empty array (all unchecked) is represented as "[]" JSON string
   - Properly handles both sentinel-only (empty array) and sentinel+values cases

This fixes data loss when:
- Option values contain commas (comma-join corruption)
- All checkboxes are unchecked (field omitted from form, can't clear to [])
This commit is contained in:
Chuck
2026-01-11 13:40:33 -05:00
parent 31faac6052
commit 550ab42f9a
3 changed files with 23 additions and 8 deletions

View File

@@ -3337,25 +3337,34 @@ def save_plugin_config():
# First pass: handle bracket notation array fields (e.g., "field_name[]" from checkbox-group)
# These fields use getlist() to preserve all values, then replace in form_data
# Sentinel empty value ("") allows clearing array to [] when all checkboxes unchecked
bracket_array_fields = {} # Maps base field path to list of values
for key in request.form.keys():
# Check if key ends with "[]" (bracket notation for array fields)
if key.endswith('[]'):
base_path = key[:-2] # Remove "[]" suffix
values = request.form.getlist(key)
if values:
bracket_array_fields[base_path] = values
# Remove the bracket notation key from form_data if present
if key in form_data:
del form_data[key]
# Filter out sentinel empty string - if only sentinel present, array should be []
# If sentinel + values present, use the actual values
filtered_values = [v for v in values if v and v.strip()]
# If no non-empty values but key exists, it means all checkboxes unchecked (empty array)
bracket_array_fields[base_path] = filtered_values
# Remove the bracket notation key from form_data if present
if key in form_data:
del form_data[key]
# Process bracket notation fields and add to form_data as comma-separated strings
# Process bracket notation fields and add to form_data as JSON strings
# Use JSON encoding instead of comma-join to handle values containing commas
import json
for base_path, values in bracket_array_fields.items():
# Get schema property to verify it's an array
base_prop = _get_schema_property(schema, base_path)
if base_prop and base_prop.get('type') == 'array':
# Combine values into comma-separated string for consistent parsing
combined_value = ', '.join(str(v) for v in values if v)
# Filter out empty values and sentinel empty strings
filtered_values = [v for v in values if v and v.strip()]
# Encode as JSON array string (handles values with commas correctly)
# Empty array (all unchecked) is represented as "[]"
combined_value = json.dumps(filtered_values)
form_data[base_path] = combined_value
logger.debug(f"Processed bracket notation array field {base_path}: {values} -> {combined_value}")

View File

@@ -3034,6 +3034,9 @@ function generateFieldHtml(key, prop, value, prefix = '') {
html += `</div>`;
// Hidden input to store selected values as JSON array (like array-of-objects pattern)
html += `<input type="hidden" id="${fieldId}_data" name="${fullKey}_data" value='${JSON.stringify(arrayValue).replace(/'/g, "&#39;")}'>`;
// Sentinel hidden input with bracket notation to allow clearing array to [] when all unchecked
// This ensures the field is always submitted, even when all checkboxes are unchecked
html += `<input type="hidden" name="${fullKey}[]" value="">`;
} else if (xWidgetValue === 'custom-feeds' || xWidgetValue2 === 'custom-feeds') {
// Custom feeds widget - check schema validation first
const itemsSchema = prop.items || {};

View File

@@ -180,6 +180,9 @@
</div>
{# Hidden input to store selected values as JSON array (like array-of-objects pattern) #}
<input type="hidden" id="{{ field_id }}_data" name="{{ full_key }}_data" value='{{ array_value|tojson|safe }}'>
{# Sentinel hidden input with bracket notation to allow clearing array to [] when all unchecked #}
{# This ensures the field is always submitted, even when all checkboxes are unchecked #}
<input type="hidden" name="{{ full_key }}[]" value="">
{% else %}
{# Check for custom-feeds widget first #}
{% set items_schema = prop.get('items') or {} %}