feat(web): honour x-display: hidden in plugin settings (#585)

* feat(web): honour x-display: hidden in plugin settings

Plugins keep deprecated and internal keys declared so stored configs keep
validating (weather api_key/radar_zoom, countdown's auto-generated row id),
but the settings form drew them as live controls.

A property marked "x-display": "hidden" -- or an object whose children are
all hidden -- now gets no control at any depth: top level, nested sections,
Advanced Settings (not counted either), array-table columns and the row
editor. A hidden top-level key is not reported in __rendered_section.

Saving never changes a hidden value. Plain and nested fields aren't posted,
so the save's deep merge keeps them; _set_missing_booleans_to_false skips
hidden booleans at every depth. A posted array row replaces the stored item,
so hidden row properties are carried as JSON-encoded hidden inputs and
decoded exactly on save (an id "1" stays a string). New rows get none.
JSON API saves are unchanged.

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

* refactor(web): read hidden row keys without dynamic property access

Build the set of x-display: hidden item properties once and look values up
through Object.entries, instead of indexing objects by a variable key on
the lines this branch added (Codacy: object injection sink, 6 warnings).
Behaviour is unchanged.

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-15 15:47:06 -04:00
committed by GitHub
co-authored by Claude Opus 5
parent 8360220809
commit fddb0e06db
8 changed files with 826 additions and 11 deletions
@@ -111,6 +111,37 @@
// ─── Helpers ────────────────────────────────────────────────────────────
/**
* "x-display": "hidden" — the property stays in the schema but gets no
* control. An object whose every child is hidden counts as hidden too.
* Mirrors prop_is_hidden in plugin_config.html and _is_hidden_prop on the
* server.
*/
function isHiddenProp(schema) {
if (!schema || typeof schema !== 'object') return false;
if (schema['x-display'] === 'hidden') return true;
const children = schema.properties;
if (children && typeof children === 'object') {
const values = Object.values(children);
return values.length > 0 && values.every(isHiddenProp);
}
return false;
}
/**
* A hidden input carrying a hidden property's stored value, JSON-encoded so
* the save restores its exact type. A posted row replaces the stored item,
* so a value that isn't posted is lost.
*/
function hiddenValueInput(name, dataKey, value) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = name;
input.value = JSON.stringify(value);
input.dataset.hiddenProp = dataKey;
return input;
}
function safeSetHTML(target, html) {
target.textContent = '';
// createContextualFragment parses html relative to the document context
@@ -343,6 +374,16 @@
if (propType === 'object' && propSchema.properties) {
const nestedVal = (item && item[propName]) || {};
Object.entries(propSchema.properties).forEach(([subName, subSchema]) => {
if (isHiddenProp(subSchema)) {
// No data-nested-prop, so the row editor never shows it.
const stored = Object.entries(nestedVal).find(([name]) => name === subName);
if (stored && stored[1] !== undefined && stored[1] !== null) {
cell.appendChild(hiddenValueInput(
`${fullKey}.${index}.${propName}.${subName}`,
`${propName}.${subName}`, stored[1]));
}
return;
}
const subType = Array.isArray(subSchema.type)
? subSchema.type.find(t => t !== 'null') || 'string'
: (subSchema.type || 'string');
@@ -386,7 +427,16 @@
row.className = 'array-table-row';
row.setAttribute('data-index', index);
// "x-display": "hidden" item properties: never a column (even if
// x-columns names one), never in the advanced cell or row editor.
const hiddenKeys = new Set(
Object.entries(fullItemProperties)
.filter(([, propSchema]) => isHiddenProp(propSchema))
.map(([propName]) => propName)
);
// Visible column cells
displayColumns = displayColumns.filter(colName => !hiddenKeys.has(colName));
displayColumns.forEach(colName => {
const colDef = itemProperties[colName] || {};
const colType = Array.isArray(colDef.type) ? colDef.type.find(t => t !== 'null') || 'string' : (colDef.type || 'string');
@@ -396,10 +446,11 @@
row.appendChild(createCell(fullKey, index, colName, colDef, colValue, pluginId));
});
// Determine non-displayed properties (these go into the advanced cell + edit modal)
// Determine non-displayed properties (these go into the advanced cell + edit modal).
// Hidden ones ("x-display": "hidden") get no column and no editor field.
const nonDisplayed = {};
Object.keys(fullItemProperties).forEach(k => {
if (!displayColumns.includes(k) && k !== 'id') {
if (!displayColumns.includes(k) && k !== 'id' && !hiddenKeys.has(k)) {
nonDisplayed[k] = fullItemProperties[k];
}
});
@@ -430,6 +481,14 @@
actionsCell.appendChild(editBtn);
}
// Hidden properties: carry stored values only. A new row has none, so
// nothing is invented (countdown fills in its own id).
Object.entries(item).forEach(([propName, stored]) => {
if (hiddenKeys.has(propName) && stored !== undefined && stored !== null) {
actionsCell.appendChild(hiddenValueInput(`${fullKey}.${index}.${propName}`, propName, stored));
}
});
row.appendChild(actionsCell);
// Hidden advanced data cell
@@ -494,7 +553,8 @@
grid.className = 'grid grid-cols-2 gap-3';
Object.entries(propSchema.properties).forEach(([subName, subSchema]) => {
const subType = Array.isArray(subSchema.type) ? subSchema.type.find(t => t !== 'null') || 'string' : (subSchema.type || 'string');
if (isHiddenProp(subSchema)) return;
const subType =Array.isArray(subSchema.type) ? subSchema.type.find(t => t !== 'null') || 'string' : (subSchema.type || 'string');
const subLabel = subSchema.title || subName.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
const subDesc = subSchema.description || '';
const nestedPath = `${propName}.${subName}`;
@@ -2409,6 +2409,19 @@ function getSchemaProperty(schema, path) {
return null;
}
// "x-display": "hidden" (or an object of nothing but hidden children).
// Mirrors prop_is_hidden in plugin_config.html.
function isHiddenSchemaProp(schema) {
if (!schema || typeof schema !== 'object') return false;
if (schema['x-display'] === 'hidden') return true;
const children = schema.properties;
if (children && typeof children === 'object') {
const values = Object.values(children);
return values.length > 0 && values.every(isHiddenSchemaProp);
}
return false;
}
// Helper function to render a single item in an array of objects
function renderArrayObjectItem(fieldId, fullKey, itemProperties, itemValue, index, itemsSchema) {
const item = itemValue || {};
@@ -2422,6 +2435,9 @@ function renderArrayObjectItem(fieldId, fullKey, itemProperties, itemValue, inde
const propertyOrder = itemsSchema['x-propertyOrder'] || Object.keys(itemProperties);
propertyOrder.forEach(propKey => {
if (!itemProperties[propKey]) return;
// "x-display": "hidden": no control. The stored value survives through
// data-item-data, which updateArrayObjectData overlays edits onto.
if (isHiddenSchemaProp(itemProperties[propKey])) return;
const propSchema = itemProperties[propKey];
const propValue = item[propKey] !== undefined ? item[propKey] : propSchema.default;
@@ -5478,6 +5494,7 @@ if (typeof window !== 'undefined') {
itemHtml = `<div class="border border-gray-300 rounded-lg p-4 bg-gray-50 array-object-item" data-index="${newIndex}">`;
Object.keys(itemsSchema.properties || {}).forEach(propKey => {
const propSchema = itemsSchema.properties[propKey];
if (propSchema && propSchema['x-display'] === 'hidden') return;
const propValue = newItem[propKey] !== undefined ? newItem[propKey] : propSchema.default;
const propLabel = propSchema.title || propKey.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
itemHtml += `<div class="mb-3"><label class="block text-sm font-medium text-gray-700 mb-1">${escapeHtml(propLabel)}</label>`;