From 3da22882e9eb8281bdd50bd397857d3b74aa6b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nathana=C3=ABl=20L=C3=A9caud=C3=A9?= Date: Tue, 7 Jan 2025 11:30:35 -0500 Subject: [PATCH 1/9] Fix typo in CHANGELOG (4.0.7-->4.0.8) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 468cc1b1c..d09c2acd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -#### 4.0.7: Maintenance Release +#### 4.0.8: Maintenance Release Editor From 953b7584a37d4c30a9bdc07e7d3d53525dea9427 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 13 Jan 2025 16:37:35 +0000 Subject: [PATCH 2/9] Avoid exceeding call stack when draining message group in Switch Fixes #5013 --- .../@node-red/nodes/core/function/10-switch.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/function/10-switch.js b/packages/node_modules/@node-red/nodes/core/function/10-switch.js index fdc345f47..9d68f6b0b 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-switch.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-switch.js @@ -352,7 +352,9 @@ module.exports = function(RED) { if (msgs.length === 0) { done() } else { - drainMessageGroup(msgs,count,done); + setImmediate(() => { + drainMessageGroup(msgs,count,done); + }) } } }) @@ -505,7 +507,9 @@ module.exports = function(RED) { if (err) { node.error(err,nextMsg); } - processMessageQueue() + setImmediate(() => { + processMessageQueue() + }) }); } From 254b6a1e23445281f4578b2577d5ce36c247ec16 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 15 Jan 2025 10:32:10 +0000 Subject: [PATCH 3/9] Fix grunt dev via better ndoemon ignore rules --- .nodemonignore | 4 ---- nodemon.json | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) delete mode 100644 .nodemonignore create mode 100644 nodemon.json diff --git a/.nodemonignore b/.nodemonignore deleted file mode 100644 index 612a1e15b..000000000 --- a/.nodemonignore +++ /dev/null @@ -1,4 +0,0 @@ -/Gruntfile.js -/.git/* -*.backup -/public/* diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 000000000..98d660626 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,16 @@ +{ + "ignoreRoot": [ + ".git", + ".nyc_output", + ".sass-cache", + "bower-components", + "coverage" + ], + "ignore": [ + "/Gruntfile.js", + "/.git/*", + "*.backup", + "/public/*" + ] +} + From e2981f29702ba10d97da6d51a13bbf02ab8ac5bb Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 17 Jan 2025 16:45:44 +0000 Subject: [PATCH 4/9] Allow env var access to context --- .../@node-red/runtime/lib/flows/Flow.js | 8 ++++++++ .../@node-red/runtime/lib/flows/Group.js | 8 ++++++++ .../@node-red/runtime/lib/flows/util.js | 19 ++++++++++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) 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 c4f4e39a2..0b6045326 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -719,6 +719,14 @@ class Flow { }); } + getContext(scope) { + if (scope === 'flow') { + return this.context + } else if (scope === 'global') { + return context.get('global') + } + } + dump() { console.log("==================") console.log(this.TYPE, this.id); diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Group.js b/packages/node_modules/@node-red/runtime/lib/flows/Group.js index 589cdf115..d95b4e553 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Group.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Group.js @@ -49,6 +49,14 @@ class Group { } return this.parent.getSetting(key); } + + error(msg) { + this.parent.error(msg); + } + + getContext(scope) { + return this.parent.getContext(scope); + } } module.exports = { diff --git a/packages/node_modules/@node-red/runtime/lib/flows/util.js b/packages/node_modules/@node-red/runtime/lib/flows/util.js index 6b7f659b9..d50825212 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/util.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/util.js @@ -100,7 +100,24 @@ async function evaluateEnvProperties(flow, env, credentials) { } } else if (type ==='jsonata') { pendingEvaluations.push(new Promise((resolve, _) => { - redUtil.evaluateNodeProperty(value, 'jsonata', {_flow: flow}, null, (err, result) => { + redUtil.evaluateNodeProperty(value, 'jsonata',{ + // Fake a node object to provide access to _flow and context + _flow: flow, + context: () => { + return { + flow: { + get: (value, store, callback) => { + return flow.getContext('flow').get(value, store, callback) + } + }, + global: { + get: (value, store, callback) => { + return flow.getContext('global').get(value, store, callback) + } + } + } + } + }, null, (err, result) => { if (!err) { if (typeof result === 'object') { result = { value: result, __clone__: true} From 4cbf672b26679614b5ae2d4ef015621df0885def Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 17 Jan 2025 17:06:17 +0000 Subject: [PATCH 5/9] Fix library icon handling within library browser component Closes #5004 --- .../@node-red/editor-client/src/js/ui/library.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/library.js b/packages/node_modules/@node-red/editor-client/src/js/ui/library.js index 0098bc947..64b1f1547 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/library.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/library.js @@ -245,10 +245,15 @@ RED.library = (function() { if (lib.types && lib.types.indexOf(options.url) === -1) { return; } + let icon = 'fa fa-hdd-o'; + if (lib.icon) { + const fullIcon = RED.utils.separateIconPath(lib.icon); + icon = (fullIcon.module==="font-awesome"?"fa ":"")+fullIcon.file; + } listing.push({ library: lib.id, type: options.url, - icon: lib.icon || 'fa fa-hdd-o', + icon, label: RED._(lib.label||lib.id), path: "", expanded: true, @@ -303,10 +308,15 @@ RED.library = (function() { if (lib.types && lib.types.indexOf(options.url) === -1) { return; } + let icon = 'fa fa-hdd-o'; + if (lib.icon) { + const fullIcon = RED.utils.separateIconPath(lib.icon); + icon = (fullIcon.module==="font-awesome"?"fa ":"")+fullIcon.file; + } listing.push({ library: lib.id, type: options.url, - icon: lib.icon || 'fa fa-hdd-o', + icon, label: RED._(lib.label||lib.id), path: "", expanded: true, From 1acc16c9ef2fe203a0e630f51eed2d67f866e42b Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Sun, 19 Jan 2025 10:57:58 +0000 Subject: [PATCH 6/9] fix debug status reporting if null --- packages/node_modules/@node-red/nodes/core/common/21-debug.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/nodes/core/common/21-debug.js b/packages/node_modules/@node-red/nodes/core/common/21-debug.js index fe9827fce..45deb5ab3 100644 --- a/packages/node_modules/@node-red/nodes/core/common/21-debug.js +++ b/packages/node_modules/@node-red/nodes/core/common/21-debug.js @@ -148,7 +148,7 @@ module.exports = function(RED) { var st = (typeof output === 'string') ? output : util.inspect(output); var fill = "grey"; var shape = "dot"; - if (typeof output === 'object' && hasOwnProperty.call(output, "fill") && hasOwnProperty.call(output, "shape") && hasOwnProperty.call(output, "text")) { + if (typeof output === 'object' && output?.fill && output?.shape && output?.text) { fill = output.fill; shape = output.shape; st = output.text; From 13cac1b5efe9888c2a1bc922674f42ed9994af27 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 21 Jan 2025 13:56:44 +0000 Subject: [PATCH 7/9] Remember context sidebar tree state when refreshing Closes #5008 --- .../editor-client/src/js/ui/tab-context.js | 67 ++++++++++++------- .../editor-client/src/js/ui/utils.js | 59 +++++++++------- .../core/common/lib/debug/debug-utils.js | 3 +- 3 files changed, 79 insertions(+), 50 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js index 6cb034ccc..2c56786c8 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js @@ -18,8 +18,6 @@ RED.sidebar.context = (function() { var content; var sections; - var localCache = {}; - var flowAutoRefresh; var nodeAutoRefresh; var nodeSection; @@ -27,6 +25,8 @@ RED.sidebar.context = (function() { var flowSection; var globalSection; + const expandedPaths = {} + var currentNode; var currentFlow; @@ -212,14 +212,41 @@ RED.sidebar.context = (function() { var l = keys.length; for (var i = 0; i < l; i++) { sortedData[keys[i]].forEach(function(v) { - var k = keys[i]; - var l2 = sortedData[k].length; - var propRow = $('').appendTo(container); - var obj = $(propRow.children()[0]); + const k = keys[i]; + let payload = v.msg; + let format = v.format; + const tools = $(''); + expandedPaths[id + "." + k] = expandedPaths[id + "." + k] || new Set() + const objectElementOptions = { + typeHint: format, + sourceId: id + "." + k, + tools, + path: k, + rootPath: k, + exposeApi: true, + ontoggle: function(path,state) { + path = path.substring(k.length+1) + if (state) { + expandedPaths[id+"."+k].add(path) + } else { + // if 'a' has been collapsed, we want to remove 'a.b' and 'a[0]...' from the set + // of collapsed paths + for (let expandedPath of expandedPaths[id+"."+k]) { + if (expandedPath.startsWith(path+".") || expandedPath.startsWith(path+"[")) { + expandedPaths[id+"."+k].delete(expandedPath) + } + } + expandedPaths[id+"."+k].delete(path) + } + }, + expandPaths: [ ...expandedPaths[id+"."+k] ].sort(), + expandLeafNodes: true + } + const propRow = $('').appendTo(container); + const obj = $(propRow.children()[0]); obj.text(k); - var tools = $(''); const urlSafeK = encodeURIComponent(k) - var refreshItem = $('').appendTo(tools).on("click", function(e) { + const refreshItem = $('').appendTo(tools).on("click", function(e) { e.preventDefault(); e.stopPropagation(); $.getJSON(baseUrl+"/"+urlSafeK+"?store="+v.store, function(data) { @@ -229,16 +256,14 @@ RED.sidebar.context = (function() { tools.detach(); $(propRow.children()[1]).empty(); RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), { + ...objectElementOptions, typeHint: data.format, - sourceId: id+"."+k, - tools: tools, - path: k }).appendTo(propRow.children()[1]); } }) }); RED.popover.tooltip(refreshItem,RED._("sidebar.context.refrsh")); - var deleteItem = $('').appendTo(tools).on("click", function(e) { + const deleteItem = $('').appendTo(tools).on("click", function(e) { e.preventDefault(); e.stopPropagation(); var popover = RED.popover.create({ @@ -246,7 +271,7 @@ RED.sidebar.context = (function() { target: propRow, direction: "left", content: function() { - var content = $('
'); + const content = $('
'); $('

