@node-red/editor-client 4.0.5 → 4.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/red/red.js CHANGED
@@ -1834,6 +1834,37 @@ RED.user = (function() {
1834
1834
  }
1835
1835
 
1836
1836
 
1837
+ } else {
1838
+ if (data.prompts) {
1839
+ if (data.loginMessage) {
1840
+ const sessionMessages = $("<div/>",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields");
1841
+ $('<div>').text(data.loginMessage).appendTo(sessionMessages);
1842
+ }
1843
+
1844
+ i = 0;
1845
+ for (;i<data.prompts.length;i++) {
1846
+ var field = data.prompts[i];
1847
+ var row = $("<div/>",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields");
1848
+ var loginButton = $('<a href="#" class="red-ui-button"></a>',{style: "padding: 10px"}).appendTo(row).on("click", function() {
1849
+ document.location = field.url;
1850
+ });
1851
+ if (field.image) {
1852
+ $("<img>",{src:field.image}).appendTo(loginButton);
1853
+ } else if (field.label) {
1854
+ var label = $('<span></span>').text(field.label);
1855
+ if (field.icon) {
1856
+ $('<i></i>',{class: "fa fa-2x "+field.icon, style:"vertical-align: middle"}).appendTo(loginButton);
1857
+ label.css({
1858
+ "verticalAlign":"middle",
1859
+ "marginLeft":"8px"
1860
+ });
1861
+
1862
+ }
1863
+ label.appendTo(loginButton);
1864
+ }
1865
+ loginButton.button();
1866
+ }
1867
+ }
1837
1868
  }
1838
1869
  if (opts.cancelable) {
1839
1870
  $("#node-dialog-login-cancel").button().on("click", function( event ) {
@@ -4495,7 +4526,13 @@ RED.nodes = (function() {
4495
4526
 
4496
4527
  var exports = {
4497
4528
  setModulePendingUpdated: function(module,version) {
4498
- moduleList[module].pending_version = version;
4529
+ if (!!RED.plugins.getModule(module)) {
4530
+ // The module updated is a plugin
4531
+ RED.plugins.getModule(module).pending_version = version;
4532
+ } else {
4533
+ moduleList[module].pending_version = version;
4534
+ }
4535
+
4499
4536
  RED.events.emit("registry:module-updated",{module:module,version:version});
4500
4537
  },
4501
4538
  getModule: function(module) {
@@ -5123,12 +5160,15 @@ RED.nodes = (function() {
5123
5160
  }
5124
5161
  n["_"] = RED._;
5125
5162
  }
5163
+
5164
+ // Both node and config node can use a config node
5165
+ updateConfigNodeUsers(newNode, { action: "add" });
5166
+
5126
5167
  if (n._def.category == "config") {
5127
- configNodes[n.id] = n;
5168
+ configNodes[n.id] = newNode;
5128
5169
  } else {
5129
5170
  if (n.wires && (n.wires.length > n.outputs)) { n.outputs = n.wires.length; }
5130
5171
  n.dirty = true;
5131
- updateConfigNodeUsers(n);
5132
5172
  if (n._def.category == "subflows" && typeof n.i === "undefined") {
5133
5173
  var nextId = 0;
5134
5174
  RED.nodes.eachNode(function(node) {
@@ -5190,9 +5230,11 @@ RED.nodes = (function() {
5190
5230
  var removedLinks = [];
5191
5231
  var removedNodes = [];
5192
5232
  var node;
5233
+
5193
5234
  if (id in configNodes) {
5194
5235
  node = configNodes[id];
5195
5236
  delete configNodes[id];
5237
+ updateConfigNodeUsers(node, { action: "remove" });
5196
5238
  RED.events.emit('nodes:remove',node);
5197
5239
  RED.workspaces.refresh();
5198
5240
  } else if (allNodes.hasNode(id)) {
@@ -5201,6 +5243,9 @@ RED.nodes = (function() {
5201
5243
  delete nodeLinks[id];
5202
5244
  removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); });
5203
5245
  removedLinks.forEach(removeLink);
5246
+ updateConfigNodeUsers(node, { action: "remove" });
5247
+
5248
+ // TODO: Legacy code for exclusive config node
5204
5249
  var updatedConfigNode = false;
5205
5250
  for (var d in node._def.defaults) {
5206
5251
  if (node._def.defaults.hasOwnProperty(d)) {
@@ -5214,10 +5259,6 @@ RED.nodes = (function() {
5214
5259
  if (configNode._def.exclusive) {
5215
5260
  removeNode(node[d]);
5216
5261
  removedNodes.push(configNode);
5217
- } else {
5218
- var users = configNode.users;
5219
- users.splice(users.indexOf(node),1);
5220
- RED.events.emit('nodes:change',configNode)
5221
5262
  }
5222
5263
  }
5223
5264
  }
@@ -5454,23 +5495,34 @@ RED.nodes = (function() {
5454
5495
  return {nodes:removedNodes,links:removedLinks, groups: removedGroups, junctions: removedJunctions};
5455
5496
  }
5456
5497
 
5498
+ /**
5499
+ * Add a Subflow to the Workspace
5500
+ *
5501
+ * @param {object} sf The Subflow to add.
5502
+ * @param {boolean|undefined} createNewIds Whether to update the name.
5503
+ */
5457
5504
  function addSubflow(sf, createNewIds) {
5458
5505
  if (createNewIds) {
5459
- var subflowNames = Object.keys(subflows).map(function(sfid) {
5460
- return subflows[sfid].name;
5461
- });
5506
+ // Update the Subflow name to highlight that this is a copy
5507
+ const subflowNames = Object.keys(subflows).map(function (sfid) {
5508
+ return subflows[sfid].name || "";
5509
+ })
5510
+ subflowNames.sort()
5462
5511
 
5463
- subflowNames.sort();
5464
- var copyNumber = 1;
5465
- var subflowName = sf.name;
5512
+ let copyNumber = 1;
5513
+ let subflowName = sf.name;
5466
5514
  subflowNames.forEach(function(name) {
5467
5515
  if (subflowName == name) {
5516
+ subflowName = sf.name + " (" + copyNumber + ")";
5468
5517
  copyNumber++;
5469
- subflowName = sf.name+" ("+copyNumber+")";
5470
5518
  }
5471
5519
  });
5520
+
5472
5521
  sf.name = subflowName;
5473
5522
  }
5523
+
5524
+ sf.instances = [];
5525
+
5474
5526
  subflows[sf.id] = sf;
5475
5527
  allNodes.addTab(sf.id);
5476
5528
  linkTabMap[sf.id] = [];
@@ -5523,7 +5575,7 @@ RED.nodes = (function() {
5523
5575
  module: "node-red"
5524
5576
  }
5525
5577
  });
5526
- sf.instances = [];
5578
+
5527
5579
  sf._def = RED.nodes.getType("subflow:"+sf.id);
5528
5580
  RED.events.emit("subflows:add",sf);
5529
5581
  }
@@ -6165,7 +6217,8 @@ RED.nodes = (function() {
6165
6217
  // Remove the old subflow definition - but leave the instances in place
6166
6218
  var removalResult = RED.subflow.removeSubflow(n.id, true);
6167
6219
  // Create the list of nodes for the new subflow def
6168
- var subflowNodes = [n].concat(zMap[n.id]);
6220
+ // Need to sort the list in order to remove missing nodes
6221
+ var subflowNodes = [n].concat(zMap[n.id]).filter((s) => !!s);
6169
6222
  // Import the new subflow - no clashes should occur as we've removed
6170
6223
  // the old version
6171
6224
  var result = importNodes(subflowNodes);
@@ -6202,9 +6255,20 @@ RED.nodes = (function() {
6202
6255
  // Replace config nodes
6203
6256
  //
6204
6257
  configNodeIds.forEach(function(id) {
6205
- removedNodes = removedNodes.concat(convertNode(getNode(id)));
6258
+ const configNode = getNode(id);
6259
+ const currentUserCount = configNode.users;
6260
+
6261
+ // Add a snapshot of the Config Node
6262
+ removedNodes = removedNodes.concat(convertNode(configNode));
6263
+
6264
+ // Remove the Config Node instance
6206
6265
  removeNode(id);
6207
- importNodes([newConfigNodes[id]])
6266
+
6267
+ // Import the new one
6268
+ importNodes([newConfigNodes[id]]);
6269
+
6270
+ // Re-attributes the user count
6271
+ getNode(id).users = currentUserCount;
6208
6272
  });
6209
6273
 
6210
6274
  return {
@@ -6445,6 +6509,8 @@ RED.nodes = (function() {
6445
6509
  if (matchingSubflow) {
6446
6510
  subflow_denylist[n.id] = matchingSubflow;
6447
6511
  } else {
6512
+ const oldId = n.id;
6513
+
6448
6514
  subflow_map[n.id] = n;
6449
6515
  if (createNewIds || options.importMap[n.id] === "copy") {
6450
6516
  nid = getID();
@@ -6472,7 +6538,7 @@ RED.nodes = (function() {
6472
6538
  n.status.id = getID();
6473
6539
  }
6474
6540
  new_subflows.push(n);
6475
- addSubflow(n,createNewIds || options.importMap[n.id] === "copy");
6541
+ addSubflow(n,createNewIds || options.importMap[oldId] === "copy");
6476
6542
  }
6477
6543
  }
6478
6544
  }
@@ -6581,6 +6647,21 @@ RED.nodes = (function() {
6581
6647
  }
6582
6648
  }
6583
6649
 
6650
+ // Config node can use another config node, must ensure that this other
6651
+ // config node is added before to exists when updating the user list
6652
+ const configNodeFilter = function (node) {
6653
+ let count = 0;
6654
+ if (node._def?.defaults) {
6655
+ for (const def of Object.values(node._def.defaults)) {
6656
+ if (def.type) {
6657
+ count++;
6658
+ }
6659
+ }
6660
+ }
6661
+ return count;
6662
+ };
6663
+ new_nodes.sort((a, b) => configNodeFilter(a) - configNodeFilter(b));
6664
+
6584
6665
  // Find regular flow nodes and subflow instances
6585
6666
  for (i=0;i<newNodes.length;i++) {
6586
6667
  n = newNodes[i];
@@ -6592,7 +6673,7 @@ RED.nodes = (function() {
6592
6673
  x:parseFloat(n.x || 0),
6593
6674
  y:parseFloat(n.y || 0),
6594
6675
  z:n.z,
6595
- type:0,
6676
+ type: n.type,
6596
6677
  info: n.info,
6597
6678
  changed:false,
6598
6679
  _config:{}
@@ -6653,7 +6734,6 @@ RED.nodes = (function() {
6653
6734
  }
6654
6735
  }
6655
6736
  }
6656
- node.type = n.type;
6657
6737
  node._def = def;
6658
6738
  if (node.type === "group") {
6659
6739
  node._def = RED.group.def;
@@ -6683,6 +6763,15 @@ RED.nodes = (function() {
6683
6763
  outputs: n.outputs|| (n.wires && n.wires.length) || 0,
6684
6764
  set: registry.getNodeSet("node-red/unknown")
6685
6765
  }
6766
+ var orig = {};
6767
+ for (var p in n) {
6768
+ if (n.hasOwnProperty(p) && p!="x" && p!="y" && p!="z" && p!="id" && p!="wires") {
6769
+ orig[p] = n[p];
6770
+ }
6771
+ }
6772
+ node._orig = orig;
6773
+ node.name = n.type;
6774
+ node.type = "unknown";
6686
6775
  } else {
6687
6776
  if (subflow_denylist[parentId] || createNewIds || options.importMap[n.id] === "copy") {
6688
6777
  parentId = subflow.id;
@@ -6743,29 +6832,31 @@ RED.nodes = (function() {
6743
6832
  node.type = "unknown";
6744
6833
  }
6745
6834
  if (node._def.category != "config") {
6746
- if (n.hasOwnProperty('inputs')) {
6747
- node.inputs = n.inputs;
6835
+ if (n.hasOwnProperty('inputs') && node._def.defaults.hasOwnProperty("inputs")) {
6836
+ node.inputs = parseInt(n.inputs, 10);
6748
6837
  node._config.inputs = JSON.stringify(n.inputs);
6749
6838
  } else {
6750
6839
  node.inputs = node._def.inputs;
6751
6840
  }
6752
- if (n.hasOwnProperty('outputs')) {
6753
- node.outputs = n.outputs;
6841
+ if (n.hasOwnProperty('outputs') && node._def.defaults.hasOwnProperty("outputs")) {
6842
+ node.outputs = parseInt(n.outputs, 10);
6754
6843
  node._config.outputs = JSON.stringify(n.outputs);
6755
6844
  } else {
6756
6845
  node.outputs = node._def.outputs;
6757
6846
  }
6758
- if (node.hasOwnProperty('wires') && node.wires.length > node.outputs) {
6759
- if (!node._def.defaults.hasOwnProperty("outputs") || !isNaN(parseInt(n.outputs))) {
6760
- // If 'wires' is longer than outputs, clip wires
6761
- console.log("Warning: node.wires longer than node.outputs - trimming wires:",node.id," wires:",node.wires.length," outputs:",node.outputs);
6762
- node.wires = node.wires.slice(0,node.outputs);
6763
- } else {
6764
- // The node declares outputs in its defaults, but has not got a valid value
6765
- // Defer to the length of the wires array
6847
+
6848
+ // The node declares outputs in its defaults, but has not got a valid value
6849
+ // Defer to the length of the wires array
6850
+ if (node.hasOwnProperty('wires')) {
6851
+ if (isNaN(node.outputs)) {
6766
6852
  node.outputs = node.wires.length;
6853
+ } else if (node.wires.length > node.outputs) {
6854
+ // If 'wires' is longer than outputs, clip wires
6855
+ console.log("Warning: node.wires longer than node.outputs - trimming wires:", node.id, " wires:", node.wires.length, " outputs:", node.outputs);
6856
+ node.wires = node.wires.slice(0, node.outputs);
6767
6857
  }
6768
6858
  }
6859
+
6769
6860
  for (d in node._def.defaults) {
6770
6861
  if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') {
6771
6862
  node[d] = n[d];
@@ -6862,11 +6953,6 @@ RED.nodes = (function() {
6862
6953
  nodeList = nodeList.map(function(id) {
6863
6954
  var node = node_map[id];
6864
6955
  if (node) {
6865
- if (node._def.category === 'config') {
6866
- if (node.users.indexOf(n) === -1) {
6867
- node.users.push(n);
6868
- }
6869
- }
6870
6956
  return node.id;
6871
6957
  }
6872
6958
  return id;
@@ -6880,9 +6966,11 @@ RED.nodes = (function() {
6880
6966
  n = new_subflows[i];
6881
6967
  n.in.forEach(function(input) {
6882
6968
  input.wires.forEach(function(wire) {
6883
- var link = {source:input, sourcePort:0, target:node_map[wire.id]};
6884
- addLink(link);
6885
- new_links.push(link);
6969
+ if (node_map.hasOwnProperty(wire.id)) {
6970
+ var link = {source:input, sourcePort:0, target:node_map[wire.id]};
6971
+ addLink(link);
6972
+ new_links.push(link);
6973
+ }
6886
6974
  });
6887
6975
  delete input.wires;
6888
6976
  });
@@ -6891,11 +6979,13 @@ RED.nodes = (function() {
6891
6979
  var link;
6892
6980
  if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) {
6893
6981
  link = {source:n.in[wire.port], sourcePort:wire.port,target:output};
6894
- } else {
6982
+ } else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) {
6895
6983
  link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:output};
6896
6984
  }
6897
- addLink(link);
6898
- new_links.push(link);
6985
+ if (link) {
6986
+ addLink(link);
6987
+ new_links.push(link);
6988
+ }
6899
6989
  });
6900
6990
  delete output.wires;
6901
6991
  });
@@ -6904,11 +6994,13 @@ RED.nodes = (function() {
6904
6994
  var link;
6905
6995
  if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) {
6906
6996
  link = {source:n.in[wire.port], sourcePort:wire.port,target:n.status};
6907
- } else {
6997
+ } else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) {
6908
6998
  link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:n.status};
6909
6999
  }
6910
- addLink(link);
6911
- new_links.push(link);
7000
+ if (link) {
7001
+ addLink(link);
7002
+ new_links.push(link);
7003
+ }
6912
7004
  });
6913
7005
  delete n.status.wires;
6914
7006
  }
@@ -7087,25 +7179,79 @@ RED.nodes = (function() {
7087
7179
  return result;
7088
7180
  }
7089
7181
 
7090
- // Update any config nodes referenced by the provided node to ensure their 'users' list is correct
7091
- function updateConfigNodeUsers(n) {
7092
- for (var d in n._def.defaults) {
7093
- if (n._def.defaults.hasOwnProperty(d)) {
7094
- var property = n._def.defaults[d];
7182
+ /**
7183
+ * Update any config nodes referenced by the provided node to ensure
7184
+ * their 'users' list is correct.
7185
+ *
7186
+ * @param {object} node The node in which to check if it contains references
7187
+ * @param {object} options Options to apply.
7188
+ * @param {"add" | "remove"} [options.action] Add or remove the node from
7189
+ * the Config Node users list. Default `add`.
7190
+ * @param {boolean} [options.emitEvent] Emit the `nodes:changes` event.
7191
+ * Default true.
7192
+ */
7193
+ function updateConfigNodeUsers(node, options) {
7194
+ const defaultOptions = { action: "add", emitEvent: true };
7195
+ options = Object.assign({}, defaultOptions, options);
7196
+
7197
+ for (var d in node._def.defaults) {
7198
+ if (node._def.defaults.hasOwnProperty(d)) {
7199
+ var property = node._def.defaults[d];
7095
7200
  if (property.type) {
7096
7201
  var type = registry.getNodeType(property.type);
7202
+ // Need to ensure the type is a config node to not treat links nodes
7097
7203
  if (type && type.category == "config") {
7098
- var configNode = configNodes[n[d]];
7204
+ var configNode = configNodes[node[d]];
7099
7205
  if (configNode) {
7100
- if (configNode.users.indexOf(n) === -1) {
7101
- configNode.users.push(n);
7102
- RED.events.emit('nodes:change',configNode)
7206
+ if (options.action === "add") {
7207
+ if (configNode.users.indexOf(node) === -1) {
7208
+ configNode.users.push(node);
7209
+ if (options.emitEvent) {
7210
+ RED.events.emit('nodes:change', configNode);
7211
+ }
7212
+ }
7213
+ } else if (options.action === "remove") {
7214
+ if (configNode.users.indexOf(node) !== -1) {
7215
+ const users = configNode.users;
7216
+ users.splice(users.indexOf(node), 1);
7217
+ if (options.emitEvent) {
7218
+ RED.events.emit('nodes:change', configNode);
7219
+ }
7220
+ }
7103
7221
  }
7104
7222
  }
7105
7223
  }
7106
7224
  }
7107
7225
  }
7108
7226
  }
7227
+
7228
+ // Subflows can have config node env
7229
+ if (node.type.indexOf("subflow:") === 0) {
7230
+ node.env?.forEach((prop) => {
7231
+ if (prop.type === "conf-type" && prop.value) {
7232
+ // Add the node to the config node users
7233
+ const configNode = getNode(prop.value);
7234
+ if (configNode) {
7235
+ if (options.action === "add") {
7236
+ if (configNode.users.indexOf(node) === -1) {
7237
+ configNode.users.push(node);
7238
+ if (options.emitEvent) {
7239
+ RED.events.emit('nodes:change', configNode);
7240
+ }
7241
+ }
7242
+ } else if (options.action === "remove") {
7243
+ if (configNode.users.indexOf(node) !== -1) {
7244
+ const users = configNode.users;
7245
+ users.splice(users.indexOf(node), 1);
7246
+ if (options.emitEvent) {
7247
+ RED.events.emit('nodes:change', configNode);
7248
+ }
7249
+ }
7250
+ }
7251
+ }
7252
+ }
7253
+ });
7254
+ }
7109
7255
  }
7110
7256
 
7111
7257
  function flowVersion(version) {
@@ -8882,10 +9028,61 @@ RED.history = (function() {
8882
9028
  RED.events.emit("nodes:change",newConfigNode);
8883
9029
  }
8884
9030
  });
9031
+ } else if (i === "env" && ev.node.type.indexOf("subflow:") === 0) {
9032
+ // Subflow can have config node in node.env
9033
+ let nodeList = ev.node.env || [];
9034
+ nodeList = nodeList.reduce((list, prop) => {
9035
+ if (prop.type === "conf-type" && prop.value) {
9036
+ list.push(prop.value);
9037
+ }
9038
+ return list;
9039
+ }, []);
9040
+
9041
+ nodeList.forEach(function(id) {
9042
+ const configNode = RED.nodes.node(id);
9043
+ if (configNode) {
9044
+ if (configNode.users.indexOf(ev.node) !== -1) {
9045
+ configNode.users.splice(configNode.users.indexOf(ev.node), 1);
9046
+ RED.events.emit("nodes:change", configNode);
9047
+ }
9048
+ }
9049
+ });
9050
+
9051
+ nodeList = ev.changes.env || [];
9052
+ nodeList = nodeList.reduce((list, prop) => {
9053
+ if (prop.type === "conf-type" && prop.value) {
9054
+ list.push(prop.value);
9055
+ }
9056
+ return list;
9057
+ }, []);
9058
+
9059
+ nodeList.forEach(function(id) {
9060
+ const configNode = RED.nodes.node(id);
9061
+ if (configNode) {
9062
+ if (configNode.users.indexOf(ev.node) === -1) {
9063
+ configNode.users.push(ev.node);
9064
+ RED.events.emit("nodes:change", configNode);
9065
+ }
9066
+ }
9067
+ });
9068
+ }
9069
+ if (i === "credentials" && ev.changes[i]) {
9070
+ // Reset - Only want to keep the changes
9071
+ inverseEv.changes[i] = {};
9072
+ for (const [key, value] of Object.entries(ev.changes[i])) {
9073
+ // Edge case: node.credentials is cleared after a deploy, so we can't
9074
+ // capture values for the inverse event when undoing past a deploy
9075
+ if (ev.node.credentials) {
9076
+ inverseEv.changes[i][key] = ev.node.credentials[key];
9077
+ }
9078
+ ev.node.credentials[key] = value;
9079
+ }
9080
+ } else {
9081
+ ev.node[i] = ev.changes[i];
8885
9082
  }
8886
- ev.node[i] = ev.changes[i];
8887
9083
  }
8888
9084
  }
9085
+
8889
9086
  ev.node.dirty = true;
8890
9087
  ev.node.changed = ev.changed;
8891
9088
 
@@ -8965,6 +9162,24 @@ RED.history = (function() {
8965
9162
  RED.editor.updateNodeProperties(ev.node,outputMap);
8966
9163
  RED.editor.validateNode(ev.node);
8967
9164
  }
9165
+ // If it's a Config Node, validate user nodes too.
9166
+ // NOTE: The Config Node must be validated before validating users.
9167
+ if (ev.node.users) {
9168
+ const validatedNodes = new Set();
9169
+ const userStack = ev.node.users.slice();
9170
+
9171
+ validatedNodes.add(ev.node.id);
9172
+ while (userStack.length) {
9173
+ const node = userStack.pop();
9174
+ if (!validatedNodes.has(node.id)) {
9175
+ validatedNodes.add(node.id);
9176
+ if (node.users) {
9177
+ userStack.push(...node.users);
9178
+ }
9179
+ RED.editor.validateNode(node);
9180
+ }
9181
+ }
9182
+ }
8968
9183
  if (ev.links) {
8969
9184
  inverseEv.createdLinks = [];
8970
9185
  for (i=0;i<ev.links.length;i++) {
@@ -22218,7 +22433,7 @@ RED.view = (function() {
22218
22433
  }
22219
22434
  selectedLinks.clearUnselected()
22220
22435
  },
22221
- length: () => groups.length,
22436
+ length: () => groups.size,
22222
22437
  forEach: (func) => { groups.forEach(func) },
22223
22438
  toArray: () => [...groups],
22224
22439
  clear: function () {
@@ -22251,8 +22466,8 @@ RED.view = (function() {
22251
22466
  evt.stopPropagation()
22252
22467
  RED.contextMenu.show({
22253
22468
  type: 'workspace',
22254
- x:evt.clientX-5,
22255
- y:evt.clientY-5
22469
+ x: evt.clientX,
22470
+ y: evt.clientY
22256
22471
  })
22257
22472
  return false
22258
22473
  })
@@ -24619,22 +24834,21 @@ RED.view = (function() {
24619
24834
  addToRemovedLinks(reconnectResult.removedLinks)
24620
24835
  }
24621
24836
 
24622
- var startDirty = RED.nodes.dirty();
24623
- var startChanged = false;
24624
- var selectedGroups = [];
24837
+ const startDirty = RED.nodes.dirty();
24838
+ let movingSelectedGroups = [];
24625
24839
  if (movingSet.length() > 0) {
24626
24840
 
24627
24841
  for (var i=0;i<movingSet.length();i++) {
24628
24842
  node = movingSet.get(i).n;
24629
24843
  if (node.type === "group") {
24630
- selectedGroups.push(node);
24844
+ movingSelectedGroups.push(node);
24631
24845
  }
24632
24846
  }
24633
24847
  // Make sure we have identified all groups about to be deleted
24634
- for (i=0;i<selectedGroups.length;i++) {
24635
- selectedGroups[i].nodes.forEach(function(n) {
24636
- if (n.type === "group" && selectedGroups.indexOf(n) === -1) {
24637
- selectedGroups.push(n);
24848
+ for (i=0;i<movingSelectedGroups.length;i++) {
24849
+ movingSelectedGroups[i].nodes.forEach(function(n) {
24850
+ if (n.type === "group" && movingSelectedGroups.indexOf(n) === -1) {
24851
+ movingSelectedGroups.push(n);
24638
24852
  }
24639
24853
  })
24640
24854
  }
@@ -24651,7 +24865,7 @@ RED.view = (function() {
24651
24865
  addToRemovedLinks(removedEntities.links);
24652
24866
  if (node.g) {
24653
24867
  var group = RED.nodes.group(node.g);
24654
- if (selectedGroups.indexOf(group) === -1) {
24868
+ if (movingSelectedGroups.indexOf(group) === -1) {
24655
24869
  // Don't use RED.group.removeFromGroup as that emits
24656
24870
  // a change event on the node - but we're deleting it
24657
24871
  var index = group.nodes.indexOf(node);
@@ -24665,7 +24879,7 @@ RED.view = (function() {
24665
24879
  removedLinks = removedLinks.concat(result.links);
24666
24880
  if (node.g) {
24667
24881
  var group = RED.nodes.group(node.g);
24668
- if (selectedGroups.indexOf(group) === -1) {
24882
+ if (movingSelectedGroups.indexOf(group) === -1) {
24669
24883
  // Don't use RED.group.removeFromGroup as that emits
24670
24884
  // a change event on the node - but we're deleting it
24671
24885
  var index = group.nodes.indexOf(node);
@@ -24687,8 +24901,8 @@ RED.view = (function() {
24687
24901
 
24688
24902
  // Groups must be removed in the right order - from inner-most
24689
24903
  // to outermost.
24690
- for (i = selectedGroups.length-1; i>=0; i--) {
24691
- var g = selectedGroups[i];
24904
+ for (i = movingSelectedGroups.length-1; i>=0; i--) {
24905
+ var g = movingSelectedGroups[i];
24692
24906
  removedGroups.push(g);
24693
24907
  RED.nodes.removeGroup(g);
24694
24908
  }
@@ -27105,8 +27319,8 @@ RED.view = (function() {
27105
27319
  var delta = Infinity;
27106
27320
  for (var i = 0; i < lineLength; i++) {
27107
27321
  var linePos = pathLine.getPointAtLength(i);
27108
- var posDeltaX = Math.abs(linePos.x-d3.event.offsetX)
27109
- var posDeltaY = Math.abs(linePos.y-d3.event.offsetY)
27322
+ var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor))
27323
+ var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor))
27110
27324
  var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY
27111
27325
  if (posDelta < delta) {
27112
27326
  pos = linePos
@@ -28440,7 +28654,7 @@ RED.view = (function() {
28440
28654
  }
28441
28655
  let badgeRDX = 0;
28442
28656
  let badgeLDX = 0;
28443
-
28657
+ const scale = RED.view.scale()
28444
28658
  for (let i=0,l=evt.el.__annotations__.length;i<l;i++) {
28445
28659
  const annotation = evt.el.__annotations__[i];
28446
28660
  if (annotations.hasOwnProperty(annotation.id)) {
@@ -28471,15 +28685,17 @@ RED.view = (function() {
28471
28685
  }
28472
28686
  if (isBadge) {
28473
28687
  if (showAnnotation) {
28474
- const rect = annotation.element.getBoundingClientRect();
28688
+ // getBoundingClientRect is in real-world scale so needs to be adjusted according to
28689
+ // the current scale factor
28690
+ const rectWidth = annotation.element.getBoundingClientRect().width / scale;
28475
28691
  let annotationX
28476
28692
  if (!opts.align || opts.align === 'right') {
28477
- annotationX = evt.node.w - 3 - badgeRDX - rect.width
28478
- badgeRDX += rect.width + 4;
28693
+ annotationX = evt.node.w - 3 - badgeRDX - rectWidth
28694
+ badgeRDX += rectWidth + 4;
28479
28695
 
28480
28696
  } else if (opts.align === 'left') {
28481
28697
  annotationX = 3 + badgeLDX
28482
- badgeLDX += rect.width + 4;
28698
+ badgeLDX += rectWidth + 4;
28483
28699
  }
28484
28700
  annotation.element.setAttribute("transform", "translate("+annotationX+", -8)");
28485
28701
  }
@@ -29870,18 +30086,27 @@ RED.view.tools = (function() {
29870
30086
  const paletteLabel = RED.utils.getPaletteLabel(n.type, nodeDef)
29871
30087
  const defaultNodeNameRE = new RegExp('^'+paletteLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')+' (\\d+)$')
29872
30088
  if (!typeIndex.hasOwnProperty(n.type)) {
29873
- const existingNodes = RED.nodes.filterNodes({type: n.type})
29874
- let maxNameNumber = 0;
29875
- existingNodes.forEach(n => {
29876
- let match = defaultNodeNameRE.exec(n.name)
30089
+ const existingNodes = RED.nodes.filterNodes({ type: n.type });
30090
+ const existingIds = existingNodes.reduce((ids, node) => {
30091
+ let match = defaultNodeNameRE.exec(node.name);
29877
30092
  if (match) {
29878
- let nodeNumber = parseInt(match[1])
29879
- if (nodeNumber > maxNameNumber) {
29880
- maxNameNumber = nodeNumber
30093
+ const nodeNumber = parseInt(match[1], 10);
30094
+ if (!ids.includes(nodeNumber)) {
30095
+ ids.push(nodeNumber);
29881
30096
  }
29882
30097
  }
29883
- })
29884
- typeIndex[n.type] = maxNameNumber + 1
30098
+ return ids;
30099
+ }, []).sort((a, b) => a - b);
30100
+
30101
+ let availableNameNumber = 1;
30102
+ for (let i = 0; i < existingIds.length; i++) {
30103
+ if (existingIds[i] !== availableNameNumber) {
30104
+ break;
30105
+ }
30106
+ availableNameNumber++;
30107
+ }
30108
+
30109
+ typeIndex[n.type] = availableNameNumber;
29885
30110
  }
29886
30111
  if ((options.renameBlank && n.name === '') || (options.renameClash && defaultNodeNameRE.test(n.name))) {
29887
30112
  if (generateHistory) {
@@ -29913,11 +30138,11 @@ RED.view.tools = (function() {
29913
30138
  }
29914
30139
  }
29915
30140
 
29916
- function addJunctionsToWires(wires) {
30141
+ function addJunctionsToWires(options = {}) {
29917
30142
  if (RED.workspaces.isLocked()) {
29918
30143
  return
29919
30144
  }
29920
- let wiresToSplit = wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
30145
+ let wiresToSplit = options.wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
29921
30146
  if (!wiresToSplit) {
29922
30147
  return
29923
30148
  }
@@ -29965,21 +30190,26 @@ RED.view.tools = (function() {
29965
30190
  if (links.length === 0) {
29966
30191
  return
29967
30192
  }
29968
- let pointCount = 0
29969
- links.forEach(function(l) {
29970
- if (l._sliceLocation) {
29971
- junction.x += l._sliceLocation.x
29972
- junction.y += l._sliceLocation.y
29973
- delete l._sliceLocation
29974
- pointCount++
29975
- } else {
29976
- junction.x += l.source.x + l.source.w/2 + l.target.x - l.target.w/2
29977
- junction.y += l.source.y + l.target.y
29978
- pointCount += 2
29979
- }
29980
- })
29981
- junction.x = Math.round(junction.x/pointCount)
29982
- junction.y = Math.round(junction.y/pointCount)
30193
+ if (addedJunctions.length === 0 && Object.hasOwn(options, 'x') && Object.hasOwn(options, 'y')) {
30194
+ junction.x = options.x
30195
+ junction.y = options.y
30196
+ } else {
30197
+ let pointCount = 0
30198
+ links.forEach(function(l) {
30199
+ if (l._sliceLocation) {
30200
+ junction.x += l._sliceLocation.x
30201
+ junction.y += l._sliceLocation.y
30202
+ delete l._sliceLocation
30203
+ pointCount++
30204
+ } else {
30205
+ junction.x += l.source.x + l.source.w/2 + l.target.x - l.target.w/2
30206
+ junction.y += l.source.y + l.target.y
30207
+ pointCount += 2
30208
+ }
30209
+ })
30210
+ junction.x = Math.round(junction.x/pointCount)
30211
+ junction.y = Math.round(junction.y/pointCount)
30212
+ }
29983
30213
  if (RED.view.snapGrid) {
29984
30214
  let gridSize = RED.view.gridSize()
29985
30215
  junction.x = (gridSize*Math.round(junction.x/gridSize));
@@ -30169,7 +30399,7 @@ RED.view.tools = (function() {
30169
30399
  RED.actions.add("core:wire-multiple-to-node", function() { wireMultipleToNode() })
30170
30400
 
30171
30401
  RED.actions.add("core:split-wire-with-link-nodes", function () { splitWiresWithLinkNodes() });
30172
- RED.actions.add("core:split-wires-with-junctions", function () { addJunctionsToWires() });
30402
+ RED.actions.add("core:split-wires-with-junctions", function (options) { addJunctionsToWires(options) });
30173
30403
 
30174
30404
  RED.actions.add("core:generate-node-names", generateNodeNames )
30175
30405
 
@@ -36156,6 +36386,20 @@ RED.editor = (function() {
36156
36386
  }
36157
36387
  }
36158
36388
 
36389
+ const oldCreds = {};
36390
+ if (editing_node._def.credentials) {
36391
+ for (const prop in editing_node._def.credentials) {
36392
+ if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) {
36393
+ if (editing_node._def.credentials[prop].type === 'password') {
36394
+ oldCreds['has_' + prop] = editing_node.credentials['has_' + prop];
36395
+ }
36396
+ if (prop in editing_node.credentials) {
36397
+ oldCreds[prop] = editing_node.credentials[prop];
36398
+ }
36399
+ }
36400
+ }
36401
+ }
36402
+
36159
36403
  try {
36160
36404
  const rc = editing_node._def.oneditsave.call(editing_node);
36161
36405
  if (rc === true) {
@@ -36187,6 +36431,25 @@ RED.editor = (function() {
36187
36431
  }
36188
36432
  }
36189
36433
  }
36434
+
36435
+ if (editing_node._def.credentials) {
36436
+ for (const prop in editing_node._def.credentials) {
36437
+ if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) {
36438
+ if (oldCreds[prop] !== editing_node.credentials[prop]) {
36439
+ if (editing_node.credentials[prop] === '__PWRD__') {
36440
+ // The password may not exist in oldCreds
36441
+ // The value '__PWRD__' means the password exists,
36442
+ // so ignore this change
36443
+ continue;
36444
+ }
36445
+ editState.changes.credentials = editState.changes.credentials || {};
36446
+ editState.changes.credentials['has_' + prop] = oldCreds['has_' + prop];
36447
+ editState.changes.credentials[prop] = oldCreds[prop];
36448
+ editState.changed = true;
36449
+ }
36450
+ }
36451
+ }
36452
+ }
36190
36453
  }
36191
36454
  }
36192
36455
 
@@ -36829,134 +37092,181 @@ RED.editor = (function() {
36829
37092
  },
36830
37093
  {
36831
37094
  id: "node-config-dialog-ok",
36832
- text: adding?RED._("editor.configAdd"):RED._("editor.configUpdate"),
37095
+ text: adding ? RED._("editor.configAdd") : RED._("editor.configUpdate"),
36833
37096
  class: "primary",
36834
37097
  click: function() {
36835
- var editState = {
37098
+ // TODO: Already defined
37099
+ const configProperty = name;
37100
+ const configType = type;
37101
+ const configTypeDef = RED.nodes.getType(configType);
37102
+
37103
+ const wasChanged = editing_config_node.changed;
37104
+ const editState = {
36836
37105
  changes: {},
36837
37106
  changed: false,
36838
37107
  outputMap: null
36839
37108
  };
36840
- var configProperty = name;
36841
- var configId = editing_config_node.id;
36842
- var configType = type;
36843
- var configAdding = adding;
36844
- var configTypeDef = RED.nodes.getType(configType);
36845
- var d;
36846
- var input;
36847
-
36848
- if (configTypeDef.oneditsave) {
36849
- try {
36850
- configTypeDef.oneditsave.call(editing_config_node);
36851
- } catch(err) {
36852
- console.warn("oneditsave",editing_config_node.id,editing_config_node.type,err.toString());
36853
- }
36854
- }
36855
-
36856
- for (d in configTypeDef.defaults) {
36857
- if (configTypeDef.defaults.hasOwnProperty(d)) {
36858
- var newValue;
36859
- input = $("#node-config-input-"+d);
36860
- if (input.attr('type') === "checkbox") {
36861
- newValue = input.prop('checked');
36862
- } else if ("format" in configTypeDef.defaults[d] && configTypeDef.defaults[d].format !== "" && input[0].nodeName === "DIV") {
36863
- newValue = input.text();
36864
- } else {
36865
- newValue = input.val();
36866
- }
36867
- if (newValue != null && newValue !== editing_config_node[d]) {
36868
- if (editing_config_node._def.defaults[d].type) {
36869
- if (newValue == "_ADD_") {
36870
- newValue = "";
36871
- }
36872
- // Change to a related config node
36873
- var configNode = RED.nodes.node(editing_config_node[d]);
36874
- if (configNode) {
36875
- var users = configNode.users;
36876
- users.splice(users.indexOf(editing_config_node),1);
36877
- RED.events.emit("nodes:change",configNode);
36878
- }
36879
- configNode = RED.nodes.node(newValue);
36880
- if (configNode) {
36881
- configNode.users.push(editing_config_node);
36882
- RED.events.emit("nodes:change",configNode);
36883
- }
36884
- }
36885
- editing_config_node[d] = newValue;
36886
- }
36887
- }
36888
- }
37109
+
37110
+ // Call `oneditsave` and search for changes
37111
+ handleEditSave(editing_config_node, editState);
36889
37112
 
36890
- activeEditPanes.forEach(function(pane) {
37113
+ // Search for changes in the edit box (panes)
37114
+ activeEditPanes.forEach(function (pane) {
36891
37115
  if (pane.apply) {
36892
37116
  pane.apply.call(pane, editState);
36893
37117
  }
36894
- })
36895
-
36896
- editing_config_node.label = configTypeDef.label;
37118
+ });
36897
37119
 
36898
- var scope = $("#red-ui-editor-config-scope").val();
36899
- editing_config_node.z = scope;
37120
+ // TODO: Why?
37121
+ editing_config_node.label = configTypeDef.label
36900
37122
 
37123
+ // Check if disabled has changed
36901
37124
  if ($("#node-config-input-node-disabled").prop('checked')) {
36902
37125
  if (editing_config_node.d !== true) {
37126
+ editState.changes.d = editing_config_node.d;
37127
+ editState.changed = true;
36903
37128
  editing_config_node.d = true;
36904
37129
  }
36905
37130
  } else {
36906
37131
  if (editing_config_node.d === true) {
37132
+ editState.changes.d = editing_config_node.d;
37133
+ editState.changed = true;
36907
37134
  delete editing_config_node.d;
36908
37135
  }
36909
37136
  }
36910
37137
 
37138
+ // NOTE: must be undefined if no scope used
37139
+ const scope = $("#red-ui-editor-config-scope").val() || undefined;
37140
+
37141
+ // Check if the scope has changed
37142
+ if (editing_config_node.z !== scope) {
37143
+ editState.changes.z = editing_config_node.z;
37144
+ editState.changed = true;
37145
+ editing_config_node.z = scope;
37146
+ }
37147
+
37148
+ // Search for nodes that use this config node that are no longer
37149
+ // in scope, so must be removed
37150
+ const historyEvents = [];
36911
37151
  if (scope) {
36912
- // Search for nodes that use this one that are no longer
36913
- // in scope, so must be removed
36914
- editing_config_node.users = editing_config_node.users.filter(function(n) {
36915
- var keep = true;
36916
- for (var d in n._def.defaults) {
36917
- if (n._def.defaults.hasOwnProperty(d)) {
36918
- if (n._def.defaults[d].type === editing_config_node.type &&
36919
- n[d] === editing_config_node.id &&
36920
- n.z !== scope) {
36921
- keep = false;
36922
- // Remove the reference to this node
36923
- // and revalidate
36924
- n[d] = null;
36925
- n.dirty = true;
36926
- n.changed = true;
36927
- validateNode(n);
37152
+ const newUsers = editing_config_node.users.filter(function (node) {
37153
+ let keepNode = false;
37154
+ let nodeModified = null;
37155
+
37156
+ for (const d in node._def.defaults) {
37157
+ if (node._def.defaults.hasOwnProperty(d)) {
37158
+ if (node._def.defaults[d].type === editing_config_node.type) {
37159
+ if (node[d] === editing_config_node.id) {
37160
+ if (node.z === editing_config_node.z) {
37161
+ // The node is kept only if at least one property uses
37162
+ // this config node in the correct scope.
37163
+ keepNode = true;
37164
+ } else {
37165
+ if (!nodeModified) {
37166
+ nodeModified = {
37167
+ t: "edit",
37168
+ node: node,
37169
+ changes: { [d]: node[d] },
37170
+ changed: node.changed,
37171
+ dirty: node.dirty
37172
+ };
37173
+ } else {
37174
+ nodeModified.changes[d] = node[d];
37175
+ }
37176
+
37177
+ // Remove the reference to the config node
37178
+ node[d] = "";
37179
+ }
37180
+ }
36928
37181
  }
36929
37182
  }
36930
37183
  }
36931
- return keep;
37184
+
37185
+ // Add the node modified to the history
37186
+ if (nodeModified) {
37187
+ historyEvents.push(nodeModified);
37188
+ }
37189
+
37190
+ // Mark as changed and revalidate this node
37191
+ if (!keepNode) {
37192
+ node.changed = true;
37193
+ node.dirty = true;
37194
+ validateNode(node);
37195
+ RED.events.emit("nodes:change", node);
37196
+ }
37197
+
37198
+ return keepNode;
36932
37199
  });
37200
+
37201
+ // Check if users are changed
37202
+ if (editing_config_node.users.length !== newUsers.length) {
37203
+ editState.changes.users = editing_config_node.users;
37204
+ editState.changed = true;
37205
+ editing_config_node.users = newUsers;
37206
+ }
36933
37207
  }
36934
37208
 
36935
- if (configAdding) {
36936
- RED.nodes.add(editing_config_node);
37209
+ if (editState.changed) {
37210
+ // Set the congig node as changed
37211
+ editing_config_node.changed = true;
36937
37212
  }
36938
37213
 
37214
+ // Now, validate the config node
36939
37215
  validateNode(editing_config_node);
36940
- var validatedNodes = {};
36941
- validatedNodes[editing_config_node.id] = true;
36942
37216
 
36943
- var userStack = editing_config_node.users.slice();
36944
- while(userStack.length > 0) {
36945
- var user = userStack.pop();
36946
- if (!validatedNodes[user.id]) {
36947
- validatedNodes[user.id] = true;
36948
- if (user.users) {
36949
- userStack = userStack.concat(user.users);
37217
+ // And validate nodes using this config node too
37218
+ const validatedNodes = new Set();
37219
+ const userStack = editing_config_node.users.slice();
37220
+
37221
+ validatedNodes.add(editing_config_node.id);
37222
+ while (userStack.length) {
37223
+ const node = userStack.pop();
37224
+ if (!validatedNodes.has(node.id)) {
37225
+ validatedNodes.add(node.id);
37226
+ if (node.users) {
37227
+ userStack.push(...node.users);
36950
37228
  }
36951
- validateNode(user);
37229
+ validateNode(node);
36952
37230
  }
36953
37231
  }
36954
- RED.nodes.dirty(true);
36955
- RED.view.redraw(true);
36956
- if (!configAdding) {
36957
- RED.events.emit("editor:save",editing_config_node);
36958
- RED.events.emit("nodes:change",editing_config_node);
37232
+
37233
+ let historyEvent = {
37234
+ t: "edit",
37235
+ node: editing_config_node,
37236
+ changes: editState.changes,
37237
+ changed: wasChanged,
37238
+ dirty: RED.nodes.dirty()
37239
+ };
37240
+
37241
+ if (historyEvents.length) {
37242
+ // Need a multi events
37243
+ historyEvent = {
37244
+ t: "multi",
37245
+ events: [historyEvent].concat(historyEvents),
37246
+ dirty: historyEvent.dirty
37247
+ };
37248
+ }
37249
+
37250
+ if (!adding) {
37251
+ // This event is triggered when the edit box is saved,
37252
+ // regardless of whether there are any modifications.
37253
+ RED.events.emit("editor:save", editing_config_node);
37254
+ }
37255
+
37256
+ if (editState.changed) {
37257
+ if (adding) {
37258
+ RED.history.push({ t: "add", nodes: [editing_config_node.id], dirty: RED.nodes.dirty() });
37259
+ // Add the new config node and trigger the `nodes:add` event
37260
+ RED.nodes.add(editing_config_node);
37261
+ } else {
37262
+ RED.history.push(historyEvent);
37263
+ RED.events.emit("nodes:change", editing_config_node);
37264
+ }
37265
+
37266
+ RED.nodes.dirty(true);
37267
+ RED.view.redraw(true);
36959
37268
  }
37269
+
36960
37270
  RED.tray.close(function() {
36961
37271
  var filter = null;
36962
37272
  // when editing a config via subflow edit panel, the `configProperty` will not
@@ -38238,10 +38548,31 @@ RED.editor = (function() {
38238
38548
  apply: function(editState) {
38239
38549
  var old_env = node.env;
38240
38550
  var new_env = [];
38551
+
38241
38552
  if (/^subflow:/.test(node.type)) {
38553
+ // Get the list of environment variables from the node properties
38242
38554
  new_env = RED.subflow.exportSubflowInstanceEnv(node);
38243
38555
  }
38244
38556
 
38557
+ if (old_env && old_env.length) {
38558
+ old_env.forEach(function (prop) {
38559
+ if (prop.type === "conf-type" && prop.value) {
38560
+ const stillInUse = new_env?.some((p) => p.type === "conf-type" && p.name === prop.name && p.value === prop.value);
38561
+ if (!stillInUse) {
38562
+ // Remove the node from the config node users
38563
+ // Only for empty value or modified
38564
+ const configNode = RED.nodes.node(prop.value);
38565
+ if (configNode) {
38566
+ if (configNode.users.indexOf(node) !== -1) {
38567
+ configNode.users.splice(configNode.users.indexOf(node), 1);
38568
+ RED.events.emit('nodes:change', configNode)
38569
+ }
38570
+ }
38571
+ }
38572
+ }
38573
+ });
38574
+ }
38575
+
38245
38576
  // Get the values from the Properties table tab
38246
38577
  var items = this.list.editableList('items');
38247
38578
  items.each(function (i,el) {
@@ -38259,7 +38590,6 @@ RED.editor = (function() {
38259
38590
  }
38260
38591
  });
38261
38592
 
38262
-
38263
38593
  if (new_env && new_env.length > 0) {
38264
38594
  new_env.forEach(function(prop) {
38265
38595
  if (prop.type === "cred") {
@@ -38270,6 +38600,15 @@ RED.editor = (function() {
38270
38600
  editState.changed = true;
38271
38601
  }
38272
38602
  delete prop.value;
38603
+ } else if (prop.type === "conf-type" && prop.value) {
38604
+ const configNode = RED.nodes.node(prop.value);
38605
+ if (configNode) {
38606
+ if (configNode.users.indexOf(node) === -1) {
38607
+ // Add the node to the config node users
38608
+ configNode.users.push(node);
38609
+ RED.events.emit('nodes:change', configNode);
38610
+ }
38611
+ }
38273
38612
  }
38274
38613
  });
38275
38614
  }
@@ -38395,6 +38734,7 @@ RED.editor = (function() {
38395
38734
  apply: function(editState) {
38396
38735
  var newValue;
38397
38736
  var d;
38737
+ // If the node is a subflow, the node's properties (exepts name) are saved by `envProperties`
38398
38738
  if (node._def.defaults) {
38399
38739
  for (d in node._def.defaults) {
38400
38740
  if (node._def.defaults.hasOwnProperty(d)) {
@@ -38482,9 +38822,16 @@ RED.editor = (function() {
38482
38822
  }
38483
38823
  }
38484
38824
  if (node._def.credentials) {
38485
- var credDefinition = node._def.credentials;
38486
- var credsChanged = updateNodeCredentials(node,credDefinition,this.inputClass);
38487
- editState.changed = editState.changed || credsChanged;
38825
+ const credDefinition = node._def.credentials;
38826
+ const credChanges = updateNodeCredentials(node, credDefinition, this.inputClass);
38827
+
38828
+ if (Object.keys(credChanges).length) {
38829
+ editState.changed = true;
38830
+ editState.changes.credentials = {
38831
+ ...(editState.changes.credentials || {}),
38832
+ ...credChanges
38833
+ };
38834
+ }
38488
38835
  }
38489
38836
  }
38490
38837
  }
@@ -38512,10 +38859,11 @@ RED.editor = (function() {
38512
38859
  * @param node - the node containing the credentials
38513
38860
  * @param credDefinition - definition of the credentials
38514
38861
  * @param prefix - prefix of the input fields
38515
- * @return {boolean} whether anything has changed
38862
+ * @return {object} an object containing the modified properties
38516
38863
  */
38517
38864
  function updateNodeCredentials(node, credDefinition, prefix) {
38518
- var changed = false;
38865
+ const changes = {};
38866
+
38519
38867
  if (!node.credentials) {
38520
38868
  node.credentials = {_:{}};
38521
38869
  } else if (!node.credentials._) {
@@ -38528,24 +38876,35 @@ RED.editor = (function() {
38528
38876
  if (input.length > 0) {
38529
38877
  var value = input.val();
38530
38878
  if (credDefinition[cred].type == 'password') {
38531
- node.credentials['has_' + cred] = (value !== "");
38532
- if (value == '__PWRD__') {
38533
- continue;
38879
+ if (value === '__PWRD__') {
38880
+ // A cred value exists - no changes
38881
+ } else if (value === '' && node.credentials['has_' + cred] === false) {
38882
+ // Empty cred value exists - no changes
38883
+ } else if (value === node.credentials[cred]) {
38884
+ // A cred value exists locally in the editor - no changes
38885
+ // Like the user sets a value, saves the config,
38886
+ // reopens the config and save the config again
38887
+ } else {
38888
+ changes['has_' + cred] = node.credentials['has_' + cred];
38889
+ changes[cred] = node.credentials[cred];
38890
+ node.credentials[cred] = value;
38534
38891
  }
38535
- changed = true;
38536
38892
 
38537
- }
38538
- node.credentials[cred] = value;
38539
- if (value != node.credentials._[cred]) {
38540
- changed = true;
38893
+ node.credentials['has_' + cred] = (value !== '');
38894
+ } else {
38895
+ // Since these creds are loaded by the editor,
38896
+ // values can be directly compared
38897
+ if (value !== node.credentials[cred]) {
38898
+ changes[cred] = node.credentials[cred];
38899
+ node.credentials[cred] = value;
38900
+ }
38541
38901
  }
38542
38902
  }
38543
38903
  }
38544
38904
  }
38545
- return changed;
38546
- }
38547
-
38548
38905
 
38906
+ return changes;
38907
+ }
38549
38908
  })();
38550
38909
  ;(function() {
38551
38910
  var _subflowModulePaneTemplate = '<form class="dialog-form form-horizontal" autocomplete="off">'+
@@ -39400,7 +39759,7 @@ RED.editor = (function() {
39400
39759
  nameField.trigger('change');
39401
39760
  }
39402
39761
  },
39403
- sortable: ".red-ui-editableList-item-handle",
39762
+ sortable: true,
39404
39763
  removable: false
39405
39764
  });
39406
39765
  var parentEnv = {};
@@ -44109,6 +44468,30 @@ RED.clipboard = (function() {
44109
44468
  },100);
44110
44469
  }
44111
44470
 
44471
+ /**
44472
+ * Validates if the provided string looks like valid flow json
44473
+ * @param {string} flowString the string to validate
44474
+ * @returns If valid, returns the node array
44475
+ */
44476
+ function validateFlowString(flowString) {
44477
+ const res = JSON.parse(flowString)
44478
+ if (!Array.isArray(res)) {
44479
+ throw new Error(RED._("clipboard.import.errors.notArray"));
44480
+ }
44481
+ for (let i = 0; i < res.length; i++) {
44482
+ if (typeof res[i] !== "object") {
44483
+ throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i}));
44484
+ }
44485
+ if (!Object.hasOwn(res[i], 'id')) {
44486
+ throw new Error(RED._("clipboard.import.errors.missingId",{index:i}));
44487
+ }
44488
+ if (!Object.hasOwn(res[i], 'type')) {
44489
+ throw new Error(RED._("clipboard.import.errors.missingType",{index:i}));
44490
+ }
44491
+ }
44492
+ return res
44493
+ }
44494
+
44112
44495
  var validateImportTimeout;
44113
44496
  function validateImport() {
44114
44497
  if (activeTab === "red-ui-clipboard-dialog-import-tab-clipboard") {
@@ -44126,21 +44509,7 @@ RED.clipboard = (function() {
44126
44509
  return;
44127
44510
  }
44128
44511
  try {
44129
- if (!/^\[[\s\S]*\]$/m.test(v)) {
44130
- throw new Error(RED._("clipboard.import.errors.notArray"));
44131
- }
44132
- var res = JSON.parse(v);
44133
- for (var i=0;i<res.length;i++) {
44134
- if (typeof res[i] !== "object") {
44135
- throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i}));
44136
- }
44137
- if (!res[i].hasOwnProperty('id')) {
44138
- throw new Error(RED._("clipboard.import.errors.missingId",{index:i}));
44139
- }
44140
- if (!res[i].hasOwnProperty('type')) {
44141
- throw new Error(RED._("clipboard.import.errors.missingType",{index:i}));
44142
- }
44143
- }
44512
+ validateFlowString(v)
44144
44513
  currentPopoverError = null;
44145
44514
  popover.close(true);
44146
44515
  importInput.removeClass("input-error");
@@ -44773,16 +45142,16 @@ RED.clipboard = (function() {
44773
45142
  }
44774
45143
 
44775
45144
  function importNodes(nodesStr,addFlow) {
44776
- var newNodes = nodesStr;
45145
+ let newNodes = nodesStr;
44777
45146
  if (typeof nodesStr === 'string') {
44778
45147
  try {
44779
45148
  nodesStr = nodesStr.trim();
44780
45149
  if (nodesStr.length === 0) {
44781
45150
  return;
44782
45151
  }
44783
- newNodes = JSON.parse(nodesStr);
45152
+ newNodes = validateFlowString(nodesStr)
44784
45153
  } catch(err) {
44785
- var e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
45154
+ const e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
44786
45155
  e.code = "NODE_RED";
44787
45156
  throw e;
44788
45157
  }
@@ -45117,6 +45486,7 @@ RED.clipboard = (function() {
45117
45486
  }
45118
45487
  }
45119
45488
  } catch(err) {
45489
+ console.warn('Import failed: ', err)
45120
45490
  // Ensure any errors throw above doesn't stop the drop target from
45121
45491
  // being hidden.
45122
45492
  }
@@ -47081,15 +47451,15 @@ RED.search = (function() {
47081
47451
  }
47082
47452
  }
47083
47453
 
47454
+ const scale = RED.view.scale()
47084
47455
  const offset = $("#red-ui-workspace-chart").offset()
47085
-
47086
- let addX = options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()
47087
- let addY = options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()
47456
+ let addX = (options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()) / scale
47457
+ let addY = (options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()) / scale
47088
47458
 
47089
47459
  if (RED.view.snapGrid) {
47090
47460
  const gridSize = RED.view.gridSize()
47091
- addX = gridSize * Math.floor(addX / gridSize)
47092
- addY = gridSize * Math.floor(addY / gridSize)
47461
+ addX = gridSize * Math.round(addX / gridSize)
47462
+ addY = gridSize * Math.round(addY / gridSize)
47093
47463
  }
47094
47464
 
47095
47465
  if (RED.settings.theme("menu.menu-item-action-list", true)) {
@@ -47114,7 +47484,9 @@ RED.search = (function() {
47114
47484
  },
47115
47485
  (hasLinks) ? { // has least 1 wire selected
47116
47486
  label: RED._("contextMenu.junction"),
47117
- onselect: 'core:split-wires-with-junctions',
47487
+ onselect: function () {
47488
+ RED.actions.invoke('core:split-wires-with-junctions', { x: addX, y: addY })
47489
+ },
47118
47490
  disabled: !canEdit || !hasLinks
47119
47491
  } : {
47120
47492
  label: RED._("contextMenu.junction"),
@@ -47924,7 +48296,7 @@ RED.actionList = (function() {
47924
48296
  var items = [];
47925
48297
  RED.nodes.registry.getNodeTypes().forEach(function(t) {
47926
48298
  var def = RED.nodes.getType(t);
47927
- if (def.category !== 'config' && t !== 'unknown' && t !== 'tab') {
48299
+ if (def.set?.enabled !== false && def.category !== 'config' && t !== 'unknown' && t !== 'tab') {
47928
48300
  items.push({type:t,def: def, label:getTypeLabel(t,def)});
47929
48301
  }
47930
48302
  });
@@ -49351,7 +49723,7 @@ RED.subflow = (function() {
49351
49723
  item.value = ""+input.prop("checked");
49352
49724
  break;
49353
49725
  case "conf-types":
49354
- item.value = input.val()
49726
+ item.value = input.val() === "_ADD_" ? "" : input.val();
49355
49727
  item.type = "conf-type"
49356
49728
  }
49357
49729
  if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) {