standaardisation updates

This commit is contained in:
znetsixe
2025-06-25 10:55:50 +02:00
parent dbc36c2f57
commit 3198690a81
126 changed files with 5028 additions and 608 deletions

242
src/menu/asset.js Normal file
View File

@@ -0,0 +1,242 @@
// 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 = '<option value="">Select…</option>';
(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 = '<option value="">Select…</option>';
(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 Properties -->
<hr />
<h3>Asset selection</h3>
<div class="form-row">
<label for="node-input-supplier"><i class="fa fa-industry"></i> Supplier</label>
<select id="node-input-supplier" style="width:70%;"></select>
</div>
<div class="form-row">
<label for="node-input-category"><i class="fa fa-sitemap"></i> Category</label>
<select id="node-input-category" style="width:70%;"></select>
</div>
<div class="form-row">
<label for="node-input-assetType"><i class="fa fa-puzzle-piece"></i> Type</label>
<select id="node-input-assetType" style="width:70%;"></select>
</div>
<div class="form-row">
<label for="node-input-model"><i class="fa fa-wrench"></i> Model</label>
<select id="node-input-model" style="width:70%;"></select>
</div>
<div class="form-row">
<label for="node-input-unit"><i class="fa fa-balance-scale"></i> Unit</label>
<select id="node-input-unit" style="width:70%;"></select>
</div>
<hr />
`;
}
/**
* 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;

85
src/menu/index.js Normal file
View File

@@ -0,0 +1,85 @@
const AssetMenu = require('./asset.js');
const LoggerMenu = require('./logger.js');
const PhysicalPositionMenu = require('./physicalPosition.js');
class MenuManager {
constructor() {
this.registeredMenus = new Map(); // Store menu type instances
this.registerMenu('asset', new AssetMenu()); // Register asset menu by default
this.registerMenu('logger', new LoggerMenu()); // Register logger menu by default
this.registerMenu('position', new PhysicalPositionMenu()); // Register position menu by default
}
/**
* Register a menu type with its handler instance
* @param {string} menuType - The type of menu (e.g., 'asset', 'logging')
* @param {object} menuHandler - The menu handler instance
*/
registerMenu(menuType, menuHandler) {
this.registeredMenus.set(menuType, menuHandler);
}
/**
* Create a complete endpoint script with data and initialization functions
* @param {string} nodeName - The name of the node type
* @param {Array<string>} menuTypes - Array of menu types to include
* @returns {string} Complete JavaScript code to serve
*/
createEndpoint(nodeName, menuTypes) {
// 1. Collect all menu data
const menuData = {};
menuTypes.forEach(menuType => {
const handler = this.registeredMenus.get(menuType);
if (handler && typeof handler.getAllMenuData === 'function') {
menuData[menuType] = handler.getAllMenuData();
}
});
// Generate HTML injection code
const htmlInjections = menuTypes.map(type => {
const menu = this.registeredMenus.get(type);
if (menu && menu.getHtmlInjectionCode) {
return menu.getHtmlInjectionCode(nodeName);
}
return '';
}).join('\n');
// 2. Collect all client initialization code
const initFunctions = [];
menuTypes.forEach(menuType => {
const handler = this.registeredMenus.get(menuType);
if (handler && typeof handler.getClientInitCode === 'function') {
initFunctions.push(handler.getClientInitCode(nodeName));
}
});
// 3. Convert menu data to JSON
const menuDataJSON = JSON.stringify(menuData, null, 2);
// 4. Assemble the complete script
return `
// Create the namespace structure
window.EVOLV = window.EVOLV || {};
window.EVOLV.nodes = window.EVOLV.nodes || {};
window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {};
// Inject the pre-loaded menu data directly into the namespace
window.EVOLV.nodes.${nodeName}.menuData = ${menuDataJSON};
${initFunctions.join('\n\n')}
// Main initialization function that calls all menu initializers
window.EVOLV.nodes.${nodeName}.initEditor = function(node) {
${menuTypes.map(type => `
if (window.EVOLV.nodes.${nodeName}.${type}Menu && window.EVOLV.nodes.${nodeName}.${type}Menu.initEditor) {
window.EVOLV.nodes.${nodeName}.${type}Menu.initEditor(node);
}`).join('')}
};
console.log('${nodeName} menu data and initializers loaded for: ${menuTypes.join(', ')}');
`;
}
}
module.exports = MenuManager;

134
src/menu/logger.js Normal file
View File

@@ -0,0 +1,134 @@
class LoggerMenu {
constructor() {
// no external data files for logger all static
}
// 1) Serverside: return the static menuData
getAllMenuData() {
return {
logLevels: [
{ value: 'error', label: 'Error', description: 'Only error messages' },
{ value: 'warn', label: 'Warn', description: 'Warning and error messages' },
{ value: 'info', label: 'Info', description: 'Info, warning and error messages' },
{ value: 'debug', label: 'Debug', description: 'All messages including debug' }
]
};
}
// 2) Clientside: inject the dropdown options
getDataInjectionCode(nodeName) {
return `
// Logger data loader for ${nodeName}
window.EVOLV.nodes.${nodeName}.loggerMenu.loadData = function(node) {
const data = window.EVOLV.nodes.${nodeName}.menuData.logger;
const sel = document.getElementById('node-input-logLevel');
if (!sel) return;
sel.innerHTML = '';
data.logLevels.forEach(l => {
const opt = document.createElement('option');
opt.value = l.value;
opt.textContent = l.label;
opt.title = l.description;
sel.appendChild(opt);
});
sel.value = node.logLevel || 'info';
};
`;
}
getHtmlInjectionCode(nodeName) {
const tpl = `
<h3>Internal logging</h3>
<div class="form-row">
<label for="node-input-enableLog"><i class="fa fa-bug"></i>Logging</label>
<input type="checkbox" id="node-input-enableLog"/>
</div>
<div class="form-row" id="row-logLevel">
<label for="node-input-logLevel"><i class="fa fa-list"></i> Log Level</label>
<select id="node-input-logLevel" style="width:60%;"></select>
</div>
`.replace(/`/g,'\\`').replace(/\$/g,'\\$');
return `
// Logger HTML injection for ${nodeName}
window.EVOLV.nodes.${nodeName}.loggerMenu.injectHtml = function() {
const ph = document.getElementById('logger-fields-placeholder');
if (ph && !ph.hasChildNodes()) {
ph.innerHTML = \`${tpl}\`;
}
};
`;
}
// 3) Clientside: wire up the enabletoggle behavior
getEventInjectionCode(nodeName) {
return `
// Logger event wiring for ${nodeName}
window.EVOLV.nodes.${nodeName}.loggerMenu.wireEvents = function(node) {
const chk = document.getElementById('node-input-enableLog');
const row = document.getElementById('row-logLevel');
if (!chk || !row) return;
const toggle = () => {
row.style.display = chk.checked ? 'block' : 'none';
};
chk.checked = node.enableLog || false;
toggle();
chk.addEventListener('change', toggle);
};
`;
}
// 4) Clientside: save logic
getSaveInjectionCode(nodeName) {
return `
// Logger Save injection for ${nodeName}
window.EVOLV.nodes.${nodeName}.loggerMenu.saveEditor = function(node) {
console.log('Saving logger properties for ${nodeName}…');
const chk = document.getElementById('node-input-enableLog');
const sel = document.getElementById('node-input-logLevel');
node.enableLog = chk ? chk.checked : false;
node.logLevel = sel ? sel.value : 'info';
const errors = [];
if (node.enableLog && !node.logLevel) {
errors.push('Log level must be selected when logging is enabled.');
}
errors.forEach(e => RED.notify(e,'error'));
// --- DEBUG: what was saved ---
console.log('→ loggerMenu.saveEditor result:', {
enableLog: node.enableLog,
logLevel: node.logLevel
});
return errors.length === 0;
};
`;
}
// 5) Compose everything into one clientside payload
getClientInitCode(nodeName) {
const dataCode = this.getDataInjectionCode(nodeName);
const eventCode = this.getEventInjectionCode(nodeName);
const saveCode = this.getSaveInjectionCode(nodeName);
const htmlCode = this.getHtmlInjectionCode(nodeName);
return `
// --- LoggerMenu for ${nodeName} ---
window.EVOLV.nodes.${nodeName}.loggerMenu =
window.EVOLV.nodes.${nodeName}.loggerMenu || {};
${htmlCode}
${dataCode}
${eventCode}
${saveCode}
// oneditprepare calls this
window.EVOLV.nodes.${nodeName}.loggerMenu.initEditor = function(node) {
// ------------------ BELOW sequence is important! -------------------------------
this.injectHtml();
this.loadData(node);
this.wireEvents(node);
};
`;
}
}
module.exports = LoggerMenu;

