fix(editor): asset/logger/position menus broken by TDZ ReferenceError in oneditprepare
The previous oneditprepare ran applyMode(initialMode) early in the function, which called validateChannelsJson(), which referenced const declarations (channelsArea, channelsHint) that were declared later in the same function. JavaScript hoists const into the Temporal Dead Zone, so accessing them before the declaration line throws a ReferenceError. That uncaught throw aborted the rest of oneditprepare — including the waitForMenuData() call that initialises the asset / logger / position menu placeholders. Symptom for the user: opening a measurement node in the editor showed Mode + analog fields but the asset menu was empty. Fixes: 1. Move waitForMenuData() to the very top of oneditprepare so the shared menu init is independent of any later mode-block work. Even if the mode logic ever throws again, the asset / logger / position menus still render. 2. Resolve every DOM reference (modeSelect, analogBlock, digitalBlock, modeHint, channelsArea, channelsHint) at the top of the function before any helper that touches them is invoked. validateChannelsJson and applyMode now read closed-over names that are guaranteed to be initialised. 3. Guard applyMode(initialMode) with try/catch as defense in depth and add null-checks on every DOM reference. A future template change that drops one of the IDs will only no-op rather than break the editor. No runtime change. 71/71 tests still green. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -74,44 +74,50 @@
|
|||||||
oneditprepare: function() {
|
oneditprepare: function() {
|
||||||
const node = this;
|
const node = this;
|
||||||
|
|
||||||
// === Mode selector — TOP-LEVEL hierarchy ===
|
// === Asset / logger / position placeholders (dynamic menus) ===
|
||||||
// The Input Mode drives whether analog-pipeline fields or the
|
// Kick these off FIRST so that any error in the downstream mode
|
||||||
// digital channels editor are shown. Initialize the <select> from
|
// logic can never block the shared menus. Historical regression:
|
||||||
// the saved node value, fall back to 'analog' for legacy nodes
|
// a ReferenceError in the mode block aborted oneditprepare and
|
||||||
// that were saved before the mode field existed.
|
// stopped the asset menu from rendering at all.
|
||||||
const modeSelect = document.getElementById('node-input-mode');
|
const waitForMenuData = () => {
|
||||||
const initialMode = (node.mode === 'digital' || node.mode === 'analog') ? node.mode : 'analog';
|
if (window.EVOLV?.nodes?.measurement?.initEditor) {
|
||||||
modeSelect.value = initialMode;
|
window.EVOLV.nodes.measurement.initEditor(node);
|
||||||
|
} else {
|
||||||
|
setTimeout(waitForMenuData, 50);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
waitForMenuData();
|
||||||
|
|
||||||
|
// IMPORTANT: all DOM references are resolved up front so helper
|
||||||
|
// functions called during initial applyMode() don't trip over the
|
||||||
|
// Temporal Dead Zone on later `const` declarations.
|
||||||
|
|
||||||
|
const modeSelect = document.getElementById('node-input-mode');
|
||||||
const analogBlock = document.getElementById('analog-only-fields');
|
const analogBlock = document.getElementById('analog-only-fields');
|
||||||
const digitalBlock = document.getElementById('digital-only-fields');
|
const digitalBlock = document.getElementById('digital-only-fields');
|
||||||
const modeHint = document.getElementById('mode-hint');
|
const modeHint = document.getElementById('mode-hint');
|
||||||
|
|
||||||
function applyMode(mode) {
|
|
||||||
const isDigital = mode === 'digital';
|
|
||||||
analogBlock.style.display = isDigital ? 'none' : 'block';
|
|
||||||
digitalBlock.style.display = isDigital ? 'block' : 'none';
|
|
||||||
if (modeHint) {
|
|
||||||
modeHint.textContent = isDigital
|
|
||||||
? 'msg.payload must be an OBJECT, e.g. {"temperature": 22.5, "humidity": 45}. Define each key below.'
|
|
||||||
: 'msg.payload must be a NUMBER (or numeric string). Configure scaling/smoothing below.';
|
|
||||||
}
|
|
||||||
validateChannelsJson();
|
|
||||||
}
|
|
||||||
|
|
||||||
modeSelect.addEventListener('change', (e) => applyMode(e.target.value));
|
|
||||||
applyMode(initialMode);
|
|
||||||
|
|
||||||
// === Channels JSON live validation (digital only) ===
|
|
||||||
const channelsArea = document.getElementById('node-input-channels');
|
const channelsArea = document.getElementById('node-input-channels');
|
||||||
const channelsHint = document.getElementById('channels-validation');
|
const channelsHint = document.getElementById('channels-validation');
|
||||||
|
|
||||||
|
// Initialise the mode <select> from the saved node.mode. Legacy
|
||||||
|
// nodes (saved before the mode field existed) fall back to
|
||||||
|
// 'analog' so they keep behaving exactly like before.
|
||||||
|
const initialMode = (node.mode === 'digital' || node.mode === 'analog') ? node.mode : 'analog';
|
||||||
|
if (modeSelect) modeSelect.value = initialMode;
|
||||||
|
|
||||||
|
// Populate the channels textarea from the saved node.channels
|
||||||
|
// (stored as a raw JSON string; parsing happens server-side).
|
||||||
if (channelsArea && typeof node.channels === 'string') {
|
if (channelsArea && typeof node.channels === 'string') {
|
||||||
channelsArea.value = node.channels;
|
channelsArea.value = node.channels;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateChannelsJson() {
|
function validateChannelsJson() {
|
||||||
if (!channelsHint) return;
|
if (!channelsHint) return;
|
||||||
if (modeSelect.value !== 'digital') { channelsHint.textContent = ''; return; }
|
if (!modeSelect || modeSelect.value !== 'digital') {
|
||||||
const raw = (channelsArea.value || '').trim();
|
channelsHint.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const raw = (channelsArea && channelsArea.value || '').trim();
|
||||||
if (!raw || raw === '[]') {
|
if (!raw || raw === '[]') {
|
||||||
channelsHint.innerHTML = '<span style="color:#b45309;">Digital mode with no channels — no measurements will be emitted.</span>';
|
channelsHint.innerHTML = '<span style="color:#b45309;">Digital mode with no channels — no measurements will be emitted.</span>';
|
||||||
return;
|
return;
|
||||||
@@ -120,7 +126,7 @@
|
|||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
if (!Array.isArray(parsed)) throw new Error('must be an array');
|
if (!Array.isArray(parsed)) throw new Error('must be an array');
|
||||||
const missing = parsed
|
const missing = parsed
|
||||||
.map((c, i) => (c && c.key && c.type ? null : `entry ${i}: missing key or type`))
|
.map((c, i) => (c && c.key && c.type ? null : 'entry ' + i + ': missing key or type'))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
if (missing.length) {
|
if (missing.length) {
|
||||||
channelsHint.innerHTML = '<span style="color:#b45309;">' + missing.join('; ') + '</span>';
|
channelsHint.innerHTML = '<span style="color:#b45309;">' + missing.join('; ') + '</span>';
|
||||||
@@ -131,17 +137,24 @@
|
|||||||
channelsHint.innerHTML = '<span style="color:#b91c1c;">Invalid JSON: ' + e.message + '</span>';
|
channelsHint.innerHTML = '<span style="color:#b91c1c;">Invalid JSON: ' + e.message + '</span>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (channelsArea) channelsArea.addEventListener('input', validateChannelsJson);
|
|
||||||
|
|
||||||
// === Asset / logger / position placeholders (dynamic menus) ===
|
function applyMode(mode) {
|
||||||
const waitForMenuData = () => {
|
const isDigital = mode === 'digital';
|
||||||
if (window.EVOLV?.nodes?.measurement?.initEditor) {
|
if (analogBlock) analogBlock.style.display = isDigital ? 'none' : 'block';
|
||||||
window.EVOLV.nodes.measurement.initEditor(node);
|
if (digitalBlock) digitalBlock.style.display = isDigital ? 'block' : 'none';
|
||||||
} else {
|
if (modeHint) {
|
||||||
setTimeout(waitForMenuData, 50);
|
modeHint.textContent = isDigital
|
||||||
|
? 'msg.payload must be an OBJECT, e.g. {"temperature": 22.5, "humidity": 45}. Define each key below.'
|
||||||
|
: 'msg.payload must be a NUMBER (or numeric string). Configure scaling/smoothing below.';
|
||||||
|
}
|
||||||
|
validateChannelsJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modeSelect) modeSelect.addEventListener('change', (e) => applyMode(e.target.value));
|
||||||
|
if (channelsArea) channelsArea.addEventListener('input', validateChannelsJson);
|
||||||
|
try { applyMode(initialMode); } catch (e) {
|
||||||
|
console.error('measurement: applyMode failed', e);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
waitForMenuData();
|
|
||||||
|
|
||||||
// === Smoothing method dropdown (analog only) ===
|
// === Smoothing method dropdown (analog only) ===
|
||||||
const smoothMethodSelect = document.getElementById('node-input-smooth_method');
|
const smoothMethodSelect = document.getElementById('node-input-smooth_method');
|
||||||
|
|||||||
Reference in New Issue
Block a user