// asset.js const fs = require('fs'); const path = require('path'); class AssetMenu { /** Define path where to find data of assets in constructor for now */ constructor(relPath = '../../datasets/assetData') { this.baseDir = path.resolve(__dirname, relPath); this.assetData = this._loadJSON('assetData'); } _loadJSON(...segments) { const filePath = path.resolve(this.baseDir, ...segments) + '.json'; try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch (err) { throw new Error(`Failed to load ${filePath}: ${err.message}`); } } /** * ADD THIS METHOD * Compiles all menu data from the file system into a single nested object. * This is run once on the server to pre-load everything. * @returns {object} A comprehensive object with all menu options. */ getAllMenuData() { // load the raw JSON once const data = this._loadJSON('assetData'); const allData = {}; data.suppliers.forEach(sup => { allData[sup.name] = {}; sup.categories.forEach(cat => { allData[sup.name][cat.name] = {}; cat.types.forEach(type => { // here: store the full array of model objects, not just names allData[sup.name][cat.name][type.name] = type.models; }); }); }); return allData; } /** * Convert the static initEditor function to a string that can be served to the client * @param {string} nodeName - The name of the node type * @returns {string} JavaScript code as a string */ getClientInitCode(nodeName) { // step 1: get the two helper strings const htmlCode = this.getHtmlInjectionCode(nodeName); const dataCode = this.getDataInjectionCode(nodeName); const eventsCode = this.getEventInjectionCode(nodeName); const saveCode = this.getSaveInjectionCode(nodeName); return ` // --- AssetMenu for ${nodeName} --- window.EVOLV.nodes.${nodeName}.assetMenu = window.EVOLV.nodes.${nodeName}.assetMenu || {}; ${htmlCode} ${dataCode} ${eventsCode} ${saveCode} // wire it all up when the editor loads window.EVOLV.nodes.${nodeName}.assetMenu.initEditor = function(node) { // ------------------ BELOW sequence is important! ------------------------------- console.log('Initializing asset properties for ${nodeName}…'); this.injectHtml(); // load the data and wire up events // this will populate the fields and set up the event listeners this.wireEvents(node); // this will load the initial data into the fields // this is important to ensure the fields are populated correctly this.loadData(node); }; `; } getDataInjectionCode(nodeName) { return ` // Asset Data loader for ${nodeName} window.EVOLV.nodes.${nodeName}.assetMenu.loadData = function(node) { const data = window.EVOLV.nodes.${nodeName}.menuData.asset; const elems = { supplier: document.getElementById('node-input-supplier'), category: document.getElementById('node-input-category'), type: document.getElementById('node-input-assetType'), model: document.getElementById('node-input-model'), unit: document.getElementById('node-input-unit') }; function populate(el, opts, sel) { const old = el.value; el.innerHTML = ''; (opts||[]).forEach(o=>{ const opt = document.createElement('option'); opt.value = o; opt.textContent = o; el.appendChild(opt); }); el.value = sel||""; if(el.value!==old) el.dispatchEvent(new Event('change')); } // initial population populate(elems.supplier, Object.keys(data), node.supplier); }; ` } getEventInjectionCode(nodeName) { return ` // Asset Event wiring for ${nodeName} window.EVOLV.nodes.${nodeName}.assetMenu.wireEvents = function(node) { const data = window.EVOLV.nodes.${nodeName}.menuData.asset; const elems = { supplier: document.getElementById('node-input-supplier'), category: document.getElementById('node-input-category'), type: document.getElementById('node-input-assetType'), model: document.getElementById('node-input-model'), unit: document.getElementById('node-input-unit') }; function populate(el, opts, sel) { const old = el.value; el.innerHTML = ''; (opts||[]).forEach(o=>{ const opt = document.createElement('option'); opt.value = o; opt.textContent = o; el.appendChild(opt); }); el.value = sel||""; if(el.value!==old) el.dispatchEvent(new Event('change')); } elems.supplier.addEventListener('change', ()=>{ populate(elems.category, elems.supplier.value? Object.keys(data[elems.supplier.value]||{}) : [], node.category); }); elems.category.addEventListener('change', ()=>{ const s=elems.supplier.value, c=elems.category.value; populate(elems.type, (s&&c)? Object.keys(data[s][c]||{}) : [], node.assetType); }); elems.type.addEventListener('change', ()=>{ const s=elems.supplier.value, c=elems.category.value, t=elems.type.value; const md = (s&&c&&t)? data[s][c][t]||[] : []; populate(elems.model, md.map(m=>m.name), node.model); }); elems.model.addEventListener('change', ()=>{ const s=elems.supplier.value, c=elems.category.value, t=elems.type.value, m=elems.model.value; const md = (s&&c&&t)? data[s][c][t]||[] : []; const entry = md.find(x=>x.name===m); populate(elems.unit, entry? entry.units : [], node.unit); }); }; ` } /** * Generate HTML template for asset fields */ getHtmlTemplate() { return `

Asset selection


`; } /** * Get client-side HTML injection code */ getHtmlInjectionCode(nodeName) { const htmlTemplate = this.getHtmlTemplate().replace(/`/g, '\\`').replace(/\$/g, '\\$'); return ` // Asset HTML injection for ${nodeName} window.EVOLV.nodes.${nodeName}.assetMenu.injectHtml = function() { const placeholder = document.getElementById('asset-fields-placeholder'); if (placeholder && !placeholder.hasChildNodes()) { placeholder.innerHTML = \`${htmlTemplate}\`; console.log('Asset HTML injected successfully'); } }; `; } /** * Returns the JS that injects the saveEditor function */ getSaveInjectionCode(nodeName) { return ` // Asset Save injection for ${nodeName} window.EVOLV.nodes.${nodeName}.assetMenu.saveEditor = function(node) { console.log('Saving asset properties for ${nodeName}…'); const fields = ['supplier','category','assetType','model','unit']; const errors = []; fields.forEach(f => { const el = document.getElementById(\`node-input-\${f}\`); node[f] = el ? el.value : ''; }); if (node.assetType && !node.unit) errors.push('Unit must be set when type is specified.'); if (!node.unit) errors.push('Unit is required.'); errors.forEach(e=>RED.notify(e,'error')); // --- DEBUG: show exactly what was saved --- const saved = fields.reduce((o,f) => { o[f] = node[f]; return o; }, {}); console.log('→ assetMenu.saveEditor result:', saved); return errors.length===0; }; `; } } module.exports = AssetMenu;