diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index da3bdb729..a19830709 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -448,12 +448,18 @@ RED.nodes = (function() { doZFilter = true; } } + var objectLookup = false; if (searchSet === null) { - searchSet = nodes; + searchSet = Object.keys(nodes); + objectLookup = true; } + for (var n=0;n'); + var cb = $('').prop('name', item.radio).prop('checked',item.selected).appendTo(selectWrapper); + cb.on('click', function(e) { + e.stopPropagation(); + }); + cb.on('change', function(e) { + item.selected = this.checked; + that._selected.forEach(function(selectedItem) { + if (selectedItem.radio === item.radio) { + selectedItem.treeList.label.removeClass("selected"); + selectedItem.selected = false; + that._selected.delete(selectedItem); + } + }) + if (item.selected) { + that._selected.add(item); + } else { + that._selected.delete(item); + } + label.toggleClass("selected",this.checked); + that._trigger("select",e,item); + }) + if (!item.children) { + label.on("click", function(e) { + e.stopPropagation(); + cb.trigger("click"); + }) + } + item.treeList.select = function(v) { + if (v !== item.selected) { + cb.trigger("click"); + } + } + selectWrapper.appendTo(label) } else { label.on("click", function(e) { if (!that.options.multi) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js index a5428e3ed..b1c6e18b7 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js @@ -151,7 +151,7 @@ RED.editor = (function() { valid = definition[property].hasOwnProperty("required") && !definition[property].required; } else { var configNode = RED.nodes.node(value); - valid = (configNode !== null && (configNode.valid == null || configNode.valid)); + valid = (configNode && (configNode.valid == null || configNode.valid)); } } return valid; diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/appearance.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/appearance.js index e910ede04..046141b52 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/appearance.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/appearance.js @@ -93,17 +93,20 @@ } + var showLabel = node._def.hasOwnProperty("showLabel")?node._def.showLabel:true; + if (!$("#node-input-show-label").prop('checked')) { // Not checked - hide label - if (!/^link (in|out)$/.test(node.type)) { - // Not a link node - default state is true + + if (showLabel) { + // Default to show label if (node.l !== false) { editState.changes.l = node.l editState.changed = true; } node.l = false; } else { - // A link node - default state is false + // Node has showLabel:false (eg link nodes) if (node.hasOwnProperty('l') && node.l) { editState.changes.l = node.l editState.changed = true; @@ -112,8 +115,8 @@ } } else { // Checked - show label - if (!/^link (in|out)$/.test(node.type)) { - // Not a link node - default state is true + if (showLabel) { + // Default to show label if (node.hasOwnProperty('l') && !node.l) { editState.changes.l = node.l editState.changed = true; @@ -204,8 +207,8 @@ }) if (!node.hasOwnProperty("l")) { - // Show label if type not link - node.l = !/^link (in|out)$/.test(node._def.type); + // Show label unless def.showLabel set to false + node.l = node._def.hasOwnProperty("showLabel")?node._def.showLabel:true; } $("#node-input-show-label").prop("checked",node.l).trigger("change"); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js index 2fba37e88..9b3977448 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js @@ -159,15 +159,15 @@ RED.view.tools = (function() { nodes.forEach(function(n) { var modified = false; var oldValue = n.l === undefined?true:n.l; - var isLink = /^link (in|out)$/.test(n._def.type); + var showLabel = n._def.hasOwnProperty("showLabel")?n._def.showLabel:true; if (labelShown) { - if (n.l === false || (isLink && !n.hasOwnProperty('l'))) { + if (n.l === false || (!showLabel && !n.hasOwnProperty('l'))) { n.l = true; modified = true; } } else { - if ((!isLink && (!n.hasOwnProperty('l') || n.l === true)) || (isLink && n.l === true) ) { + if ((showLabel && (!n.hasOwnProperty('l') || n.l === true)) || (!showLabel && n.l === true) ) { n.l = false; modified = true; } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 8ccbf6fe4..42455055b 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -413,7 +413,7 @@ RED.view = (function() { var nn = result.node; var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label"); - if (showLabel !== undefined && !/^link (in|out)$/.test(nn._def.type) && !nn._def.defaults.hasOwnProperty("l")) { + if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) { nn.l = showLabel; } @@ -1088,7 +1088,7 @@ RED.view = (function() { nn.x = point[0]; nn.y = point[1]; var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label"); - if (showLabel !== undefined && !/^link (in|out)$/.test(nn._def.type) && !nn._def.defaults.hasOwnProperty("l")) { + if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) { nn.l = showLabel; } if (quickAddLink) { @@ -1991,7 +1991,7 @@ RED.view = (function() { activeLinkNodes = {}; for (var i=0;i +
+ + +
+ + + + + @@ -48,7 +68,6 @@ } }); var candidateNodes = RED.nodes.filterNodes({type:targetType}); - var search = $("#node-input-link-target-filter").searchBox({ style: "compact", delay: 300, @@ -104,7 +123,8 @@ node: n, label: n.name||n.id, selected: isChecked, - checkbox: true + checkbox: node.type !== "link call", + radio: node.type === "link call" }) } }); @@ -129,15 +149,22 @@ function onEditSave(node) { var flows = treeList.treeList('data'); node.links = []; - flows.forEach(function(f) { - f.children.forEach(function(n) { - if (n.selected) { - node.links.push(n.id); - } + if (node.type !== "link out" || $("node-input-mode").val() === 'link') { + flows.forEach(function(f) { + f.children.forEach(function(n) { + if (n.selected) { + node.links.push(n.id); + } + }) }) - }) + } node.oldLinks.sort(); node.links.sort(); + + if (node.type === "link call") { + return + } + var nodeMap = {}; var length = Math.max(node.oldLinks.length,node.links.length); for (var i=0;i 0) { + var targetNode = RED.nodes.node(this.links[0]); + return targetNode && (targetNode.name || targetNode.id); + } + return this._("link.linkCall"); }, labelStyle: function() { - return this.name?"node_label_italic":""; + return (this.name || this.links.length > 0)?"node_label_italic":""; }, oneditprepare: function() { onEditPrepare(this,"link in"); @@ -237,8 +272,56 @@ oneditsave: function() { onEditSave(this); }, + oneditresize: resizeNodeList + }); + + + RED.nodes.registerType('link out',{ + category: 'common', + color:"#ddd",//"#87D8CF", + defaults: { + name: {value:""}, + mode: { value: "link" },// link || return + links: { value: [], type:"link in[]"} + }, + align:"right", + inputs:1, + outputs:0, + icon: function() { + if (this.mode === "return") { + return "link-return.svg"; + } else { + return "link-out.svg"; + } + }, + inputLabels: function(i) { + return this.name||(this.mode === "return" ?this._("link.linkOutReturn"):this._("link.linkOut")); + }, + showLabel: false, + label: function() { + return this.name||(this.mode === "return" ?this._("link.linkOutReturn"):this._("link.linkOut")); + }, + labelStyle: function() { + return this.name?"node_label_italic":""; + }, + oneditprepare: function() { + onEditPrepare(this,"link in"); + $("#node-input-mode").on("change", function() { + $(".node-input-link-rows").toggle(this.value === "link") + }) + if (!this.mode) { + $("#node-input-mode").val('link').trigger("change"); + } + + }, + oneditsave: function() { + onEditSave(this); + }, onadd: onAdd, oneditresize: resizeNodeList }); + + + })(); diff --git a/packages/node_modules/@node-red/nodes/core/common/60-link.js b/packages/node_modules/@node-red/nodes/core/common/60-link.js index 5470ed434..53404e446 100644 --- a/packages/node_modules/@node-red/nodes/core/common/60-link.js +++ b/packages/node_modules/@node-red/nodes/core/common/60-link.js @@ -17,6 +17,8 @@ module.exports = function(RED) { "use strict"; + const crypto = require("crypto"); + function LinkInNode(n) { RED.nodes.createNode(this,n); var node = this; @@ -40,13 +42,91 @@ module.exports = function(RED) { function LinkOutNode(n) { RED.nodes.createNode(this,n); var node = this; + var mode = n.mode || "link"; + var event = "node:"+n.id; this.on("input", function(msg, send, done) { msg._event = event; RED.events.emit(event,msg) - send(msg); - done(); + + if (mode === "return") { + if (Array.isArray(msg._linkSource) && msg._linkSource.length > 0) { + var messageEvent = msg._linkSource.pop(); + var returnNode = RED.nodes.getNode(messageEvent.node); + if (returnNode && returnNode.returnLinkMessage) { + returnNode.returnLinkMessage(messageEvent.id, msg); + } else { + node.warn(RED._("link.error.missingReturn")) + } + } else { + node.warn(RED._("link.error.missingReturn")) + } + done(); + } else if (mode === "link") { + send(msg); + done(); + } }); } RED.nodes.registerType("link out",LinkOutNode); + + + function LinkCallNode(n) { + RED.nodes.createNode(this,n); + const node = this; + const target = n.links[0]; + const messageEvents = {}; + let timeout = parseFloat(n.timeout || "30")*1000; + if (isNaN(timeout)) { + timeout = 30000; + } + + this.on("input", function(msg, send, done) { + msg._linkSource = msg._linkSource || []; + const messageEvent = { + id: crypto.randomBytes(14).toString('hex'), + node: node.id, + } + messageEvents[messageEvent.id] = { + msg: RED.util.cloneMessage(msg), + send, + done, + ts: setTimeout(function() { + timeoutMessage(messageEvent.id) + }, timeout ) + }; + msg._linkSource.push(messageEvent); + var targetNode = RED.nodes.getNode(target); + if (targetNode) { + targetNode.receive(msg); + } + }); + + this.returnLinkMessage = function(eventId, msg) { + if (Array.isArray(msg._linkSource) && msg._linkSource.length === 0) { + delete msg._linkSource; + } + const messageEvent = messageEvents[eventId]; + if (messageEvent) { + clearTimeout(messageEvent.ts); + delete messageEvents[eventId]; + messageEvent.send(msg); + messageEvent.done(); + } else { + node.send(msg); + } + } + + function timeoutMessage(eventId) { + const messageEvent = messageEvents[eventId]; + if (messageEvent) { + delete messageEvents[eventId]; + node.error("timeout",messageEvent.msg); + } + } + + } + RED.nodes.registerType("link call",LinkCallNode); + + } diff --git a/packages/node_modules/@node-red/nodes/icons/link-call.svg b/packages/node_modules/@node-red/nodes/icons/link-call.svg new file mode 100644 index 000000000..a3dcbdd22 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/icons/link-call.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/node_modules/@node-red/nodes/icons/link-return.svg b/packages/node_modules/@node-red/nodes/icons/link-return.svg new file mode 100644 index 000000000..e82ee92f6 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/icons/link-return.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/common/60-link.html b/packages/node_modules/@node-red/nodes/locales/en-US/common/60-link.html index 3d986ee9f..6bd6305f1 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/common/60-link.html +++ b/packages/node_modules/@node-red/nodes/locales/en-US/common/60-link.html @@ -28,10 +28,23 @@ + + diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json index a2e63a046..cbb344ed3 100755 --- a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json @@ -159,7 +159,15 @@ }, "link": { "linkIn": "link in", - "linkOut": "link out" + "linkOut": "link out", + "linkCall": "link call", + "linkOutReturn": "link return", + "outMode": "Mode", + "sendToAll": "Send to all connected link nodes", + "returnToCaller": "Return to calling link node", + "error": { + "missingReturn": "Missing return node information" + } }, "tls": { "tls": "TLS configuration", diff --git a/test/nodes/core/common/60-link_spec.js b/test/nodes/core/common/60-link_spec.js index 50072d761..5314eed15 100644 --- a/test/nodes/core/common/60-link_spec.js +++ b/test/nodes/core/common/60-link_spec.js @@ -119,4 +119,69 @@ describe('link Node', function() { }); }); + describe("link-call node", function() { + it('should call link-in node and get response', function(done) { + var flow = [{id:"link-in-1", type:"link in", wires: [[ "func"]]}, + {id:"func", type:"helper", wires: [["link-out-1"]]}, + {id:"link-out-1", type:"link out", mode: "return"}, + {id:"link-call", type:"link call", links:["link-in-1"], wires:[["n4"]]}, + {id:"n4", type:"helper"} ]; + helper.load(linkNode, flow, function() { + var func = helper.getNode("func"); + func.on("input", function(msg, send, done) { + msg.payload = "123"; + send(msg); + done(); + }) + var n1 = helper.getNode("link-call"); + var n4 = helper.getNode("n4"); + n4.on("input", function(msg) { + try { + msg.should.have.property('payload', '123'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"hello"}); + }); + }) + }); + + it('should allow nested link-call flows', function(done) { + var flow = [/** Multiply by 2 link flow **/ + {id:"li1", type:"link in", wires: [[ "m2"]]}, + {id:"m2", type:"helper", wires: [["lo1"]]}, + {id:"lo1", type:"link out", mode: "return"}, + /** Multiply by 3 link flow **/ + {id:"li2", type:"link in", wires: [[ "m3"]]}, + {id:"m3", type:"helper", wires: [["lo2"]]}, + {id:"lo2", type:"link out", mode: "return"}, + /** Multiply by 6 link flow **/ + {id:"li3", type:"link in", wires: [[ "link-call-1"]]}, + {id:"link-call-1", type:"link call", links:["m2"], wires:[["link-call-2"]]}, + {id:"link-call-2", type:"link call", links:["m3"], wires:[["lo3"]]}, + {id:"lo3", type:"link out", mode: "return"}, + /** Test Flow Entry **/ + {id:"link-call", type:"link call", links:["li3"], wires:[["n4"]]}, + {id:"n4", type:"helper"} ]; + helper.load(linkNode, flow, function() { + var m2 = helper.getNode("m2"); + m2.on("input", function(msg, send, done) { msg.payload *= 2 ; send(msg); done(); }) + var m3 = helper.getNode("m3"); + m3.on("input", function(msg, send, done) { msg.payload *= 3 ; send(msg); done(); }) + + var n1 = helper.getNode("link-call"); + var n4 = helper.getNode("n4"); + n4.on("input", function(msg) { + try { + msg.should.have.property('payload', 24); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:4}); + }); + }) });