From d7a10328c047b2d9bad9733c79f1c5d536fe7fdf Mon Sep 17 00:00:00 2001 From: NetHans Date: Sun, 14 Aug 2022 20:20:59 +0200 Subject: [PATCH 1/6] function for group analysis added --- .../@node-red/runtime/lib/flows/Flow.js | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index b5685d3ec..6350755c5 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -128,6 +128,65 @@ class Flow { } this.parent.log(msg); } + + /** + * Checks if node A and node B are in the same group. + * Node B can also be placed in a subgroup. + * If node A is not in any group, false is returned + * @param {[type]} nodeIdA [description] + * @param {[type]} nodeIdB [description] + * @returns {[type]} Returns true if all nodes are in the same group. If not, then false or if node A is not in a group then also false. + */ + isNodeInSameGroup(nodeIdA, nodeIdB) { + const groups = this.global.groups; + let result = false; + for(let key in groups) { + let group = groups[key]; + + if(!group.nodes.includes(nodeIdA.id)) { + continue; + } + + if(group.nodes.includes(nodeIdB.id)) { + result = true; + break; + } + + /** + * Subfunction to recursively search the groups for matches + * @param {[type]} targetNode [description] + * @param {[type]} targetGroup [description] + * @returns Returns true if a match was found. Otherwise false. + */ + const isInSubGroup = (targetNode, targetGroup) => { + let _result = false; + if(targetGroup.nodes.includes(targetNode.id)) { + _result = true; + } else { + for(let nodeId of targetGroup.nodes) { + let node = this.getGroupNode(nodeId); + + if(!node){ + continue; + } + + if(node.type === "group"){ + let result = isInSubGroup(targetNode, node); + if(result === true){ + _result = true; + break; + } + } + } + } + + return _result; + }; + + result = isInSubGroup(nodeIdB, group); + } + return result; + } /** * Start this flow. From 08ce6cce97f81a3db9d0ffe1c5f3c9297646ce07 Mon Sep 17 00:00:00 2001 From: NetHans Date: Sun, 14 Aug 2022 20:24:05 +0200 Subject: [PATCH 2/6] status node extended --- .../@node-red/nodes/core/common/25-status.html | 5 +++++ .../node_modules/@node-red/nodes/locales/de/messages.json | 1 + .../@node-red/nodes/locales/en-US/messages.json | 1 + packages/node_modules/@node-red/runtime/lib/flows/Flow.js | 8 +++++++- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/nodes/core/common/25-status.html b/packages/node_modules/@node-red/nodes/core/common/25-status.html index 47a3192e4..4c1b9c313 100644 --- a/packages/node_modules/@node-red/nodes/core/common/25-status.html +++ b/packages/node_modules/@node-red/nodes/core/common/25-status.html @@ -4,6 +4,7 @@ @@ -157,6 +158,8 @@ }); if (this.scope === null) { $("#node-input-scope-select").val("all"); + } else if(this.scope === "group"){ + $("#node-input-scope-select").val("group"); } else { $("#node-input-scope-select").val("target"); } @@ -166,6 +169,8 @@ var scope = $("#node-input-scope-select").val(); if (scope === 'all') { this.scope = null; + } else if(scope === 'group') { + this.scope = "group"; } else { this.scope = $("#node-input-status-target-container-div").treeList('selected').map(function(i) { return i.node.id}) } diff --git a/packages/node_modules/@node-red/nodes/locales/de/messages.json b/packages/node_modules/@node-red/nodes/locales/de/messages.json index 65f251e98..2ae7ba3b8 100755 --- a/packages/node_modules/@node-red/nodes/locales/de/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/de/messages.json @@ -110,6 +110,7 @@ }, "scope": { "all": "allen Nodes", + "group": "in gleicher Gruppe", "selected": "ausgewählten Nodes" } }, diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json index 62d5f351f..8318693b0 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json @@ -115,6 +115,7 @@ }, "scope": { "all": "all nodes", + "group": "in same group", "selected": "selected nodes" } }, diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 6350755c5..56c98be0e 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -665,10 +665,16 @@ class Flow { } handled = true; } else { + const flow = this; this.statusNodes.forEach(function(targetStatusNode) { - if (targetStatusNode.scope && targetStatusNode.scope.indexOf(reportingNode.id) === -1) { + if (Array.isArray(targetStatusNode.scope) && targetStatusNode.scope.indexOf(reportingNode.id) === -1) { return; } + + if (targetStatusNode.scope === "group" && flow.isNodeInSameGroup(targetStatusNode, reportingNode) === false){ + return; + } + var message = { status: clone(statusMessage) } From 371253a4f6fff9b11a83fcba0c323ac92e82f641 Mon Sep 17 00:00:00 2001 From: NetHans Date: Sun, 14 Aug 2022 20:28:34 +0200 Subject: [PATCH 3/6] catch node extended --- .../@node-red/nodes/core/common/25-catch.html | 5 +++++ .../@node-red/nodes/locales/de/messages.json | 1 + .../@node-red/nodes/locales/en-US/messages.json | 1 + .../node_modules/@node-red/runtime/lib/flows/Flow.js | 10 ++++++++-- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/common/25-catch.html b/packages/node_modules/@node-red/nodes/core/common/25-catch.html index 0b976ea78..4c591226d 100644 --- a/packages/node_modules/@node-red/nodes/core/common/25-catch.html +++ b/packages/node_modules/@node-red/nodes/core/common/25-catch.html @@ -4,6 +4,7 @@ @@ -170,6 +171,8 @@ }); if (this.scope === null) { $("#node-input-scope-select").val("all"); + } else if(this.scope === "group"){ + $("#node-input-scope-select").val("group"); } else { $("#node-input-scope-select").val("target"); } @@ -179,6 +182,8 @@ var scope = $("#node-input-scope-select").val(); if (scope === 'all') { this.scope = null; + } else if(scope === 'group') { + this.scope = "group"; } else { $("#node-input-uncaught").prop("checked",false); this.scope = $("#node-input-catch-target-container-div").treeList('selected').map(function(i) { return i.node.id}) diff --git a/packages/node_modules/@node-red/nodes/locales/de/messages.json b/packages/node_modules/@node-red/nodes/locales/de/messages.json index 2ae7ba3b8..edc881001 100755 --- a/packages/node_modules/@node-red/nodes/locales/de/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/de/messages.json @@ -98,6 +98,7 @@ }, "scope": { "all": "allen Nodes", + "group": "in gleicher Gruppe", "selected": "ausgewählten Nodes" } }, diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json index 8318693b0..b600bda91 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json @@ -103,6 +103,7 @@ }, "scope": { "all": "all nodes", + "group": "in same group", "selected": "selected nodes" } }, diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 56c98be0e..7df148b7f 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -734,11 +734,17 @@ class Flow { } else { var handledByUncaught = false; + const flow = this; this.catchNodes.forEach(function(targetCatchNode) { - if (targetCatchNode.scope && targetCatchNode.scope.indexOf(reportingNode.id) === -1) { + if (Array.isArray(targetCatchNode.scope) && targetCatchNode.scope.indexOf(reportingNode.id) === -1) { return; } - if (!targetCatchNode.scope && targetCatchNode.uncaught && !handledByUncaught) { + + if (targetCatchNode.scope === "group" && flow.isNodeInSameGroup(targetCatchNode, reportingNode) === false){ + return; + } + + if ((!targetCatchNode.scope || targetCatchNode.scope === "group") && targetCatchNode.uncaught && !handledByUncaught) { if (handled) { // This has been handled by a !uncaught catch node return; From b7a016edcfde73de6d309edbd50309e948143cdd Mon Sep 17 00:00:00 2001 From: NetHans Date: Tue, 16 Aug 2022 06:37:03 +0200 Subject: [PATCH 4/6] Update Flow.js replace tabs with whitespace --- .../@node-red/runtime/lib/flows/Flow.js | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 7df148b7f..4f87854ea 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -137,32 +137,32 @@ class Flow { * @param {[type]} nodeIdB [description] * @returns {[type]} Returns true if all nodes are in the same group. If not, then false or if node A is not in a group then also false. */ - isNodeInSameGroup(nodeIdA, nodeIdB) { - const groups = this.global.groups; + isNodeInSameGroup(nodeIdA, nodeIdB) { + const groups = this.global.groups; let result = false; - for(let key in groups) { - let group = groups[key]; + for(let key in groups) { + let group = groups[key]; - if(!group.nodes.includes(nodeIdA.id)) { - continue; - } + if(!group.nodes.includes(nodeIdA.id)) { + continue; + } - if(group.nodes.includes(nodeIdB.id)) { - result = true; + if(group.nodes.includes(nodeIdB.id)) { + result = true; break; - } - + } + /** * Subfunction to recursively search the groups for matches * @param {[type]} targetNode [description] * @param {[type]} targetGroup [description] * @returns Returns true if a match was found. Otherwise false. */ - const isInSubGroup = (targetNode, targetGroup) => { + const isInSubGroup = (targetNode, targetGroup) => { let _result = false; - if(targetGroup.nodes.includes(targetNode.id)) { - _result = true; - } else { + if(targetGroup.nodes.includes(targetNode.id)) { + _result = true; + } else { for(let nodeId of targetGroup.nodes) { let node = this.getGroupNode(nodeId); @@ -180,13 +180,13 @@ class Flow { } } - return _result; - }; - - result = isInSubGroup(nodeIdB, group); - } - return result; - } + return _result; + }; + + result = isInSubGroup(nodeIdB, group); + } + return result; + } /** * Start this flow. From e147602a3a587956cf8c42481d3e6a8c4e586223 Mon Sep 17 00:00:00 2001 From: NetHans Date: Thu, 18 Aug 2022 21:27:59 +0200 Subject: [PATCH 5/6] JSdoc documentation fixed --- .../node_modules/@node-red/runtime/lib/flows/Flow.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 4f87854ea..2616b8878 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -133,9 +133,9 @@ class Flow { * Checks if node A and node B are in the same group. * Node B can also be placed in a subgroup. * If node A is not in any group, false is returned - * @param {[type]} nodeIdA [description] - * @param {[type]} nodeIdB [description] - * @returns {[type]} Returns true if all nodes are in the same group. If not, then false or if node A is not in a group then also false. + * @param {Node} nodeIdA Node which defines the first search level + * @param {Node} nodeIdB Node which is to be searched in the group or a subgroup + * @returns {boolean} Returns true if all nodes are in the same group. If not, then false or if node A is not in a group then also false. */ isNodeInSameGroup(nodeIdA, nodeIdB) { const groups = this.global.groups; @@ -154,9 +154,9 @@ class Flow { /** * Subfunction to recursively search the groups for matches - * @param {[type]} targetNode [description] - * @param {[type]} targetGroup [description] - * @returns Returns true if a match was found. Otherwise false. + * @param {Node} Node which is to be searched in the group or a subgroup + * @param {Group} targetGroup group currently under analysis + * @returns {boolean} Returns true if a match was found. Otherwise false. */ const isInSubGroup = (targetNode, targetGroup) => { let _result = false; From 2388232179f59c71066f4cf5eaea881885946088 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 22 May 2023 22:33:31 +0100 Subject: [PATCH 6/6] Fix catch/status group scoping to handle group hierarchies --- .../@node-red/runtime/lib/flows/Flow.js | 83 ++++++++++++++----- .../@node-red/runtime/lib/flows/Flow_spec.js | 74 +++++++++++++++++ 2 files changed, 135 insertions(+), 22 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 0c0d1e61e..4430e59ed 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -665,16 +665,36 @@ class Flow { } handled = true; } else { - const flow = this; - this.statusNodes.forEach(function(targetStatusNode) { + const candidateNodes = []; + this.statusNodes.forEach(targetStatusNode => { + if (targetStatusNode.g && targetStatusNode.scope === 'group' && !reportingNode.g) { + // Status node inside a group, reporting node not in a group - skip it + return + } if (Array.isArray(targetStatusNode.scope) && targetStatusNode.scope.indexOf(reportingNode.id) === -1) { return; } - - if (targetStatusNode.scope === "group" && flow.isNodeInSameGroup(targetStatusNode, reportingNode) === false){ - return; + let distance = 0 + if (reportingNode.g) { + // Reporting node inside a group. Calculate the distance between it and the status node + let containingGroup = this.global.groups[reportingNode.g] + while (containingGroup && containingGroup.id !== targetStatusNode.g) { + distance++ + containingGroup = this.global.groups[containingGroup.g] + } + if (!containingGroup && targetStatusNode.g && targetStatusNode.scope === 'group') { + // This status node is in a group, but not in the same hierachy + // the reporting node is in + return + } } - + candidateNodes.push({ d: distance, n: targetStatusNode }) + }) + candidateNodes.sort((A,B) => { + return A.d - B.d + }) + candidateNodes.forEach(candidate => { + const targetStatusNode = candidate.n var message = { status: clone(statusMessage) } @@ -732,27 +752,46 @@ class Flow { } handled = true; } else { - var handledByUncaught = false; - - const flow = this; - this.catchNodes.forEach(function(targetCatchNode) { + const candidateNodes = []; + this.catchNodes.forEach(targetCatchNode => { + if (targetCatchNode.g && targetCatchNode.scope === 'group' && !reportingNode.g) { + // Catch node inside a group, reporting node not in a group - skip it + return + } if (Array.isArray(targetCatchNode.scope) && targetCatchNode.scope.indexOf(reportingNode.id) === -1) { + // Catch node has a scope set and it doesn't include the reporting node return; } - - if (targetCatchNode.scope === "group" && flow.isNodeInSameGroup(targetCatchNode, reportingNode) === false){ - return; - } - - if ((!targetCatchNode.scope || targetCatchNode.scope === "group") && targetCatchNode.uncaught && !handledByUncaught) { - if (handled) { - // This has been handled by a !uncaught catch node - return; + let distance = 0 + if (reportingNode.g) { + // Reporting node inside a group. Calculate the distance between it and the catch node + let containingGroup = this.global.groups[reportingNode.g] + while (containingGroup && containingGroup.id !== targetCatchNode.g) { + distance++ + containingGroup = this.global.groups[containingGroup.g] + } + if (!containingGroup && targetCatchNode.g && targetCatchNode.scope === 'group') { + // This catch node is in a group, but not in the same hierachy + // the reporting node is in + return } - // This is an uncaught error - handledByUncaught = true; } - var errorMessage; + candidateNodes.push({ d: distance, n: targetCatchNode }) + }) + candidateNodes.sort((A,B) => { + return A.d - B.d + }) + let handledByUncaught = false + candidateNodes.forEach(candidate => { + const targetCatchNode = candidate.n + if (targetCatchNode.uncaught && !handledByUncaught) { + // This node only wants errors that haven't already been handled + if (handled) { + return + } + handledByUncaught = true + } + let errorMessage; if (msg) { errorMessage = redUtil.cloneMessage(msg); } else { diff --git a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js index ca30868d7..6d53747ef 100644 --- a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js @@ -686,6 +686,44 @@ describe('Flow', function() { },50); }); + it.only("passes a status event to the group scoped status node",function(done) { + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id: "g1", type: "group", g: "g3" }, + {id: "g2", type: "group" }, + {id: "g3", type: "group" }, + {id:"1",x:10,y:10,z:"t1",g:"g1", type:"test",name:"a",wires:["2"]}, + // sn - in the same group as source node + {id:"sn",x:10,y:10,z:"t1",g:"g1", type:"status",scope:"group",wires:[]}, + // sn2 - in a different group hierarchy to the source node + {id:"sn2",x:10,y:10,z:"t1", g:"g2", type:"status",scope:"group",wires:[]}, + // sn3 - in a higher-level group to the source node + {id:"sn3",x:10,y:10,z:"t1", g:"g3", type:"status",scope:"group",wires:[]}, + // sn2 - in a different group hierarchy, but not scope to the group + {id:"sn4",x:10,y:10,z:"t1", g:"g2", type:"status",wires:[]}, + + ]); + var flow = Flow.create({},config,config.flows["t1"]); + + flow.start(); + + var activeNodes = flow.getActiveNodes(); + flow.handleStatus(config.flows["t1"].nodes["1"],{text:"my-status"}); + setTimeout(function() { + try { + currentNodes["sn"].should.have.a.property("handled",1); + currentNodes["sn2"].should.have.a.property("handled",0); + currentNodes["sn3"].should.have.a.property("handled",1); + currentNodes["sn3"].should.have.a.property("handled",1); + done() + } catch(err) { + done(err) + } + },50); + }); + + + }); describe("#handleError",function() { @@ -796,6 +834,42 @@ describe('Flow', function() { },50); },50); }); + it("passes an error event to the group scoped catch node",function(done) { + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id: "g1", type: "group", g: "g3" }, + {id: "g2", type: "group" }, + {id: "g3", type: "group" }, + {id:"1",x:10,y:10,z:"t1",g:"g1", type:"test",name:"a",wires:["2"]}, + // sn - in the same group as source node + {id:"sn",x:10,y:10,z:"t1",g:"g1", type:"catch",scope:"group",wires:[]}, + // sn2 - in a different group hierarchy to the source node + {id:"sn2",x:10,y:10,z:"t1", g:"g2", type:"catch",scope:"group",wires:[]}, + // sn3 - in a higher-level group to the source node + {id:"sn3",x:10,y:10,z:"t1", g:"g3", type:"catch",scope:"group",wires:[]}, + // sn2 - in a different group hierarchy, but not scope to the group + {id:"sn4",x:10,y:10,z:"t1", g:"g2", type:"catch",wires:[]}, + + ]); + var flow = Flow.create({},config,config.flows["t1"]); + + flow.start(); + + var activeNodes = flow.getActiveNodes(); + + flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo"}); + setTimeout(function() { + try { + currentNodes["sn"].should.have.a.property("handled",1); + currentNodes["sn2"].should.have.a.property("handled",0); + currentNodes["sn3"].should.have.a.property("handled",1); + currentNodes["sn3"].should.have.a.property("handled",1); + done() + } catch(err) { + done(err) + } + },50); + }); it("moves any existing error object sideways",function(done){ var config = flowUtils.parseConfig([ {id:"t1",type:"tab"},