mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	Add Status Node to Subflow to allow subflow-specific status
Closes #597
This commit is contained in:
		| @@ -273,6 +273,7 @@ | ||||
|         "editSubflowProperties": "edit properties", | ||||
|         "input": "inputs:", | ||||
|         "output": "outputs:", | ||||
|         "status": "status node", | ||||
|         "deleteSubflow": "delete subflow", | ||||
|         "info": "Description", | ||||
|         "category": "Category", | ||||
|   | ||||
| @@ -125,14 +125,20 @@ 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 (ev.subflow) { | ||||
|                     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; | ||||
|                             } | ||||
|                         }); | ||||
|                     } | ||||
|                     if (ev.subflow.hasOwnProperty('status')) { | ||||
|                         subflow = RED.nodes.subflow(ev.subflow.id); | ||||
|                         subflow.status = ev.subflow.status; | ||||
|                     } | ||||
|                 } | ||||
|                 if (subflow) { | ||||
|                     RED.nodes.filterNodes({type:"subflow:"+subflow.id}).forEach(function(n) { | ||||
| @@ -232,6 +238,11 @@ RED.history = (function() { | ||||
|                             } | ||||
|                         }); | ||||
|                     } | ||||
|                     if (ev.subflow.hasOwnProperty('status')) { | ||||
|                         if (ev.subflow.status) { | ||||
|                             delete ev.node.status; | ||||
|                         } | ||||
|                     } | ||||
|                     RED.editor.validateNode(ev.node); | ||||
|                     RED.nodes.filterNodes({type:"subflow:"+ev.node.id}).forEach(function(n) { | ||||
|                         n.inputs = ev.node.in.length; | ||||
| @@ -290,6 +301,7 @@ RED.history = (function() { | ||||
|                     RED.workspaces.order(ev.order); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             Object.keys(modifiedTabs).forEach(function(id) { | ||||
|                 var subflow = RED.nodes.subflow(id); | ||||
|                 if (subflow) { | ||||
| @@ -303,6 +315,7 @@ RED.history = (function() { | ||||
|             RED.palette.refresh(); | ||||
|             RED.workspaces.refresh(); | ||||
|             RED.sidebar.config.refresh(); | ||||
|             RED.subflow.refresh(); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|   | ||||
| @@ -571,6 +571,18 @@ RED.nodes = (function() { | ||||
|                 node.icon = n.icon; | ||||
|             } | ||||
|         } | ||||
|         if (n.status) { | ||||
|             node.status = {x: n.status.x, y: n.status.y, wires:[]}; | ||||
|             links.forEach(function(d) { | ||||
|                 if (d.target === n.status) { | ||||
|                     if (d.source.type != "subflow") { | ||||
|                         node.status.wires.push({id:d.source.id, port:d.sourcePort}) | ||||
|                     } else { | ||||
|                         node.status.wires.push({id:n.id, port:0}) | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         return node; | ||||
|     } | ||||
| @@ -851,6 +863,12 @@ RED.nodes = (function() { | ||||
|                         output.i = i; | ||||
|                         output.id = getID(); | ||||
|                     }); | ||||
|                     if (n.status) { | ||||
|                         n.status.type = "subflow"; | ||||
|                         n.status.direction = "status"; | ||||
|                         n.status.z = n.id; | ||||
|                         n.status.id = getID(); | ||||
|                     } | ||||
|                     new_subflows.push(n); | ||||
|                     addSubflow(n,createNewIds); | ||||
|                 } | ||||
| @@ -1189,6 +1207,19 @@ RED.nodes = (function() { | ||||
|                 }); | ||||
|                 delete output.wires; | ||||
|             }); | ||||
|             if (n.status) { | ||||
|                 n.status.wires.forEach(function(wire) { | ||||
|                     var link; | ||||
|                     if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) { | ||||
|                         link = {source:n.in[wire.port], sourcePort:wire.port,target:n.status}; | ||||
|                     } else { | ||||
|                         link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:n.status}; | ||||
|                     } | ||||
|                     addLink(link); | ||||
|                     new_links.push(link); | ||||
|                 }); | ||||
|                 delete n.status.wires; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         RED.workspaces.refresh(); | ||||
|   | ||||
| @@ -16,7 +16,6 @@ | ||||
|  | ||||
| RED.subflow = (function() { | ||||
|  | ||||
|  | ||||
|     var _subflowEditTemplate = '<script type="text/x-red" data-template-name="subflow"><div class="form-row"><label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label><input type="text" id="node-input-name"></div></script>'; | ||||
|     var _subflowTemplateEditTemplate = '<script type="text/x-red" data-template-name="subflow-template">'+ | ||||
|         '<div class="form-row"><i class="fa fa-tag"></i> <label for="subflow-input-name" data-i18n="common.label.name"></label><input type="text" id="subflow-input-name"></div>'+ | ||||
| @@ -24,26 +23,22 @@ RED.subflow = (function() { | ||||
|         '<div class="form-row form-tips" id="subflow-dialog-user-count"></div>'+ | ||||
|         '</script>'; | ||||
|  | ||||
|  | ||||
|     function getSubflow() { | ||||
|         return RED.nodes.subflow(RED.workspaces.active()); | ||||
|     } | ||||
|  | ||||
|     function findAvailableSubflowIOPosition(subflow,isInput) { | ||||
|         var pos = {x:50,y:30}; | ||||
|         if (!isInput) { | ||||
|             pos.x += 110; | ||||
|         } | ||||
|         for (var i=0;i<subflow.out.length+subflow.in.length;i++) { | ||||
|             var port; | ||||
|             if (i < subflow.out.length) { | ||||
|                 port = subflow.out[i]; | ||||
|             } else { | ||||
|                 port = subflow.in[i-subflow.out.length]; | ||||
|             } | ||||
|         var ports = [].concat(subflow.out).concat(subflow.in); | ||||
|         if (subflow.status) { | ||||
|             ports.push(subflow.status); | ||||
|         } | ||||
|         ports.sort(function(A,B) { | ||||
|             return A.x-B.x; | ||||
|         }); | ||||
|         for (var i=0; i<ports.length; i++) { | ||||
|             var port = ports[i]; | ||||
|             if (port.x == pos.x && port.y == pos.y) { | ||||
|                 pos.x += 55; | ||||
|                 i=0; | ||||
|             } | ||||
|         } | ||||
|         return pos; | ||||
| @@ -191,6 +186,61 @@ RED.subflow = (function() { | ||||
|         return {subflowOutputs: removedSubflowOutputs, links: removedLinks} | ||||
|     } | ||||
|  | ||||
|     function addSubflowStatus() { | ||||
|         var subflow = RED.nodes.subflow(RED.workspaces.active()); | ||||
|         if (subflow.status) { | ||||
|             return; | ||||
|         } | ||||
|         var position = findAvailableSubflowIOPosition(subflow,false); | ||||
|         var statusNode = { | ||||
|             type:"subflow", | ||||
|             direction:"status", | ||||
|             z:subflow.id, | ||||
|             x:position.x, | ||||
|             y:position.y, | ||||
|             id:RED.nodes.id() | ||||
|         }; | ||||
|         subflow.status = statusNode; | ||||
|         subflow.dirty = true; | ||||
|         var wasDirty = RED.nodes.dirty(); | ||||
|         var wasChanged = subflow.changed; | ||||
|         subflow.changed = true; | ||||
|         var result = refresh(true); | ||||
|         var historyEvent = { | ||||
|             t:'edit', | ||||
|             node:subflow, | ||||
|             dirty:wasDirty, | ||||
|             changed:wasChanged, | ||||
|             subflow: { status: true } | ||||
|         }; | ||||
|         RED.history.push(historyEvent); | ||||
|         RED.view.select(); | ||||
|         RED.nodes.dirty(true); | ||||
|         RED.view.redraw(); | ||||
|         $("#workspace-subflow-status").prop("checked",!!subflow.status); | ||||
|         $("#workspace-subflow-status").parent().parent().toggleClass("active",!!subflow.status); | ||||
|     } | ||||
|  | ||||
|     function removeSubflowStatus() { | ||||
|         var subflow = RED.nodes.subflow(RED.workspaces.active()); | ||||
|         if (!subflow.status) { | ||||
|             return; | ||||
|         } | ||||
|         var subflowRemovedLinks = []; | ||||
|         RED.nodes.eachLink(function(l) { | ||||
|             if (l.target.type == "subflow" && l.target.z == subflow.id && l.target.direction == "status") { | ||||
|                 subflowRemovedLinks.push(l); | ||||
|             } | ||||
|         }); | ||||
|         subflowRemovedLinks.forEach(function(l) { RED.nodes.removeLink(l)}); | ||||
|         delete subflow.status; | ||||
|  | ||||
|         $("#workspace-subflow-status").prop("checked",!!subflow.status); | ||||
|         $("#workspace-subflow-status").parent().parent().toggleClass("active",!!subflow.status); | ||||
|  | ||||
|         return { links: subflowRemovedLinks } | ||||
|     } | ||||
|  | ||||
|     function refresh(markChange) { | ||||
|         var activeSubflow = RED.nodes.subflow(RED.workspaces.active()); | ||||
|         refreshToolbar(activeSubflow); | ||||
| @@ -219,12 +269,17 @@ RED.subflow = (function() { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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").text(activeSubflow.out.length); | ||||
|  | ||||
|             $("#workspace-subflow-status").prop("checked",!!activeSubflow.status); | ||||
|             $("#workspace-subflow-status").parent().parent().toggleClass("active",!!activeSubflow.status); | ||||
|  | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -232,22 +287,32 @@ RED.subflow = (function() { | ||||
|         var toolbar = $("#workspace-toolbar"); | ||||
|         toolbar.empty(); | ||||
|  | ||||
|         // Edit properties | ||||
|         $('<a class="button" id="workspace-subflow-edit" href="#" data-i18n="[append]subflow.editSubflowProperties"><i class="fa fa-pencil"></i> </a>').appendTo(toolbar); | ||||
|  | ||||
|         // Inputs | ||||
|         $('<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); | ||||
|  | ||||
|         // Outputs | ||||
|         $('<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); | ||||
|  | ||||
|         // Status | ||||
|         $('<span class="button-group"><span class="button" style="padding:0"><label for="workspace-subflow-status"><input id="workspace-subflow-status" type="checkbox"> <span data-i18n="subflow.status"></span></label></span></span>').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); | ||||
|  | ||||
|         // Delete | ||||
|         $('<a class="button" id="workspace-subflow-delete" href="#" data-i18n="[append]subflow.deleteSubflow"><i class="fa fa-trash"></i> </a>').appendTo(toolbar); | ||||
|  | ||||
|         toolbar.i18n(); | ||||
|  | ||||
|  | ||||
| @@ -274,6 +339,7 @@ RED.subflow = (function() { | ||||
|                 RED.view.redraw(true); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         $("#workspace-subflow-output-add").click(function(event) { | ||||
|             event.preventDefault(); | ||||
|             addSubflowOutput(); | ||||
| @@ -283,6 +349,7 @@ RED.subflow = (function() { | ||||
|             event.preventDefault(); | ||||
|             addSubflowInput(); | ||||
|         }); | ||||
|  | ||||
|         $("#workspace-subflow-input-remove").click(function(event) { | ||||
|             event.preventDefault(); | ||||
|             var wasDirty = RED.nodes.dirty(); | ||||
| @@ -307,6 +374,33 @@ RED.subflow = (function() { | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         $("#workspace-subflow-status").change(function(evt) { | ||||
|             if (this.checked) { | ||||
|                 addSubflowStatus(); | ||||
|             } else { | ||||
|                 var currentStatus = activeSubflow.status; | ||||
|                 var wasChanged = activeSubflow.changed; | ||||
|                 var result = removeSubflowStatus(); | ||||
|                 if (result) { | ||||
|                     activeSubflow.changed = true; | ||||
|                     var wasDirty = RED.nodes.dirty(); | ||||
|                     RED.history.push({ | ||||
|                         t:'delete', | ||||
|                         links:result.links, | ||||
|                         changed: wasChanged, | ||||
|                         dirty:wasDirty, | ||||
|                         subflow: { | ||||
|                             id: activeSubflow.id, | ||||
|                             status: currentStatus | ||||
|                         } | ||||
|                     }); | ||||
|                     RED.view.select(); | ||||
|                     RED.nodes.dirty(true); | ||||
|                     RED.view.redraw(); | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         $("#workspace-subflow-edit").click(function(event) { | ||||
|             RED.editor.editSubflow(RED.nodes.subflow(RED.workspaces.active())); | ||||
|             event.preventDefault(); | ||||
| @@ -328,6 +422,7 @@ RED.subflow = (function() { | ||||
|         $("#chart").css({"margin-top": "40px"}); | ||||
|         $("#workspace-toolbar").show(); | ||||
|     } | ||||
|  | ||||
|     function hideWorkspaceToolbar() { | ||||
|         $("#workspace-toolbar").hide().empty(); | ||||
|         $("#chart").css({"margin-top": "0"}); | ||||
| @@ -373,6 +468,7 @@ RED.subflow = (function() { | ||||
|             subflows: [activeSubflow] | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function init() { | ||||
|         RED.events.on("workspace:change",function(event) { | ||||
|             var activeSubflow = RED.nodes.subflow(event.workspace); | ||||
| @@ -436,6 +532,7 @@ RED.subflow = (function() { | ||||
|         } | ||||
|         return x; | ||||
|     } | ||||
|  | ||||
|     function convertToSubflow() { | ||||
|         var selection = RED.view.selection(); | ||||
|         if (!selection.nodes) { | ||||
| @@ -451,7 +548,6 @@ RED.subflow = (function() { | ||||
|         var candidateOutputs = []; | ||||
|         var candidateInputNodes = {}; | ||||
|  | ||||
|  | ||||
|         var boundingBox = [selection.nodes[0].x, | ||||
|             selection.nodes[0].y, | ||||
|             selection.nodes[0].x, | ||||
| @@ -467,8 +563,8 @@ RED.subflow = (function() { | ||||
|                 Math.max(boundingBox[3],n.y) | ||||
|             ] | ||||
|         } | ||||
|         var offsetX = snapToGrid(boundingBox[0] - 180); | ||||
|         var offsetY = snapToGrid(boundingBox[1] - 60); | ||||
|         var offsetX = snapToGrid(boundingBox[0] - 200); | ||||
|         var offsetY = snapToGrid(boundingBox[1] - 80); | ||||
|  | ||||
|  | ||||
|         var center = [ | ||||
| @@ -643,8 +739,6 @@ RED.subflow = (function() { | ||||
|         RED.view.redraw(true); | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|     return { | ||||
|         init: init, | ||||
|         createSubflow: createSubflow, | ||||
| @@ -652,6 +746,7 @@ RED.subflow = (function() { | ||||
|         removeSubflow: removeSubflow, | ||||
|         refresh: refresh, | ||||
|         removeInput: removeSubflowInput, | ||||
|         removeOutput: removeSubflowOutput | ||||
|         removeOutput: removeSubflowOutput, | ||||
|         removeStatus: removeSubflowStatus | ||||
|     } | ||||
| })(); | ||||
|   | ||||
| @@ -1261,6 +1261,13 @@ RED.view = (function() { | ||||
|                         moving_set.push({n:n}); | ||||
|                     } | ||||
|                 }); | ||||
|                 if (activeSubflow.status) { | ||||
|                     activeSubflow.status.selected = (activeSubflow.status.x > x && activeSubflow.status.x < x2 && activeSubflow.status.y > y && activeSubflow.status.y < y2); | ||||
|                     if (activeSubflow.status.selected) { | ||||
|                         activeSubflow.status.dirty = true; | ||||
|                         moving_set.push({n:activeSubflow.status}); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             updateSelection(); | ||||
|             lasso.remove(); | ||||
| @@ -1367,6 +1374,13 @@ RED.view = (function() { | ||||
|                     moving_set.push({n:n}); | ||||
|                 } | ||||
|             }); | ||||
|             if (activeSubflow.status) { | ||||
|                 if (!activeSubflow.status.selected) { | ||||
|                     activeSubflow.status.selected = true; | ||||
|                     activeSubflow.status.dirty = true; | ||||
|                     moving_set.push({n:activeSubflow.status}); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         selected_link = null; | ||||
| @@ -1552,6 +1566,7 @@ RED.view = (function() { | ||||
|             var removedLinks = []; | ||||
|             var removedSubflowOutputs = []; | ||||
|             var removedSubflowInputs = []; | ||||
|             var removedSubflowStatus = undefined; | ||||
|             var subflowInstances = []; | ||||
|  | ||||
|             var startDirty = RED.nodes.dirty(); | ||||
| @@ -1573,6 +1588,8 @@ RED.view = (function() { | ||||
|                             removedSubflowOutputs.push(node); | ||||
|                         } else if (node.direction === "in") { | ||||
|                             removedSubflowInputs.push(node); | ||||
|                         } else if (node.direction === "status") { | ||||
|                             removedSubflowStatus = node; | ||||
|                         } | ||||
|                         node.dirty = true; | ||||
|                     } | ||||
| @@ -1590,12 +1607,19 @@ RED.view = (function() { | ||||
|                         removedLinks = removedLinks.concat(result.links); | ||||
|                     } | ||||
|                 } | ||||
|                 if (removedSubflowStatus) { | ||||
|                     result = RED.subflow.removeStatus(); | ||||
|                     if (result) { | ||||
|                         removedLinks = removedLinks.concat(result.links); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 var instances = RED.subflow.refresh(true); | ||||
|                 if (instances) { | ||||
|                     subflowInstances = instances.instances; | ||||
|                 } | ||||
|                 moving_set = []; | ||||
|                 if (removedNodes.length > 0 || removedSubflowOutputs.length > 0 || removedSubflowInputs.length > 0) { | ||||
|                 if (removedNodes.length > 0 || removedSubflowOutputs.length > 0 || removedSubflowInputs.length > 0 || removedSubflowStatus) { | ||||
|                     RED.nodes.dirty(true); | ||||
|                 } | ||||
|             } | ||||
| @@ -1651,10 +1675,14 @@ RED.view = (function() { | ||||
|                     subflowOutputs:removedSubflowOutputs, | ||||
|                     subflowInputs:removedSubflowInputs, | ||||
|                     subflow: { | ||||
|                         id: activeSubflow?activeSubflow.id:undefined, | ||||
|                         instances: subflowInstances | ||||
|                     }, | ||||
|                     dirty:startDirty | ||||
|                 }; | ||||
|                 if (removedSubflowStatus) { | ||||
|                     historyEvent.subflow.status = removedSubflowStatus; | ||||
|                 } | ||||
|             } | ||||
|             RED.history.push(historyEvent); | ||||
|  | ||||
| @@ -2420,6 +2448,49 @@ RED.view = (function() { | ||||
|  | ||||
|                 inGroup.append("svg:text").attr("class","port_label").attr("x",18).attr("y",20).style("font-size","10px").text("input"); | ||||
|  | ||||
|                 var subflowStatus = nodeLayer.selectAll(".subflowstatus").data(activeSubflow.status?[activeSubflow.status]:[],function(d,i){ return d.id;}); | ||||
|                 subflowStatus.exit().remove(); | ||||
|  | ||||
|                 var statusGroup = subflowStatus.enter().insert("svg:g").attr("class","node subflowstatus").attr("transform",function(d) { return "translate("+(d.x-20)+","+(d.y-20)+")"}); | ||||
|                 statusGroup.each(function(d,i) { | ||||
|                     d.w=40; | ||||
|                     d.h=40; | ||||
|                 }); | ||||
|                 statusGroup.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); | ||||
|                     }); | ||||
|  | ||||
|                 statusGroup.append("g").attr('transform','translate(-5,15)').append("rect").attr("class","port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10) | ||||
|                     .on("mousedown", function(d,i){portMouseDown(d,PORT_TYPE_INPUT,0);} ) | ||||
|                     .on("touchstart", function(d,i){portMouseDown(d,PORT_TYPE_INPUT,0);} ) | ||||
|                     .on("mouseup", function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);}) | ||||
|                     .on("touchend",function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);} ) | ||||
|                     .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);}) | ||||
|                     .on("mouseout",function(d){portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);}); | ||||
|  | ||||
|                 statusGroup.append("svg:text").attr("class","port_label").attr("x",22).attr("y",20).style("font-size","10px").text("status"); | ||||
|  | ||||
|                 subflowOutputs.each(function(d,i) { | ||||
|                     if (d.dirty) { | ||||
|                         var output = d3.select(this); | ||||
| @@ -2439,9 +2510,22 @@ RED.view = (function() { | ||||
|                         d.dirty = false; | ||||
|                     } | ||||
|                 }); | ||||
|                 subflowStatus.each(function(d,i) { | ||||
|                     if (d.dirty) { | ||||
|                         var output = d3.select(this); | ||||
|                         output.selectAll(".subflowport").classed("node_selected",function(d) { return d.selected; }) | ||||
|                         output.selectAll(".port_index").text(function(d){ return d.i+1}); | ||||
|                         output.attr("transform", function(d) { return "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"; }); | ||||
|                         dirtyNodes[d.id] = d; | ||||
|                         d.dirty = false; | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|  | ||||
|             } else { | ||||
|                 nodeLayer.selectAll(".subflowoutput").remove(); | ||||
|                 nodeLayer.selectAll(".subflowinput").remove(); | ||||
|                 nodeLayer.selectAll(".subflowstatus").remove(); | ||||
|             } | ||||
|  | ||||
|             var node = nodeLayer.selectAll(".nodegroup").data(activeNodes,function(d){return d.id}); | ||||
|   | ||||
| @@ -33,6 +33,15 @@ | ||||
|     transition: right 0.2s ease; | ||||
|     overflow: hidden; | ||||
|  | ||||
|     label { | ||||
|         padding: 1px 8px; | ||||
|         margin: 0; | ||||
|         font-size: 12px; | ||||
|     } | ||||
|     input[type="checkbox"] { | ||||
|         margin: 0 3px 0 0 ; | ||||
|         padding: 0; | ||||
|     } | ||||
|     .button { | ||||
|         @include workspace-button; | ||||
|         margin-right: 10px; | ||||
|   | ||||
| @@ -265,7 +265,6 @@ class Flow { | ||||
|         return Promise.all(promises); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Update the flow definition. This doesn't change anything that is running. | ||||
|      * This should be called after `stop` and before `start`. | ||||
|   | ||||
| @@ -17,6 +17,8 @@ | ||||
| const clone = require("clone"); | ||||
| const Flow = require('./Flow').Flow; | ||||
|  | ||||
| const util = require("util"); | ||||
|  | ||||
| const redUtil = require("@node-red/util").util; | ||||
| const flowUtil = require("./util"); | ||||
|  | ||||
| @@ -104,6 +106,40 @@ class Subflow extends Flow { | ||||
|         var self = this; | ||||
|         // Create a subflow node to accept inbound messages and route appropriately | ||||
|         var Node = require("../Node"); | ||||
|  | ||||
|         if (this.subflowDef.status) { | ||||
|             var subflowStatusConfig = { | ||||
|                 id: this.subflowInstance.id+":status", | ||||
|                 type: "subflow-status", | ||||
|                 z: this.subflowInstance.id, | ||||
|                 _flow: this.parent | ||||
|             } | ||||
|             this.statusNode = new Node(subflowStatusConfig); | ||||
|             this.statusNode.on("input", function(msg) { | ||||
|                 if (msg.payload !== undefined) { | ||||
|                     if (typeof msg.payload === "string") { | ||||
|                         // if msg.payload is a String, use it as status text | ||||
|                         self.node.status({text:msg.payload}) | ||||
|                         return; | ||||
|                     } else if (Object.prototype.toString.call(msg.payload) === "[object Object]") { | ||||
|                         if (msg.payload.hasOwnProperty('text') || msg.payload.hasOwnProperty('fill') || msg.payload.hasOwnProperty('shape') || Object.keys(msg.payload).length === 0) { | ||||
|                             // msg.payload is an object that looks like a status object | ||||
|                             self.node.status(msg.payload); | ||||
|                             return; | ||||
|                         } | ||||
|                     } | ||||
|                     // Anything else - inspect it and use as status text | ||||
|                     var text = util.inspect(msg.payload); | ||||
|                     if (text.length > 32) { text = text.substr(0,32) + "..."; } | ||||
|                     self.node.status({text:text}); | ||||
|                 } else if (msg.status !== undefined) { | ||||
|                     // if msg.status exists | ||||
|                     self.node.status(msg.status) | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|  | ||||
|         var subflowInstanceConfig = { | ||||
|             id: this.subflowInstance.id, | ||||
|             type: this.subflowInstance.type, | ||||
| @@ -168,7 +204,6 @@ class Subflow extends Flow { | ||||
|  | ||||
|         // Wire the subflow outputs | ||||
|         if (this.subflowDef.out) { | ||||
|             var modifiedNodes = {}; | ||||
|             for (var i=0;i<this.subflowDef.out.length;i++) { | ||||
|                 // i: the output index | ||||
|                 // This is what this Output is wired to | ||||
| @@ -180,7 +215,6 @@ class Subflow extends Flow { | ||||
|                         this.node._updateWires(subflowInstanceConfig.wires); | ||||
|                     } else { | ||||
|                         var node = self.node_map[wires[j].id]; | ||||
|                         modifiedNodes[node.id] = node; | ||||
|                         if (!node._originalWires) { | ||||
|                             node._originalWires = clone(node.wires); | ||||
|                         } | ||||
| @@ -189,9 +223,47 @@ class Subflow extends Flow { | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (this.subflowDef.status) { | ||||
|             var subflowStatusId = this.statusNode.id; | ||||
|             wires = this.subflowDef.status.wires; | ||||
|             for (var j=0;j<wires.length;j++) { | ||||
|                 if (wires[j].id === this.subflowDef.id) { | ||||
|                     // A subflow input wired straight to a subflow output | ||||
|                     subflowInstanceConfig.wires[wires[j].port].push(subflowStatusId); | ||||
|                     this.node._updateWires(subflowInstanceConfig.wires); | ||||
|                 } else { | ||||
|                     var node = self.node_map[wires[j].id]; | ||||
|                     if (!node._originalWires) { | ||||
|                         node._originalWires = clone(node.wires); | ||||
|                     } | ||||
|                     node.wires[wires[j].port] = (node.wires[wires[j].port]||[]); | ||||
|                     node.wires[wires[j].port].push(subflowStatusId); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         super.start(diff); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a node instance from this subflow. | ||||
|      * If the subflow has a status node, check for that, otherwise use | ||||
|      * the super-class function | ||||
|      * @param  {String} id [description] | ||||
|      * @param  {Boolean} cancelBubble    if true, prevents the flow from passing the request to the parent | ||||
|      *                                   This stops infinite loops when the parent asked this Flow for the | ||||
|      *                                   node to begin with. | ||||
|      * @return {[type]}    [description] | ||||
|      */ | ||||
|     getNode(id, cancelBubble) { | ||||
|         if (this.statusNode && this.statusNode.id === id) { | ||||
|             return this.statusNode; | ||||
|         } | ||||
|         return super.getNode(id,cancelBubble); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Handle a status event from a node within this flow. | ||||
|      * @param  {Node}    node          The original node that triggered the event | ||||
| @@ -205,10 +277,14 @@ class Subflow extends Flow { | ||||
|     handleStatus(node,statusMessage,reportingNode,muteStatus) { | ||||
|         let handled = super.handleStatus(node,statusMessage,reportingNode,muteStatus); | ||||
|         if (!handled) { | ||||
|             // No status node on this subflow caught the status message. | ||||
|             // Pass up to the parent with this subflow's instance as the | ||||
|             // reporting node | ||||
|             handled = this.parent.handleStatus(node,statusMessage,this.node,true); | ||||
|             if (!this.statusNode || node === this.node) { | ||||
|                 // No status node on this subflow caught the status message. | ||||
|                 // AND there is no Subflow Status node - so the user isn't | ||||
|                 // wanting to manage status messages themselves | ||||
|                 // Pass up to the parent with this subflow's instance as the | ||||
|                 // reporting node | ||||
|                 handled = this.parent.handleStatus(node,statusMessage,this.node,true); | ||||
|             } | ||||
|         } | ||||
|         return handled; | ||||
|  | ||||
|   | ||||
| @@ -265,7 +265,6 @@ describe('Subflow', function() { | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("instantiates a subflow inside a subflow and stops it",function(done) { | ||||
|             var config = flowUtils.parseConfig([ | ||||
|                 {id:"t1",type:"tab"}, | ||||
| @@ -419,7 +418,6 @@ describe('Subflow', function() { | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("passes a status event to the subflow's parent tab status node - targetted scope",function(done) { | ||||
|             var config = flowUtils.parseConfig([ | ||||
|                 {id:"t1",type:"tab"}, | ||||
| @@ -457,9 +455,164 @@ describe('Subflow', function() { | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     describe("status node", function() { | ||||
|         it("emits a status event when a message is passed to a subflow-status node - msg.payload as string", function(done) { | ||||
|             var config = flowUtils.parseConfig([ | ||||
|                 {id:"t1",type:"tab"}, | ||||
|                 {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, | ||||
|                 {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, | ||||
|                 {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, | ||||
|                 { | ||||
|                     id:"sf1", | ||||
|                     type:"subflow", | ||||
|                     name:"Subflow 2", | ||||
|                     info:"", | ||||
|                     in:[{wires:[{id:"sf1-1"}]}], | ||||
|                     out:[{wires:[{id:"sf1-1",port:0}]}], | ||||
|                     status:{wires:[{id:"sf1-1", port:0}]} | ||||
|                 }, | ||||
|                 {id:"sf1-1",type:"test",name:"test","z":"sf1",x:166,y:99,"wires":[[]]}, | ||||
|                 {id:"sn",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]} | ||||
|             ]); | ||||
|             var flow = Flow.create({},config,config.flows["t1"]); | ||||
|  | ||||
|             flow.start(); | ||||
|  | ||||
|             var activeNodes = flow.getActiveNodes(); | ||||
|  | ||||
|             activeNodes["1"].receive({payload:"test-payload"}); | ||||
|  | ||||
|             currentNodes["sn"].should.have.a.property("handled",1); | ||||
|             var statusMessage = currentNodes["sn"].messages[0]; | ||||
|  | ||||
|             statusMessage.should.have.a.property("status"); | ||||
|             statusMessage.status.should.have.a.property("text","test-payload"); | ||||
|             statusMessage.status.should.have.a.property("source"); | ||||
|             statusMessage.status.source.should.have.a.property("id","2"); | ||||
|             statusMessage.status.source.should.have.a.property("type","subflow:sf1"); | ||||
|  | ||||
|             flow.stop().then(function() { | ||||
|  | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|         it("emits a status event when a message is passed to a subflow-status node - msg.payload as status obj", function(done) { | ||||
|             var config = flowUtils.parseConfig([ | ||||
|                 {id:"t1",type:"tab"}, | ||||
|                 {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, | ||||
|                 {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, | ||||
|                 {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, | ||||
|                 { | ||||
|                     id:"sf1", | ||||
|                     type:"subflow", | ||||
|                     name:"Subflow 2", | ||||
|                     info:"", | ||||
|                     in:[{wires:[{id:"sf1-1"}]}], | ||||
|                     out:[{wires:[{id:"sf1-1",port:0}]}], | ||||
|                     status:{wires:[{id:"sf1-1", port:0}]} | ||||
|                 }, | ||||
|                 {id:"sf1-1",type:"test",name:"test","z":"sf1",x:166,y:99,"wires":[[]]}, | ||||
|                 {id:"sn",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]} | ||||
|             ]); | ||||
|             var flow = Flow.create({},config,config.flows["t1"]); | ||||
|  | ||||
|             flow.start(); | ||||
|  | ||||
|             var activeNodes = flow.getActiveNodes(); | ||||
|  | ||||
|             activeNodes["1"].receive({payload:{text:"payload-obj"}}); | ||||
|  | ||||
|             currentNodes["sn"].should.have.a.property("handled",1); | ||||
|             var statusMessage = currentNodes["sn"].messages[0]; | ||||
|  | ||||
|             statusMessage.should.have.a.property("status"); | ||||
|             statusMessage.status.should.have.a.property("text","payload-obj"); | ||||
|             statusMessage.status.should.have.a.property("source"); | ||||
|             statusMessage.status.source.should.have.a.property("id","2"); | ||||
|             statusMessage.status.source.should.have.a.property("type","subflow:sf1"); | ||||
|  | ||||
|             flow.stop().then(function() { | ||||
|  | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|         it("emits a status event when a message is passed to a subflow-status node - msg.status", function(done) { | ||||
|             var config = flowUtils.parseConfig([ | ||||
|                 {id:"t1",type:"tab"}, | ||||
|                 {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, | ||||
|                 {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, | ||||
|                 {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, | ||||
|                 { | ||||
|                     id:"sf1", | ||||
|                     type:"subflow", | ||||
|                     name:"Subflow 2", | ||||
|                     info:"", | ||||
|                     in:[{wires:[{id:"sf1-1"}]}], | ||||
|                     out:[{wires:[{id:"sf1-1",port:0}]}], | ||||
|                     status:{wires:[{id:"sf1-1", port:0}]} | ||||
|                 }, | ||||
|                 {id:"sf1-1",type:"test",name:"test","z":"sf1",x:166,y:99,"wires":[[]]}, | ||||
|                 {id:"sn",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]} | ||||
|             ]); | ||||
|             var flow = Flow.create({},config,config.flows["t1"]); | ||||
|  | ||||
|             flow.start(); | ||||
|  | ||||
|             var activeNodes = flow.getActiveNodes(); | ||||
|  | ||||
|             activeNodes["1"].receive({status:{text:"status-obj"}}); | ||||
|  | ||||
|             currentNodes["sn"].should.have.a.property("handled",1); | ||||
|             var statusMessage = currentNodes["sn"].messages[0]; | ||||
|  | ||||
|             statusMessage.should.have.a.property("status"); | ||||
|             statusMessage.status.should.have.a.property("text","status-obj"); | ||||
|             statusMessage.status.should.have.a.property("source"); | ||||
|             statusMessage.status.source.should.have.a.property("id","2"); | ||||
|             statusMessage.status.source.should.have.a.property("type","subflow:sf1"); | ||||
|  | ||||
|             flow.stop().then(function() { | ||||
|  | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|         it("does not emit a regular status event if it contains a subflow-status node", function(done) { | ||||
|             var config = flowUtils.parseConfig([ | ||||
|                 {id:"t1",type:"tab"}, | ||||
|                 {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, | ||||
|                 {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, | ||||
|                 {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, | ||||
|                 { | ||||
|                     id:"sf1", | ||||
|                     type:"subflow", | ||||
|                     name:"Subflow 2", | ||||
|                     info:"", | ||||
|                     in:[{wires:[{id:"sf1-1"}]}], | ||||
|                     out:[{wires:[{id:"sf1-1",port:0}]}], | ||||
|                     status:{wires:[]} | ||||
|                 }, | ||||
|                 {id:"sf1-1",type:"testStatus",name:"test-status-node","z":"sf1",x:166,y:99,"wires":[[]]}, | ||||
|                 {id:"sn",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]} | ||||
|             ]); | ||||
|             var flow = Flow.create({},config,config.flows["t1"]); | ||||
|  | ||||
|             flow.start(); | ||||
|  | ||||
|             var activeNodes = flow.getActiveNodes(); | ||||
|  | ||||
|             activeNodes["1"].receive({payload:"test-payload"}); | ||||
|  | ||||
|             currentNodes["sn"].should.have.a.property("handled",0); | ||||
|  | ||||
|             flow.stop().then(function() { | ||||
|  | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|     }) | ||||
|  | ||||
|     describe("#handleError",function() { | ||||
|         it("passes an error event to the subflow's parent tab catch node - all scope",function(done) { | ||||
|             var config = flowUtils.parseConfig([ | ||||
| @@ -493,7 +646,6 @@ describe('Subflow', function() { | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("passes an error event to the subflow's parent tab catch node - targetted scope",function(done) { | ||||
|             var config = flowUtils.parseConfig([ | ||||
|                 {id:"t1",type:"tab"}, | ||||
| @@ -530,7 +682,6 @@ describe('Subflow', function() { | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user