').appendTo(content); var row = $('

').appendTo(content); var bg = $('').appendTo(row); @@ -269,16 +294,15 @@ RED.sidebar.context = (function() { if (container.children().length === 0) { $('').appendTo(container).i18n(); } + delete expandedPaths[id + "." + k] } else { payload = data.msg; format = data.format; tools.detach(); $(propRow.children()[1]).empty(); RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), { - typeHint: data.format, - sourceId: id+"."+k, - tools: tools, - path: k + ...objectElementOptions, + typeHint: data.format }).appendTo(propRow.children()[1]); } }); @@ -293,14 +317,7 @@ RED.sidebar.context = (function() { }); RED.popover.tooltip(deleteItem,RED._("sidebar.context.delete")); - var payload = v.msg; - var format = v.format; - RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), { - typeHint: v.format, - sourceId: id+"."+k, - tools: tools, - path: k - }).appendTo(propRow.children()[1]); + RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), objectElementOptions).appendTo(propRow.children()[1]); if (contextStores.length > 1) { $("",{class:"red-ui-sidebar-context-property-storename"}).text(v.store).appendTo($(propRow.children()[0])) } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js index cfc7f65de..49ad15d87 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js @@ -230,7 +230,7 @@ RED.utils = (function() { var pinnedPaths = {}; var formattedPaths = {}; - function addMessageControls(obj,sourceId,key,msg,rootPath,strippedKey,extraTools) { + function addMessageControls(obj,sourceId,key,msg,rootPath,strippedKey,extraTools,enablePinning) { if (!pinnedPaths.hasOwnProperty(sourceId)) { pinnedPaths[sourceId] = {} } @@ -250,7 +250,7 @@ RED.utils = (function() { RED.clipboard.copyText(msg,copyPayload,"clipboard.copyMessageValue"); }) RED.popover.tooltip(copyPayload,RED._("node-red:debug.sidebar.copyPayload")); - if (strippedKey !== undefined && strippedKey !== '') { + if (enablePinning && strippedKey !== undefined && strippedKey !== '') { var isPinned = pinnedPaths[sourceId].hasOwnProperty(strippedKey); var pinPath = $('').appendTo(tools).on("click", function(e) { @@ -281,13 +281,16 @@ RED.utils = (function() { } } } - function checkExpanded(strippedKey,expandPaths,minRange,maxRange) { + function checkExpanded(strippedKey, expandPaths, { minRange, maxRange, expandLeafNodes }) { if (expandPaths && expandPaths.length > 0) { if (strippedKey === '' && minRange === undefined) { return true; } for (var i=0;i').appendTo(element); if (sourceId) { - addMessageControls(header,sourceId,path,obj,rootPath,strippedKey,tools); + addMessageControls(header,sourceId,path,obj,rootPath,strippedKey,tools, enablePinning); } if (!key) { element.addClass("red-ui-debug-msg-top-level"); - if (sourceId) { + if (sourceId && !expandPaths) { var pinned = pinnedPaths[sourceId]; expandPaths = []; if (pinned) { @@ -476,7 +481,7 @@ RED.utils = (function() { $('').text(typeHint||'string').appendTo(header); var row = $('

').appendTo(element); $('
').text(obj).appendTo(row);
-                },function(state) {if (ontoggle) { ontoggle(path,state);}}, checkExpanded(strippedKey,expandPaths));
+                },function(state) {if (ontoggle) { ontoggle(path,state);}}, checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
             }
             e = $('').html('"'+formatString(sanitize(obj))+'"').appendTo(entryObj);
             if (/^#[0-9a-f]{6}$/i.test(obj)) {
@@ -592,14 +597,16 @@ RED.utils = (function() {
                                     typeHint: type==='buffer'?'hex':false,
                                     hideKey: false,
                                     path: path+"["+i+"]",
-                                    sourceId: sourceId,
-                                    rootPath: rootPath,
-                                    expandPaths: expandPaths,
-                                    ontoggle: ontoggle,
-                                    exposeApi: exposeApi,
+                                    sourceId,
+                                    rootPath,
+                                    expandPaths,
+                                    expandLeafNodes,
+                                    ontoggle,
+                                    exposeApi,
                                     // tools: tools // Do not pass tools down as we
                                                     // keep them attached to the top-level header
                                     nodeSelector: options.nodeSelector,
+                                    enablePinning
                                 }
                             ).appendTo(row);
                         }
@@ -623,21 +630,23 @@ RED.utils = (function() {
                                                 typeHint: type==='buffer'?'hex':false,
                                                 hideKey: false,
                                                 path: path+"["+i+"]",
-                                                sourceId: sourceId,
-                                                rootPath: rootPath,
-                                                expandPaths: expandPaths,
-                                                ontoggle: ontoggle,
-                                                exposeApi: exposeApi,
+                                                sourceId,
+                                                rootPath,
+                                                expandPaths,
+                                                expandLeafNodes,
+                                                ontoggle,
+                                                exposeApi,
                                                 // tools: tools // Do not pass tools down as we
                                                                 // keep them attached to the top-level header
                                                 nodeSelector: options.nodeSelector,
+                                                enablePinning
                                             }
                                         ).appendTo(row);
                                     }
                                 }
                             })(),
                             (function() { var path = path+"["+i+"]"; return function(state) {if (ontoggle) { ontoggle(path,state);}}})(),
