From 3bcff91328ef4de44643c7ceb9d93745b5fd13ae Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 1 Feb 2019 23:44:50 +0000 Subject: [PATCH] Add Status Node to Subflow to allow subflow-specific status Closes #597 --- .../editor-client/locales/en-US/editor.json | 1 + .../@node-red/editor-client/src/js/history.js | 29 +++- .../@node-red/editor-client/src/js/nodes.js | 31 ++++ .../editor-client/src/js/ui/subflow.js | 135 ++++++++++++--- .../@node-red/editor-client/src/js/ui/view.js | 86 +++++++++- .../src/sass/workspaceToolbar.scss | 9 + .../@node-red/runtime/lib/nodes/flows/Flow.js | 1 - .../runtime/lib/nodes/flows/Subflow.js | 88 +++++++++- .../runtime/lib/nodes/flows/Subflow_spec.js | 161 +++++++++++++++++- 9 files changed, 500 insertions(+), 41 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 1fff8b87e..6a06d3094 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -273,6 +273,7 @@ "editSubflowProperties": "edit properties", "input": "inputs:", "output": "outputs:", + "status": "status node", "deleteSubflow": "delete subflow", "info": "Description", "category": "Category", diff --git a/packages/node_modules/@node-red/editor-client/src/js/history.js b/packages/node_modules/@node-red/editor-client/src/js/history.js index 95cc78292..2e256564c 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/history.js +++ b/packages/node_modules/@node-red/editor-client/src/js/history.js @@ -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(); } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index d9ef6ab44..b531f89f0 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -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(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js index ba303152f..eb179c205 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js @@ -16,7 +16,6 @@ RED.subflow = (function() { - var _subflowEditTemplate = ''; var _subflowTemplateEditTemplate = ''; - - 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 ').appendTo(toolbar); + + // Inputs $(' '+ '
'+ '0'+ '1'+ '
').appendTo(toolbar); + // Outputs $('
'+ ''+ '
3
'+ ''+ '
').appendTo(toolbar); + // Status + $('').appendTo(toolbar); + // $(' ').appendTo(toolbar); // $(' ').appendTo(toolbar); + + // Delete $(' ').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 } })(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 9405b60dd..4eca5b1c5 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -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}); diff --git a/packages/node_modules/@node-red/editor-client/src/sass/workspaceToolbar.scss b/packages/node_modules/@node-red/editor-client/src/sass/workspaceToolbar.scss index 26d45bcf8..3224707fe 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/workspaceToolbar.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/workspaceToolbar.scss @@ -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; diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/nodes/flows/Flow.js index 7ca6afb77..ca555a5a5 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/flows/Flow.js @@ -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`. diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/flows/Subflow.js b/packages/node_modules/@node-red/runtime/lib/nodes/flows/Subflow.js index cf393868e..24ae72a49 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/flows/Subflow.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/flows/Subflow.js @@ -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