@node-red/editor-client 4.0.5 → 4.0.6

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
@@ -4495,7 +4495,13 @@ RED.nodes = (function() {
4495
4495
 
4496
4496
  var exports = {
4497
4497
  setModulePendingUpdated: function(module,version) {
4498
- moduleList[module].pending_version = version;
4498
+ if (!!RED.plugins.getModule(module)) {
4499
+ // The module updated is a plugin
4500
+ RED.plugins.getModule(module).pending_version = version;
4501
+ } else {
4502
+ moduleList[module].pending_version = version;
4503
+ }
4504
+
4499
4505
  RED.events.emit("registry:module-updated",{module:module,version:version});
4500
4506
  },
4501
4507
  getModule: function(module) {
@@ -5124,11 +5130,11 @@ RED.nodes = (function() {
5124
5130
  n["_"] = RED._;
5125
5131
  }
5126
5132
  if (n._def.category == "config") {
5127
- configNodes[n.id] = n;
5133
+ configNodes[n.id] = newNode;
5128
5134
  } else {
5129
5135
  if (n.wires && (n.wires.length > n.outputs)) { n.outputs = n.wires.length; }
5130
5136
  n.dirty = true;
5131
- updateConfigNodeUsers(n);
5137
+ updateConfigNodeUsers(newNode, { action: "add" });
5132
5138
  if (n._def.category == "subflows" && typeof n.i === "undefined") {
5133
5139
  var nextId = 0;
5134
5140
  RED.nodes.eachNode(function(node) {
@@ -5201,6 +5207,7 @@ RED.nodes = (function() {
5201
5207
  delete nodeLinks[id];
5202
5208
  removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); });
5203
5209
  removedLinks.forEach(removeLink);
5210
+ updateConfigNodeUsers(node, { action: "remove" });
5204
5211
  var updatedConfigNode = false;
5205
5212
  for (var d in node._def.defaults) {
5206
5213
  if (node._def.defaults.hasOwnProperty(d)) {
@@ -5214,10 +5221,6 @@ RED.nodes = (function() {
5214
5221
  if (configNode._def.exclusive) {
5215
5222
  removeNode(node[d]);
5216
5223
  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
5224
  }
5222
5225
  }
5223
5226
  }
@@ -5454,23 +5457,34 @@ RED.nodes = (function() {
5454
5457
  return {nodes:removedNodes,links:removedLinks, groups: removedGroups, junctions: removedJunctions};
5455
5458
  }
5456
5459
 
5460
+ /**
5461
+ * Add a Subflow to the Workspace
5462
+ *
5463
+ * @param {object} sf The Subflow to add.
5464
+ * @param {boolean|undefined} createNewIds Whether to update the name.
5465
+ */
5457
5466
  function addSubflow(sf, createNewIds) {
5458
5467
  if (createNewIds) {
5459
- var subflowNames = Object.keys(subflows).map(function(sfid) {
5460
- return subflows[sfid].name;
5461
- });
5468
+ // Update the Subflow name to highlight that this is a copy
5469
+ const subflowNames = Object.keys(subflows).map(function (sfid) {
5470
+ return subflows[sfid].name || "";
5471
+ })
5472
+ subflowNames.sort()
5462
5473
 
5463
- subflowNames.sort();
5464
- var copyNumber = 1;
5465
- var subflowName = sf.name;
5474
+ let copyNumber = 1;
5475
+ let subflowName = sf.name;
5466
5476
  subflowNames.forEach(function(name) {
5467
5477
  if (subflowName == name) {
5478
+ subflowName = sf.name + " (" + copyNumber + ")";
5468
5479
  copyNumber++;
5469
- subflowName = sf.name+" ("+copyNumber+")";
5470
5480
  }
5471
5481
  });
5482
+
5472
5483
  sf.name = subflowName;
5473
5484
  }
5485
+
5486
+ sf.instances = [];
5487
+
5474
5488
  subflows[sf.id] = sf;
5475
5489
  allNodes.addTab(sf.id);
5476
5490
  linkTabMap[sf.id] = [];
@@ -5523,7 +5537,7 @@ RED.nodes = (function() {
5523
5537
  module: "node-red"
5524
5538
  }
5525
5539
  });
5526
- sf.instances = [];
5540
+
5527
5541
  sf._def = RED.nodes.getType("subflow:"+sf.id);
5528
5542
  RED.events.emit("subflows:add",sf);
5529
5543
  }
@@ -6165,7 +6179,8 @@ RED.nodes = (function() {
6165
6179
  // Remove the old subflow definition - but leave the instances in place
6166
6180
  var removalResult = RED.subflow.removeSubflow(n.id, true);
6167
6181
  // Create the list of nodes for the new subflow def
6168
- var subflowNodes = [n].concat(zMap[n.id]);
6182
+ // Need to sort the list in order to remove missing nodes
6183
+ var subflowNodes = [n].concat(zMap[n.id]).filter((s) => !!s);
6169
6184
  // Import the new subflow - no clashes should occur as we've removed
6170
6185
  // the old version
6171
6186
  var result = importNodes(subflowNodes);
@@ -6202,9 +6217,20 @@ RED.nodes = (function() {
6202
6217
  // Replace config nodes
6203
6218
  //
6204
6219
  configNodeIds.forEach(function(id) {
6205
- removedNodes = removedNodes.concat(convertNode(getNode(id)));
6220
+ const configNode = getNode(id);
6221
+ const currentUserCount = configNode.users;
6222
+
6223
+ // Add a snapshot of the Config Node
6224
+ removedNodes = removedNodes.concat(convertNode(configNode));
6225
+
6226
+ // Remove the Config Node instance
6206
6227
  removeNode(id);
6207
- importNodes([newConfigNodes[id]])
6228
+
6229
+ // Import the new one
6230
+ importNodes([newConfigNodes[id]]);
6231
+
6232
+ // Re-attributes the user count
6233
+ getNode(id).users = currentUserCount;
6208
6234
  });
6209
6235
 
6210
6236
  return {
@@ -6445,6 +6471,8 @@ RED.nodes = (function() {
6445
6471
  if (matchingSubflow) {
6446
6472
  subflow_denylist[n.id] = matchingSubflow;
6447
6473
  } else {
6474
+ const oldId = n.id;
6475
+
6448
6476
  subflow_map[n.id] = n;
6449
6477
  if (createNewIds || options.importMap[n.id] === "copy") {
6450
6478
  nid = getID();
@@ -6472,7 +6500,7 @@ RED.nodes = (function() {
6472
6500
  n.status.id = getID();
6473
6501
  }
6474
6502
  new_subflows.push(n);
6475
- addSubflow(n,createNewIds || options.importMap[n.id] === "copy");
6503
+ addSubflow(n,createNewIds || options.importMap[oldId] === "copy");
6476
6504
  }
6477
6505
  }
6478
6506
  }
@@ -6592,7 +6620,7 @@ RED.nodes = (function() {
6592
6620
  x:parseFloat(n.x || 0),
6593
6621
  y:parseFloat(n.y || 0),
6594
6622
  z:n.z,
6595
- type:0,
6623
+ type: n.type,
6596
6624
  info: n.info,
6597
6625
  changed:false,
6598
6626
  _config:{}
@@ -6653,7 +6681,6 @@ RED.nodes = (function() {
6653
6681
  }
6654
6682
  }
6655
6683
  }
6656
- node.type = n.type;
6657
6684
  node._def = def;
6658
6685
  if (node.type === "group") {
6659
6686
  node._def = RED.group.def;
@@ -6683,6 +6710,15 @@ RED.nodes = (function() {
6683
6710
  outputs: n.outputs|| (n.wires && n.wires.length) || 0,
6684
6711
  set: registry.getNodeSet("node-red/unknown")
6685
6712
  }
6713
+ var orig = {};
6714
+ for (var p in n) {
6715
+ if (n.hasOwnProperty(p) && p!="x" && p!="y" && p!="z" && p!="id" && p!="wires") {
6716
+ orig[p] = n[p];
6717
+ }
6718
+ }
6719
+ node._orig = orig;
6720
+ node.name = n.type;
6721
+ node.type = "unknown";
6686
6722
  } else {
6687
6723
  if (subflow_denylist[parentId] || createNewIds || options.importMap[n.id] === "copy") {
6688
6724
  parentId = subflow.id;
@@ -6743,29 +6779,31 @@ RED.nodes = (function() {
6743
6779
  node.type = "unknown";
6744
6780
  }
6745
6781
  if (node._def.category != "config") {
6746
- if (n.hasOwnProperty('inputs')) {
6747
- node.inputs = n.inputs;
6782
+ if (n.hasOwnProperty('inputs') && def.defaults.hasOwnProperty("inputs")) {
6783
+ node.inputs = parseInt(n.inputs, 10);
6748
6784
  node._config.inputs = JSON.stringify(n.inputs);
6749
6785
  } else {
6750
6786
  node.inputs = node._def.inputs;
6751
6787
  }
6752
- if (n.hasOwnProperty('outputs')) {
6753
- node.outputs = n.outputs;
6788
+ if (n.hasOwnProperty('outputs') && def.defaults.hasOwnProperty("outputs")) {
6789
+ node.outputs = parseInt(n.outputs, 10);
6754
6790
  node._config.outputs = JSON.stringify(n.outputs);
6755
6791
  } else {
6756
6792
  node.outputs = node._def.outputs;
6757
6793
  }
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
6794
+
6795
+ // The node declares outputs in its defaults, but has not got a valid value
6796
+ // Defer to the length of the wires array
6797
+ if (node.hasOwnProperty('wires')) {
6798
+ if (isNaN(node.outputs)) {
6766
6799
  node.outputs = node.wires.length;
6800
+ } else if (node.wires.length > node.outputs) {
6801
+ // If 'wires' is longer than outputs, clip wires
6802
+ console.log("Warning: node.wires longer than node.outputs - trimming wires:", node.id, " wires:", node.wires.length, " outputs:", node.outputs);
6803
+ node.wires = node.wires.slice(0, node.outputs);
6767
6804
  }
6768
6805
  }
6806
+
6769
6807
  for (d in node._def.defaults) {
6770
6808
  if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') {
6771
6809
  node[d] = n[d];
@@ -6862,11 +6900,6 @@ RED.nodes = (function() {
6862
6900
  nodeList = nodeList.map(function(id) {
6863
6901
  var node = node_map[id];
6864
6902
  if (node) {
6865
- if (node._def.category === 'config') {
6866
- if (node.users.indexOf(n) === -1) {
6867
- node.users.push(n);
6868
- }
6869
- }
6870
6903
  return node.id;
6871
6904
  }
6872
6905
  return id;
@@ -6880,9 +6913,11 @@ RED.nodes = (function() {
6880
6913
  n = new_subflows[i];
6881
6914
  n.in.forEach(function(input) {
6882
6915
  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);
6916
+ if (node_map.hasOwnProperty(wire.id)) {
6917
+ var link = {source:input, sourcePort:0, target:node_map[wire.id]};
6918
+ addLink(link);
6919
+ new_links.push(link);
6920
+ }
6886
6921
  });
6887
6922
  delete input.wires;
6888
6923
  });
@@ -6891,11 +6926,13 @@ RED.nodes = (function() {
6891
6926
  var link;
6892
6927
  if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) {
6893
6928
  link = {source:n.in[wire.port], sourcePort:wire.port,target:output};
6894
- } else {
6929
+ } else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) {
6895
6930
  link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:output};
6896
6931
  }
6897
- addLink(link);
6898
- new_links.push(link);
6932
+ if (link) {
6933
+ addLink(link);
6934
+ new_links.push(link);
6935
+ }
6899
6936
  });
6900
6937
  delete output.wires;
6901
6938
  });
@@ -6904,11 +6941,13 @@ RED.nodes = (function() {
6904
6941
  var link;
6905
6942
  if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) {
6906
6943
  link = {source:n.in[wire.port], sourcePort:wire.port,target:n.status};
6907
- } else {
6944
+ } else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) {
6908
6945
  link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:n.status};
6909
6946
  }
6910
- addLink(link);
6911
- new_links.push(link);
6947
+ if (link) {
6948
+ addLink(link);
6949
+ new_links.push(link);
6950
+ }
6912
6951
  });
6913
6952
  delete n.status.wires;
6914
6953
  }
@@ -7087,25 +7126,78 @@ RED.nodes = (function() {
7087
7126
  return result;
7088
7127
  }
7089
7128
 
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];
7129
+ /**
7130
+ * Update any config nodes referenced by the provided node to ensure
7131
+ * their 'users' list is correct.
7132
+ *
7133
+ * @param {object} node The node in which to check if it contains references
7134
+ * @param {object} options Options to apply.
7135
+ * @param {"add" | "remove"} [options.action] Add or remove the node from
7136
+ * the Config Node users list. Default `add`.
7137
+ * @param {boolean} [options.emitEvent] Emit the `nodes:changes` event.
7138
+ * Default true.
7139
+ */
7140
+ function updateConfigNodeUsers(node, options) {
7141
+ const defaultOptions = { action: "add", emitEvent: true };
7142
+ options = Object.assign({}, defaultOptions, options);
7143
+
7144
+ for (var d in node._def.defaults) {
7145
+ if (node._def.defaults.hasOwnProperty(d)) {
7146
+ var property = node._def.defaults[d];
7095
7147
  if (property.type) {
7096
7148
  var type = registry.getNodeType(property.type);
7097
7149
  if (type && type.category == "config") {
7098
- var configNode = configNodes[n[d]];
7150
+ var configNode = configNodes[node[d]];
7099
7151
  if (configNode) {
7100
- if (configNode.users.indexOf(n) === -1) {
7101
- configNode.users.push(n);
7102
- RED.events.emit('nodes:change',configNode)
7152
+ if (options.action === "add") {
7153
+ if (configNode.users.indexOf(node) === -1) {
7154
+ configNode.users.push(node);
7155
+ if (options.emitEvent) {
7156
+ RED.events.emit('nodes:change', configNode);
7157
+ }
7158
+ }
7159
+ } else if (options.action === "remove") {
7160
+ if (configNode.users.indexOf(node) !== -1) {
7161
+ const users = configNode.users;
7162
+ users.splice(users.indexOf(node), 1);
7163
+ if (options.emitEvent) {
7164
+ RED.events.emit('nodes:change', configNode);
7165
+ }
7166
+ }
7103
7167
  }
7104
7168
  }
7105
7169
  }
7106
7170
  }
7107
7171
  }
7108
7172
  }
7173
+
7174
+ // Subflows can have config node env
7175
+ if (node.type.indexOf("subflow:") === 0) {
7176
+ node.env?.forEach((prop) => {
7177
+ if (prop.type === "conf-type" && prop.value) {
7178
+ // Add the node to the config node users
7179
+ const configNode = getNode(prop.value);
7180
+ if (configNode) {
7181
+ if (options.action === "add") {
7182
+ if (configNode.users.indexOf(node) === -1) {
7183
+ configNode.users.push(node);
7184
+ if (options.emitEvent) {
7185
+ RED.events.emit('nodes:change', configNode);
7186
+ }
7187
+ }
7188
+ } else if (options.action === "remove") {
7189
+ if (configNode.users.indexOf(node) !== -1) {
7190
+ const users = configNode.users;
7191
+ users.splice(users.indexOf(node), 1);
7192
+ if (options.emitEvent) {
7193
+ RED.events.emit('nodes:change', configNode);
7194
+ }
7195
+ }
7196
+ }
7197
+ }
7198
+ }
7199
+ });
7200
+ }
7109
7201
  }
7110
7202
 
7111
7203
  function flowVersion(version) {
@@ -8882,10 +8974,61 @@ RED.history = (function() {
8882
8974
  RED.events.emit("nodes:change",newConfigNode);
8883
8975
  }
8884
8976
  });
8977
+ } else if (i === "env" && ev.node.type.indexOf("subflow:") === 0) {
8978
+ // Subflow can have config node in node.env
8979
+ let nodeList = ev.node.env || [];
8980
+ nodeList = nodeList.reduce((list, prop) => {
8981
+ if (prop.type === "conf-type" && prop.value) {
8982
+ list.push(prop.value);
8983
+ }
8984
+ return list;
8985
+ }, []);
8986
+
8987
+ nodeList.forEach(function(id) {
8988
+ const configNode = RED.nodes.node(id);
8989
+ if (configNode) {
8990
+ if (configNode.users.indexOf(ev.node) !== -1) {
8991
+ configNode.users.splice(configNode.users.indexOf(ev.node), 1);
8992
+ RED.events.emit("nodes:change", configNode);
8993
+ }
8994
+ }
8995
+ });
8996
+
8997
+ nodeList = ev.changes.env || [];
8998
+ nodeList = nodeList.reduce((list, prop) => {
8999
+ if (prop.type === "conf-type" && prop.value) {
9000
+ list.push(prop.value);
9001
+ }
9002
+ return list;
9003
+ }, []);
9004
+
9005
+ nodeList.forEach(function(id) {
9006
+ const configNode = RED.nodes.node(id);
9007
+ if (configNode) {
9008
+ if (configNode.users.indexOf(ev.node) === -1) {
9009
+ configNode.users.push(ev.node);
9010
+ RED.events.emit("nodes:change", configNode);
9011
+ }
9012
+ }
9013
+ });
9014
+ }
9015
+ if (i === "credentials" && ev.changes[i]) {
9016
+ // Reset - Only want to keep the changes
9017
+ inverseEv.changes[i] = {};
9018
+ for (const [key, value] of Object.entries(ev.changes[i])) {
9019
+ // Edge case: node.credentials is cleared after a deploy, so we can't
9020
+ // capture values for the inverse event when undoing past a deploy
9021
+ if (ev.node.credentials) {
9022
+ inverseEv.changes[i][key] = ev.node.credentials[key];
9023
+ }
9024
+ ev.node.credentials[key] = value;
9025
+ }
9026
+ } else {
9027
+ ev.node[i] = ev.changes[i];
8885
9028
  }
8886
- ev.node[i] = ev.changes[i];
8887
9029
  }
8888
9030
  }
9031
+
8889
9032
  ev.node.dirty = true;
8890
9033
  ev.node.changed = ev.changed;
8891
9034
 
@@ -8965,6 +9108,24 @@ RED.history = (function() {
8965
9108
  RED.editor.updateNodeProperties(ev.node,outputMap);
8966
9109
  RED.editor.validateNode(ev.node);
8967
9110
  }
9111
+ // If it's a Config Node, validate user nodes too.
9112
+ // NOTE: The Config Node must be validated before validating users.
9113
+ if (ev.node.users) {
9114
+ const validatedNodes = new Set();
9115
+ const userStack = ev.node.users.slice();
9116
+
9117
+ validatedNodes.add(ev.node.id);
9118
+ while (userStack.length) {
9119
+ const node = userStack.pop();
9120
+ if (!validatedNodes.has(node.id)) {
9121
+ validatedNodes.add(node.id);
9122
+ if (node.users) {
9123
+ userStack.push(...node.users);
9124
+ }
9125
+ RED.editor.validateNode(node);
9126
+ }
9127
+ }
9128
+ }
8968
9129
  if (ev.links) {
8969
9130
  inverseEv.createdLinks = [];
8970
9131
  for (i=0;i<ev.links.length;i++) {
@@ -22218,7 +22379,7 @@ RED.view = (function() {
22218
22379
  }
22219
22380
  selectedLinks.clearUnselected()
22220
22381
  },
22221
- length: () => groups.length,
22382
+ length: () => groups.size,
22222
22383
  forEach: (func) => { groups.forEach(func) },
22223
22384
  toArray: () => [...groups],
22224
22385
  clear: function () {
@@ -22251,8 +22412,8 @@ RED.view = (function() {
22251
22412
  evt.stopPropagation()
22252
22413
  RED.contextMenu.show({
22253
22414
  type: 'workspace',
22254
- x:evt.clientX-5,
22255
- y:evt.clientY-5
22415
+ x: evt.clientX,
22416
+ y: evt.clientY
22256
22417
  })
22257
22418
  return false
22258
22419
  })
@@ -24619,22 +24780,21 @@ RED.view = (function() {
24619
24780
  addToRemovedLinks(reconnectResult.removedLinks)
24620
24781
  }
24621
24782
 
24622
- var startDirty = RED.nodes.dirty();
24623
- var startChanged = false;
24624
- var selectedGroups = [];
24783
+ const startDirty = RED.nodes.dirty();
24784
+ let movingSelectedGroups = [];
24625
24785
  if (movingSet.length() > 0) {
24626
24786
 
24627
24787
  for (var i=0;i<movingSet.length();i++) {
24628
24788
  node = movingSet.get(i).n;
24629
24789
  if (node.type === "group") {
24630
- selectedGroups.push(node);
24790
+ movingSelectedGroups.push(node);
24631
24791
  }
24632
24792
  }
24633
24793
  // 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);
24794
+ for (i=0;i<movingSelectedGroups.length;i++) {
24795
+ movingSelectedGroups[i].nodes.forEach(function(n) {
24796
+ if (n.type === "group" && movingSelectedGroups.indexOf(n) === -1) {
24797
+ movingSelectedGroups.push(n);
24638
24798
  }
24639
24799
  })
24640
24800
  }
@@ -24651,7 +24811,7 @@ RED.view = (function() {
24651
24811
  addToRemovedLinks(removedEntities.links);
24652
24812
  if (node.g) {
24653
24813
  var group = RED.nodes.group(node.g);
24654
- if (selectedGroups.indexOf(group) === -1) {
24814
+ if (movingSelectedGroups.indexOf(group) === -1) {
24655
24815
  // Don't use RED.group.removeFromGroup as that emits
24656
24816
  // a change event on the node - but we're deleting it
24657
24817
  var index = group.nodes.indexOf(node);
@@ -24665,7 +24825,7 @@ RED.view = (function() {
24665
24825
  removedLinks = removedLinks.concat(result.links);
24666
24826
  if (node.g) {
24667
24827
  var group = RED.nodes.group(node.g);
24668
- if (selectedGroups.indexOf(group) === -1) {
24828
+ if (movingSelectedGroups.indexOf(group) === -1) {
24669
24829
  // Don't use RED.group.removeFromGroup as that emits
24670
24830
  // a change event on the node - but we're deleting it
24671
24831
  var index = group.nodes.indexOf(node);
@@ -24687,8 +24847,8 @@ RED.view = (function() {
24687
24847
 
24688
24848
  // Groups must be removed in the right order - from inner-most
24689
24849
  // to outermost.
24690
- for (i = selectedGroups.length-1; i>=0; i--) {
24691
- var g = selectedGroups[i];
24850
+ for (i = movingSelectedGroups.length-1; i>=0; i--) {
24851
+ var g = movingSelectedGroups[i];
24692
24852
  removedGroups.push(g);
24693
24853
  RED.nodes.removeGroup(g);
24694
24854
  }
@@ -27105,8 +27265,8 @@ RED.view = (function() {
27105
27265
  var delta = Infinity;
27106
27266
  for (var i = 0; i < lineLength; i++) {
27107
27267
  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)
27268
+ var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor))
27269
+ var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor))
27110
27270
  var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY
27111
27271
  if (posDelta < delta) {
27112
27272
  pos = linePos
@@ -28440,7 +28600,7 @@ RED.view = (function() {
28440
28600
  }
28441
28601
  let badgeRDX = 0;
28442
28602
  let badgeLDX = 0;
28443
-
28603
+ const scale = RED.view.scale()
28444
28604
  for (let i=0,l=evt.el.__annotations__.length;i<l;i++) {
28445
28605
  const annotation = evt.el.__annotations__[i];
28446
28606
  if (annotations.hasOwnProperty(annotation.id)) {
@@ -28471,15 +28631,17 @@ RED.view = (function() {
28471
28631
  }
28472
28632
  if (isBadge) {
28473
28633
  if (showAnnotation) {
28474
- const rect = annotation.element.getBoundingClientRect();
28634
+ // getBoundingClientRect is in real-world scale so needs to be adjusted according to
28635
+ // the current scale factor
28636
+ const rectWidth = annotation.element.getBoundingClientRect().width / scale;
28475
28637
  let annotationX
28476
28638
  if (!opts.align || opts.align === 'right') {
28477
- annotationX = evt.node.w - 3 - badgeRDX - rect.width
28478
- badgeRDX += rect.width + 4;
28639
+ annotationX = evt.node.w - 3 - badgeRDX - rectWidth
28640
+ badgeRDX += rectWidth + 4;
28479
28641
 
28480
28642
  } else if (opts.align === 'left') {
28481
28643
  annotationX = 3 + badgeLDX
28482
- badgeLDX += rect.width + 4;
28644
+ badgeLDX += rectWidth + 4;
28483
28645
  }
28484
28646
  annotation.element.setAttribute("transform", "translate("+annotationX+", -8)");
28485
28647
  }
@@ -29870,18 +30032,27 @@ RED.view.tools = (function() {
29870
30032
  const paletteLabel = RED.utils.getPaletteLabel(n.type, nodeDef)
29871
30033
  const defaultNodeNameRE = new RegExp('^'+paletteLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')+' (\\d+)$')
29872
30034
  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)
30035
+ const existingNodes = RED.nodes.filterNodes({ type: n.type });
30036
+ const existingIds = existingNodes.reduce((ids, node) => {
30037
+ let match = defaultNodeNameRE.exec(node.name);
29877
30038
  if (match) {
29878
- let nodeNumber = parseInt(match[1])
29879
- if (nodeNumber > maxNameNumber) {
29880
- maxNameNumber = nodeNumber
30039
+ const nodeNumber = parseInt(match[1], 10);
30040
+ if (!ids.includes(nodeNumber)) {
30041
+ ids.push(nodeNumber);
29881
30042
  }
29882
30043
  }
29883
- })
29884
- typeIndex[n.type] = maxNameNumber + 1
30044
+ return ids;
30045
+ }, []).sort((a, b) => a - b);
30046
+
30047
+ let availableNameNumber = 1;
30048
+ for (let i = 0; i < existingIds.length; i++) {
30049
+ if (existingIds[i] !== availableNameNumber) {
30050
+ break;
30051
+ }
30052
+ availableNameNumber++;
30053
+ }
30054
+
30055
+ typeIndex[n.type] = availableNameNumber;
29885
30056
  }
29886
30057
  if ((options.renameBlank && n.name === '') || (options.renameClash && defaultNodeNameRE.test(n.name))) {
29887
30058
  if (generateHistory) {
@@ -29913,11 +30084,11 @@ RED.view.tools = (function() {
29913
30084
  }
29914
30085
  }
29915
30086
 
29916
- function addJunctionsToWires(wires) {
30087
+ function addJunctionsToWires(options = {}) {
29917
30088
  if (RED.workspaces.isLocked()) {
29918
30089
  return
29919
30090
  }
29920
- let wiresToSplit = wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
30091
+ let wiresToSplit = options.wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
29921
30092
  if (!wiresToSplit) {
29922
30093
  return
29923
30094
  }
@@ -29965,21 +30136,26 @@ RED.view.tools = (function() {
29965
30136
  if (links.length === 0) {
29966
30137
  return
29967
30138
  }
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)
30139
+ if (addedJunctions.length === 0 && Object.hasOwn(options, 'x') && Object.hasOwn(options, 'y')) {
30140
+ junction.x = options.x
30141
+ junction.y = options.y
30142
+ } else {
30143
+ let pointCount = 0
30144
+ links.forEach(function(l) {
30145
+ if (l._sliceLocation) {
30146
+ junction.x += l._sliceLocation.x
30147
+ junction.y += l._sliceLocation.y
30148
+ delete l._sliceLocation
30149
+ pointCount++
30150
+ } else {
30151
+ junction.x += l.source.x + l.source.w/2 + l.target.x - l.target.w/2
30152
+ junction.y += l.source.y + l.target.y
30153
+ pointCount += 2
30154
+ }
30155
+ })
30156
+ junction.x = Math.round(junction.x/pointCount)
30157
+ junction.y = Math.round(junction.y/pointCount)
30158
+ }
29983
30159
  if (RED.view.snapGrid) {
29984
30160
  let gridSize = RED.view.gridSize()
29985
30161
  junction.x = (gridSize*Math.round(junction.x/gridSize));
@@ -30169,7 +30345,7 @@ RED.view.tools = (function() {
30169
30345
  RED.actions.add("core:wire-multiple-to-node", function() { wireMultipleToNode() })
30170
30346
 
30171
30347
  RED.actions.add("core:split-wire-with-link-nodes", function () { splitWiresWithLinkNodes() });
30172
- RED.actions.add("core:split-wires-with-junctions", function () { addJunctionsToWires() });
30348
+ RED.actions.add("core:split-wires-with-junctions", function (options) { addJunctionsToWires(options) });
30173
30349
 
30174
30350
  RED.actions.add("core:generate-node-names", generateNodeNames )
30175
30351
 
@@ -36156,6 +36332,20 @@ RED.editor = (function() {
36156
36332
  }
36157
36333
  }
36158
36334
 
36335
+ const oldCreds = {};
36336
+ if (editing_node._def.credentials) {
36337
+ for (const prop in editing_node._def.credentials) {
36338
+ if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) {
36339
+ if (editing_node._def.credentials[prop].type === 'password') {
36340
+ oldCreds['has_' + prop] = editing_node.credentials['has_' + prop];
36341
+ }
36342
+ if (prop in editing_node.credentials) {
36343
+ oldCreds[prop] = editing_node.credentials[prop];
36344
+ }
36345
+ }
36346
+ }
36347
+ }
36348
+
36159
36349
  try {
36160
36350
  const rc = editing_node._def.oneditsave.call(editing_node);
36161
36351
  if (rc === true) {
@@ -36187,6 +36377,25 @@ RED.editor = (function() {
36187
36377
  }
36188
36378
  }
36189
36379
  }
36380
+
36381
+ if (editing_node._def.credentials) {
36382
+ for (const prop in editing_node._def.credentials) {
36383
+ if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) {
36384
+ if (oldCreds[prop] !== editing_node.credentials[prop]) {
36385
+ if (editing_node.credentials[prop] === '__PWRD__') {
36386
+ // The password may not exist in oldCreds
36387
+ // The value '__PWRD__' means the password exists,
36388
+ // so ignore this change
36389
+ continue;
36390
+ }
36391
+ editState.changes.credentials = editState.changes.credentials || {};
36392
+ editState.changes.credentials['has_' + prop] = oldCreds['has_' + prop];
36393
+ editState.changes.credentials[prop] = oldCreds[prop];
36394
+ editState.changed = true;
36395
+ }
36396
+ }
36397
+ }
36398
+ }
36190
36399
  }
36191
36400
  }
36192
36401
 
@@ -36829,134 +37038,181 @@ RED.editor = (function() {
36829
37038
  },
36830
37039
  {
36831
37040
  id: "node-config-dialog-ok",
36832
- text: adding?RED._("editor.configAdd"):RED._("editor.configUpdate"),
37041
+ text: adding ? RED._("editor.configAdd") : RED._("editor.configUpdate"),
36833
37042
  class: "primary",
36834
37043
  click: function() {
36835
- var editState = {
37044
+ // TODO: Already defined
37045
+ const configProperty = name;
37046
+ const configType = type;
37047
+ const configTypeDef = RED.nodes.getType(configType);
37048
+
37049
+ const wasChanged = editing_config_node.changed;
37050
+ const editState = {
36836
37051
  changes: {},
36837
37052
  changed: false,
36838
37053
  outputMap: null
36839
37054
  };
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
- }
37055
+
37056
+ // Call `oneditsave` and search for changes
37057
+ handleEditSave(editing_config_node, editState);
36889
37058
 
36890
- activeEditPanes.forEach(function(pane) {
37059
+ // Search for changes in the edit box (panes)
37060
+ activeEditPanes.forEach(function (pane) {
36891
37061
  if (pane.apply) {
36892
37062
  pane.apply.call(pane, editState);
36893
37063
  }
36894
- })
36895
-
36896
- editing_config_node.label = configTypeDef.label;
37064
+ });
36897
37065
 
36898
- var scope = $("#red-ui-editor-config-scope").val();
36899
- editing_config_node.z = scope;
37066
+ // TODO: Why?
37067
+ editing_config_node.label = configTypeDef.label
36900
37068
 
37069
+ // Check if disabled has changed
36901
37070
  if ($("#node-config-input-node-disabled").prop('checked')) {
36902
37071
  if (editing_config_node.d !== true) {
37072
+ editState.changes.d = editing_config_node.d;
37073
+ editState.changed = true;
36903
37074
  editing_config_node.d = true;
36904
37075
  }
36905
37076
  } else {
36906
37077
  if (editing_config_node.d === true) {
37078
+ editState.changes.d = editing_config_node.d;
37079
+ editState.changed = true;
36907
37080
  delete editing_config_node.d;
36908
37081
  }
36909
37082
  }
36910
37083
 
37084
+ // NOTE: must be undefined if no scope used
37085
+ const scope = $("#red-ui-editor-config-scope").val() || undefined;
37086
+
37087
+ // Check if the scope has changed
37088
+ if (editing_config_node.z !== scope) {
37089
+ editState.changes.z = editing_config_node.z;
37090
+ editState.changed = true;
37091
+ editing_config_node.z = scope;
37092
+ }
37093
+
37094
+ // Search for nodes that use this config node that are no longer
37095
+ // in scope, so must be removed
37096
+ const historyEvents = [];
36911
37097
  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);
37098
+ const newUsers = editing_config_node.users.filter(function (node) {
37099
+ let keepNode = false;
37100
+ let nodeModified = null;
37101
+
37102
+ for (const d in node._def.defaults) {
37103
+ if (node._def.defaults.hasOwnProperty(d)) {
37104
+ if (node._def.defaults[d].type === editing_config_node.type) {
37105
+ if (node[d] === editing_config_node.id) {
37106
+ if (node.z === editing_config_node.z) {
37107
+ // The node is kept only if at least one property uses
37108
+ // this config node in the correct scope.
37109
+ keepNode = true;
37110
+ } else {
37111
+ if (!nodeModified) {
37112
+ nodeModified = {
37113
+ t: "edit",
37114
+ node: node,
37115
+ changes: { [d]: node[d] },
37116
+ changed: node.changed,
37117
+ dirty: node.dirty
37118
+ };
37119
+ } else {
37120
+ nodeModified.changes[d] = node[d];
37121
+ }
37122
+
37123
+ // Remove the reference to the config node
37124
+ node[d] = "";
37125
+ }
37126
+ }
36928
37127
  }
36929
37128
  }
36930
37129
  }
36931
- return keep;
37130
+
37131
+ // Add the node modified to the history
37132
+ if (nodeModified) {
37133
+ historyEvents.push(nodeModified);
37134
+ }
37135
+
37136
+ // Mark as changed and revalidate this node
37137
+ if (!keepNode) {
37138
+ node.changed = true;
37139
+ node.dirty = true;
37140
+ validateNode(node);
37141
+ RED.events.emit("nodes:change", node);
37142
+ }
37143
+
37144
+ return keepNode;
36932
37145
  });
37146
+
37147
+ // Check if users are changed
37148
+ if (editing_config_node.users.length !== newUsers.length) {
37149
+ editState.changes.users = editing_config_node.users;
37150
+ editState.changed = true;
37151
+ editing_config_node.users = newUsers;
37152
+ }
36933
37153
  }
36934
37154
 
36935
- if (configAdding) {
36936
- RED.nodes.add(editing_config_node);
37155
+ if (editState.changed) {
37156
+ // Set the congig node as changed
37157
+ editing_config_node.changed = true;
36937
37158
  }
36938
37159
 
37160
+ // Now, validate the config node
36939
37161
  validateNode(editing_config_node);
36940
- var validatedNodes = {};
36941
- validatedNodes[editing_config_node.id] = true;
36942
37162
 
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);
37163
+ // And validate nodes using this config node too
37164
+ const validatedNodes = new Set();
37165
+ const userStack = editing_config_node.users.slice();
37166
+
37167
+ validatedNodes.add(editing_config_node.id);
37168
+ while (userStack.length) {
37169
+ const node = userStack.pop();
37170
+ if (!validatedNodes.has(node.id)) {
37171
+ validatedNodes.add(node.id);
37172
+ if (node.users) {
37173
+ userStack.push(...node.users);
36950
37174
  }
36951
- validateNode(user);
37175
+ validateNode(node);
36952
37176
  }
36953
37177
  }
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);
37178
+
37179
+ let historyEvent = {
37180
+ t: "edit",
37181
+ node: editing_config_node,
37182
+ changes: editState.changes,
37183
+ changed: wasChanged,
37184
+ dirty: RED.nodes.dirty()
37185
+ };
37186
+
37187
+ if (historyEvents.length) {
37188
+ // Need a multi events
37189
+ historyEvent = {
37190
+ t: "multi",
37191
+ events: [historyEvent].concat(historyEvents),
37192
+ dirty: historyEvent.dirty
37193
+ };
37194
+ }
37195
+
37196
+ if (!adding) {
37197
+ // This event is triggered when the edit box is saved,
37198
+ // regardless of whether there are any modifications.
37199
+ RED.events.emit("editor:save", editing_config_node);
36959
37200
  }
37201
+
37202
+ if (editState.changed) {
37203
+ if (adding) {
37204
+ RED.history.push({ t: "add", nodes: [editing_config_node.id], dirty: RED.nodes.dirty() });
37205
+ // Add the new config node and trigger the `nodes:add` event
37206
+ RED.nodes.add(editing_config_node);
37207
+ } else {
37208
+ RED.history.push(historyEvent);
37209
+ RED.events.emit("nodes:change", editing_config_node);
37210
+ }
37211
+
37212
+ RED.nodes.dirty(true);
37213
+ RED.view.redraw(true);
37214
+ }
37215
+
36960
37216
  RED.tray.close(function() {
36961
37217
  var filter = null;
36962
37218
  // when editing a config via subflow edit panel, the `configProperty` will not
@@ -38238,10 +38494,31 @@ RED.editor = (function() {
38238
38494
  apply: function(editState) {
38239
38495
  var old_env = node.env;
38240
38496
  var new_env = [];
38497
+
38241
38498
  if (/^subflow:/.test(node.type)) {
38499
+ // Get the list of environment variables from the node properties
38242
38500
  new_env = RED.subflow.exportSubflowInstanceEnv(node);
38243
38501
  }
38244
38502
 
38503
+ if (old_env && old_env.length) {
38504
+ old_env.forEach(function (prop) {
38505
+ if (prop.type === "conf-type" && prop.value) {
38506
+ const stillInUse = new_env?.some((p) => p.type === "conf-type" && p.name === prop.name && p.value === prop.value);
38507
+ if (!stillInUse) {
38508
+ // Remove the node from the config node users
38509
+ // Only for empty value or modified
38510
+ const configNode = RED.nodes.node(prop.value);
38511
+ if (configNode) {
38512
+ if (configNode.users.indexOf(node) !== -1) {
38513
+ configNode.users.splice(configNode.users.indexOf(node), 1);
38514
+ RED.events.emit('nodes:change', configNode)
38515
+ }
38516
+ }
38517
+ }
38518
+ }
38519
+ });
38520
+ }
38521
+
38245
38522
  // Get the values from the Properties table tab
38246
38523
  var items = this.list.editableList('items');
38247
38524
  items.each(function (i,el) {
@@ -38259,7 +38536,6 @@ RED.editor = (function() {
38259
38536
  }
38260
38537
  });
38261
38538
 
38262
-
38263
38539
  if (new_env && new_env.length > 0) {
38264
38540
  new_env.forEach(function(prop) {
38265
38541
  if (prop.type === "cred") {
@@ -38270,6 +38546,15 @@ RED.editor = (function() {
38270
38546
  editState.changed = true;
38271
38547
  }
38272
38548
  delete prop.value;
38549
+ } else if (prop.type === "conf-type" && prop.value) {
38550
+ const configNode = RED.nodes.node(prop.value);
38551
+ if (configNode) {
38552
+ if (configNode.users.indexOf(node) === -1) {
38553
+ // Add the node to the config node users
38554
+ configNode.users.push(node);
38555
+ RED.events.emit('nodes:change', configNode);
38556
+ }
38557
+ }
38273
38558
  }
38274
38559
  });
38275
38560
  }
@@ -38395,6 +38680,7 @@ RED.editor = (function() {
38395
38680
  apply: function(editState) {
38396
38681
  var newValue;
38397
38682
  var d;
38683
+ // If the node is a subflow, the node's properties (exepts name) are saved by `envProperties`
38398
38684
  if (node._def.defaults) {
38399
38685
  for (d in node._def.defaults) {
38400
38686
  if (node._def.defaults.hasOwnProperty(d)) {
@@ -38482,9 +38768,16 @@ RED.editor = (function() {
38482
38768
  }
38483
38769
  }
38484
38770
  if (node._def.credentials) {
38485
- var credDefinition = node._def.credentials;
38486
- var credsChanged = updateNodeCredentials(node,credDefinition,this.inputClass);
38487
- editState.changed = editState.changed || credsChanged;
38771
+ const credDefinition = node._def.credentials;
38772
+ const credChanges = updateNodeCredentials(node, credDefinition, this.inputClass);
38773
+
38774
+ if (Object.keys(credChanges).length) {
38775
+ editState.changed = true;
38776
+ editState.changes.credentials = {
38777
+ ...(editState.changes.credentials || {}),
38778
+ ...credChanges
38779
+ };
38780
+ }
38488
38781
  }
38489
38782
  }
38490
38783
  }
@@ -38512,10 +38805,11 @@ RED.editor = (function() {
38512
38805
  * @param node - the node containing the credentials
38513
38806
  * @param credDefinition - definition of the credentials
38514
38807
  * @param prefix - prefix of the input fields
38515
- * @return {boolean} whether anything has changed
38808
+ * @return {object} an object containing the modified properties
38516
38809
  */
38517
38810
  function updateNodeCredentials(node, credDefinition, prefix) {
38518
- var changed = false;
38811
+ const changes = {};
38812
+
38519
38813
  if (!node.credentials) {
38520
38814
  node.credentials = {_:{}};
38521
38815
  } else if (!node.credentials._) {
@@ -38528,24 +38822,35 @@ RED.editor = (function() {
38528
38822
  if (input.length > 0) {
38529
38823
  var value = input.val();
38530
38824
  if (credDefinition[cred].type == 'password') {
38531
- node.credentials['has_' + cred] = (value !== "");
38532
- if (value == '__PWRD__') {
38533
- continue;
38825
+ if (value === '__PWRD__') {
38826
+ // A cred value exists - no changes
38827
+ } else if (value === '' && node.credentials['has_' + cred] === false) {
38828
+ // Empty cred value exists - no changes
38829
+ } else if (value === node.credentials[cred]) {
38830
+ // A cred value exists locally in the editor - no changes
38831
+ // Like the user sets a value, saves the config,
38832
+ // reopens the config and save the config again
38833
+ } else {
38834
+ changes['has_' + cred] = node.credentials['has_' + cred];
38835
+ changes[cred] = node.credentials[cred];
38836
+ node.credentials[cred] = value;
38534
38837
  }
38535
- changed = true;
38536
38838
 
38537
- }
38538
- node.credentials[cred] = value;
38539
- if (value != node.credentials._[cred]) {
38540
- changed = true;
38839
+ node.credentials['has_' + cred] = (value !== '');
38840
+ } else {
38841
+ // Since these creds are loaded by the editor,
38842
+ // values can be directly compared
38843
+ if (value !== node.credentials[cred]) {
38844
+ changes[cred] = node.credentials[cred];
38845
+ node.credentials[cred] = value;
38846
+ }
38541
38847
  }
38542
38848
  }
38543
38849
  }
38544
38850
  }
38545
- return changed;
38546
- }
38547
-
38548
38851
 
38852
+ return changes;
38853
+ }
38549
38854
  })();
38550
38855
  ;(function() {
38551
38856
  var _subflowModulePaneTemplate = '<form class="dialog-form form-horizontal" autocomplete="off">'+
@@ -39400,7 +39705,7 @@ RED.editor = (function() {
39400
39705
  nameField.trigger('change');
39401
39706
  }
39402
39707
  },
39403
- sortable: ".red-ui-editableList-item-handle",
39708
+ sortable: true,
39404
39709
  removable: false
39405
39710
  });
39406
39711
  var parentEnv = {};
@@ -44109,6 +44414,30 @@ RED.clipboard = (function() {
44109
44414
  },100);
44110
44415
  }
44111
44416
 
44417
+ /**
44418
+ * Validates if the provided string looks like valid flow json
44419
+ * @param {string} flowString the string to validate
44420
+ * @returns If valid, returns the node array
44421
+ */
44422
+ function validateFlowString(flowString) {
44423
+ const res = JSON.parse(flowString)
44424
+ if (!Array.isArray(res)) {
44425
+ throw new Error(RED._("clipboard.import.errors.notArray"));
44426
+ }
44427
+ for (let i = 0; i < res.length; i++) {
44428
+ if (typeof res[i] !== "object") {
44429
+ throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i}));
44430
+ }
44431
+ if (!Object.hasOwn(res[i], 'id')) {
44432
+ throw new Error(RED._("clipboard.import.errors.missingId",{index:i}));
44433
+ }
44434
+ if (!Object.hasOwn(res[i], 'type')) {
44435
+ throw new Error(RED._("clipboard.import.errors.missingType",{index:i}));
44436
+ }
44437
+ }
44438
+ return res
44439
+ }
44440
+
44112
44441
  var validateImportTimeout;
44113
44442
  function validateImport() {
44114
44443
  if (activeTab === "red-ui-clipboard-dialog-import-tab-clipboard") {
@@ -44126,21 +44455,7 @@ RED.clipboard = (function() {
44126
44455
  return;
44127
44456
  }
44128
44457
  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
- }
44458
+ validateFlowString(v)
44144
44459
  currentPopoverError = null;
44145
44460
  popover.close(true);
44146
44461
  importInput.removeClass("input-error");
@@ -44773,16 +45088,16 @@ RED.clipboard = (function() {
44773
45088
  }
44774
45089
 
44775
45090
  function importNodes(nodesStr,addFlow) {
44776
- var newNodes = nodesStr;
45091
+ let newNodes = nodesStr;
44777
45092
  if (typeof nodesStr === 'string') {
44778
45093
  try {
44779
45094
  nodesStr = nodesStr.trim();
44780
45095
  if (nodesStr.length === 0) {
44781
45096
  return;
44782
45097
  }
44783
- newNodes = JSON.parse(nodesStr);
45098
+ newNodes = validateFlowString(nodesStr)
44784
45099
  } catch(err) {
44785
- var e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
45100
+ const e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
44786
45101
  e.code = "NODE_RED";
44787
45102
  throw e;
44788
45103
  }
@@ -45117,6 +45432,7 @@ RED.clipboard = (function() {
45117
45432
  }
45118
45433
  }
45119
45434
  } catch(err) {
45435
+ console.warn('Import failed: ', err)
45120
45436
  // Ensure any errors throw above doesn't stop the drop target from
45121
45437
  // being hidden.
45122
45438
  }
@@ -47081,15 +47397,15 @@ RED.search = (function() {
47081
47397
  }
47082
47398
  }
47083
47399
 
47400
+ const scale = RED.view.scale()
47084
47401
  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()
47402
+ let addX = (options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()) / scale
47403
+ let addY = (options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()) / scale
47088
47404
 
47089
47405
  if (RED.view.snapGrid) {
47090
47406
  const gridSize = RED.view.gridSize()
47091
- addX = gridSize * Math.floor(addX / gridSize)
47092
- addY = gridSize * Math.floor(addY / gridSize)
47407
+ addX = gridSize * Math.round(addX / gridSize)
47408
+ addY = gridSize * Math.round(addY / gridSize)
47093
47409
  }
47094
47410
 
47095
47411
  if (RED.settings.theme("menu.menu-item-action-list", true)) {
@@ -47114,7 +47430,9 @@ RED.search = (function() {
47114
47430
  },
47115
47431
  (hasLinks) ? { // has least 1 wire selected
47116
47432
  label: RED._("contextMenu.junction"),
47117
- onselect: 'core:split-wires-with-junctions',
47433
+ onselect: function () {
47434
+ RED.actions.invoke('core:split-wires-with-junctions', { x: addX, y: addY })
47435
+ },
47118
47436
  disabled: !canEdit || !hasLinks
47119
47437
  } : {
47120
47438
  label: RED._("contextMenu.junction"),
@@ -47924,7 +48242,7 @@ RED.actionList = (function() {
47924
48242
  var items = [];
47925
48243
  RED.nodes.registry.getNodeTypes().forEach(function(t) {
47926
48244
  var def = RED.nodes.getType(t);
47927
- if (def.category !== 'config' && t !== 'unknown' && t !== 'tab') {
48245
+ if (def.set?.enabled !== false && def.category !== 'config' && t !== 'unknown' && t !== 'tab') {
47928
48246
  items.push({type:t,def: def, label:getTypeLabel(t,def)});
47929
48247
  }
47930
48248
  });
@@ -49351,7 +49669,7 @@ RED.subflow = (function() {
49351
49669
  item.value = ""+input.prop("checked");
49352
49670
  break;
49353
49671
  case "conf-types":
49354
- item.value = input.val()
49672
+ item.value = input.val() === "_ADD_" ? "" : input.val();
49355
49673
  item.type = "conf-type"
49356
49674
  }
49357
49675
  if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) {