-                            checkExpanded(strippedKey,expandPaths,minRange,Math.min(fullLength-1,(minRange+9))));
+                            checkExpanded(strippedKey,expandPaths,{ minRange, maxRange: Math.min(fullLength-1,(minRange+9)), expandLeafNodes}));
                             $('').html("["+minRange+" … "+Math.min(fullLength-1,(minRange+9))+"]").appendTo(header);
                         }
                         if (fullLength < originalLength) {
@@ -646,7 +655,7 @@ RED.utils = (function() {
                     }
                 },
                 function(state) {if (ontoggle) { ontoggle(path,state);}},
-                checkExpanded(strippedKey,expandPaths));
+                checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
             }
         } else if (typeof obj === 'object') {
             element.addClass('collapsed');
@@ -680,14 +689,16 @@ RED.utils = (function() {
                                 typeHint: false,
                                 hideKey: false,
                                 path: newPath,
-                                sourceId: sourceId,
-                                rootPath: rootPath,
-                                expandPaths: expandPaths,
-                                ontoggle: ontoggle,
-                                exposeApi: exposeApi,
+                                sourceId,
+                                rootPath,
+                                expandPaths,
+                                expandLeafNodes,
+                                ontoggle,
+                                exposeApi,
                                 // tools: tools // Do not pass tools down as we
                                                 // keep them attached to the top-level header
                                 nodeSelector: options.nodeSelector,
+                                enablePinning
                             }
                         ).appendTo(row);
                     }
