1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Clean up subflow editor

- new appearance of subflow tabs
 - input/output buttons now counters
 - allow multiple input wires to the same node when converting to subflow
 - ensure edit history is propagated properly to instance nodes
This commit is contained in:
Nick O'Leary 2015-07-30 11:03:37 +01:00
parent d1a5395727
commit fdbf079896
11 changed files with 493 additions and 178 deletions

View File

@ -15,7 +15,7 @@
**/ **/
RED.history = (function() { RED.history = (function() {
var undo_history = []; var undo_history = [];
return { return {
//TODO: this function is a placeholder until there is a 'save' event that can be listened to //TODO: this function is a placeholder until there is a 'save' event that can be listened to
markAllDirty: function() { markAllDirty: function() {
@ -62,6 +62,23 @@ RED.history = (function() {
RED.workspaces.remove(ev.subflows[i]); RED.workspaces.remove(ev.subflows[i]);
} }
} }
if (ev.subflow) {
if (ev.subflow.instances) {
ev.subflow.instances.forEach(function(n) {
var node = RED.nodes.node(n.id);
if (node) {
node.changed = n.changed;
node.dirty = true;
}
});
}
if (ev.subflow.hasOwnProperty('changed')) {
var subflow = RED.nodes.subflow(ev.subflow.id);
if (subflow) {
subflow.changed = ev.subflow.changed;
}
}
}
} 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++) {
@ -69,8 +86,8 @@ RED.history = (function() {
RED.workspaces.add(ev.workspaces[i]); RED.workspaces.add(ev.workspaces[i]);
} }
} }
if (ev.subflow) { if (ev.subflow && ev.subflow.subflow) {
RED.nodes.addSubflow(ev.subflow); RED.nodes.addSubflow(ev.subflow.subflow);
} }
var subflow; var subflow;
if (ev.subflowInputs && ev.subflowInputs.length > 0) { if (ev.subflowInputs && ev.subflowInputs.length > 0) {
@ -97,9 +114,17 @@ RED.history = (function() {
}); });
} }
} }
if (ev.subflow && ev.subflow.hasOwnProperty('instances')) {
ev.subflow.instances.forEach(function(n) {
var node = RED.nodes.node(n.id);
if (node) {
node.changed = n.changed;
node.dirty = true;
}
});
}
if (subflow) { if (subflow) {
RED.nodes.filterNodes({type:"subflow:"+subflow.id}).forEach(function(n) { RED.nodes.filterNodes({type:"subflow:"+subflow.id}).forEach(function(n) {
n.changed = true;
n.inputs = subflow.in.length; n.inputs = subflow.in.length;
n.outputs = subflow.out.length; n.outputs = subflow.out.length;
while (n.outputs > n.ports.length) { while (n.outputs > n.ports.length) {
@ -148,13 +173,21 @@ RED.history = (function() {
ev.node.out = ev.node.out.concat(ev.subflow.outputs); ev.node.out = ev.node.out.concat(ev.subflow.outputs);
} }
} }
if (ev.subflow.hasOwnProperty('instances')) {
ev.subflow.instances.forEach(function(n) {
var node = RED.nodes.node(n.id);
if (node) {
node.changed = n.changed;
node.dirty = true;
}
});
}
RED.nodes.filterNodes({type:"subflow:"+ev.node.id}).forEach(function(n) { RED.nodes.filterNodes({type:"subflow:"+ev.node.id}).forEach(function(n) {
n.changed = ev.changed;
n.inputs = ev.node.in.length; n.inputs = ev.node.in.length;
n.outputs = ev.node.out.length; n.outputs = ev.node.out.length;
RED.editor.updateNodeProperties(n); RED.editor.updateNodeProperties(n);
}); });
RED.palette.refresh(); RED.palette.refresh();
} else { } else {
RED.editor.updateNodeProperties(ev.node); RED.editor.updateNodeProperties(ev.node);
@ -182,10 +215,10 @@ RED.history = (function() {
RED.nodes.removeLink(ev.links[i]); RED.nodes.removeLink(ev.links[i]);
} }
} }
RED.nodes.removeSubflow(ev.subflow); RED.nodes.removeSubflow(ev.subflow.subflow);
RED.workspaces.remove(ev.subflow); RED.workspaces.remove(ev.subflow.subflow);
if (ev.removedLinks) { if (ev.removedLinks) {
for (i=0;i<ev.removedLinks.length;i++) { for (i=0;i<ev.removedLinks.length;i++) {
RED.nodes.addLink(ev.removedLinks[i]); RED.nodes.addLink(ev.removedLinks[i]);
@ -199,7 +232,6 @@ RED.history = (function() {
} }
}); });
RED.nodes.dirty(ev.dirty); RED.nodes.dirty(ev.dirty);
RED.view.redraw(true); RED.view.redraw(true);
RED.palette.refresh(); RED.palette.refresh();

View File

@ -33,7 +33,6 @@ RED.editor = (function() {
var subflow; var subflow;
var isValid; var isValid;
var hasChanged; var hasChanged;
if (node.type.indexOf("subflow:")===0) { if (node.type.indexOf("subflow:")===0) {
subflow = RED.nodes.subflow(node.type.substring(8)); subflow = RED.nodes.subflow(node.type.substring(8));
isValid = subflow.valid; isValid = subflow.valid;
@ -43,7 +42,7 @@ RED.editor = (function() {
hasChanged = subflow.changed; hasChanged = subflow.changed;
} }
node.valid = isValid; node.valid = isValid;
node.changed = hasChanged; node.changed = node.changed || hasChanged;
} else if (node._def) { } else if (node._def) {
node.valid = validateNodeProperties(node, node._def.defaults, node); node.valid = validateNodeProperties(node, node._def.defaults, node);
if (node._def._creds) { if (node._def._creds) {
@ -65,7 +64,7 @@ RED.editor = (function() {
var modifiedTabs = {}; var modifiedTabs = {};
for (i=0;i<subflowInstances.length;i++) { for (i=0;i<subflowInstances.length;i++) {
subflowInstances[i].valid = node.valid; subflowInstances[i].valid = node.valid;
subflowInstances[i].changed = node.changed; subflowInstances[i].changed = subflowInstances[i].changed || node.changed;
subflowInstances[i].dirty = true; subflowInstances[i].dirty = true;
modifiedTabs[subflowInstances[i].z] = true; modifiedTabs[subflowInstances[i].z] = true;
} }
@ -270,7 +269,36 @@ RED.editor = (function() {
var wasChanged = editing_node.changed; var wasChanged = editing_node.changed;
editing_node.changed = true; editing_node.changed = true;
RED.nodes.dirty(true); RED.nodes.dirty(true);
RED.history.push({t:'edit',node:editing_node,changes:changes,links:removedLinks,dirty:wasDirty,changed:wasChanged});
var activeSubflow = RED.nodes.subflow(RED.workspaces.active());
if (activeSubflow) {
var subflowInstances = [];
RED.nodes.eachNode(function(n) {
if (n.type == "subflow:"+RED.workspaces.active()) {
subflowInstances.push({
id:n.id,
changed:n.changed
});
n.changed = true;
n.dirty = true;
updateNodeProperties(n);
}
});
}
var historyEvent = {
t:'edit',
node:editing_node,
changes:changes,
links:removedLinks,
dirty:wasDirty,
changed:wasChanged
};
if (subflowInstances) {
historyEvent.subflow = {
instances:subflowInstances
}
}
RED.history.push(historyEvent);
} }
editing_node.dirty = true; editing_node.dirty = true;
validateNode(editing_node); validateNode(editing_node);
@ -900,15 +928,21 @@ RED.editor = (function() {
changes['name'] = editing_node.name; changes['name'] = editing_node.name;
editing_node.name = newName; editing_node.name = newName;
changed = true; changed = true;
$("#menu-item-workspace-menu-"+editing_node.id.replace(".","-")).text(RED._("subflow.tabLabel",{name:newName})); $("#menu-item-workspace-menu-"+editing_node.id.replace(".","-")).text(newName);
} }
RED.palette.refresh(); RED.palette.refresh();
if (changed) { if (changed) {
var subflowInstances = [];
RED.nodes.eachNode(function(n) { RED.nodes.eachNode(function(n) {
if (n.type == "subflow:"+editing_node.id) { if (n.type == "subflow:"+editing_node.id) {
subflowInstances.push({
id:n.id,
changed:n.changed
})
n.changed = true; n.changed = true;
n.dirty = true;
updateNodeProperties(n); updateNodeProperties(n);
} }
}); });
@ -920,7 +954,10 @@ RED.editor = (function() {
node:editing_node, node:editing_node,
changes:changes, changes:changes,
dirty:wasDirty, dirty:wasDirty,
changed:wasChanged changed:wasChanged,
subflow: {
instances:subflowInstances
}
}; };
RED.history.push(historyEvent); RED.history.push(historyEvent);

View File

@ -21,8 +21,11 @@ RED.subflow = (function() {
return RED.nodes.subflow(RED.workspaces.active()); return RED.nodes.subflow(RED.workspaces.active());
} }
function findAvailableSubflowIOPosition(subflow) { function findAvailableSubflowIOPosition(subflow,isInput) {
var pos = {x:70,y:70}; var pos = {x:50,y:30};
if (!isInput) {
pos.x += 110;
}
for (var i=0;i<subflow.out.length+subflow.in.length;i++) { for (var i=0;i<subflow.out.length+subflow.in.length;i++) {
var port; var port;
if (i < subflow.out.length) { if (i < subflow.out.length) {
@ -40,7 +43,10 @@ RED.subflow = (function() {
function addSubflowInput() { function addSubflowInput() {
var subflow = RED.nodes.subflow(RED.workspaces.active()); var subflow = RED.nodes.subflow(RED.workspaces.active());
var position = findAvailableSubflowIOPosition(subflow); if (subflow.in.length === 1) {
return;
}
var position = findAvailableSubflowIOPosition(subflow,true);
var newInput = { var newInput = {
type:"subflow", type:"subflow",
direction:"in", direction:"in",
@ -56,31 +62,50 @@ RED.subflow = (function() {
var wasDirty = RED.nodes.dirty(); var wasDirty = RED.nodes.dirty();
var wasChanged = subflow.changed; var wasChanged = subflow.changed;
subflow.changed = true; subflow.changed = true;
var result = refresh(true);
RED.nodes.eachNode(function(n) {
if (n.type == "subflow:"+subflow.id) {
n.changed = true;
n.inputs = subflow.in.length;
RED.editor.updateNodeProperties(n);
}
});
var historyEvent = { var historyEvent = {
t:'edit', t:'edit',
node:subflow, node:subflow,
dirty:wasDirty, dirty:wasDirty,
changed:wasChanged, changed:wasChanged,
subflow: { subflow: {
inputCount: oldInCount inputCount: oldInCount,
instances: result.instances
} }
}; };
RED.history.push(historyEvent); RED.history.push(historyEvent);
$("#workspace-subflow-add-input").toggleClass("disabled",true);
RED.view.select(); RED.view.select();
RED.nodes.dirty(true);
RED.view.redraw();
$("#workspace-subflow-input-add").addClass("active");
$("#workspace-subflow-input-remove").removeClass("active");
}
function removeSubflowInput() {
var activeSubflow = RED.nodes.subflow(RED.workspaces.active());
if (activeSubflow.in.length === 0) {
return;
}
var removedInput = activeSubflow.in[0];
var removedInputLinks = [];
RED.nodes.eachLink(function(l) {
if (l.source.type == "subflow" && l.source.z == activeSubflow.id && l.source.i == removedInput.i) {
removedInputLinks.push(l);
} else if (l.target.type == "subflow:"+activeSubflow.id) {
removedInputLinks.push(l);
}
});
removedInputLinks.forEach(function(l) { RED.nodes.removeLink(l)});
activeSubflow.in = [];
$("#workspace-subflow-input-add").removeClass("active");
$("#workspace-subflow-input-remove").addClass("active");
activeSubflow.changed = true;
return {subflowInputs: [ removedInput ], links:removedInputLinks};
} }
function addSubflowOutput(id) { function addSubflowOutput(id) {
var subflow = RED.nodes.subflow(RED.workspaces.active()); var subflow = RED.nodes.subflow(RED.workspaces.active());
var position = findAvailableSubflowIOPosition(subflow); var position = findAvailableSubflowIOPosition(subflow,false);
var newOutput = { var newOutput = {
type:"subflow", type:"subflow",
@ -98,45 +123,186 @@ RED.subflow = (function() {
var wasChanged = subflow.changed; var wasChanged = subflow.changed;
subflow.changed = true; subflow.changed = true;
RED.nodes.eachNode(function(n) { var result = refresh(true);
if (n.type == "subflow:"+subflow.id) {
n.changed = true;
n.outputs = subflow.out.length;
RED.editor.updateNodeProperties(n);
}
});
var historyEvent = { var historyEvent = {
t:'edit', t:'edit',
node:subflow, node:subflow,
dirty:wasDirty, dirty:wasDirty,
changed:wasChanged, changed:wasChanged,
subflow: { subflow: {
outputCount: oldOutCount outputCount: oldOutCount,
instances: result.instances
} }
}; };
RED.history.push(historyEvent); RED.history.push(historyEvent);
RED.view.select(); RED.view.select();
RED.nodes.dirty(true);
RED.view.redraw();
$("#workspace-subflow-output .spinner-value").html(subflow.out.length);
} }
function init() { function removeSubflowOutput(removedSubflowOutputs) {
var activeSubflow = RED.nodes.subflow(RED.workspaces.active());
if (activeSubflow.out.length === 0) {
return;
}
if (typeof removedSubflowOutputs === "undefined") {
removedSubflowOutputs = [activeSubflow.out[activeSubflow.out.length-1]];
}
var removedLinks = [];
removedSubflowOutputs.sort(function(a,b) { return b.i-a.i});
for (i=0;i<removedSubflowOutputs.length;i++) {
var output = removedSubflowOutputs[i];
activeSubflow.out.splice(output.i,1);
var subflowRemovedLinks = [];
var subflowMovedLinks = [];
RED.nodes.eachLink(function(l) {
if (l.target.type == "subflow" && l.target.z == activeSubflow.id && l.target.i == output.i) {
subflowRemovedLinks.push(l);
}
if (l.source.type == "subflow:"+activeSubflow.id) {
if (l.sourcePort == output.i) {
subflowRemovedLinks.push(l);
} else if (l.sourcePort > output.i) {
subflowMovedLinks.push(l);
}
}
});
subflowRemovedLinks.forEach(function(l) { RED.nodes.removeLink(l)});
subflowMovedLinks.forEach(function(l) { l.sourcePort--; });
removedLinks = removedLinks.concat(subflowRemovedLinks);
for (var j=output.i;j<activeSubflow.out.length;j++) {
activeSubflow.out[j].i--;
activeSubflow.out[j].dirty = true;
}
}
activeSubflow.changed = true;
return {subflowOutputs: removedSubflowOutputs, links: removedLinks}
}
function refresh(markChange) {
var activeSubflow = RED.nodes.subflow(RED.workspaces.active());
refreshToolbar(activeSubflow);
var subflowInstances = [];
if (activeSubflow) {
RED.nodes.filterNodes({type:"subflow:"+activeSubflow.id}).forEach(function(n) {
subflowInstances.push({
id: n.id,
changed: n.changed
});
if (markChange) {
n.changed = true;
}
n.inputs = activeSubflow.in.length;
n.outputs = activeSubflow.out.length;
while (n.outputs < n.ports.length) {
n.ports.pop();
}
n.resize = true;
n.dirty = true;
RED.editor.updateNodeProperties(n);
});
RED.editor.validateNode(activeSubflow);
return {
instances: subflowInstances
}
}
}
function refreshToolbar(activeSubflow) {
if (activeSubflow) {
$("#workspace-subflow-input-add").toggleClass("active", activeSubflow.in.length !== 0);
$("#workspace-subflow-input-remove").toggleClass("active",activeSubflow.in.length === 0);
$("#workspace-subflow-output .spinner-value").html(activeSubflow.out.length);
}
}
function showWorkspaceToolbar(activeSubflow) {
var toolbar = $("#workspace-toolbar");
toolbar.empty();
$('<a class="button" id="workspace-subflow-edit" href="#" data-i18n="[append]subflow.editSubflowName"><i class="fa fa-pencil"></i></a>').appendTo(toolbar);
$('<span style="margin-left: 5px;" data-i18n="subflow.input"></span> '+
'<div style="display: inline-block;" class="button-group">'+
'<a id="workspace-subflow-input-remove" class="button active" href="#">0</a>'+
'<a id="workspace-subflow-input-add" class="button" href="#">1</a>'+
'</div>').appendTo(toolbar);
$('<span style="margin-left: 5px;" data-i18n="subflow.output"></span> <div id="workspace-subflow-output" style="display: inline-block;" class="button-group spinner-group">'+
'<a id="workspace-subflow-output-remove" class="button" href="#"><i class="fa fa-minus"></i></a>'+
'<div class="spinner-value">3</div>'+
'<a id="workspace-subflow-output-add" class="button" href="#"><i class="fa fa-plus"></i></a>'+
'</div>').appendTo(toolbar);
// $('<a class="button disabled" id="workspace-subflow-add-input" href="#" data-i18n="[append]subflow.input"><i class="fa fa-plus"></i> </a>').appendTo(toolbar);
// $('<a class="button" id="workspace-subflow-add-output" href="#" data-i18n="[append]subflow.output"><i class="fa fa-plus"></i> </a>').appendTo(toolbar);
$('<a class="button" id="workspace-subflow-delete" href="#" data-i18n="[append]subflow.deleteSubflow"><i class="fa fa-trash"></i> </a>').appendTo(toolbar);
toolbar.i18n();
$("#workspace-subflow-output-remove").click(function(event) {
event.preventDefault();
var wasDirty = RED.nodes.dirty();
var wasChanged = activeSubflow.changed;
var result = removeSubflowOutput();
if (result) {
var inst = refresh(true);
RED.history.push({
t:'delete',
links:result.links,
subflowOutputs: result.subflowOutputs,
changed: wasChanged,
dirty:wasDirty,
subflow: {
instances: inst.instances
}
});
RED.view.select();
RED.nodes.dirty(true);
RED.view.redraw(true);
}
});
$("#workspace-subflow-output-add").click(function(event) {
event.preventDefault();
addSubflowOutput();
});
$("#workspace-subflow-input-add").click(function(event) {
event.preventDefault();
addSubflowInput();
});
$("#workspace-subflow-input-remove").click(function(event) {
event.preventDefault();
var wasDirty = RED.nodes.dirty();
var wasChanged = activeSubflow.changed;
activeSubflow.changed = true;
var result = removeSubflowInput();
if (result) {
var inst = refresh(true);
RED.history.push({
t:'delete',
links:result.links,
changed: wasChanged,
subflowInputs: result.subflowInputs,
dirty:wasDirty,
subflow: {
instances: inst.instances
}
});
RED.view.select();
RED.nodes.dirty(true);
RED.view.redraw(true);
}
});
$("#workspace-subflow-edit").click(function(event) { $("#workspace-subflow-edit").click(function(event) {
RED.editor.editSubflow(RED.nodes.subflow(RED.workspaces.active())); RED.editor.editSubflow(RED.nodes.subflow(RED.workspaces.active()));
event.preventDefault(); event.preventDefault();
}); });
$("#workspace-subflow-add-input").click(function(event) {
event.preventDefault();
if ($(this).hasClass("disabled")) {
return;
}
addSubflowInput();
});
$("#workspace-subflow-add-output").click(function(event) {
event.preventDefault();
if ($(this).hasClass("disabled")) {
return;
}
addSubflowOutput();
});
$("#workspace-subflow-delete").click(function(event) { $("#workspace-subflow-delete").click(function(event) {
event.preventDefault(); event.preventDefault();
@ -170,7 +336,9 @@ RED.subflow = (function() {
t:'delete', t:'delete',
nodes:removedNodes, nodes:removedNodes,
links:removedLinks, links:removedLinks,
subflow: activeSubflow, subflow: {
subflow: activeSubflow
},
dirty:startDirty dirty:startDirty
}); });
@ -179,6 +347,26 @@ RED.subflow = (function() {
RED.view.redraw(); RED.view.redraw();
}); });
refreshToolbar(activeSubflow);
$("#chart").css({"margin-top": "40px"});
$("#workspace-toolbar").show();
}
function hideWorkspaceToolbar() {
$("#workspace-toolbar").hide().empty();
$("#chart").css({"margin-top": "0"});
}
function init() {
RED.events.on("workspace:change",function(event) {
var activeSubflow = RED.nodes.subflow(event.workspace);
if (activeSubflow) {
showWorkspaceToolbar(activeSubflow);
} else {
hideWorkspaceToolbar();
}
});
RED.events.on("view:selection-changed",function(selection) { RED.events.on("view:selection-changed",function(selection) {
if (!selection.nodes) { if (!selection.nodes) {
RED.menu.setDisabled("menu-item-subflow-convert",true); RED.menu.setDisabled("menu-item-subflow-convert",true);
@ -211,7 +399,9 @@ RED.subflow = (function() {
RED.nodes.addSubflow(subflow); RED.nodes.addSubflow(subflow);
RED.history.push({ RED.history.push({
t:'createSubflow', t:'createSubflow',
subflow: subflow, subflow: {
subflow:subflow
},
dirty:RED.nodes.dirty() dirty:RED.nodes.dirty()
}); });
RED.workspaces.show(subflowId); RED.workspaces.show(subflowId);
@ -230,6 +420,8 @@ RED.subflow = (function() {
var candidateInputs = []; var candidateInputs = [];
var candidateOutputs = []; var candidateOutputs = [];
var candidateInputNodes = {};
var boundingBox = [selection.nodes[0].x, var boundingBox = [selection.nodes[0].x,
selection.nodes[0].y, selection.nodes[0].y,
@ -262,6 +454,7 @@ RED.subflow = (function() {
if (!nodes[link.source.id] && nodes[link.target.id]) { if (!nodes[link.source.id] && nodes[link.target.id]) {
// An inbound link // An inbound link
candidateInputs.push(link); candidateInputs.push(link);
candidateInputNodes[link.target.id] = link.target;
removedLinks.push(link); removedLinks.push(link);
} }
}); });
@ -279,15 +472,10 @@ RED.subflow = (function() {
}); });
candidateOutputs.sort(function(a,b) { return a.source.y-b.source.y}); candidateOutputs.sort(function(a,b) { return a.source.y-b.source.y});
if (candidateInputs.length > 1) { if (Object.keys(candidateInputNodes).length > 1) {
RED.notify(RED._("subflow.errors.multipleInputsToSelection"),"error"); RED.notify(RED._("subflow.errors.multipleInputsToSelection"),"error");
return; return;
} }
//if (candidateInputs.length == 0) {
// RED.notify("<strong>Cannot create subflow</strong>: no input to selection","error");
// return;
//}
var lastIndex = 0; var lastIndex = 0;
RED.nodes.eachSubflow(function(sf) { RED.nodes.eachSubflow(function(sf) {
@ -304,15 +492,15 @@ RED.subflow = (function() {
type:"subflow", type:"subflow",
id:subflowId, id:subflowId,
name:name, name:name,
in: candidateInputs.map(function(v,i) { var index = i; return { in: Object.keys(candidateInputNodes).map(function(v,i) { var index = i; return {
type:"subflow", type:"subflow",
direction:"in", direction:"in",
x:v.target.x-(v.target.w/2)-80, x:candidateInputNodes[v].x-(candidateInputNodes[v].w/2)-80,
y:v.target.y, y:candidateInputNodes[v].y,
z:subflowId, z:subflowId,
i:index, i:index,
id:RED.nodes.id(), id:RED.nodes.id(),
wires:[{id:v.target.id}] wires:[{id:candidateInputNodes[v].id}]
}}), }}),
out: candidateOutputs.map(function(v,i) { var index = i; return { out: candidateOutputs.map(function(v,i) { var index = i; return {
type:"subflow", type:"subflow",
@ -325,6 +513,7 @@ RED.subflow = (function() {
wires:[{id:v.source.id,port:v.sourcePort}] wires:[{id:v.source.id,port:v.sourcePort}]
}}) }})
}; };
RED.nodes.addSubflow(subflow); RED.nodes.addSubflow(subflow);
var subflowInstance = { var subflowInstance = {
@ -383,7 +572,9 @@ RED.subflow = (function() {
t:'createSubflow', t:'createSubflow',
nodes:[subflowInstance.id], nodes:[subflowInstance.id],
links:new_links, links:new_links,
subflow: subflow, subflow: {
subflow: subflow
},
activeWorkspace: RED.workspaces.active(), activeWorkspace: RED.workspaces.active(),
removedLinks: removedLinks, removedLinks: removedLinks,
@ -401,6 +592,9 @@ RED.subflow = (function() {
return { return {
init: init, init: init,
createSubflow: createSubflow, createSubflow: createSubflow,
convertToSubflow: convertToSubflow convertToSubflow: convertToSubflow,
refresh: refresh,
removeInput: removeSubflowInput,
removeOutput: removeSubflowOutput
} }
})(); })();

View File

@ -80,12 +80,15 @@ RED.tabs = (function() {
tabs.css({width:currentTabWidth+"%"}); tabs.css({width:currentTabWidth+"%"});
if (tabWidth < 50) { if (tabWidth < 50) {
ul.find(".red-ui-tab-close").hide(); ul.find(".red-ui-tab-close").hide();
ul.find(".red-ui-tab-icon").hide();
} else { } else {
ul.find(".red-ui-tab-close").show(); ul.find(".red-ui-tab-close").show();
ul.find(".red-ui-tab-icon").show();
} }
if (currentActiveTabWidth !== 0) { if (currentActiveTabWidth !== 0) {
ul.find("li.red-ui-tab.active").css({"width":options.minimumActiveTabWidth}); ul.find("li.red-ui-tab.active").css({"width":options.minimumActiveTabWidth});
ul.find("li.red-ui-tab.active .red-ui-tab-close").show(); ul.find("li.red-ui-tab.active .red-ui-tab-close").show();
ul.find("li.red-ui-tab.active .red-ui-tab-icon").show();
} }
} }
@ -116,13 +119,16 @@ RED.tabs = (function() {
tabs[tab.id] = tab; tabs[tab.id] = tab;
var li = $("<li/>",{class:"red-ui-tab"}).appendTo(ul); var li = $("<li/>",{class:"red-ui-tab"}).appendTo(ul);
var link = $("<a/>",{href:"#"+tab.id, class:"red-ui-tab-label"}).appendTo(li); var link = $("<a/>",{href:"#"+tab.id, class:"red-ui-tab-label"}).appendTo(li);
link.html(tab.label); if (tab.icon) {
$('<img src="'+tab.icon+'" class="red-ui-tab-icon"/>').appendTo(link);
}
$('<span/>').text(tab.label).appendTo(link);
link.on("click",onTabClick); link.on("click",onTabClick);
link.on("dblclick",onTabDblClick); link.on("dblclick",onTabDblClick);
if (tab.closeable) { if (tab.closeable) {
var closeLink = $("<a/>",{href:"#",class:"red-ui-tab-close"}).appendTo(li); var closeLink = $("<a/>",{href:"#",class:"red-ui-tab-close"}).appendTo(li);
closeLink.html('<i class="fa fa-times" />'); closeLink.append('<i class="fa fa-times" />');
closeLink.on("click",function(event) { closeLink.on("click",function(event) {
removeTab(tab.id); removeTab(tab.id);
@ -150,7 +156,7 @@ RED.tabs = (function() {
tabs[id].label = label; tabs[id].label = label;
var tab = ul.find("a[href='#"+id+"']"); var tab = ul.find("a[href='#"+id+"']");
tab.attr("title",label); tab.attr("title",label);
tab.text(label); tab.find("span").text(label);
updateTabWidths(); updateTabWidths();
} }

View File

@ -255,9 +255,6 @@ RED.view = (function() {
var scrollStartTop = chart.scrollTop(); var scrollStartTop = chart.scrollTop();
activeSubflow = RED.nodes.subflow(event.workspace); activeSubflow = RED.nodes.subflow(event.workspace);
if (activeSubflow) {
$("#workspace-subflow-add-input").toggleClass("disabled",activeSubflow.in.length > 0);
}
RED.menu.setDisabled("menu-item-workspace-edit", activeSubflow); RED.menu.setDisabled("menu-item-workspace-edit", activeSubflow);
RED.menu.setDisabled("menu-item-workspace-delete",RED.workspaces.count() == 1 || activeSubflow); RED.menu.setDisabled("menu-item-workspace-delete",RED.workspaces.count() == 1 || activeSubflow);
@ -351,7 +348,23 @@ RED.view = (function() {
nn.changed = true; nn.changed = true;
nn.h = Math.max(node_height,(nn.outputs||0) * 15); nn.h = Math.max(node_height,(nn.outputs||0) * 15);
RED.history.push({t:'add',nodes:[nn.id],dirty:RED.nodes.dirty()}); var historyEvent = {
t:'add',
nodes:[nn.id],
dirty:RED.nodes.dirty()
}
if (activeSubflow) {
var subflowRefresh = RED.subflow.refresh(true);
if (subflowRefresh) {
historyEvent.subflow = {
id:activeSubflow.id,
changed: activeSubflow.changed,
instances: subflowRefresh.instances
}
}
}
RED.history.push(historyEvent);
RED.nodes.add(nn); RED.nodes.add(nn);
RED.editor.validateNode(nn); RED.editor.validateNode(nn);
RED.nodes.dirty(true); RED.nodes.dirty(true);
@ -745,8 +758,10 @@ RED.view = (function() {
var removedLinks = []; var removedLinks = [];
var removedSubflowOutputs = []; var removedSubflowOutputs = [];
var removedSubflowInputs = []; var removedSubflowInputs = [];
var subflowInstances = [];
var startDirty = RED.nodes.dirty(); var startDirty = RED.nodes.dirty();
var startChanged = false;
if (moving_set.length > 0) { if (moving_set.length > 0) {
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;
@ -769,65 +784,22 @@ RED.view = (function() {
} }
} }
if (removedSubflowOutputs.length > 0) { if (removedSubflowOutputs.length > 0) {
removedSubflowOutputs.sort(function(a,b) { return b.i-a.i}); var result = RED.subflow.removeOutput(removedSubflowOutputs);
for (i=0;i<removedSubflowOutputs.length;i++) { if (result) {
var output = removedSubflowOutputs[i]; removedLinks = removedLinks.concat(result.links);
activeSubflow.out.splice(output.i,1);
var subflowRemovedLinks = [];
var subflowMovedLinks = [];
RED.nodes.eachLink(function(l) {
if (l.target.type == "subflow" && l.target.z == activeSubflow.id && l.target.i == output.i) {
subflowRemovedLinks.push(l);
}
if (l.source.type == "subflow:"+activeSubflow.id) {
if (l.sourcePort == output.i) {
subflowRemovedLinks.push(l);
} else if (l.sourcePort > output.i) {
subflowMovedLinks.push(l);
}
}
});
subflowRemovedLinks.forEach(function(l) { RED.nodes.removeLink(l)});
subflowMovedLinks.forEach(function(l) { l.sourcePort--; });
removedLinks = removedLinks.concat(subflowRemovedLinks);
for (var j=output.i;j<activeSubflow.out.length;j++) {
activeSubflow.out[j].i--;
activeSubflow.out[j].dirty = true;
}
} }
} }
// Assume 0/1 inputs // Assume 0/1 inputs
if (removedSubflowInputs.length == 1) { if (removedSubflowInputs.length == 1) {
var input = removedSubflowInputs[0]; var result = RED.subflow.removeInput();
var subflowRemovedInputLinks = []; if (result) {
RED.nodes.eachLink(function(l) { removedLinks = removedLinks.concat(result.links);
if (l.source.type == "subflow" && l.source.z == activeSubflow.id && l.source.i == input.i) { }
subflowRemovedInputLinks.push(l);
} else if (l.target.type == "subflow:"+activeSubflow.id) {
subflowRemovedInputLinks.push(l);
}
});
subflowRemovedInputLinks.forEach(function(l) { RED.nodes.removeLink(l)});
removedLinks = removedLinks.concat(subflowRemovedInputLinks);
activeSubflow.in = [];
$("#workspace-subflow-add-input").toggleClass("disabled",false);
} }
var instances = RED.subflow.refresh(true);
if (activeSubflow) { if (instances) {
RED.nodes.filterNodes({type:"subflow:"+activeSubflow.id}).forEach(function(n) { subflowInstances = instances.instances;
n.changed = true;
n.inputs = activeSubflow.in.length;
n.outputs = activeSubflow.out.length;
while (n.outputs < n.ports.length) {
n.ports.pop();
}
n.resize = true;
n.dirty = true;
});
RED.editor.validateNode(activeSubflow);
} }
moving_set = []; moving_set = [];
if (removedNodes.length > 0 || removedSubflowOutputs.length > 0 || removedSubflowInputs.length > 0) { if (removedNodes.length > 0 || removedSubflowOutputs.length > 0 || removedSubflowInputs.length > 0) {
RED.nodes.dirty(true); RED.nodes.dirty(true);
@ -838,7 +810,18 @@ RED.view = (function() {
removedLinks.push(selected_link); removedLinks.push(selected_link);
RED.nodes.dirty(true); RED.nodes.dirty(true);
} }
RED.history.push({t:'delete',nodes:removedNodes,links:removedLinks,subflowOutputs:removedSubflowOutputs,subflowInputs:removedSubflowInputs,dirty:startDirty}); var historyEvent = {
t:'delete',
nodes:removedNodes,
links:removedLinks,
subflowOutputs:removedSubflowOutputs,
subflowInputs:removedSubflowInputs,
subflow: {
instances: subflowInstances
},
dirty:startDirty
};
RED.history.push(historyEvent);
selected_link = null; selected_link = null;
updateActiveNodes(); updateActiveNodes();
@ -945,7 +928,22 @@ RED.view = (function() {
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:RED.nodes.dirty()}); var historyEvent = {
t:'add',
links:[link],
dirty:RED.nodes.dirty()
};
if (activeSubflow) {
var subflowRefresh = RED.subflow.refresh(true);
if (subflowRefresh) {
historyEvent.subflow = {
id:activeSubflow.id,
changed: activeSubflow.changed,
instances: subflowRefresh.instances
}
}
}
RED.history.push(historyEvent);
updateActiveNodes(); updateActiveNodes();
RED.nodes.dirty(true); RED.nodes.dirty(true);
} else { } else {
@ -1695,6 +1693,10 @@ RED.view = (function() {
*/ */
function importNodes(newNodesStr,touchImport) { function importNodes(newNodesStr,touchImport) {
try { try {
var activeSubflowChanged;
if (activeSubflow) {
activeSubflowChanged = activeSubflow.changed;
}
var result = RED.nodes.import(newNodesStr,true); var result = RED.nodes.import(newNodesStr,true);
if (result) { if (result) {
var new_nodes = result[0]; var new_nodes = result[0];
@ -1752,14 +1754,25 @@ RED.view = (function() {
moving_set = new_ms; moving_set = new_ms;
} }
RED.history.push({ var historyEvent = {
t:'add', t:'add',
nodes:new_node_ids, nodes:new_node_ids,
links:new_links, links:new_links,
workspaces:new_workspaces, workspaces:new_workspaces,
subflows:new_subflows, subflows:new_subflows,
dirty:RED.nodes.dirty() dirty:RED.nodes.dirty()
}); };
if (activeSubflow) {
var subflowRefresh = RED.subflow.refresh(true);
if (subflowRefresh) {
historyEvent.subflow = {
id:activeSubflow.id,
changed: activeSubflowChanged,
instances: subflowRefresh.instances
}
}
}
RED.history.push(historyEvent);
updateActiveNodes(); updateActiveNodes();
redraw(); redraw();

View File

@ -84,13 +84,6 @@ RED.workspaces = (function() {
workspace_tabs = RED.tabs.create({ workspace_tabs = RED.tabs.create({
id: "workspace-tabs", id: "workspace-tabs",
onchange: function(tab) { onchange: function(tab) {
if (tab.type == "subflow") {
$("#chart").css({"margin-top": "40px"});
$("#workspace-toolbar").show();
} else {
$("#workspace-toolbar").hide();
$("#chart").css({"margin-top": "0"});
}
var event = { var event = {
old: activeWorkspace old: activeWorkspace
} }
@ -242,7 +235,7 @@ RED.workspaces = (function() {
if (!workspace_tabs.contains(id)) { if (!workspace_tabs.contains(id)) {
var sf = RED.nodes.subflow(id); var sf = RED.nodes.subflow(id);
if (sf) { if (sf) {
addWorkspace({type:"subflow",id:id,label:RED._("subflow.tabLabel",{name:sf.name}), closeable: true}); addWorkspace({type:"subflow",id:id,icon:"red/images/subflow_tab.png",label:sf.name, closeable: true});
} }
} }
workspace_tabs.activateTab(id); workspace_tabs.activateTab(id);
@ -250,7 +243,7 @@ RED.workspaces = (function() {
refresh: function() { refresh: function() {
RED.nodes.eachSubflow(function(sf) { RED.nodes.eachSubflow(function(sf) {
if (workspace_tabs.contains(sf.id)) { if (workspace_tabs.contains(sf.id)) {
workspace_tabs.renameTab(sf.id,RED._("subflow.tabLabel",{name:sf.name})); workspace_tabs.renameTab(sf.id,sf.name);
} }
}); });
}, },

View File

@ -47,7 +47,7 @@ span.logo {
font-size: 30px; font-size: 30px;
line-height: 30px; line-height: 30px;
text-decoration: none; text-decoration: none;
span { span {
vertical-align: middle; vertical-align: middle;
font-size: 16px !important; font-size: 16px !important;
@ -55,14 +55,14 @@ span.logo {
img { img {
height: 18px; height: 18px;
} }
a { a {
color: inherit; color: inherit;
&:hover { &:hover {
text-decoration: none; text-decoration: none;
} }
} }
} }
.header-toolbar { .header-toolbar {
@ -70,13 +70,13 @@ span.logo {
margin: 0; margin: 0;
list-style: none; list-style: none;
float: right; float: right;
> li { > li {
display: inline-block; display: inline-block;
padding: 0; padding: 0;
margin: 0; margin: 0;
position: relative; position: relative;
} }
} }
@ -97,19 +97,19 @@ span.logo {
vertical-align: middle; vertical-align: middle;
border-left: 2px solid #000; border-left: 2px solid #000;
border-right: 2px solid #000; border-right: 2px solid #000;
&:hover { &:hover {
border-color: $headerMenuItemHover; border-color: $headerMenuItemHover;
} }
} }
.button-group { #header .button-group {
display: inline-block; display: inline-block;
margin: auto 15px; margin: auto 15px;
vertical-align: middle; vertical-align: middle;
clear: both; clear: both;
} }
.button-group > a { #header .button-group > a {
display: inline-block; display: inline-block;
float: left; float: left;
line-height: 22px; line-height: 22px;
@ -122,11 +122,11 @@ span.logo {
.deploy-button { .deploy-button {
background: $deployButton; background: $deployButton;
color: #eee !important; color: #eee !important;
&:hover { &:hover {
background: $deployButtonHover; background: $deployButtonHover;
} }
&:active { &:active {
background: $deployButtonActive; background: $deployButtonActive;
color: #ccc !important; color: #ccc !important;
@ -134,18 +134,18 @@ span.logo {
} }
#btn-deploy { #btn-deploy {
padding: 4px 12px; padding: 4px 12px;
&.disabled { &.disabled {
cursor: default; cursor: default;
background: $deployDisabledButton; background: $deployDisabledButton;
color: #999 !important; color: #999 !important;
img { img {
opacity: 0.3; opacity: 0.3;
} }
&+ #btn-deploy-options { &+ #btn-deploy-options {
background: $deployDisabledButton; background: $deployDisabledButton;
color: #ddd; color: #ddd;
@ -157,7 +157,7 @@ span.logo {
background: $deployDisabledButton; background: $deployDisabledButton;
} }
} }
img { img {
margin-right: 8px; margin-right: 8px;
} }
@ -264,4 +264,3 @@ span.logo {
font-size: 16px; font-size: 16px;
color: #fff; color: #fff;
} }

View File

@ -21,13 +21,10 @@ ul.red-ui-tabs {
display: block; display: block;
height: 35px; height: 35px;
box-sizing:border-box; box-sizing:border-box;
white-space: nowrap;
border-bottom: 1px solid $primary-border-color; border-bottom: 1px solid $primary-border-color;
background: #fff; background: #fff;
-webkit-user-select: none; @include disable-selection;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
} }
ul.red-ui-tabs li { ul.red-ui-tabs li {
@ -110,3 +107,16 @@ ul.red-ui-tabs li.active {
ul.red-ui-tabs li.active a { ul.red-ui-tabs li.active a {
color: #333; color: #333;
} }
ul.red-ui-tabs li img {
margin-left: -8px;
margin-right: 3px;
margin-top: -2px;
opacity: 0.1;
width: 20px;
height: 20px;
vertical-align: middle;
}
ul.red-ui-tabs li.active img {
opacity: 0.2;
}

View File

@ -17,6 +17,9 @@
#workspace-toolbar { #workspace-toolbar {
display: none; display: none;
color: $workspace-button-color;
font-size: 12px;
line-height: 18px;
position: absolute; position: absolute;
top: 35px; top: 35px;
left:0; left:0;
@ -26,12 +29,46 @@
box-sizing: border-box; box-sizing: border-box;
background: #fff; background: #fff;
border-bottom: 1px solid $secondary-border-color; border-bottom: 1px solid $secondary-border-color;
} white-space: nowrap;
.button {
@include workspace-button;
margin-right: 10px;
padding: 2px 8px;
}
.button-group {
@include disable-selection;
.button:first-child {
margin-right: 0;
}
&:not(.spinner-group) {
.button:not(:first-child) {
border-left: none;
}
}
.button.active {
background: $workspace-button-background-active;
cursor: default;
}
}
.spinner-value {
width: 25px;
color: $workspace-button-color;
padding: 0 5px;
display: inline-block;
text-align: center;
border-top: 1px solid $secondary-border-color;
border-bottom: 1px solid $secondary-border-color;
margin: 0;
height: 24px;
font-size: 12px;
background: #f9f9f9;
line-height: 22px;
box-sizing: border-box;
}
#workspace-toolbar .button {
@include workspace-button;
line-height: 18px;
font-size: 12px;
margin-right: 5px;
padding: 2px 8px;
} }

View File

@ -58,12 +58,7 @@
<ul id="workspace-tabs"></ul> <ul id="workspace-tabs"></ul>
<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>
<a class="button" id="workspace-subflow-edit" href="#" data-i18n="[append]subflow.editSubflowName"><i class="fa fa-pencil"></i> </a>
<a class="button disabled" id="workspace-subflow-add-input" href="#" data-i18n="[append]subflow.input"><i class="fa fa-plus"></i> </a>
<a class="button" id="workspace-subflow-add-output" href="#" data-i18n="[append]subflow.output"><i class="fa fa-plus"></i> </a>
<a class="button" id="workspace-subflow-delete" href="#" data-i18n="[append]subflow.deleteSubflow"><i class="fa fa-trash"></i> </a>
</div>
<div id="workspace-footer"> <div id="workspace-footer">
<a class="workspace-footer-button" id="btn-zoom-out" href="#"><i class="fa fa-minus"></i></a> <a class="workspace-footer-button" id="btn-zoom-out" href="#"><i class="fa fa-minus"></i></a>
<a class="workspace-footer-button" id="btn-zoom-zero" href="#"><i class="fa fa-circle-o"></i></a> <a class="workspace-footer-button" id="btn-zoom-zero" href="#"><i class="fa fa-circle-o"></i></a>

View File

@ -94,14 +94,13 @@
} }
}, },
"subflow": { "subflow": {
"tabLabel": "Subflow: __name__",
"editSubflow": "Edit flow __name__", "editSubflow": "Edit flow __name__",
"edit": "Edit flow", "edit": "Edit flow",
"subflowInstances": "There is __count__ instance of this subflow", "subflowInstances": "There is __count__ instance of this subflow",
"subflowInstances_plural": "There are __count__ instances of this subflow", "subflowInstances_plural": "There are __count__ instances of this subflow",
"editSubflowName": "edit name", "editSubflowName": "edit name",
"input": "input", "input": "inputs:",
"output": "output", "output": "outputs:",
"deleteSubflow": "delete subflow", "deleteSubflow": "delete subflow",
"errors": { "errors": {
"noNodesSelected": "<strong>Cannot create subflow</strong>: no nodes selected", "noNodesSelected": "<strong>Cannot create subflow</strong>: no nodes selected",