View File

@@ -0,0 +1,123 @@
class PhysicalPositionMenu {
// 1) Server-side: provide the option groups
getAllMenuData() {
return {
positionGroups: [
{ group: 'Positional', options: [
{ value: 'upstream', label: '⬅ Upstream' },
{ value: 'atEquipment', label: '⚙️ At Equipment' },
{ value: 'downstream', label: '➡ Downstream' }
]
}
]
};
}
// 2) HTML template (pure markup)
getHtmlTemplate() {
return `
<hr />
<h3>Physical Position vs parent</h3>
<div class="form-row">
<label for="node-input-physicalAspect"><i class="fa fa-map-marker"></i>Position</label>
<select id="node-input-physicalAspect" style="width:70%;">
<!-- optgroups will be injected -->
</select>
</div>
<hr />
`;
}
// 3) HTML injector
getHtmlInjectionCode(nodeName) {
const tpl = this.getHtmlTemplate()
.replace(/`/g,'\\`').replace(/\$/g,'\\$');
return `
// PhysicalPosition HTML injection for ${nodeName}
window.EVOLV.nodes.${nodeName}.positionMenu.injectHtml = function() {
const ph = document.getElementById('position-fields-placeholder');
if (ph && !ph.hasChildNodes()) {
ph.innerHTML = \`${tpl}\`;
}
};
`;
}
// 4) Data-loader injector
getDataInjectionCode(nodeName) {
return `
// PhysicalPosition data loader for ${nodeName}
window.EVOLV.nodes.${nodeName}.positionMenu.loadData = function(node) {
const data = window.EVOLV.nodes.${nodeName}.menuData.position;
const sel = document.getElementById('node-input-physicalAspect');
if (!sel) return;
sel.innerHTML = '';
(data.positionGroups||[]).forEach(grp => {
const optg = document.createElement('optgroup');
optg.label = grp.group;
grp.options.forEach(o=>{
const opt = document.createElement('option');
opt.value = o.value;
opt.textContent = o.label;
optg.appendChild(opt);
});
sel.appendChild(optg);
});
// default to “atEquipment” if not set
sel.value = node.physicalAspect || 'atEquipment';
};
`;
}
// 5) (no special events needed, but stub for symmetry)
getEventInjectionCode(nodeName) {
return `
// PhysicalPosition events for ${nodeName}
window.EVOLV.nodes.${nodeName}.positionMenu.wireEvents = function(node) {
// no dynamic behavior
};
`;
}
// 6) Save-logic injector
getSaveInjectionCode(nodeName) {
return `
// PhysicalPosition Save injection for ${nodeName}
window.EVOLV.nodes.${nodeName}.positionMenu.saveEditor = function(node) {
const sel = document.getElementById('node-input-physicalAspect');
node.physicalAspect = sel? sel.value : 'atEquipment';
return true;
};
`;
}
// 7) Compose everything into one client bundle
getClientInitCode(nodeName) {
const htmlCode = this.getHtmlInjectionCode(nodeName);
const dataCode = this.getDataInjectionCode(nodeName);
const eventCode = this.getEventInjectionCode(nodeName);
const saveCode = this.getSaveInjectionCode(nodeName);
return `
// --- PhysicalPositionMenu for ${nodeName} ---
window.EVOLV.nodes.${nodeName}.positionMenu =
window.EVOLV.nodes.${nodeName}.positionMenu || {};
${htmlCode}
${dataCode}
${eventCode}
${saveCode}
// hook into oneditprepare
window.EVOLV.nodes.${nodeName}.positionMenu.initEditor = function(node) {
this.injectHtml();
this.loadData(node);
this.wireEvents(node);
};
`;
}
}
module.exports = PhysicalPositionMenu;