From eb6d093e56ee2c8122ee0b08ca8bdfb41824cd7f Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 21 May 2018 15:10:06 +0100 Subject: [PATCH 1/9] Add env-var support to TypedInput --- editor/images/typedInput/env.png | Bin 0 -> 809 bytes editor/js/ui/common/typedInput.js | 5 +++ nodes/core/core/20-inject.html | 2 +- nodes/core/logic/10-switch.html | 8 ++-- nodes/core/logic/15-change.html | 6 +-- nodes/core/logic/15-change.js | 2 + red/runtime/util.js | 19 ++++++++ test/nodes/core/logic/15-change_spec.js | 56 +++++++++++++++++++++++- test/red/runtime/util_spec.js | 32 ++++++++++++++ 9 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 editor/images/typedInput/env.png diff --git a/editor/images/typedInput/env.png b/editor/images/typedInput/env.png new file mode 100644 index 0000000000000000000000000000000000000000..0ea51da0023ac14d71b8c2c32bc976f01116cc0b GIT binary patch literal 809 zcmV+^1J?YBP)rcp@khRiULR=6-%Tr4HYQq=sp z5tBbH%b${^T&!FeQBEr^q@+D9n&nU1>*lCmZ_oSg`MvKeCFknAr}KNi&pFTU`#tA@ zf88ee9uSd|AP8;+K~Nfo;Zxu9i7Xy+B~TCS&!)nJXLZFuh5ei=BIP1d;PW2^F+j?I zgTOkVD&irBfhWLmRehclpor`RP6N|o7H|=0Rn?!~mqtVm0Ox?o77Br0;2BT@ddrHr~Z`PB2t?#z;rFWVCqt4E$tJL8^FzM z1V5J2h)5N1-$0{m6lez?0&i3`nX{IfShoX*9L0&U_f~zUs=mufybX8(umNZ=j!w^T zyE{zC$Fc@Y0j~${nU-F*DY7Ib?Gmsns?FI9;H0CaZK_%gtO6c6IXi*Id`o=yU"interval" option.

$("#node-input-payload").typedInput({ default: 'str', typeField: $("#node-input-payloadType"), - types:['flow','global','str','num','bool','json','bin','date'] + types:['flow','global','str','num','bool','json','bin','date','env'] }); $("#inject-time-type-select").change(function() { diff --git a/nodes/core/logic/10-switch.html b/nodes/core/logic/10-switch.html index ffa5a48e5..f4ffc7dea 100644 --- a/nodes/core/logic/10-switch.html +++ b/nodes/core/logic/10-switch.html @@ -230,12 +230,12 @@ selectField.append($("").val(operators[d].v).text(/^switch/.test(operators[d].t)?node._(operators[d].t):operators[d].t)); } } - var valueField = $('',{class:"node-input-rule-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'str',types:['msg','flow','global','str','num','jsonata',previousValueType]}); - var numValueField = $('',{class:"node-input-rule-num-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'num',types:['flow','global','num','jsonata']}); + var valueField = $('',{class:"node-input-rule-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'str',types:['msg','flow','global','str','num','jsonata','env',previousValueType]}); + var numValueField = $('',{class:"node-input-rule-num-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'num',types:['flow','global','num','jsonata','env']}); var expValueField = $('',{class:"node-input-rule-exp-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'jsonata',types:['jsonata']}); - var btwnValueField = $('',{class:"node-input-rule-btwn-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'num',types:['msg','flow','global','str','num','jsonata',previousValueType]}); + var btwnValueField = $('',{class:"node-input-rule-btwn-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'num',types:['msg','flow','global','str','num','jsonata','env',previousValueType]}); var btwnAndLabel = $('',{class:"node-input-rule-btwn-label"}).text(" "+andLabel+" ").appendTo(row3); - var btwnValue2Field = $('',{class:"node-input-rule-btwn-value2",type:"text",style:"margin-left:2px;"}).appendTo(row3).typedInput({default:'num',types:['msg','flow','global','str','num','jsonata',previousValueType]}); + var btwnValue2Field = $('',{class:"node-input-rule-btwn-value2",type:"text",style:"margin-left:2px;"}).appendTo(row3).typedInput({default:'num',types:['msg','flow','global','str','num','jsonata','env',previousValueType]}); var typeValueField = $('',{class:"node-input-rule-type-value",type:"text",style:"margin-left: 5px;"}).appendTo(row) .typedInput({default:'string',types:[ {value:"string",label:"string",hasValue:false}, diff --git a/nodes/core/logic/15-change.html b/nodes/core/logic/15-change.html index 999c0ed71..83bdecf9e 100644 --- a/nodes/core/logic/15-change.html +++ b/nodes/core/logic/15-change.html @@ -146,7 +146,7 @@ .appendTo(row2); var propertyValue = $('',{class:"node-input-rule-property-value",type:"text"}) .appendTo(row2) - .typedInput({default:'str',types:['msg','flow','global','str','num','bool','json','bin','date','jsonata']}); + .typedInput({default:'str',types:['msg','flow','global','str','num','bool','json','bin','date','jsonata','env']}); var row3_1 = $('
').appendTo(row3); $('
',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"}) @@ -154,7 +154,7 @@ .appendTo(row3_1); var fromValue = $('',{class:"node-input-rule-property-search-value",type:"text"}) .appendTo(row3_1) - .typedInput({default:'str',types:['msg','flow','global','str','re','num','bool']}); + .typedInput({default:'str',types:['msg','flow','global','str','re','num','bool','env']}); var row3_2 = $('
',{style:"margin-top:8px;"}).appendTo(row3); $('
',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"}) @@ -162,7 +162,7 @@ .appendTo(row3_2); var toValue = $('',{class:"node-input-rule-property-replace-value",type:"text"}) .appendTo(row3_2) - .typedInput({default:'str',types:['msg','flow','global','str','num','bool','json','bin']}); + .typedInput({default:'str',types:['msg','flow','global','str','num','bool','json','bin','env']}); $('
',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"}) .text(to) diff --git a/nodes/core/logic/15-change.js b/nodes/core/logic/15-change.js index ae50394ed..c7c05118e 100644 --- a/nodes/core/logic/15-change.js +++ b/nodes/core/logic/15-change.js @@ -93,6 +93,8 @@ module.exports = function(RED) { valid = false; this.error(RED._("change.errors.invalid-expr",{error:e.message})); } + } else if (rule.tot === 'env') { + rule.to = RED.util.evaluateNodeProperty(rule.to,'env'); } } diff --git a/red/runtime/util.js b/red/runtime/util.js index 88c06a029..1480a841a 100644 --- a/red/runtime/util.js +++ b/red/runtime/util.js @@ -303,6 +303,23 @@ function setMessageProperty(msg,prop,value,createMissing) { } } +function evaluteEnvProperty(value) { + if (/^\${[^}]+}$/.test(value)) { + // ${ENV_VAR} + value = value.substring(2,value.length-1); + value = process.env.hasOwnProperty(value)?process.env[value]:"" + } else if (!/\${\S+}/.test(value)) { + // ENV_VAR + value = process.env.hasOwnProperty(value)?process.env[value]:"" + } else { + // FOO${ENV_VAR}BAR + value = value.replace(/\${([^}]+)}/g, function(match, v) { + return process.env.hasOwnProperty(v)?process.env[v]:"" + }); + } + return value; +} + function evaluateNodeProperty(value, type, node, msg) { if (type === 'str') { return ""+value; @@ -328,6 +345,8 @@ function evaluateNodeProperty(value, type, node, msg) { } else if (type === 'jsonata') { var expr = prepareJSONataExpression(value,node); return evaluateJSONataExpression(expr,msg); + } else if (type === 'env') { + return evaluteEnvProperty(value); } return value; } diff --git a/test/nodes/core/logic/15-change_spec.js b/test/nodes/core/logic/15-change_spec.js index f5ca0f3d2..e444b2663 100644 --- a/test/nodes/core/logic/15-change_spec.js +++ b/test/nodes/core/logic/15-change_spec.js @@ -390,7 +390,7 @@ describe('change Node', function() { changeNode1.receive({payload:""}); }); }); - + it('sets the value of the message property to the current timestamp', function(done) { var flow = [{"id":"changeNode1","type":"change","rules":[{"t":"set","p":"ts","pt":"msg","to":"","tot":"date"}],"name":"changeNode","wires":[["helperNode1"]]}, {id:"helperNode1", type:"helper", wires:[]}]; @@ -409,6 +409,33 @@ describe('change Node', function() { }); }); + describe('env var', function() { + before(function() { + process.env.NR_TEST_A = 'foo'; + }) + after(function() { + delete process.env.NR_TEST_A; + }) + it('sets the value using env property', function(done) { + var flow = [{"id":"changeNode1","type":"change",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("foo"); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.receive({payload:"123",topic:"ABC"}); + }); + }); + }); + + it('changes the value using jsonata', function(done) { var flow = [{"id":"changeNode1","type":"change",rules:[{"t":"set","p":"payload","to":"$length(payload)","tot":"jsonata"}],"name":"changeNode","wires":[["helperNode1"]]}, {id:"helperNode1", type:"helper", wires:[]}]; @@ -872,6 +899,33 @@ describe('change Node', function() { changeNode1.receive({payload:""}); }); }); + + describe('env var', function() { + before(function() { + process.env.NR_TEST_A = 'foo'; + }) + after(function() { + delete process.env.NR_TEST_A; + }) + it('changes the value using env property', function(done) { + var flow = [{"id":"changeNode1","type":"change",rules:[{"t":"change","p":"payload","from":"topic","to":"NR_TEST_A","fromt":"msg","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("abcfooabc"); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.receive({payload:"abcABCabc",topic:"ABC"}); + }); + }); + }); + }); describe("#delete", function() { diff --git a/test/red/runtime/util_spec.js b/test/red/runtime/util_spec.js index f542228a2..1b7438efd 100644 --- a/test/red/runtime/util_spec.js +++ b/test/red/runtime/util_spec.js @@ -307,6 +307,38 @@ describe("red/util", function() { },{}); result.should.eql("123"); }); + describe('environment variable', function() { + before(function() { + process.env.NR_TEST_A = "foo"; + process.env.NR_TEST_B = "${NR_TEST_A}"; + }) + after(function() { + delete process.env.NR_TEST_A; + delete process.env.NR_TEST_B; + }) + + it('returns an environment variable - NR_TEST_A', function() { + var result = util.evaluateNodeProperty('NR_TEST_A','env'); + result.should.eql('foo'); + }); + it('returns an environment variable - ${NR_TEST_A}', function() { + var result = util.evaluateNodeProperty('${NR_TEST_A}','env'); + result.should.eql('foo'); + }); + it('returns an environment variable - ${NR_TEST_A', function() { + var result = util.evaluateNodeProperty('${NR_TEST_A','env'); + result.should.eql(''); + }); + it('returns an environment variable - foo${NR_TEST_A}bar', function() { + var result = util.evaluateNodeProperty('123${NR_TEST_A}456','env'); + result.should.eql('123foo456'); + }); + it('returns an environment variable - foo${NR_TEST_B}bar', function() { + var result = util.evaluateNodeProperty('123${NR_TEST_B}456','env'); + result.should.eql('123${NR_TEST_A}456'); + }); + + }); }); describe('normalisePropertyExpression', function() { From e13fed9fc652086ace3a80034212bfc36bec282d Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 21 May 2018 15:19:50 +0100 Subject: [PATCH 2/9] Widen support for env var to use ${} or $() syntax --- red/runtime/nodes/flows/util.js | 13 ++++++------- test/red/runtime/nodes/flows/util_spec.js | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/red/runtime/nodes/flows/util.js b/red/runtime/nodes/flows/util.js index 16b9f461b..e7664715f 100644 --- a/red/runtime/nodes/flows/util.js +++ b/red/runtime/nodes/flows/util.js @@ -37,7 +37,8 @@ function diffNodes(oldNode,newNode) { return false; } -var EnvVarPropertyRE = /^\$\((\S+)\)$/; +var EnvVarPropertyRE_old = /^\$\((\S+)\)$/; +var EnvVarPropertyRE = /^\${(\S+)}$/; function mapEnvVarProperties(obj,prop) { if (Buffer.isBuffer(obj[prop])) { @@ -47,11 +48,9 @@ function mapEnvVarProperties(obj,prop) { mapEnvVarProperties(obj[prop],i); } } else if (typeof obj[prop] === 'string') { - var m; - if ( (m = EnvVarPropertyRE.exec(obj[prop])) !== null) { - if (process.env.hasOwnProperty(m[1])) { - obj[prop] = process.env[m[1]]; - } + if (obj[prop][0] === "$" && (EnvVarPropertyRE_old.test(obj[prop]) || EnvVarPropertyRE.test(obj[prop])) ) { + var envVar = obj[prop].substring(2,obj[prop].length-1); + obj[prop] = process.env.hasOwnProperty(envVar)?process.env[envVar]:obj[prop]; } } else { for (var p in obj[prop]) { @@ -203,7 +202,7 @@ module.exports = { var linkMap = {}; var changedTabs = {}; - + // Look for tabs that have been removed for (id in oldConfig.flows) { if (oldConfig.flows.hasOwnProperty(id) && (!newConfig.flows.hasOwnProperty(id))) { diff --git a/test/red/runtime/nodes/flows/util_spec.js b/test/red/runtime/nodes/flows/util_spec.js index 2cdc72fb3..184aaa70c 100644 --- a/test/red/runtime/nodes/flows/util_spec.js +++ b/test/red/runtime/nodes/flows/util_spec.js @@ -35,10 +35,17 @@ describe('flows/util', function() { }); describe('#mapEnvVarProperties',function() { - it('handles ENV substitutions in an object', function() { + before(function() { process.env.foo1 = "bar1"; process.env.foo2 = "bar2"; process.env.foo3 = "bar3"; + }) + after(function() { + delete process.env.foo1; + delete process.env.foo2; + delete process.env.foo3; + }) + it('handles ENV substitutions in an object - $()', function() { var foo = {a:"$(foo1)",b:"$(foo2)",c:{d:"$(foo3)"}}; for (var p in foo) { if (foo.hasOwnProperty(p)) { @@ -47,6 +54,15 @@ describe('flows/util', function() { } foo.should.eql({ a: 'bar1', b: 'bar2', c: { d: 'bar3' } } ); }); + it('handles ENV substitutions in an object - ${}', function() { + var foo = {a:"${foo1}",b:"${foo2}",c:{d:"${foo3}"}}; + for (var p in foo) { + if (foo.hasOwnProperty(p)) { + flowUtil.mapEnvVarProperties(foo,p); + } + } + foo.should.eql({ a: 'bar1', b: 'bar2', c: { d: 'bar3' } } ); + }); }); describe('#diffNodes',function() { From 9ad9c0ec6a9b6d470bb32ba6258d41531f3d1223 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 21 May 2018 15:28:15 +0100 Subject: [PATCH 3/9] Add $env function to JSONata expressions --- editor/vendor/jsonata/formatter.js | 1 + red/api/editor/locales/en-US/jsonata.json | 8 ++++++-- red/runtime/util.js | 3 +++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/editor/vendor/jsonata/formatter.js b/editor/vendor/jsonata/formatter.js index 9b0f544c8..6ff26accb 100644 --- a/editor/vendor/jsonata/formatter.js +++ b/editor/vendor/jsonata/formatter.js @@ -117,6 +117,7 @@ '$contains':{ args:[ 'str', 'pattern' ]}, '$count':{ args:[ 'array' ]}, '$each':{ args:[ 'object', 'function' ]}, + '$env': { args:[ 'arg' ]}, '$exists':{ args:[ 'arg' ]}, '$filter':{ args:[ 'array', 'function' ]}, '$floor':{ args:[ 'number' ]}, diff --git a/red/api/editor/locales/en-US/jsonata.json b/red/api/editor/locales/en-US/jsonata.json index 14c7f7cd1..c839c759e 100644 --- a/red/api/editor/locales/en-US/jsonata.json +++ b/red/api/editor/locales/en-US/jsonata.json @@ -190,11 +190,11 @@ }, "$flowContext": { "args": "string", - "desc": "Retrieves a flow context property." + "desc": "Retrieves a flow context property.\n\nThis is a Node-RED defined function." }, "$globalContext": { "args": "string", - "desc": "Retrieves a global context property." + "desc": "Retrieves a global context property.\n\nThis is a Node-RED defined function." }, "$pad": { "args": "string, width [, char]", @@ -215,5 +215,9 @@ "$toMillis": { "args": "timestamp", "desc": "Convert a `timestamp` string in the ISO 8601 format to the number of milliseconds since the Unix Epoch (1 January, 1970 UTC) as a number. An error is thrown if the string is not in the correct format." + }, + "$env": { + "args": "arg", + "desc": "Returns the value of an environment variable.\n\nThis is a Node-RED defined function." } } diff --git a/red/runtime/util.js b/red/runtime/util.js index 1480a841a..257e87777 100644 --- a/red/runtime/util.js +++ b/red/runtime/util.js @@ -359,6 +359,9 @@ function prepareJSONataExpression(value,node) { expr.assign('globalContext',function(val) { return node.context().global.get(val); }); + expr.assign('env', function(val) { + return process.env[val]; + }) expr.registerFunction('clone', cloneMessage, '<(oa)-:o>'); expr._legacyMode = /(^|[^a-zA-Z0-9_'"])msg([^a-zA-Z0-9_'"]|$)/.test(value); return expr; From 626d012775898a2bcf592ffd05c8d57a2958c6c5 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 21 May 2018 16:14:43 +0100 Subject: [PATCH 4/9] Do not disable the export-clipboard menu option with empty selection --- editor/js/main.js | 4 ++-- editor/js/ui/clipboard.js | 12 ------------ editor/js/ui/library.js | 4 ---- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/editor/js/main.js b/editor/js/main.js index cf295e7e7..e65b8f30a 100644 --- a/editor/js/main.js +++ b/editor/js/main.js @@ -410,8 +410,8 @@ {id:"menu-item-import-clipboard",label:RED._("menu.label.clipboard"),onselect:"core:show-import-dialog"}, {id:"menu-item-import-library",label:RED._("menu.label.library"),options:[]} ]}); - menuOptions.push({id:"menu-item-export",label:RED._("menu.label.export"),disabled:true,options:[ - {id:"menu-item-export-clipboard",label:RED._("menu.label.clipboard"),disabled:true,onselect:"core:show-export-dialog"}, + menuOptions.push({id:"menu-item-export",label:RED._("menu.label.export"),options:[ + {id:"menu-item-export-clipboard",label:RED._("menu.label.clipboard"),onselect:"core:show-export-dialog"}, {id:"menu-item-export-library",label:RED._("menu.label.library"),disabled:true,onselect:"core:library-export"} ]}); menuOptions.push(null); diff --git a/editor/js/ui/clipboard.js b/editor/js/ui/clipboard.js index 2bdd20052..0d7519e10 100644 --- a/editor/js/ui/clipboard.js +++ b/editor/js/ui/clipboard.js @@ -309,18 +309,6 @@ RED.clipboard = (function() { $('').appendTo("body"); - RED.events.on("view:selection-changed",function(selection) { - if (!selection.nodes) { - RED.menu.setDisabled("menu-item-export",true); - RED.menu.setDisabled("menu-item-export-clipboard",true); - RED.menu.setDisabled("menu-item-export-library",true); - } else { - RED.menu.setDisabled("menu-item-export",false); - RED.menu.setDisabled("menu-item-export-clipboard",false); - RED.menu.setDisabled("menu-item-export-library",false); - } - }); - RED.actions.add("core:show-export-dialog",exportNodes); RED.actions.add("core:show-import-dialog",importNodes); diff --git a/editor/js/ui/library.js b/editor/js/ui/library.js index 96aa9efeb..2f78828c3 100644 --- a/editor/js/ui/library.js +++ b/editor/js/ui/library.js @@ -414,12 +414,8 @@ RED.library = (function() { RED.events.on("view:selection-changed",function(selection) { if (!selection.nodes) { - RED.menu.setDisabled("menu-item-export",true); - RED.menu.setDisabled("menu-item-export-clipboard",true); RED.menu.setDisabled("menu-item-export-library",true); } else { - RED.menu.setDisabled("menu-item-export",false); - RED.menu.setDisabled("menu-item-export-clipboard",false); RED.menu.setDisabled("menu-item-export-library",false); } }); From f3e1b85d82eaf5ba289c0c2a96d32e3b0101f929 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 21 May 2018 22:08:04 +0100 Subject: [PATCH 5/9] Add RED.require to allow nodes to access other modules --- red/runtime/nodes/registry/loader.js | 48 ++- red/runtime/nodes/registry/localfilesystem.js | 61 ++-- red/runtime/nodes/registry/registry.js | 71 ++--- .../red/runtime/nodes/registry/loader_spec.js | 295 ++++++++++-------- .../nodes/registry/localfilesystem_spec.js | 28 +- .../runtime/nodes/registry/registry_spec.js | 218 +++++++------ .../NestedNode/icons/arrow-in.png | Bin 0 -> 393 bytes .../NestedNode/icons/file.txt | 3 - .../TestNodeModule/icons/file.txt | 3 - 9 files changed, 438 insertions(+), 289 deletions(-) create mode 100644 test/red/runtime/nodes/resources/local/NestedDirectoryNode/NestedNode/icons/arrow-in.png delete mode 100644 test/red/runtime/nodes/resources/local/NestedDirectoryNode/NestedNode/icons/file.txt delete mode 100644 test/red/runtime/nodes/resources/local/TestNodeModule/node_modules/TestNodeModule/icons/file.txt diff --git a/red/runtime/nodes/registry/loader.js b/red/runtime/nodes/registry/loader.js index f88463609..10c37c294 100644 --- a/red/runtime/nodes/registry/loader.js +++ b/red/runtime/nodes/registry/loader.js @@ -62,6 +62,17 @@ function copyObjectProperties(src,dst,copyList,blockList) { } } } +function requireModule(name) { + var moduleInfo = registry.getModuleInfo(name); + if (moduleInfo && moduleInfo.path) { + var relPath = path.relative(__dirname, moduleInfo.path); + return require(relPath); + } else { + var err = new Error(`Cannot find module '${name}'`); + err.code = "MODULE_NOT_FOUND"; + throw err; + } +} function createNodeApi(node) { var red = { @@ -71,6 +82,7 @@ function createNodeApi(node) { events: runtime.events, util: runtime.util, version: runtime.version, + require: requireModule } copyObjectProperties(runtime.nodes,red.nodes,["createNode","getNode","eachNode","addCredentials","getCredentials","deleteCredentials" ]); red.nodes.registerType = function(type,constructor,opts) { @@ -112,6 +124,7 @@ function createNodeApi(node) { function loadNodeFiles(nodeFiles) { var promises = []; + var nodes = []; for (var module in nodeFiles) { /* istanbul ignore else */ if (nodeFiles.hasOwnProperty(module)) { @@ -119,6 +132,7 @@ function loadNodeFiles(nodeFiles) { !semver.satisfies(runtime.version().replace(/(\-[1-9A-Za-z-][0-9A-Za-z-\.]*)?(\+[0-9A-Za-z-\.]+)?$/,""), nodeFiles[module].redVersion)) { //TODO: log it runtime.log.warn("["+module+"] "+runtime.log._("server.node-version-mismatch",{version:nodeFiles[module].redVersion})); + nodeFiles[module].err = "version_mismatch"; continue; } if (module == "node-red" || !registry.getModuleInfo(module)) { @@ -148,7 +162,14 @@ function loadNodeFiles(nodeFiles) { } try { - promises.push(loadNodeConfig(nodeFiles[module].nodes[node])) + promises.push(loadNodeConfig(nodeFiles[module].nodes[node]).then((function() { + var m = module; + var n = node; + return function(nodeSet) { + nodeFiles[m].nodes[n] = nodeSet; + nodes.push(nodeSet); + } + })())); } catch(err) { // } @@ -158,16 +179,19 @@ function loadNodeFiles(nodeFiles) { } } return when.settle(promises).then(function(results) { - var nodes = results.map(function(r) { - registry.addNodeSet(r.value.id,r.value,r.value.version); - return r.value; - }); + for (var module in nodeFiles) { + if (nodeFiles.hasOwnProperty(module)) { + if (!nodeFiles[module].err) { + registry.addModule(nodeFiles[module]); + } + } + } return loadNodeSetList(nodes); }); } function loadNodeConfig(fileInfo) { - return when.promise(function(resolve) { + return new Promise(function(resolve) { var file = fileInfo.file; var module = fileInfo.module; var name = fileInfo.name; @@ -292,7 +316,7 @@ function loadNodeSet(node) { var nodeDir = path.dirname(node.file); var nodeFn = path.basename(node.file); if (!node.enabled) { - return when.resolve(node); + return Promise.resolve(node); } else { } try { @@ -316,7 +340,7 @@ function loadNodeSet(node) { if (loadPromise == null) { node.enabled = true; node.loaded = true; - loadPromise = when.resolve(node); + loadPromise = Promise.resolve(node); } return loadPromise; } catch(err) { @@ -333,7 +357,7 @@ function loadNodeSet(node) { } } } - return when.resolve(node); + return Promise.resolve(node); } } @@ -365,19 +389,19 @@ function addModule(module) { // TODO: nls var e = new Error("module_already_loaded"); e.code = "module_already_loaded"; - return when.reject(e); + return Promise.reject(e); } try { var moduleFiles = localfilesystem.getModuleFiles(module); return loadNodeFiles(moduleFiles); } catch(err) { - return when.reject(err); + return Promise.reject(err); } } function loadNodeHelp(node,lang) { var base = path.basename(node.template); - var localePath = undefined; + var localePath; if (node.module === 'node-red') { var cat_dir = path.dirname(node.template); var cat = path.basename(cat_dir); diff --git a/red/runtime/nodes/registry/localfilesystem.js b/red/runtime/nodes/registry/localfilesystem.js index c2f0f9359..b8a516597 100644 --- a/red/runtime/nodes/registry/localfilesystem.js +++ b/red/runtime/nodes/registry/localfilesystem.js @@ -14,7 +14,6 @@ * limitations under the License. **/ -var when = require("when"); var fs = require("fs"); var path = require("path"); @@ -85,6 +84,7 @@ function getLocalNodeFiles(dir) { var result = []; var files = []; + var icons = []; try { files = fs.readdirSync(dir); } catch(err) { @@ -103,14 +103,16 @@ function getLocalNodeFiles(dir) { } else if (stats.isDirectory()) { // Ignore /.dirs/, /lib/ /node_modules/ if (!/^(\..*|lib|icons|node_modules|test|locales)$/.test(fn)) { - result = result.concat(getLocalNodeFiles(path.join(dir,fn))); + var subDirResults = getLocalNodeFiles(path.join(dir,fn)); + result = result.concat(subDirResults.files); + icons = icons.concat(subDirResults.icons); } else if (fn === "icons") { var iconList = scanIconDir(path.join(dir,fn)); - events.emit("node-icon-dir",{name:'node-red',path:path.join(dir,fn),icons:iconList}); + icons.push({path:path.join(dir,fn),icons:iconList}); } } }); - return result; + return {files: result, icons: icons} } function scanDirForNodesModules(dir,moduleName) { @@ -198,7 +200,7 @@ function getModuleNodeFiles(module) { var nodes = pkg['node-red'].nodes||{}; var results = []; var iconDirs = []; - + var iconList = []; for (var n in nodes) { /* istanbul ignore else */ if (nodes.hasOwnProperty(n)) { @@ -213,47 +215,54 @@ function getModuleNodeFiles(module) { if (iconDirs.indexOf(iconDir) == -1) { try { fs.statSync(iconDir); - var iconList = scanIconDir(iconDir); - events.emit("node-icon-dir",{name:pkg.name,path:iconDir,icons:iconList}); + var icons = scanIconDir(iconDir); + iconList.push({path:iconDir,icons:icons}); iconDirs.push(iconDir); } catch(err) { } } } } + var result = {files:results,icons:iconList}; + var examplesDir = path.join(moduleDir,"examples"); try { fs.statSync(examplesDir) events.emit("node-examples-dir",{name:pkg.name,path:examplesDir}); } catch(err) { } - return results; + return result; } function getNodeFiles(disableNodePathScan) { var dir; // Find all of the nodes to load var nodeFiles = []; + var results; var dir = path.resolve(__dirname + '/../../../../public/icons'); - var iconList = scanIconDir(dir); - events.emit("node-icon-dir",{name:'node-red',path:dir,icons:iconList}); + var iconList = [{path:dir,icons:scanIconDir(dir)}]; if (settings.coreNodesDir) { - nodeFiles = getLocalNodeFiles(path.resolve(settings.coreNodesDir)); + results = getLocalNodeFiles(path.resolve(settings.coreNodesDir)); + nodeFiles = nodeFiles.concat(results.files); + iconList = iconList.concat(results.icons); + var defaultLocalesPath = path.join(settings.coreNodesDir,"core","locales"); i18n.registerMessageCatalog("node-red",defaultLocalesPath,"messages.json"); } if (settings.userDir) { dir = path.join(settings.userDir,"lib","icons"); - iconList = scanIconDir(dir); - if (iconList.length > 0) { - events.emit("node-icon-dir",{name:'Library',path:dir,icons:iconList}); + var icons = scanIconDir(dir); + if (icons.length > 0) { + iconList.push({path:dir,icons:icons}); } dir = path.join(settings.userDir,"nodes"); - nodeFiles = nodeFiles.concat(getLocalNodeFiles(dir)); + results = getLocalNodeFiles(path.resolve(dir)); + nodeFiles = nodeFiles.concat(results.files); + iconList = iconList.concat(results.icons); } if (settings.nodesDir) { dir = settings.nodesDir; @@ -261,7 +270,9 @@ function getNodeFiles(disableNodePathScan) { dir = [dir]; } for (var i=0;iicon_paths[module.name].push(path.resolve(icon.path)) ) } - - if (version) { - moduleConfigs[set.module].version = version; - } - moduleConfigs[set.module].local = set.local; - - moduleConfigs[set.module].nodes[set.name] = set; - nodeList.push(id); nodeConfigCache = null; } + function removeNode(id) { var config = moduleConfigs[getModule(id)].nodes[getNode(id)]; if (!config) { @@ -346,6 +343,7 @@ function getModuleInfo(module) { name: module, version: moduleConfigs[module].version, local: moduleConfigs[module].local, + path: moduleConfigs[module].path, nodes: [] }; for (var i = 0; i < nodes.length; ++i) { @@ -592,6 +590,7 @@ var iconCache = {}; var defaultIcon = path.resolve(__dirname + '/../../../../public/icons/arrow-in.png'); function nodeIconDir(dir) { + return; icon_paths[dir.name] = icon_paths[dir.name] || []; icon_paths[dir.name].push(path.resolve(dir.path)); @@ -647,11 +646,11 @@ function getNodeIcons() { for (var module in moduleConfigs) { if (moduleConfigs.hasOwnProperty(module)) { if (moduleConfigs[module].icons) { - iconList[module] = moduleConfigs[module].icons; + iconList[module] = []; + moduleConfigs[module].icons.forEach(icon=>{ iconList[module] = iconList[module].concat(icon.icons) }); } } } - return iconList; } @@ -663,7 +662,9 @@ var registry = module.exports = { registerNodeConstructor: registerNodeConstructor, getNodeConstructor: getNodeConstructor, - addNodeSet: addNodeSet, + + addModule: addModule, + enableNodeSet: enableNodeSet, disableNodeSet: disableNodeSet, diff --git a/test/red/runtime/nodes/registry/loader_spec.js b/test/red/runtime/nodes/registry/loader_spec.js index 87e1c060b..d34d802a3 100644 --- a/test/red/runtime/nodes/registry/loader_spec.js +++ b/test/red/runtime/nodes/registry/loader_spec.js @@ -79,6 +79,7 @@ describe("red/nodes/registry/loader",function() { var result = {}; result["node-red"] = { "name": "node-red", + "version": "1.2.3", "nodes": { "TestNode1": { "file": path.join(resourcesDir,"TestNode1","TestNode1.js"), @@ -91,34 +92,39 @@ describe("red/nodes/registry/loader",function() { })); stubs.push(sinon.stub(registry,"saveNodeList", function(){ return })); - stubs.push(sinon.stub(registry,"addNodeSet", function(){ return })); + stubs.push(sinon.stub(registry,"addModule", function(){ return })); // This module isn't already loaded stubs.push(sinon.stub(registry,"getNodeInfo", function(){ return null; })); stubs.push(sinon.stub(nodes,"registerType")); loader.init({nodes:nodes,i18n:{defaultLang:"en-US"},events:{on:function(){},removeListener:function(){}},log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}}); loader.load().then(function(result) { - registry.addNodeSet.called.should.be.true(); - registry.addNodeSet.lastCall.args[0].should.eql("node-red/TestNode1"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('id',"node-red/TestNode1"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('module',"node-red"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('enabled',true); - registry.addNodeSet.lastCall.args[1].should.have.a.property('loaded',true); - registry.addNodeSet.lastCall.args[1].should.have.a.property('version',undefined); - registry.addNodeSet.lastCall.args[1].should.have.a.property('types'); - registry.addNodeSet.lastCall.args[1].types.should.have.a.length(1); - registry.addNodeSet.lastCall.args[1].types[0].should.eql('test-node-1'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('config'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('help'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('namespace','node-red'); - registry.addNodeSet.lastCall.args[1].should.not.have.a.property('err'); + registry.addModule.called.should.be.true(); + var module = registry.addModule.lastCall.args[0]; + module.should.have.property("name","node-red"); + module.should.have.property("version","1.2.3"); + module.should.have.property("nodes"); + module.nodes.should.have.property("TestNode1"); + module.nodes.TestNode1.should.have.property("id","node-red/TestNode1"); + module.nodes.TestNode1.should.have.property("module","node-red"); + module.nodes.TestNode1.should.have.property("name","TestNode1"); + module.nodes.TestNode1.should.have.property("file"); + module.nodes.TestNode1.should.have.property("template"); + module.nodes.TestNode1.should.have.property("enabled",true); + module.nodes.TestNode1.should.have.property("loaded",true); + module.nodes.TestNode1.should.have.property("types"); + module.nodes.TestNode1.types.should.have.a.length(1); + module.nodes.TestNode1.types[0].should.eql('test-node-1'); + module.nodes.TestNode1.should.have.property("config"); + module.nodes.TestNode1.should.have.property("help"); + module.nodes.TestNode1.should.have.property("namespace","node-red"); nodes.registerType.calledOnce.should.be.true(); nodes.registerType.lastCall.args[0].should.eql('node-red/TestNode1'); nodes.registerType.lastCall.args[1].should.eql('test-node-1'); done(); - }).otherwise(function(err) { + }).catch(function(err) { done(err); }); }); @@ -128,6 +134,7 @@ describe("red/nodes/registry/loader",function() { var result = {}; result["node-red"] = { "name": "node-red", + "version": "4.5.6", "nodes": { "MultipleNodes1": { "file": path.join(resourcesDir,"MultipleNodes1","MultipleNodes1.js"), @@ -140,27 +147,33 @@ describe("red/nodes/registry/loader",function() { })); stubs.push(sinon.stub(registry,"saveNodeList", function(){ return })); - stubs.push(sinon.stub(registry,"addNodeSet", function(){ return })); + stubs.push(sinon.stub(registry,"addModule", function(){ return })); // This module isn't already loaded stubs.push(sinon.stub(registry,"getNodeInfo", function(){ return null; })); stubs.push(sinon.stub(nodes,"registerType")); loader.init({nodes:nodes,i18n:{defaultLang:"en-US"},events:{on:function(){},removeListener:function(){}},log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}}); loader.load().then(function(result) { - registry.addNodeSet.called.should.be.true(); - registry.addNodeSet.lastCall.args[0].should.eql("node-red/MultipleNodes1"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('id',"node-red/MultipleNodes1"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('module',"node-red"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('enabled',true); - registry.addNodeSet.lastCall.args[1].should.have.a.property('loaded',true); - registry.addNodeSet.lastCall.args[1].should.have.a.property('version',undefined); - registry.addNodeSet.lastCall.args[1].should.have.a.property('types'); - registry.addNodeSet.lastCall.args[1].types.should.have.a.length(2); - registry.addNodeSet.lastCall.args[1].types[0].should.eql('test-node-multiple-1a'); - registry.addNodeSet.lastCall.args[1].types[1].should.eql('test-node-multiple-1b'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('config'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('help'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('namespace','node-red'); - registry.addNodeSet.lastCall.args[1].should.not.have.a.property('err'); + + registry.addModule.called.should.be.true(); + var module = registry.addModule.lastCall.args[0]; + module.should.have.property("name","node-red"); + module.should.have.property("version","4.5.6"); + module.should.have.property("nodes"); + module.nodes.should.have.property("MultipleNodes1"); + module.nodes.MultipleNodes1.should.have.property("id","node-red/MultipleNodes1"); + module.nodes.MultipleNodes1.should.have.property("module","node-red"); + module.nodes.MultipleNodes1.should.have.property("name","MultipleNodes1"); + module.nodes.MultipleNodes1.should.have.property("file"); + module.nodes.MultipleNodes1.should.have.property("template"); + module.nodes.MultipleNodes1.should.have.property("enabled",true); + module.nodes.MultipleNodes1.should.have.property("loaded",true); + module.nodes.MultipleNodes1.should.have.property("types"); + module.nodes.MultipleNodes1.types.should.have.a.length(2); + module.nodes.MultipleNodes1.types[0].should.eql('test-node-multiple-1a'); + module.nodes.MultipleNodes1.types[1].should.eql('test-node-multiple-1b'); + module.nodes.MultipleNodes1.should.have.property("config"); + module.nodes.MultipleNodes1.should.have.property("help"); + module.nodes.MultipleNodes1.should.have.property("namespace","node-red"); nodes.registerType.calledTwice.should.be.true(); nodes.registerType.firstCall.args[0].should.eql('node-red/MultipleNodes1'); @@ -168,8 +181,9 @@ describe("red/nodes/registry/loader",function() { nodes.registerType.secondCall.args[0].should.eql('node-red/MultipleNodes1'); nodes.registerType.secondCall.args[1].should.eql('test-node-multiple-1b'); + done(); - }).otherwise(function(err) { + }).catch(function(err) { done(err); }); }); @@ -180,6 +194,7 @@ describe("red/nodes/registry/loader",function() { var result = {}; result["node-red"] = { "name": "node-red", + "version":"2.4.6", "nodes": { "TestNode2": { "file": path.join(resourcesDir,"TestNode2","TestNode2.js"), @@ -192,34 +207,41 @@ describe("red/nodes/registry/loader",function() { })); stubs.push(sinon.stub(registry,"saveNodeList", function(){ return })); - stubs.push(sinon.stub(registry,"addNodeSet", function(){ return })); + stubs.push(sinon.stub(registry,"addModule", function(){ return })); // This module isn't already loaded stubs.push(sinon.stub(registry,"getNodeInfo", function(){ return null; })); stubs.push(sinon.stub(nodes,"registerType")); loader.init({nodes:nodes,i18n:{defaultLang:"en-US"},events:{on:function(){},removeListener:function(){}},log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}}); loader.load().then(function(result) { - registry.addNodeSet.called.should.be.true(); - registry.addNodeSet.lastCall.args[0].should.eql("node-red/TestNode2"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('id',"node-red/TestNode2"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('module',"node-red"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('enabled',true); - registry.addNodeSet.lastCall.args[1].should.have.a.property('loaded',true); - registry.addNodeSet.lastCall.args[1].should.have.a.property('version',undefined); - registry.addNodeSet.lastCall.args[1].should.have.a.property('types'); - registry.addNodeSet.lastCall.args[1].types.should.have.a.length(1); - registry.addNodeSet.lastCall.args[1].types[0].should.eql('test-node-2'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('config'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('help'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('namespace','node-red'); - registry.addNodeSet.lastCall.args[1].should.not.have.a.property('err'); + + registry.addModule.called.should.be.true(); + var module = registry.addModule.lastCall.args[0]; + module.should.have.property("name","node-red"); + module.should.have.property("version","2.4.6"); + module.should.have.property("nodes"); + module.nodes.should.have.property("TestNode2"); + module.nodes.TestNode2.should.have.property("id","node-red/TestNode2"); + module.nodes.TestNode2.should.have.property("module","node-red"); + module.nodes.TestNode2.should.have.property("name","TestNode2"); + module.nodes.TestNode2.should.have.property("file"); + module.nodes.TestNode2.should.have.property("template"); + module.nodes.TestNode2.should.have.property("enabled",true); + module.nodes.TestNode2.should.have.property("loaded",true); + module.nodes.TestNode2.should.have.property("types"); + module.nodes.TestNode2.types.should.have.a.length(1); + module.nodes.TestNode2.types[0].should.eql('test-node-2'); + module.nodes.TestNode2.should.have.property("config"); + module.nodes.TestNode2.should.have.property("help"); + module.nodes.TestNode2.should.have.property("namespace","node-red"); + module.nodes.TestNode2.should.not.have.property('err'); nodes.registerType.calledOnce.should.be.true(); nodes.registerType.lastCall.args[0].should.eql('node-red/TestNode2'); nodes.registerType.lastCall.args[1].should.eql('test-node-2'); done(); - }).otherwise(function(err) { + }).catch(function(err) { done(err); }); }); @@ -230,6 +252,7 @@ describe("red/nodes/registry/loader",function() { var result = {}; result["node-red"] = { "name": "node-red", + "version":"1.2.3", "nodes": { "TestNode3": { "file": path.join(resourcesDir,"TestNode3","TestNode3.js"), @@ -242,32 +265,38 @@ describe("red/nodes/registry/loader",function() { })); stubs.push(sinon.stub(registry,"saveNodeList", function(){ return })); - stubs.push(sinon.stub(registry,"addNodeSet", function(){ return })); + stubs.push(sinon.stub(registry,"addModule", function(){ return })); // This module isn't already loaded stubs.push(sinon.stub(registry,"getNodeInfo", function(){ return null; })); stubs.push(sinon.stub(nodes,"registerType")); loader.init({nodes:nodes,i18n:{defaultLang:"en-US"},events:{on:function(){},removeListener:function(){}},log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}}); loader.load().then(function(result) { - registry.addNodeSet.called.should.be.true(); - registry.addNodeSet.lastCall.args[0].should.eql("node-red/TestNode3"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('id',"node-red/TestNode3"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('module',"node-red"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('enabled',true); - registry.addNodeSet.lastCall.args[1].should.have.a.property('loaded',false); - registry.addNodeSet.lastCall.args[1].should.have.a.property('version',undefined); - registry.addNodeSet.lastCall.args[1].should.have.a.property('types'); - registry.addNodeSet.lastCall.args[1].types.should.have.a.length(1); - registry.addNodeSet.lastCall.args[1].types[0].should.eql('test-node-3'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('config'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('help'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('namespace','node-red'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('err','fail'); + registry.addModule.called.should.be.true(); + var module = registry.addModule.lastCall.args[0]; + module.should.have.property("name","node-red"); + module.should.have.property("version","1.2.3"); + module.should.have.property("nodes"); + module.nodes.should.have.property("TestNode3"); + module.nodes.TestNode3.should.have.property("id","node-red/TestNode3"); + module.nodes.TestNode3.should.have.property("module","node-red"); + module.nodes.TestNode3.should.have.property("name","TestNode3"); + module.nodes.TestNode3.should.have.property("file"); + module.nodes.TestNode3.should.have.property("template"); + module.nodes.TestNode3.should.have.property("enabled",true); + module.nodes.TestNode3.should.have.property("loaded",false); + module.nodes.TestNode3.should.have.property("types"); + module.nodes.TestNode3.types.should.have.a.length(1); + module.nodes.TestNode3.types[0].should.eql('test-node-3'); + module.nodes.TestNode3.should.have.property("config"); + module.nodes.TestNode3.should.have.property("help"); + module.nodes.TestNode3.should.have.property("namespace","node-red"); + module.nodes.TestNode3.should.have.property('err','fail'); - nodes.registerType.calledOnce.should.be.false(); + nodes.registerType.called.should.be.false(); done(); - }).otherwise(function(err) { + }).catch(function(err) { done(err); }); }); @@ -277,6 +306,7 @@ describe("red/nodes/registry/loader",function() { var result = {}; result["node-red"] = { "name": "node-red", + "version":"1.2.3", "nodes": { "DoesNotExist": { "file": path.join(resourcesDir,"doesnotexist"), @@ -289,31 +319,37 @@ describe("red/nodes/registry/loader",function() { })); stubs.push(sinon.stub(registry,"saveNodeList", function(){ return })); - stubs.push(sinon.stub(registry,"addNodeSet", function(){ return })); + stubs.push(sinon.stub(registry,"addModule", function(){ return })); // This module isn't already loaded stubs.push(sinon.stub(registry,"getNodeInfo", function(){ return null; })); stubs.push(sinon.stub(nodes,"registerType")); loader.init({nodes:nodes,i18n:{defaultLang:"en-US"},events:{on:function(){},removeListener:function(){}},log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}}); loader.load().then(function(result) { - registry.addNodeSet.called.should.be.true(); - registry.addNodeSet.lastCall.args[0].should.eql("node-red/DoesNotExist"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('id',"node-red/DoesNotExist"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('module',"node-red"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('enabled',true); - registry.addNodeSet.lastCall.args[1].should.have.a.property('loaded',false); - registry.addNodeSet.lastCall.args[1].should.have.a.property('version',undefined); - registry.addNodeSet.lastCall.args[1].should.have.a.property('types'); - registry.addNodeSet.lastCall.args[1].types.should.have.a.length(0); - registry.addNodeSet.lastCall.args[1].should.not.have.a.property('config'); - registry.addNodeSet.lastCall.args[1].should.not.have.a.property('help'); - registry.addNodeSet.lastCall.args[1].should.not.have.a.property('namespace','node-red'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('err'); + registry.addModule.called.should.be.true(); + var module = registry.addModule.lastCall.args[0]; + module.should.have.property("name","node-red"); + module.should.have.property("version","1.2.3"); + module.should.have.property("nodes"); + module.nodes.should.have.property("DoesNotExist"); + module.nodes.DoesNotExist.should.have.property("id","node-red/DoesNotExist"); + module.nodes.DoesNotExist.should.have.property("module","node-red"); + module.nodes.DoesNotExist.should.have.property("name","DoesNotExist"); + module.nodes.DoesNotExist.should.have.property("file"); + module.nodes.DoesNotExist.should.have.property("template"); + module.nodes.DoesNotExist.should.have.property("enabled",true); + module.nodes.DoesNotExist.should.have.property("loaded",false); + module.nodes.DoesNotExist.should.have.property("types"); + module.nodes.DoesNotExist.types.should.have.a.length(0); + module.nodes.DoesNotExist.should.not.have.property("config"); + module.nodes.DoesNotExist.should.not.have.property("help"); + module.nodes.DoesNotExist.should.not.have.property("namespace","node-red"); + module.nodes.DoesNotExist.should.have.property('err'); - nodes.registerType.calledOnce.should.be.false(); + nodes.registerType.called.should.be.false(); done(); - }).otherwise(function(err) { + }).catch(function(err) { done(err); }); }); @@ -323,6 +359,7 @@ describe("red/nodes/registry/loader",function() { var result = {}; result["node-red"] = { "name": "node-red", + "version": "1.2.3", "nodes": { "DuffNode": { "file": path.join(resourcesDir,"DuffNode","DuffNode.js"), @@ -335,32 +372,39 @@ describe("red/nodes/registry/loader",function() { })); stubs.push(sinon.stub(registry,"saveNodeList", function(){ return })); - stubs.push(sinon.stub(registry,"addNodeSet", function(){ return })); + stubs.push(sinon.stub(registry,"addModule", function(){ return })); // This module isn't already loaded stubs.push(sinon.stub(registry,"getNodeInfo", function(){ return null; })); stubs.push(sinon.stub(nodes,"registerType")); loader.init({nodes:nodes,i18n:{defaultLang:"en-US"},events:{on:function(){},removeListener:function(){}},log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}}); loader.load().then(function(result) { - registry.addNodeSet.called.should.be.true(); - registry.addNodeSet.lastCall.args[0].should.eql("node-red/DuffNode"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('id',"node-red/DuffNode"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('module',"node-red"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('enabled',true); - registry.addNodeSet.lastCall.args[1].should.have.a.property('loaded',false); - registry.addNodeSet.lastCall.args[1].should.have.a.property('version',undefined); - registry.addNodeSet.lastCall.args[1].should.have.a.property('types'); - registry.addNodeSet.lastCall.args[1].types.should.have.a.length(0); - registry.addNodeSet.lastCall.args[1].should.not.have.a.property('config'); - registry.addNodeSet.lastCall.args[1].should.not.have.a.property('help'); - registry.addNodeSet.lastCall.args[1].should.not.have.a.property('namespace','node-red'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('err'); - registry.addNodeSet.lastCall.args[1].err.should.endWith("DuffNode.html does not exist"); - nodes.registerType.calledOnce.should.be.false(); + registry.addModule.called.should.be.true(); + var module = registry.addModule.lastCall.args[0]; + module.should.have.property("name","node-red"); + module.should.have.property("version","1.2.3"); + module.should.have.property("nodes"); + module.nodes.should.have.property("DuffNode"); + module.nodes.DuffNode.should.have.property("id","node-red/DuffNode"); + module.nodes.DuffNode.should.have.property("module","node-red"); + module.nodes.DuffNode.should.have.property("name","DuffNode"); + module.nodes.DuffNode.should.have.property("file"); + module.nodes.DuffNode.should.have.property("template"); + module.nodes.DuffNode.should.have.property("enabled",true); + module.nodes.DuffNode.should.have.property("loaded",false); + module.nodes.DuffNode.should.have.property("types"); + module.nodes.DuffNode.types.should.have.a.length(0); + module.nodes.DuffNode.should.not.have.property("config"); + module.nodes.DuffNode.should.not.have.property("help"); + module.nodes.DuffNode.should.not.have.property("namespace","node-red"); + module.nodes.DuffNode.should.have.property('err'); + module.nodes.DuffNode.err.should.endWith("DuffNode.html does not exist"); + + nodes.registerType.called.should.be.false(); done(); - }).otherwise(function(err) { + }).catch(function(err) { done(err); }); }); @@ -379,7 +423,7 @@ describe("red/nodes/registry/loader",function() { stubs.push(sinon.stub(registry,"getModuleInfo",function(){return{}})); loader.init({nodes:nodes,i18n:{defaultLang:"en-US"},events:{on:function(){},removeListener:function(){}},log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}}); - loader.addModule("test-module").otherwise(function(err) { + loader.addModule("test-module").catch(function(err) { err.code.should.eql("module_already_loaded"); done(); }); @@ -390,7 +434,7 @@ describe("red/nodes/registry/loader",function() { throw new Error("failure"); })); loader.init({nodes:nodes,i18n:{defaultLang:"en-US"},events:{on:function(){},removeListener:function(){}},log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}}); - loader.addModule("test-module").otherwise(function(err) { + loader.addModule("test-module").catch(function(err) { err.message.should.eql("failure"); done(); }); @@ -419,29 +463,36 @@ describe("red/nodes/registry/loader",function() { })); stubs.push(sinon.stub(registry,"saveNodeList", function(){ return "a node list" })); - stubs.push(sinon.stub(registry,"addNodeSet", function(){ return })); + stubs.push(sinon.stub(registry,"addModule", function(){ return })); stubs.push(sinon.stub(nodes,"registerType")); loader.init({nodes:nodes,i18n:{defaultLang:"en-US"},events:{on:function(){},removeListener:function(){}},log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}}); loader.addModule("TestNodeModule").then(function(result) { result.should.eql("a node list"); - registry.addNodeSet.calledOnce.should.be.true(); - registry.addNodeSet.lastCall.args[0].should.eql("TestNodeModule/TestNode1"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('id',"TestNodeModule/TestNode1"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('module',"TestNodeModule"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('enabled',true); - registry.addNodeSet.lastCall.args[1].should.have.a.property('loaded',true); - registry.addNodeSet.lastCall.args[1].should.have.a.property('version',"1.2.3"); - registry.addNodeSet.lastCall.args[1].should.have.a.property('types'); - registry.addNodeSet.lastCall.args[1].types.should.have.a.length(1); - registry.addNodeSet.lastCall.args[1].types[0].should.eql('test-node-mod-1'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('config'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('help'); - registry.addNodeSet.lastCall.args[1].should.have.a.property('namespace','TestNodeModule'); - registry.addNodeSet.lastCall.args[1].should.not.have.a.property('err'); + + registry.addModule.called.should.be.true(); + var module = registry.addModule.lastCall.args[0]; + module.should.have.property("name","TestNodeModule"); + module.should.have.property("version","1.2.3"); + module.should.have.property("nodes"); + module.nodes.should.have.property("TestNode1"); + module.nodes.TestNode1.should.have.property("id","TestNodeModule/TestNode1"); + module.nodes.TestNode1.should.have.property("module","TestNodeModule"); + module.nodes.TestNode1.should.have.property("name","TestNode1"); + module.nodes.TestNode1.should.have.property("file"); + module.nodes.TestNode1.should.have.property("template"); + module.nodes.TestNode1.should.have.property("enabled",true); + module.nodes.TestNode1.should.have.property("loaded",true); + module.nodes.TestNode1.should.have.property("types"); + module.nodes.TestNode1.types.should.have.a.length(1); + module.nodes.TestNode1.types[0].should.eql('test-node-mod-1'); + module.nodes.TestNode1.should.have.property("config"); + module.nodes.TestNode1.should.have.property("help"); + module.nodes.TestNode1.should.have.property("namespace","TestNodeModule"); + module.nodes.TestNode1.should.not.have.property('err'); nodes.registerType.calledOnce.should.be.true(); done(); - }).otherwise(function(err) { + }).catch(function(err) { done(err); }); }); @@ -469,15 +520,15 @@ describe("red/nodes/registry/loader",function() { })); stubs.push(sinon.stub(registry,"saveNodeList", function(){ return "a node list" })); - stubs.push(sinon.stub(registry,"addNodeSet", function(){ return })); + stubs.push(sinon.stub(registry,"addModule", function(){ return })); stubs.push(sinon.stub(nodes,"registerType")); loader.init({log:{"_":function(){},warn:function(){}},nodes:nodes,i18n:{defaultLang:"en-US"},events:{on:function(){},removeListener:function(){}},version: function() { return "0.12.0"}, settings:{available:function(){return true;}}}); loader.addModule("TestNodeModule").then(function(result) { result.should.eql("a node list"); - registry.addNodeSet.called.should.be.false(); + registry.addModule.called.should.be.false(); nodes.registerType.called.should.be.false(); done(); - }).otherwise(function(err) { + }).catch(function(err) { done(err); }); }); @@ -498,7 +549,7 @@ describe("red/nodes/registry/loader",function() { node.enabled.should.be.false(); nodes.registerType.called.should.be.false(); done(); - }).otherwise(function(err) { + }).catch(function(err) { done(err); }); }); @@ -517,7 +568,7 @@ describe("red/nodes/registry/loader",function() { node.err.toString().should.eql("Error: fail to require (line:1)"); done(); - }).otherwise(function(err) { + }).catch(function(err) { done(err); }); }); diff --git a/test/red/runtime/nodes/registry/localfilesystem_spec.js b/test/red/runtime/nodes/registry/localfilesystem_spec.js index d240e2ca5..2ffc220f4 100644 --- a/test/red/runtime/nodes/registry/localfilesystem_spec.js +++ b/test/red/runtime/nodes/registry/localfilesystem_spec.js @@ -166,7 +166,15 @@ describe("red/nodes/registry/localfilesystem",function() { }}, settings:{coreNodesDir:resourcesDir} }); - localfilesystem.getNodeFiles(true); + var list = localfilesystem.getNodeFiles(true); + list.should.have.property("node-red"); + list["node-red"].should.have.property("icons"); + list["node-red"].icons.should.have.length(2); + //list["node-red"].icons[1].should.have.property("path",path.join(__dirname,"resources/local/NestedDirectoryNode/NestedNode/icons")) + list["node-red"].icons[1].should.have.property("icons"); + list["node-red"].icons[1].icons.should.have.length(1); + list["node-red"].icons[1].icons[0].should.eql("arrow-in.png"); + done(); }); it("scans icons dir in library",function(done) { var count = 0; @@ -188,7 +196,15 @@ describe("red/nodes/registry/localfilesystem",function() { }}, settings:{userDir:userDir} }); - localfilesystem.getNodeFiles(true); + var list = localfilesystem.getNodeFiles(true); + list.should.have.property("node-red"); + list["node-red"].should.have.property("icons"); + list["node-red"].icons.should.have.length(2); + //list["node-red"].icons[1].should.have.property("path",path.join(__dirname,"resources/userDir/lib/icons")) + list["node-red"].icons[1].should.have.property("icons"); + list["node-red"].icons[1].icons.should.have.length(1); + list["node-red"].icons[1].icons[0].should.eql("test_icon.png"); + done(); }); }); describe("#getModuleFiles",function() { @@ -256,6 +272,14 @@ describe("red/nodes/registry/localfilesystem",function() { settings:{coreNodesDir:moduleDir} }); var nodeModule = localfilesystem.getModuleFiles('TestNodeModule'); + nodeModule.should.have.property("TestNodeModule"); + nodeModule.TestNodeModule.should.have.property('icons'); + + nodeModule.TestNodeModule.icons.should.have.length(1); + nodeModule.TestNodeModule.icons[0].should.have.property("path"); + nodeModule.TestNodeModule.icons[0].should.have.property("icons"); + nodeModule.TestNodeModule.icons[0].icons[0].should.eql("arrow-in.png"); + done(); }); }); }); diff --git a/test/red/runtime/nodes/registry/registry_spec.js b/test/red/runtime/nodes/registry/registry_spec.js index cdfba8c45..c5b9f6b5a 100644 --- a/test/red/runtime/nodes/registry/registry_spec.js +++ b/test/red/runtime/nodes/registry/registry_spec.js @@ -132,7 +132,7 @@ describe("red/nodes/registry/registry",function() { }); - describe('#addNodeSet', function() { + describe.skip('#addNodeSet', function() { it('adds a node set for an unknown module', function() { typeRegistry.init(settings); @@ -290,29 +290,34 @@ describe("red/nodes/registry/registry",function() { describe('#saveNodeList',function() { it('rejects when settings unavailable',function(done) { - typeRegistry.init(stubSettings({},false,{})); - typeRegistry.addNodeSet("test-module/test-name",testNodeSet1, "0.0.1"); - typeRegistry.saveNodeList().otherwise(function(err) { + typeRegistry.init(stubSettings({},false,{}),null,events); + typeRegistry.addModule({name: "test-module",version:"0.0.1",nodes: {"test-name":{module:"test-module",name:"test-name",types:[]}}}); + typeRegistry.saveNodeList().catch(function(err) { done(); }); }); it('saves the list',function(done) { var s = stubSettings({},true,{}); typeRegistry.init(s); - typeRegistry.addNodeSet("test-module/test-name",testNodeSet1, "0.0.1"); - typeRegistry.addNodeSet("test-module/test-name-2",testNodeSet2WithError, "0.0.1"); + typeRegistry.addModule({name: "test-module",version:"0.0.1",nodes: { + "test-name":testNodeSet1, + "test-name-2":testNodeSet2WithError + }}); + typeRegistry.saveNodeList().then(function() { s.set.called.should.be.true(); s.set.lastCall.args[0].should.eql('nodes'); var nodes = s.set.lastCall.args[1]; nodes.should.have.property('test-module'); for (var n in nodes['test-module'].nodes) { - var nn = nodes['test-module'].nodes[n]; - nn.should.not.have.property('err'); - nn.should.not.have.property('id'); + if (nodes['test-module'].nodes.hasOwnProperty(n)) { + var nn = nodes['test-module'].nodes[n]; + nn.should.not.have.property('err'); + nn.should.not.have.property('id'); + } } done(); - }).otherwise(function(err) { + }).catch(function(err) { done(err); }); }); @@ -338,7 +343,9 @@ describe("red/nodes/registry/registry",function() { it('removes a known module', function() { var s = stubSettings({},true,{}); typeRegistry.init(s); - typeRegistry.addNodeSet("test-module/test-name",testNodeSet1, "0.0.1"); + typeRegistry.addModule({name: "test-module",version:"0.0.1",nodes: { + "test-name":testNodeSet1 + }}); var moduleList = typeRegistry.getModuleList(); moduleList.should.have.a.property("test-module"); typeRegistry.getNodeList().should.have.lengthOf(1); @@ -355,27 +362,28 @@ describe("red/nodes/registry/registry",function() { typeRegistry.init(settings,{ getNodeHelp: function(config) { return "HE"+config.name+"LP" } }); - typeRegistry.addNodeSet("test-module/test-name",{ - id: "test-module/test-name", - module: "test-module", - name: "test-name", - enabled: true, - loaded: false, - config: "configA", - types: [ "test-a","test-b"] - }, "0.0.1"); - typeRegistry.getNodeConfig("test-module/test-name").should.eql('\nconfigAHEtest-nameLP'); - typeRegistry.getAllNodeConfigs().should.eql('\n\nconfigAHEtest-nameLP'); - typeRegistry.addNodeSet("test-module/test-name-2",{ - id: "test-module/test-name-2", - module: "test-module", - name: "test-name-2", - enabled: true, - loaded: false, - config: "configB", - types: [ "test-c","test-d"] - }, "0.0.1"); + typeRegistry.addModule({name: "test-module",version:"0.0.1",nodes: { + "test-name":{ + id: "test-module/test-name", + module: "test-module", + name: "test-name", + enabled: true, + loaded: false, + config: "configA", + types: [ "test-a","test-b"] + }, + "test-name-2":{ + id: "test-module/test-name-2", + module: "test-module", + name: "test-name-2", + enabled: true, + loaded: false, + config: "configB", + types: [ "test-c","test-d"] + } + }}); + typeRegistry.getNodeConfig("test-module/test-name").should.eql('\nconfigAHEtest-nameLP'); typeRegistry.getNodeConfig("test-module/test-name-2").should.eql('\nconfigBHEtest-name-2LP'); typeRegistry.getAllNodeConfigs().should.eql('\n\nconfigAHEtest-nameLP\n\nconfigBHEtest-name-2LP'); }); @@ -383,16 +391,18 @@ describe("red/nodes/registry/registry",function() { describe('#getModuleInfo', function() { it('returns module info', function() { typeRegistry.init(settings,{}); - typeRegistry.addNodeSet("test-module/test-name",{ - id: "test-module/test-name", - module: "test-module", - name: "test-name", - enabled: true, - loaded: false, - config: "configA", - types: [ "test-a","test-b"], - file: "abc" - }, "0.0.1"); + typeRegistry.addModule({name: "test-module",version:"0.0.1",nodes: { + "test-name":{ + id: "test-module/test-name", + module: "test-module", + name: "test-name", + enabled: true, + loaded: false, + config: "configA", + types: [ "test-a","test-b"], + file: "abc" + } + }}); var moduleInfo = typeRegistry.getModuleInfo("test-module"); moduleInfo.should.have.a.property('name','test-module'); moduleInfo.should.have.a.property('version','0.0.1'); @@ -405,16 +415,18 @@ describe("red/nodes/registry/registry",function() { describe('#getNodeInfo', function() { it('returns node info', function() { typeRegistry.init(settings,{}); - typeRegistry.addNodeSet("test-module/test-name",{ - id: "test-module/test-name", - module: "test-module", - name: "test-name", - enabled: true, - loaded: false, - config: "configA", - types: [ "test-a","test-b"], - file: "abc" - }, "0.0.1"); + typeRegistry.addModule({name: "test-module",version:"0.0.1",nodes: { + "test-name":{ + id: "test-module/test-name", + module: "test-module", + name: "test-name", + enabled: true, + loaded: false, + config: "configA", + types: [ "test-a","test-b"], + file: "abc" + } + }}); var nodeSetInfo = typeRegistry.getNodeInfo("test-module/test-name"); nodeSetInfo.should.have.a.property('id',"test-module/test-name"); nodeSetInfo.should.not.have.a.property('config'); @@ -424,17 +436,19 @@ describe("red/nodes/registry/registry",function() { describe('#getFullNodeInfo', function() { it('returns node info', function() { typeRegistry.init(settings,{}); - typeRegistry.addNodeSet("test-module/test-name",{ - id: "test-module/test-name", - module: "test-module", - name: "test-name", - enabled: true, - loaded: false, - config: "configA", - types: [ "test-a","test-b"], - file: "abc" + typeRegistry.addModule({name: "test-module",version:"0.0.1",nodes: { + "test-name":{ + id: "test-module/test-name", + module: "test-module", + name: "test-name", + enabled: true, + loaded: false, + config: "configA", + types: [ "test-a","test-b"], + file: "abc" - }, "0.0.1"); + } + }}); var nodeSetInfo = typeRegistry.getFullNodeInfo("test-module/test-name"); nodeSetInfo.should.have.a.property('id',"test-module/test-name"); nodeSetInfo.should.have.a.property('config'); @@ -447,26 +461,28 @@ describe("red/nodes/registry/registry",function() { describe('#getNodeList', function() { it("returns a filtered list", function() { typeRegistry.init(settings,{}); - typeRegistry.addNodeSet("test-module/test-name",{ - id: "test-module/test-name", - module: "test-module", - name: "test-name", - enabled: true, - loaded: false, - config: "configA", - types: [ "test-a","test-b"], - file: "abc" - }, "0.0.1"); - typeRegistry.addNodeSet("test-module/test-name-2",{ - id: "test-module/test-name-2", - module: "test-module", - name: "test-name-2", - enabled: true, - loaded: false, - config: "configB", - types: [ "test-c","test-d"], - file: "def" - }, "0.0.1"); + typeRegistry.addModule({name: "test-module",version:"0.0.1",nodes: { + "test-name":{ + id: "test-module/test-name", + module: "test-module", + name: "test-name", + enabled: true, + loaded: false, + config: "configA", + types: [ "test-a","test-b"], + file: "abc" + }, + "test-name-2":{ + id: "test-module/test-name-2", + module: "test-module", + name: "test-name-2", + enabled: true, + loaded: false, + config: "configB", + types: [ "test-c","test-d"], + file: "def" + } + }}); var filterCallCount = 0; var filteredList = typeRegistry.getNodeList(function(n) { filterCallCount++; return n.name === 'test-name-2';}); filterCallCount.should.eql(2); @@ -532,10 +548,22 @@ describe("red/nodes/registry/registry",function() { }); it('returns a registered icon' , function() { - var testIcon = path.resolve(__dirname+'/../../../../resources/icons/test_icon.png'); - events.emit("node-icon-dir",{name:"test-module", path: path.resolve(__dirname+'/../../../../resources/icons'), icons:[]}); + var testIcon = path.resolve(__dirname+'/../../../../resources/icons/'); + typeRegistry.init(settings,{}); + typeRegistry.addModule({name: "test-module",version:"0.0.1",nodes: { + "test-name":{ + id: "test-module/test-name", + module: "test-module", + name: "test-name", + enabled: true, + loaded: false, + config: "configA", + types: [ "test-a","test-b"], + file: "abc" + } + },icons: [{path:testIcon,icons:['test_icon.png']}]}); var iconPath = typeRegistry.getNodeIconPath('test-module','test_icon.png'); - iconPath.should.eql(testIcon); + iconPath.should.eql(testIcon+"/test_icon.png"); }); it('returns the debug icon when getting an unknown module', function() { @@ -552,14 +580,26 @@ describe("red/nodes/registry/registry",function() { }); it('returns an icon list of registered node module', function() { - typeRegistry.addNodeSet("test-module/test-name",testNodeSet1,"0.0.1"); - events.emit("node-icon-dir",{name:"test-module", path:"",icons:["test_icon1.png"]}); + var testIcon = path.resolve(__dirname+'/resources/userDir/lib/icons/'); + typeRegistry.init(settings,{},events); + typeRegistry.addModule({name: "test-module",version:"0.0.1",nodes: { + "test-name":{ + id: "test-module/test-name", + module: "test-module", + name: "test-name", + enabled: true, + loaded: false, + config: "configA", + types: [ "test-a","test-b"], + file: "abc" + } + },icons: [{path:testIcon,icons:['test_icon.png']}]}); var iconList = typeRegistry.getNodeIcons(); - iconList.should.eql({"test-module":["test_icon1.png"]}); + iconList.should.eql({"test-module":["test_icon.png"]}); }); - it('returns an icon list of unregistered node module', function() { - events.emit("node-icon-dir",{name:"test-module", path:"", icons:["test_icon1.png", "test_icon2.png"]}); + it.skip('returns an icon list of unregistered node module', function() { + // events.emit("node-icon-dir",{name:"test-module", path:"", icons:["test_icon1.png", "test_icon2.png"]}); var iconList = typeRegistry.getNodeIcons(); iconList.should.eql({"test-module":["test_icon1.png","test_icon2.png"]}); }); diff --git a/test/red/runtime/nodes/resources/local/NestedDirectoryNode/NestedNode/icons/arrow-in.png b/test/red/runtime/nodes/resources/local/NestedDirectoryNode/NestedNode/icons/arrow-in.png new file mode 100644 index 0000000000000000000000000000000000000000..e38f3914600901b736f5fa18786ee11be6d41c28 GIT binary patch literal 393 zcmeAS@N?(olHy`uVBq!ia0vp^B0wz1!3HFCgzU0`6icy_X9x!n)NrJ90QsB+9+AZi z3~X;em{G3O!W1YdS>hT|5}cn_Ql40p%1~Zju9umYU7Va)kgAtols@~NjT8d|Bb%p- zV~9uR+N-+$hZ97Oeyp#Y$RU!&XX#m>@|$agvYL9Jz$O6}72y^gZVi>ZMeaH^t*Q|! zLhVk<0xdko8zYVo#8M-LBBd8b>1kw|i?FO)bWqsy_rwQg27N4y zeX6pqO$_Cex^^A7_NsS@+=bOASA^wN0&GKN(hlAguRU&q- jTBY?`*L>xN+|S&+7B*edvmQhOgN?z{)z4*}Q$iB}+9!`i literal 0 HcmV?d00001 diff --git a/test/red/runtime/nodes/resources/local/NestedDirectoryNode/NestedNode/icons/file.txt b/test/red/runtime/nodes/resources/local/NestedDirectoryNode/NestedNode/icons/file.txt deleted file mode 100644 index 59a29af14..000000000 --- a/test/red/runtime/nodes/resources/local/NestedDirectoryNode/NestedNode/icons/file.txt +++ /dev/null @@ -1,3 +0,0 @@ -This file exists just to ensure the 'icons' directory is in the repository. -TODO: a future test needs to ensure the right icon files are loaded - this - directory can be used for that diff --git a/test/red/runtime/nodes/resources/local/TestNodeModule/node_modules/TestNodeModule/icons/file.txt b/test/red/runtime/nodes/resources/local/TestNodeModule/node_modules/TestNodeModule/icons/file.txt deleted file mode 100644 index 59a29af14..000000000 --- a/test/red/runtime/nodes/resources/local/TestNodeModule/node_modules/TestNodeModule/icons/file.txt +++ /dev/null @@ -1,3 +0,0 @@ -This file exists just to ensure the 'icons' directory is in the repository. -TODO: a future test needs to ensure the right icon files are loaded - this - directory can be used for that From 184b1b018ce0e4c041370f2aa914663864432b89 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 21 May 2018 22:38:07 +0100 Subject: [PATCH 6/9] Add missing resource file --- .../node_modules/TestNodeModule/icons/arrow-in.png | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 test/red/runtime/nodes/resources/local/TestNodeModule/node_modules/TestNodeModule/icons/arrow-in.png diff --git a/test/red/runtime/nodes/resources/local/TestNodeModule/node_modules/TestNodeModule/icons/arrow-in.png b/test/red/runtime/nodes/resources/local/TestNodeModule/node_modules/TestNodeModule/icons/arrow-in.png new file mode 100644 index 000000000..59a29af14 --- /dev/null +++ b/test/red/runtime/nodes/resources/local/TestNodeModule/node_modules/TestNodeModule/icons/arrow-in.png @@ -0,0 +1,3 @@ +This file exists just to ensure the 'icons' directory is in the repository. +TODO: a future test needs to ensure the right icon files are loaded - this + directory can be used for that From 36105412b1b8f27ce9344c62a39d1c40c9bdf184 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 22 May 2018 11:41:22 +0100 Subject: [PATCH 7/9] Add 'private' property to userDir generated package.json This stops the warnings from npm about missing repo and license fields. As there's no expectation for a user to publish their userDir to npm, then setting private is entirely appropriate. --- red/runtime/storage/localfilesystem/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/red/runtime/storage/localfilesystem/index.js b/red/runtime/storage/localfilesystem/index.js index 2a57c7323..5a33aaecf 100644 --- a/red/runtime/storage/localfilesystem/index.js +++ b/red/runtime/storage/localfilesystem/index.js @@ -72,7 +72,8 @@ var localfilesystem = { var defaultPackage = { "name": "node-red-project", "description": "A Node-RED Project", - "version": "0.0.1" + "version": "0.0.1", + "private": true }; return util.writeFile(packageFile,JSON.stringify(defaultPackage,"",4)); } From a4eb8e11c3df0805ef347eb14ba4c77674638b47 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 23 May 2018 10:25:10 +0100 Subject: [PATCH 8/9] Collapse sidebar tabs --- editor/js/ui/common/popover.js | 8 +- editor/js/ui/common/tabs.js | 194 ++++++++++++++++---- editor/js/ui/projects/tab-versionControl.js | 2 + editor/js/ui/sidebar.js | 7 +- editor/js/ui/tab-config.js | 3 +- editor/js/ui/tab-info.js | 2 + editor/sass/popover.scss | 30 ++- editor/sass/tabs.scss | 61 +++++- nodes/core/core/58-debug.html | 4 +- 9 files changed, 269 insertions(+), 42 deletions(-) diff --git a/editor/js/ui/common/popover.js b/editor/js/ui/common/popover.js index 6a0bc9fdf..0170f7070 100644 --- a/editor/js/ui/common/popover.js +++ b/editor/js/ui/common/popover.js @@ -19,12 +19,14 @@ RED.popover = (function() { "default": { top: 10, leftRight: 17, - leftLeft: 25 + leftLeft: 25, + leftBottom: 8, }, "small": { top: 5, leftRight: 17, - leftLeft: 16 + leftLeft: 16, + leftBottom: 3, } } function createPopover(options) { @@ -69,6 +71,8 @@ RED.popover = (function() { div.css({top: targetPos.top+targetHeight/2-divHeight/2-deltaSizes[size].top,left:targetPos.left+targetWidth+deltaSizes[size].leftRight}); } else if (direction === 'left') { div.css({top: targetPos.top+targetHeight/2-divHeight/2-deltaSizes[size].top,left:targetPos.left-deltaSizes[size].leftLeft-divWidth}); + } else if (direction === 'bottom') { + div.css({top: targetPos.top+targetHeight+deltaSizes[size].top,left:targetPos.left+targetWidth/2-divWidth/2 - deltaSizes[size].leftBottom}); } if (instant) { div.show(); diff --git a/editor/js/ui/common/tabs.js b/editor/js/ui/common/tabs.js index f66b95421..23b69f5d1 100644 --- a/editor/js/ui/common/tabs.js +++ b/editor/js/ui/common/tabs.js @@ -19,8 +19,10 @@ RED.tabs = (function() { function createTabs(options) { var tabs = {}; + var pinnedTabsCount = 0; var currentTabWidth; var currentActiveTabWidth = 0; + var collapsibleMenu; var ul = options.element || $("#"+options.id); var wrapper = ul.wrap( "
" ).parent(); @@ -50,6 +52,55 @@ RED.tabs = (function() { scrollRight = $('
').appendTo(wrapper).find("a"); scrollRight.on('mousedown',function(evt) { scrollEventHandler(evt,'+=150') }).on('click',function(evt){ evt.preventDefault();}); } + + if (options.collapsible) { + // var dropDown = $('
',{class:"red-ui-tabs-select"}).appendTo(wrapper); + // ul.hide(); + wrapper.addClass("red-ui-tabs-collapsible"); + + var collapsedButtonsRow = $('').appendTo(wrapper); + + var selectButton = $('').appendTo(collapsedButtonsRow); + selectButton.addClass("red-ui-tab-link-button-menu") + selectButton.click(function(evt) { + evt.preventDefault(); + if (!collapsibleMenu) { + var pinnedOptions = []; + var options = []; + ul.children().each(function(i,el) { + var id = $(el).data('tabId'); + var opt = { + id:"red-ui-tabs-menu-option-"+id, + label: tabs[id].name, + onselect: function() { + activateTab(id); + } + }; + if (tabs[id].pinned) { + pinnedOptions.push(opt); + } else { + options.push(opt); + } + }); + options = pinnedOptions.concat(options); + collapsibleMenu = RED.menu.init({id:"debug-message-option-menu",options: options}); + collapsibleMenu.css({ + position: "absolute" + }) + collapsibleMenu.on('mouseleave', function(){ $(this).hide() }); + collapsibleMenu.on('mouseup', function() { $(this).hide() }); + collapsibleMenu.appendTo("body"); + var elementPos = selectButton.offset(); + collapsibleMenu.css({ + top: (elementPos.top+selectButton.height()-20)+"px", + left: (elementPos.left - collapsibleMenu.width() + selectButton.width())+"px" + }) + } + collapsibleMenu.toggle(); + }) + + } + function scrollEventHandler(evt,dir) { evt.preventDefault(); if ($(this).hasClass('disabled')) { @@ -118,6 +169,9 @@ RED.tabs = (function() { ul.children().removeClass("active"); ul.children().css({"transition": "width 100ms"}); link.parent().addClass("active"); + var parentId = link.parent().attr('id'); + wrapper.find(".red-ui-tab-link-button").removeClass("active"); + $("#"+parentId+"-link-button").addClass("active"); if (options.scrollable) { var pos = link.parent().position().left; if (pos-21 < 0) { @@ -155,41 +209,70 @@ RED.tabs = (function() { var tabs = ul.find("li.red-ui-tab"); var width = wrapper.width(); var tabCount = tabs.size(); - var tabWidth = (width-12-(tabCount*6))/tabCount; - currentTabWidth = (100*tabWidth/width)+"%"; - currentActiveTabWidth = currentTabWidth+"%"; - if (options.scrollable) { - tabWidth = Math.max(tabWidth,140); - currentTabWidth = tabWidth+"px"; - currentActiveTabWidth = 0; - var listWidth = Math.max(wrapper.width(),12+(tabWidth+6)*tabCount); - ul.width(listWidth); - updateScroll(); - } else if (options.hasOwnProperty("minimumActiveTabWidth")) { - if (tabWidth < options.minimumActiveTabWidth) { - tabCount -= 1; - tabWidth = (width-12-options.minimumActiveTabWidth-(tabCount*6))/tabCount; - currentTabWidth = (100*tabWidth/width)+"%"; - currentActiveTabWidth = options.minimumActiveTabWidth+"px"; + var tabWidth; + + if (options.collapsible) { + tabWidth = width - collapsedButtonsRow.width()-10; + if (tabWidth < 198) { + var delta = 198 - tabWidth; + var b = collapsedButtonsRow.find("a:last").prev(); + while (b.is(":not(:visible)")) { + b = b.prev(); + } + if (!b.hasClass("red-ui-tab-link-button-pinned")) { + b.hide(); + } + tabWidth = width - collapsedButtonsRow.width()-10; } else { - currentActiveTabWidth = 0; + var space = width - 198 - collapsedButtonsRow.width(); + if (space > 40) { + collapsedButtonsRow.find("a:not(:visible):first").show(); + tabWidth = width - collapsedButtonsRow.width()-10; + } } - } - tabs.css({width:currentTabWidth}); - if (tabWidth < 50) { - ul.find(".red-ui-tab-close").hide(); - ul.find(".red-ui-tab-icon").hide(); - ul.find(".red-ui-tab-label").css({paddingLeft:Math.min(12,Math.max(0,tabWidth-38))+"px"}) + tabs.css({width:tabWidth}); + } else { - ul.find(".red-ui-tab-close").show(); - ul.find(".red-ui-tab-icon").show(); - ul.find(".red-ui-tab-label").css({paddingLeft:""}) - } - if (currentActiveTabWidth !== 0) { - ul.find("li.red-ui-tab.active").css({"width":options.minimumActiveTabWidth}); - ul.find("li.red-ui-tab.active .red-ui-tab-close").show(); - ul.find("li.red-ui-tab.active .red-ui-tab-icon").show(); - ul.find("li.red-ui-tab.active .red-ui-tab-label").css({paddingLeft:""}) + var tabWidth = (width-12-(tabCount*6))/tabCount; + currentTabWidth = (100*tabWidth/width)+"%"; + currentActiveTabWidth = currentTabWidth+"%"; + if (options.scrollable) { + tabWidth = Math.max(tabWidth,140); + currentTabWidth = tabWidth+"px"; + currentActiveTabWidth = 0; + var listWidth = Math.max(wrapper.width(),12+(tabWidth+6)*tabCount); + ul.width(listWidth); + updateScroll(); + } else if (options.hasOwnProperty("minimumActiveTabWidth")) { + if (tabWidth < options.minimumActiveTabWidth) { + tabCount -= 1; + tabWidth = (width-12-options.minimumActiveTabWidth-(tabCount*6))/tabCount; + currentTabWidth = (100*tabWidth/width)+"%"; + currentActiveTabWidth = options.minimumActiveTabWidth+"px"; + } else { + currentActiveTabWidth = 0; + } + } + if (options.collapsible) { + console.log(currentTabWidth); + } + + tabs.css({width:currentTabWidth}); + if (tabWidth < 50) { + ul.find(".red-ui-tab-close").hide(); + ul.find(".red-ui-tab-icon").hide(); + ul.find(".red-ui-tab-label").css({paddingLeft:Math.min(12,Math.max(0,tabWidth-38))+"px"}) + } else { + ul.find(".red-ui-tab-close").show(); + ul.find(".red-ui-tab-icon").show(); + ul.find(".red-ui-tab-label").css({paddingLeft:""}) + } + if (currentActiveTabWidth !== 0) { + ul.find("li.red-ui-tab.active").css({"width":options.minimumActiveTabWidth}); + ul.find("li.red-ui-tab.active .red-ui-tab-close").show(); + ul.find("li.red-ui-tab.active .red-ui-tab-icon").show(); + ul.find("li.red-ui-tab.active .red-ui-tab-label").css({paddingLeft:""}) + } } } @@ -210,11 +293,15 @@ RED.tabs = (function() { activateTab(tab.find("a")); } li.remove(); + if (tabs[id].pinned) { + pinnedTabsCount--; + } if (options.onremove) { options.onremove(tabs[id]); } delete tabs[id]; updateTabWidths(); + collapsibleMenu = null; } return { @@ -223,13 +310,55 @@ RED.tabs = (function() { var li = $("
  • ",{class:"red-ui-tab"}).appendTo(ul); li.attr('id',"red-ui-tab-"+(tab.id.replace(".","-"))); li.data("tabId",tab.id); + + if (options.maximumTabWidth) { + li.css("maxWidth",options.maximumTabWidth+"px"); + } var link = $("",{href:"#"+tab.id, class:"red-ui-tab-label"}).appendTo(li); if (tab.icon) { $('').appendTo(link); + } else if (tab.iconClass) { + $('',{class:"red-ui-tab-icon "+tab.iconClass}).appendTo(link); } var span = $('',{class:"bidiAware"}).text(tab.label).appendTo(link); span.attr('dir', RED.text.bidi.resolveBaseTextDir(tab.label)); + if (options.collapsible) { + li.addClass("red-ui-tab-pinned"); + var pinnedLink = $(''); + if (tab.pinned) { + if (pinnedTabsCount === 0) { + pinnedLink.prependTo(collapsedButtonsRow) + } else { + pinnedLink.insertAfter(collapsedButtonsRow.find("a.red-ui-tab-link-button-pinned:last")); + } + } else { + pinnedLink.insertBefore(collapsedButtonsRow.find("a:last")); + } + pinnedLink.attr('id',li.attr('id')+"-link-button"); + if (tab.iconClass) { + $('',{class:tab.iconClass}).appendTo(pinnedLink); + } else { + $('',{class:"fa fa-lemon-o"}).appendTo(pinnedLink); + } + pinnedLink.click(function(evt) { + evt.preventDefault(); + activateTab(tab.id); + }); + if (tab.pinned) { + pinnedLink.addClass("red-ui-tab-link-button-pinned"); + pinnedTabsCount++; + } + RED.popover.create({ + target:$(pinnedLink), + trigger: "hover", + size: "small", + direction: "bottom", + content: tab.name, + delay: { show: 550, hide: 10 } + }); + + } link.on("click",onTabClick); link.on("dblclick",onTabDblClick); if (tab.closeable) { @@ -326,6 +455,7 @@ RED.tabs = (function() { } }) } + collapsibleMenu = null; }, removeTab: removeTab, activateTab: activateTab, diff --git a/editor/js/ui/projects/tab-versionControl.js b/editor/js/ui/projects/tab-versionControl.js index 44b602019..4560a56e5 100644 --- a/editor/js/ui/projects/tab-versionControl.js +++ b/editor/js/ui/projects/tab-versionControl.js @@ -1003,6 +1003,8 @@ RED.sidebar.versionControl = (function() { name: "Project History", content: sidebarContent, enableOnEdit: false, + pinned: true, + iconClass: "fa fa-code-fork", onchange: function() { setTimeout(function() { sections.resize(); diff --git a/editor/js/ui/sidebar.js b/editor/js/ui/sidebar.js index 222217668..d9f302885 100644 --- a/editor/js/ui/sidebar.js +++ b/editor/js/ui/sidebar.js @@ -35,7 +35,8 @@ RED.sidebar = (function() { tab.onremove.call(tab); } }, - minimumActiveTabWidth: 70 + // minimumActiveTabWidth: 70, + collapsible: true // scrollable: true }); @@ -59,6 +60,8 @@ RED.sidebar = (function() { options = title; } + delete options.closeable; + options.wrapper = $('
    ',{style:"height:100%"}).appendTo("#sidebar-content") options.wrapper.append(options.content); options.wrapper.hide(); @@ -82,6 +85,8 @@ RED.sidebar = (function() { group: "sidebar-tabs" }); + options.iconClass = options.iconClass || "fa fa-square-o" + knownTabs[options.id] = options; if (options.visible !== false) { diff --git a/editor/js/ui/tab-config.js b/editor/js/ui/tab-config.js index 9a4619b28..00caea6cd 100644 --- a/editor/js/ui/tab-config.js +++ b/editor/js/ui/tab-config.js @@ -221,8 +221,7 @@ RED.sidebar.config = (function() { name: RED._("sidebar.config.name"), content: content, toolbar: toolbar, - closeable: true, - visible: false, + iconClass: "fa fa-cog", onchange: function() { refreshConfigNodeList(); } }); RED.actions.add("core:show-config-tab",function() {RED.sidebar.show('config')}); diff --git a/editor/js/ui/tab-info.js b/editor/js/ui/tab-info.js index b4cf87d46..340a6bd74 100644 --- a/editor/js/ui/tab-info.js +++ b/editor/js/ui/tab-info.js @@ -83,7 +83,9 @@ RED.sidebar.info = (function() { id: "info", label: RED._("sidebar.info.label"), name: RED._("sidebar.info.name"), + iconClass: "fa fa-info", content: content, + pinned: true, enableOnEdit: true }); if (tips.enabled()) { diff --git a/editor/sass/popover.scss b/editor/sass/popover.scss index d3c077e6d..691acd5c8 100644 --- a/editor/sass/popover.scss +++ b/editor/sass/popover.scss @@ -30,7 +30,6 @@ } .red-ui-popover:after, .red-ui-popover:before { - top: 50%; border: solid transparent; content: " "; height: 0; @@ -39,12 +38,18 @@ pointer-events: none; } .red-ui-popover.red-ui-popover-right:after, .red-ui-popover.red-ui-popover-right:before { + top: 50%; right: 100%; } .red-ui-popover.red-ui-popover-left:after, .red-ui-popover.red-ui-popover-left:before { + top: 50%; left: 100%; } +.red-ui-popover.red-ui-popover-bottom:after, .red-ui-popover.red-ui-popover-bottom:before { + bottom: 100%; + left: 50%; +} .red-ui-popover.red-ui-popover-right:after { border-color: rgba(136, 183, 213, 0); @@ -72,6 +77,21 @@ margin-top: -11px; } + +.red-ui-popover.red-ui-popover-bottom:after { + border-color: rgba(136, 183, 213, 0); + border-bottom-color: #fff; + border-width: 10px; + margin-left: -10px; +} +.red-ui-popover.red-ui-popover-bottom:before { + border-color: rgba(194, 225, 245, 0); + border-bottom-color: $primary-border-color; + border-width: 11px; + margin-left: -11px; +} + + .red-ui-popover-size-small { font-size: 11px; padding: 5px; @@ -93,4 +113,12 @@ border-width: 6px; margin-top: -6px; } + &.red-ui-popover-bottom:after { + border-width: 5px; + margin-left: -5px; + } + &.red-ui-popover-bottom:before { + border-width: 6px; + margin-left: -6px; + } } diff --git a/editor/sass/tabs.scss b/editor/sass/tabs.scss index 4b2dc7f5d..488f4f3c9 100644 --- a/editor/sass/tabs.scss +++ b/editor/sass/tabs.scss @@ -93,7 +93,7 @@ color: $workspace-button-color-hover; } } - .red-ui-tab-icon { + img.red-ui-tab-icon { opacity: 0.2; } } @@ -113,6 +113,21 @@ &.red-ui-tabs-add.red-ui-tabs-scrollable { padding-right: 59px; } + &.red-ui-tabs-collapsible { + li:not(.active) { + display: none; + &.red-ui-tab-pinned { + a { + padding-left: 0; + text-align: center; + } + span { + display: none; + } + width: 32px; + } + } + } &.red-ui-tabs-vertical { box-sizing: border-box; @@ -157,6 +172,15 @@ } } } + .red-ui-tabs-select { + position: absolute; + top:0; + bottom: 0; + left: 0; + right: 0; + opacity: 0.4; + background: red; + } } .red-ui-tab-button { position: absolute; @@ -180,7 +204,33 @@ z-index: 2; } } - +.red-ui-tab-link-buttons { + position: absolute; + box-sizing: border-box; + top: 0; + right: 0; + height: 35px; + background: #fff; + border-bottom: 1px solid $primary-border-color; + z-index: 2; + a { + @include workspace-button; + line-height: 26px; + height: 28px; + width: 28px; + margin: 4px 3px 3px; + border: 1px solid $primary-border-color; + z-index: 2; + &.red-ui-tab-link-button { + &:not(.active) { + background: #eee; + } + } + &.red-ui-tab-link-button-menu { + border-color: white; + } + } +} .red-ui-tab-scroll { width: 21px; top: 0; @@ -216,7 +266,7 @@ right: 38px; } -.red-ui-tab-icon { +img.red-ui-tab-icon { margin-left: -8px; margin-right: 3px; margin-top: -2px; @@ -225,6 +275,11 @@ height: 20px; vertical-align: middle; } +i.red-ui-tab-icon { + opacity: 0.7; + width: 18px; + height: 20px; +} .red-ui-tabs-badges { position: absolute; diff --git a/nodes/core/core/58-debug.html b/nodes/core/core/58-debug.html index 7317d7064..95b55d7f5 100644 --- a/nodes/core/core/58-debug.html +++ b/nodes/core/core/58-debug.html @@ -154,7 +154,9 @@ name: this._("debug.sidebar.name"), content: uiComponents.content, toolbar: uiComponents.footer, - enableOnEdit: true + enableOnEdit: true, + pinned: true, + iconClass: "fa fa-wrench" }); RED.actions.add("core:show-debug-tab",function() { RED.sidebar.show('debug'); }); From 26bc142cc25b03df42759b4bc3d8fe342911726c Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 23 May 2018 10:59:08 +0100 Subject: [PATCH 9/9] Handle loading empty nodesDir --- red/runtime/nodes/registry/localfilesystem.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/red/runtime/nodes/registry/localfilesystem.js b/red/runtime/nodes/registry/localfilesystem.js index b8a516597..b591245d5 100644 --- a/red/runtime/nodes/registry/localfilesystem.js +++ b/red/runtime/nodes/registry/localfilesystem.js @@ -88,7 +88,7 @@ function getLocalNodeFiles(dir) { try { files = fs.readdirSync(dir); } catch(err) { - return result; + return {files: [], icons: []}; } files.sort(); files.forEach(function(fn) {