Merge pull request #3152 from node-red/link-call

Add Link Call node
This commit is contained in:
Nick O'Leary 2021-10-01 15:59:43 +01:00 committed by GitHub
commit da114fa3a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 349 additions and 44 deletions

View File

@ -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<searchSet.length;n++) {
var node = searchSet[n];
if (objectLookup) {
node = nodes[node];
}
if (filter.hasOwnProperty("type") && node.type !== filter.type) {
continue;
}
@ -1118,8 +1124,8 @@ RED.nodes = (function() {
}
}
if ((!n._def.defaults || !n._def.defaults.hasOwnProperty("l")) && n.hasOwnProperty('l')) {
var isLink = /^link (in|out)$/.test(node.type);
if (isLink == n.l) {
var showLabel = n._def.hasOwnProperty("showLabel")?n._def.showLabel:true;
if (showLabel != n.l) {
node.l = n.l;
}
}

View File

@ -41,6 +41,7 @@
* sublabel: 'Local', // a sub-label for the item
* icon: 'fa fa-rocket', // (optional) icon for the item
* checkbox: true/false, // (optional) if present, display checkbox accordingly
* radio: 'group-name', // (optional) if present, display radio box - using group-name to set radio group
* selected: true/false, // (optional) whether the item is selected or not
* children: [] | function(done,item) // (optional) an array of child items, or a function
* // that will call the `done` callback with an array
@ -640,6 +641,41 @@
}
}
selectWrapper.appendTo(label)
} else if (item.radio) {
var selectWrapper = $('<span class="red-ui-treeList-icon"></span>');
var cb = $('<input class="red-ui-treeList-radio" type="radio">').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) {

View File

@ -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;

View File

@ -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");

View File

@ -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;
}

View File