@@ -696,7 +707,7 @@ RED.utils = (function() {
                     }
                 },
                 function(state) {if (ontoggle) { ontoggle(path,state);}},
-                checkExpanded(strippedKey,expandPaths));
+                checkExpanded(strippedKey, expandPaths, { expandLeafNodes }));
             }
             if (key) {
                 $('').text(type).appendTo(entryObj);
diff --git a/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js b/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js
index b244273dc..0a84b24c7 100644
--- a/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js
+++ b/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js
@@ -511,9 +511,10 @@ RED.debug = (function() {
             typeHint: format,
             hideKey: false,
             path: path,
-            sourceId: sourceNode&&sourceNode.id,
+            sourceId: sourceNode && sourceNode.id,
             rootPath: path,
             nodeSelector: config.messageSourceClick,
+            enablePinning: true
         });
         // Do this in a separate step so the element functions aren't stripped
         debugMessage.appendTo(el);

From 48d2d269a5ede2882d1677596da6497316d3b756 Mon Sep 17 00:00:00 2001
From: Nick O'Leary 
Date: Tue, 21 Jan 2025 16:15:13 +0000
Subject: [PATCH 8/9] Do not select group when triggering quick-add within it

---
 .../@node-red/editor-client/src/js/ui/view.js             | 8 --------
 1 file changed, 8 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 8ce6dc630..92d4593c6 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
@@ -1265,11 +1265,6 @@ RED.view = (function() {
         var targetGroup = options.group;
         var touchTrigger = options.touchTrigger;
 
-        if (targetGroup) {
-            selectedGroups.add(targetGroup,false);
-            RED.view.redraw();
-        }
-
         // `point` is the place in the workspace the mouse has clicked.
         //  This takes into account scrolling and scaling of the workspace.
         var ox = point[0];
@@ -1591,9 +1586,6 @@ RED.view = (function() {
                 // auto select dropped node - so info shows (if visible)
                 clearSelection();
                 nn.selected = true;
-                if (targetGroup) {
-                    selectedGroups.add(targetGroup,false);
-                }
                 movingSet.add(nn);
                 updateActiveNodes();
                 updateSelection();

From daa76e6e5fe6c68d835b0c2bf71cdec2916ee05a Mon Sep 17 00:00:00 2001
From: Nick O'Leary 
Date: Wed, 22 Jan 2025 10:25:24 +0000
Subject: [PATCH 9/9] Update sf instance env vars when removed from template

---
 .../editor-client/src/js/ui/editor.js         | 49 +++++++++++++++++--
 1 file changed, 44 insertions(+), 5 deletions(-)

diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js
index ca74b3eff..f2e40147d 100644
--- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js
+++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js
@@ -1836,8 +1836,18 @@ RED.editor = (function() {
                                 }
                             });
                         }
-
+                        let envToRemove = new Set()
                         if (!isSameObj(old_env, new_env)) {
+                            // Get a list of env properties that have been removed
+                            // by comparing old_env and new_env
+                            if (old_env) {
+                                old_env.forEach(env => { envToRemove.add(env.name) })
+                            }
+                            if (new_env) {
+                                new_env.forEach(env => {
+                                    envToRemove.delete(env.name)
+                                })
+                            }
                             editState.changes.env = editing_node.env;
                             editing_node.env = new_env;
                             editState.changed = true;
@@ -1846,10 +1856,11 @@ RED.editor = (function() {
 
 
                         if (editState.changed) {
-                            var wasChanged = editing_node.changed;
+                            let wasChanged = editing_node.changed;
                             editing_node.changed = true;
                             validateNode(editing_node);
-                            var subflowInstances = [];
+                            let subflowInstances = [];
+                            let instanceHistoryEvents = []
                             RED.nodes.eachNode(function(n) {
                                 if (n.type == "subflow:"+editing_node.id) {
                                     subflowInstances.push({
@@ -1859,13 +1870,35 @@ RED.editor = (function() {
                                     n._def.color = editing_node.color;
                                     n.changed = true;
                                     n.dirty = true;
+                                    if (n.env) {
+                                        const oldEnv = n.env
+                                        const newEnv = []
+                                        let envChanged = false
+                                        n.env.forEach((env, index) => {
+                                            if (envToRemove.has(env.name)) {
+                                                envChanged = true
+                                            } else {
+                                                newEnv.push(env)
+                                            }
+                                        })
+                                        if (envChanged) {
+                                            instanceHistoryEvents.push({
+                                                t: 'edit',
+                                                node: n,
+                                                changes: { env: oldEnv },
+                                                dirty: n.dirty,
+                                                changed: n.changed
+                                            })
+                                            n.env = newEnv
+                                        }
+                                    }
                                     updateNodeProperties(n);
                                     validateNode(n);
                                 }
                             });
                             RED.events.emit("subflows:change",editing_node);
                             RED.nodes.dirty(true);
-                            var historyEvent = {
+                            let historyEvent = {
                                 t:'edit',
                                 node:editing_node,
                                 changes:editState.changes,
@@ -1875,7 +1908,13 @@ RED.editor = (function() {
                                     instances:subflowInstances
                                 }
                             };
-
+                            if (instanceHistoryEvents.length > 0) {
+                                historyEvent = {
+                                    t: 'multi',
+                                    events: [ historyEvent, ...instanceHistoryEvents ],
+                                    dirty: wasDirty
+                                }
+                            }
                             RED.history.push(historyEvent);
                         }
                         editing_node.dirty = true;