fix: array-table boolean columns always saved as unchecked (#381)

Reported bug: countdown plugin always shows "No Active" even with a
countdown configured and enabled — because every save silently unchecked
it. Root cause: the boolean column's hidden-sentinel input (needed so
unchecked checkboxes still submit "false", since browsers omit unchecked
checkboxes entirely) was hardcoded to value="false" and shared the same
`name` as the checkbox, with no sync between them. Whichever of the two
same-named inputs the save request's form-collection happens to prefer,
the hidden's stale "false" could silently override an actually-checked
box on every save, independent of what the user set.

Fixed in both places this pattern is rendered:
- plugin_config.html: the initial server-rendered row for every array-table
  boolean column (affects countdown's per-countdown "enabled" and the
  custom-feeds widget's per-feed "enabled") — now sets the hidden's initial
  value from the actual data, and syncs it via onchange on every toggle.
- array-table.js: the client-side row renderer used when adding/rebuilding
  rows in the browser — same fix, keeping both inputs in sync at render
  time and via a change listener.

Verified other checkbox/hidden-input patterns in plugin_config.html and
confirmed they're unrelated/already-safe: the default single-checkbox
renderer (no hidden pair), checkbox-group (array-bracket names with its
own explicit sync), the array-table advanced-props modal editor (single
hidden per name, explicitly written on Save), the top-level plugin
enable/disable toggle (separate immediate hx-post), and the no-schema
fallback checkbox. custom-feeds.js's own client-side row renderer never
creates a hidden sentinel at all, so it isn't affected either.

Verified the fix's rendered output directly with an isolated Jinja render
of the exact snippet: hidden and checkbox now agree ("true"/checked or
"false"/unchecked) for both states, where before the hidden was always
"false" regardless of the actual value.


Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-07-06 08:05:06 -04:00
committed by GitHub
co-authored by Claude Sonnet 5
parent cbb8ec41e8
commit deaa3d7a98
2 changed files with 22 additions and 6 deletions
@@ -174,11 +174,16 @@
cell.style.verticalAlign = 'middle'; cell.style.verticalAlign = 'middle';
if (colType === 'boolean') { if (colType === 'boolean') {
// Boolean: hidden sentinel + visible checkbox // Boolean: hidden sentinel + visible checkbox, same `name` (so
// unchecked boxes still submit "false"). Keep the hidden's value
// synced to the checkbox at all times — some form-collection
// paths prefer whichever of the two same-named inputs comes
// first/last in the DOM, and a stale hidden value previously
// caused this field to silently revert to false on every save.
const hidden = document.createElement('input'); const hidden = document.createElement('input');
hidden.type = 'hidden'; hidden.type = 'hidden';
hidden.name = inputName; hidden.name = inputName;
hidden.value = 'false'; hidden.value = String(Boolean(colValue));
cell.appendChild(hidden); cell.appendChild(hidden);
const cb = document.createElement('input'); const cb = document.createElement('input');
@@ -187,6 +192,9 @@
cb.checked = Boolean(colValue); cb.checked = Boolean(colValue);
cb.value = 'true'; cb.value = 'true';
cb.className = 'h-4 w-4 text-blue-600'; cb.className = 'h-4 w-4 text-blue-600';
cb.addEventListener('change', () => {
hidden.value = String(cb.checked);
});
cell.appendChild(cb); cell.appendChild(cb);
} else if (colType === 'integer' || colType === 'number') { } else if (colType === 'integer' || colType === 'number') {
@@ -450,11 +450,12 @@
</div> </div>
</td> </td>
<td class="px-4 py-3 whitespace-nowrap text-center"> <td class="px-4 py-3 whitespace-nowrap text-center">
<input type="hidden" name="{{ full_key }}.{{ item_index }}.enabled" value="false"> <input type="hidden" name="{{ full_key }}.{{ item_index }}.enabled" value="{{ 'true' if item.get('enabled', true) else 'false' }}">
<input type="checkbox" <input type="checkbox"
name="{{ full_key }}.{{ item_index }}.enabled" name="{{ full_key }}.{{ item_index }}.enabled"
{% if item.get('enabled', true) %}checked{% endif %} {% if item.get('enabled', true) %}checked{% endif %}
value="true" value="true"
onchange="this.previousElementSibling.value = this.checked ? 'true' : 'false'"
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">
@@ -549,11 +550,18 @@
{% else %}{% set td_min_w = '110px' %}{% endif %} {% else %}{% set td_min_w = '110px' %}{% endif %}
<td class="px-3 py-3 whitespace-nowrap" style="min-width:{{ td_min_w }};vertical-align:middle"> <td class="px-3 py-3 whitespace-nowrap" style="min-width:{{ td_min_w }};vertical-align:middle">
{% if col_type == 'boolean' %} {% if col_type == 'boolean' %}
<input type="hidden" name="{{ full_key }}.{{ item_index }}.{{ col_name }}" value="false"> {# Hidden sentinel ensures unchecked boxes still submit "false" (browsers
omit unchecked checkboxes entirely). It shares the checkbox's `name`,
so it must always mirror the checkbox's actual state — both here at
render time and on every toggle — or whichever of the two same-named
inputs the save request happens to prefer can silently revert this
field to false regardless of what's checked. #}
<input type="hidden" name="{{ full_key }}.{{ item_index }}.{{ col_name }}" value="{{ 'true' if col_value else 'false' }}">
<input type="checkbox" <input type="checkbox"
name="{{ full_key }}.{{ item_index }}.{{ col_name }}" name="{{ full_key }}.{{ item_index }}.{{ col_name }}"
{% if col_value %}checked{% endif %} {% if col_value %}checked{% endif %}
value="true" value="true"
onchange="this.previousElementSibling.value = this.checked ? 'true' : 'false'"
class="h-4 w-4 text-blue-600"> class="h-4 w-4 text-blue-600">
{% elif col_type == 'integer' or col_type == 'number' %} {% elif col_type == 'integer' or col_type == 'number' %}
<input type="number" <input type="number"