@node-red/nodes 3.1.6 → 4.0.0-beta.1

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.
@@ -5,6 +5,7 @@ module.exports = function(RED) {
5
5
  const fs = require("fs-extra");
6
6
  const path = require("path");
7
7
  var debuglength = RED.settings.debugMaxLength || 1000;
8
+ var statuslength = RED.settings.debugStatusLength || 32;
8
9
  var useColors = RED.settings.debugUseColors || false;
9
10
  util.inspect.styles.boolean = "red";
10
11
  const { hasOwnProperty } = Object.prototype;
@@ -164,7 +165,7 @@ module.exports = function(RED) {
164
165
  }
165
166
  }
166
167
 
167
- if (st.length > 32) { st = st.substr(0,32) + "..."; }
168
+ if (st.length > statuslength) { st = st.substr(0,statuslength) + "..."; }
168
169
 
169
170
  var newStatus = {fill:fill, shape:shape, text:st};
170
171
  if (JSON.stringify(newStatus) !== node.oldState) { // only send if we have to
@@ -512,7 +512,8 @@ RED.debug = (function() {
512
512
  hideKey: false,
513
513
  path: path,
514
514
  sourceId: sourceNode&&sourceNode.id,
515
- rootPath: path
515
+ rootPath: path,
516
+ nodeSelector: config.messageSourceClick,
516
517
  });
517
518
  // Do this in a separate step so the element functions aren't stripped
518
519
  debugMessage.appendTo(el);
@@ -117,7 +117,7 @@ module.exports = function(RED) {
117
117
  });
118
118
  return
