From fb8d5c03e604bff5d84e08f28855863f96243b47 Mon Sep 17 00:00:00 2001 From: znetsixe Date: Mon, 13 Apr 2026 14:15:06 +0200 Subject: [PATCH] fix(editor): asset/logger/position menus broken by TDZ ReferenceError in oneditprepare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- measurement.html | 87 ++++++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/measurement.html b/measurement.html index a80bc50..31c5594 100644 --- a/measurement.html +++ b/measurement.html @@ -74,44 +74,50 @@ oneditprepare: function() { const node = this; - // === Mode selector — TOP-LEVEL hierarchy === - // The Input Mode drives whether analog-pipeline fields or the - // digital channels editor are shown. Initialize the 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; - modeSelect.addEventListener('change', (e) => applyMode(e.target.value)); - applyMode(initialMode); - - // === Channels JSON live validation (digital only) === - const channelsArea = document.getElementById('node-input-channels'); - const channelsHint = document.getElementById('channels-validation'); + // 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') { channelsArea.value = node.channels; } + function validateChannelsJson() { if (!channelsHint) return; - if (modeSelect.value !== 'digital') { channelsHint.textContent = ''; return; } - const raw = (channelsArea.value || '').trim(); + if (!modeSelect || modeSelect.value !== 'digital') { + channelsHint.textContent = ''; + return; + } + const raw = (channelsArea && channelsArea.value || '').trim(); if (!raw || raw === '[]') { channelsHint.innerHTML = 'Digital mode with no channels — no measurements will be emitted.'; return; @@ -120,7 +126,7 @@ const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) throw new Error('must be an array'); 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); if (missing.length) { channelsHint.innerHTML = '' + missing.join('; ') + ''; @@ -131,17 +137,24 @@ channelsHint.innerHTML = 'Invalid JSON: ' + e.message + ''; } } - if (channelsArea) channelsArea.addEventListener('input', validateChannelsJson); - // === Asset / logger / position placeholders (dynamic menus) === - const waitForMenuData = () => { - if (window.EVOLV?.nodes?.measurement?.initEditor) { - window.EVOLV.nodes.measurement.initEditor(node); - } else { - setTimeout(waitForMenuData, 50); + function applyMode(mode) { + const isDigital = mode === 'digital'; + if (analogBlock) analogBlock.style.display = isDigital ? 'none' : 'block'; + if (digitalBlock) 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.'; } - }; - waitForMenuData(); + 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); + } // === Smoothing method dropdown (analog only) === const smoothMethodSelect = document.getElementById('node-input-smooth_method');