mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
commit
aff8a7802a
BIN
public/icons/subflow.png
Normal file
BIN
public/icons/subflow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 439 B |
@ -51,11 +51,7 @@
|
||||
<div id="workspace-add-tab"><a id="btn-workspace-add-tab" href="#"><i class="fa fa-plus"></i></a></div>
|
||||
<div id="chart"></div>
|
||||
<div id="workspace-toolbar">
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-small" href="#"><i class="fa fa-search-minus"></i></a>
|
||||
<a class="btn btn-small" href="#"><i class="fa fa-dot-circle-o"></i></a>
|
||||
<a class="btn btn-small" href="#"><i class="fa fa-search-plus"></i></a>
|
||||
</div>
|
||||
<a class="button" id="workspace-edit-subflow" href="#"><i class="fa fa-pencil"></i> edit subflow properties</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -81,6 +77,20 @@
|
||||
|
||||
<div id="dialog" class="hide"><form id="dialog-form" class="form-horizontal"></form></div>
|
||||
<div id="node-config-dialog" class="hide"><form id="dialog-config-form" class="form-horizontal"></form><div class="form-tips" id="node-config-dialog-user-count"></div></div>
|
||||
<div id="subflow-dialog" class="hide">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-row">
|
||||
<label>Name</label><input type="text" id="subflow-input-name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>Inputs</label><input style="width: 60px; height: 1.7em;" id="subflow-input-inCount">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>Outputs</label><input style="width: 60px; height: 1.7em;" id="subflow-input-outCount">
|
||||
</div>
|
||||
</form>
|
||||
<div class="form-tips" id="subflow-dialog-user-count"></div>
|
||||
</div>
|
||||
|
||||
<div id="node-dialog-confirm-deploy" class="hide">
|
||||
<form class="form-horizontal">
|
||||
@ -199,7 +209,6 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script type="text/x-red" data-template-name="export-clipboard-dialog">
|
||||
<div class="form-row">
|
||||
<label for="node-input-export" style="display: block; width:100%;"><i class="fa fa-clipboard"></i> Nodes:</label>
|
||||
@ -222,6 +231,13 @@
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="subflow">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script src="jquery/js/jquery-1.11.1.min.js"></script>
|
||||
<script src="bootstrap/js/bootstrap.min.js"></script>
|
||||
<script src="jquery/js/jquery-ui-1.10.3.custom.min.js"></script>
|
||||
|
@ -50,6 +50,12 @@ RED.history = (function() {
|
||||
RED.view.removeWorkspace(ev.workspaces[i]);
|
||||
}
|
||||
}
|
||||
if (ev.subflows) {
|
||||
for (i=0;i<ev.subflows.length;i++) {
|
||||
RED.nodes.removeSubflow(ev.subflows[i]);
|
||||
RED.view.removeWorkspace(ev.subflows[i]);
|
||||
}
|
||||
}
|
||||
} else if (ev.t == "delete") {
|
||||
if (ev.workspaces) {
|
||||
for (i=0;i<ev.workspaces.length;i++) {
|
||||
@ -57,6 +63,9 @@ RED.history = (function() {
|
||||
RED.view.addWorkspace(ev.workspaces[i]);
|
||||
}
|
||||
}
|
||||
if (ev.subflow) {
|
||||
RED.nodes.addSubflow(ev.subflow);
|
||||
}
|
||||
if (ev.nodes) {
|
||||
for (i=0;i<ev.nodes.length;i++) {
|
||||
RED.nodes.add(ev.nodes[i]);
|
||||
@ -80,15 +89,66 @@ RED.history = (function() {
|
||||
ev.node[i] = ev.changes[i];
|
||||
}
|
||||
}
|
||||
if (ev.subflow) {
|
||||
if (ev.node.in.length > ev.subflow.inputCount) {
|
||||
ev.node.in.splice(ev.subflow.inputCount);
|
||||
} else if (ev.subflow.inputs.length > 0) {
|
||||
ev.node.in = ev.node.in.concat(ev.subflow.inputs);
|
||||
}
|
||||
if (ev.node.out.length > ev.subflow.outputCount) {
|
||||
ev.node.out.splice(ev.subflow.outputCount);
|
||||
} else if (ev.subflow.outputs.length > 0) {
|
||||
ev.node.out = ev.node.out.concat(ev.subflow.outputs);
|
||||
}
|
||||
RED.nodes.eachNode(function(n) {
|
||||
if (n.type == "subflow:"+ev.node.id) {
|
||||
n.changed = ev.changed;
|
||||
n.inputs = ev.node.in.length;
|
||||
n.outputs = ev.node.out.length;
|
||||
RED.editor.updateNodeProperties(n);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
RED.palette.refresh();
|
||||
} else {
|
||||
RED.editor.updateNodeProperties(ev.node);
|
||||
RED.editor.validateNode(ev.node);
|
||||
}
|
||||
if (ev.links) {
|
||||
for (i=0;i<ev.links.length;i++) {
|
||||
RED.nodes.addLink(ev.links[i]);
|
||||
}
|
||||
}
|
||||
RED.editor.validateNode(ev.node);
|
||||
ev.node.dirty = true;
|
||||
ev.node.changed = ev.changed;
|
||||
} else if (ev.t == "createSubflow") {
|
||||
if (ev.nodes) {
|
||||
RED.nodes.eachNode(function(n) {
|
||||
if (n.z === ev.subflow.id) {
|
||||
n.z = ev.activeWorkspace;
|
||||
n.dirty = true;
|
||||
}
|
||||
});
|
||||
for (i=0;i<ev.nodes.length;i++) {
|
||||
RED.nodes.remove(ev.nodes[i]);
|
||||
}
|
||||
}
|
||||
if (ev.links) {
|
||||
for (i=0;i<ev.links.length;i++) {
|
||||
RED.nodes.removeLink(ev.links[i]);
|
||||
}
|
||||
}
|
||||
|
||||
RED.nodes.removeSubflow(ev.subflow);
|
||||
RED.view.removeWorkspace(ev.subflow);
|
||||
|
||||
if (ev.removedLinks) {
|
||||
for (i=0;i<ev.removedLinks.length;i++) {
|
||||
RED.nodes.addLink(ev.removedLinks[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
RED.view.dirty(ev.dirty);
|
||||
RED.view.redraw();
|
||||
|
@ -301,6 +301,9 @@ var RED = (function() {
|
||||
null,
|
||||
{id:"btn-config-nodes",icon:"fa fa-th-list",label:"Configuration nodes...",onselect:RED.sidebar.config.show},
|
||||
null,
|
||||
{id:"btn-create-subflow",icon:"fa fa-share-alt",label:"Create subflow",onselect:RED.view.createSubflow},
|
||||
{id:"btn-convert-subflow",icon:"fa fa-share-alt",label:"Convert to subflow",disabled:true,onselect:RED.view.convertToSubflow},
|
||||
null,
|
||||
{id:"btn-workspace-menu",icon:"fa fa-th-large",label:"Workspaces",options:[
|
||||
{id:"btn-workspace-add",icon:"fa fa-plus",label:"Add"},
|
||||
{id:"btn-workspace-edit",icon:"fa fa-pencil",label:"Rename"},
|
||||
|
@ -21,6 +21,7 @@ RED.nodes = (function() {
|
||||
var links = [];
|
||||
var defaultWorkspace;
|
||||
var workspaces = {};
|
||||
var subflows = {};
|
||||
|
||||
var registry = (function() {
|
||||
var nodeList = [];
|
||||
@ -98,13 +99,22 @@ RED.nodes = (function() {
|
||||
},
|
||||
registerNodeType: function(nt,def) {
|
||||
nodeDefinitions[nt] = def;
|
||||
if (def.category != "subflows") {
|
||||
nodeSets[typeToId[nt]].added = true;
|
||||
// TODO: too tightly coupled into palette UI
|
||||
}
|
||||
RED.palette.add(nt,def);
|
||||
if (def.onpaletteadd && typeof def.onpaletteadd === "function") {
|
||||
def.onpaletteadd.call(def);
|
||||
}
|
||||
},
|
||||
removeNodeType: function(nt) {
|
||||
if (nt.substring(0,8) != "subflow:") {
|
||||
throw new Error("this api is subflow only. called with:",nt);
|
||||
}
|
||||
delete nodeDefinitions[nt];
|
||||
RED.palette.remove(nt);
|
||||
},
|
||||
getNodeType: function(nt) {
|
||||
return nodeDefinitions[nt];
|
||||
}
|
||||
@ -122,7 +132,6 @@ RED.nodes = (function() {
|
||||
RED.sidebar.config.refresh();
|
||||
} else {
|
||||
n.dirty = true;
|
||||
nodes.push(n);
|
||||
var updatedConfigNode = false;
|
||||
for (var d in n._def.defaults) {
|
||||
if (n._def.defaults.hasOwnProperty(d)) {
|
||||
@ -142,6 +151,14 @@ RED.nodes = (function() {
|
||||
if (updatedConfigNode) {
|
||||
RED.sidebar.config.refresh();
|
||||
}
|
||||
if (n._def.category == "subflows" && typeof n.i === "undefined") {
|
||||
var nextId = 0;
|
||||
RED.nodes.eachNode(function(node) {
|
||||
nextId = Math.max(nextId,node.i||0);
|
||||
});
|
||||
n.i = nextId+1;
|
||||
}
|
||||
nodes.push(n);
|
||||
}
|
||||
}
|
||||
function addLink(l) {
|
||||
@ -174,8 +191,7 @@ RED.nodes = (function() {
|
||||
if (node) {
|
||||
nodes.splice(nodes.indexOf(node),1);
|
||||
removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); });
|
||||
removedLinks.map(function(l) {links.splice(links.indexOf(l), 1); });
|
||||
}
|
||||
removedLinks.forEach(function(l) {links.splice(links.indexOf(l), 1); });
|
||||
var updatedConfigNode = false;
|
||||
for (var d in node._def.defaults) {
|
||||
if (node._def.defaults.hasOwnProperty(d)) {
|
||||
@ -197,6 +213,7 @@ RED.nodes = (function() {
|
||||
RED.sidebar.config.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
return removedLinks;
|
||||
}
|
||||
|
||||
@ -237,6 +254,50 @@ RED.nodes = (function() {
|
||||
return {nodes:removedNodes,links:removedLinks};
|
||||
}
|
||||
|
||||
function addSubflow(sf) {
|
||||
subflows[sf.id] = sf;
|
||||
RED.nodes.registerType("subflow:"+sf.id, {
|
||||
defaults:{name:{value:""}},
|
||||
icon:"subflow.png",
|
||||
category: "subflows",
|
||||
inputs: sf.in.length,
|
||||
outputs: sf.out.length,
|
||||
color: "#da9",
|
||||
label: function() { return this.name||RED.nodes.subflow(sf.id).name },
|
||||
labelStyle: function() { return this.name?"node_label_italic":""; },
|
||||
paletteLabel: function() { return RED.nodes.subflow(sf.id).name }
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
function getSubflow(id) {
|
||||
return subflows[id];
|
||||
}
|
||||
function removeSubflow(sf) {
|
||||
delete subflows[sf.id];
|
||||
registry.removeNodeType("subflow:"+sf.id);
|
||||
}
|
||||
|
||||
function subflowContains(sfid,nodeid) {
|
||||
for (var i=0;i<nodes.length;i++) {
|
||||
var node = nodes[i];
|
||||
if (node.z === sfid) {
|
||||
var m = /^subflow:(.+)$/.exec(node.type);
|
||||
if (m) {
|
||||
if (m[1] === nodeid) {
|
||||
return true;
|
||||
} else {
|
||||
var result = subflowContains(m[1],nodeid);
|
||||
if (result) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getAllFlowNodes(node) {
|
||||
var visited = {};
|
||||
visited[node.id] = true;
|
||||
@ -247,8 +308,12 @@ RED.nodes = (function() {
|
||||
var childLinks = links.filter(function(d) { return (d.source === n) || (d.target === n);});
|
||||
for (var i=0;i<childLinks.length;i++) {
|
||||
var child = (childLinks[i].source === n)?childLinks[i].target:childLinks[i].source;
|
||||
if (!visited[child.id]) {
|
||||
visited[child.id] = true;
|
||||
var id = child.id;
|
||||
if (!id) {
|
||||
id = child.direction+":"+child.i;
|
||||
}
|
||||
if (!visited[id]) {
|
||||
visited[id] = true;
|
||||
nns.push(child);
|
||||
stack.push(child);
|
||||
}
|
||||
@ -291,20 +356,74 @@ RED.nodes = (function() {
|
||||
var wires = links.filter(function(d){return d.source === n;});
|
||||
for (var j=0;j<wires.length;j++) {
|
||||
var w = wires[j];
|
||||
if (w.target.type != "subflow") {
|
||||
node.wires[w.sourcePort].push(w.target.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function convertSubflow(n) {
|
||||
var node = {};
|
||||
node.id = n.id;
|
||||
node.type = n.type;
|
||||
node.name = n.name;
|
||||
node.in = [];
|
||||
node.out = [];
|
||||
|
||||
n.in.forEach(function(p) {
|
||||
var nIn = {x:p.x,y:p.y,wires:[]};
|
||||
var wires = links.filter(function(d) { return d.source === p });
|
||||
for (var i=0;i<wires.length;i++) {
|
||||
var w = wires[i];
|
||||
if (w.target.id != p.id) {
|
||||
nIn.wires.push({id:w.target.id})
|
||||
}
|
||||
}
|
||||
node.in.push(nIn);
|
||||
});
|
||||
n.out.forEach(function(p,c) {
|
||||
var nOut = {x:p.x,y:p.y,wires:[]};
|
||||
var wires = links.filter(function(d) { return d.target === p });
|
||||
for (i=0;i<wires.length;i++) {
|
||||
if (wires[i].source.type != "subflow") {
|
||||
nOut.wires.push({id:wires[i].source.id,port:wires[i].sourcePort})
|
||||
} else {
|
||||
nOut.wires.push({id:n.id,port:0})
|
||||
}
|
||||
}
|
||||
node.out.push(nOut);
|
||||
});
|
||||
|
||||
|
||||
return node;
|
||||
}
|
||||
/**
|
||||
* Converts the current node selection to an exportable JSON Object
|
||||
**/
|
||||
function createExportableNodeSet(set) {
|
||||
var nns = [];
|
||||
var exportedConfigNodes = {};
|
||||
var exportedSubflows = {};
|
||||
for (var n=0;n<set.length;n++) {
|
||||
var node = set[n].n;
|
||||
if (node.type.substring(0,8) == "subflow:") {
|
||||
var subflowId = node.type.substring(8);
|
||||
if (!exportedSubflows[subflowId]) {
|
||||
exportedSubflows[subflowId] = true;
|
||||
var subflow = getSubflow(subflowId);
|
||||
var subflowSet = [{n:subflow}];
|
||||
RED.nodes.eachNode(function(n) {
|
||||
if (n.z == subflowId) {
|
||||
subflowSet.push({n:n});
|
||||
}
|
||||
});
|
||||
var exportableSubflow = createExportableNodeSet(subflowSet);
|
||||
nns = exportableSubflow.concat(nns);
|
||||
}
|
||||
}
|
||||
if (node.type != "subflow") {
|
||||
var convertedNode = RED.nodes.convertNode(node);
|
||||
for (var d in node._def.defaults) {
|
||||
if (node._def.defaults[d].type && node[d] in configNodes) {
|
||||
@ -320,8 +439,11 @@ RED.nodes = (function() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nns.push(convertedNode);
|
||||
} else {
|
||||
var convertedSubflow = convertSubflow(node);
|
||||
nns.push(convertedSubflow);
|
||||
}
|
||||
}
|
||||
return nns;
|
||||
}
|
||||
@ -332,9 +454,16 @@ RED.nodes = (function() {
|
||||
var i;
|
||||
for (i in workspaces) {
|
||||
if (workspaces.hasOwnProperty(i)) {
|
||||
if (workspaces[i].type == "tab") {
|
||||
nns.push(workspaces[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i in subflows) {
|
||||
if (subflows.hasOwnProperty(i)) {
|
||||
nns.push(convertSubflow(subflows[i]));
|
||||
}
|
||||
}
|
||||
for (i in configNodes) {
|
||||
if (configNodes.hasOwnProperty(i)) {
|
||||
nns.push(convertNode(configNodes[i], true));
|
||||
@ -368,7 +497,11 @@ RED.nodes = (function() {
|
||||
for (i=0;i<newNodes.length;i++) {
|
||||
n = newNodes[i];
|
||||
// TODO: remove workspace in next release+1
|
||||
if (n.type != "workspace" && n.type != "tab" && !registry.getNodeType(n.type)) {
|
||||
if (n.type != "workspace" &&
|
||||
n.type != "tab" &&
|
||||
n.type != "subflow" &&
|
||||
!registry.getNodeType(n.type) &&
|
||||
n.type.substring(0,8) != "subflow:") {
|
||||
// TODO: get this UI thing out of here! (see below as well)
|
||||
n.name = n.type;
|
||||
n.type = "unknown";
|
||||
@ -389,9 +522,35 @@ RED.nodes = (function() {
|
||||
//"DO NOT DEPLOY while in this state.<br/>Either, add missing types to Node-RED, restart and then reload page,<br/>or delete unknown "+n.name+", rewire as required, and then deploy.","error");
|
||||
}
|
||||
|
||||
var activeWorkspace = RED.view.getWorkspace();
|
||||
var activeSubflow = getSubflow(activeWorkspace);
|
||||
if (activeSubflow) {
|
||||
for (i=0;i<newNodes.length;i++) {
|
||||
var m = /^subflow:(.+)$/.exec(newNodes[i].type);
|
||||
if (m) {
|
||||
var subflowId = m[1];
|
||||
var err;
|
||||
if (subflowId === activeSubflow.id) {
|
||||
err = new Error("Cannot add subflow to itself");
|
||||
}
|
||||
if (subflowContains(m[1],activeSubflow.id)) {
|
||||
err = new Error("Cannot add subflow - circular reference detected");
|
||||
}
|
||||
if (err) {
|
||||
// TODO: standardise error codes
|
||||
err.code = "NODE_RED";
|
||||
throw err;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var new_workspaces = [];
|
||||
var workspace_map = {};
|
||||
|
||||
var new_subflows = [];
|
||||
var subflow_map = {};
|
||||
var nid;
|
||||
for (i=0;i<newNodes.length;i++) {
|
||||
n = newNodes[i];
|
||||
// TODO: remove workspace in next release+1
|
||||
@ -403,13 +562,34 @@ RED.nodes = (function() {
|
||||
defaultWorkspace = n;
|
||||
}
|
||||
if (createNewIds) {
|
||||
var nid = getID();
|
||||
nid = getID();
|
||||
workspace_map[n.id] = nid;
|
||||
n.id = nid;
|
||||
}
|
||||
addWorkspace(n);
|
||||
RED.view.addWorkspace(n);
|
||||
new_workspaces.push(n);
|
||||
} else if (n.type === "subflow") {
|
||||
subflow_map[n.id] = n;
|
||||
if (createNewIds) {
|
||||
nid = getID();
|
||||
n.id = nid;
|
||||
}
|
||||
// TODO: handle createNewIds - map old to new subflow ids
|
||||
n.in.forEach(function(input,i) {
|
||||
input.type = "subflow";
|
||||
input.direction = "in";
|
||||
input.z = n.id;
|
||||
input.i = i;
|
||||
});
|
||||
n.out.forEach(function(output,i) {
|
||||
output.type = "subflow";
|
||||
output.direction = "out";
|
||||
output.z = n.id;
|
||||
output.i = i;
|
||||
});
|
||||
new_subflows.push(n);
|
||||
addSubflow(n);
|
||||
}
|
||||
}
|
||||
if (defaultWorkspace == null) {
|
||||
@ -426,7 +606,7 @@ RED.nodes = (function() {
|
||||
for (i=0;i<newNodes.length;i++) {
|
||||
n = newNodes[i];
|
||||
// TODO: remove workspace in next release+1
|
||||
if (n.type !== "workspace" && n.type !== "tab") {
|
||||
if (n.type !== "workspace" && n.type !== "tab" && n.type !== "subflow") {
|
||||
var def = registry.getNodeType(n.type);
|
||||
if (def && def.category == "config") {
|
||||
if (!RED.nodes.node(n.id)) {
|
||||
@ -443,19 +623,35 @@ RED.nodes = (function() {
|
||||
} else {
|
||||
var node = {x:n.x,y:n.y,z:n.z,type:0,wires:n.wires,changed:false};
|
||||
if (createNewIds) {
|
||||
if (subflow_map[node.z]) {
|
||||
node.z = subflow_map[node.z].id;
|
||||
} else {
|
||||
node.z = workspace_map[node.z];
|
||||
if (!workspaces[node.z]) {
|
||||
node.z = RED.view.getWorkspace();
|
||||
node.z = activeWorkspace;
|
||||
}
|
||||
}
|
||||
node.id = getID();
|
||||
} else {
|
||||
node.id = n.id;
|
||||
if (node.z == null || !workspaces[node.z]) {
|
||||
node.z = RED.view.getWorkspace();
|
||||
if (node.z == null || (!workspaces[node.z] && !subflow_map[node.z])) {
|
||||
node.z = activeWorkspace;
|
||||
}
|
||||
}
|
||||
node.type = n.type;
|
||||
node._def = def;
|
||||
if (n.type.substring(0,7) === "subflow") {
|
||||
var parentId = n.type.split(":")[1];
|
||||
var subflow = subflow_map[parentId]||getSubflow(parentId);
|
||||
if (createNewIds) {
|
||||
parentId = subflow.id;
|
||||
node.type = "subflow:"+parentId;
|
||||
node._def = registry.getNodeType(node.type);
|
||||
delete node.i;
|
||||
}
|
||||
node.outputs = subflow.out.length;
|
||||
node.inputs = subflow.in.length;
|
||||
} else {
|
||||
if (!node._def) {
|
||||
node._def = {
|
||||
color:"#fee",
|
||||
@ -465,6 +661,7 @@ RED.nodes = (function() {
|
||||
outputs: n.outputs||n.wires.length
|
||||
}
|
||||
}
|
||||
node.inputs = n.inputs||node._def.inputs;
|
||||
node.outputs = n.outputs||node._def.outputs;
|
||||
|
||||
for (var d2 in node._def.defaults) {
|
||||
@ -472,7 +669,7 @@ RED.nodes = (function() {
|
||||
node[d2] = n[d2];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
addNode(node);
|
||||
RED.editor.validateNode(node);
|
||||
node_map[n.id] = node;
|
||||
@ -494,10 +691,38 @@ RED.nodes = (function() {
|
||||
}
|
||||
delete n.wires;
|
||||
}
|
||||
return [new_nodes,new_links,new_workspaces];
|
||||
for (i=0;i<new_subflows.length;i++) {
|
||||
n = new_subflows[i];
|
||||
n.in.forEach(function(input) {
|
||||
input.wires.forEach(function(wire) {
|
||||
var link = {source:input, sourcePort:0, target:node_map[wire.id]};
|
||||
addLink(link);
|
||||
new_links.push(link);
|
||||
});
|
||||
delete input.wires;
|
||||
});
|
||||
n.out.forEach(function(output) {
|
||||
output.wires.forEach(function(wire) {
|
||||
var link;
|
||||
if (wire.id == n.id) {
|
||||
link = {source:n.in[wire.port], sourcePort:wire.port,target:output};
|
||||
} else {
|
||||
link = {source:node_map[wire.id], sourcePort:wire.port,target:output};
|
||||
}
|
||||
addLink(link);
|
||||
new_links.push(link);
|
||||
});
|
||||
delete output.wires;
|
||||
});
|
||||
}
|
||||
return [new_nodes,new_links,new_workspaces,new_subflows];
|
||||
} catch(error) {
|
||||
//TODO: get this UI thing out of here! (see above as well)
|
||||
if (error.code != "NODE_RED") {
|
||||
console.log(error.stack);
|
||||
RED.notify("<strong>Error</strong>: "+error,"error");
|
||||
} else {
|
||||
RED.notify("<strong>Error</strong>: "+error.message,"error");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -516,13 +741,22 @@ RED.nodes = (function() {
|
||||
registerType: registry.registerNodeType,
|
||||
getType: registry.getNodeType,
|
||||
convertNode: convertNode,
|
||||
|
||||
add: addNode,
|
||||
addLink: addLink,
|
||||
remove: removeNode,
|
||||
|
||||
addLink: addLink,
|
||||
removeLink: removeLink,
|
||||
|
||||
addWorkspace: addWorkspace,
|
||||
removeWorkspace: removeWorkspace,
|
||||
workspace: getWorkspace,
|
||||
|
||||
addSubflow: addSubflow,
|
||||
removeSubflow: removeSubflow,
|
||||
subflow: getSubflow,
|
||||
subflowContains: subflowContains,
|
||||
|
||||
eachNode: function(cb) {
|
||||
for (var n=0;n<nodes.length;n++) {
|
||||
cb(nodes[n]);
|
||||
@ -540,6 +774,13 @@ RED.nodes = (function() {
|
||||
}
|
||||
}
|
||||
},
|
||||
eachSubflow: function(cb) {
|
||||
for (var id in subflows) {
|
||||
if (subflows.hasOwnProperty(id)) {
|
||||
cb(subflows[id]);
|
||||
}
|
||||
}
|
||||
},
|
||||
node: getNode,
|
||||
import: importNodes,
|
||||
refreshValidation: refreshValidation,
|
||||
|
@ -95,6 +95,7 @@ RED.editor = (function() {
|
||||
node.resize = true;
|
||||
node.dirty = true;
|
||||
var removedLinks = [];
|
||||
if (node.ports) {
|
||||
if (node.outputs < node.ports.length) {
|
||||
while (node.outputs < node.ports.length) {
|
||||
node.ports.pop();
|
||||
@ -104,14 +105,22 @@ RED.editor = (function() {
|
||||
removedLinks.push(l);
|
||||
}
|
||||
});
|
||||
for (var l=0;l<removedLinks.length;l++) {
|
||||
RED.nodes.removeLink(removedLinks[l]);
|
||||
}
|
||||
} else if (node.outputs > node.ports.length) {
|
||||
while (node.outputs > node.ports.length) {
|
||||
node.ports.push(node.ports.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node.inputs === 0) {
|
||||
RED.nodes.eachLink(function(l) {
|
||||
if (l.target === node) {
|
||||
removedLinks.push(l);
|
||||
}
|
||||
});
|
||||
}
|
||||
for (var l=0;l<removedLinks.length;l++) {
|
||||
RED.nodes.removeLink(removedLinks[l]);
|
||||
}
|
||||
return removedLinks;
|
||||
}
|
||||
|
||||
@ -274,6 +283,11 @@ RED.editor = (function() {
|
||||
RED.sidebar.info.refresh(editing_node);
|
||||
}
|
||||
RED.sidebar.config.refresh();
|
||||
|
||||
var buttons = $( this ).dialog("option","buttons");
|
||||
if (buttons.length == 3) {
|
||||
$( this ).dialog("option","buttons",buttons.splice(1));
|
||||
}
|
||||
editing_node = null;
|
||||
}
|
||||
});
|
||||
@ -459,10 +473,30 @@ RED.editor = (function() {
|
||||
function showEditDialog(node) {
|
||||
editing_node = node;
|
||||
RED.view.state(RED.state.EDITING);
|
||||
$("#dialog-form").html($("script[data-template-name='"+node.type+"']").html());
|
||||
var type = node.type;
|
||||
if (node.type.substring(0,8) == "subflow:") {
|
||||
type = "subflow";
|
||||
var id = editing_node.type.substring(8);
|
||||
var buttons = $( "#dialog" ).dialog("option","buttons");
|
||||
buttons.unshift({
|
||||
class: 'leftButton',
|
||||
text: "Edit flow",
|
||||
click: function() {
|
||||
RED.view.showSubflow(id);
|
||||
$("#node-dialog-ok").click();
|
||||
}
|
||||
});
|
||||
$( "#dialog" ).dialog("option","buttons",buttons);
|
||||
}
|
||||
$("#dialog-form").html($("script[data-template-name='"+type+"']").html());
|
||||
$('<input type="text" style="display: none;" />').appendTo("#dialog-form");
|
||||
prepareEditDialog(node,node._def,"node-input");
|
||||
$( "#dialog" ).dialog("option","title","Edit "+node.type+" node").dialog( "open" );
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
$( "#dialog" ).dialog("option","title","Edit "+type+" node").dialog( "open" );
|
||||
}
|
||||
|
||||
function showEditConfigNodeDialog(name,type,id) {
|
||||
@ -655,10 +689,207 @@ RED.editor = (function() {
|
||||
}
|
||||
});
|
||||
|
||||
$( "#subflow-dialog" ).dialog({
|
||||
modal: true,
|
||||
autoOpen: false,
|
||||
closeOnEscape: false,
|
||||
width: 500,
|
||||
buttons: [
|
||||
{
|
||||
class: 'leftButton',
|
||||
text: "Delete",
|
||||
click: function() {
|
||||
var removedNodes = [];
|
||||
var removedLinks = [];
|
||||
var startDirty = RED.view.dirty();
|
||||
|
||||
RED.nodes.eachNode(function(n) {
|
||||
if (n.type == "subflow:"+editing_node.id) {
|
||||
removedNodes.push(n);
|
||||
}
|
||||
if (n.z == editing_node.id) {
|
||||
removedNodes.push(n);
|
||||
}
|
||||
});
|
||||
|
||||
for (var i=0;i<removedNodes.length;i++) {
|
||||
var rmlinks = RED.nodes.remove(removedNodes[i].id);
|
||||
removedLinks = removedLinks.concat(rmlinks);
|
||||
}
|
||||
|
||||
RED.nodes.removeSubflow(editing_node);
|
||||
|
||||
RED.view.removeWorkspace(editing_node);
|
||||
|
||||
RED.history.push({
|
||||
t:'delete',
|
||||
nodes:removedNodes,
|
||||
links:removedLinks,
|
||||
subflow: editing_node,
|
||||
dirty:startDirty
|
||||
});
|
||||
RED.view.dirty(true);
|
||||
RED.view.redraw();
|
||||
|
||||
$( this ).dialog( "close" );
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "subflow-dialog-ok",
|
||||
text: "Ok",
|
||||
click: function() {
|
||||
if (editing_node) {
|
||||
var i;
|
||||
var changes = {};
|
||||
var changed = false;
|
||||
var wasDirty = RED.view.dirty();
|
||||
|
||||
var newName = $("#subflow-input-name").val();
|
||||
var newInCount = $("#subflow-input-inCount").val();
|
||||
var newOutCount = $("#subflow-input-outCount").val();
|
||||
|
||||
var oldInCount = editing_node.in.length;
|
||||
var oldOutCount = editing_node.out.length;
|
||||
|
||||
if (newName != editing_node.name) {
|
||||
changes['name'] = editing_node.name;
|
||||
editing_node.name = newName;
|
||||
changed = true;
|
||||
$("#btn-workspace-menu-"+editing_node.id.replace(".","-")).text("Subflow: "+newName);
|
||||
}
|
||||
|
||||
var xpos = 40;
|
||||
var addedOutputs = [];
|
||||
var removedOutputs = [];
|
||||
var addedInputs = [];
|
||||
var removedInputs = [];
|
||||
var removedLinks = [];
|
||||
|
||||
if (editing_node.in.length < newInCount) {
|
||||
var l = editing_node.in.length;
|
||||
for (i=l;i<newInCount;i++) {
|
||||
var newInput = {type:"subflow",direction:"in",z:editing_node.id,i:i,x:xpos,y:70};
|
||||
addedInputs.push(newInput);
|
||||
editing_node.in.push(newInput);
|
||||
xpos += 55;
|
||||
}
|
||||
changed = true;
|
||||
} else if (editing_node.in.length > newInCount) {
|
||||
removedInputs = editing_node.in.splice(newInCount);
|
||||
changed = true;
|
||||
}
|
||||
if (editing_node.out.length < newOutCount) {
|
||||
for (i=editing_node.out.length;i<newOutCount;i++) {
|
||||
var newOutput = {type:"subflow",direction:"out",z:editing_node.id,i:i,x:xpos,y:70};
|
||||
addedOutputs.push(newOutput);
|
||||
editing_node.out.push(newOutput);
|
||||
xpos += 55;
|
||||
}
|
||||
changed = true;
|
||||
} else if (editing_node.out.length > newOutCount) {
|
||||
removedOutputs = editing_node.out.splice(newOutCount);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (removedOutputs.length > 0 || removedInputs.length > 0) {
|
||||
RED.nodes.eachLink(function(l) {
|
||||
if (newInCount === 0 && l.source.type == "subflow" && l.source.z == editing_node.id) {
|
||||
removedLinks.push(l);
|
||||
return;
|
||||
}
|
||||
if (l.target.type == "subflow" && l.target.z == editing_node.id && l.target.i >= newOutCount) {
|
||||
removedLinks.push(l);
|
||||
return;
|
||||
}
|
||||
});
|
||||
removedLinks.forEach(function(l) { RED.nodes.removeLink(l)});
|
||||
}
|
||||
RED.palette.refresh();
|
||||
|
||||
if (changed) {
|
||||
RED.nodes.eachNode(function(n) {
|
||||
if (n.type == "subflow:"+editing_node.id) {
|
||||
n.changed = true;
|
||||
n.inputs = editing_node.in.length;
|
||||
n.outputs = editing_node.out.length;
|
||||
removedLinks = removedLinks.concat(updateNodeProperties(n));
|
||||
}
|
||||
});
|
||||
var wasChanged = editing_node.changed;
|
||||
editing_node.changed = true;
|
||||
RED.view.dirty(true);
|
||||
var historyEvent = {
|
||||
t:'edit',
|
||||
node:editing_node,
|
||||
changes:changes,
|
||||
links:removedLinks,
|
||||
dirty:wasDirty,
|
||||
changed:wasChanged,
|
||||
subflow: {
|
||||
outputCount: oldOutCount,
|
||||
inputCount: oldInCount,
|
||||
outputs: removedOutputs,
|
||||
inputs: removedInputs
|
||||
}
|
||||
};
|
||||
|
||||
RED.history.push(historyEvent);
|
||||
}
|
||||
editing_node.dirty = true;
|
||||
RED.view.redraw();
|
||||
}
|
||||
$( this ).dialog( "close" );
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "subflow-dialog-cancel",
|
||||
text: "Cancel",
|
||||
click: function() {
|
||||
$( this ).dialog( "close" );
|
||||
editing_node = null;
|
||||
}
|
||||
}
|
||||
],
|
||||
open: function(e) {
|
||||
RED.keyboard.disable();
|
||||
},
|
||||
close: function(e) {
|
||||
RED.keyboard.enable();
|
||||
|
||||
if (RED.view.state() != RED.state.IMPORT_DRAGGING) {
|
||||
RED.view.state(RED.state.DEFAULT);
|
||||
}
|
||||
RED.sidebar.info.refresh(editing_node);
|
||||
editing_node = null;
|
||||
}
|
||||
});
|
||||
|
||||
function showEditSubflowDialog(subflow) {
|
||||
editing_node = subflow;
|
||||
RED.view.state(RED.state.EDITING);
|
||||
$("#subflow-input-name").val(subflow.name);
|
||||
$("#subflow-input-inCount").spinner({ min:0, max:1 }).val(subflow.in.length);
|
||||
$("#subflow-input-outCount").spinner({ min:0 }).val(subflow.out.length);
|
||||
var userCount = 0;
|
||||
var subflowType = "subflow:"+editing_node.id;
|
||||
|
||||
RED.nodes.eachNode(function(n) {
|
||||
if (n.type === subflowType) {
|
||||
userCount++;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$("#subflow-dialog-user-count").html("There "+(userCount==1?"is":"are")+" "+userCount+" instance"+(userCount==1?" ":"s")+" of this subflow").show();
|
||||
$("#subflow-dialog").dialog("option","title","Edit flow "+subflow.name).dialog( "open" );
|
||||
}
|
||||
|
||||
|
||||
|
||||
return {
|
||||
edit: showEditDialog,
|
||||
editConfig: showEditConfigNodeDialog,
|
||||
editSubflow: showEditSubflowDialog,
|
||||
validateNode: validateNode,
|
||||
updateNodeProperties: updateNodeProperties // TODO: only exposed for edit-undo
|
||||
}
|
||||
|
@ -110,13 +110,28 @@ RED.menu = (function() {
|
||||
$("#"+id).parent().remove();
|
||||
}
|
||||
|
||||
function setAction(id,action) {
|
||||
menuItems[id].onselect = action;
|
||||
$("#"+id).click(function() {
|
||||
if ($(this).parent().hasClass("disabled")) {
|
||||
return;
|
||||
}
|
||||
if (menuItems[id].toggle) {
|
||||
setSelected(id,!isSelected(id));
|
||||
} else {
|
||||
menuItems[id].onselect.call(menuItems[id]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
init: createMenu,
|
||||
setSelected: setSelected,
|
||||
isSelected: isSelected,
|
||||
setDisabled: setDisabled,
|
||||
addItem: addItem,
|
||||
removeItem: removeItem
|
||||
removeItem: removeItem,
|
||||
setAction: setAction
|
||||
//TODO: add an api for replacing a submenu - see library.js:loadFlowLibrary
|
||||
}
|
||||
})();
|
||||
|
@ -17,7 +17,7 @@
|
||||
RED.palette = (function() {
|
||||
|
||||
var exclusion = ['config','unknown','deprecated'];
|
||||
var core = ['input', 'output', 'function', 'social', 'storage', 'analysis', 'advanced'];
|
||||
var core = ['input', 'output', 'function', 'subflows', 'social', 'storage', 'analysis', 'advanced'];
|
||||
|
||||
function createCategoryContainer(category){
|
||||
var escapedCategory = category.replace(" ","_");
|
||||
@ -38,10 +38,66 @@ RED.palette = (function() {
|
||||
|
||||
core.forEach(createCategoryContainer);
|
||||
|
||||
function setLabel(type, el,label) {
|
||||
var nodeWidth = 80;
|
||||
var nodeHeight = 25;
|
||||
var lineHeight = 20;
|
||||
var portHeight = 10;
|
||||
|
||||
var words = label.split(" ");
|
||||
|
||||
var displayLines = [];
|
||||
|
||||
var currentLine = words[0];
|
||||
var currentLineWidth = RED.view.calculateTextWidth(currentLine, "palette_label", 0);
|
||||
|
||||
for (var i=1;i<words.length;i++) {
|
||||
var newWidth = RED.view.calculateTextWidth(currentLine+" "+words[i], "palette_label", 0);
|
||||
if (newWidth < nodeWidth) {
|
||||
currentLine += " "+words[i];
|
||||
currentLineWidth = newWidth;
|
||||
} else {
|
||||
displayLines.push(currentLine);
|
||||
currentLine = words[i];
|
||||
currentLineWidth = RED.view.calculateTextWidth(currentLine, "palette_label", 0);
|
||||
}
|
||||
}
|
||||
displayLines.push(currentLine);
|
||||
|
||||
var lines = displayLines.join("<br/>");
|
||||
var multiLineNodeHeight = 8+(lineHeight*displayLines.length);
|
||||
el.css({height:multiLineNodeHeight+"px"});
|
||||
|
||||
var labelElement = el.find(".palette_label");
|
||||
labelElement.html(lines);
|
||||
|
||||
el.find(".palette_port").css({top:(multiLineNodeHeight/2-5)+"px"});
|
||||
|
||||
var popOverContent;
|
||||
try {
|
||||
var l = "<p><b>"+label+"</b></p>";
|
||||
if (label != type) {
|
||||
l = "<p><b>"+label+"</b><br/><i>"+type+"</i></p>";
|
||||
}
|
||||
popOverContent = $(l+($("script[data-help-name|='"+type+"']").html()||"<p>no information available</p>").trim()).slice(0,2);
|
||||
} catch(err) {
|
||||
// Malformed HTML may cause errors. TODO: need to understand what can break
|
||||
console.log("Error generating pop-over label for '"+type+"'.");
|
||||
console.log(err.toString());
|
||||
popOverContent = "<p><b>"+label+"</b></p><p>no information available</p>";
|
||||
}
|
||||
|
||||
|
||||
el.data('popover').options.content = popOverContent;
|
||||
}
|
||||
|
||||
function escapeNodeType(nt) {
|
||||
return nt.replace(" ","_").replace(".","_").replace(":","_");
|
||||
}
|
||||
|
||||
function addNodeType(nt,def) {
|
||||
|
||||
var nodeTypeId = nt.replace(" ","_");
|
||||
|
||||
var nodeTypeId = escapeNodeType(nt);
|
||||
if ($("#palette_node_"+nodeTypeId).length) {
|
||||
return;
|
||||
}
|
||||
@ -63,27 +119,12 @@ RED.palette = (function() {
|
||||
label = (typeof def.paletteLabel === "function" ? def.paletteLabel.call(def) : def.paletteLabel)||"";
|
||||
}
|
||||
|
||||
var pixels = RED.view.calculateTextWidth(label, "palette_label", 0);
|
||||
var nodeWidth = 90;
|
||||
var numLines = Math.ceil(pixels / nodeWidth);
|
||||
var multiLine = numLines > 1;
|
||||
d.innerHTML = '<div class="palette_label"></div>';
|
||||
|
||||
// styles matching with style.css
|
||||
var nodeHeight = 25;
|
||||
var lineHeight = 16;
|
||||
var portHeight = 10;
|
||||
var multiLineNodeHeight = lineHeight * numLines + (nodeHeight - lineHeight);
|
||||
|
||||
d.innerHTML = '<div class="palette_label"'+
|
||||
(multiLine ? 'style="line-height: '+
|
||||
lineHeight + 'px; margin-top: 5px"' : '')+
|
||||
'>'+label+"</div>";
|
||||
d.className="palette_node";
|
||||
if (def.icon) {
|
||||
d.style.backgroundImage = "url(icons/"+def.icon+")";
|
||||
if (multiLine) {
|
||||
d.style.backgroundSize = "18px 27px";
|
||||
}
|
||||
if (def.align == "right") {
|
||||
d.style.backgroundPosition = "95% 50%";
|
||||
} else if (def.inputs > 0) {
|
||||
@ -92,23 +133,16 @@ RED.palette = (function() {
|
||||
}
|
||||
|
||||
d.style.backgroundColor = def.color;
|
||||
d.style.height = multiLineNodeHeight + "px";
|
||||
|
||||
if (def.outputs > 0) {
|
||||
var portOut = document.createElement("div");
|
||||
portOut.className = "palette_port palette_port_output";
|
||||
if (multiLine) {
|
||||
portOut.style.top = ((multiLineNodeHeight - portHeight) / 2) + "px";
|
||||
}
|
||||
d.appendChild(portOut);
|
||||
}
|
||||
|
||||
if (def.inputs > 0) {
|
||||
var portIn = document.createElement("div");
|
||||
portIn.className = "palette_port";
|
||||
if (multiLine) {
|
||||
portIn.style.top = ((multiLineNodeHeight - portHeight) / 2) + "px";
|
||||
}
|
||||
portIn.className = "palette_port palette_port_input";
|
||||
d.appendChild(portIn);
|
||||
}
|
||||
|
||||
@ -123,23 +157,13 @@ RED.palette = (function() {
|
||||
$("#palette-"+category).append(d);
|
||||
d.onmousedown = function(e) { e.preventDefault(); };
|
||||
|
||||
var popOverContent;
|
||||
try {
|
||||
popOverContent = $("<p><b>"+label+"</b></p>"+($("script[data-help-name|='"+nt+"']").html().trim()||"<p>no information available</p>")).slice(0,2);
|
||||
} catch(err) {
|
||||
// Malformed HTML may cause errors. TODO: need to understand what can break
|
||||
console.log("Error generating pop-over label for '"+nt+"'.");
|
||||
console.log(err.toString());
|
||||
popOverContent = "<p><b>"+label+"</b></p><p>no information available</p>";
|
||||
}
|
||||
$(d).popover({
|
||||
title:d.type,
|
||||
placement:"right",
|
||||
trigger: "hover",
|
||||
delay: { show: 750, hide: 50 },
|
||||
html: true,
|
||||
container:'body',
|
||||
content: popOverContent
|
||||
container:'body'
|
||||
});
|
||||
$(d).click(function() {
|
||||
var help = '<div class="node-help">'+($("script[data-help-name|='"+d.type+"']").html()||"")+"</div>";
|
||||
@ -151,23 +175,50 @@ RED.palette = (function() {
|
||||
revert: true,
|
||||
revertDuration: 50
|
||||
});
|
||||
|
||||
setLabel(nt,$(d),label);
|
||||
}
|
||||
}
|
||||
|
||||
function removeNodeType(nt) {
|
||||
var nodeTypeId = nt.replace(" ","_");
|
||||
var nodeTypeId = escapeNodeType(nt);
|
||||
$("#palette_node_"+nodeTypeId).remove();
|
||||
}
|
||||
function hideNodeType(nt) {
|
||||
var nodeTypeId = nt.replace(" ","_");
|
||||
var nodeTypeId = escapeNodeType(nt);
|
||||
$("#palette_node_"+nodeTypeId).hide();
|
||||
}
|
||||
|
||||
function showNodeType(nt) {
|
||||
var nodeTypeId = nt.replace(" ","_");
|
||||
var nodeTypeId = escapeNodeType(nt);
|
||||
$("#palette_node_"+nodeTypeId).show();
|
||||
}
|
||||
|
||||
function refreshNodeTypes() {
|
||||
RED.nodes.eachSubflow(function(sf) {
|
||||
var paletteNode = $("#palette_node_subflow_"+sf.id.replace(".","_"));
|
||||
var portInput = paletteNode.find(".palette_port_input");
|
||||
var portOutput = paletteNode.find(".palette_port_output");
|
||||
|
||||
if (portInput.length === 0 && sf.in.length > 0) {
|
||||
var portIn = document.createElement("div");
|
||||
portIn.className = "palette_port palette_port_input";
|
||||
paletteNode.append(portIn);
|
||||
} else if (portInput.length !== 0 && sf.in.length === 0) {
|
||||
portInput.remove();
|
||||
}
|
||||
|
||||
if (portOutput === 0 && sf.out.length > 0) {
|
||||
var portOut = document.createElement("div");
|
||||
portOut.className = "palette_port palette_port_output";
|
||||
paletteNode.append(portOut);
|
||||
} else if (portOutput !== 0 && sf.out.length === 0) {
|
||||
portOutput.remove();
|
||||
}
|
||||
setLabel(sf.type+":"+sf.id,paletteNode,sf.name);
|
||||
});
|
||||
}
|
||||
|
||||
function filterChange() {
|
||||
var val = $("#palette-search-input").val();
|
||||
if (val === "") {
|
||||
@ -215,6 +266,7 @@ RED.palette = (function() {
|
||||
add:addNodeType,
|
||||
remove:removeNodeType,
|
||||
hide:hideNodeType,
|
||||
show:showNodeType
|
||||
show:showNodeType,
|
||||
refresh:refreshNodeTypes
|
||||
};
|
||||
})();
|
||||
|
@ -47,6 +47,20 @@ RED.sidebar.info = (function() {
|
||||
table += "<tr><td>Type</td><td> "+node.type+"</td></tr>";
|
||||
table += "<tr><td>ID</td><td> "+node.id+"</td></tr>";
|
||||
table += '<tr class="blank"><td colspan="2">Properties</td></tr>';
|
||||
if (node.type == "subflow") {
|
||||
var userCount = 0;
|
||||
var subflowType = "subflow:"+node.id;
|
||||
RED.nodes.eachNode(function(n) {
|
||||
if (n.type === subflowType) {
|
||||
userCount++;
|
||||
}
|
||||
});
|
||||
table += "<tr><td>name</td><td>"+node.name+"</td></tr>";
|
||||
table += "<tr><td>inputs</td><td>"+node.in.length+"</td></tr>";
|
||||
table += "<tr><td>outputs</td><td>"+node.out.length+"</td></tr>";
|
||||
table += "<tr><td>instances</td><td>"+userCount+"</td></tr>";
|
||||
}
|
||||
if (node._def) {
|
||||
for (var n in node._def.defaults) {
|
||||
if (node._def.defaults.hasOwnProperty(n)) {
|
||||
var val = node[n]||"";
|
||||
@ -73,11 +87,13 @@ RED.sidebar.info = (function() {
|
||||
val = val.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
||||
}
|
||||
|
||||
table += "<tr><td> "+n+"</td><td>"+val+"</td></tr>";
|
||||
table += "<tr><td>"+n+"</td><td>"+val+"</td></tr>";
|
||||
}
|
||||
}
|
||||
}
|
||||
table += "</tbody></table><br/>";
|
||||
table += '<div class="node-help">'+($("script[data-help-name|='"+node.type+"']").html()||"")+"</div>";
|
||||
var helpText = $("script[data-help-name|='"+node.type+"']").html()||"";
|
||||
table += '<div class="node-help">'+helpText+"</div>";
|
||||
$("#tab-info").html(table);
|
||||
}
|
||||
|
||||
|
@ -116,6 +116,13 @@ RED.tabs = (function() {
|
||||
},
|
||||
contains: function(id) {
|
||||
return ul.find("a[href='#"+id+"']").length > 0;
|
||||
},
|
||||
renameTab: function(id,name) {
|
||||
tabs[id].name = name;
|
||||
var tab = ul.find("a[href='#"+id+"']");
|
||||
tab.attr("title",name);
|
||||
tab.text(name);
|
||||
updateTabWidths();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -31,6 +31,8 @@ RED.view = (function() {
|
||||
|
||||
|
||||
var activeWorkspace = 0;
|
||||
var activeSubflow = null;
|
||||
|
||||
var workspaceScrollPositions = {};
|
||||
|
||||
var selected_link = null,
|
||||
@ -225,6 +227,11 @@ RED.view = (function() {
|
||||
|
||||
var drag_line = vis.append("svg:path").attr("class", "drag_line");
|
||||
|
||||
$("#workspace-edit-subflow").click(function(event) {
|
||||
showSubflowDialog(activeSubflow.id);
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
var workspace_tabs = RED.tabs.create({
|
||||
id: "workspace-tabs",
|
||||
onchange: function(tab) {
|
||||
@ -244,6 +251,8 @@ RED.view = (function() {
|
||||
var scrollStartTop = chart.scrollTop();
|
||||
|
||||
activeWorkspace = tab.id;
|
||||
activeSubflow = RED.nodes.subflow(activeWorkspace);
|
||||
|
||||
if (workspaceScrollPositions[activeWorkspace]) {
|
||||
chart.scrollLeft(workspaceScrollPositions[activeWorkspace].left);
|
||||
chart.scrollTop(workspaceScrollPositions[activeWorkspace].top);
|
||||
@ -258,6 +267,9 @@ RED.view = (function() {
|
||||
mouse_position[1] += scrollDeltaTop;
|
||||
}
|
||||
|
||||
RED.menu.setDisabled("btn-workspace-edit", activeSubflow);
|
||||
RED.menu.setDisabled("btn-workspace-delete",workspace_tabs.count() == 1 || activeSubflow);
|
||||
|
||||
clearSelection();
|
||||
RED.nodes.eachNode(function(n) {
|
||||
n.dirty = true;
|
||||
@ -265,7 +277,11 @@ RED.view = (function() {
|
||||
redraw();
|
||||
},
|
||||
ondblclick: function(tab) {
|
||||
if (tab.type != "subflow") {
|
||||
showRenameWorkspaceDialog(tab.id);
|
||||
} else {
|
||||
showSubflowDialog(tab.id);
|
||||
}
|
||||
},
|
||||
onadd: function(tab) {
|
||||
RED.menu.addItem("btn-workspace-menu",{
|
||||
@ -300,11 +316,12 @@ RED.view = (function() {
|
||||
}
|
||||
$(function() {
|
||||
$('#btn-workspace-add-tab').on("click",addWorkspace);
|
||||
$('#btn-workspace-add').on("click",addWorkspace);
|
||||
$('#btn-workspace-edit').on("click",function() {
|
||||
|
||||
RED.menu.setAction('btn-workspace-add',addWorkspace);
|
||||
RED.menu.setAction('btn-workspace-edit',function() {
|
||||
showRenameWorkspaceDialog(activeWorkspace);
|
||||
});
|
||||
$('#btn-workspace-delete').on("click",function() {
|
||||
RED.menu.setAction('btn-workspace-delete',function() {
|
||||
deleteWorkspace(activeWorkspace);
|
||||
});
|
||||
});
|
||||
@ -349,7 +366,6 @@ RED.view = (function() {
|
||||
|
||||
function canvasMouseMove() {
|
||||
mouse_position = d3.touches(this)[0]||d3.mouse(this);
|
||||
|
||||
// Prevent touch scrolling...
|
||||
//if (d3.touches(this)[0]) {
|
||||
// d3.event.preventDefault();
|
||||
@ -478,8 +494,10 @@ RED.view = (function() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mouse_mode !== 0) {
|
||||
redraw();
|
||||
}
|
||||
}
|
||||
|
||||
function canvasMouseUp() {
|
||||
if (mousedown_node && mouse_mode == RED.state.JOINING) {
|
||||
@ -502,6 +520,22 @@ RED.view = (function() {
|
||||
}
|
||||
}
|
||||
});
|
||||
if (activeSubflow) {
|
||||
activeSubflow.in.forEach(function(n) {
|
||||
n.selected = (n.x > x && n.x < x2 && n.y > y && n.y < y2);
|
||||
if (n.selected) {
|
||||
n.dirty = true;
|
||||
moving_set.push({n:n});
|
||||
}
|
||||
});
|
||||
activeSubflow.out.forEach(function(n) {
|
||||
n.selected = (n.x > x && n.x < x2 && n.y > y && n.y < y2);
|
||||
if (n.selected) {
|
||||
n.dirty = true;
|
||||
moving_set.push({n:n});
|
||||
}
|
||||
});
|
||||
}
|
||||
updateSelection();
|
||||
lasso.remove();
|
||||
lasso = null;
|
||||
@ -545,11 +579,28 @@ RED.view = (function() {
|
||||
else { zoomIn(); }
|
||||
}
|
||||
});
|
||||
|
||||
$("#chart").droppable({
|
||||
accept:".palette_node",
|
||||
drop: function( event, ui ) {
|
||||
d3.event = event;
|
||||
var selected_tool = ui.draggable[0].type;
|
||||
|
||||
var m = /^subflow:(.+)$/.exec(selected_tool);
|
||||
|
||||
if (activeSubflow && m) {
|
||||
var subflowId = m[1];
|
||||
if (subflowId === activeSubflow.id) {
|
||||
RED.notify("<strong>Error</strong>: Cannot add subflow to itself","error");
|
||||
return;
|
||||
}
|
||||
if (RED.nodes.subflowContains(m[1],activeSubflow.id)) {
|
||||
RED.notify("<strong>Error</strong>: Cannot add subflow - circular reference detected","error");
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var mousePos = d3.touches(this)[0]||d3.mouse(this);
|
||||
mousePos[1] += this.scrollTop;
|
||||
mousePos[0] += this.scrollLeft;
|
||||
@ -560,6 +611,9 @@ RED.view = (function() {
|
||||
|
||||
nn.type = selected_tool;
|
||||
nn._def = RED.nodes.getType(nn.type);
|
||||
|
||||
if (!m) {
|
||||
nn.inputs = nn._def.inputs || 0;
|
||||
nn.outputs = nn._def.outputs;
|
||||
nn.changed = true;
|
||||
|
||||
@ -572,6 +626,11 @@ RED.view = (function() {
|
||||
if (nn._def.onadd) {
|
||||
nn._def.onadd.call(nn);
|
||||
}
|
||||
} else {
|
||||
var subflow = RED.nodes.subflow(m[1]);
|
||||
nn.inputs = subflow.in.length;
|
||||
nn.outputs = subflow.out.length;
|
||||
}
|
||||
|
||||
nn.h = Math.max(node_height,(nn.outputs||0) * 15);
|
||||
RED.history.push({t:'add',nodes:[nn.id],dirty:dirty});
|
||||
@ -618,6 +677,23 @@ RED.view = (function() {
|
||||
}
|
||||
}
|
||||
});
|
||||
if (activeSubflow) {
|
||||
activeSubflow.in.forEach(function(n) {
|
||||
if (!n.selected) {
|
||||
n.selected = true;
|
||||
n.dirty = true;
|
||||
moving_set.push({n:n});
|
||||
}
|
||||
});
|
||||
activeSubflow.out.forEach(function(n) {
|
||||
if (!n.selected) {
|
||||
n.selected = true;
|
||||
n.dirty = true;
|
||||
moving_set.push({n:n});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selected_link = null;
|
||||
updateSelection();
|
||||
redraw();
|
||||
@ -638,10 +714,12 @@ RED.view = (function() {
|
||||
RED.menu.setDisabled("btn-export-menu",true);
|
||||
RED.menu.setDisabled("btn-export-clipboard",true);
|
||||
RED.menu.setDisabled("btn-export-library",true);
|
||||
RED.menu.setDisabled("btn-convert-subflow",true);
|
||||
} else {
|
||||
RED.menu.setDisabled("btn-export-menu",false);
|
||||
RED.menu.setDisabled("btn-export-clipboard",false);
|
||||
RED.menu.setDisabled("btn-export-library",false);
|
||||
RED.menu.setDisabled("btn-convert-subflow",false);
|
||||
}
|
||||
if (moving_set.length === 0 && selected_link == null) {
|
||||
RED.keyboard.remove(/* backspace */ 8);
|
||||
@ -666,7 +744,13 @@ RED.view = (function() {
|
||||
RED.keyboard.add(/* right*/ 39, function() { if(d3.event.shiftKey){moveSelection( 20, 0)}else{moveSelection( 1, 0);}d3.event.preventDefault();},endKeyboardMove);
|
||||
}
|
||||
if (moving_set.length == 1) {
|
||||
if (moving_set[0].n.type === "subflow" && !moving_set[0].n.id) {
|
||||
RED.sidebar.info.refresh(RED.nodes.subflow(moving_set[0].n.z));
|
||||
} else {
|
||||
RED.sidebar.info.refresh(moving_set[0].n);
|
||||
}
|
||||
} else if (moving_set.length === 0 && activeSubflow) {
|
||||
RED.sidebar.info.refresh(activeSubflow);
|
||||
} else {
|
||||
RED.sidebar.info.clear();
|
||||
}
|
||||
@ -716,16 +800,22 @@ RED.view = (function() {
|
||||
for (var i=0;i<moving_set.length;i++) {
|
||||
var node = moving_set[i].n;
|
||||
node.selected = false;
|
||||
if (node.type != "subflow") {
|
||||
if (node.x < 0) {
|
||||
node.x = 25
|
||||
}
|
||||
var rmlinks = RED.nodes.remove(node.id);
|
||||
removedNodes.push(node);
|
||||
removedLinks = removedLinks.concat(rmlinks);
|
||||
} else {
|
||||
node.dirty = true;
|
||||
}
|
||||
}
|
||||
moving_set = [];
|
||||
if (removedNodes.length > 0) {
|
||||
setDirty(true);
|
||||
}
|
||||
}
|
||||
if (selected_link) {
|
||||
RED.nodes.removeLink(selected_link);
|
||||
removedLinks.push(selected_link);
|
||||
@ -743,10 +833,12 @@ RED.view = (function() {
|
||||
var nns = [];
|
||||
for (var n=0;n<moving_set.length;n++) {
|
||||
var node = moving_set[n].n;
|
||||
if (node.type != "subflow") {
|
||||
nns.push(RED.nodes.convertNode(node));
|
||||
}
|
||||
}
|
||||
clipboard = JSON.stringify(nns);
|
||||
RED.notify(moving_set.length+" node"+(moving_set.length>1?"s":"")+" copied");
|
||||
RED.notify(nns.length+" node"+(nns.length>1?"s":"")+" copied");
|
||||
}
|
||||
}
|
||||
|
||||
@ -772,6 +864,7 @@ RED.view = (function() {
|
||||
}
|
||||
|
||||
function portMouseDown(d,portType,portIndex) {
|
||||
//console.log(d,portType,portIndex);
|
||||
// disable zoom
|
||||
//vis.call(d3.behavior.zoom().on("zoom"), null);
|
||||
mousedown_node = d;
|
||||
@ -794,7 +887,7 @@ RED.view = (function() {
|
||||
if (n.x-hw<mouse_position[0] && n.x+hw> mouse_position[0] &&
|
||||
n.y-hh<mouse_position[1] && n.y+hh>mouse_position[1]) {
|
||||
mouseup_node = n;
|
||||
portType = mouseup_node._def.inputs>0?1:0;
|
||||
portType = mouseup_node.inputs>0?1:0;
|
||||
portIndex = 0;
|
||||
}
|
||||
}
|
||||
@ -817,7 +910,6 @@ RED.view = (function() {
|
||||
dst = mousedown_node;
|
||||
src_port = portIndex;
|
||||
}
|
||||
|
||||
var existingLink = false;
|
||||
RED.nodes.eachLink(function(d) {
|
||||
existingLink = existingLink || (d.source === src && d.target === dst && d.sourcePort == src_port);
|
||||
@ -827,6 +919,7 @@ RED.view = (function() {
|
||||
RED.nodes.addLink(link);
|
||||
RED.history.push({t:'add',links:[link],dirty:dirty});
|
||||
setDirty(true);
|
||||
} else {
|
||||
}
|
||||
selected_link = null;
|
||||
redraw();
|
||||
@ -835,12 +928,18 @@ RED.view = (function() {
|
||||
|
||||
function nodeMouseUp(d) {
|
||||
if (dblClickPrimed && mousedown_node == d && clickElapsed > 0 && clickElapsed < 750) {
|
||||
mouse_mode = RED.state.DEFAULT;
|
||||
if (d.type != "subflow") {
|
||||
RED.editor.edit(d);
|
||||
} else {
|
||||
RED.editor.editSubflow(activeSubflow);
|
||||
}
|
||||
clickElapsed = 0;
|
||||
d3.event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
portMouseUp(d, d._def.inputs > 0 ? 1 : 0, 0);
|
||||
var direction = d._def? (d.inputs > 0 ? 1: 0) : (d.direction == "in" ? 0: 1)
|
||||
portMouseUp(d, direction, 0);
|
||||
}
|
||||
|
||||
function nodeMouseDown(d) {
|
||||
@ -949,6 +1048,114 @@ RED.view = (function() {
|
||||
if (mouse_mode != RED.state.JOINING) {
|
||||
// Don't bother redrawing nodes if we're drawing links
|
||||
|
||||
if (activeSubflow) {
|
||||
var subflowOutputs = vis.selectAll(".subflowoutput").data(activeSubflow.out,function(d,i){ return d.z+":"+i;});
|
||||
subflowOutputs.exit().remove();
|
||||
var outGroup = subflowOutputs.enter().insert("svg:g").attr("class","node subflowoutput").attr("transform",function(d) { return "translate("+(d.x-20)+","+(d.y-20)+")"});
|
||||
outGroup.each(function(d,i) {
|
||||
d.w=40;
|
||||
d.h=40;
|
||||
});
|
||||
outGroup.append("rect").attr("class","subflowport").attr("rx",8).attr("ry",8).attr("width",40).attr("height",40)
|
||||
// TODO: This is exactly the same set of handlers used for regular nodes - DRY
|
||||
.on("mouseup",nodeMouseUp)
|
||||
.on("mousedown",nodeMouseDown)
|
||||
.on("touchstart",function(d) {
|
||||
var obj = d3.select(this);
|
||||
var touch0 = d3.event.touches.item(0);
|
||||
var pos = [touch0.pageX,touch0.pageY];
|
||||
startTouchCenter = [touch0.pageX,touch0.pageY];
|
||||
startTouchDistance = 0;
|
||||
touchStartTime = setTimeout(function() {
|
||||
showTouchMenu(obj,pos);
|
||||
},touchLongPressTimeout);
|
||||
nodeMouseDown.call(this,d)
|
||||
})
|
||||
.on("touchend", function(d) {
|
||||
clearTimeout(touchStartTime);
|
||||
touchStartTime = null;
|
||||
if (RED.touch.radialMenu.active()) {
|
||||
d3.event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
nodeMouseUp.call(this,d);
|
||||
});
|
||||
|
||||
outGroup.append("rect").attr("class","port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10).attr("x",-5).attr("y",15)
|
||||
.on("mousedown", function(d,i){portMouseDown(d,1,0);} )
|
||||
.on("touchstart", function(d,i){portMouseDown(d,1,0);} )
|
||||
.on("mouseup", function(d,i){portMouseUp(d,1,0);})
|
||||
.on("touchend",function(d,i){portMouseUp(d,1,0);} )
|
||||
.on("mouseover",function(d,i) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type !== 0 ));})
|
||||
.on("mouseout",function(d,i) { var port = d3.select(this); port.classed("port_hovered",false);});
|
||||
|
||||
outGroup.append("svg:text").attr('class','port_label').attr('x',20).attr('y',8).style("font-size","10px").text("output");
|
||||
outGroup.append("svg:text").attr('class','port_label').attr('x',20).attr('y',24).text(function(d,i){ return i+1});
|
||||
|
||||
var subflowInputs = vis.selectAll(".subflowinput").data(activeSubflow.in,function(d,i){ return d.z+":"+i;});
|
||||
subflowInputs.exit().remove();
|
||||
var inGroup = subflowInputs.enter().insert("svg:g").attr("class","node subflowinput").attr("transform",function(d) { return "translate("+(d.x-20)+","+(d.y-20)+")"});
|
||||
inGroup.each(function(d,i) {
|
||||
d.w=40;
|
||||
d.h=40;
|
||||
});
|
||||
inGroup.append("rect").attr("class","subflowport").attr("rx",8).attr("ry",8).attr("width",40).attr("height",40)
|
||||
// TODO: This is exactly the same set of handlers used for regular nodes - DRY
|
||||
.on("mouseup",nodeMouseUp)
|
||||
.on("mousedown",nodeMouseDown)
|
||||
.on("touchstart",function(d) {
|
||||
var obj = d3.select(this);
|
||||
var touch0 = d3.event.touches.item(0);
|
||||
var pos = [touch0.pageX,touch0.pageY];
|
||||
startTouchCenter = [touch0.pageX,touch0.pageY];
|
||||
startTouchDistance = 0;
|
||||
touchStartTime = setTimeout(function() {
|
||||
showTouchMenu(obj,pos);
|
||||
},touchLongPressTimeout);
|
||||
nodeMouseDown.call(this,d)
|
||||
})
|
||||
.on("touchend", function(d) {
|
||||
clearTimeout(touchStartTime);
|
||||
touchStartTime = null;
|
||||
if (RED.touch.radialMenu.active()) {
|
||||
d3.event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
nodeMouseUp.call(this,d);
|
||||
});
|
||||
|
||||
inGroup.append("rect").attr("class","port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10).attr("x",35).attr("y",15)
|
||||
.on("mousedown", function(d,i){portMouseDown(d,0,i);} )
|
||||
.on("touchstart", function(d,i){portMouseDown(d,0,i);} )
|
||||
.on("mouseup", function(d,i){portMouseUp(d,0,i);})
|
||||
.on("touchend",function(d,i){portMouseUp(d,0,i);} )
|
||||
.on("mouseover",function(d,i) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type !== 0 ));})
|
||||
.on("mouseout",function(d,i) { var port = d3.select(this); port.classed("port_hovered",false);});
|
||||
inGroup.append("svg:text").attr('class','port_label').attr('x',18).attr('y',20).style("font-size","10px").text("input");
|
||||
|
||||
|
||||
|
||||
subflowOutputs.each(function(d) {
|
||||
if (d.dirty) {
|
||||
var output = d3.select(this);
|
||||
output.selectAll(".subflowport").classed("node_selected",function(d) { return d.selected; })
|
||||
output.attr("transform", function(d) { return "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"; });
|
||||
d.dirty = false;
|
||||
}
|
||||
});
|
||||
subflowInputs.each(function(d) {
|
||||
if (d.dirty) {
|
||||
var input = d3.select(this);
|
||||
input.selectAll(".subflowport").classed("node_selected",function(d) { return d.selected; })
|
||||
input.attr("transform", function(d) { return "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"; });
|
||||
d.dirty = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
vis.selectAll(".subflowoutput").remove();
|
||||
vis.selectAll(".subflowinput").remove();
|
||||
}
|
||||
|
||||
var node = vis.selectAll(".nodegroup").data(RED.nodes.nodes.filter(function(d) { return d.z == activeWorkspace }),function(d){return d.id});
|
||||
node.exit().remove();
|
||||
|
||||
@ -1083,7 +1290,7 @@ RED.view = (function() {
|
||||
//icon.attr('class','node_icon_shade_border node_icon_shade_border_'+d._def.align);
|
||||
}
|
||||
|
||||
//if (d._def.inputs > 0 && d._def.align == null) {
|
||||
//if (d.inputs > 0 && d._def.align == null) {
|
||||
// icon_shade.attr("width",35);
|
||||
// icon.attr("transform","translate(5,0)");
|
||||
// icon_shade_border.attr("transform","translate(5,0)");
|
||||
@ -1133,17 +1340,6 @@ RED.view = (function() {
|
||||
|
||||
//node.append("circle").attr({"class":"centerDot","cx":0,"cy":0,"r":5});
|
||||
|
||||
if (d._def.inputs > 0) {
|
||||
text.attr("x",38);
|
||||
node.append("rect").attr("class","port port_input").attr("rx",3).attr("ry",3).attr("x",-5).attr("width",10).attr("height",10)
|
||||
.on("mousedown",function(d){portMouseDown(d,1,0);})
|
||||
.on("touchstart",function(d){portMouseDown(d,1,0);})
|
||||
.on("mouseup",function(d){portMouseUp(d,1,0);} )
|
||||
.on("touchend",function(d){portMouseUp(d,1,0);} )
|
||||
.on("mouseover",function(d) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type != 1 ));})
|
||||
.on("mouseout",function(d) { var port = d3.select(this); port.classed("port_hovered",false);})
|
||||
}
|
||||
|
||||
//node.append("path").attr("class","node_error").attr("d","M 3,-3 l 10,0 l -5,-8 z");
|
||||
node.append("image").attr("class","node_error hidden").attr("xlink:href","icons/node-error.png").attr("x",0).attr("y",-6).attr("width",10).attr("height",9);
|
||||
node.append("image").attr("class","node_changed hidden").attr("xlink:href","icons/node-changed.png").attr("x",12).attr("y",-6).attr("width",10).attr("height",10);
|
||||
@ -1157,6 +1353,7 @@ RED.view = (function() {
|
||||
l = (typeof l === "function" ? l.call(d) : l)||"";
|
||||
d.w = Math.max(node_width,calculateTextWidth(l, "node_label", 50)+(d._def.inputs>0?7:0) );
|
||||
d.h = Math.max(node_height,(d.outputs||0) * 15);
|
||||
d.resize = false;
|
||||
}
|
||||
var thisNode = d3.select(this);
|
||||
//thisNode.selectAll(".centerDot").attr({"cx":function(d) { return d.w/2;},"cy":function(d){return d.h/2}});
|
||||
@ -1176,18 +1373,35 @@ RED.view = (function() {
|
||||
//thisNode.selectAll(".node_icon_shade_right").attr("x",function(d){return d.w-30;});
|
||||
//thisNode.selectAll(".node_icon_shade_border_right").attr("d",function(d){return "M "+(d.w-30)+" 1 l 0 "+(d.h-2)});
|
||||
|
||||
var inputPorts = thisNode.selectAll(".port_input");
|
||||
if (d.inputs === 0 && !inputPorts.empty()) {
|
||||
inputPorts.remove();
|
||||
//nodeLabel.attr("x",30);
|
||||
} else if (d.inputs === 1 && inputPorts.empty()) {
|
||||
var inputGroup = thisNode.append("g").attr("class","port_input");
|
||||
inputGroup.append("rect").attr("class","port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
|
||||
.on("mousedown",function(d){portMouseDown(d,1,0);})
|
||||
.on("touchstart",function(d){portMouseDown(d,1,0);})
|
||||
.on("mouseup",function(d){portMouseUp(d,1,0);} )
|
||||
.on("touchend",function(d){portMouseUp(d,1,0);} )
|
||||
.on("mouseover",function(d) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type != 1 ));})
|
||||
.on("mouseout",function(d) { var port = d3.select(this); port.classed("port_hovered",false);})
|
||||
}
|
||||
|
||||
var numOutputs = d.outputs;
|
||||
var y = (d.h/2)-((numOutputs-1)/2)*13;
|
||||
d.ports = d.ports || d3.range(numOutputs);
|
||||
d._ports = thisNode.selectAll(".port_output").data(d.ports);
|
||||
d._ports.enter().append("rect").attr("class","port port_output").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
|
||||
var output_group = d._ports.enter().append("g").attr("class","port_output");
|
||||
|
||||
output_group.append("rect").attr("class","port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
|
||||
.on("mousedown",(function(){var node = d; return function(d,i){portMouseDown(node,0,i);}})() )
|
||||
.on("touchstart",(function(){var node = d; return function(d,i){portMouseDown(node,0,i);}})() )
|
||||
.on("mouseup",(function(){var node = d; return function(d,i){portMouseUp(node,0,i);}})() )
|
||||
.on("touchend",(function(){var node = d; return function(d,i){portMouseUp(node,0,i);}})() )
|
||||
.on("mouseover",function(d,i) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type !== 0 ));})
|
||||
.on("mouseout",function(d,i) { var port = d3.select(this); port.classed("port_hovered",false);});
|
||||
|
||||
d._ports.exit().remove();
|
||||
if (d._ports) {
|
||||
numOutputs = d.outputs || 1;
|
||||
@ -1195,7 +1409,8 @@ RED.view = (function() {
|
||||
var x = d.w - 5;
|
||||
d._ports.each(function(d,i) {
|
||||
var port = d3.select(this);
|
||||
port.attr("y",(y+13*i)-5).attr("x",x);
|
||||
//port.attr("y",(y+13*i)-5).attr("x",x);
|
||||
port.attr("transform", function(d) { return "translate("+x+","+((y+13*i)-5)+")";});
|
||||
});
|
||||
}
|
||||
thisNode.selectAll('text.node_label').text(function(d,i){
|
||||
@ -1226,7 +1441,7 @@ RED.view = (function() {
|
||||
|
||||
thisNode.selectAll(".port_input").each(function(d,i) {
|
||||
var port = d3.select(this);
|
||||
port.attr("y",function(d){return (d.h/2)-5;})
|
||||
port.attr("transform",function(d){return "translate(-5,"+((d.h/2)-5)+")";})
|
||||
});
|
||||
|
||||
thisNode.selectAll(".node_icon").attr("y",function(d){return (d.h-d3.select(this).attr("height"))/2;});
|
||||
@ -1299,7 +1514,14 @@ RED.view = (function() {
|
||||
});
|
||||
}
|
||||
|
||||
var link = vis.selectAll(".link").data(RED.nodes.links.filter(function(d) { return d.source.z == activeWorkspace && d.target.z == activeWorkspace }),function(d) { return d.source.id+":"+d.sourcePort+":"+d.target.id;});
|
||||
var link = vis.selectAll(".link").data(
|
||||
RED.nodes.links.filter(function(d) {
|
||||
return d.source.z == activeWorkspace && d.target.z == activeWorkspace;
|
||||
}),
|
||||
function(d) {
|
||||
return d.source.id+":"+d.sourcePort+":"+d.target.id+":"+d.target.i;
|
||||
}
|
||||
);
|
||||
|
||||
var linkEnter = link.enter().insert("g",".node").attr("class","link");
|
||||
|
||||
@ -1323,7 +1545,8 @@ RED.view = (function() {
|
||||
d3.event.stopPropagation();
|
||||
});
|
||||
l.append("svg:path").attr("class","link_outline link_path");
|
||||
l.append("svg:path").attr("class","link_line link_path");
|
||||
l.append("svg:path").attr("class","link_line link_path")
|
||||
.classed("link_subflow", function(d) { return activeSubflow && (d.source.type === "subflow" || d.target.type === "subflow") });
|
||||
});
|
||||
|
||||
link.exit().remove();
|
||||
@ -1367,6 +1590,7 @@ RED.view = (function() {
|
||||
if (d3.event) {
|
||||
d3.event.preventDefault();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RED.keyboard.add(/* z */ 90,{ctrl:true},function(){RED.history.pop();});
|
||||
@ -1401,6 +1625,7 @@ RED.view = (function() {
|
||||
var new_nodes = result[0];
|
||||
var new_links = result[1];
|
||||
var new_workspaces = result[2];
|
||||
var new_subflows = result[3];
|
||||
|
||||
var new_ms = new_nodes.filter(function(n) { return n.z == activeWorkspace }).map(function(n) { return {n:n};});
|
||||
var new_node_ids = new_nodes.map(function(n){ return n.id; });
|
||||
@ -1452,14 +1677,25 @@ RED.view = (function() {
|
||||
moving_set = new_ms;
|
||||
}
|
||||
|
||||
RED.history.push({t:'add',nodes:new_node_ids,links:new_links,workspaces:new_workspaces,dirty:RED.view.dirty()});
|
||||
RED.history.push({
|
||||
t:'add',
|
||||
nodes:new_node_ids,
|
||||
links:new_links,
|
||||
workspaces:new_workspaces,
|
||||
subflows:new_subflows,
|
||||
dirty:RED.view.dirty()
|
||||
});
|
||||
|
||||
|
||||
redraw();
|
||||
}
|
||||
} catch(error) {
|
||||
if (error.code != "NODE_RED") {
|
||||
console.log(error.stack);
|
||||
RED.notify("<strong>Error</strong>: "+error,"error");
|
||||
} else {
|
||||
RED.notify("<strong>Error</strong>: "+error.message,"error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1513,6 +1749,11 @@ RED.view = (function() {
|
||||
$( "#node-dialog-rename-workspace" ).dialog("open");
|
||||
}
|
||||
|
||||
function showSubflowDialog(id) {
|
||||
RED.editor.editSubflow(RED.nodes.subflow(id));
|
||||
|
||||
}
|
||||
|
||||
$("#node-dialog-rename-workspace form" ).submit(function(e) { e.preventDefault();});
|
||||
$( "#node-dialog-rename-workspace" ).dialog({
|
||||
modal: true,
|
||||
@ -1535,11 +1776,10 @@ RED.view = (function() {
|
||||
var workspace = $(this).dialog('option','workspace');
|
||||
var label = $( "#node-input-workspace-name" ).val();
|
||||
if (workspace.label != label) {
|
||||
workspace.label = label;
|
||||
var link = $("#workspace-tabs a[href='#"+workspace.id+"']");
|
||||
link.attr("title",label);
|
||||
link.text(label);
|
||||
workspace_tabs.renameTab(workspace.id,label);
|
||||
RED.view.dirty(true);
|
||||
$("#btn-workspace-menu-"+workspace.id.replace(".","-")).text(label);
|
||||
// TODO: update entry in menu
|
||||
}
|
||||
$( this ).dialog( "close" );
|
||||
}
|
||||
@ -1607,7 +1847,9 @@ RED.view = (function() {
|
||||
workspace_tabs.resize();
|
||||
},
|
||||
removeWorkspace: function(ws) {
|
||||
if (workspace_tabs.contains(ws.id)) {
|
||||
workspace_tabs.removeTab(ws.id);
|
||||
}
|
||||
},
|
||||
getWorkspace: function() {
|
||||
return activeWorkspace;
|
||||
@ -1615,7 +1857,14 @@ RED.view = (function() {
|
||||
showWorkspace: function(id) {
|
||||
workspace_tabs.activateTab(id);
|
||||
},
|
||||
redraw:redraw,
|
||||
redraw: function() {
|
||||
RED.nodes.eachSubflow(function(sf) {
|
||||
if (workspace_tabs.contains(sf.id)) {
|
||||
workspace_tabs.renameTab(sf.id,"Subflow: "+sf.name);
|
||||
}
|
||||
});
|
||||
redraw();
|
||||
},
|
||||
dirty: function(d) {
|
||||
if (d == null) {
|
||||
return dirty;
|
||||
@ -1638,6 +1887,220 @@ RED.view = (function() {
|
||||
//TODO: should these move to an import/export module?
|
||||
showImportNodesDialog: showImportNodesDialog,
|
||||
showExportNodesDialog: showExportNodesDialog,
|
||||
showExportNodesLibraryDialog: showExportNodesLibraryDialog
|
||||
showExportNodesLibraryDialog: showExportNodesLibraryDialog,
|
||||
addFlow: function() {
|
||||
var ws = {type:"subflow",id:RED.nodes.id(),label:"Flow 1", closeable: true};
|
||||
RED.nodes.addWorkspace(ws);
|
||||
workspace_tabs.addTab(ws);
|
||||
workspace_tabs.activateTab(ws.id);
|
||||
return ws;
|
||||
},
|
||||
|
||||
showSubflow: function(id) {
|
||||
if (!workspace_tabs.contains(id)) {
|
||||
var sf = RED.nodes.subflow(id);
|
||||
workspace_tabs.addTab({type:"subflow",id:id,label:"Subflow: "+sf.name, closeable: true});
|
||||
workspace_tabs.resize();
|
||||
}
|
||||
workspace_tabs.activateTab(id);
|
||||
},
|
||||
|
||||
createSubflow: function() {
|
||||
var lastIndex = 0;
|
||||
RED.nodes.eachSubflow(function(sf) {
|
||||
var m = (new RegExp("^Subflow (\\d+)$")).exec(sf.name);
|
||||
if (m) {
|
||||
lastIndex = Math.max(lastIndex,m[1]);
|
||||
}
|
||||
});
|
||||
|
||||
var name = "Subflow "+(lastIndex+1);
|
||||
|
||||
var subflowId = RED.nodes.id();
|
||||
var subflow = {
|
||||
type:"subflow",
|
||||
id:subflowId,
|
||||
name:name,
|
||||
in: [],
|
||||
out: []
|
||||
};
|
||||
RED.nodes.addSubflow(subflow);
|
||||
RED.history.push({
|
||||
t:'createSubflow',
|
||||
subflow: subflow,
|
||||
dirty:RED.view.dirty()
|
||||
});
|
||||
RED.view.showSubflow(subflowId);
|
||||
},
|
||||
|
||||
convertToSubflow: function() {
|
||||
if (moving_set.length === 0) {
|
||||
RED.notify("<strong>Cannot create subflow</strong>: no nodes selected","error");
|
||||
return;
|
||||
}
|
||||
var i;
|
||||
var nodes = {};
|
||||
var new_links = [];
|
||||
var removedLinks = [];
|
||||
|
||||
var candidateInputs = [];
|
||||
var candidateOutputs = [];
|
||||
|
||||
var boundingBox = [moving_set[0].n.x,moving_set[0].n.y,moving_set[0].n.x,moving_set[0].n.y];
|
||||
|
||||
for (i=0;i<moving_set.length;i++) {
|
||||
var n = moving_set[i];
|
||||
nodes[n.n.id] = {n:n.n,outputs:{}};
|
||||
boundingBox = [
|
||||
Math.min(boundingBox[0],n.n.x),
|
||||
Math.min(boundingBox[1],n.n.y),
|
||||
Math.max(boundingBox[2],n.n.x),
|
||||
Math.max(boundingBox[3],n.n.y)
|
||||
]
|
||||
}
|
||||
|
||||
var center = [(boundingBox[2]+boundingBox[0]) / 2,(boundingBox[3]+boundingBox[1]) / 2];
|
||||
|
||||
RED.nodes.eachLink(function(link) {
|
||||
if (nodes[link.source.id] && nodes[link.target.id]) {
|
||||
// A link wholely within the selection
|
||||
}
|
||||
|
||||
if (nodes[link.source.id] && !nodes[link.target.id]) {
|
||||
// An outbound link from the selection
|
||||
candidateOutputs.push(link);
|
||||
removedLinks.push(link);
|
||||
}
|
||||
if (!nodes[link.source.id] && nodes[link.target.id]) {
|
||||
// An inbound link
|
||||
candidateInputs.push(link);
|
||||
removedLinks.push(link);
|
||||
}
|
||||
});
|
||||
|
||||
var outputs = {};
|
||||
candidateOutputs = candidateOutputs.filter(function(v) {
|
||||
if (outputs[v.source.id+":"+v.sourcePort]) {
|
||||
outputs[v.source.id+":"+v.sourcePort].targets.push(v.target);
|
||||
return false;
|
||||
}
|
||||
v.targets = [];
|
||||
v.targets.push(v.target);
|
||||
outputs[v.source.id+":"+v.sourcePort] = v;
|
||||
return true;
|
||||
});
|
||||
candidateOutputs.sort(function(a,b) { return a.source.y-b.source.y});
|
||||
|
||||
if (candidateInputs.length > 1) {
|
||||
RED.notify("<strong>Cannot create subflow</strong>: multiple inputs to selection","error");
|
||||
return;
|
||||
}
|
||||
//if (candidateInputs.length == 0) {
|
||||
// RED.notify("<strong>Cannot create subflow</strong>: no input to selection","error");
|
||||
// return;
|
||||
//}
|
||||
|
||||
|
||||
var lastIndex = 0;
|
||||
RED.nodes.eachSubflow(function(sf) {
|
||||
var m = (new RegExp("^Subflow (\\d+)$")).exec(sf.name);
|
||||
if (m) {
|
||||
lastIndex = Math.max(lastIndex,m[1]);
|
||||
}
|
||||
});
|
||||
|
||||
var name = "Subflow "+(lastIndex+1);
|
||||
|
||||
var subflowId = RED.nodes.id();
|
||||
var subflow = {
|
||||
type:"subflow",
|
||||
id:subflowId,
|
||||
name:name,
|
||||
in: candidateInputs.map(function(v,i) { return {
|
||||
type:"subflow",
|
||||
direction:"in",
|
||||
x:v.target.x-(v.target.w/2)-80,
|
||||
y:v.target.y,
|
||||
z:subflowId,
|
||||
wires:[{id:v.target.id}]
|
||||
}}),
|
||||
out: candidateOutputs.map(function(v) { return {
|
||||
type:"subflow",
|
||||
direction:"in",
|
||||
x:v.source.x+(v.source.w/2)+80,
|
||||
y:v.source.y,
|
||||
z:subflowId,
|
||||
wires:[{id:v.source.id,port:v.sourcePort}]
|
||||
}})
|
||||
};
|
||||
RED.nodes.addSubflow(subflow);
|
||||
|
||||
var subflowInstance = {
|
||||
id:RED.nodes.id(),
|
||||
type:"subflow:"+subflow.id,
|
||||
x: center[0],
|
||||
y: center[1],
|
||||
z: activeWorkspace,
|
||||
inputs: subflow.in.length,
|
||||
outputs: subflow.out.length,
|
||||
h: Math.max(node_height,(subflow.out.length||0) * 15),
|
||||
changed:true
|
||||
}
|
||||
subflowInstance._def = RED.nodes.getType(subflowInstance.type);
|
||||
RED.editor.validateNode(subflowInstance);
|
||||
RED.nodes.add(subflowInstance);
|
||||
|
||||
candidateInputs.forEach(function(l) {
|
||||
var link = {source:l.source, sourcePort:l.sourcePort, target: subflowInstance};
|
||||
new_links.push(link);
|
||||
RED.nodes.addLink(link);
|
||||
});
|
||||
|
||||
candidateOutputs.forEach(function(output,i) {
|
||||
output.targets.forEach(function(target) {
|
||||
var link = {source:subflowInstance, sourcePort:i, target: target};
|
||||
new_links.push(link);
|
||||
RED.nodes.addLink(link);
|
||||
});
|
||||
});
|
||||
|
||||
subflow.in.forEach(function(input) {
|
||||
input.wires.forEach(function(wire) {
|
||||
var link = {source: input, sourcePort: 0, target: RED.nodes.node(wire.id) }
|
||||
new_links.push(link);
|
||||
RED.nodes.addLink(link);
|
||||
});
|
||||
});
|
||||
subflow.out.forEach(function(output,i) {
|
||||
output.wires.forEach(function(wire) {
|
||||
var link = {source: RED.nodes.node(wire.id), sourcePort: wire.port , target: output }
|
||||
new_links.push(link);
|
||||
RED.nodes.addLink(link);
|
||||
});
|
||||
});
|
||||
|
||||
for (i=0;i<removedLinks.length;i++) {
|
||||
RED.nodes.removeLink(removedLinks[i]);
|
||||
}
|
||||
|
||||
for (i=0;i<moving_set.length;i++) {
|
||||
moving_set[i].n.z = subflow.id;
|
||||
}
|
||||
|
||||
RED.history.push({
|
||||
t:'createSubflow',
|
||||
nodes:[subflowInstance.id],
|
||||
links:new_links,
|
||||
subflow: subflow,
|
||||
|
||||
activeWorkspace: activeWorkspace,
|
||||
removedLinks: removedLinks,
|
||||
|
||||
dirty:RED.view.dirty()
|
||||
});
|
||||
|
||||
setDirty(true);
|
||||
redraw();
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
@ -74,6 +74,14 @@ span.logo span {
|
||||
span.logo img {
|
||||
height: 18px;
|
||||
}
|
||||
.button {
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#header .button {
|
||||
line-height: 22px;
|
||||
display: inline-block;
|
||||
@ -118,7 +126,26 @@ span.logo img {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
#workspace-toolbar .button {
|
||||
line-height: 18px;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
color: #666;
|
||||
background: #f6f6f6;
|
||||
vertical-align: middle;
|
||||
box-shadow: 0 0 2px #888;
|
||||
}
|
||||
#workspace-toolbar .button:hover {
|
||||
background: #e6e6e6;
|
||||
box-shadow: 0 0 2px #666;
|
||||
}
|
||||
#workspace-toolbar .button:active {
|
||||
background: #e0e0e0;
|
||||
box-shadow: 0 0 2px #444;
|
||||
}
|
||||
|
||||
#workspace {
|
||||
margin-left: 160px;
|
||||
@ -140,8 +167,9 @@ span.logo img {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left:0;
|
||||
right: 18px;
|
||||
padding: 5px;
|
||||
right: 20px;
|
||||
padding: 7px;
|
||||
border-bottom-right-radius: 5px;
|
||||
background: #f3f3f3;
|
||||
}
|
||||
|
||||
@ -263,9 +291,10 @@ span.logo img {
|
||||
clear: both;
|
||||
}
|
||||
.palette_label {
|
||||
line-height: 25px;
|
||||
margin: 4px 0;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
.palette_node {
|
||||
cursor:move;
|
||||
@ -425,6 +454,22 @@ span.logo img {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.port_label {
|
||||
stroke-width: 0;
|
||||
fill: #888;
|
||||
font-size: 16px;
|
||||
alignment-baseline: middle;
|
||||
text-anchor: middle;
|
||||
pointer-events: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
||||
.function_label {
|
||||
font-size: 12px;
|
||||
}
|
||||
@ -460,9 +505,21 @@ span.logo img {
|
||||
|
||||
}
|
||||
.port {
|
||||
stroke: #999;
|
||||
stroke-width: 2;
|
||||
fill: #ddd;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.port_highlight {
|
||||
stroke: #6DA332;
|
||||
stroke-width: 3;
|
||||
fill: #fff;
|
||||
pointer-events:none;
|
||||
fill-opacity: 0.5;
|
||||
}
|
||||
|
||||
|
||||
.node_error {
|
||||
stroke: #ff0000;
|
||||
stroke-width: 2;
|
||||
@ -491,7 +548,7 @@ span.logo img {
|
||||
stroke: #ff0000;
|
||||
}
|
||||
.node_selected {
|
||||
stroke: #ff7f0e;
|
||||
stroke: #ff7f0e !important;
|
||||
}
|
||||
.node_highlighted {
|
||||
stroke: #dd1616;
|
||||
@ -505,6 +562,11 @@ span.logo img {
|
||||
stroke: #ff7f0e;
|
||||
fill: #ff7f0e;
|
||||
}
|
||||
.subflowport {
|
||||
stroke-dasharray: 5,5;
|
||||
fill: #eee;
|
||||
stroke: #999;
|
||||
}
|
||||
|
||||
.drag_line {
|
||||
stroke: #ff7f0e;
|
||||
@ -527,6 +589,12 @@ span.logo img {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.link_subflow {
|
||||
stroke: #bbb;
|
||||
stroke-dasharray: 10,5;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.link_outline {
|
||||
stroke: #fff;
|
||||
stroke-width: 6;
|
||||
|
@ -15,6 +15,7 @@
|
||||
**/
|
||||
|
||||
var util = require("util");
|
||||
var clone = require("clone");
|
||||
var when = require("when");
|
||||
|
||||
var typeRegistry = require("./registry");
|
||||
@ -25,6 +26,7 @@ var events = require("../events");
|
||||
var storage = null;
|
||||
|
||||
var nodes = {};
|
||||
var subflows = {};
|
||||
var activeConfig = [];
|
||||
var missingTypes = [];
|
||||
|
||||
@ -41,19 +43,111 @@ events.on('type-registered',function(type) {
|
||||
}
|
||||
});
|
||||
|
||||
function getID() {
|
||||
return (1+Math.random()*4294967295).toString(16);
|
||||
}
|
||||
|
||||
function createSubflow(sf,sfn) {
|
||||
var node_map = {};
|
||||
var newNodes = [];
|
||||
var node;
|
||||
var wires;
|
||||
var i,j,k;
|
||||
|
||||
// Clone all of the subflow node definitions and give them new IDs
|
||||
for (i=0;i<sf.nodes.length;i++) {
|
||||
node = clone(sf.nodes[i]);
|
||||
var nid = getID();
|
||||
node_map[node.id] = node;
|
||||
node.id = nid;
|
||||
newNodes.push(node);
|
||||
}
|
||||
// Update all subflow interior wiring to reflect new node IDs
|
||||
for (i=0;i<newNodes.length;i++) {
|
||||
node = newNodes[i];
|
||||
var outputs = node.wires;
|
||||
|
||||
for (j=0;j<outputs.length;j++) {
|
||||
wires = outputs[j];
|
||||
for (k=0;k<wires.length;k++) {
|
||||
outputs[j][k] = node_map[outputs[j][k]].id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a subflow node to accept inbound messages and route appropriately
|
||||
var Node = require("./Node");
|
||||
var subflowInstance = {
|
||||
id: sfn.id,
|
||||
type: sfn.type,
|
||||
name: sfn.name,
|
||||
wires: []
|
||||
}
|
||||
if (sf.in) {
|
||||
subflowInstance.wires = sf.in.map(function(n) { return n.wires.map(function(w) { return node_map[w.id].id;})})
|
||||
}
|
||||
var subflowNode = new Node(subflowInstance);
|
||||
subflowNode.on("input", function(msg) { this.send(msg);});
|
||||
|
||||
// Wire the subflow outputs
|
||||
if (sf.out) {
|
||||
for (i=0;i<sf.out.length;i++) {
|
||||
wires = sf.out[i].wires;
|
||||
for (j=0;j<wires.length;j++) {
|
||||
if (wires[j].id === sf.id) {
|
||||
node = subflowNode;
|
||||
delete subflowNode._wire;
|
||||
} else {
|
||||
node = node_map[wires[j].id];
|
||||
}
|
||||
node.wires[wires[j].port] = node.wires[wires[j].port].concat(sfn.wires[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate the nodes
|
||||
for (i=0;i<newNodes.length;i++) {
|
||||
node = newNodes[i];
|
||||
var nn = null;
|
||||
var type = node.type;
|
||||
|
||||
var m = /^subflow:(.+)$/.exec(type);
|
||||
if (!m) {
|
||||
var nt = typeRegistry.get(type);
|
||||
if (nt) {
|
||||
try {
|
||||
nn = new nt(node);
|
||||
}
|
||||
catch (err) {
|
||||
util.log("[red] "+type+" : "+err);
|
||||
}
|
||||
}
|
||||
if (nn === null) {
|
||||
util.log("[red] unknown type: "+type);
|
||||
}
|
||||
} else {
|
||||
var subflowId = m[1];
|
||||
createSubflow(subflows[subflowId],node);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the current activeConfig and creates the required node instances
|
||||
*/
|
||||
function parseConfig() {
|
||||
var i;
|
||||
var nt;
|
||||
var type;
|
||||
var subflow;
|
||||
missingTypes = [];
|
||||
|
||||
// Scan the configuration for any unknown node types
|
||||
for (i=0;i<activeConfig.length;i++) {
|
||||
var type = activeConfig[i].type;
|
||||
type = activeConfig[i].type;
|
||||
// TODO: remove workspace in next release+1
|
||||
if (type != "workspace" && type != "tab") {
|
||||
if (type != "workspace" && type != "tab" && !/^subflow($|:.+$)/.test(type)) {
|
||||
nt = typeRegistry.get(type);
|
||||
if (!nt && missingTypes.indexOf(type) == -1) {
|
||||
missingTypes.push(type);
|
||||
@ -72,25 +166,50 @@ function parseConfig() {
|
||||
util.log("[red] Starting flows");
|
||||
events.emit("nodes-starting");
|
||||
|
||||
for (i=0;i<activeConfig.length;i++) {
|
||||
type = activeConfig[i].type;
|
||||
if (type === "subflow") {
|
||||
subflow = activeConfig[i];
|
||||
subflow.nodes = [];
|
||||
subflow.instances = [];
|
||||
subflows[subflow.id] = subflow;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
for (i=0;i<activeConfig.length;i++) {
|
||||
if (subflows[activeConfig[i].z]) {
|
||||
subflow = subflows[activeConfig[i].z];
|
||||
subflow.nodes.push(activeConfig[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate each node in the flow
|
||||
for (i=0;i<activeConfig.length;i++) {
|
||||
var nn = null;
|
||||
type = activeConfig[i].type;
|
||||
|
||||
var m = /^subflow:(.+)$/.exec(type);
|
||||
if (!m) {
|
||||
// TODO: remove workspace in next release+1
|
||||
if (activeConfig[i].type != "workspace" && activeConfig[i].type != "tab") {
|
||||
nt = typeRegistry.get(activeConfig[i].type);
|
||||
if (type != "workspace" && type != "tab" && type != "subflow" && !subflows[activeConfig[i].z]) {
|
||||
nt = typeRegistry.get(type);
|
||||
if (nt) {
|
||||
try {
|
||||
nn = new nt(activeConfig[i]);
|
||||
}
|
||||
catch (err) {
|
||||
util.log("[red] "+activeConfig[i].type+" : "+err);
|
||||
util.log("[red] "+type+" : "+err);
|
||||
}
|
||||
}
|
||||
// console.log(nn);
|
||||
if (nn === null) {
|
||||
util.log("[red] unknown type: "+activeConfig[i].type);
|
||||
util.log("[red] unknown type: "+type);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var subflowId = m[1];
|
||||
createSubflow(subflows[subflowId],activeConfig[i]);
|
||||
}
|
||||
}
|
||||
// Clean up any orphaned credentials
|
||||
credentials.clean(flowNodes.get);
|
||||
|
@ -46,6 +46,12 @@ function loadFlows(testFlows, cb) {
|
||||
|
||||
describe('flows', function() {
|
||||
|
||||
afterEach(function(done) {
|
||||
flows.clear().then(function() {
|
||||
loadFlows([],done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#add',function() {
|
||||
it('should be called by node constructor',function(done) {
|
||||
var n = new RedNode({id:'123',type:'abc'});
|
||||
@ -108,6 +114,58 @@ describe('flows', function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not instantiate nodes of an unused subflow', function(done) {
|
||||
RED.registerType('abc', function() {});
|
||||
var typeRegistryGet = sinon.stub(typeRegistry,"get",function(nt) {
|
||||
return function() {};
|
||||
});
|
||||
loadFlows([{"id":"n1","type":"subflow",inputs:[],outputs:[],wires:[]},
|
||||
{"id":"n2","type":"abc","z":"n1",wires:[]}
|
||||
],function() { });
|
||||
events.once('nodes-started', function() {
|
||||
(flows.get("n2") == null).should.be.true;
|
||||
var ncount = 0
|
||||
flows.each(function(n) {
|
||||
ncount++;
|
||||
});
|
||||
ncount.should.equal(0);
|
||||
console.log(ncount);
|
||||
typeRegistryGet.restore();
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should instantiate nodes of an used subflow with new IDs', function(done) {
|
||||
RED.registerType('abc', function() {});
|
||||
var typeRegistryGet = sinon.stub(typeRegistry,"get",function(nt) {
|
||||
return RedNode;
|
||||
});
|
||||
loadFlows([{"id":"n1","type":"subflow",inputs:[],outputs:[]},
|
||||
{"id":"n2","type":"abc","z":"n1","name":"def",wires:[]},
|
||||
{"id":"n3","type":"subflow:n1"}
|
||||
], function() { });
|
||||
events.once('nodes-started', function() {
|
||||
// n2 should not get instantiated with that id
|
||||
(flows.get("n2") == null).should.be.true;
|
||||
var ncount = 0
|
||||
var nodes = [];
|
||||
flows.each(function(n) {
|
||||
nodes.push(n);
|
||||
});
|
||||
nodes.should.have.lengthOf(2);
|
||||
|
||||
// Assume the nodes are instantiated in this order - not
|
||||
// a requirement, but makes the test easier to write.
|
||||
nodes[0].should.have.property("id","n3");
|
||||
nodes[0].should.have.property("type","subflow:n1");
|
||||
nodes[1].should.not.have.property("id","n2");
|
||||
nodes[1].should.have.property("name","def");
|
||||
|
||||
// TODO: verify instance wiring is correct
|
||||
typeRegistryGet.restore();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setFlows',function() {
|
||||
|
Loading…
Reference in New Issue
Block a user