diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index ee3542eee..3ba5e5961 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -53,7 +53,8 @@ RED.nodes = (function() { defaults: { label: {value:""}, disabled: {value: false}, - info: {value: ""} + info: {value: ""}, + env: {value: []} } }; @@ -1387,7 +1388,8 @@ RED.nodes = (function() { type: "tab", disabled: false, label: RED._("clipboard.recoveredNodes"), - info: RED._("clipboard.recoveredNodesInfo") + info: RED._("clipboard.recoveredNodesInfo"), + env: [] } addWorkspace(recoveryWorkspace); RED.workspaces.add(recoveryWorkspace); @@ -1518,7 +1520,7 @@ RED.nodes = (function() { // Add a tab if there isn't one there already if (defaultWorkspace == null) { - defaultWorkspace = { type:"tab", id:getID(), disabled: false, info:"", label:RED._('workspace.defaultName',{number:1})}; + defaultWorkspace = { type:"tab", id:getID(), disabled: false, info:"", label:RED._('workspace.defaultName',{number:1}), env:[]}; addWorkspace(defaultWorkspace); RED.workspaces.add(defaultWorkspace); new_workspaces.push(defaultWorkspace); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js index 9cac57872..0f24408ba 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js @@ -2637,6 +2637,28 @@ var buildingEditDialog = false; } } } + { + var old_env = editing_node.env; + var new_env = RED.subflow.exportSubflowInstanceEnv(editing_node); + if (new_env && new_env.length > 0) { + new_env.forEach(function(prop) { + if (prop.type === "cred") { + editing_node.credentials = editing_node.credentials || {_:{}}; + editing_node.credentials[prop.name] = prop.value; + editing_node.credentials['has_'+prop.name] = (prop.value !== ""); + if (prop.value !== '__PWRD__') { + changed = true; + } + delete prop.value; + } + }); + } + if (!isSameObj(old_env, new_env)) { + editing_node.env = new_env; + changes.env = editing_node.env; + changed = true; + } + } if (changed) { var wasChanged = editing_node.changed; editing_node.changed = true; @@ -2668,6 +2690,16 @@ var buildingEditDialog = false; // console.log("oneditresize",editing_node.id,editing_node.type,err.toString()); // } // } + var envContainer = $("#red-ui-editor-group-env-list"); + if (envContainer.length) { + var rows = $("#dialog-form>div:not(#group-env-tabs-content)"); + var height = size.height; + for (var i=0; i', { + id: "editor-group-envProperties-content", + class: "red-ui-tray-content" + }).appendTo(editorContent).hide(), + iconClass: "fa fa-list", + }; + editorTabs.addTab(groupPropertiesTab); + var descriptionTab = { id: "editor-tab-description", label: RED._("editor-tab.description"), @@ -2725,6 +2769,7 @@ var buildingEditDialog = false; buildingEditDialog = false; done(); }); + }, close: function() { if (RED.view.state() != RED.state.IMPORT_DRAGGING) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/group.js b/packages/node_modules/@node-red/editor-client/src/js/ui/group.js index 1c86eb631..7a56de563 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/group.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/group.js @@ -87,14 +87,18 @@ RED.group = (function() { "label-position": "nw" }; + var groupDef = { defaults:{ name:{value:""}, style:{value:{label:true}}, - nodes:{value:[]} + nodes:{value:[]}, + env: {value:[]}, }, category: "config", oneditprepare: function() { + RED.subflow.buildPropertiesForm(this); + var style = this.style || {}; RED.colorPicker.create({ id:"node-input-style-stroke", @@ -144,7 +148,6 @@ RED.group = (function() { }) $("#node-input-style-label").prop("checked", this.style.label) $("#node-input-style-label").trigger("change"); - }, oneditresize: function(size) { }, diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js index b0a93630a..e2a982462 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js @@ -980,6 +980,9 @@ RED.subflow = (function() { function buildPropertiesList(envContainer, node) { var isTemplateNode = (node.type === "subflow"); + var isGroup = (node.type === "group"); + var isTab = (node.type === "tab"); + var kind = isGroup ? "group" : (isTab ? "tab" : "subflow"); if (isTemplateNode) { buildEnvControl(envContainer, node); @@ -1841,27 +1844,30 @@ RED.subflow = (function() { function exportSubflowInstanceEnv(node) { var env = []; + var isGroup = (node.type === "group"); + var isTab = (node.type === "tab"); - // First, get the values for the SubflowTemplate defined properties - // - these are the ones with custom UI elements - var parentEnv = getSubflowInstanceParentEnv(node); - parentEnv.forEach(function(data) { - var item; - var ui = data.ui || {}; - if (!ui.type) { - if (data.parent && data.parent.type === "cred") { - ui.type = "cred"; + if (!isGroup && !isTab) { + // First, get the values for the SubflowTemplate defined properties + // - these are the ones with custom UI elements + var parentEnv = getSubflowInstanceParentEnv(node); + parentEnv.forEach(function(data) { + var item; + var ui = data.ui || {}; + if (!ui.type) { + if (data.parent && data.parent.type === "cred") { + ui.type = "cred"; + } else { + ui.type = "input"; + ui.opts = {types:DEFAULT_ENV_TYPE_LIST} + } } else { - ui.type = "input"; - ui.opts = {types:DEFAULT_ENV_TYPE_LIST} + ui.opts = ui.opts || {}; } - } else { - ui.opts = ui.opts || {}; - } - var input = $("#"+getSubflowEnvPropertyName(data.name)); - if (input.length || ui.type === "cred") { - item = { name: data.name }; - switch(ui.type) { + var input = $("#"+getSubflowEnvPropertyName(data.name)); + if (input.length || ui.type === "cred") { + item = { name: data.name }; + switch(ui.type) { case "input": if (ui.opts.types && ui.opts.types.length > 0) { item.value = input.typedInput('value'); @@ -1887,14 +1893,16 @@ RED.subflow = (function() { item.type = 'bool'; item.value = ""+input.prop("checked"); break; + } + if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) { + env.push(item); + } } - if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) { - env.push(item); - } - } - }) + }) + } // Second, get the values from the Properties table tab - var items = $('#red-ui-editor-subflow-env-list').editableList('items'); + var kind = isGroup ? "group" : (isTab ? "tab" : "subflow"); + var items = $("#red-ui-editor-"+kind+"-env-list").editableList('items'); items.each(function (i,el) { var data = el.data('data'); var item; @@ -1947,11 +1955,13 @@ RED.subflow = (function() { buildEnvUI($("#subflow-input-ui"), getSubflowInstanceParentEnv(node), node); } } + function buildPropertiesForm(node) { - var container = $('#editor-subflow-envProperties-content'); + var kind = (node.type === "group") ? "group" : ((node.type === "tab") ? "tab": "subflow"); + var container = $("#editor-"+kind+"-envProperties-content"); var form = $('
').appendTo(container); var listContainer = $('
').appendTo(form); - var list = $('
    ').appendTo(listContainer); + var list = $('
      ').appendTo(listContainer); buildPropertiesList(list, node); } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js index a7a4e024f..507447686 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js @@ -23,6 +23,9 @@ RED.workspaces = (function() { var viewStack = []; var viewStackPos = 0; + function isSameObj(env0, env1) { + return (JSON.stringify(env0) === JSON.stringify(env1)); + } function addToViewStack(id) { if (viewStackPos !== viewStack.length) { @@ -43,7 +46,7 @@ RED.workspaces = (function() { workspaceIndex += 1; } while ($("#red-ui-workspace-tabs a[title='"+RED._('workspace.defaultName',{number:workspaceIndex})+"']").size() !== 0); - ws = {type:"tab",id:tabId,disabled: false,info:"",label:RED._('workspace.defaultName',{number:workspaceIndex})}; + ws = {type:"tab",id:tabId,disabled: false,info:"",label:RED._('workspace.defaultName',{number:workspaceIndex}),env:[]}; RED.nodes.addWorkspace(ws,targetIndex); workspace_tabs.addTab(ws,targetIndex); workspace_tabs.activateTab(tabId); @@ -55,6 +58,7 @@ RED.workspaces = (function() { RED.view.focus(); return ws; } + function deleteWorkspace(ws) { if (workspaceTabCount === 1) { return; @@ -71,6 +75,51 @@ RED.workspaces = (function() { RED.sidebar.config.refresh(); } + var tabflowEditor; + + function buildProperties(container, workspace) { + var dialogForm = $('
      ').appendTo(container); + $('
      '+ + ''+ + ''+ + '
      ').appendTo(dialogForm); + + var row = $('
      '+ + ''+ + '
      '+ + '
      ').appendTo(dialogForm); + tabflowEditor = RED.editor.createEditor({ + id: 'node-input-info', + mode: 'ace/mode/markdown', + value: "" + }); + + $('#node-info-input-info-expand').on("click", function(e) { + e.preventDefault(); + var value = tabflowEditor.getValue(); + RED.editor.editMarkdown({ + value: value, + width: "Infinity", + cursor: tabflowEditor.getCursorPosition(), + complete: function(v,cursor) { + tabflowEditor.setValue(v, -1); + tabflowEditor.gotoLine(cursor.row+1,cursor.column,false); + setTimeout(function() { + tabflowEditor.focus(); + },300); + } + }) + }); + + $('').prependTo(dialogForm); + dialogForm.on("submit", function(e) { e.preventDefault();}); + + $("#node-input-name").val(workspace.label); + RED.text.bidi.prepareInput($("#node-input-name")); + tabflowEditor.getSession().setValue(workspace.info || "", -1); + dialogForm.i18n(); + } + function showEditWorkspaceDialog(id) { var workspace = RED.nodes.workspace(id); if (!workspace) { @@ -81,7 +130,6 @@ RED.workspaces = (function() { return; } RED.view.state(RED.state.EDITING); - var tabflowEditor; var trayOptions = { title: RED._("workspace.editFlow",{name:RED.utils.sanitize(workspace.label)}), buttons: [ @@ -130,6 +178,28 @@ RED.workspaces = (function() { $("#red-ui-tab-"+(workspace.id.replace(".","-"))).toggleClass('red-ui-workspace-disabled',!!workspace.disabled); $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!workspace.disabled); + var old_env = workspace.env; + var new_env = RED.subflow.exportSubflowInstanceEnv(workspace); + console.log("; NE", new_env); + if (new_env && (new_env.length > 0)) { + new_env.forEach(function(prop) { + if (prop.type === "cred") { + workspace.credentials = workspace.credentials || {_:{}}; + workspace.credentials[prop.name] = prop.value; + workspace.credentials['has_'+prop.name] = (prop.value !== ""); + if (prop.value !== '__PWRD__') { + changed = true; + } + delete prop.value; + } + }); + } + if (!isSameObj(old_env, new_env)) { + workspace.env = new_env; + changes.env = old_env; + changed = true; + } + if (changed) { var historyEvent = { t: "edit", @@ -156,27 +226,67 @@ RED.workspaces = (function() { } ], resize: function(dimensions) { + var height = dimensions.height; + var rows = $("#dialog-form>div:not(.node-text-editor-row)"); var editorRow = $("#dialog-form>div.node-text-editor-row"); - var height = $("#dialog-form").height(); for (var i=0; i').appendTo(trayFooter) - var dialogForm = $('
      ').appendTo(trayBody); - $('
      '+ - ''+ - ''+ - '
      ').appendTo(dialogForm); + var editorTabEl = $('').appendTo(trayBody); + var editorContent = $('
      ').appendTo(trayBody); + var finishedBuilding = false; + + var editorTabs = RED.tabs.create({ + element:editorTabEl, + onchange:function(tab) { + editorContent.children().hide(); + if (tab.onchange) { + tab.onchange.call(tab); + } + tab.content.show(); + if (finishedBuilding) { + RED.tray.resize(); + } + }, + collapsible: true, + menu: false + }); + + var nodePropertiesTab = { + id: "editor-tab-properties", + label: RED._("editor-tab.properties"), + name: RED._("editor-tab.properties"), + content: $('
      ', {class:"red-ui-tray-content"}).appendTo(editorContent).hide(), + iconClass: "fa fa-cog" + }; + buildProperties(nodePropertiesTab.content, workspace); + editorTabs.addTab(nodePropertiesTab); + + var tabPropertiesTab = { + id: "editor-tab-envProperties", + label: RED._("editor-tab.envProperties"), + name: RED._("editor-tab.envProperties"), + content: $('
      ', { + id: "editor-tab-envProperties-content", + class: "red-ui-tray-content" + }).appendTo(editorContent).hide(), + iconClass: "fa fa-list", + }; + RED.subflow.buildPropertiesForm(workspace); + editorTabs.addTab(tabPropertiesTab); if (!workspace.hasOwnProperty("disabled")) { workspace.disabled = false; @@ -187,43 +297,8 @@ RED.workspaces = (function() { disabledIcon: "fa-ban", invertState: true }) - - - var row = $('
      '+ - ''+ - '
      '+ - '
      ').appendTo(dialogForm); - tabflowEditor = RED.editor.createEditor({ - id: 'node-input-info', - mode: 'ace/mode/markdown', - value: "" - }); - - $('#node-info-input-info-expand').on("click", function(e) { - e.preventDefault(); - var value = tabflowEditor.getValue(); - RED.editor.editMarkdown({ - value: value, - width: "Infinity", - cursor: tabflowEditor.getCursorPosition(), - complete: function(v,cursor) { - tabflowEditor.setValue(v, -1); - tabflowEditor.gotoLine(cursor.row+1,cursor.column,false); - setTimeout(function() { - tabflowEditor.focus(); - },300); - } - }) - }); - - - - $('').prependTo(dialogForm); - dialogForm.on("submit", function(e) { e.preventDefault();}); - $("#node-input-name").val(workspace.label); - RED.text.bidi.prepareInput($("#node-input-name")); - tabflowEditor.getSession().setValue(workspace.info || "", -1); - dialogForm.i18n(); + finishedBuilding = true; + RED.tray.resize(); }, close: function() { if (RED.view.state() != RED.state.IMPORT_DRAGGING) { diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 9294121e7..3ef75b4c6 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -396,6 +396,17 @@ class Flow { return undefined; } + + /** + * Get a group node instance + * @param {String} id + * @return {Node} group node + */ + getGroupNode(id) { + const groups = this.global.groups; + return groups[id]; + } + /** * Get all of the nodes instantiated within this flow * @return {[type]} [description] @@ -404,6 +415,57 @@ class Flow { return this.activeNodes; } + /** + * Get a flow setting value deined in this flow. + * @param {String} key + * @return {Object} result containinig val property or null + */ + getFlowSetting(name) { + const flow = this.flow; + if (flow.env) { + if (!name.startsWith("$parent.")) { + if (!flow._env) { + const envs = flow.env; + const entries = envs.map((env) => [env.name, env]); + flow._env = Object.fromEntries(entries); + } + const env = flow._env[name]; + if (env) { + let value = env.value; + const type = env.type; + if ((type !== "env") || (value !== name)) { + if (type === "env") { + value = value.replace(new RegExp("\\${"+name+"}","g"),"${$parent."+name+"}"); + } + try { + if (type === "bool") { + const val = ((value === "true") || + (value === true)); + return { + val: val + }; + } + if (type === "cred") { + return { + val: value + }; + } + var val = redUtil.evaluateNodeProperty(value, type, null, null, null); + return { + val: val + }; + } + catch (e) { + this.error(e); + return null; + } + } + } + } + } + return null; + } + /** * Get a flow setting value. This currently automatically defers to the parent * flow which, as defined in ./index.js returns `process.env[key]`. @@ -412,6 +474,10 @@ class Flow { * @return {[type]} [description] */ getSetting(key) { + const result = this.getFlowSetting(key); + if (result) { + return result.val; + } return this.parent.getSetting(key); } diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js b/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js index a0b682faa..b4e8cd99a 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js @@ -43,6 +43,54 @@ function evaluateInputValue(value, type, node) { return redUtil.evaluateNodeProperty(value, type, node, null, null); } +/*! + * Get value of environment variable defined in group node. + * @param {String} group - group node + * @param {String} name - name of variable + * @return {Object} object containing the value in val property or null if not defined + */ +function getGroupSetting(node, group, flow, name) { + if (group) { + if (!name.startsWith("$parent.")) { + if (group.env) { + if (!group._env) { + const envs = group.env; + const entries = envs.map((env) => [env.name, env]); + group._env = Object.fromEntries(entries); + } + const env = group._env[name]; + if (env) { + let value = env.value; + const type = env.type; + if ((type !== "env") || (value !== name)) { + if (type === "env") { + value = value.replace(new RegExp("\\${"+name+"}","g"),"${$parent."+name+"}"); + } + try { + const val = evaluateInputValue(value, type, node); + return { + val: val + }; + } + catch (e) { + this.error(e); + return null; + } + } + } + } + } + else { + name = name.substring(8); + } + if (group.g && flow) { + const parent = flow.getGroupNode(group.g); + return getGroupSetting(node, parent, flow, name); + } + } + return null; +} + /** * This class represents a subflow - which is handled as a special type of Flow */ @@ -370,6 +418,16 @@ class Subflow extends Flow { // name starts $parent. ... so delegate to parent automatically name = name.substring(8); } + const node = this.subflowInstance; + if (node.g) { + const group = this.getGroupNode(node.g); + const result = getGroupSetting(node, group, this, name); + if (result) { + return result.val; + } + } + + var parent = this.parent; if (parent) { var val = parent.getSetting(name); diff --git a/packages/node_modules/@node-red/runtime/lib/flows/index.js b/packages/node_modules/@node-red/runtime/lib/flows/index.js index 92c3e5b1e..c6bdcea1c 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/index.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/index.js @@ -539,6 +539,9 @@ async function addFlow(flow, user) { if (flow.hasOwnProperty('disabled')) { tabNode.disabled = flow.disabled; } + if (flow.hasOwnProperty('env')) { + tabNode.env = flow.env; + } var nodes = [tabNode]; @@ -599,6 +602,9 @@ function getFlow(id) { if (flow.hasOwnProperty('info')) { result.info = flow.info; } + if (flow.hasOwnProperty('env')) { + result.env = flow.env; + } if (id !== 'global') { result.nodes = []; } @@ -694,6 +700,9 @@ async function updateFlow(id,newFlow, user) { if (newFlow.hasOwnProperty('disabled')) { tabNode.disabled = newFlow.disabled; } + if (newFlow.hasOwnProperty('env')) { + tabNode.env = newFlow.env; + } nodes = [tabNode].concat(newFlow.nodes||[]).concat(newFlow.configs||[]); nodes.forEach(function(n) { diff --git a/packages/node_modules/@node-red/runtime/lib/flows/util.js b/packages/node_modules/@node-red/runtime/lib/flows/util.js index 00b54bc32..f81b6cdce 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/util.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/util.js @@ -44,24 +44,25 @@ function diffNodes(oldNode,newNode) { var EnvVarPropertyRE_old = /^\$\((\S+)\)$/; var EnvVarPropertyRE = /^\${(\S+)}$/; -function mapEnvVarProperties(obj,prop,flow) { + +function mapEnvVarProperties(obj,prop,flow,config) { var v = obj[prop]; if (Buffer.isBuffer(v)) { return; } else if (Array.isArray(v)) { for (var i=0;i [env.name, env]); + group._env = Object.fromEntries(entries); + } + const env = group._env[name]; + if (env) { + let value = env.value; + const type = env.type; + if ((type !== "env") || (value !== name)) { + if (type === "env") { + value = value.replace(new RegExp("\\${"+name+"}","g"),"${$parent."+name+"}"); + } + try { + if (type === "bool") { + const val = ((value === "true") || + (value === true)); + return { + val: val + }; + } + if (type === "cred") { + return { + val: value + }; + } + var val = evaluateNodeProperty(value, type, node, null, null); + return { + val: val + }; + } + catch (e) { + this.error(e); + return null; + } + } + } + } + } + else { + name = name.substring(8); + } + if (group.g && flow) { + const parent = flow.getGroupNode(group.g); + return getGroupSetting(node, parent, flow, name); + } + } + return null; +} + /*! * Get value of environment variable. * @param {Node} node - accessing node * @param {String} name - name of variable * @return {String} value of env var */ -function getSetting(node, name) { - if (node && node._flow) { - var flow = node._flow; - if (flow) { - return flow.getSetting(name); +function getSetting(node, name, flow_) { + var flow = (flow_ ? flow_ : (node ? node._flow : null)); + if (flow) { + if (node && node.g) { + const group = flow.getGroupNode(node.g); + const result = getGroupSetting(node, group, flow, name); + if (result) { + return result.val; + } } + return flow.getSetting(name); } return process.env[name]; } @@ -544,18 +609,19 @@ function getSetting(node, name) { * @memberof @node-red/util_util */ function evaluateEnvProperty(value, node) { + var flow = (node && node.hasOwnProperty("_flow")) ? node._flow : null; var result; if (/^\${[^}]+}$/.test(value)) { // ${ENV_VAR} var name = value.substring(2,value.length-1); - result = getSetting(node, name); + result = getSetting(node, name, flow); } else if (!/\${\S+}/.test(value)) { // ENV_VAR - result = getSetting(node, value); + result = getSetting(node, value, flow); } else { // FOO${ENV_VAR}BAR return value.replace(/\${([^}]+)}/g, function(match, name) { - var val = getSetting(node, name); + var val = getSetting(node, name, flow); return (val === undefined)?"":val; }); } @@ -668,7 +734,7 @@ function prepareJSONataExpression(value,node) { return node.context().global.get(val, store); }); expr.assign('env', function(name) { - var val = getSetting(node, name); + var val = getSetting(node, name, node._flow); if (typeof val !== 'undefined') { return val; } else { @@ -976,5 +1042,6 @@ module.exports = { normaliseNodeTypeName: normaliseNodeTypeName, prepareJSONataExpression: prepareJSONataExpression, evaluateJSONataExpression: evaluateJSONataExpression, - parseContextStore: parseContextStore + parseContextStore: parseContextStore, + getSetting: getSetting }; diff --git a/test/nodes/core/function/15-change_spec.js b/test/nodes/core/function/15-change_spec.js index fc0c14600..06d1e3774 100644 --- a/test/nodes/core/function/15-change_spec.js +++ b/test/nodes/core/function/15-change_spec.js @@ -542,6 +542,77 @@ describe('change Node', function() { changeNode1.receive({payload:"123",topic:"ABC"}); }); }); + + it('sets the value using env property from tab', function(done) { + var flow = [ + {"id":"tab1","type":"tab","env":[ + {"name":"NR_TEST_A", "value":"bar", "type": "str"} + ]}, + {"id":"changeNode1","type":"change","z":"tab1",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]} + ]; + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.equal("bar"); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.receive({payload:"123",topic:"ABC"}); + }); + }); + + it('sets the value using env property from group', function(done) { + var flow = [ + {"id":"group1","type":"group","env":[ + {"name":"NR_TEST_A", "value":"bar", "type": "str"} + ]}, + {"id":"changeNode1","type":"change","g":"group1",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]} + ]; + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.equal("bar"); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.receive({payload:"123",topic:"ABC"}); + }); + }); + + it('sets the value using env property from nested group', function(done) { + var flow = [ + {"id":"group1","type":"group","env":[ + {"name":"NR_TEST_A", "value":"bar", "type": "str"} + ]}, + {"id":"group2","type":"group","g":"group1","env":[]}, + {"id":"changeNode1","type":"change","g":"group2",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]} + ]; + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.equal("bar"); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.receive({payload:"123",topic:"ABC"}); + }); + }); + }); diff --git a/test/nodes/subflow/subflow_spec.js b/test/nodes/subflow/subflow_spec.js index b60328296..d5b8d78ae 100644 --- a/test/nodes/subflow/subflow_spec.js +++ b/test/nodes/subflow/subflow_spec.js @@ -447,4 +447,124 @@ describe('subflow', function() { }); }); + it('should access env var of tab', function(done) { + var flow = [ + {id:"t0", type:"tab", label:"", disabled:false, info:"", env: [ + {name: "K", type: "str", value: "V"} + ]}, + {id:"n1", x:10, y:10, z:"t0", type:"subflow:s1", wires:[["n2"]]}, + {id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]}, + // Subflow + {id:"s1", type:"subflow", name:"Subflow", info:"", env: [], + in:[{ + x:10, y:10, + wires:[ {id:"s1-n1"} ] + }], + out:[{ + x:10, y:10, + wires:[ {id:"s1-n1", port:0} ] + }] + }, + {id:"s1-n1", x:10, y:10, z:"s1", type:"function", + func:"msg.V = env.get('K'); return msg;", + wires:[]} + ]; + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property("V", "V"); + done(); + } + catch (e) { + console.log(e); + done(e); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should access env var of group', function(done) { + var flow = [ + {id:"t0", type:"tab", label:"", disabled:false, info:""}, + {id:"g1", z:"t0", type:"group", env:[ + {name: "K", type: "str", value: "V"} + ]}, + {id:"n1", x:10, y:10, z:"t0", g:"g1", type:"subflow:s1", wires:[["n2"]]}, + {id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]}, + // Subflow + {id:"s1", type:"subflow", name:"Subflow", info:"", env: [], + in:[{ + x:10, y:10, + wires:[ {id:"s1-n1"} ] + }], + out:[{ + x:10, y:10, + wires:[ {id:"s1-n1", port:0} ] + }] + }, + {id:"s1-n1", x:10, y:10, z:"s1", type:"function", + func:"msg.V = env.get('K'); return msg;", + wires:[]} + ]; + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property("V", "V"); + done(); + } + catch (e) { + console.log(e); + done(e); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should access env var of nested group', function(done) { + var flow = [ + {id:"t0", type:"tab", label:"", disabled:false, info:""}, + {id:"g1", z:"t0", type:"group", env:[ + {name: "K", type: "str", value: "V"} + ]}, + {id:"g2", z:"t0", g:"g1", type:"group", env:[]}, + {id:"n1", x:10, y:10, z:"t0", g:"g2", type:"subflow:s1", wires:[["n2"]]}, + {id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]}, + // Subflow + {id:"s1", type:"subflow", name:"Subflow", info:"", env: [], + in:[{ + x:10, y:10, + wires:[ {id:"s1-n1"} ] + }], + out:[{ + x:10, y:10, + wires:[ {id:"s1-n1", port:0} ] + }] + }, + {id:"s1-n1", x:10, y:10, z:"s1", type:"function", + func:"msg.V = env.get('K'); return msg;", + wires:[]} + ]; + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property("V", "V"); + done(); + } + catch (e) { + console.log(e); + done(e); + } + }); + n1.receive({payload:"foo"}); + }); + }); + }); diff --git a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js index 8caa31349..95086c296 100644 --- a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js @@ -1228,4 +1228,54 @@ describe('Flow', function() { }) }) + describe("#env", function () { + it("can instantiate a node with environment variable property values of group and tab", function (done) { + try { + after(function() { + delete process.env.V0; + delete process.env.V1; + }) + process.env.V0 = "gv0"; + process.env.V1 = "gv1"; + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab",env:[ + {"name": "V0", value: "v0", type: "str"} + ]}, + {id:"g1",type:"group",z:"t1",env:[ + {"name": "V0", value: "v1", type: "str"}, + {"name": "V1", value: "v2", type: "str"} + ]}, + {id:"g2",type:"group",z:"t1",g:"g1",env:[ + {"name": "V1", value: "v3", type: "str"} + ]}, + {id:"1",x:10,y:10,z:"t1",type:"test",foo:"$(V0)",wires:[]}, + {id:"2",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V0)",wires:[]}, + {id:"3",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V1)",wires:[]}, + {id:"4",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"$(V1)",wires:[]}, + {id:"5",x:10,y:10,z:"t1",type:"test",foo:"$(V1)",wires:[]}, + ]); + var flow = Flow.create({getSetting:v=>process.env[v]},config,config.flows["t1"]); + flow.start(); + + var activeNodes = flow.getActiveNodes(); + + activeNodes["1"].foo.should.equal("v0"); + activeNodes["2"].foo.should.equal("v1"); + activeNodes["3"].foo.should.equal("v2"); + activeNodes["4"].foo.should.equal("v3"); + activeNodes["5"].foo.should.equal("gv1"); + + flow.stop().then(function() { + done(); + }); + } + catch (e) { + console.log(e.stack); + done(e); + } + + }); + + }); + });