119
119
  } else if (rule.tot === 'date') {
120
- value = Date.now();
120
+ value = RED.util.evaluateNodeProperty(rule.to, rule.tot, node)
121
121
  } else if (rule.tot === 'jsonata') {
122
122
  RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => {
123
123
  if (err) {
@@ -233,7 +233,9 @@ module.exports = function(RED) {
233
233
  // only replace if they match exactly
234
234
  RED.util.setMessageProperty(msg,property,value);
235
235
  } else {
236
- current = current.replace(fromRE,value);
236
+ // if target is boolean then just replace it
237
+ if (rule.tot === "bool") { current = value; }
238
+ else { current = current.replace(fromRE,value); }
237
239
  RED.util.setMessageProperty(msg,property,current);
238
240
  }
239
241
  } else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {
@@ -40,6 +40,99 @@
40
40
 
41
41
  (function() {
42
42
 
43
+ const headerTypes = [
44
+ /*
45
+ { value: "Accept", label: "Accept", hasValue: false },
46
+ { value: "Accept-Encoding", label: "Accept-Encoding", hasValue: false },
47
+ { value: "Accept-Language", label: "Accept-Language", hasValue: false },
48
+ */
49
+ { value: "Authorization", label: "Authorization", hasValue: false },
50
+ /*
51
+ { value: "Content-Type", label: "Content-Type", hasValue: false },
52
+ { value: "Cache-Control", label: "Cache-Control", hasValue: false },
53
+ */
54
+ { value: "User-Agent", label: "User-Agent", hasValue: false },
55
+ /*
56
+ { value: "Location", label: "Location", hasValue: false },
57
+ */
58
+ { value: "other", label: RED._("node-red:httpin.label.other"),
59
+ hasValue: true, icon: "red/images/typedInput/az.svg" },
60
+ ]
61
+
62
+ const headerOptions = {};
63
+ const defaultOptions = [
64
+ { value: "other", label: RED._("node-red:httpin.label.other"),
65
+ hasValue: true, icon: "red/images/typedInput/az.svg" },
66
+ "env",
67
+ ];
68
+ /*
69
+ headerOptions["accept"] = [
70
+ { value: "text/plain", label: "text/plain", hasValue: false },
71
+ { value: "text/html", label: "text/html", hasValue: false },
72
+ { value: "application/json", label: "application/json", hasValue: false },
73
+ { value: "application/xml", label: "application/xml", hasValue: false },
74
+ ...defaultOptions,
75
+ ];
76
+
77
+ headerOptions["accept-encoding"] = [
78
+ { value: "gzip", label: "gzip", hasValue: false },
79
+ { value: "deflate", label: "deflate", hasValue: false },
80
+ { value: "compress", label: "compress", hasValue: false },
81
+ { value: "br", label: "br", hasValue: false },
82
+ { value: "gzip, deflate", label: "gzip, deflate", hasValue: false },
83
+ { value: "gzip, deflate, br", label: "gzip, deflate, br", hasValue: false },
84
+ ...defaultOptions,
85
+ ];
86
+ headerOptions["accept-language"] = [
87
+ { value: "*", label: "*", hasValue: false },
88
+ { value: "en-GB, en-US, en;q=0.9", label: "en-GB, en-US, en;q=0.9", hasValue: false },
89
+ { value: "de-AT, de-DE;q=0.9, en;q=0.5", label: "de-AT, de-DE;q=0.9, en;q=0.5", hasValue: false },
90
+ { value: "es-mx,es,en;q=0.5", label: "es-mx,es,en;q=0.5", hasValue: false },
91
+ { value: "fr-CH, fr;q=0.9, en;q=0.8", label: "fr-CH, fr;q=0.9, en;q=0.8", hasValue: false },
92
+ { value: "zh-CN, zh-TW; q = 0.9, zh-HK; q = 0.8, zh; q = 0.7, en; q = 0.6", label: "zh-CN, zh-TW; q = 0.9, zh-HK; q = 0.8, zh; q = 0.7, en; q = 0.6", hasValue: false },
93
+ { value: "ja-JP, jp", label: "ja-JP, jp", hasValue: false },
94
+ ...defaultOptions,
95
+ ];
96
+ headerOptions["content-type"] = [
97
+ { value: "text/css", label: "text/css", hasValue: false },
98
+ { value: "text/plain", label: "text/plain", hasValue: false },
99
+ { value: "text/html", label: "text/html", hasValue: false },
100
+ { value: "application/json", label: "application/json", hasValue: false },
101
+ { value: "application/octet-stream", label: "application/octet-stream", hasValue: false },
102
+ { value: "application/pdf", label: "application/pdf", hasValue: false },
103
+ { value: "application/xml", label: "application/xml", hasValue: false },
104
+ { value: "application/zip", label: "application/zip", hasValue: false },
105
+ { value: "multipart/form-data", label: "multipart/form-data", hasValue: false },
106
+ { value: "audio/aac", label: "audio/aac", hasValue: false },
107
+ { value: "audio/ac3", label: "audio/ac3", hasValue: false },
108
+ { value: "audio/basic", label: "audio/basic", hasValue: false },
109
+ { value: "audio/mp4", label: "audio/mp4", hasValue: false },
110
+ { value: "audio/ogg", label: "audio/ogg", hasValue: false },
111
+ { value: "image/bmp", label: "image/bmp", hasValue: false },
112
+ { value: "image/gif", label: "image/gif", hasValue: false },
113
+ { value: "image/jpeg", label: "image/jpeg", hasValue: false },
114
+ { value: "image/png", label: "image/png", hasValue: false },
115
+ { value: "image/tiff", label: "image/tiff", hasValue: false },
116
+ ...defaultOptions,
117
+ ];
118
+ headerOptions["cache-control"] = [
119
+ { value: "max-age=0", label: "max-age=0", hasValue: false },
120
+ { value: "max-age=86400", label: "max-age=86400", hasValue: false },
121
+ { value: "no-cache", label: "no-cache", hasValue: false },
122
+ ...defaultOptions,
123
+ ];
124
+ */
125
+ headerOptions["user-agent"] = [
126
+ { value: "Mozilla/5.0", label: "Mozilla/5.0", hasValue: false },
127
+ ...defaultOptions,
128
+ ];
129
+
130
+ function getHeaderOptions(headerName) {
131
+ const lc = (headerName || "").toLowerCase();
132
+ let opts = headerOptions[lc];
133
+ return opts || defaultOptions;
134
+ }
135
+
43
136
  function ws_oneditprepare() {
44
137
  $("#websocket-client-row").hide();
45
138
  $("#node-input-mode").on("change", function() {
@@ -192,7 +285,8 @@
192
285
  value: "",
193
286
  label:RED._("node-red:websocket.sendheartbeat"),
194
287
  validate: RED.validators.number(/*blank allowed*/true) },
195
- subprotocol: {value:"",required: false}
288
+ subprotocol: {value:"",required: false},
289
+ headers: { value: [] }
196
290
  },
197
291
  inputs:0,
198
292
  outputs:0,
@@ -200,6 +294,9 @@
200
294
  return this.path;
201
295
  },
202
296
  oneditprepare: function() {
297
+
298
+ const node = this;
299
+
203
300
  $("#node-config-input-path").on("change keyup paste",function() {
204
301
  $(".node-config-row-tls").toggle(/^wss:/i.test($(this).val()))
205
302
  });
@@ -214,14 +311,114 @@
214
311
  if (!heartbeatActive) {
215
312
  $("#node-config-input-hb").val("");
216
313
  }
314
+
315
+ const hasMatch = function (arr, value) {
316
+ return arr.some(function (ht) {
317
+ return ht.value === value
318
+ });
319
+ }
320
+
321
+ const headerList = $("#node-input-headers-container").css('min-height', '150px').css('min-width', '450px').editableList({
322
+ addItem: function (container, i, header) {
323
+ const row = $('<div/>').css({
324
+ overflow: 'hidden',
325
+ whiteSpace: 'nowrap',
326
+ display: 'flex'
327
+ }).appendTo(container);
328
+ const propertNameCell = $('<div/>').css({ 'flex-grow': 1 }).appendTo(row);
329
+ const propertyName = $('<input/>', { class: "node-input-header-name", type: "text", style: "width: 100%" })
330
+ .appendTo(propertNameCell)
331
+ .typedInput({ types: headerTypes });
332
+
333
+ const propertyValueCell = $('<div/>').css({ 'flex-grow': 1, 'margin-left': '10px' }).appendTo(row);
334
+ const propertyValue = $('<input/>', { class: "node-input-header-value", type: "text", style: "width: 100%" })
335
+ .appendTo(propertyValueCell)
336
+ .typedInput({
337
+ types: getHeaderOptions(header.keyType)
338
+ });
339
+
340
+ const setup = function(_header) {
341
+ const headerTypeIsAPreset = function(h) {return hasMatch(headerTypes, h) };
342
+ const headerValueIsAPreset = function(h, v) {return hasMatch(getHeaderOptions(h), v) };
343
+
344
+ const {keyType, keyValue, valueType, valueValue} = header;
345
+
346
+ if(keyType == "other") {
347
+ propertyName.typedInput('type', keyType);
348
+ propertyName.typedInput('value', keyValue);
349
+ } else if (headerTypeIsAPreset(keyType)) {
350
+ propertyName.typedInput('type', keyType);
351
+ } else {
352
+ propertyName.typedInput('type', "other");
353
+ propertyName.typedInput('value', keyValue);
354
+ }
355
+
356
+ if(valueType == "other" || valueType == "env" ) {
357
+ propertyValue.typedInput('type', valueType);
358
+ propertyValue.typedInput('value', valueValue);
359
+ } else if (headerValueIsAPreset(propertyName.typedInput('type'), valueType)) {
360
+ propertyValue.typedInput('type', valueType);
361
+ } else {
362
+ propertyValue.typedInput('type', "other");
363
+ propertyValue.typedInput('value', valueValue);
364
+ }
365
+ }
366
+ setup(header);
367
+
368
+ propertyName.on('change', function (event) {
369
+ propertyValue.typedInput('types', getHeaderOptions(propertyName.typedInput('type')));
370
+ });
371
+
372
+ },
373
+ sortable: true,
374
+ removable: true
375
+ });
376
+ if (node.headers) {
377
+ for (let index = 0; index < node.headers.length; index++) {
378
+ const element = node.headers[index];
379
+ headerList.editableList('addItem', node.headers[index]);
380
+ }
381
+ }
217
382
  },
218
383
  oneditsave: function() {
384
+
385
+ const node = this;
386
+
219
387
  if (!/^wss:/i.test($("#node-config-input-path").val())) {
220
388
  $("#node-config-input-tls").val("_ADD_");
221
389
  }
222
390
  if (!$("#node-config-input-hb-cb").prop("checked")) {
223
391
  $("#node-config-input-hb").val("0");
224
392
  }
393
+
394
+ const headers = $("#node-input-headers-container").editableList('items');
395
+
396
+ node.headers = [];
397
+ headers.each(function(i) {
398
+ const header = $(this);
399
+ const keyType = header.find(".node-input-header-name").typedInput('type');
400
+ const keyValue = header.find(".node-input-header-name").typedInput('value');
401
+ const valueType = header.find(".node-input-header-value").typedInput('type');
402
+ const valueValue = header.find(".node-input-header-value").typedInput('value');
403
+ node.headers.push({
404
+ keyType, keyValue, valueType, valueValue
405
+ })
406
+
407
+ });
408
+ },
409
+ oneditresize: function(size) {
410
+ const dlg = $("#dialog-form");
411
+ const expandRow = dlg.find('.node-input-headers-container-row');
412
+ let height = dlg.height() - 5;
413
+ if(expandRow && expandRow.length){
414
+ const siblingRows = dlg.find('> .form-row:not(.node-input-headers-container-row)');
415
+ for (let i = 0; i < siblingRows.size(); i++) {
416
+ const cr = $(siblingRows[i]);
417
+ if(cr.is(":visible"))
418
+ height -= cr.outerHeight(true);
419
+ }
420
+ $("#node-input-headers-container").editableList('height',height);
421
+ }
225
422
  }
226
423
  });
227
424
 
@@ -299,8 +496,15 @@
299
496
  <span data-i18n="inject.seconds"></span>
300
497
  </span>
301
498
  </div>
499
+ <div class="form-row" style="margin-bottom:0;">
500
+ <label><i class="fa fa-list"></i> <span data-i18n="httpin.label.headers"></span></label>
501
+ </div>
502
+ <div class="form-row node-input-headers-container-row">
503
+ <ol id="node-input-headers-container"></ol>
504
+ </div>
302
505
  <div class="form-tips">
303
506
  <p><span data-i18n="[html]websocket.tip.url1"></span></p>
304
- <span data-i18n="[html]websocket.tip.url2"></span>
507
+ <p><span data-i18n="[html]websocket.tip.url2"></span></p>
508
+ <span data-i18n="[html]websocket.tip.headers"></span>
305
509
  </div>
306
510
  </script>
@@ -58,6 +58,7 @@ module.exports = function(RED) {
58
58
  node.isServer = !/^ws{1,2}:\/\//i.test(node.path);
59
59
  node.closing = false;
60
60
  node.tls = n.tls;
61
+ node.upgradeHeaders = n.headers
61
62
 
62
63
  if (n.hb) {
63
64
  var heartbeat = parseInt(n.hb);
@@ -96,6 +97,42 @@ module.exports = function(RED) {
96
97
  tlsNode.addTLSOptions(options);
97
98
  }
98
99
  }
100
+
101
+ // We need to check if undefined, to guard against previous installs, that will not have had this property set (applies to 3.1.x setups)
102
+ // Else this will be breaking potentially
103
+ if(node.upgradeHeaders !== undefined && node.upgradeHeaders.length > 0){
104
+ options.headers = {};
105
+ for(let i = 0;i<node.upgradeHeaders.length;i++){
106
+ const header = node.upgradeHeaders[i];
107
+ const keyType = header.keyType;
108
+ const keyValue = header.keyValue;
109
+ const valueType = header.valueType;
110
+ const valueValue = header.valueValue;
111
+
112
+ const headerName = keyType === 'other' ? keyValue : keyType;
113
+ let headerValue;
114
+
115
+ switch(valueType){
116
+ case 'other':
117
+ headerValue = valueValue;
118
+ break;
119
+
120
+ case 'env':
121
+ headerValue = RED.util.evaluateNodeProperty(valueValue,valueType,node);
122
+ break;
123
+
124
+ default:
125
+ headerValue = valueType;
126
+ break;
127
+ }
128
+
129
+ if(headerName && headerValue){
130
+ options.headers[headerName] = headerValue
131
+ }
132
+
133
+ }
134
+ }
135
+
99
136
  var socket = new ws(node.path,node.subprotocol,options);
100
137
  socket.setMaxListeners(0);
101
138
  node.server = socket; // keep for closing
@@ -411,23 +411,33 @@ module.exports = function(RED) {
411
411
  if (msg._session && msg._session.type == "tcp") {
412
412
  var client = connectionPool[msg._session.id];
413
413
  if (client) {
414
- if (Buffer.isBuffer(msg.payload)) {
415
- client.write(msg.payload);
416
- } else if (typeof msg.payload === "string" && node.base64) {
417
- client.write(Buffer.from(msg.payload,'base64'));
418
- } else {
419
- client.write(Buffer.from(""+msg.payload));
414
+ if (msg?.reset === true) {
415
+ client.destroy();
416
+ }
417
+ else {
418
+ if (Buffer.isBuffer(msg.payload)) {
419
+ client.write(msg.payload);
420
+ } else if (typeof msg.payload === "string" && node.base64) {
421
+ client.write(Buffer.from(msg.payload,'base64'));
422
+ } else {
423
+ client.write(Buffer.from(""+msg.payload));
424
+ }
420
425
  }
421
426
  }
422
427
  }
423
428
  else {
424
429
  for (var i in connectionPool) {
425
- if (Buffer.isBuffer(msg.payload)) {
426
- connectionPool[i].write(msg.payload);
427
- } else if (typeof msg.payload === "string" && node.base64) {
428
- connectionPool[i].write(Buffer.from(msg.payload,'base64'));
429
- } else {
430
- connectionPool[i].write(Buffer.from(""+msg.payload));
430
+ if (msg?.reset === true) {
431
+ connectionPool[i].destroy();
432
+ }
433
+ else {
434
+ if (Buffer.isBuffer(msg.payload)) {
435
+ connectionPool[i].write(msg.payload);
436
+ } else if (typeof msg.payload === "string" && node.base64) {
437
+ connectionPool[i].write(Buffer.from(msg.payload,'base64'));
438
+ } else {
439
+ connectionPool[i].write(Buffer.from(""+msg.payload));
440
+ }
431
441
  }
432
442
  }
433
443
  }
@@ -547,13 +557,34 @@ module.exports = function(RED) {
547
557
 
548
558
  this.on("input", function(msg, nodeSend, nodeDone) {
549
559
  var i = 0;
550
- if ((!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) {
560
+ if (msg.payload !== undefined && (!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) {
551
561
  msg.payload = msg.payload.toString();
552
562
  }
553
563
 
554
564
  var host = node.server || msg.host;
555
565
  var port = node.port || msg.port;
556
566
 
567
+ if (node.out === "sit" && msg?.reset) {
568
+ if (msg.reset === true) { // kill all connections
569
+ for (var cl in clients) {
570
+ if (clients[cl].hasOwnProperty("client")) {
571
+ clients[cl].client.destroy();
572
+ delete clients[cl];
573
+ }
574
+ }
575
+ }
576
+ if (typeof(msg.reset) === "string" && msg.reset.includes(":")) { // just kill connection host:port
577
+ if (clients.hasOwnProperty(msg.reset) && clients[msg.reset].hasOwnProperty("client")) {
578
+ clients[msg.reset].client.destroy();
579
+ delete clients[msg.reset];
580
+ }
581
+ }
582
+ const cc = Object.keys(clients).length;
583
+ node.status({fill:"green",shape:cc===0?"ring":"dot",text:RED._("tcpin.status.connections",{count:cc})});
584
+ if ((host === undefined || port === undefined) && !msg.hasOwnProperty("payload")) { return; }
585
+ if (!msg.hasOwnProperty("payload")) { return; }
586
+ }
587
+
557
588
  // Store client information independently
558
589
  // the clients object will have:
559
590
  // clients[id].client, clients[id].msg, clients[id].timeout
@@ -621,13 +652,16 @@ module.exports = function(RED) {
621
652
  clients[connection_id].connecting = true;
622
653
  clients[connection_id].client.connect(connOpts, function() {
623
654
  //node.log(RED._("tcpin.errors.client-connected"));
624
- node.status({fill:"green",shape:"dot",text:"common.status.connected"});
655
+ // node.status({fill:"green",shape:"dot",text:"common.status.connected"});
656
+ node.status({fill:"green",shape:"dot",text:RED._("tcpin.status.connections",{count:Object.keys(clients).length})});
625
657
  if (clients[connection_id] && clients[connection_id].client) {
626
658
  clients[connection_id].connected = true;
627
659
  clients[connection_id].connecting = false;
628
660
  let event;
629
661
  while (event = dequeue(clients[connection_id].msgQueue)) {
630
- clients[connection_id].client.write(event.msg.payload);
662
+ if (event.msg.payload !== undefined) {
663
+ clients[connection_id].client.write(event.msg.payload);
664
+ }
631
665
  event.nodeDone();
632
666
  }
633
667
  if (node.out === "time" && node.splitc < 0) {
@@ -823,7 +857,9 @@ module.exports = function(RED) {
823
857
  else if (!clients[connection_id].connecting && clients[connection_id].connected) {
824
858
  if (clients[connection_id] && clients[connection_id].client) {
825
859
  let event = dequeue(clients[connection_id].msgQueue)
826
- clients[connection_id].client.write(event.msg.payload);
860
+ if (event.msg.payload !== undefined ) {
861
+ clients[connection_id].client.write(event.msg.payload);
862
+ }
827
863
  event.nodeDone();
828
864
  }
829
865
  }
@@ -17,7 +17,20 @@
17
17
  </select>
18
18
  <input style="width:40px;" type="text" id="node-input-sep" pattern=".">
19
19
  </div>
20
-
20
+ <div class="form-row">
21
+ <label><i class="fa fa-code"></i> <span data-i18n="csv.label.spec"></span></label>
22
+ <div style="display: inline-grid;width: 70%;">
23
+ <select style="width:100%" id="csv-option-spec">
24
+ <option value="rfc" data-i18n="csv.spec.rfc"></option>
25
+ <option value="" data-i18n="csv.spec.legacy"></option>
26
+ </select>
27
+ <div>
28
+ <div class="form-tips csv-lecacy-warning" data-i18n="node-red:csv.spec.legacy_warning"
29
+ style="width: calc(100% - 18px); margin-top: 4px; max-width: unset;">
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </div>
21
34
  <div class="form-row">
22
35
  <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
23
36
  <input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
@@ -60,10 +73,10 @@
60
73
  <div class="form-row" style="padding-left:20px;">
61
74
  <label></label>
62
75
  <label style="width:auto; margin-right:10px;" for="node-input-ret"><span data-i18n="csv.label.newline"></span></label>
63
- <select style="width:150px;" id="node-input-ret">
76
+ <select style="width:calc(70% - 108px);" id="node-input-ret">
77
+ <option value='\r\n' data-i18n="csv.newline.windows"></option>
64
78
  <option value='\n' data-i18n="csv.newline.linux"></option>
65
79
  <option value='\r' data-i18n="csv.newline.mac"></option>
66
- <option value='\r\n' data-i18n="csv.newline.windows"></option>
67
80
  </select>
68
81
  </div>
69
82
  </script>
@@ -75,6 +88,7 @@
75
88
  color:"#DEBD5C",
76
89
  defaults: {
77
90
  name: {value:""},
91
+ spec: {value:"rfc"},
78
92
  sep: {
79
93
  value:',', required:true,
80
94
  label:RED._("node-red:csv.label.separator"),
@@ -83,7 +97,7 @@
83
97
  hdrin: {value:""},
84
98
  hdrout: {value:"none"},
85
99
  multi: {value:"one",required:true},
86
- ret: {value:'\\n'},
100
+ ret: {value:'\\r\\n'}, // default to CRLF (RFC4180 Sec 2.1: "Each record is located on a separate line, delimited by a line break (CRLF)")
87
101
  temp: {value:""},
88
102
  skip: {value:"0"},
89
103
  strings: {value:true},
@@ -123,6 +137,27 @@
123
137
  $("#node-input-sep").hide();
124
138
  }
125
139
  });
140
+
141
+ $("#csv-option-spec").on("change", function() {
142
+ if ($("#csv-option-spec").val() == "rfc") {
143
+ $(".form-tips.csv-lecacy-warning").hide();
144
+ } else {
145
+ $(".form-tips.csv-lecacy-warning").show();
146
+ }
147
+ });
148
+ // new nodes will have `spec` set to "rfc" (default), but existing nodes will either not have
149
+ // a spec value or it will be empty - we need to maintain the legacy behaviour for existing
150
+ // flows but default to rfc for new nodes
151
+ let spec = !this.spec ? "" : "rfc"
152
+ $("#csv-option-spec").val(spec).trigger("change")
153
+ },
154
+ oneditsave: function() {
155
+ const specFormVal = $("#csv-option-spec").val() || '' // empty === legacy
156
+ const spectNodeVal = this.spec || '' // empty === legacy, null/undefined means in-place node upgrade (keep as is)
157
+ if (specFormVal !== spectNodeVal) {
158
+ // only update the flow value if changed (avoid marking the node dirty unnecessarily)
159
+ this.spec = specFormVal
160
+ }
126
161
  }
127
162
  });
128
163
  </script>