pull/5329/merge
Gauthier Dandele 2026-03-24 10:15:08 -04:00 committed by GitHub
commit 9c526e23b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 139 additions and 35 deletions

View File

@ -12,6 +12,14 @@
<input type="checkbox" id="node-input-uncaught" style="display: inline-block; width: auto; vertical-align: top; margin-left: 30px; margin-right: 5px;">
<label for="node-input-uncaught" style="width: auto" data-i18n="catch.label.uncaught"></label>
</div>
<div class="form-row node-input-anyError-row">
<input type="checkbox" id="node-input-anyError" style="display: inline-block; width: auto; vertical-align: top; margin-left: 30px; margin-right: 5px;">
<label for="node-input-anyError" style="width: auto" data-i18n="catch.label.anyError"></label>
</div>
<div class="form-row node-input-includeConfig-row">
<input type="checkbox" id="node-input-includeConfig" style="display: inline-block; width: auto; vertical-align: top; margin-left: 30px; margin-right: 5px;">
<label for="node-input-includeConfig" style="width: auto" data-i18n="catch.label.includeConfig"></label>
</div>
<div class="form-row node-input-target-row">
<button type="button" id="node-input-catch-target-select" class="red-ui-button" data-i18n="common.label.selectNodes"></button>
</div>
@ -30,9 +38,11 @@
category: 'common',
color:"#e49191",
defaults: {
name: {value:""},
scope: {value:null, type:"*[]"},
uncaught: {value:false}
name: { value: "" },
scope: { value: null, type: "*[]" },
uncaught: { value: false },
anyError: { value: false },
includeConfig: { value: false }
},
inputs:0,
outputs:1,
@ -80,29 +90,64 @@
}
}
});
const sidebar = $("#red-ui-sidebar-config-category-global");
var dirList = $("#node-input-catch-target-container-div").css({width: "100%", height: "100%"})
.treeList({multi:true}).on("treelistitemmouseover", function(e, item) {
item.node.highlighted = true;
item.node.dirty = true;
RED.view.redraw();
if (item.node) {
if (!item.node.x && !item.node.y) {
sidebar.find(".red-ui-palette-node_id_" + item.node.id).children("div").addClass("highlighted");
} else {
item.node.highlighted = true;
item.node.dirty = true;
RED.view.redraw();
}
}
}).on("treelistitemmouseout", function(e, item) {
item.node.highlighted = false;
item.node.dirty = true;
RED.view.redraw();
if (item.node) {
if (!item.node.x && !item.node.y) {
sidebar.find(".red-ui-palette-node_id_" + item.node.id).children("div").removeClass("highlighted");
} else {
item.node.highlighted = false;
item.node.dirty = true;
RED.view.redraw();
}
}
})
var candidateNodes = RED.nodes.filterNodes({z:node.z});
var allChecked = true;
var items = [];
var nodeItemMap = {};
const flowItems = [];
const flowItemMap = {};
let activeWorkspace = RED.nodes.workspace(node.z);
activeWorkspace = activeWorkspace || RED.nodes.subflow(node.z);
flowItemMap[activeWorkspace.id] = {
id: activeWorkspace.id,
class: "red-ui-palette-header",
label: activeWorkspace.label || activeWorkspace.name || activeWorkspace.id,
expanded: true,
children: []
};
flowItemMap.global = {
id: "global",
class: "red-ui-palette-header",
label: this._("catch.label.globalConfig"),
expanded: true,
children: []
};
const nodeItemMap = {};
const configNodeSet = new Set();
const candidateNodes = RED.nodes.filterNodes({ z: node.z });
RED.nodes.eachConfig((cn) => {
// Add config nodes with the same or global scope
if (cn.z === node.z || !cn.z) {
configNodeSet.add(cn);
candidateNodes.push(cn);
}
});
candidateNodes.forEach(function(n) {
if (n.id === node.id) {
if (n.id === node.id || n.type === "global-config") {
return;
}
var isChecked = scope.indexOf(n.id) !== -1;
allChecked = allChecked && isChecked;
var nodeDef = RED.nodes.getType(n.type);
var label;
var sublabel;
@ -119,21 +164,42 @@
if (!nodeDef || !label) {
label = n.type;
}
const isChecked = scope.indexOf(n.id) !== -1;
const isConfigNode = (nodeDef && nodeDef.category === "config") || (!nodeDef && !n.x && !n.y);
nodeItemMap[n.id] = {
node: n,
label: label,
sublabel: sublabel,
selected: isChecked,
checkbox: true
checkbox: true,
icon: isConfigNode ? "fa fa-cog" : ""
};
items.push(nodeItemMap[n.id]);
if (!n.z && isConfigNode) {
flowItemMap.global.children.push(nodeItemMap[n.id]);
} else {
flowItemMap[activeWorkspace.id].children.push(nodeItemMap[n.id]);
}
});
flowItems.push(flowItemMap[activeWorkspace.id], flowItemMap.global);
$("#node-input-includeConfig").on("change", function () {
const checked = $(this).prop("checked");
if (checked) {
dirList.treeList('data', flowItems);
} else {
// Remove config nodes from the list
const filteredItems = flowItemMap[activeWorkspace.id].children
.filter((e) => !configNodeSet.has(e.node));
dirList.treeList('data', filteredItems);
}
});
dirList.treeList('data',items);
$("#node-input-catch-target-select").on("click", function(e) {
e.preventDefault();
var preselected = dirList.treeList('selected').map(function(n) {return n.node.id});
RED.tray.hide();
// TODO: extend selection to config nodes too
RED.view.selectNodes({
selected: preselected,
onselect: function(selection) {
@ -169,6 +235,13 @@
$(".node-input-target-row").hide();
$(".node-input-uncaught-row").show();
}
if (scope === "group") {
$(".node-input-anyError-row").hide();
$(".node-input-includeConfig-row").hide();
} else {
$(".node-input-anyError-row").show();
$(".node-input-includeConfig-row").show();
}
node._resize();
});
if (this.scope === null) {

View File

@ -14,19 +14,20 @@
* limitations under the License.
**/
module.exports = function(RED) {
module.exports = function (RED) {
"use strict";
function CatchNode(n) {
RED.nodes.createNode(this,n);
var node = this;
RED.nodes.createNode(this, n);
this.scope = n.scope;
this.uncaught = n.uncaught;
this.on("input",function(msg, send, done) {
this.anyError = n.anyError;
this.includeConfig = n.includeConfig;
this.on("input", function (msg, send, done) {
send(msg);
done();
});
}
RED.nodes.registerType("catch",CatchNode);
RED.nodes.registerType("catch", CatchNode);
}

View File

@ -100,7 +100,10 @@
"label": {
"source": "Catch errors from",
"selectAll": "select all",
"uncaught": "Ignore errors handled by other Catch nodes"
"uncaught": "Ignore errors handled by other Catch nodes",
"anyError": "Allow any error other than message processing",
"includeConfig": "Catch errors from config nodes too",
"globalConfig": "Global config nodes"
},
"scope": {
"all": "all nodes",

View File

@ -98,14 +98,17 @@
"catchNodes": "catch : __number__",
"catchUncaught": "catch : non capturé",
"label": {
"source": "Détecter les erreurs de",
"source": "Capturer les erreurs ",
"selectAll": "Tout sélectionner",
"uncaught": "Ignorer les erreurs gérées par les autres noeuds Catch"
"uncaught": "Ignorer les erreurs gérées par les autres noeuds Catch",
"anyError": "Autoriser toute erreur autre que le traitement des messages",
"includeConfig": "Capturer également les erreurs des noeuds de configuration",
"globalConfig": "Noeuds de configuration globale"
},
"scope": {
"all": "tous les noeuds",
"all": "de tous les noeuds",
"group": "dans le même groupe",
"selected": "noeuds sélectionnés"
"selected": "des noeuds sélectionnés"
}
},
"status": {

View File

@ -26,6 +26,9 @@ let Subflow;
let Log;
let Group;
/** @type {Record<string, Flow>} */
let activeFlows;
let nodeCloseTimeout = 15000;
let asyncMessageDelivery = true;
@ -620,9 +623,25 @@ class Flow {
handled = node.users[userNode]._flow.handleError(node,logMessage,msg,node.users[userNode]) || handled;
}
}
} else if (this.id === 'global') {
for (const flow of Object.values(activeFlows)) {
if (flow.id === 'global') {
continue;
}
handled = flow.handleError(node, logMessage, msg) || handled;
}
} else {
const candidateNodes = [];
const isConfigNode = !!node._flow?.flow.configs[node.id];
this.catchNodes.forEach(targetCatchNode => {
if (msg === null && !targetCatchNode.anyError) {
// Skip if the catch node only accepts errors produced by message processing
return;
}
if (isConfigNode && !targetCatchNode.includeConfig) {
// Skip if the catch node only accepts errors produced by non config nodes
return;
}
if (targetCatchNode.g && targetCatchNode.scope === 'group' && !reportingNode.g) {
// Catch node inside a group, reporting node not in a group - skip it
return
@ -860,6 +879,9 @@ module.exports = {
Subflow = require("./Subflow");
Group = require("./Group").Group
},
setActiveFlows: function (flows) {
activeFlows = flows;
},
create: function(parent,global,conf) {
return new Flow(parent,global,conf)
},

View File

@ -70,6 +70,7 @@ function init(runtime) {
typeEventRegistered = true;
}
Flow.init(runtime);
Flow.setActiveFlows(activeFlows);
flowUtil.init(runtime);
}

View File

@ -566,9 +566,10 @@ Node.prototype.error = function(logMessage,msg) {
if (typeof logMessage != 'boolean') {
logMessage = logMessage || "";
}
var handled = false;
if (this._flow && msg && typeof msg === 'object') {
handled = this._flow.handleError(this,logMessage,msg);
let handled = false;
const shouldHandle = (msg && typeof msg === 'object') || msg === undefined;
if (this._flow && shouldHandle) {
handled = this._flow.handleError(this, logMessage, msg || null);
}
if (!handled) {
log_helper(this, Log.ERROR, logMessage);

View File

@ -69,7 +69,7 @@ describe('function node', function() {
});
it('should do something with the catch node', function(done) {
var flow = [{"id":"funcNode","type":"function","wires":[["goodNode"]],"func":"node.error('This is an error', msg);"},{"id":"goodNode","type":"helper"},{"id":"badNode","type":"helper"},{"id":"catchNode","type":"catch","scope":null,"uncaught":false,"wires":[["badNode"]]}];
var flow = [{"id":"flow","type":"tab"},{"id":"funcNode","type":"function","wires":[["goodNode"]],"func":"node.error('This is an error', msg);","x":0,"y":0,"z":"flow"},{"id":"goodNode","type":"helper","z":"flow"},{"id":"badNode","type":"helper","z":"flow"},{"id":"catchNode","type":"catch","scope":null,"uncaught":false,"wires":[["badNode"]],"z":"flow"}];
var catchNodeModule = require("nr-test-utils").require("@node-red/nodes/core/common/25-catch.js")
helper.load([catchNodeModule, functionNode], flow, function() {
var funcNode = helper.getNode("funcNode");