fix(web): two plugin config saves that always returned 400 (geochron, news) (#575)

* fix(web): render widget-less arrays of objects as a table, not comma text

An array of objects with no x-widget (geochron's `cities`) fell through to
the comma-separated text input. Jinja joined each item as a Python dict
repr, the save route read them back as a list of strings, and the schema
rejected them -- so every save of the plugin returned 400 "Configuration
validation failed", whatever setting was changed.

Default such arrays to the existing array-table widget, which already
edits arrays of objects and posts `field.N.key` inputs the save route
rebuilds into a list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(web): don't leave an empty object stub in array items on save

The unchecked-checkbox pass walked into every nested object of an array
item looking for booleans, creating it when absent. A news custom feed
with no logo came out with `logo: {}`, which fails the logo's
`required: [id, path]`, so every save of the news plugin returned 400.

Recurse into a scratch dict instead and attach it only if a boolean was
actually set in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-14 13:04:11 -04:00
committed by GitHub
co-authored by Claude Opus 5
parent bc60b41445
commit 11bf39cd66
3 changed files with 219 additions and 5 deletions
+10 -4
View File
@@ -1131,14 +1131,20 @@ def _set_missing_booleans_to_false(plugin_config, schema_props, form_keys, prefi
elif prop_type == 'object' and 'properties' in prop_schema:
# Recurse into nested objects
if config_node is not None:
# Inside an array item — ensure nested dict exists in item
if prop_name not in node or not isinstance(node[prop_name], dict):
node[prop_name] = {}
# Inside an array item. Walk into the existing dict, or a scratch
# one that is attached only if a boolean actually landed in it:
# an optional object the item never had (a custom feed with no
# logo) must stay absent, because an empty stub fails the
# object's own `required` list and 400s every save.
existing = node.get(prop_name)
child = existing if isinstance(existing, dict) else {}
_set_missing_booleans_to_false(
plugin_config, prop_schema['properties'], form_keys, full_path,
config_node=node[prop_name],
config_node=child,
sections=sections, submitted_parents=submitted_parents
)
if child is not existing and child:
node[prop_name] = child
else:
_set_missing_booleans_to_false(
plugin_config, prop_schema['properties'], form_keys, full_path,
@@ -315,7 +315,14 @@
{# Array - check for file upload widget first (to avoid breaking static-image plugin), then checkbox-group, then array of objects #}
{% elif field_type == 'array' %}
{% set x_widget = prop.get('x-widget') or prop.get('x_widget') %}
{# An array of objects with no widget named would otherwise reach the
comma-separated text input, which joins each item as a Python dict
repr and posts it back as a list of strings -- the schema rejects
that and every save of the plugin returns 400. The table widget
already edits any array of objects, so make it the default. #}
{% set _items = prop.get('items') or {} %}
{% set x_widget = prop.get('x-widget') or prop.get('x_widget')
or ('array-table' if (_items.get('type') == 'object' and _items.get('properties')) else none) %}
{% if x_widget == 'file-upload' %}
{# File upload widget for arrays #}
{% set upload_config = prop.get('x-upload-config') or {} %}