mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 08:48:05 +00:00
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:
@@ -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,15 +450,16 @@
|
|||||||
</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">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
onclick="removeCustomFeedRow(this)"
|
onclick="removeCustomFeedRow(this)"
|
||||||
class="text-red-600 hover:text-red-800 px-2 py-1">
|
class="text-red-600 hover:text-red-800 px-2 py-1">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user