mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
Add subflow support
This commit is contained in:
parent
348b642d25
commit
d9648ca76b
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="workspace-add-tab"><a id="btn-workspace-add-tab" href="#"><i class="fa fa-plus"></i></a></div>
|
||||||
<div id="chart"></div>
|
<div id="chart"></div>
|
||||||
<div id="workspace-toolbar">
|
<div id="workspace-toolbar">
|
||||||
<div class="btn-group">
|
<a class="button" id="workspace-edit-subflow" href="#"><i class="fa fa-pencil"></i> edit subflow properties</a>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -81,6 +77,20 @@
|
|||||||
|
|
||||||
<div id="dialog" class="hide"><form id="dialog-form" class="form-horizontal"></form></div>
|
<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="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">
|
<div id="node-dialog-confirm-deploy" class="hide">
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
@ -199,7 +209,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/x-red" data-template-name="export-clipboard-dialog">
|
<script type="text/x-red" data-template-name="export-clipboard-dialog">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-export" style="display: block; width:100%;"><i class="fa fa-clipboard"></i> Nodes:</label>
|
<label for="node-input-export" style="display: block; width:100%;"><i class="fa fa-clipboard"></i> Nodes:</label>
|
||||||
@ -222,6 +231,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</script>
|
</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="jquery/js/jquery-1.11.1.min.js"></script>
|
||||||
<script src="bootstrap/js/bootstrap.min.js"></script>
|
<script src="bootstrap/js/bootstrap.min.js"></script>
|
||||||
<script src="jquery/js/jquery-ui-1.10.3.custom.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]);
|
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") {
|
} else if (ev.t == "delete") {
|
||||||
if (ev.workspaces) {
|
if (ev.workspaces) {
|
||||||
for (i=0;i<ev.workspaces.length;i++) {
|
for (i=0;i<ev.workspaces.length;i++) {
|
||||||
@ -57,6 +63,9 @@ RED.history = (function() {
|
|||||||
RED.view.addWorkspace(ev.workspaces[i]);
|
RED.view.addWorkspace(ev.workspaces[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (ev.subflow) {
|
||||||
|
RED.nodes.addSubflow(ev.subflow);
|
||||||
|
}
|
||||||
if (ev.nodes) {
|
if (ev.nodes) {
|
||||||
for (i=0;i<ev.nodes.length;i++) {
|
for (i=0;i<ev.nodes.length;i++) {
|
||||||
RED.nodes.add(ev.nodes[i]);
|
RED.nodes.add(ev.nodes[i]);
|
||||||
@ -80,15 +89,66 @@ RED.history = (function() {
|
|||||||
ev.node[i] = ev.changes[i];
|
ev.node[i] = ev.changes[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RED.editor.updateNodeProperties(ev.node);
|
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) {
|
if (ev.links) {
|
||||||
for (i=0;i<ev.links.length;i++) {
|
for (i=0;i<ev.links.length;i++) {
|
||||||
RED.nodes.addLink(ev.links[i]);
|
RED.nodes.addLink(ev.links[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RED.editor.validateNode(ev.node);
|
|
||||||
ev.node.dirty = true;
|
ev.node.dirty = true;
|
||||||
ev.node.changed = ev.changed;
|
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.dirty(ev.dirty);
|
||||||
RED.view.redraw();
|
RED.view.redraw();
|
||||||
|
@ -301,6 +301,9 @@ var RED = (function() {
|
|||||||
null,
|
null,
|
||||||
{id:"btn-config-nodes",icon:"fa fa-th-list",label:"Configuration nodes...",onselect:RED.sidebar.config.show},
|
{id:"btn-config-nodes",icon:"fa fa-th-list",label:"Configuration nodes...",onselect:RED.sidebar.config.show},
|
||||||
null,
|
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-menu",icon:"fa fa-th-large",label:"Workspaces",options:[
|
||||||
{id:"btn-workspace-add",icon:"fa fa-plus",label:"Add"},
|
{id:"btn-workspace-add",icon:"fa fa-plus",label:"Add"},
|
||||||
{id:"btn-workspace-edit",icon:"fa fa-pencil",label:"Rename"},
|
{id:"btn-workspace-edit",icon:"fa fa-pencil",label:"Rename"},
|
||||||
|
@ -21,6 +21,7 @@ RED.nodes = (function() {
|
|||||||
var links = [];
|
var links = [];
|
||||||
var defaultWorkspace;
|
var defaultWorkspace;
|
||||||
var workspaces = {};
|
var workspaces = {};
|
||||||
|
var subflows = {};
|
||||||
|
|
||||||
var registry = (function() {
|
var registry = (function() {
|
||||||
var nodeList = [];
|
var nodeList = [];
|
||||||
@ -98,13 +99,22 @@ RED.nodes = (function() {
|
|||||||
},
|
},
|
||||||
registerNodeType: function(nt,def) {
|
registerNodeType: function(nt,def) {
|
||||||
nodeDefinitions[nt] = def;
|
nodeDefinitions[nt] = def;
|
||||||
nodeSets[typeToId[nt]].added = true;
|
if (def.category != "subflows") {
|
||||||
// TODO: too tightly coupled into palette UI
|
nodeSets[typeToId[nt]].added = true;
|
||||||
|
// TODO: too tightly coupled into palette UI
|
||||||
|
}
|
||||||
RED.palette.add(nt,def);
|
RED.palette.add(nt,def);
|
||||||
if (def.onpaletteadd && typeof def.onpaletteadd === "function") {
|
if (def.onpaletteadd && typeof def.onpaletteadd === "function") {
|
||||||
def.onpaletteadd.call(def);
|
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) {
|
getNodeType: function(nt) {
|
||||||
return nodeDefinitions[nt];
|
return nodeDefinitions[nt];
|
||||||
}
|
}
|
||||||
@ -122,7 +132,6 @@ RED.nodes = (function() {
|
|||||||
RED.sidebar.config.refresh();
|
RED.sidebar.config.refresh();
|
||||||
} else {
|
} else {
|
||||||
n.dirty = true;
|
n.dirty = true;
|
||||||
nodes.push(n);
|
|
||||||
var updatedConfigNode = false;
|
var updatedConfigNode = false;
|
||||||
for (var d in n._def.defaults) {
|
for (var d in n._def.defaults) {
|
||||||
if (n._def.defaults.hasOwnProperty(d)) {
|
if (n._def.defaults.hasOwnProperty(d)) {
|
||||||
@ -142,6 +151,14 @@ RED.nodes = (function() {
|
|||||||
if (updatedConfigNode) {
|
if (updatedConfigNode) {
|
||||||
RED.sidebar.config.refresh();
|
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) {
|
function addLink(l) {
|
||||||
@ -174,27 +191,27 @@ RED.nodes = (function() {
|
|||||||
if (node) {
|
if (node) {
|
||||||
nodes.splice(nodes.indexOf(node),1);
|
nodes.splice(nodes.indexOf(node),1);
|
||||||
removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); });
|
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;
|
||||||
var updatedConfigNode = false;
|
for (var d in node._def.defaults) {
|
||||||
for (var d in node._def.defaults) {
|
if (node._def.defaults.hasOwnProperty(d)) {
|
||||||
if (node._def.defaults.hasOwnProperty(d)) {
|
var property = node._def.defaults[d];
|
||||||
var property = node._def.defaults[d];
|
if (property.type) {
|
||||||
if (property.type) {
|
var type = registry.getNodeType(property.type);
|
||||||
var type = registry.getNodeType(property.type);
|
if (type && type.category == "config") {
|
||||||
if (type && type.category == "config") {
|
var configNode = configNodes[node[d]];
|
||||||
var configNode = configNodes[node[d]];
|
if (configNode) {
|
||||||
if (configNode) {
|
updatedConfigNode = true;
|
||||||
updatedConfigNode = true;
|
var users = configNode.users;
|
||||||
var users = configNode.users;
|
users.splice(users.indexOf(node),1);
|
||||||
users.splice(users.indexOf(node),1);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (updatedConfigNode) {
|
||||||
if (updatedConfigNode) {
|
RED.sidebar.config.refresh();
|
||||||
RED.sidebar.config.refresh();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return removedLinks;
|
return removedLinks;
|
||||||
@ -237,6 +254,50 @@ RED.nodes = (function() {
|
|||||||
return {nodes:removedNodes,links:removedLinks};
|
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) {
|
function getAllFlowNodes(node) {
|
||||||
var visited = {};
|
var visited = {};
|
||||||
visited[node.id] = true;
|
visited[node.id] = true;
|
||||||
@ -247,8 +308,12 @@ RED.nodes = (function() {
|
|||||||
var childLinks = links.filter(function(d) { return (d.source === n) || (d.target === n);});
|
var childLinks = links.filter(function(d) { return (d.source === n) || (d.target === n);});
|
||||||
for (var i=0;i<childLinks.length;i++) {
|
for (var i=0;i<childLinks.length;i++) {
|
||||||
var child = (childLinks[i].source === n)?childLinks[i].target:childLinks[i].source;
|
var child = (childLinks[i].source === n)?childLinks[i].target:childLinks[i].source;
|
||||||
if (!visited[child.id]) {
|
var id = child.id;
|
||||||
visited[child.id] = true;
|
if (!id) {
|
||||||
|
id = child.direction+":"+child.i;
|
||||||
|
}
|
||||||
|
if (!visited[id]) {
|
||||||
|
visited[id] = true;
|
||||||
nns.push(child);
|
nns.push(child);
|
||||||
stack.push(child);
|
stack.push(child);
|
||||||
}
|
}
|
||||||
@ -291,37 +356,94 @@ RED.nodes = (function() {
|
|||||||
var wires = links.filter(function(d){return d.source === n;});
|
var wires = links.filter(function(d){return d.source === n;});
|
||||||
for (var j=0;j<wires.length;j++) {
|
for (var j=0;j<wires.length;j++) {
|
||||||
var w = wires[j];
|
var w = wires[j];
|
||||||
node.wires[w.sourcePort].push(w.target.id);
|
if (w.target.type != "subflow") {
|
||||||
|
node.wires[w.sourcePort].push(w.target.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return node;
|
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
|
* Converts the current node selection to an exportable JSON Object
|
||||||
**/
|
**/
|
||||||
function createExportableNodeSet(set) {
|
function createExportableNodeSet(set) {
|
||||||
var nns = [];
|
var nns = [];
|
||||||
var exportedConfigNodes = {};
|
var exportedConfigNodes = {};
|
||||||
|
var exportedSubflows = {};
|
||||||
for (var n=0;n<set.length;n++) {
|
for (var n=0;n<set.length;n++) {
|
||||||
var node = set[n].n;
|
var node = set[n].n;
|
||||||
var convertedNode = RED.nodes.convertNode(node);
|
if (node.type.substring(0,8) == "subflow:") {
|
||||||
for (var d in node._def.defaults) {
|
var subflowId = node.type.substring(8);
|
||||||
if (node._def.defaults[d].type && node[d] in configNodes) {
|
if (!exportedSubflows[subflowId]) {
|
||||||
var confNode = configNodes[node[d]];
|
exportedSubflows[subflowId] = true;
|
||||||
var exportable = registry.getNodeType(node._def.defaults[d].type).exportable;
|
var subflow = getSubflow(subflowId);
|
||||||
if ((exportable == null || exportable)) {
|
var subflowSet = [{n:subflow}];
|
||||||
if (!(node[d] in exportedConfigNodes)) {
|
RED.nodes.eachNode(function(n) {
|
||||||
exportedConfigNodes[node[d]] = true;
|
if (n.z == subflowId) {
|
||||||
nns.unshift(RED.nodes.convertNode(confNode));
|
subflowSet.push({n:n});
|
||||||
}
|
}
|
||||||
} else {
|
});
|
||||||
convertedNode[d] = "";
|
var exportableSubflow = createExportableNodeSet(subflowSet);
|
||||||
}
|
nns = exportableSubflow.concat(nns);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (node.type != "subflow") {
|
||||||
nns.push(convertedNode);
|
var convertedNode = RED.nodes.convertNode(node);
|
||||||
|
for (var d in node._def.defaults) {
|
||||||
|
if (node._def.defaults[d].type && node[d] in configNodes) {
|
||||||
|
var confNode = configNodes[node[d]];
|
||||||
|
var exportable = registry.getNodeType(node._def.defaults[d].type).exportable;
|
||||||
|
if ((exportable == null || exportable)) {
|
||||||
|
if (!(node[d] in exportedConfigNodes)) {
|
||||||
|
exportedConfigNodes[node[d]] = true;
|
||||||
|
nns.unshift(RED.nodes.convertNode(confNode));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
convertedNode[d] = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nns.push(convertedNode);
|
||||||
|
} else {
|
||||||
|
var convertedSubflow = convertSubflow(node);
|
||||||
|
nns.push(convertedSubflow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nns;
|
return nns;
|
||||||
}
|
}
|
||||||
@ -332,7 +454,14 @@ RED.nodes = (function() {
|
|||||||
var i;
|
var i;
|
||||||
for (i in workspaces) {
|
for (i in workspaces) {
|
||||||
if (workspaces.hasOwnProperty(i)) {
|
if (workspaces.hasOwnProperty(i)) {
|
||||||
nns.push(workspaces[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) {
|
for (i in configNodes) {
|
||||||
@ -368,7 +497,11 @@ RED.nodes = (function() {
|
|||||||
for (i=0;i<newNodes.length;i++) {
|
for (i=0;i<newNodes.length;i++) {
|
||||||
n = newNodes[i];
|
n = newNodes[i];
|
||||||
// TODO: remove workspace in next release+1
|
// 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)
|
// TODO: get this UI thing out of here! (see below as well)
|
||||||
n.name = n.type;
|
n.name = n.type;
|
||||||
n.type = "unknown";
|
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");
|
//"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 new_workspaces = [];
|
||||||
var workspace_map = {};
|
var workspace_map = {};
|
||||||
|
var new_subflows = [];
|
||||||
|
var subflow_map = {};
|
||||||
|
var nid;
|
||||||
for (i=0;i<newNodes.length;i++) {
|
for (i=0;i<newNodes.length;i++) {
|
||||||
n = newNodes[i];
|
n = newNodes[i];
|
||||||
// TODO: remove workspace in next release+1
|
// TODO: remove workspace in next release+1
|
||||||
@ -403,13 +562,34 @@ RED.nodes = (function() {
|
|||||||
defaultWorkspace = n;
|
defaultWorkspace = n;
|
||||||
}
|
}
|
||||||
if (createNewIds) {
|
if (createNewIds) {
|
||||||
var nid = getID();
|
nid = getID();
|
||||||
workspace_map[n.id] = nid;
|
workspace_map[n.id] = nid;
|
||||||
n.id = nid;
|
n.id = nid;
|
||||||
}
|
}
|
||||||
addWorkspace(n);
|
addWorkspace(n);
|
||||||
RED.view.addWorkspace(n);
|
RED.view.addWorkspace(n);
|
||||||
new_workspaces.push(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) {
|
if (defaultWorkspace == null) {
|
||||||
@ -426,7 +606,7 @@ RED.nodes = (function() {
|
|||||||
for (i=0;i<newNodes.length;i++) {
|
for (i=0;i<newNodes.length;i++) {
|
||||||
n = newNodes[i];
|
n = newNodes[i];
|
||||||
// TODO: remove workspace in next release+1
|
// 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);
|
var def = registry.getNodeType(n.type);
|
||||||
if (def && def.category == "config") {
|
if (def && def.category == "config") {
|
||||||
if (!RED.nodes.node(n.id)) {
|
if (!RED.nodes.node(n.id)) {
|
||||||
@ -443,36 +623,53 @@ RED.nodes = (function() {
|
|||||||
} else {
|
} else {
|
||||||
var node = {x:n.x,y:n.y,z:n.z,type:0,wires:n.wires,changed:false};
|
var node = {x:n.x,y:n.y,z:n.z,type:0,wires:n.wires,changed:false};
|
||||||
if (createNewIds) {
|
if (createNewIds) {
|
||||||
node.z = workspace_map[node.z];
|
if (subflow_map[node.z]) {
|
||||||
if (!workspaces[node.z]) {
|
node.z = subflow_map[node.z].id;
|
||||||
node.z = RED.view.getWorkspace();
|
} else {
|
||||||
|
node.z = workspace_map[node.z];
|
||||||
|
if (!workspaces[node.z]) {
|
||||||
|
node.z = activeWorkspace;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
node.id = getID();
|
node.id = getID();
|
||||||
} else {
|
} else {
|
||||||
node.id = n.id;
|
node.id = n.id;
|
||||||
if (node.z == null || !workspaces[node.z]) {
|
if (node.z == null || (!workspaces[node.z] && !subflow_map[node.z])) {
|
||||||
node.z = RED.view.getWorkspace();
|
node.z = activeWorkspace;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
node.type = n.type;
|
node.type = n.type;
|
||||||
node._def = def;
|
node._def = def;
|
||||||
if (!node._def) {
|
if (n.type.substring(0,7) === "subflow") {
|
||||||
node._def = {
|
var parentId = n.type.split(":")[1];
|
||||||
color:"#fee",
|
var subflow = subflow_map[parentId]||getSubflow(parentId);
|
||||||
defaults: {},
|
if (createNewIds) {
|
||||||
label: "unknown: "+n.type,
|
parentId = subflow.id;
|
||||||
labelStyle: "node_label_italic",
|
node.type = "subflow:"+parentId;
|
||||||
outputs: n.outputs||n.wires.length
|
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",
|
||||||
|
defaults: {},
|
||||||
|
label: "unknown: "+n.type,
|
||||||
|
labelStyle: "node_label_italic",
|
||||||
|
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) {
|
||||||
|
if (node._def.defaults.hasOwnProperty(d2)) {
|
||||||
|
node[d2] = n[d2];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
node.outputs = n.outputs||node._def.outputs;
|
|
||||||
|
|
||||||
for (var d2 in node._def.defaults) {
|
|
||||||
if (node._def.defaults.hasOwnProperty(d2)) {
|
|
||||||
node[d2] = n[d2];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addNode(node);
|
addNode(node);
|
||||||
RED.editor.validateNode(node);
|
RED.editor.validateNode(node);
|
||||||
node_map[n.id] = node;
|
node_map[n.id] = node;
|
||||||
@ -494,10 +691,38 @@ RED.nodes = (function() {
|
|||||||
}
|
}
|
||||||
delete n.wires;
|
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) {
|
} catch(error) {
|
||||||
//TODO: get this UI thing out of here! (see above as well)
|
if (error.code != "NODE_RED") {
|
||||||
RED.notify("<strong>Error</strong>: "+error,"error");
|
console.log(error.stack);
|
||||||
|
RED.notify("<strong>Error</strong>: "+error,"error");
|
||||||
|
} else {
|
||||||
|
RED.notify("<strong>Error</strong>: "+error.message,"error");
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -516,13 +741,22 @@ RED.nodes = (function() {
|
|||||||
registerType: registry.registerNodeType,
|
registerType: registry.registerNodeType,
|
||||||
getType: registry.getNodeType,
|
getType: registry.getNodeType,
|
||||||
convertNode: convertNode,
|
convertNode: convertNode,
|
||||||
|
|
||||||
add: addNode,
|
add: addNode,
|
||||||
addLink: addLink,
|
|
||||||
remove: removeNode,
|
remove: removeNode,
|
||||||
|
|
||||||
|
addLink: addLink,
|
||||||
removeLink: removeLink,
|
removeLink: removeLink,
|
||||||
|
|
||||||
addWorkspace: addWorkspace,
|
addWorkspace: addWorkspace,
|
||||||
removeWorkspace: removeWorkspace,
|
removeWorkspace: removeWorkspace,
|
||||||
workspace: getWorkspace,
|
workspace: getWorkspace,
|
||||||
|
|
||||||
|
addSubflow: addSubflow,
|
||||||
|
removeSubflow: removeSubflow,
|
||||||
|
subflow: getSubflow,
|
||||||
|
subflowContains: subflowContains,
|
||||||
|
|
||||||
eachNode: function(cb) {
|
eachNode: function(cb) {
|
||||||
for (var n=0;n<nodes.length;n++) {
|
for (var n=0;n<nodes.length;n++) {
|
||||||
cb(nodes[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,
|
node: getNode,
|
||||||
import: importNodes,
|
import: importNodes,
|
||||||
refreshValidation: refreshValidation,
|
refreshValidation: refreshValidation,
|
||||||
|
@ -95,22 +95,31 @@ RED.editor = (function() {
|
|||||||
node.resize = true;
|
node.resize = true;
|
||||||
node.dirty = true;
|
node.dirty = true;
|
||||||
var removedLinks = [];
|
var removedLinks = [];
|
||||||
if (node.outputs < node.ports.length) {
|
if (node.ports) {
|
||||||
while (node.outputs < node.ports.length) {
|
if (node.outputs < node.ports.length) {
|
||||||
node.ports.pop();
|
while (node.outputs < node.ports.length) {
|
||||||
|
node.ports.pop();
|
||||||
|
}
|
||||||
|
RED.nodes.eachLink(function(l) {
|
||||||
|
if (l.source === node && l.sourcePort >= node.outputs) {
|
||||||
|
removedLinks.push(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) {
|
RED.nodes.eachLink(function(l) {
|
||||||
if (l.source === node && l.sourcePort >= node.outputs) {
|
if (l.target === node) {
|
||||||
removedLinks.push(l);
|
removedLinks.push(l);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
for (var l=0;l<removedLinks.length;l++) {
|
}
|
||||||
RED.nodes.removeLink(removedLinks[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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return removedLinks;
|
return removedLinks;
|
||||||
}
|
}
|
||||||
@ -274,6 +283,11 @@ RED.editor = (function() {
|
|||||||
RED.sidebar.info.refresh(editing_node);
|
RED.sidebar.info.refresh(editing_node);
|
||||||
}
|
}
|
||||||
RED.sidebar.config.refresh();
|
RED.sidebar.config.refresh();
|
||||||
|
|
||||||
|
var buttons = $( this ).dialog("option","buttons");
|
||||||
|
if (buttons.length == 3) {
|
||||||
|
$( this ).dialog("option","buttons",buttons.splice(1));
|
||||||
|
}
|
||||||
editing_node = null;
|
editing_node = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -459,10 +473,30 @@ RED.editor = (function() {
|
|||||||
function showEditDialog(node) {
|
function showEditDialog(node) {
|
||||||
editing_node = node;
|
editing_node = node;
|
||||||
RED.view.state(RED.state.EDITING);
|
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");
|
$('<input type="text" style="display: none;" />').appendTo("#dialog-form");
|
||||||
prepareEditDialog(node,node._def,"node-input");
|
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) {
|
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 {
|
return {
|
||||||
edit: showEditDialog,
|
edit: showEditDialog,
|
||||||
editConfig: showEditConfigNodeDialog,
|
editConfig: showEditConfigNodeDialog,
|
||||||
|
editSubflow: showEditSubflowDialog,
|
||||||
validateNode: validateNode,
|
validateNode: validateNode,
|
||||||
updateNodeProperties: updateNodeProperties // TODO: only exposed for edit-undo
|
updateNodeProperties: updateNodeProperties // TODO: only exposed for edit-undo
|
||||||
}
|
}
|
||||||
|
@ -36,14 +36,14 @@ RED.menu = (function() {
|
|||||||
|
|
||||||
if (opt.onselect) {
|
if (opt.onselect) {
|
||||||
link.click(function() {
|
link.click(function() {
|
||||||
if ($(this).parent().hasClass("disabled")) {
|
if ($(this).parent().hasClass("disabled")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (opt.toggle) {
|
if (opt.toggle) {
|
||||||
setSelected(opt.id,!isSelected(opt.id));
|
setSelected(opt.id,!isSelected(opt.id));
|
||||||
} else {
|
} else {
|
||||||
opt.onselect.call(opt);
|
opt.onselect.call(opt);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if (opt.href) {
|
} else if (opt.href) {
|
||||||
link.attr("target","_blank").attr("href",opt.href);
|
link.attr("target","_blank").attr("href",opt.href);
|
||||||
@ -110,13 +110,28 @@ RED.menu = (function() {
|
|||||||
$("#"+id).parent().remove();
|
$("#"+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 {
|
return {
|
||||||
init: createMenu,
|
init: createMenu,
|
||||||
setSelected: setSelected,
|
setSelected: setSelected,
|
||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
setDisabled: setDisabled,
|
setDisabled: setDisabled,
|
||||||
addItem: addItem,
|
addItem: addItem,
|
||||||
removeItem: removeItem
|
removeItem: removeItem,
|
||||||
|
setAction: setAction
|
||||||
//TODO: add an api for replacing a submenu - see library.js:loadFlowLibrary
|
//TODO: add an api for replacing a submenu - see library.js:loadFlowLibrary
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
RED.palette = (function() {
|
RED.palette = (function() {
|
||||||
|
|
||||||
var exclusion = ['config','unknown','deprecated'];
|
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){
|
function createCategoryContainer(category){
|
||||||
var escapedCategory = category.replace(" ","_");
|
var escapedCategory = category.replace(" ","_");
|
||||||
@ -38,10 +38,66 @@ RED.palette = (function() {
|
|||||||
|
|
||||||
core.forEach(createCategoryContainer);
|
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) {
|
function addNodeType(nt,def) {
|
||||||
|
|
||||||
var nodeTypeId = nt.replace(" ","_");
|
var nodeTypeId = escapeNodeType(nt);
|
||||||
|
|
||||||
if ($("#palette_node_"+nodeTypeId).length) {
|
if ($("#palette_node_"+nodeTypeId).length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -63,27 +119,12 @@ RED.palette = (function() {
|
|||||||
label = (typeof def.paletteLabel === "function" ? def.paletteLabel.call(def) : def.paletteLabel)||"";
|
label = (typeof def.paletteLabel === "function" ? def.paletteLabel.call(def) : def.paletteLabel)||"";
|
||||||
}
|
}
|
||||||
|
|
||||||
var pixels = RED.view.calculateTextWidth(label, "palette_label", 0);
|
d.innerHTML = '<div class="palette_label"></div>';
|
||||||
var nodeWidth = 90;
|
|
||||||
var numLines = Math.ceil(pixels / nodeWidth);
|
|
||||||
var multiLine = numLines > 1;
|
|
||||||
|
|
||||||
// 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";
|
d.className="palette_node";
|
||||||
if (def.icon) {
|
if (def.icon) {
|
||||||
d.style.backgroundImage = "url(icons/"+def.icon+")";
|
d.style.backgroundImage = "url(icons/"+def.icon+")";
|
||||||
if (multiLine) {
|
d.style.backgroundSize = "18px 27px";
|
||||||
d.style.backgroundSize = "18px 27px";
|
|
||||||
}
|
|
||||||
if (def.align == "right") {
|
if (def.align == "right") {
|
||||||
d.style.backgroundPosition = "95% 50%";
|
d.style.backgroundPosition = "95% 50%";
|
||||||
} else if (def.inputs > 0) {
|
} else if (def.inputs > 0) {
|
||||||
@ -92,23 +133,16 @@ RED.palette = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
d.style.backgroundColor = def.color;
|
d.style.backgroundColor = def.color;
|
||||||
d.style.height = multiLineNodeHeight + "px";
|
|
||||||
|
|
||||||
if (def.outputs > 0) {
|
if (def.outputs > 0) {
|
||||||
var portOut = document.createElement("div");
|
var portOut = document.createElement("div");
|
||||||
portOut.className = "palette_port palette_port_output";
|
portOut.className = "palette_port palette_port_output";
|
||||||
if (multiLine) {
|
|
||||||
portOut.style.top = ((multiLineNodeHeight - portHeight) / 2) + "px";
|
|
||||||
}
|
|
||||||
d.appendChild(portOut);
|
d.appendChild(portOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (def.inputs > 0) {
|
if (def.inputs > 0) {
|
||||||
var portIn = document.createElement("div");
|
var portIn = document.createElement("div");
|
||||||
portIn.className = "palette_port";
|
portIn.className = "palette_port palette_port_input";
|
||||||
if (multiLine) {
|
|
||||||
portIn.style.top = ((multiLineNodeHeight - portHeight) / 2) + "px";
|
|
||||||
}
|
|
||||||
d.appendChild(portIn);
|
d.appendChild(portIn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,23 +157,13 @@ RED.palette = (function() {
|
|||||||
$("#palette-"+category).append(d);
|
$("#palette-"+category).append(d);
|
||||||
d.onmousedown = function(e) { e.preventDefault(); };
|
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({
|
$(d).popover({
|
||||||
title:d.type,
|
title:d.type,
|
||||||
placement:"right",
|
placement:"right",
|
||||||
trigger: "hover",
|
trigger: "hover",
|
||||||
delay: { show: 750, hide: 50 },
|
delay: { show: 750, hide: 50 },
|
||||||
html: true,
|
html: true,
|
||||||
container:'body',
|
container:'body'
|
||||||
content: popOverContent
|
|
||||||
});
|
});
|
||||||
$(d).click(function() {
|
$(d).click(function() {
|
||||||
var help = '<div class="node-help">'+($("script[data-help-name|='"+d.type+"']").html()||"")+"</div>";
|
var help = '<div class="node-help">'+($("script[data-help-name|='"+d.type+"']").html()||"")+"</div>";
|
||||||
@ -151,23 +175,50 @@ RED.palette = (function() {
|
|||||||
revert: true,
|
revert: true,
|
||||||
revertDuration: 50
|
revertDuration: 50
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setLabel(nt,$(d),label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeNodeType(nt) {
|
function removeNodeType(nt) {
|
||||||
var nodeTypeId = nt.replace(" ","_");
|
var nodeTypeId = escapeNodeType(nt);
|
||||||
$("#palette_node_"+nodeTypeId).remove();
|
$("#palette_node_"+nodeTypeId).remove();
|
||||||
}
|
}
|
||||||
function hideNodeType(nt) {
|
function hideNodeType(nt) {
|
||||||
var nodeTypeId = nt.replace(" ","_");
|
var nodeTypeId = escapeNodeType(nt);
|
||||||
$("#palette_node_"+nodeTypeId).hide();
|
$("#palette_node_"+nodeTypeId).hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showNodeType(nt) {
|
function showNodeType(nt) {
|
||||||
var nodeTypeId = nt.replace(" ","_");
|
var nodeTypeId = escapeNodeType(nt);
|
||||||
$("#palette_node_"+nodeTypeId).show();
|
$("#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() {
|
function filterChange() {
|
||||||
var val = $("#palette-search-input").val();
|
var val = $("#palette-search-input").val();
|
||||||
if (val === "") {
|
if (val === "") {
|
||||||
@ -215,6 +266,7 @@ RED.palette = (function() {
|
|||||||
add:addNodeType,
|
add:addNodeType,
|
||||||
remove:removeNodeType,
|
remove:removeNodeType,
|
||||||
hide:hideNodeType,
|
hide:hideNodeType,
|
||||||
show:showNodeType
|
show:showNodeType,
|
||||||
|
refresh:refreshNodeTypes
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
@ -47,37 +47,53 @@ RED.sidebar.info = (function() {
|
|||||||
table += "<tr><td>Type</td><td> "+node.type+"</td></tr>";
|
table += "<tr><td>Type</td><td> "+node.type+"</td></tr>";
|
||||||
table += "<tr><td>ID</td><td> "+node.id+"</td></tr>";
|
table += "<tr><td>ID</td><td> "+node.id+"</td></tr>";
|
||||||
table += '<tr class="blank"><td colspan="2">Properties</td></tr>';
|
table += '<tr class="blank"><td colspan="2">Properties</td></tr>';
|
||||||
for (var n in node._def.defaults) {
|
if (node.type == "subflow") {
|
||||||
if (node._def.defaults.hasOwnProperty(n)) {
|
var userCount = 0;
|
||||||
var val = node[n]||"";
|
var subflowType = "subflow:"+node.id;
|
||||||
var type = typeof val;
|
RED.nodes.eachNode(function(n) {
|
||||||
if (type === "string") {
|
if (n.type === subflowType) {
|
||||||
if (val.length > 30) {
|
userCount++;
|
||||||
val = val.substring(0,30)+" ...";
|
|
||||||
}
|
|
||||||
val = val.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
|
||||||
} else if (type === "number") {
|
|
||||||
val = val.toString();
|
|
||||||
} else if ($.isArray(val)) {
|
|
||||||
val = "[<br/>";
|
|
||||||
for (var i=0;i<Math.min(node[n].length,10);i++) {
|
|
||||||
var vv = JSON.stringify(node[n][i],jsonFilter," ").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
|
||||||
val += " "+i+": "+vv+"<br/>";
|
|
||||||
}
|
|
||||||
if (node[n].length > 10) {
|
|
||||||
val += " ... "+node[n].length+" items<br/>";
|
|
||||||
}
|
|
||||||
val += "]";
|
|
||||||
} else {
|
|
||||||
val = JSON.stringify(val,jsonFilter," ");
|
|
||||||
val = val.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
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]||"";
|
||||||
|
var type = typeof val;
|
||||||
|
if (type === "string") {
|
||||||
|
if (val.length > 30) {
|
||||||
|
val = val.substring(0,30)+" ...";
|
||||||
|
}
|
||||||
|
val = val.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
||||||
|
} else if (type === "number") {
|
||||||
|
val = val.toString();
|
||||||
|
} else if ($.isArray(val)) {
|
||||||
|
val = "[<br/>";
|
||||||
|
for (var i=0;i<Math.min(node[n].length,10);i++) {
|
||||||
|
var vv = JSON.stringify(node[n][i],jsonFilter," ").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
||||||
|
val += " "+i+": "+vv+"<br/>";
|
||||||
|
}
|
||||||
|
if (node[n].length > 10) {
|
||||||
|
val += " ... "+node[n].length+" items<br/>";
|
||||||
|
}
|
||||||
|
val += "]";
|
||||||
|
} else {
|
||||||
|
val = JSON.stringify(val,jsonFilter," ");
|
||||||
|
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 += "</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);
|
$("#tab-info").html(table);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,6 +116,13 @@ RED.tabs = (function() {
|
|||||||
},
|
},
|
||||||
contains: function(id) {
|
contains: function(id) {
|
||||||
return ul.find("a[href='#"+id+"']").length > 0;
|
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 activeWorkspace = 0;
|
||||||
|
var activeSubflow = null;
|
||||||
|
|
||||||
var workspaceScrollPositions = {};
|
var workspaceScrollPositions = {};
|
||||||
|
|
||||||
var selected_link = null,
|
var selected_link = null,
|
||||||
@ -225,6 +227,11 @@ RED.view = (function() {
|
|||||||
|
|
||||||
var drag_line = vis.append("svg:path").attr("class", "drag_line");
|
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({
|
var workspace_tabs = RED.tabs.create({
|
||||||
id: "workspace-tabs",
|
id: "workspace-tabs",
|
||||||
onchange: function(tab) {
|
onchange: function(tab) {
|
||||||
@ -244,6 +251,8 @@ RED.view = (function() {
|
|||||||
var scrollStartTop = chart.scrollTop();
|
var scrollStartTop = chart.scrollTop();
|
||||||
|
|
||||||
activeWorkspace = tab.id;
|
activeWorkspace = tab.id;
|
||||||
|
activeSubflow = RED.nodes.subflow(activeWorkspace);
|
||||||
|
|
||||||
if (workspaceScrollPositions[activeWorkspace]) {
|
if (workspaceScrollPositions[activeWorkspace]) {
|
||||||
chart.scrollLeft(workspaceScrollPositions[activeWorkspace].left);
|
chart.scrollLeft(workspaceScrollPositions[activeWorkspace].left);
|
||||||
chart.scrollTop(workspaceScrollPositions[activeWorkspace].top);
|
chart.scrollTop(workspaceScrollPositions[activeWorkspace].top);
|
||||||
@ -258,6 +267,9 @@ RED.view = (function() {
|
|||||||
mouse_position[1] += scrollDeltaTop;
|
mouse_position[1] += scrollDeltaTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RED.menu.setDisabled("btn-workspace-edit", activeSubflow);
|
||||||
|
RED.menu.setDisabled("btn-workspace-delete",workspace_tabs.count() == 1 || activeSubflow);
|
||||||
|
|
||||||
clearSelection();
|
clearSelection();
|
||||||
RED.nodes.eachNode(function(n) {
|
RED.nodes.eachNode(function(n) {
|
||||||
n.dirty = true;
|
n.dirty = true;
|
||||||
@ -265,7 +277,11 @@ RED.view = (function() {
|
|||||||
redraw();
|
redraw();
|
||||||
},
|
},
|
||||||
ondblclick: function(tab) {
|
ondblclick: function(tab) {
|
||||||
showRenameWorkspaceDialog(tab.id);
|
if (tab.type != "subflow") {
|
||||||
|
showRenameWorkspaceDialog(tab.id);
|
||||||
|
} else {
|
||||||
|
showSubflowDialog(tab.id);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onadd: function(tab) {
|
onadd: function(tab) {
|
||||||
RED.menu.addItem("btn-workspace-menu",{
|
RED.menu.addItem("btn-workspace-menu",{
|
||||||
@ -300,11 +316,12 @@ RED.view = (function() {
|
|||||||
}
|
}
|
||||||
$(function() {
|
$(function() {
|
||||||
$('#btn-workspace-add-tab').on("click",addWorkspace);
|
$('#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);
|
showRenameWorkspaceDialog(activeWorkspace);
|
||||||
});
|
});
|
||||||
$('#btn-workspace-delete').on("click",function() {
|
RED.menu.setAction('btn-workspace-delete',function() {
|
||||||
deleteWorkspace(activeWorkspace);
|
deleteWorkspace(activeWorkspace);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -349,7 +366,6 @@ RED.view = (function() {
|
|||||||
|
|
||||||
function canvasMouseMove() {
|
function canvasMouseMove() {
|
||||||
mouse_position = d3.touches(this)[0]||d3.mouse(this);
|
mouse_position = d3.touches(this)[0]||d3.mouse(this);
|
||||||
|
|
||||||
// Prevent touch scrolling...
|
// Prevent touch scrolling...
|
||||||
//if (d3.touches(this)[0]) {
|
//if (d3.touches(this)[0]) {
|
||||||
// d3.event.preventDefault();
|
// d3.event.preventDefault();
|
||||||
@ -478,7 +494,9 @@ RED.view = (function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
redraw();
|
if (mouse_mode !== 0) {
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function canvasMouseUp() {
|
function canvasMouseUp() {
|
||||||
@ -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();
|
updateSelection();
|
||||||
lasso.remove();
|
lasso.remove();
|
||||||
lasso = null;
|
lasso = null;
|
||||||
@ -545,11 +579,28 @@ RED.view = (function() {
|
|||||||
else { zoomIn(); }
|
else { zoomIn(); }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#chart").droppable({
|
$("#chart").droppable({
|
||||||
accept:".palette_node",
|
accept:".palette_node",
|
||||||
drop: function( event, ui ) {
|
drop: function( event, ui ) {
|
||||||
d3.event = event;
|
d3.event = event;
|
||||||
var selected_tool = ui.draggable[0].type;
|
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);
|
var mousePos = d3.touches(this)[0]||d3.mouse(this);
|
||||||
mousePos[1] += this.scrollTop;
|
mousePos[1] += this.scrollTop;
|
||||||
mousePos[0] += this.scrollLeft;
|
mousePos[0] += this.scrollLeft;
|
||||||
@ -560,17 +611,25 @@ RED.view = (function() {
|
|||||||
|
|
||||||
nn.type = selected_tool;
|
nn.type = selected_tool;
|
||||||
nn._def = RED.nodes.getType(nn.type);
|
nn._def = RED.nodes.getType(nn.type);
|
||||||
nn.outputs = nn._def.outputs;
|
|
||||||
nn.changed = true;
|
|
||||||
|
|
||||||
for (var d in nn._def.defaults) {
|
if (!m) {
|
||||||
if (nn._def.defaults.hasOwnProperty(d)) {
|
nn.inputs = nn._def.inputs || 0;
|
||||||
nn[d] = nn._def.defaults[d].value;
|
nn.outputs = nn._def.outputs;
|
||||||
|
nn.changed = true;
|
||||||
|
|
||||||
|
for (var d in nn._def.defaults) {
|
||||||
|
if (nn._def.defaults.hasOwnProperty(d)) {
|
||||||
|
nn[d] = nn._def.defaults[d].value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (nn._def.onadd) {
|
if (nn._def.onadd) {
|
||||||
nn._def.onadd.call(nn);
|
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);
|
nn.h = Math.max(node_height,(nn.outputs||0) * 15);
|
||||||
@ -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;
|
selected_link = null;
|
||||||
updateSelection();
|
updateSelection();
|
||||||
redraw();
|
redraw();
|
||||||
@ -638,10 +714,12 @@ RED.view = (function() {
|
|||||||
RED.menu.setDisabled("btn-export-menu",true);
|
RED.menu.setDisabled("btn-export-menu",true);
|
||||||
RED.menu.setDisabled("btn-export-clipboard",true);
|
RED.menu.setDisabled("btn-export-clipboard",true);
|
||||||
RED.menu.setDisabled("btn-export-library",true);
|
RED.menu.setDisabled("btn-export-library",true);
|
||||||
|
RED.menu.setDisabled("btn-convert-subflow",true);
|
||||||
} else {
|
} else {
|
||||||
RED.menu.setDisabled("btn-export-menu",false);
|
RED.menu.setDisabled("btn-export-menu",false);
|
||||||
RED.menu.setDisabled("btn-export-clipboard",false);
|
RED.menu.setDisabled("btn-export-clipboard",false);
|
||||||
RED.menu.setDisabled("btn-export-library",false);
|
RED.menu.setDisabled("btn-export-library",false);
|
||||||
|
RED.menu.setDisabled("btn-convert-subflow",false);
|
||||||
}
|
}
|
||||||
if (moving_set.length === 0 && selected_link == null) {
|
if (moving_set.length === 0 && selected_link == null) {
|
||||||
RED.keyboard.remove(/* backspace */ 8);
|
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);
|
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.length == 1) {
|
||||||
RED.sidebar.info.refresh(moving_set[0].n);
|
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 {
|
} else {
|
||||||
RED.sidebar.info.clear();
|
RED.sidebar.info.clear();
|
||||||
}
|
}
|
||||||
@ -716,15 +800,21 @@ RED.view = (function() {
|
|||||||
for (var i=0;i<moving_set.length;i++) {
|
for (var i=0;i<moving_set.length;i++) {
|
||||||
var node = moving_set[i].n;
|
var node = moving_set[i].n;
|
||||||
node.selected = false;
|
node.selected = false;
|
||||||
if (node.x < 0) {
|
if (node.type != "subflow") {
|
||||||
node.x = 25
|
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;
|
||||||
}
|
}
|
||||||
var rmlinks = RED.nodes.remove(node.id);
|
|
||||||
removedNodes.push(node);
|
|
||||||
removedLinks = removedLinks.concat(rmlinks);
|
|
||||||
}
|
}
|
||||||
moving_set = [];
|
moving_set = [];
|
||||||
setDirty(true);
|
if (removedNodes.length > 0) {
|
||||||
|
setDirty(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (selected_link) {
|
if (selected_link) {
|
||||||
RED.nodes.removeLink(selected_link);
|
RED.nodes.removeLink(selected_link);
|
||||||
@ -743,10 +833,12 @@ RED.view = (function() {
|
|||||||
var nns = [];
|
var nns = [];
|
||||||
for (var n=0;n<moving_set.length;n++) {
|
for (var n=0;n<moving_set.length;n++) {
|
||||||
var node = moving_set[n].n;
|
var node = moving_set[n].n;
|
||||||
nns.push(RED.nodes.convertNode(node));
|
if (node.type != "subflow") {
|
||||||
|
nns.push(RED.nodes.convertNode(node));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
clipboard = JSON.stringify(nns);
|
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) {
|
function portMouseDown(d,portType,portIndex) {
|
||||||
|
//console.log(d,portType,portIndex);
|
||||||
// disable zoom
|
// disable zoom
|
||||||
//vis.call(d3.behavior.zoom().on("zoom"), null);
|
//vis.call(d3.behavior.zoom().on("zoom"), null);
|
||||||
mousedown_node = d;
|
mousedown_node = d;
|
||||||
@ -794,7 +887,7 @@ RED.view = (function() {
|
|||||||
if (n.x-hw<mouse_position[0] && n.x+hw> mouse_position[0] &&
|
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]) {
|
n.y-hh<mouse_position[1] && n.y+hh>mouse_position[1]) {
|
||||||
mouseup_node = n;
|
mouseup_node = n;
|
||||||
portType = mouseup_node._def.inputs>0?1:0;
|
portType = mouseup_node.inputs>0?1:0;
|
||||||
portIndex = 0;
|
portIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -817,16 +910,16 @@ RED.view = (function() {
|
|||||||
dst = mousedown_node;
|
dst = mousedown_node;
|
||||||
src_port = portIndex;
|
src_port = portIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
var existingLink = false;
|
var existingLink = false;
|
||||||
RED.nodes.eachLink(function(d) {
|
RED.nodes.eachLink(function(d) {
|
||||||
existingLink = existingLink || (d.source === src && d.target === dst && d.sourcePort == src_port);
|
existingLink = existingLink || (d.source === src && d.target === dst && d.sourcePort == src_port);
|
||||||
});
|
});
|
||||||
if (!existingLink) {
|
if (!existingLink) {
|
||||||
var link = {source: src, sourcePort:src_port, target: dst};
|
var link = {source: src, sourcePort:src_port, target: dst};
|
||||||
RED.nodes.addLink(link);
|
RED.nodes.addLink(link);
|
||||||
RED.history.push({t:'add',links:[link],dirty:dirty});
|
RED.history.push({t:'add',links:[link],dirty:dirty});
|
||||||
setDirty(true);
|
setDirty(true);
|
||||||
|
} else {
|
||||||
}
|
}
|
||||||
selected_link = null;
|
selected_link = null;
|
||||||
redraw();
|
redraw();
|
||||||
@ -835,12 +928,18 @@ RED.view = (function() {
|
|||||||
|
|
||||||
function nodeMouseUp(d) {
|
function nodeMouseUp(d) {
|
||||||
if (dblClickPrimed && mousedown_node == d && clickElapsed > 0 && clickElapsed < 750) {
|
if (dblClickPrimed && mousedown_node == d && clickElapsed > 0 && clickElapsed < 750) {
|
||||||
RED.editor.edit(d);
|
mouse_mode = RED.state.DEFAULT;
|
||||||
|
if (d.type != "subflow") {
|
||||||
|
RED.editor.edit(d);
|
||||||
|
} else {
|
||||||
|
RED.editor.editSubflow(activeSubflow);
|
||||||
|
}
|
||||||
clickElapsed = 0;
|
clickElapsed = 0;
|
||||||
d3.event.stopPropagation();
|
d3.event.stopPropagation();
|
||||||
return;
|
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) {
|
function nodeMouseDown(d) {
|
||||||
@ -949,6 +1048,114 @@ RED.view = (function() {
|
|||||||
if (mouse_mode != RED.state.JOINING) {
|
if (mouse_mode != RED.state.JOINING) {
|
||||||
// Don't bother redrawing nodes if we're drawing links
|
// 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});
|
var node = vis.selectAll(".nodegroup").data(RED.nodes.nodes.filter(function(d) { return d.z == activeWorkspace }),function(d){return d.id});
|
||||||
node.exit().remove();
|
node.exit().remove();
|
||||||
|
|
||||||
@ -1083,7 +1290,7 @@ RED.view = (function() {
|
|||||||
//icon.attr('class','node_icon_shade_border node_icon_shade_border_'+d._def.align);
|
//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_shade.attr("width",35);
|
||||||
// icon.attr("transform","translate(5,0)");
|
// icon.attr("transform","translate(5,0)");
|
||||||
// icon_shade_border.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});
|
//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("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_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);
|
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)||"";
|
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.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.h = Math.max(node_height,(d.outputs||0) * 15);
|
||||||
|
d.resize = false;
|
||||||
}
|
}
|
||||||
var thisNode = d3.select(this);
|
var thisNode = d3.select(this);
|
||||||
//thisNode.selectAll(".centerDot").attr({"cx":function(d) { return d.w/2;},"cy":function(d){return d.h/2}});
|
//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_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)});
|
//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 numOutputs = d.outputs;
|
||||||
var y = (d.h/2)-((numOutputs-1)/2)*13;
|
var y = (d.h/2)-((numOutputs-1)/2)*13;
|
||||||
d.ports = d.ports || d3.range(numOutputs);
|
d.ports = d.ports || d3.range(numOutputs);
|
||||||
d._ports = thisNode.selectAll(".port_output").data(d.ports);
|
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("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("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("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("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("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);});
|
.on("mouseout",function(d,i) { var port = d3.select(this); port.classed("port_hovered",false);});
|
||||||
|
|
||||||
d._ports.exit().remove();
|
d._ports.exit().remove();
|
||||||
if (d._ports) {
|
if (d._ports) {
|
||||||
numOutputs = d.outputs || 1;
|
numOutputs = d.outputs || 1;
|
||||||
@ -1195,7 +1409,8 @@ RED.view = (function() {
|
|||||||
var x = d.w - 5;
|
var x = d.w - 5;
|
||||||
d._ports.each(function(d,i) {
|
d._ports.each(function(d,i) {
|
||||||
var port = d3.select(this);
|
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){
|
thisNode.selectAll('text.node_label').text(function(d,i){
|
||||||
@ -1207,7 +1422,7 @@ RED.view = (function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
})
|
})
|
||||||
.attr('y', function(d){return (d.h/2)-1;})
|
.attr('y', function(d){return (d.h/2)-1;})
|
||||||
.attr('class',function(d){
|
.attr('class',function(d){
|
||||||
return 'node_label'+
|
return 'node_label'+
|
||||||
@ -1226,7 +1441,7 @@ RED.view = (function() {
|
|||||||
|
|
||||||
thisNode.selectAll(".port_input").each(function(d,i) {
|
thisNode.selectAll(".port_input").each(function(d,i) {
|
||||||
var port = d3.select(this);
|
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;});
|
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");
|
var linkEnter = link.enter().insert("g",".node").attr("class","link");
|
||||||
|
|
||||||
@ -1323,7 +1545,8 @@ RED.view = (function() {
|
|||||||
d3.event.stopPropagation();
|
d3.event.stopPropagation();
|
||||||
});
|
});
|
||||||
l.append("svg:path").attr("class","link_outline link_path");
|
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();
|
link.exit().remove();
|
||||||
@ -1367,6 +1590,7 @@ RED.view = (function() {
|
|||||||
if (d3.event) {
|
if (d3.event) {
|
||||||
d3.event.preventDefault();
|
d3.event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RED.keyboard.add(/* z */ 90,{ctrl:true},function(){RED.history.pop();});
|
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_nodes = result[0];
|
||||||
var new_links = result[1];
|
var new_links = result[1];
|
||||||
var new_workspaces = result[2];
|
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_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; });
|
var new_node_ids = new_nodes.map(function(n){ return n.id; });
|
||||||
@ -1452,14 +1677,25 @@ RED.view = (function() {
|
|||||||
moving_set = new_ms;
|
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();
|
redraw();
|
||||||
}
|
}
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.log(error.stack);
|
if (error.code != "NODE_RED") {
|
||||||
RED.notify("<strong>Error</strong>: "+error,"error");
|
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");
|
$( "#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 form" ).submit(function(e) { e.preventDefault();});
|
||||||
$( "#node-dialog-rename-workspace" ).dialog({
|
$( "#node-dialog-rename-workspace" ).dialog({
|
||||||
modal: true,
|
modal: true,
|
||||||
@ -1535,11 +1776,10 @@ RED.view = (function() {
|
|||||||
var workspace = $(this).dialog('option','workspace');
|
var workspace = $(this).dialog('option','workspace');
|
||||||
var label = $( "#node-input-workspace-name" ).val();
|
var label = $( "#node-input-workspace-name" ).val();
|
||||||
if (workspace.label != label) {
|
if (workspace.label != label) {
|
||||||
workspace.label = label;
|
workspace_tabs.renameTab(workspace.id,label);
|
||||||
var link = $("#workspace-tabs a[href='#"+workspace.id+"']");
|
|
||||||
link.attr("title",label);
|
|
||||||
link.text(label);
|
|
||||||
RED.view.dirty(true);
|
RED.view.dirty(true);
|
||||||
|
$("#btn-workspace-menu-"+workspace.id.replace(".","-")).text(label);
|
||||||
|
// TODO: update entry in menu
|
||||||
}
|
}
|
||||||
$( this ).dialog( "close" );
|
$( this ).dialog( "close" );
|
||||||
}
|
}
|
||||||
@ -1607,7 +1847,9 @@ RED.view = (function() {
|
|||||||
workspace_tabs.resize();
|
workspace_tabs.resize();
|
||||||
},
|
},
|
||||||
removeWorkspace: function(ws) {
|
removeWorkspace: function(ws) {
|
||||||
workspace_tabs.removeTab(ws.id);
|
if (workspace_tabs.contains(ws.id)) {
|
||||||
|
workspace_tabs.removeTab(ws.id);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getWorkspace: function() {
|
getWorkspace: function() {
|
||||||
return activeWorkspace;
|
return activeWorkspace;
|
||||||
@ -1615,7 +1857,14 @@ RED.view = (function() {
|
|||||||
showWorkspace: function(id) {
|
showWorkspace: function(id) {
|
||||||
workspace_tabs.activateTab(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) {
|
dirty: function(d) {
|
||||||
if (d == null) {
|
if (d == null) {
|
||||||
return dirty;
|
return dirty;
|
||||||
@ -1638,6 +1887,220 @@ RED.view = (function() {
|
|||||||
//TODO: should these move to an import/export module?
|
//TODO: should these move to an import/export module?
|
||||||
showImportNodesDialog: showImportNodesDialog,
|
showImportNodesDialog: showImportNodesDialog,
|
||||||
showExportNodesDialog: showExportNodesDialog,
|
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 {
|
span.logo img {
|
||||||
height: 18px;
|
height: 18px;
|
||||||
}
|
}
|
||||||
|
.button {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
#header .button {
|
#header .button {
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -118,7 +126,26 @@ span.logo img {
|
|||||||
outline: none;
|
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 {
|
#workspace {
|
||||||
margin-left: 160px;
|
margin-left: 160px;
|
||||||
@ -140,8 +167,9 @@ span.logo img {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 30px;
|
top: 30px;
|
||||||
left:0;
|
left:0;
|
||||||
right: 18px;
|
right: 20px;
|
||||||
padding: 5px;
|
padding: 7px;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
background: #f3f3f3;
|
background: #f3f3f3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,9 +291,10 @@ span.logo img {
|
|||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
.palette_label {
|
.palette_label {
|
||||||
line-height: 25px;
|
margin: 4px 0;
|
||||||
text-align: center;
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.palette_node {
|
.palette_node {
|
||||||
cursor:move;
|
cursor:move;
|
||||||
@ -425,6 +454,22 @@ span.logo img {
|
|||||||
user-select: none;
|
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 {
|
.function_label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
@ -460,9 +505,21 @@ span.logo img {
|
|||||||
|
|
||||||
}
|
}
|
||||||
.port {
|
.port {
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 2;
|
||||||
fill: #ddd;
|
fill: #ddd;
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.port_highlight {
|
||||||
|
stroke: #6DA332;
|
||||||
|
stroke-width: 3;
|
||||||
|
fill: #fff;
|
||||||
|
pointer-events:none;
|
||||||
|
fill-opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.node_error {
|
.node_error {
|
||||||
stroke: #ff0000;
|
stroke: #ff0000;
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
@ -491,7 +548,7 @@ span.logo img {
|
|||||||
stroke: #ff0000;
|
stroke: #ff0000;
|
||||||
}
|
}
|
||||||
.node_selected {
|
.node_selected {
|
||||||
stroke: #ff7f0e;
|
stroke: #ff7f0e !important;
|
||||||
}
|
}
|
||||||
.node_highlighted {
|
.node_highlighted {
|
||||||
stroke: #dd1616;
|
stroke: #dd1616;
|
||||||
@ -505,6 +562,11 @@ span.logo img {
|
|||||||
stroke: #ff7f0e;
|
stroke: #ff7f0e;
|
||||||
fill: #ff7f0e;
|
fill: #ff7f0e;
|
||||||
}
|
}
|
||||||
|
.subflowport {
|
||||||
|
stroke-dasharray: 5,5;
|
||||||
|
fill: #eee;
|
||||||
|
stroke: #999;
|
||||||
|
}
|
||||||
|
|
||||||
.drag_line {
|
.drag_line {
|
||||||
stroke: #ff7f0e;
|
stroke: #ff7f0e;
|
||||||
@ -527,6 +589,12 @@ span.logo img {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link_subflow {
|
||||||
|
stroke: #bbb;
|
||||||
|
stroke-dasharray: 10,5;
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
|
||||||
.link_outline {
|
.link_outline {
|
||||||
stroke: #fff;
|
stroke: #fff;
|
||||||
stroke-width: 6;
|
stroke-width: 6;
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
**/
|
**/
|
||||||
|
|
||||||
var util = require("util");
|
var util = require("util");
|
||||||
|
var clone = require("clone");
|
||||||
var when = require("when");
|
var when = require("when");
|
||||||
|
|
||||||
var typeRegistry = require("./registry");
|
var typeRegistry = require("./registry");
|
||||||
@ -25,6 +26,7 @@ var events = require("../events");
|
|||||||
var storage = null;
|
var storage = null;
|
||||||
|
|
||||||
var nodes = {};
|
var nodes = {};
|
||||||
|
var subflows = {};
|
||||||
var activeConfig = [];
|
var activeConfig = [];
|
||||||
var missingTypes = [];
|
var missingTypes = [];
|
||||||
|
|
||||||
@ -41,19 +43,110 @@ 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,
|
||||||
|
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
|
* Parses the current activeConfig and creates the required node instances
|
||||||
*/
|
*/
|
||||||
function parseConfig() {
|
function parseConfig() {
|
||||||
var i;
|
var i;
|
||||||
var nt;
|
var nt;
|
||||||
|
var type;
|
||||||
|
var subflow;
|
||||||
missingTypes = [];
|
missingTypes = [];
|
||||||
|
|
||||||
// Scan the configuration for any unknown node types
|
// Scan the configuration for any unknown node types
|
||||||
for (i=0;i<activeConfig.length;i++) {
|
for (i=0;i<activeConfig.length;i++) {
|
||||||
var type = activeConfig[i].type;
|
type = activeConfig[i].type;
|
||||||
// TODO: remove workspace in next release+1
|
// TODO: remove workspace in next release+1
|
||||||
if (type != "workspace" && type != "tab") {
|
if (type != "workspace" && type != "tab" && !/^subflow($|:.+$)/.test(type)) {
|
||||||
nt = typeRegistry.get(type);
|
nt = typeRegistry.get(type);
|
||||||
if (!nt && missingTypes.indexOf(type) == -1) {
|
if (!nt && missingTypes.indexOf(type) == -1) {
|
||||||
missingTypes.push(type);
|
missingTypes.push(type);
|
||||||
@ -72,24 +165,49 @@ function parseConfig() {
|
|||||||
util.log("[red] Starting flows");
|
util.log("[red] Starting flows");
|
||||||
events.emit("nodes-starting");
|
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
|
// Instantiate each node in the flow
|
||||||
for (i=0;i<activeConfig.length;i++) {
|
for (i=0;i<activeConfig.length;i++) {
|
||||||
var nn = null;
|
var nn = null;
|
||||||
// TODO: remove workspace in next release+1
|
type = activeConfig[i].type;
|
||||||
if (activeConfig[i].type != "workspace" && activeConfig[i].type != "tab") {
|
|
||||||
nt = typeRegistry.get(activeConfig[i].type);
|
var m = /^subflow:(.+)$/.exec(type);
|
||||||
if (nt) {
|
if (!m) {
|
||||||
try {
|
// TODO: remove workspace in next release+1
|
||||||
nn = new nt(activeConfig[i]);
|
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] "+type+" : "+err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (err) {
|
if (nn === null) {
|
||||||
util.log("[red] "+activeConfig[i].type+" : "+err);
|
util.log("[red] unknown type: "+type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// console.log(nn);
|
} else {
|
||||||
if (nn === null) {
|
var subflowId = m[1];
|
||||||
util.log("[red] unknown type: "+activeConfig[i].type);
|
createSubflow(subflows[subflowId],activeConfig[i]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Clean up any orphaned credentials
|
// Clean up any orphaned credentials
|
||||||
|
Loading…
Reference in New Issue
Block a user