fix(web): style editor no longer strands leaf-valued layout fields

A prior fix on this PR made elementKeys() append any layout-only key with
no style block of its own (a logo, a timeout indicator, a possession
arrow), so table() draws a row for it instead of losing it when the
wholesale `layout` claim removes the generic fallback. That covers a
layout-only key shaped like an object (x_offset/y_offset, ...), because
columnsFor() only ever produced columns from a key's *sub-fields*.

It missed the case where the layout-only key's own value is itself a
leaf -- a plain "show_logo" boolean directly under layout, no x/y object
underneath. elementKeys() still lists it (any row: no matching column),
so it renders as an uneditable blank row and its only control -- the
generic fallback checkbox -- is still gone. Confirmed by executing the
real widget's render() against a synthetic schema in Node (a DOM-stub
harness, not committed): the field's name never appeared as an <input>.

columnsFor() now gives such a leaf key a column keyed to itself
('layout-leaf'), and elementRow() binds it to the leaf's own path
(customization.layout.<key>, matching the name the fallback would have
used) instead of leaving every cell blank.

New regression test (test/js/unit/test_style_editor_layout_leaf_columns.js,
following this PR's existing eval-extraction pattern) checks the leaf
column is produced, is self-keyed, doesn't duplicate, and that a schema
with no leaf-valued layout key is unaffected; wired into run_all.js and
the suite table in test/js/README.md.

test/js/run_all.js: 84 + 6 + 6 = all suites passed (jsdom unavailable
here, DOM suites skip as before). Python suite untouched by this change;
test_style_editor_extra_fields.py, test_style_editor_save_roundtrip.py
and the one PIL-dependent style_editor_takeover.py case fail identically
before this commit -- missing flask/PIL in this sandbox, not this PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-09-14 03:36:14 +00:00
parent 75e6b2b20c
commit 324a7eac87
4 changed files with 119 additions and 10 deletions
@@ -382,9 +382,12 @@
/**
* The columns a table needs, derived from the schema rather than fixed.
*
* Two sources: the sub-fields elements declare (font, font_size,
* text_color, visible, align) and the sub-fields their layout blocks
* declare (x_offset, y_offset, scale).
* Three sources: the sub-fields elements declare (font, font_size,
* text_color, visible, align), the sub-fields their layout blocks
* declare (x_offset, y_offset, scale), and a layout-only key whose own
* value *is* the field to set -- a plain "show_logo" toggle has no x/y
* object underneath it, so it gets a column keyed to itself rather than
* to a shared sub-field name.
*/
function columnsFor(schema) {
var props = schema.properties || {};
@@ -396,8 +399,17 @@
elementKeys(schema).forEach(function (key) {
Object.keys(ownObj(props, key).properties || {}).forEach(
function (f) { seen.set(f, 'element'); });
Object.keys(ownObj(layoutProps, key).properties || {}).forEach(
function (f) { seen.set(f, 'layout'); });
var layoutEntry = own(layoutProps, key);
if (layoutEntry && typeof layoutEntry === 'object' && layoutEntry.properties) {
Object.keys(layoutEntry.properties).forEach(
function (f) { seen.set(f, 'layout'); });
} else if (layoutEntry && !seen.has(key)) {
// layout-only and a leaf: nothing else will share this
// column, but leaving it out drops the field's only control
// the moment the wholesale `layout` claim removes its
// fallback (#569 review).
seen.set(key, 'layout-leaf');
}
});
var known = COLUMN_ORDER.filter(function (f) { return seen.get(f); });
// Anything the schema declares that this file has never heard of
@@ -483,8 +495,13 @@
}
opts.columns.forEach(function (col) {
var isLeaf = col.where === 'layout-leaf';
var inLayout = col.where === 'layout';
var prop = inLayout ? own(axes, col.key) : own(props, col.key);
// A leaf column only applies to the one row named after it --
// every other row leaves it blank, same as an element that
// doesn't declare a shared sub-field.
var prop = isLeaf ? (col.key === key ? own(layoutProps, key) : null)
: inLayout ? own(axes, col.key) : own(props, col.key);
var cell;
if (!prop) {
// This element does not declare that field; keep the grid
@@ -492,13 +509,14 @@
row.appendChild(el('span'));
return;
}
var path = inLayout ? ['layout', key, col.key] : [key, col.key];
var base = inLayout ? opts.layoutPrefix + '.' + key
var path = isLeaf ? ['layout', key]
: inLayout ? ['layout', key, col.key] : [key, col.key];
var base = (isLeaf || inLayout) ? opts.layoutPrefix + '.' + key
: opts.prefix + '.' + key;
var node = control({
key: col.key,
prop: prop,
name: base + '.' + col.key,
name: isLeaf ? base : base + '.' + col.key,
current: effective(value, path, prop, optional),
optional: optional,
fonts: opts.fonts,