@ -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<movingSet.length();i++) {
var msn = movingSet.get(i);
if ((msn.n.type === "link out" || msn.n.type === "link in") &&
if (((msn.n.type === "link out" && msn.n.mode !== 'return') || msn.n.type === "link in") &&
(msn.n.z === activeWorkspace)) {
var linkNode = msn.n;
activeLinkNodes[linkNode.id] = linkNode;
@ -4140,7 +4140,7 @@ RED.view = (function() {
}
var numOutputs = d.outputs;
if (isLink && d.type === "link out") {
if (showAllLinkPorts===PORT_TYPE_OUTPUT || activeLinkNodes[d.id]) {
if (d.mode !== "return" && (showAllLinkPorts===PORT_TYPE_OUTPUT || activeLinkNodes[d.id])) {
numOutputs = 1;
} else {
numOutputs = 0;

View File

@ -95,7 +95,8 @@
color: $list-item-color;
}
input.red-ui-treeList-checkbox {
input.red-ui-treeList-checkbox,
input.red-ui-treeList-radio {
margin: 0;
}
}

View File

@ -12,6 +12,26 @@
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row">
<label for="node-input-mode"><span data-i18n="link.outMode"></span></label>
<select id="node-input-mode" style="width: 70%">
<option value="link" selected data-i18n="link.sendToAll"></option>
<option value="return" data-i18n="link.returnToCaller"></option>
</select>
</div>
<div class="node-input-link-rows" style="position:relative; height: 30px; text-align: right;"><div style="display:inline-block"><input type="text" id="node-input-link-target-filter"></div></div>
<div class="form-row node-input-link-row node-input-link-rows"></div>
</script>
<script type="text/html" data-template-name="link call">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row">
<label for="node-input-timeout"><span data-i18n="exec.label.timeout"></span></label>
<input type="text" id="node-input-timeout" placeholder="30" style="width: 70px; margin-right: 5px;"><span data-i18n="inject.seconds"></span>
</div>
<div style="position:relative; height: 30px; text-align: right;"><div style="display:inline-block"><input type="text" id="node-input-link-target-filter"></div></div>
<div class="form-row node-input-link-row"></div>
</script>
@ -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<length;i++) {
@ -195,6 +222,7 @@
outputLabels: function(i) {
return this.name||this._("link.linkIn");
},
showLabel: false,
label: function() {
return this.name||this._("link.linkIn");
},
@ -211,25 +239,32 @@
oneditresize: resizeNodeList
});
RED.nodes.registerType('link out',{
RED.nodes.registerType('link call',{
category: 'common',
color:"#ddd",//"#87D8CF",
defaults: {
name: {value:""},
links: { value: [], type:"link in[]"}
links: { value: [], type:"link in[]"},
timeout: { value: "30", validate:RED.validators.number(true) }
},
align:"right",
inputs:1,
outputs:0,
icon: "link-out.svg",
inputs: 1,
outputs: 1,
icon: "link-call.svg",
inputLabels: function(i) {
return this.name||this._("link.linkOut");
return this.name||this._("link.linkCall");
},
label: function() {
return this.name||this._("link.linkOut");
if (this.name) {
return this.name;
}
if (this.links.length > 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
});
})();
</script>

View File

@ -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);
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="10.583mm" height="15.875mm" version="1.1" viewBox="0 0 10.583 15.875" xmlns="http://www.w3.org/2000/svg">
<path d="m8.2021 2.3812-4.8922 0.53612 1.604 0.92604-1.0395 1.8004c0.73719-0.37402 1.6437-0.38227 2.4095 0.059892 0.76511 0.44174 1.2118 1.2301 1.2577 2.055l1.0384-1.7986 1.604 0.92604zm-2.3813 4.1244c-0.77016-0.44465-1.7402-0.18474-2.1848 0.58542-0.44465 0.77016-0.185 1.7406 0.58516 2.1853 0.77016 0.44465 1.7422 0.18533 2.1869-0.58483 0.44465-0.77016 0.18295-1.7412-0.58721-2.1858zm-3.3193 1.5159-1.8211 3.1542 3.6662 2.1167 1.82-3.1524c-0.73731 0.37266-1.6431 0.37961-2.4082-0.062129-0.76585-0.44216-1.2122-1.2309-1.2569-2.0563z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 771 B

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="10.583mm" height="15.875mm" version="1.1" viewBox="0 0 10.583 15.875" xmlns="http://www.w3.org/2000/svg">
<path d="m2.6623 13.292 4.8922-0.53612-1.604-0.92604 1.0395-1.8004c-0.73719 0.37402-1.6437 0.38227-2.4095-0.059892-0.76511-0.44174-1.2118-1.2301-1.2577-2.055l-1.0384 1.7986-1.604-0.92604zm2.3813-4.1244c0.77016 0.44465 1.7402 0.18474 2.1848-0.58542 0.44465-0.77016 0.185-1.7406-0.58516-2.1853-0.77016-0.44465-1.7422-0.18533-2.1869 0.58483-0.44465 0.77016-0.18295 1.7412 0.58721 2.1858zm3.3193-1.5159 1.8211-3.1542-3.6662-2.1167-1.82 3.1524c0.73731-0.37266 1.6431-0.37961 2.4082 0.062129 0.76585 0.44216 1.2122 1.2309 1.2569 2.0563z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 769 B

View File

@ -28,10 +28,23 @@
<script type="text/html" data-help-name="link out">
<p>Create virtual wires between flows.</p>
<h3>Details</h3>
<p>The node can be connected to any <code>link in</code> node that exists on any tab.
Once connected, they behave as if they were wired together.</p>
<p>The wires between link nodes are only displayed when a link node is selected.
If there are any wires to other tabs, a virtual node is show that can be clicked
on to jump to the appropriate tab.</p>
<p>This node can be configured to either send messages to all <code>link in</code>
nodes it is connected to, or to send a response back to the <code>link call</code>
node that triggered the flow.</p>
<p>When in 'send to all' mode, the wires between link nodes are only displayed when
the node is selected. If there are any wires to other tabs, a virtual node
is shown that can be clicked on to jump to the appropriate tab.</p>
<p><b>Note: </b>Links cannot be created going into, or out of, a subflow.</p>
</script>
<script type="text/html" data-help-name="link call">
<p>Calls a flow that starts with a <code>link in</code> node and passes on the response.</p>
<h3>Details</h3>
<p>This node can be connected to a <code>link in</code> node that exists on any tab.
The flow connected to that node must end with a <code>link out</code> node configured
in 'return' mode.</p>
<p>When this node receives a message, it is passed to the connected <code>link in</code> node.
It then waits for a response which it then sends on.</o>
<p>If no response is received within the configured timeout, default 30 seconds, the node
will log an error that can be caught using the <code>catch</code> node.</p>
</script>

View File

@ -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",

View File

@ -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});
});
})
});