From ced0b306320a30b2ce2172e09c2e164aa9b76f90 Mon Sep 17 00:00:00 2001 From: GogoVega <92022724+GogoVega@users.noreply.github.com> Date: Thu, 16 Oct 2025 17:06:05 +0200 Subject: [PATCH 1/7] Fixe a typo in the variable name --- packages/node_modules/@node-red/editor-client/src/js/comms.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/comms.js b/packages/node_modules/@node-red/editor-client/src/js/comms.js index 2af8b69a6..a58ce8f7c 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/comms.js +++ b/packages/node_modules/@node-red/editor-client/src/js/comms.js @@ -120,8 +120,8 @@ RED.comms = (function() { subscribers[i](msg.topic,msg.data); } catch (error) { // need to decide what to do with this uncaught error - console.warn('Uncaught error from RED.comms.subscribe: ' + err.toString()) - console.warn(err) + console.warn('Uncaught error from RED.comms.subscribe: ' + error.toString()) + console.warn(error) } } } From 25e92c350137429da8e189075eb798544465d6d9 Mon Sep 17 00:00:00 2001 From: Marek Serafin Date: Sat, 18 Oct 2025 18:04:06 +0200 Subject: [PATCH 2/7] Fix race condition in projects initialization Add missing return statement for gitTools.init() promise to ensure activeProject is set before getFlows() is called during startup. Fixes intermittent 'No active project' warnings when projects feature is enabled. --- .../runtime/lib/storage/localfilesystem/projects/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js index ca87d76e9..bb8b7861e 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js @@ -92,7 +92,7 @@ function init(_settings, _runtime) { if (projectsEnabled) { return sshTools.init(settings,runtime).then(function() { - gitTools.init(_settings).then(function(gitConfig) { + return gitTools.init(_settings).then(function(gitConfig) { if (!gitConfig || /^1\./.test(gitConfig.version)) { if (!gitConfig) { projectLogMessages.push(log._("storage.localfilesystem.projects.git-not-found")) From 6bb32775baca8a7d4bb531e8757ca521ca662230 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 23 Oct 2025 13:52:44 +0100 Subject: [PATCH 3/7] Clear suggestions on node/port mouse down Closes #5244 --- .../@node-red/editor-client/src/js/ui/view.js | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) 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 89019005f..4f40c4a31 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 @@ -105,6 +105,7 @@ RED.view = (function() { let suggestedLinks = []; let suggestedJunctions = []; + let forceFullRedraw = false // Note: these are the permitted status colour aliases. The actual RGB values // are set in the CSS - flow.scss/colors.scss const status_colours = { @@ -3215,6 +3216,7 @@ RED.view = (function() { function portMouseDown(d,portType,portIndex, evt) { if (RED.view.DEBUG) { console.warn("portMouseDown", mouse_mode,d,portType,portIndex); } + clearSuggestedFlow(); RED.contextMenu.hide(); evt = evt || d3.event; if (evt === 1) { @@ -3799,6 +3801,7 @@ RED.view = (function() { } function nodeMouseDown(d) { if (RED.view.DEBUG) { console.warn("nodeMouseDown", mouse_mode,d); } + clearSuggestedFlow() focusView(); RED.contextMenu.hide(); if (d3.event.button === 1) { @@ -4429,8 +4432,8 @@ RED.view = (function() { outer.attr("width", space_width*scaleFactor).attr("height", space_height*scaleFactor); // Don't bother redrawing nodes if we're drawing links - if (showAllLinkPorts !== -1 || mouse_mode != RED.state.JOINING) { - + if (forceFullRedraw || showAllLinkPorts !== -1 || mouse_mode != RED.state.JOINING) { + forceFullRedraw = false var dirtyNodes = {}; if (activeSubflow) { @@ -6720,7 +6723,6 @@ RED.view = (function() { refreshSuggestedFlow(); } else { // Anything else; clear the suggestion clearSuggestedFlow(); - RED.view.redraw(true); // manually push the event to the keyboard handler RED.keyboard.handle(evt) } @@ -6729,7 +6731,6 @@ RED.view = (function() { if (suggestion.clickToApply) { $(window).on('mousedown.suggestedFlow', function (evnt) { clearSuggestedFlow(); - RED.view.redraw(true); }) } } @@ -6750,12 +6751,16 @@ RED.view = (function() { } function clearSuggestedFlow () { - $(window).off('mousedown.suggestedFlow'); - $(window).off('keydown.suggestedFlow') - RED.keyboard.enable() - currentSuggestion = null - suggestedNodes = [] - suggestedLinks = [] + if (currentSuggestion) { + $(window).off('mousedown.suggestedFlow'); + $(window).off('keydown.suggestedFlow') + RED.keyboard.enable() + currentSuggestion = null + suggestedNodes = [] + suggestedLinks = [] + forceFullRedraw = true + RED.view.redraw(true); + } } function applySuggestedFlow () { From fe4d40a7769dd1114f515a01e14b3c0a0e093d31 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 23 Oct 2025 15:37:07 +0100 Subject: [PATCH 4/7] Show subflow input label on virtual port --- .../@node-red/editor-client/src/js/history.js | 10 +- .../src/js/ui/editors/panes/appearance.js | 3 + .../@node-red/editor-client/src/js/ui/view.js | 385 +++++++++--------- 3 files changed, 201 insertions(+), 197 deletions(-) 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 142606822..08e83ee87 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 @@ -514,7 +514,15 @@ RED.history = (function() { } } } - + if (ev.node.type === 'subflow') { + // Ensure ports get a refresh in case of a label change + if (ev.changes.inputLabels) { + ev.node.in.forEach(function(input) { input.dirty = true; }); + } + if (ev.changes.outputLabels) { + ev.node.out.forEach(function(output) { output.dirty = true; }); + } + } ev.node.dirty = true; ev.node.changed = ev.changed; diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/appearance.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/appearance.js index d6dd5112d..2d225a277 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/appearance.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/appearance.js @@ -489,6 +489,9 @@ changes.inputLabels = node.inputLabels; node.inputLabels = newValue; changed = true; + if (node.type === "subflow") { + node.in[0].dirty = true + } } hasNonBlankLabel = false; newValue = new Array(node.outputs); 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 89019005f..56cbd4399 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 @@ -4424,6 +4424,186 @@ RED.view = (function() { } } + function buildSubflowPort (d) { + const NODE_TYPE = d.direction === "in" ? PORT_TYPE_INPUT : PORT_TYPE_OUTPUT; + // PORT_TYPE is the 'opposite' of NODE_TYPE + const PORT_TYPE = NODE_TYPE === PORT_TYPE_INPUT ? PORT_TYPE_OUTPUT : PORT_TYPE_INPUT; + var node = d3.select(this); + var nodeContents = document.createDocumentFragment(); + + d.h = 40; + d.resize = true; + d.dirty = true; + + var mainRect = document.createElementNS("http://www.w3.org/2000/svg","rect"); + mainRect.__data__ = d; + mainRect.setAttribute("class", "red-ui-flow-subflow-port"); + mainRect.setAttribute("rx", 8); + mainRect.setAttribute("ry", 8); + mainRect.setAttribute("width", 40); + mainRect.setAttribute("height", 40); + node[0][0].__mainRect__ = mainRect; + d3.select(mainRect) + .on("mouseup",nodeMouseUp) + .on("mousedown",nodeMouseDown) + .on("touchstart",nodeTouchStart) + .on("touchend",nodeTouchEnd) + nodeContents.appendChild(mainRect); + + const port_label_group = document.createElementNS("http://www.w3.org/2000/svg","g"); + port_label_group.setAttribute("x",0); + port_label_group.setAttribute("y",0); + node[0][0].__portLabelGroup__ = port_label_group; + + const port_label = document.createElementNS("http://www.w3.org/2000/svg","text"); + port_label.setAttribute("class","red-ui-flow-port-label"); + port_label.style["font-size"] = "10px"; + port_label.textContent = NODE_TYPE === PORT_TYPE_INPUT? "input" : "output"; + port_label_group.appendChild(port_label); + node[0][0].__portLabel__ = port_label; + + if (NODE_TYPE === PORT_TYPE_OUTPUT) { + const port_number = document.createElementNS("http://www.w3.org/2000/svg","text"); + port_number.setAttribute("class","red-ui-flow-port-label red-ui-flow-port-index"); + port_number.setAttribute("x",0); + port_number.setAttribute("y",0); + port_number.textContent = d.i+1; + port_label_group.appendChild(port_number); + node[0][0].__portNumber__ = port_number; + } + + const port_border = document.createElementNS("http://www.w3.org/2000/svg","path"); + port_border.setAttribute("d","M 40 1 l 0 38") + port_border.setAttribute("class", "red-ui-flow-node-icon-shade-border") + port_label_group.appendChild(port_border); + node[0][0].__portBorder__ = port_border; + + nodeContents.appendChild(port_label_group); + + var text = document.createElementNS("http://www.w3.org/2000/svg","g"); + text.setAttribute("class","red-ui-flow-port-label"); + text.setAttribute("transform","translate(38,0)"); + text.setAttribute('style', 'fill : #888'); // hard coded here! + node[0][0].__textGroup__ = text; + nodeContents.append(text); + + var portEl = document.createElementNS("http://www.w3.org/2000/svg","g"); + portEl.setAttribute('transform','translate(-5,15)') + + var port = document.createElementNS("http://www.w3.org/2000/svg","rect"); + port.setAttribute("class","red-ui-flow-port"); + port.setAttribute("rx",3); + port.setAttribute("ry",3); + port.setAttribute("width",10); + port.setAttribute("height",10); + portEl.appendChild(port); + port.__data__ = d; + + d3.select(port) + .on("mousedown", function(d,i){portMouseDown(d,PORT_TYPE,0);} ) + .on("touchstart", function(d,i){portMouseDown(d,PORT_TYPE,0);d3.event.preventDefault();} ) + .on("mouseup", function(d,i){portMouseUp(d,PORT_TYPE,0);}) + .on("touchend",function(d,i){portMouseUp(d,PORT_TYPE,0);d3.event.preventDefault();} ) + .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE,0);}) + .on("mouseout",function(d){portMouseOut(d3.select(this),d,PORT_TYPE,0);}); + + node[0][0].__port__ = portEl + nodeContents.appendChild(portEl); + node[0][0].appendChild(nodeContents); + } + function updateSubflowPort (d) { + if (d.dirty) { + const port_height = 40; + const NODE_TYPE = d.direction === "in" ? PORT_TYPE_INPUT : PORT_TYPE_OUTPUT; + // PORT_TYPE is the 'opposite' of NODE_TYPE + const PORT_TYPE = NODE_TYPE === PORT_TYPE_INPUT ? PORT_TYPE_OUTPUT : PORT_TYPE_INPUT; + + var label = getPortLabel(activeSubflow, NODE_TYPE, d.i) || ""; + var hideLabel = (label.length < 1) + var labelParts; + if (d.resize || this.__hideLabel__ !== hideLabel || this.__label__ !== label) { + labelParts = getLabelParts(label, "red-ui-flow-node-label"); + if (labelParts.lines.length !== this.__labelLineCount__ || this.__label__ !== label) { + d.resize = true; + } + this.__label__ = label; + this.__labelLineCount__ = labelParts.lines.length; + + if (hideLabel) { + d.h = Math.max(port_height,(d.outputs || 0) * 15); + } else { + d.h = Math.max(6+24*labelParts.lines.length,(d.outputs || 0) * 15, port_height); + } + this.__hideLabel__ = hideLabel; + } + + if (d.resize) { + var ow = d.w; + if (hideLabel) { + d.w = port_height; + } else { + d.w = Math.max(port_height,20*(Math.ceil((labelParts.width+50+7)/20)) ); + } + if (ow !== undefined) { + d.x += (d.w-ow)/2; + } + d.resize = false; + } + + this.setAttribute("transform", "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"); + // This might be the first redraw after a node has been click-dragged to start a move. + // So its selected state might have changed since the last redraw. + this.classList.toggle("red-ui-flow-node-selected", !!d.selected ) + if (mouse_mode != RED.state.MOVING_ACTIVE) { + this.classList.toggle("red-ui-flow-node-disabled", d.d === true); + this.__mainRect__.setAttribute("width", d.w) + this.__mainRect__.setAttribute("height", d.h) + this.__mainRect__.classList.toggle("red-ui-flow-node-highlighted",!!d.highlighted ); + + if (labelParts) { + // The label has changed + var sa = labelParts.lines; + var sn = labelParts.lines.length; + var textLines = this.__textGroup__.childNodes; + while(textLines.length > sn) { + textLines[textLines.length-1].remove(); + } + for (var i=0; i sn) { - textLines[textLines.length-1].remove(); - } - for (var i=0; i Date: Wed, 29 Oct 2025 15:29:35 +0000 Subject: [PATCH 5/7] Fix lock icon for read-only user --- .../node_modules/@node-red/editor-client/src/js/ui/deploy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js index d318f476c..d929e2560 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js @@ -180,7 +180,7 @@ RED.deploy = (function() { } function updateLockedState() { - if (RED.settings.user?.permissions === 'read') { + if (!RED.user.hasPermission('flows.write')) { $(".red-ui-deploy-button-group").addClass("readOnly"); $("#red-ui-header-button-deploy").addClass("disabled"); } else { From 18f2285a48d73b874b0dce08d43438626f62d96b Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 29 Oct 2025 17:31:23 +0000 Subject: [PATCH 6/7] Add selection-to-subflow context menu item --- .../@node-red/editor-client/src/js/ui/contextMenu.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js b/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js index c70757dfa..b528579ef 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js @@ -223,6 +223,11 @@ RED.contextMenu = (function () { { onselect: 'core:show-export-dialog', label: RED._("menu.label.export") } ) } + if (hasSelection && canEdit) { + menuItems.push( + { onselect: 'core:convert-to-subflow', label: RED._("menu.label.selectionToSubflow") } + ) + } menuItems.push( { onselect: 'core:select-all-nodes', label: RED._("keyboard.selectAll") } ) From 9d019e25c2ea604d96bd6eba32d825f1238d7eab Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 29 Oct 2025 17:50:50 +0000 Subject: [PATCH 7/7] Fix up port event cancelling on node-select --- .../@node-red/editor-client/src/js/ui/view.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 825e78a07..c459de1ff 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 @@ -3640,9 +3640,9 @@ RED.view = (function() { return tooltip; } - function portMouseOver(port,d,portType,portIndex) { + function portMouseOver(port,d,portType,portIndex, event) { if (mouse_mode === RED.state.SELECTING_NODE) { - d3.event.stopPropagation(); + (d3.event || event).stopPropagation(); return; } clearTimeout(portLabelHoverTimeout); @@ -3681,9 +3681,9 @@ RED.view = (function() { } port.classed("red-ui-flow-port-hovered",active); } - function portMouseOut(port,d,portType,portIndex) { + function portMouseOut(port,d,portType,portIndex, event) { if (mouse_mode === RED.state.SELECTING_NODE) { - d3.event.stopPropagation(); + (d3.event || event).stopPropagation(); return; } clearTimeout(portLabelHoverTimeout);