From bba57f8d2b6ecc8accd58c794109c7f13c0a070f Mon Sep 17 00:00:00 2001 From: Kazuhito Yokoi Date: Mon, 6 Aug 2018 21:39:37 +0900 Subject: [PATCH 01/16] Add node description property UI --- editor/js/nodes.js | 4 ++++ editor/js/ui/editor.js | 22 ++++++++++++++++++++++ editor/js/ui/tab-info.js | 4 ++++ red/api/editor/locales/en-US/editor.json | 1 + red/api/editor/locales/ja/editor.json | 1 + 5 files changed, 32 insertions(+) diff --git a/editor/js/nodes.js b/editor/js/nodes.js index 12ff3e682..062654ca2 100644 --- a/editor/js/nodes.js +++ b/editor/js/nodes.js @@ -509,6 +509,9 @@ RED.nodes = (function() { node.icon = n.icon; } } + if (n.info) { + node.info = n.info; + } } return node; } @@ -947,6 +950,7 @@ RED.nodes = (function() { inputLabels: n.inputLabels, outputLabels: n.outputLabels, icon: n.icon, + info: n.info, changed:false, _config:{} }; diff --git a/editor/js/ui/editor.js b/editor/js/ui/editor.js index e0445be54..344914b5e 100644 --- a/editor/js/ui/editor.js +++ b/editor/js/ui/editor.js @@ -20,6 +20,7 @@ RED.editor = (function() { var editing_node = null; var editing_config_node = null; var subflowEditor; + var nodeInfoEditor; var editTrayWidthCache = {}; @@ -839,6 +840,20 @@ RED.editor = (function() { }) $('
').text(node.icon).appendTo(iconRow); } + + if (node.type.indexOf("subflow") != 0 && node.type !== "comment") { + $('
').appendTo(dialogForm); + $('
').appendTo(dialogForm); + $('
').appendTo(dialogForm); + nodeInfoEditor = RED.editor.createEditor({ + id: 'node-info-input-info-editor', + mode: 'ace/mode/markdown', + value: "" + }); + if (node.info) { + nodeInfoEditor.getSession().setValue(node.info, -1); + } + } } function updateLabels(editing_node, changes, outputMap) { @@ -1126,6 +1141,10 @@ RED.editor = (function() { } } + if (node.type.indexOf("subflow") != 0 && node.type !== "comment") { + node.info = nodeInfoEditor.getValue(); + } + if (changed) { var wasChanged = editing_node.changed; editing_node.changed = true; @@ -1240,6 +1259,9 @@ RED.editor = (function() { RED.sidebar.info.refresh(editing_node); } RED.workspaces.refresh(); + if (node.type.indexOf("subflow") != 0 && node.type !== "comment") { + nodeInfoEditor.destroy(); + } RED.view.redraw(true); editStack.pop(); }, diff --git a/editor/js/ui/tab-info.js b/editor/js/ui/tab-info.js index 78dc246c0..a2664a7fe 100644 --- a/editor/js/ui/tab-info.js +++ b/editor/js/ui/tab-info.js @@ -277,6 +277,10 @@ RED.sidebar.info = (function() { // TODO: help infoText = infoText + marked(textInfo); } + if (node.info) { + infoSection.title.text(RED._("sidebar.info.nodeHelp")); + infoText = marked(node.info || "") || ('' + RED._("sidebar.info.none") + ''); + } if (infoText) { setInfoText(infoText); } diff --git a/red/api/editor/locales/en-US/editor.json b/red/api/editor/locales/en-US/editor.json index 689b030b1..ae81d7a07 100644 --- a/red/api/editor/locales/en-US/editor.json +++ b/red/api/editor/locales/en-US/editor.json @@ -267,6 +267,7 @@ "defaultLabel": "use default label", "searchIcons": "Search icons", "useDefault": "use default", + "description": "Description", "errors": { "scopeChange": "Changing the scope will make it unavailable to nodes in other flows that use it" } diff --git a/red/api/editor/locales/ja/editor.json b/red/api/editor/locales/ja/editor.json index 3da5791dd..a52ec733a 100644 --- a/red/api/editor/locales/ja/editor.json +++ b/red/api/editor/locales/ja/editor.json @@ -266,6 +266,7 @@ "defaultLabel": "既定の名前を使用", "searchIcons": "アイコンを検索", "useDefault": "デフォルトを使用", + "description": "詳細", "errors": { "scopeChange": "スコープの変更は、他のフローで使われているノードを無効にします" } From 4374506981139e4988d1d6b099b47b3344642ff7 Mon Sep 17 00:00:00 2001 From: cclauss Date: Wed, 15 Aug 2018 16:25:58 +0200 Subject: [PATCH 02/16] Define raw_input() in Python 3 & fix time.sleep() * __raw_input()__ was removed in Python 3 in favor of __input()__ * Fix __sleep()__ to match the import on line 22 [flake8](http://flake8.pycqa.org) testing of https://github.com/node-red/node-red on Python 3.7.0 $ __flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics__ ``` ./nodes/core/hardware/nrgpio.py:45:24: F821 undefined name 'raw_input' data = raw_input() ^ ./nodes/core/hardware/nrgpio.py:63:24: F821 undefined name 'raw_input' data = raw_input() ^ ./nodes/core/hardware/nrgpio.py:85:24: F821 undefined name 'raw_input' data = raw_input() ^ ./nodes/core/hardware/nrgpio.py:120:24: F821 undefined name 'raw_input' data = raw_input() ^ ./nodes/core/hardware/nrgpio.py:134:24: F821 undefined name 'raw_input' data = raw_input() ^ ./nodes/core/hardware/nrgpio.py:164:24: F821 undefined name 'raw_input' data = raw_input() ^ ./nodes/core/hardware/nrgpio.py:201:17: F821 undefined name 'time' time.sleep(10) ^ 7 F821 undefined name 'raw_input' 7 ``` @dceejay --- nodes/core/hardware/nrgpio.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/nodes/core/hardware/nrgpio.py b/nodes/core/hardware/nrgpio.py index 0cde0e4df..7908ca117 100755 --- a/nodes/core/hardware/nrgpio.py +++ b/nodes/core/hardware/nrgpio.py @@ -21,7 +21,12 @@ import os import subprocess from time import sleep -bounce = 25; +try: + raw_input # Python 2 +except NameError: + raw_input = input # Python 3 + +bounce = 25 if len(sys.argv) > 2: cmd = sys.argv[1].lower() @@ -198,7 +203,7 @@ if len(sys.argv) > 2: elif cmd == "kbd": # catch keyboard button events try: while not os.path.isdir("/dev/input/by-path"): - time.sleep(10) + sleep(10) infile = subprocess.check_output("ls /dev/input/by-path/ | grep -m 1 'kbd'", shell=True).strip() infile_path = "/dev/input/by-path/" + infile EVENT_SIZE = struct.calcsize('llHHI') From 048f9c02943f7c8f47b844a024ef0bef3529ff35 Mon Sep 17 00:00:00 2001 From: Kazuhito Yokoi Date: Sun, 9 Sep 2018 06:41:38 +0900 Subject: [PATCH 03/16] Fix node color bug (#1877) * Fix node color bug * Add color property into sample node * Revert view.js * Add color handling into getNodeColor() --- editor/js/ui/utils.js | 6 +++++- nodes/99-sample.html.demo | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/editor/js/ui/utils.js b/editor/js/ui/utils.js index 8e6654a7a..c944f3504 100644 --- a/editor/js/ui/utils.js +++ b/editor/js/ui/utils.js @@ -826,7 +826,11 @@ RED.utils = (function() { } result = nodeColorCache[type]; } - return result; + if (result) { + return result; + } else { + return "#ddd"; + } } function addSpinnerOverlay(container,contain) { diff --git a/nodes/99-sample.html.demo b/nodes/99-sample.html.demo index 94e049e31..c7548b032 100644 --- a/nodes/99-sample.html.demo +++ b/nodes/99-sample.html.demo @@ -67,6 +67,7 @@ }, inputs:1, // set the number of inputs - only 0 or 1 outputs:1, // set the number of outputs - 0 to n + color: "#ddd", // set icon color // set the icon (held in icons dir below where you save the node) icon: "myicon.png", // saved in icons/myicon.png label: function() { // sets the default label contents From 0f4d46671f5cefe00d306eefd3e9f208986ff210 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sun, 9 Sep 2018 11:07:44 +0100 Subject: [PATCH 04/16] Fix global.get("foo.bar") for functionGlobalContext set values --- red/runtime/nodes/context/index.js | 7 +++--- test/red/runtime/nodes/context/index_spec.js | 23 +++++++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/red/runtime/nodes/context/index.js b/red/runtime/nodes/context/index.js index be79eb00e..c85f18d80 100644 --- a/red/runtime/nodes/context/index.js +++ b/red/runtime/nodes/context/index.js @@ -17,6 +17,7 @@ var clone = require("clone"); var log = require("../../log"); var memory = require("./memory"); +var util = require("../../util"); var settings; @@ -209,12 +210,12 @@ function createContext(id,seed) { insertSeedValues = function(keys,values) { if (!Array.isArray(keys)) { if (values[0] === undefined) { - values[0] = seed[keys]; + values[0] = util.getObjectProperty(seed,keys); } } else { for (var i=0;i Date: Sun, 9 Sep 2018 23:47:31 +0100 Subject: [PATCH 05/16] Handle context.get with multiple levels of unknown key Fixes #1883 --- red/runtime/nodes/context/localfilesystem.js | 21 +++++++++-- red/runtime/nodes/context/memory.js | 9 ++++- red/runtime/util.js | 36 +++++++++++-------- .../nodes/context/localfilesystem_spec.js | 2 +- test/red/runtime/nodes/context/memory_spec.js | 2 +- 5 files changed, 50 insertions(+), 20 deletions(-) diff --git a/red/runtime/nodes/context/localfilesystem.js b/red/runtime/nodes/context/localfilesystem.js index dc25b6290..548c98c60 100644 --- a/red/runtime/nodes/context/localfilesystem.js +++ b/red/runtime/nodes/context/localfilesystem.js @@ -230,14 +230,31 @@ LocalFileSystem.prototype.get = function(scope, key, callback) { } var storagePath = getStoragePath(this.storageBaseDir ,scope); loadFile(storagePath + ".json").then(function(data){ + var value; if(data){ data = JSON.parse(data); if (!Array.isArray(key)) { - callback(null, util.getObjectProperty(data,key)); + try { + value = util.getObjectProperty(data,key); + } catch(err) { + if (err.code === "INVALID_EXPR") { + throw err; + } + value = undefined; + } + callback(null, value); } else { var results = [undefined]; for (var i=0;i Date: Mon, 10 Sep 2018 10:47:05 +0900 Subject: [PATCH 06/16] Bugfix. http-request node --- nodes/core/io/21-httprequest.js | 35 +++-- test/nodes/core/io/21-httprequest_spec.js | 183 ++++++++++++++++++++++ 2 files changed, 208 insertions(+), 10 deletions(-) diff --git a/nodes/core/io/21-httprequest.js b/nodes/core/io/21-httprequest.js index 4ddb1d3b9..28d67d077 100644 --- a/nodes/core/io/21-httprequest.js +++ b/nodes/core/io/21-httprequest.js @@ -83,6 +83,7 @@ module.exports = function(RED) { opts.headers = {}; opts.encoding = null; // Force NodeJs to return a Buffer (instead of a string) opts.maxRedirects = 21; + opts.jar = request.jar(); var ctSet = "Content-Type"; // set default camel case var clSet = "Content-Length"; if (msg.headers) { @@ -112,28 +113,42 @@ module.exports = function(RED) { } if (msg.hasOwnProperty('followRedirects')) { opts.followRedirect = msg.followRedirects; + } + if (!opts.hasOwnProperty('followRedirect') || opts.followRedirect) { + opts.followRedirect = function(res) { + if (this.headers.cookie) { + delete this.headers.cookie; + } + return true; + }; + } + if (opts.headers.hasOwnProperty('cookie')) { + var cookies = cookie.parse(opts.headers.cookie); + for (var name in cookies) { + if (cookies.hasOwnProperty(name)) { + if (cookies[name] === null) { + // This case clears a cookie for HTTP In/Response nodes. + // Ignore for this node. + } else { + opts.jar.setCookie(name + '=' + cookies[name], url); + } + } } + delete opts.headers.cookie; + } if (msg.cookies) { - var cookies = []; - if (opts.headers.hasOwnProperty('cookie')) { - cookies.push(opts.headers.cookie); - } - for (var name in msg.cookies) { if (msg.cookies.hasOwnProperty(name)) { if (msg.cookies[name] === null || msg.cookies[name].value === null) { // This case clears a cookie for HTTP In/Response nodes. // Ignore for this node. } else if (typeof msg.cookies[name] === 'object') { - cookies.push(cookie.serialize(name,msg.cookies[name].value)); + opts.jar.setCookie(name + '=' + msg.cookies[name].value, url); } else { - cookies.push(cookie.serialize(name,msg.cookies[name])); + opts.jar.setCookie(name + '=' + msg.cookies[name], url); } } } - if (cookies.length > 0) { - opts.headers.cookie = cookies.join("; "); - } } if (this.credentials && this.credentials.user) { opts.auth = { diff --git a/test/nodes/core/io/21-httprequest_spec.js b/test/nodes/core/io/21-httprequest_spec.js index 8ca5f0a14..2eab428cd 100644 --- a/test/nodes/core/io/21-httprequest_spec.js +++ b/test/nodes/core/io/21-httprequest_spec.js @@ -46,6 +46,9 @@ describe('HTTP Request Node', function() { var preEnvNoProxyLowerCase; var preEnvNoProxyUpperCase; + //rediect cookie variables + var receivedCookies = {}; + function startServer(done) { testPort += 1; testServer = stoppable(http.createServer(testApp)); @@ -98,6 +101,10 @@ describe('HTTP Request Node', function() { return "https://localhost:"+testSslPort+url; } + function getDifferentTestURL(url) { + return "http://127.0.0.1:"+testPort+url; + } + function getSslTestURLWithoutProtocol(url) { return "localhost:"+testSslPort+url; } @@ -182,6 +189,24 @@ describe('HTTP Request Node', function() { testApp.options('/*', function(req,res) { res.status(200).end(); }); + testApp.get('/redirectToSameDomain', function(req, res) { + var key = req.headers.host + req.url; + receivedCookies[key] = req.cookies; + res.cookie('redirectToSameDomainCookie','same1'); + res.redirect(getTestURL('/redirectReturn')); + }); + testApp.get('/redirectToDifferentDomain', function(req, res) { + var key = req.headers.host + req.url; + receivedCookies[key] = req.cookies; + res.cookie('redirectToDifferentDomain','different1'); + res.redirect(getDifferentTestURL('/redirectReturn')); + }); + testApp.get('/redirectReturn', function(req, res) { + var key = req.headers.host + req.url; + receivedCookies[key] = req.cookies; + res.cookie('redirectReturn','return1'); + res.status(200).end(); + }); startServer(function(err) { if (err) { done(err); @@ -1240,4 +1265,162 @@ describe('HTTP Request Node', function() { }); }); }); + + describe('redirect-cookie', function() { + it('should send cookies to the same domain when redirected(no cookies)', function(done) { + var flow = [{id:'n1',type:'http request',wires:[['n2']],method:'GET',ret:'obj',url:getTestURL('/redirectToSameDomain')}, + {id:"n2", type:"helper"}]; + receivedCookies = {}; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + var cookies1 = receivedCookies['localhost:'+testPort+'/redirectToSameDomain']; + var cookies2 = receivedCookies['localhost:'+testPort+'/redirectReturn']; + if (cookies1 && Object.keys(cookies1).length != 0) { + done(new Error('Invalid cookie(path:/rediectToSame)')); + return; + } + if ((cookies2 && Object.keys(cookies2).length != 1) || + cookies2['redirectToSameDomainCookie'] !== 'same1') { + done(new Error('Invalid cookie(path:/rediectReurn)')); + return; + } + done(); + }); + n1.receive({}); + }); + }); + it('should not send cookies to the different domain when redirected(no cookies)', function(done) { + var flow = [{id:'n1',type:'http request',wires:[['n2']],method:'GET',ret:'obj',url:getTestURL('/redirectToDifferentDomain')}, + {id:"n2", type:"helper"}]; + receivedCookies = {}; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + var cookies1 = receivedCookies['localhost:'+testPort+'/redirectToSameDomain']; + var cookies2 = receivedCookies['127.0.0.1:'+testPort+'/redirectReturn']; + if (cookies1 && Object.keys(cookies1).length != 0) { + done(new Error('Invalid cookie(path:/rediectToDiffer)')); + return; + } + if (cookies2 && Object.keys(cookies2).length != 0) { + done(new Error('Invalid cookie(path:/rediectReurn)')); + return; + } + done(); + }); + n1.receive({}); + }); + }); + it('should send cookies to the same domain when redirected(msg.cookies)', function(done) { + var flow = [{id:'n1',type:'http request',wires:[['n2']],method:'GET',ret:'obj',url:getTestURL('/redirectToSameDomain')}, + {id:"n2", type:"helper"}]; + receivedCookies = {}; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + var cookies1 = receivedCookies['localhost:'+testPort+'/redirectToSameDomain']; + var cookies2 = receivedCookies['localhost:'+testPort+'/redirectReturn']; + if ((cookies1 && Object.keys(cookies1).length != 1) || + cookies1['requestCookie'] !== 'request1') { + done(new Error('Invalid cookie(path:/rediectToSame)')); + return; + } + if ((cookies2 && Object.keys(cookies2).length != 2) || + cookies1['requestCookie'] !== 'request1' || + cookies2['redirectToSameDomainCookie'] !== 'same1') { + done(new Error('Invalid cookie(path:/rediectReurn)')); + return; + } + done(); + }); + n1.receive({ + cookies: { requestCookie: 'request1' } + }); + }); + }); + it('should not send cookies to the different domain when redirected(msg.cookies)', function(done) { + var flow = [{id:'n1',type:'http request',wires:[['n2']],method:'GET',ret:'obj',url:getTestURL('/redirectToDifferentDomain')}, + {id:"n2", type:"helper"}]; + receivedCookies = {}; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + var cookies1 = receivedCookies['localhost:'+testPort+'/redirectToDifferentDomain']; + var cookies2 = receivedCookies['127.0.0.1:'+testPort+'/redirectReturn']; + if ((cookies1 && Object.keys(cookies1).length != 1) || + cookies1['requestCookie'] !== 'request1') { + done(new Error('Invalid cookie(path:/rediectToDiffer)')); + return; + } + if (cookies2 && Object.keys(cookies2).length != 0) { + done(new Error('Invalid cookie(path:/rediectReurn)')); + return; + } + done(); + }); + n1.receive({ + cookies: { requestCookie: 'request1' } + }); + }); + }); + it('should send cookies to the same domain when redirected(msg.headers.cookie)', function(done) { + var flow = [{id:'n1',type:'http request',wires:[['n2']],method:'GET',ret:'obj',url:getTestURL('/redirectToSameDomain')}, + {id:"n2", type:"helper"}]; + receivedCookies = {}; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + var cookies1 = receivedCookies['localhost:'+testPort+'/redirectToSameDomain']; + var cookies2 = receivedCookies['localhost:'+testPort+'/redirectReturn']; + if ((cookies1 && Object.keys(cookies1).length != 1) || + cookies1['requestCookie'] !== 'request1') { + done(new Error('Invalid cookie(path:/rediectToSame)')); + return; + } + if ((cookies2 && Object.keys(cookies2).length != 2) || + cookies1['requestCookie'] !== 'request1' || + cookies2['redirectToSameDomainCookie'] !== 'same1') { + done(new Error('Invalid cookie(path:/rediectReurn)')); + return; + } + done(); + }); + n1.receive({ + headers: { cookie: 'requestCookie=request1' } + }); + }); + }); + it('should not send cookies to the different domain when redirected(msg.headers.cookie)', function(done) { + var flow = [{id:'n1',type:'http request',wires:[['n2']],method:'GET',ret:'obj',url:getTestURL('/redirectToDifferentDomain')}, + {id:"n2", type:"helper"}]; + receivedCookies = {}; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + var cookies1 = receivedCookies['localhost:'+testPort+'/redirectToDifferentDomain']; + var cookies2 = receivedCookies['127.0.0.1:'+testPort+'/redirectReturn']; + if ((cookies1 && Object.keys(cookies1).length != 1) || + cookies1['requestCookie'] !== 'request1') { + done(new Error('Invalid cookie(path:/rediectToDiffer)')); + return; + } + if (cookies2 && Object.keys(cookies2).length != 0) { + done(new Error('Invalid cookie(path:/rediectReurn)')); + return; + } + done(); + }); + n1.receive({ + headers: { cookie: 'requestCookie=request1' } + }); + }); + }); + }); }); From 75e7c0e50da025d9ae1b6968ab21293ba4884be4 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 10 Sep 2018 22:30:51 +0100 Subject: [PATCH 07/16] Ensure context.flow/global cannot be deleted or enumerated --- red/runtime/nodes/context/index.js | 181 ++++++++++--------- test/red/runtime/nodes/context/index_spec.js | 15 ++ 2 files changed, 111 insertions(+), 85 deletions(-) diff --git a/red/runtime/nodes/context/index.js b/red/runtime/nodes/context/index.js index c85f18d80..f048947ad 100644 --- a/red/runtime/nodes/context/index.js +++ b/red/runtime/nodes/context/index.js @@ -221,93 +221,100 @@ function createContext(id,seed) { } } } - - obj.get = function(key, storage, callback) { - var context; - if (!storage && !callback) { - context = stores["_"]; - } else { - if (typeof storage === 'function') { - callback = storage; - storage = "_"; - } - if (callback && typeof callback !== 'function'){ - throw new Error("Callback must be a function"); - } - context = getContextStorage(storage); - } - if (callback) { - if (!seed) { - context.get(scope,key,callback); - } else { - context.get(scope,key,function() { - if (arguments[0]) { - callback(arguments[0]); - return; + Object.defineProperties(obj, { + get: { + value: function(key, storage, callback) { + var context; + if (!storage && !callback) { + context = stores["_"]; + } else { + if (typeof storage === 'function') { + callback = storage; + storage = "_"; } - var results = Array.prototype.slice.call(arguments,[1]); - insertSeedValues(key,results); - // Put the err arg back - results.unshift(undefined); - callback.apply(null,results); - }); - } - } else { - // No callback, attempt to do this synchronously - var results = context.get(scope,key); - if (seed) { - if (Array.isArray(key)) { - insertSeedValues(key,results); - } else if (results === undefined){ - results = util.getObjectProperty(seed,key); + if (callback && typeof callback !== 'function'){ + throw new Error("Callback must be a function"); + } + context = getContextStorage(storage); + } + if (callback) { + if (!seed) { + context.get(scope,key,callback); + } else { + context.get(scope,key,function() { + if (arguments[0]) { + callback(arguments[0]); + return; + } + var results = Array.prototype.slice.call(arguments,[1]); + insertSeedValues(key,results); + // Put the err arg back + results.unshift(undefined); + callback.apply(null,results); + }); + } + } else { + // No callback, attempt to do this synchronously + var results = context.get(scope,key); + if (seed) { + if (Array.isArray(key)) { + insertSeedValues(key,results); + } else if (results === undefined){ + results = util.getObjectProperty(seed,key); + } + } + return results; + } + } + }, + set: { + value: function(key, value, storage, callback) { + var context; + if (!storage && !callback) { + context = stores["_"]; + } else { + if (typeof storage === 'function') { + callback = storage; + storage = "_"; + } + if (callback && typeof callback !== 'function') { + throw new Error("Callback must be a function"); + } + context = getContextStorage(storage); + } + context.set(scope, key, value, callback); + } + }, + keys: { + value: function(storage, callback) { + var context; + if (!storage && !callback) { + context = stores["_"]; + } else { + if (typeof storage === 'function') { + callback = storage; + storage = "_"; + } + if (callback && typeof callback !== 'function') { + throw new Error("Callback must be a function"); + } + context = getContextStorage(storage); + } + if (seed) { + if (callback) { + context.keys(scope, function(err,keys) { + callback(err,Array.from(new Set(seedKeys.concat(keys)).keys())); + }); + } else { + var keys = context.keys(scope); + return Array.from(new Set(seedKeys.concat(keys)).keys()) + } + } else { + return context.keys(scope, callback); } } - return results; } - }; - obj.set = function(key, value, storage, callback) { - var context; - if (!storage && !callback) { - context = stores["_"]; - } else { - if (typeof storage === 'function') { - callback = storage; - storage = "_"; - } - if (callback && typeof callback !== 'function') { - throw new Error("Callback must be a function"); - } - context = getContextStorage(storage); - } - context.set(scope, key, value, callback); - }; - obj.keys = function(storage, callback) { - var context; - if (!storage && !callback) { - context = stores["_"]; - } else { - if (typeof storage === 'function') { - callback = storage; - storage = "_"; - } - if (callback && typeof callback !== 'function') { - throw new Error("Callback must be a function"); - } - context = getContextStorage(storage); - } - if (seed) { - if (callback) { - context.keys(scope, function(err,keys) { - callback(err,Array.from(new Set(seedKeys.concat(keys)).keys())); - }); - } else { - var keys = context.keys(scope); - return Array.from(new Set(seedKeys.concat(keys)).keys()) - } - } else { - return context.keys(scope, callback); - } - }; + }); return obj; } @@ -321,9 +328,13 @@ function getContext(localId,flowId) { } var newContext = createContext(contextId); if (flowId) { - newContext.flow = getContext(flowId); + Object.defineProperty(newContext, 'flow', { + value: getContext(flowId) + }); } - newContext.global = contexts['global']; + Object.defineProperty(newContext, 'global', { + value: contexts['global'] + }) contexts[contextId] = newContext; return newContext; } diff --git a/test/red/runtime/nodes/context/index_spec.js b/test/red/runtime/nodes/context/index_spec.js index 6f5a73908..3beb9b201 100644 --- a/test/red/runtime/nodes/context/index_spec.js +++ b/test/red/runtime/nodes/context/index_spec.js @@ -113,6 +113,21 @@ describe('context', function() { context2.global.get("foo").should.equal("test"); }); + it('context.flow/global are not enumerable', function() { + var context1 = Context.get("1","flowA"); + Object.keys(context1).length.should.equal(0); + Object.keys(context1.flow).length.should.equal(0); + Object.keys(context1.global).length.should.equal(0); + }) + + it('context.flow/global cannot be deleted', function() { + var context1 = Context.get("1","flowA"); + delete context1.flow; + should.exist(context1.flow); + delete context1.global; + should.exist(context1.global); + }) + it('deletes context',function() { var context = Context.get("1","flowA"); should.not.exist(context.get("foo")); From f30ff7a2fd569ec4aa6e46b92c718f5de7e8de85 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 14 Sep 2018 10:54:24 +0100 Subject: [PATCH 08/16] Redesign node edit dialog to tabbed style --- editor/js/nodes.js | 15 +- editor/js/ui/editor.js | 321 ++++++++++++++++++++++++++++--------- editor/js/ui/tray.js | 1 + editor/templates/index.mst | 7 - 4 files changed, 253 insertions(+), 91 deletions(-) diff --git a/editor/js/nodes.js b/editor/js/nodes.js index 062654ca2..d1ff205ab 100644 --- a/editor/js/nodes.js +++ b/editor/js/nodes.js @@ -509,9 +509,9 @@ RED.nodes = (function() { node.icon = n.icon; } } - if (n.info) { - node.info = n.info; - } + } + if (n.info) { + node.info = n.info; } return node; } @@ -907,7 +907,14 @@ RED.nodes = (function() { } if (!existingConfigNode || existingConfigNode._def.exclusive) { //} || !compareNodes(existingConfigNode,n,true) || existingConfigNode.z !== n.z) { - configNode = {id:n.id, z:n.z, type:n.type, users:[], _config:{}}; + configNode = { + id:n.id, + z:n.z, + type:n.type, + info: n.info, + users:[], + _config:{} + }; for (d in def.defaults) { if (def.defaults.hasOwnProperty(d)) { configNode[d] = n[d]; diff --git a/editor/js/ui/editor.js b/editor/js/ui/editor.js index 634006963..73ef5e6b7 100644 --- a/editor/js/ui/editor.js +++ b/editor/js/ui/editor.js @@ -20,7 +20,6 @@ RED.editor = (function() { var editing_node = null; var editing_config_node = null; var subflowEditor; - var nodeInfoEditor; var editTrayWidthCache = {}; @@ -773,7 +772,7 @@ RED.editor = (function() { searchInput.focus(); } - function buildLabelForm(container,node) { + function buildAppearanceForm(container,node) { var dialogForm = $('
').appendTo(container); var inputCount = node.inputs || node._def.inputs || 0; @@ -840,20 +839,6 @@ RED.editor = (function() { }) $('
').text(node.icon).appendTo(iconRow); } - - if (node.type.indexOf("subflow") != 0 && node.type !== "comment") { - $('
').appendTo(dialogForm); - $('
').appendTo(dialogForm); - $('
').appendTo(dialogForm); - nodeInfoEditor = RED.editor.createEditor({ - id: 'node-info-input-info-editor', - mode: 'ace/mode/markdown', - value: "" - }); - if (node.info) { - nodeInfoEditor.getSession().setValue(node.info, -1); - } - } } function updateLabels(editing_node, changes, outputMap) { @@ -897,10 +882,27 @@ RED.editor = (function() { return changed; } + function buildDescriptionForm(container,node) { + var dialogForm = $('
').appendTo(container); + $('
').appendTo(dialogForm); + var nodeInfoEditor = RED.editor.createEditor({ + id: "node-info-input-info-editor", + mode: 'ace/mode/markdown', + value: "" + }); + if (node.info) { + nodeInfoEditor.getSession().setValue(node.info, -1); + } + return nodeInfoEditor; + } + function showEditDialog(node) { var editing_node = node; var isDefaultIcon; var defaultIcon; + var nodeInfoEditor; + var finishedBuilding = false; + editStack.push(node); RED.view.state(RED.state.EDITING); var type = node.type; @@ -1141,8 +1143,31 @@ RED.editor = (function() { } } - if (node.type.indexOf("subflow") != 0 && node.type !== "comment") { - node.info = nodeInfoEditor.getValue(); + var oldInfo = node.info; + if (nodeInfoEditor) { + var newInfo = nodeInfoEditor.getValue(); + if (!!oldInfo) { + // Has existing info property + if (newInfo.trim() === "") { + // New value is blank - remove the property + changed = true; + changes.info = oldInfo; + delete node.info; + } else if (newInfo !== oldInfo) { + // New value is different + changed = true; + changes.info = oldInfo; + node.info = newInfo; + } + } else { + // No existing info + if (newInfo.trim() !== "") { + // New value is not blank + changed = true; + changes.info = undefined; + node.info = newInfo; + } + } } if (changed) { @@ -1193,8 +1218,8 @@ RED.editor = (function() { ], resize: function(dimensions) { editTrayWidthCache[type] = dimensions.width; - $(".editor-tray-content").height(dimensions.height - 78); - var form = $(".editor-tray-content form").height(dimensions.height - 78 - 40); + $(".editor-tray-content").height(dimensions.height - 50); + var form = $(".editor-tray-content form").height(dimensions.height - 50 - 40); if (editing_node && editing_node._def.oneditresize) { try { editing_node._def.oneditresize.call(editing_node,{width:form.width(),height:form.height()}); @@ -1208,25 +1233,23 @@ RED.editor = (function() { var trayBody = tray.find('.editor-tray-body'); trayBody.parent().css('overflow','hidden'); - var stack = RED.stack.create({ - container: trayBody, - singleExpanded: true - }); - var nodeProperties = stack.add({ - title: RED._("editor.nodeProperties"), - expanded: true - }); - nodeProperties.content.addClass("editor-tray-content"); + var editorTabEl = $('
    ').appendTo(trayBody); + var editorContent = $('
    ').appendTo(trayBody); - var portLabels = stack.add({ - title: RED._("editor.portLabels"), - onexpand: function() { - refreshLabelForm(this.content,node); - } + 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 }); - portLabels.content.addClass("editor-tray-content"); - - if (editing_node) { RED.sidebar.info.refresh(editing_node); } @@ -1243,11 +1266,48 @@ RED.editor = (function() { } else { isDefaultIcon = true; } - buildEditForm(nodeProperties.content,"dialog-form",type,ns); - buildLabelForm(portLabels.content,node); + + var nodePropertiesTab = { + id: "editor-tab-properties", + label: "Properties", + name: "Properties", + content: $('
    ', {class:"editor-tray-content"}).appendTo(editorContent).hide(), + iconClass: "fa fa-cog" + }; + buildEditForm(nodePropertiesTab.content,"dialog-form",type,ns); + editorTabs.addTab(nodePropertiesTab); + + if (!node._def.defaults || !node._def.defaults.hasOwnProperty('info')) { + var descriptionTab = { + id: "editor-tab-description", + label: "Description", + name: "Description", + content: $('
    ', {class:"editor-tray-content"}).appendTo(editorContent).hide(), + iconClass: "fa fa-file-text-o", + onchange: function() { + nodeInfoEditor.focus(); + } + }; + editorTabs.addTab(descriptionTab); + nodeInfoEditor = buildDescriptionForm(descriptionTab.content,node); + } + + var appearanceTab = { + id: "editor-tab-appearance", + label: "Appearance", + name: "Appearance", + content: $('
    ', {class:"editor-tray-content"}).appendTo(editorContent).hide(), + iconClass: "fa fa-object-group", + onchange: function() { + refreshLabelForm(this.content,node); + } + }; + buildAppearanceForm(appearanceTab.content,node); + editorTabs.addTab(appearanceTab); prepareEditDialog(node,node._def,"node-input", function() { trayBody.i18n(); + finishedBuilding = true; done(); }); }, @@ -1259,8 +1319,9 @@ RED.editor = (function() { RED.sidebar.info.refresh(editing_node); } RED.workspaces.refresh(); - if (node.type.indexOf("subflow") != 0 && node.type !== "comment") { + if (nodeInfoEditor) { nodeInfoEditor.destroy(); + nodeInfoEditor = null; } RED.view.redraw(true); editStack.pop(); @@ -1299,6 +1360,8 @@ RED.editor = (function() { var adding = (id == "_ADD_"); var node_def = RED.nodes.getType(type); var editing_config_node = RED.nodes.node(id); + var nodeInfoEditor; + var finishedBuilding = false; var ns; if (node_def.set.module === "node-red") { @@ -1331,7 +1394,8 @@ RED.editor = (function() { RED.view.state(RED.state.EDITING); var trayOptions = { title: getEditStackTitle(), //(adding?RED._("editor.addNewConfig", {type:type}):RED._("editor.editConfig", {type:type})), - resize: function() { + resize: function(dimensions) { + $(".editor-tray-content").height(dimensions.height - 50); if (editing_config_node && editing_config_node._def.oneditresize) { var form = $("#node-config-dialog-edit-form"); try { @@ -1343,6 +1407,7 @@ RED.editor = (function() { }, open: function(tray, done) { var trayHeader = tray.find(".editor-tray-header"); + var trayBody = tray.find('.editor-tray-body'); var trayFooter = tray.find(".editor-tray-footer"); if (node_def.hasUsers !== false) { @@ -1350,7 +1415,49 @@ RED.editor = (function() { } trayFooter.append(''); - var dialogForm = buildEditForm(tray.find('.editor-tray-body'),"node-config-dialog-edit-form",type,ns); + var editorTabEl = $('
      ').appendTo(trayBody); + var editorContent = $('
      ').appendTo(trayBody); + + 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 + }); + + var nodePropertiesTab = { + id: "editor-tab-cproperties", + label: "Properties", + name: "Properties", + content: $('
      ', {class:"editor-tray-content"}).appendTo(editorContent).hide(), + iconClass: "fa fa-cog" + }; + editorTabs.addTab(nodePropertiesTab); + buildEditForm(nodePropertiesTab.content,"node-config-dialog-edit-form",type,ns); + + if (!node_def.defaults || !node_def.defaults.hasOwnProperty('info')) { + var descriptionTab = { + id: "editor-tab-description", + label: "Description", + name: "Description", + content: $('
      ', {class:"editor-tray-content"}).appendTo(editorContent).hide(), + iconClass: "fa fa-file-text-o", + onchange: function() { + nodeInfoEditor.focus(); + } + }; + editorTabs.addTab(descriptionTab); + nodeInfoEditor = buildDescriptionForm(descriptionTab.content,editing_config_node); + } + prepareEditDialog(editing_config_node,node_def,"node-config-input", function() { if (editing_config_node._def.exclusive) { @@ -1398,17 +1505,20 @@ RED.editor = (function() { } }); } - tabSelect.i18n(); - - dialogForm.i18n(); if (node_def.hasUsers !== false) { $("#node-config-dialog-user-count").find("span").text(RED._("editor.nodesUse", {count:editing_config_node.users.length})).parent().show(); } + trayBody.i18n(); + finishedBuilding = true; done(); }); }, close: function() { RED.workspaces.refresh(); + if (nodeInfoEditor) { + nodeInfoEditor.destroy(); + nodeInfoEditor = null; + } editStack.pop(); }, show: function() { @@ -1502,6 +1612,31 @@ RED.editor = (function() { } } } + + if (nodeInfoEditor) { + editing_config_node.info = nodeInfoEditor.getValue(); + + var oldInfo = editing_config_node.info; + if (nodeInfoEditor) { + var newInfo = nodeInfoEditor.getValue(); + if (!!oldInfo) { + // Has existing info property + if (newInfo.trim() === "") { + // New value is blank - remove the property + delete editing_config_node.info; + } else if (newInfo !== oldInfo) { + // New value is different + editing_config_node.info = newInfo; + } + } else { + // No existing info + if (newInfo.trim() !== "") { + // New value is not blank + editing_config_node.info = newInfo; + } + } + } + } editing_config_node.label = configTypeDef.label; editing_config_node.z = scope; @@ -1689,7 +1824,7 @@ RED.editor = (function() { editStack.push(subflow); RED.view.state(RED.state.EDITING); var subflowEditor; - + var finishedBuilding = false; var trayOptions = { title: getEditStackTitle(), buttons: [ @@ -1790,48 +1925,76 @@ RED.editor = (function() { } ], resize: function(dimensions) { - $(".editor-tray-content").height(dimensions.height - 78); - var form = $(".editor-tray-content form").height(dimensions.height - 78 - 40); - - 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(trayBody); + var editorContent = $('
      ').appendTo(trayBody); + + 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 }); + var nodePropertiesTab = { + id: "editor-tab-properties", + label: "Properties", + name: "Properties", + content: $('
      ', {class:"editor-tray-content"}).appendTo(editorContent).hide(), + iconClass: "fa fa-cog" + }; + buildEditForm(nodePropertiesTab.content,"dialog-form","subflow-template"); + editorTabs.addTab(nodePropertiesTab); + + var descriptionTab = { + id: "editor-tab-description", + label: "Description", + name: "Description", + content: $('
      ', {class:"editor-tray-content"}).appendTo(editorContent).hide(), + iconClass: "fa fa-file-text-o", + onchange: function() { + subflowEditor.focus(); + } + }; + editorTabs.addTab(descriptionTab); + subflowEditor = buildDescriptionForm(descriptionTab.content,editing_node); + + var appearanceTab = { + id: "editor-tab-appearance", + label: "Appearance", + name: "Appearance", + content: $('
      ', {class:"editor-tray-content"}).appendTo(editorContent).hide(), + iconClass: "fa fa-object-group", + onchange: function() { + refreshLabelForm(this.content,editing_node); + } + }; + buildAppearanceForm(appearanceTab.content,editing_node); + editorTabs.addTab(appearanceTab); + + + + $("#subflow-input-name").val(subflow.name); RED.text.bidi.prepareInput($("#subflow-input-name")); @@ -1858,10 +2021,7 @@ RED.editor = (function() { } }) - $("#subflow-input-category").val(subflow.category||"subflows"); - - subflowEditor.getSession().setValue(subflow.info||"",-1); var userCount = 0; var subflowType = "subflow:"+editing_node.id; @@ -1872,8 +2032,8 @@ RED.editor = (function() { }); $("#subflow-dialog-user-count").text(RED._("subflow.subflowInstances", {count:userCount})).show(); - buildLabelForm(portLabels.content,subflow); trayBody.i18n(); + finishedBuilding = true; }, close: function() { if (RED.view.state() != RED.state.IMPORT_DRAGGING) { @@ -1882,6 +2042,7 @@ RED.editor = (function() { RED.sidebar.info.refresh(editing_node); RED.workspaces.refresh(); subflowEditor.destroy(); + subflowEditor = null; editStack.pop(); editing_node = null; }, diff --git a/editor/js/ui/tray.js b/editor/js/ui/tray.js index 3c6a78a6b..2fdbb2ac8 100644 --- a/editor/js/ui/tray.js +++ b/editor/js/ui/tray.js @@ -232,6 +232,7 @@ RED.tray = (function() { } }, + resize: handleWindowResize, close: function close(done) { if (stack.length > 0) { var tray = stack.pop(); diff --git a/editor/templates/index.mst b/editor/templates/index.mst index e63008584..5815b5afb 100644 --- a/editor/templates/index.mst +++ b/editor/templates/index.mst @@ -136,13 +136,6 @@
      -
      - - -
      -
      -
      -
      From 17a737ca889d58366eebea6e66dfb6596b1d32c5 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 14 Sep 2018 11:09:56 +0100 Subject: [PATCH 09/16] Fix dbl-click handling on webkit-based browsers d3.event.buttons is not as widely supported as I thought. Can change this one instance as it is inside a click handler so d3.event.button will be defined instead --- editor/js/ui/view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/js/ui/view.js b/editor/js/ui/view.js index 4688c15cd..f3928351f 100644 --- a/editor/js/ui/view.js +++ b/editor/js/ui/view.js @@ -1766,7 +1766,7 @@ RED.view = (function() { clickTime = now; dblClickPrimed = (lastClickNode == mousedown_node && - d3.event.buttons === 1 && + d3.event.button === 0 && !d3.event.shiftKey && !d3.event.metaKey && !d3.event.altKey && !d3.event.ctrlKey); lastClickNode = mousedown_node; From 66ee27c5fa054d5f1269d91f0bbaf6b6e0e4c41d Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 14 Sep 2018 14:03:36 +0100 Subject: [PATCH 10/16] Switch node: only use promises when absolutely necessary Fixes a significant performance regression introduced when the node was made async-only with the persistent context work. --- nodes/core/logic/10-switch.js | 321 ++++++++++++++++++++++++---------- 1 file changed, 230 insertions(+), 91 deletions(-) diff --git a/nodes/core/logic/10-switch.js b/nodes/core/logic/10-switch.js index 89593047e..1d9c52971 100644 --- a/nodes/core/logic/10-switch.js +++ b/nodes/core/logic/10-switch.js @@ -92,31 +92,80 @@ module.exports = function(RED) { } function getProperty(node,msg) { - return new Promise((resolve,reject) => { + if (node.useAsyncRules) { + return new Promise((resolve,reject) => { + if (node.propertyType === 'jsonata') { + RED.util.evaluateJSONataExpression(node.property,msg,(err,value) => { + if (err) { + reject(RED._("switch.errors.invalid-expr",{error:err.message})); + } else { + resolve(value); + } + }); + } else { + RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg,(err,value) => { + if (err) { + resolve(undefined); + } else { + resolve(value); + } + }); + } + }); + } else { if (node.propertyType === 'jsonata') { - RED.util.evaluateJSONataExpression(node.property,msg,(err,value) => { - if (err) { - reject(RED._("switch.errors.invalid-expr",{error:err.message})); - } else { - resolve(value); - } - }); + try { + return RED.util.evaluateJSONataExpression(node.property,msg); + } catch(err) { + throw new Error(RED._("switch.errors.invalid-expr",{error:err.message})) + } } else { - RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg,(err,value) => { - if (err) { - resolve(undefined); - } else { - resolve(value); - } - }); + try { + return RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg); + } catch(err) { + return undefined; + } } - }); + } } function getV1(node,msg,rule,hasParts) { - return new Promise( (resolve,reject) => { + if (node.useAsyncRules) { + return new Promise( (resolve,reject) => { + if (rule.vt === 'prev') { + resolve(node.previousValue); + } else if (rule.vt === 'jsonata') { + var exp = rule.v; + if (rule.t === 'jsonata_exp') { + if (hasParts) { + exp.assign("I", msg.parts.index); + exp.assign("N", msg.parts.count); + } + } + RED.util.evaluateJSONataExpression(exp,msg,(err,value) => { + if (err) { + reject(RED._("switch.errors.invalid-expr",{error:err.message})); + } else { + resolve(value); + } + }); + } else if (rule.vt === 'json') { + resolve("json"); // TODO: ?! invalid case + } else if (rule.vt === 'null') { + resolve("null"); + } else { + RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg, function(err,value) { + if (err) { + resolve(undefined); + } else { + resolve(value); + } + }); + } + }); + } else { if (rule.vt === 'prev') { - resolve(node.previousValue); + return node.previousValue; } else if (rule.vt === 'jsonata') { var exp = rule.v; if (rule.t === 'jsonata_exp') { @@ -125,83 +174,120 @@ module.exports = function(RED) { exp.assign("N", msg.parts.count); } } - RED.util.evaluateJSONataExpression(exp,msg,(err,value) => { - if (err) { - reject(RED._("switch.errors.invalid-expr",{error:err.message})); - } else { - resolve(value); - } - }); + try { + return RED.util.evaluateJSONataExpression(exp,msg); + } catch(err) { + throw new Error(RED._("switch.errors.invalid-expr",{error:err.message})) + } } else if (rule.vt === 'json') { - resolve("json"); + return "json"; // TODO: ?! invalid case } else if (rule.vt === 'null') { - resolve("null"); + return "null"; } else { - RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg, function(err,value) { - if (err) { - resolve(undefined); - } else { - resolve(value); - } - }); + try { + return RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg); + } catch(err) { + return undefined; + } } - }); + } } function getV2(node,msg,rule) { - return new Promise((resolve,reject) => { + if (node.useAsyncRules) { + return new Promise((resolve,reject) => { + var v2 = rule.v2; + if (rule.v2t === 'prev') { + resolve(node.previousValue); + } else if (rule.v2t === 'jsonata') { + RED.util.evaluateJSONataExpression(rule.v2,msg,(err,value) => { + if (err) { + reject(RED._("switch.errors.invalid-expr",{error:err.message})); + } else { + resolve(value); + } + }); + } else if (typeof v2 !== 'undefined') { + RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg, function(err,value) { + if (err) { + resolve(undefined); + } else { + resolve(value); + } + }); + } else { + resolve(v2); + } + }) + } else { var v2 = rule.v2; if (rule.v2t === 'prev') { - resolve(node.previousValue); + return node.previousValue; } else if (rule.v2t === 'jsonata') { - RED.util.evaluateJSONataExpression(rule.v2,msg,(err,value) => { - if (err) { - reject(RED._("switch.errors.invalid-expr",{error:err.message})); - } else { - resolve(value); - } - }); + try { + return RED.util.evaluateJSONataExpression(rule.v2,msg); + } catch(err) { + throw new Error(RED._("switch.errors.invalid-expr",{error:err.message})) + } } else if (typeof v2 !== 'undefined') { - RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg, function(err,value) { - if (err) { - resolve(undefined); - } else { - resolve(value); - } - }); + try { + return RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg); + } catch(err) { + return undefined; + } } else { - resolve(v2); + return v2; } - }) + } } function applyRule(node, msg, property, state) { - return new Promise((resolve,reject) => { + if (node.useAsyncRules) { + return new Promise((resolve,reject) => { - var rule = node.rules[state.currentRule]; - var v1,v2; + var rule = node.rules[state.currentRule]; + var v1,v2; - getV1(node,msg,rule,state.hasParts).then(value => { - v1 = value; - }).then(()=>getV2(node,msg,rule)).then(value => { - v2 = value; - }).then(() => { - if (rule.t == "else") { - property = state.elseflag; - state.elseflag = true; - } - if (operators[rule.t](property,v1,v2,rule.case,msg.parts)) { - state.onward.push(msg); - state.elseflag = false; - if (node.checkall == "false") { - return resolve(false); + getV1(node,msg,rule,state.hasParts).then(value => { + v1 = value; + }).then(()=>getV2(node,msg,rule)).then(value => { + v2 = value; + }).then(() => { + if (rule.t == "else") { + property = state.elseflag; + state.elseflag = true; } - } else { - state.onward.push(null); + if (operators[rule.t](property,v1,v2,rule.case,msg.parts)) { + state.onward.push(msg); + state.elseflag = false; + if (node.checkall == "false") { + return resolve(false); + } + } else { + state.onward.push(null); + } + resolve(state.currentRule < node.rules.length - 1); + }); + }) + } else { + var rule = node.rules[state.currentRule]; + var v1 = getV1(node,msg,rule,state.hasParts); + var v2 = getV2(node,msg,rule); + if (rule.t == "else") { + property = state.elseflag; + state.elseflag = true; + } + if (operators[rule.t](property,v1,v2,rule.case,msg.parts)) { + state.onward.push(msg); + state.elseflag = false; + if (node.checkall == "false") { + return false; } - resolve(state.currentRule < node.rules.length - 1); - }); - }) + } else { + state.onward.push(null); + } + return state.currentRule < node.rules.length - 1 + } } function applyRules(node, msg, property,state) { @@ -215,7 +301,18 @@ module.exports = function(RED) { msg.parts.hasOwnProperty("index") } } - return applyRule(node,msg,property,state).then(hasMore => { + if (node.useAsyncRules) { + return applyRule(node,msg,property,state).then(hasMore => { + if (hasMore) { + state.currentRule++; + return applyRules(node,msg,property,state); + } else { + node.previousValue = property; + return state.onward; + } + }); + } else { + var hasMore = applyRule(node,msg,property,state); if (hasMore) { state.currentRule++; return applyRules(node,msg,property,state); @@ -223,7 +320,7 @@ module.exports = function(RED) { node.previousValue = property; return state.onward; } - }); + } } @@ -248,6 +345,14 @@ module.exports = function(RED) { var valid = true; var repair = n.repair; var needsCount = repair; + this.useAsyncRules = ( + this.propertyType === 'flow' || + this.propertyType === 'global' || ( + this.propertyType === 'jsonata' && + /\$(flow|global)Context/.test(this.property) + ) + ); + for (var i=0; i applyRules(node,msg,property)) - .then(onward => { - if (!repair || !hasParts) { - node.send(onward); - } - else { - sendGroupMessages(onward, msg); - } - }).catch(err => { - node.warn(err); - }); + if (node.useAsyncRules) { + return getProperty(node,msg) + .then(property => applyRules(node,msg,property)) + .then(onward => { + if (!repair || !hasParts) { + node.send(onward); + } + else { + sendGroupMessages(onward, msg); + } + }).catch(err => { + node.warn(err); + }); + } else { + try { + var property = getProperty(node,msg); + var onward = applyRules(node,msg,property); + if (!repair || !hasParts) { + node.send(onward); + } else { + sendGroupMessages(onward, msg); + } + } catch(err) { + node.warn(err); + } + } } function clearPending() { @@ -473,7 +608,11 @@ module.exports = function(RED) { } this.on('input', function(msg) { - processMessageQueue(msg); + if (node.useAsyncRules) { + processMessageQueue(msg); + } else { + processMessage(msg,true); + } }); this.on('close', function() { From a8ec032553cc4fa51803b84fff411507c1b4dfa7 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 14 Sep 2018 23:21:05 +0100 Subject: [PATCH 11/16] Allow context store name to be provided in the key For nodes that get/set context, when multiple stores are configured they will not know to parse the store name from the key. So they will pass the store name in the key, such as #:(store)::key. Currently that will cause that full string to be used as the key and the default context store used - which is wrong. The code now parses out the store name from the key if it is set - athough if the call to get/set does include the store argument, it will take precedence. This only applies when the key is a string - it doesn't apply when an array of keys is provided. --- red/runtime/nodes/context/index.js | 52 ++++++++++++++------ test/red/runtime/nodes/context/index_spec.js | 17 +++++++ 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/red/runtime/nodes/context/index.js b/red/runtime/nodes/context/index.js index f048947ad..d08734a2f 100644 --- a/red/runtime/nodes/context/index.js +++ b/red/runtime/nodes/context/index.js @@ -225,18 +225,28 @@ function createContext(id,seed) { get: { value: function(key, storage, callback) { var context; - if (!storage && !callback) { - context = stores["_"]; + + if (!callback && typeof storage === 'function') { + callback = storage; + storage = undefined; + } + if (callback && typeof callback !== 'function'){ + throw new Error("Callback must be a function"); + } + + if (!Array.isArray(key)) { + var keyParts = util.parseContextStore(key); + key = keyParts.key; + if (!storage) { + storage = keyParts.store || "_"; + } } else { - if (typeof storage === 'function') { - callback = storage; + if (!storage) { storage = "_"; } - if (callback && typeof callback !== 'function'){ - throw new Error("Callback must be a function"); - } - context = getContextStorage(storage); } + context = getContextStorage(storage); + if (callback) { if (!seed) { context.get(scope,key,callback); @@ -270,18 +280,28 @@ function createContext(id,seed) { set: { value: function(key, value, storage, callback) { var context; - if (!storage && !callback) { - context = stores["_"]; + + if (!callback && typeof storage === 'function') { + callback = storage; + storage = undefined; + } + if (callback && typeof callback !== 'function'){ + throw new Error("Callback must be a function"); + } + + if (!Array.isArray(key)) { + var keyParts = util.parseContextStore(key); + key = keyParts.key; + if (!storage) { + storage = keyParts.store || "_"; + } } else { - if (typeof storage === 'function') { - callback = storage; + if (!storage) { storage = "_"; } - if (callback && typeof callback !== 'function') { - throw new Error("Callback must be a function"); - } - context = getContextStorage(storage); } + context = getContextStorage(storage); + context.set(scope, key, value, callback); } }, diff --git a/test/red/runtime/nodes/context/index_spec.js b/test/red/runtime/nodes/context/index_spec.js index 3beb9b201..c723a0a6e 100644 --- a/test/red/runtime/nodes/context/index_spec.js +++ b/test/red/runtime/nodes/context/index_spec.js @@ -499,6 +499,23 @@ describe('context', function() { done(); }).catch(done); }); + + it('should allow the store name to be provide in the key', function(done) { + Context.init({contextStorage:contextDefaultStorage}); + Context.load().then(function(){ + var context = Context.get("1","flow"); + var cb = function(){done("An error occurred")} + context.set("#:(test)::foo","bar"); + context.get("#:(test)::foo"); + stubGet2.called.should.be.false(); + stubSet2.called.should.be.false(); + stubSet.calledWithExactly("1:flow","foo","bar",undefined).should.be.true(); + stubGet.calledWith("1:flow","foo").should.be.true(); + done(); + }).catch(done); + }); + + it('should use default as the alias of other context', function(done) { Context.init({contextStorage:contextAlias}); Context.load().then(function(){ From fd86035865b0da7bf92fee2e9872a4039cc6054e Mon Sep 17 00:00:00 2001 From: Hiroki Uchikawa <31908137+HirokiUchikawa@users.noreply.github.com> Date: Mon, 17 Sep 2018 05:15:23 +0900 Subject: [PATCH 12/16] Prevent race condition (#1889) * Make pending Flag to be deleted after write process complete. * Prevent executing write process until the previous process is completed * Fix to prevent file write race condition when closing file context * Make flushing rerun if pendingWrites was added --- red/runtime/nodes/context/localfilesystem.js | 35 ++++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/red/runtime/nodes/context/localfilesystem.js b/red/runtime/nodes/context/localfilesystem.js index 548c98c60..ee07dd26b 100644 --- a/red/runtime/nodes/context/localfilesystem.js +++ b/red/runtime/nodes/context/localfilesystem.js @@ -142,6 +142,7 @@ function LocalFileSystem(config){ this.storageBaseDir = getBasePath(this.config); if (config.hasOwnProperty('cache')?config.cache:true) { this.cache = MemoryStore({}); + this.writePromise = Promise.resolve(); } this.pendingWrites = {}; this.knownCircularRefs = {}; @@ -203,8 +204,22 @@ LocalFileSystem.prototype.open = function(){ log.debug("Flushing localfilesystem context scope "+scope); promises.push(fs.outputFile(storagePath + ".json", stringifiedContext.json, "utf8")); }); - delete self._pendingWriteTimeout; - return Promise.all(promises); + return Promise.all(promises).then(function(){ + if(Object.keys(self.pendingWrites).length > 0){ + // Rerun flushing if pendingWrites was added when the promise was running + return new Promise(function(resolve, reject) { + setTimeout(function() { + self._flushPendingWrites.call(self).then(function(){ + resolve(); + }).catch(function(err) { + reject(err); + }); + }, self.flushInterval); + }); + } else { + delete self._pendingWriteTimeout; + } + }); } }); } else { @@ -213,10 +228,16 @@ LocalFileSystem.prototype.open = function(){ } LocalFileSystem.prototype.close = function(){ - if (this.cache && this._flushPendingWrites) { + var self = this; + if (this.cache && this._pendingWriteTimeout) { clearTimeout(this._pendingWriteTimeout); delete this._pendingWriteTimeout; - return this._flushPendingWrites(); + this.flushInterval = 0; + return this.writePromise.then(function(){ + if(Object.keys(self.pendingWrites).length > 0) { + return self._flushPendingWrites(); + } + }); } return Promise.resolve(); } @@ -277,8 +298,10 @@ LocalFileSystem.prototype.set = function(scope, key, value, callback) { return; } else { this._pendingWriteTimeout = setTimeout(function() { - self._flushPendingWrites.call(self).catch(function(err) { - log.error(log._("context.localfilesystem.error-write",{message:err.toString()})) + self.writePromise = self.writePromise.then(function(){ + return self._flushPendingWrites.call(self).catch(function(err) { + log.error(log._("context.localfilesystem.error-write",{message:err.toString()})); + }); }); }, this.flushInterval); } From 9777af7cb5f0eca4767cef8ad7820b6ce4b2b288 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sun, 16 Sep 2018 22:04:09 +0100 Subject: [PATCH 13/16] LocalFileSystem Context: Remove extra flush code --- red/runtime/nodes/context/localfilesystem.js | 24 +++----------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/red/runtime/nodes/context/localfilesystem.js b/red/runtime/nodes/context/localfilesystem.js index ee07dd26b..a507ce2b4 100644 --- a/red/runtime/nodes/context/localfilesystem.js +++ b/red/runtime/nodes/context/localfilesystem.js @@ -204,22 +204,8 @@ LocalFileSystem.prototype.open = function(){ log.debug("Flushing localfilesystem context scope "+scope); promises.push(fs.outputFile(storagePath + ".json", stringifiedContext.json, "utf8")); }); - return Promise.all(promises).then(function(){ - if(Object.keys(self.pendingWrites).length > 0){ - // Rerun flushing if pendingWrites was added when the promise was running - return new Promise(function(resolve, reject) { - setTimeout(function() { - self._flushPendingWrites.call(self).then(function(){ - resolve(); - }).catch(function(err) { - reject(err); - }); - }, self.flushInterval); - }); - } else { - delete self._pendingWriteTimeout; - } - }); + delete self._pendingWriteTimeout; + return Promise.all(promises); } }); } else { @@ -233,11 +219,7 @@ LocalFileSystem.prototype.close = function(){ clearTimeout(this._pendingWriteTimeout); delete this._pendingWriteTimeout; this.flushInterval = 0; - return this.writePromise.then(function(){ - if(Object.keys(self.pendingWrites).length > 0) { - return self._flushPendingWrites(); - } - }); + return this.writePromise; } return Promise.resolve(); } From c1d50e82e1bbf09f41626066f9fac90076a99cd4 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 17 Sep 2018 10:31:00 +0100 Subject: [PATCH 14/16] Fix race condition in non-cache lfs context Fixes #1888 --- red/runtime/nodes/context/localfilesystem.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/red/runtime/nodes/context/localfilesystem.js b/red/runtime/nodes/context/localfilesystem.js index a507ce2b4..0e6f8735b 100644 --- a/red/runtime/nodes/context/localfilesystem.js +++ b/red/runtime/nodes/context/localfilesystem.js @@ -140,9 +140,9 @@ function stringify(value) { function LocalFileSystem(config){ this.config = config; this.storageBaseDir = getBasePath(this.config); + this.writePromise = Promise.resolve(); if (config.hasOwnProperty('cache')?config.cache:true) { this.cache = MemoryStore({}); - this.writePromise = Promise.resolve(); } this.pendingWrites = {}; this.knownCircularRefs = {}; @@ -219,9 +219,8 @@ LocalFileSystem.prototype.close = function(){ clearTimeout(this._pendingWriteTimeout); delete this._pendingWriteTimeout; this.flushInterval = 0; - return this.writePromise; } - return Promise.resolve(); + return this.writePromise; } LocalFileSystem.prototype.get = function(scope, key, callback) { @@ -290,7 +289,7 @@ LocalFileSystem.prototype.set = function(scope, key, value, callback) { } else if (callback && typeof callback !== 'function') { throw new Error("Callback must be a function"); } else { - loadFile(storagePath + ".json").then(function(data){ + self.writePromise = self.writePromise.then(function() { return loadFile(storagePath + ".json") }).then(function(data){ var obj = data ? JSON.parse(data) : {} if (!Array.isArray(key)) { key = [key]; From 08fccc4e775121cb4eba2526666e010dd6e85b72 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 17 Sep 2018 11:26:06 +0100 Subject: [PATCH 15/16] Update for 0.19.4 --- CHANGELOG.md | 15 +++++++++++++++ package.json | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6770f3158..29b8dabb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +#### 0.19.4: Maintenance Release + + - Fix race condition in non-cache lfs context Fixes #1888 + - LocalFileSystem Context: Remove extra flush code + - Prevent race condition in caching mode of lfs context (#1889) + - Allow context store name to be provided in the key + - Switch node: only use promises when absolutely necessary + - Fix dbl-click handling on webkit-based browsers + - Ensure context.flow/global cannot be deleted or enumerated + - Handle context.get with multiple levels of unknown key Fixes #1883 + - Fix global.get("foo.bar") for functionGlobalContext set values + - Fix node color bug (#1877) + - Merge pull request #1857 from cclauss/patch-1 + - Define raw_input() in Python 3 & fix time.sleep() + #### 0.19.3: Maintenance Release - Split node - fix complete to send msg for k/v object diff --git a/package.json b/package.json index 57d3cc9d8..6c025db08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "0.19.3", + "version": "0.19.4", "description": "A visual tool for wiring the Internet of Things", "homepage": "http://nodered.org", "license": "Apache-2.0", From 5bb27109bf5ee3859e2b4a29625b155cb684d36d Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Mon, 17 Sep 2018 11:35:00 +0100 Subject: [PATCH 16/16] Config search (#1880) * Let nodes be findable direct from config node info * add link to search as tag * don't expose onclick in sidebar --- editor/js/ui/search.js | 7 ++++++- editor/js/ui/tab-config.js | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/editor/js/ui/search.js b/editor/js/ui/search.js index f13281807..837f6831e 100644 --- a/editor/js/ui/search.js +++ b/editor/js/ui/search.js @@ -118,6 +118,7 @@ RED.search = (function() { } } } + function ensureSelectedIsVisible() { var selectedEntry = searchResults.find("li.selected"); if (selectedEntry.length === 1) { @@ -143,6 +144,7 @@ RED.search = (function() { search($(this).val()); } }); + searchInput.on('keydown',function(evt) { var children; if (results.length > 0) { @@ -229,12 +231,13 @@ RED.search = (function() { }); } + function reveal(node) { hide(); RED.view.reveal(node.id); } - function show() { + function show(v) { if (disabled) { return; } @@ -250,11 +253,13 @@ RED.search = (function() { createDialog(); } dialog.slideDown(300); + searchInput.searchBox('value',v) RED.events.emit("search:open"); visible = true; } searchInput.focus(); } + function hide() { if (visible) { RED.keyboard.remove("escape"); diff --git a/editor/js/ui/tab-config.js b/editor/js/ui/tab-config.js index 00caea6cd..438fbe359 100644 --- a/editor/js/ui/tab-config.js +++ b/editor/js/ui/tab-config.js @@ -140,7 +140,8 @@ RED.sidebar.config = (function() { var entry = $('
    • ').appendTo(list); $('
      ').text(label).appendTo(entry); if (node._def.hasUsers !== false) { - var iconContainer = $('