@node-red/editor-client 3.1.7 → 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.
package/public/red/red.js CHANGED
@@ -3808,6 +3808,31 @@ RED.nodes = (function() {
3808
3808
  getNodeTypes: function() {
3809
3809
  return Object.keys(nodeDefinitions);
3810
3810
  },
3811
+ /**
3812
+ * Get an array of node definitions
3813
+ * @param {Object} options - options object
3814
+ * @param {boolean} [options.configOnly] - if true, only return config nodes
3815
+ * @param {function} [options.filter] - a filter function to apply to the list of nodes
3816
+ * @returns array of node definitions
3817
+ */
3818
+ getNodeDefinitions: function(options) {
3819
+ const result = []
3820
+ const configOnly = (options && options.configOnly)
3821
+ const filter = (options && options.filter)
3822
+ const keys = Object.keys(nodeDefinitions)
3823
+ for (const key of keys) {
3824
+ const def = nodeDefinitions[key]
3825
+ if(!def) { continue }
3826
+ if (configOnly && def.category !== "config") {
3827
+ continue
3828
+ }
3829
+ if (filter && !filter(nodeDefinitions[key])) {
3830
+ continue
3831
+ }
3832
+ result.push(nodeDefinitions[key])
3833
+ }
3834
+ return result
3835
+ },
3811
3836
  setNodeList: function(list) {
3812
3837
  nodeList = [];
3813
3838
  for(var i=0;i<list.length;i++) {
@@ -8996,6 +9021,16 @@ RED.utils = (function() {
8996
9021
  $('<span class="red-ui-debug-msg-type-string-swatch"></span>').css('backgroundColor',obj).appendTo(e);
8997
9022
  }
8998
9023
 
9024
+ let n = RED.nodes.node(obj) ?? RED.nodes.workspace(obj);
9025
+ if (n) {
9026
+ if (options.nodeSelector && "function" == typeof options.nodeSelector) {
9027
+ e.css('cursor', 'pointer').on("click", function(evt) {
9028
+ evt.preventDefault();
9029
+ options.nodeSelector(n.id);
9030
+ })
9031
+ }
9032
+ }
9033
+
8999
9034
  } else if (typeof obj === 'number') {
9000
9035
  e = $('<span class="red-ui-debug-msg-type-number"></span>').appendTo(entryObj);
9001
9036
 
@@ -9102,6 +9137,7 @@ RED.utils = (function() {
9102
9137
  exposeApi: exposeApi,
9103
9138
  // tools: tools // Do not pass tools down as we
9104
9139
  // keep them attached to the top-level header
9140
+ nodeSelector: options.nodeSelector,
9105
9141
  }
9106
9142
  ).appendTo(row);
9107
9143
  }
@@ -9132,6 +9168,7 @@ RED.utils = (function() {
9132
9168
  exposeApi: exposeApi,
9133
9169
  // tools: tools // Do not pass tools down as we
9134
9170
  // keep them attached to the top-level header
9171
+ nodeSelector: options.nodeSelector,
9135
9172
  }
9136
9173
  ).appendTo(row);
9137
9174
  }
@@ -9188,6 +9225,7 @@ RED.utils = (function() {
9188
9225
  exposeApi: exposeApi,
9189
9226
  // tools: tools // Do not pass tools down as we
9190
9227
  // keep them attached to the top-level header
9228
+ nodeSelector: options.nodeSelector,
9191
9229
  }
9192
9230
  ).appendTo(row);
9193
9231
  }
@@ -10172,12 +10210,24 @@ RED.utils = (function() {
10172
10210
  this.uiContainer.width(m[1]);
10173
10211
  }
10174
10212
  if (this.options.sortable) {
10213
+ var isCanceled = false; // Flag to track if an item has been canceled from being dropped into a different list
10214
+ var noDrop = false; // Flag to track if an item is being dragged into a different list
10175
10215
  var handle = (typeof this.options.sortable === 'string')?
10176
10216
  this.options.sortable :
10177
10217
  ".red-ui-editableList-item-handle";
10178
10218
  var sortOptions = {
10179
10219
  axis: "y",
10180
10220
  update: function( event, ui ) {
10221
+ // dont trigger update if the item is being canceled
10222
+ const targetList = $(event.target);
10223
+ const draggedItem = ui.item;
10224
+ const draggedItemParent = draggedItem.parent();
10225
+ if (!targetList.is(draggedItemParent) && draggedItem.hasClass("red-ui-editableList-item-constrained")) {
10226
+ noDrop = true;
10227
+ }
10228
+ if (isCanceled || noDrop) {
10229
+ return;
10230
+ }
10181
10231
  if (that.options.sortItems) {
10182
10232
  that.options.sortItems(that.items());
10183
10233
  }
@@ -10187,8 +10237,32 @@ RED.utils = (function() {
10187
10237
  tolerance: "pointer",
10188
10238
  forcePlaceholderSize:true,
10189
10239
  placeholder: "red-ui-editabelList-item-placeholder",
10190
- start: function(e, ui){
10191
- ui.placeholder.height(ui.item.height()-4);
10240
+ start: function (event, ui) {
10241
+ isCanceled = false;
10242
+ ui.placeholder.height(ui.item.height() - 4);
10243
+ ui.item.css('cursor', 'grabbing'); // TODO: this doesn't seem to work, use a class instead?
10244
+ },
10245
+ stop: function (event, ui) {
10246
+ ui.item.css('cursor', 'auto');
10247
+ },
10248
+ receive: function (event, ui) {
10249
+ if (ui.item.hasClass("red-ui-editableList-item-constrained")) {
10250
+ isCanceled = true;
10251
+ $(ui.sender).sortable('cancel');
10252
+ }
10253
+ },
10254
+ over: function (event, ui) {
10255
+ // if the dragged item is constrained, prevent it from being dropped into a different list
10256
+ const targetList = $(event.target);
10257
+ const draggedItem = ui.item;
10258
+ const draggedItemParent = draggedItem.parent();
10259
+ if (!targetList.is(draggedItemParent) && draggedItem.hasClass("red-ui-editableList-item-constrained")) {
10260
+ noDrop = true;
10261
+ draggedItem.css('cursor', 'no-drop'); // TODO: this doesn't seem to work, use a class instead?
10262
+ } else {
10263
+ noDrop = false;
10264
+ draggedItem.css('cursor', 'grabbing'); // TODO: this doesn't seem to work, use a class instead?
10265
+ }
10192
10266
  }
10193
10267
  };
10194
10268
  if (this.options.connectWith) {
@@ -14152,25 +14226,26 @@ RED.stack = (function() {
14152
14226
  return icon;
14153
14227
  }
14154
14228
 
14155
- var autoComplete = function(options) {
14156
- function getMatch(value, searchValue) {
14157
- const idx = value.toLowerCase().indexOf(searchValue.toLowerCase());
14158
- const len = idx > -1 ? searchValue.length : 0;
14159
- return {
14160
- index: idx,
14161
- found: idx > -1,
14162
- pre: value.substring(0,idx),
14163
- match: value.substring(idx,idx+len),
14164
- post: value.substring(idx+len),
14165
- }
14166
- }
14167
- function generateSpans(match) {
14168
- const els = [];
14169
- if(match.pre) { els.push($('<span/>').text(match.pre)); }
14170
- if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); }
14171
- if(match.post) { els.push($('<span/>').text(match.post)); }
14172
- return els;
14173
- }
14229
+ function getMatch(value, searchValue) {
14230
+ const idx = value.toLowerCase().indexOf(searchValue.toLowerCase());
14231
+ const len = idx > -1 ? searchValue.length : 0;
14232
+ return {
14233
+ index: idx,
14234
+ found: idx > -1,
14235
+ pre: value.substring(0,idx),
14236
+ match: value.substring(idx,idx+len),
14237
+ post: value.substring(idx+len),
14238
+ }
14239
+ }
14240
+ function generateSpans(match) {
14241
+ const els = [];
14242
+ if(match.pre) { els.push($('<span/>').text(match.pre)); }
14243
+ if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); }
14244
+ if(match.post) { els.push($('<span/>').text(match.post)); }
14245
+ return els;
14246
+ }
14247
+
14248
+ const msgAutoComplete = function(options) {
14174
14249
  return function(val) {
14175
14250
  var matches = [];
14176
14251
  options.forEach(opt => {
@@ -14200,6 +14275,197 @@ RED.stack = (function() {
14200
14275
  }
14201
14276
  }
14202
14277
 
14278
+ function getEnvVars (obj, envVars = {}) {
14279
+ contextKnownKeys.env = contextKnownKeys.env || {}
14280
+ if (contextKnownKeys.env[obj.id]) {
14281
+ return contextKnownKeys.env[obj.id]
14282
+ }
14283
+ let parent
14284
+ if (obj.type === 'tab' || obj.type === 'subflow') {
14285
+ RED.nodes.eachConfig(function (conf) {
14286
+ if (conf.type === "global-config") {
14287
+ parent = conf;
14288
+ }
14289
+ })
14290
+ } else if (obj.g) {
14291
+ parent = RED.nodes.group(obj.g)
14292
+ } else if (obj.z) {
14293
+ parent = RED.nodes.workspace(obj.z) || RED.nodes.subflow(obj.z)
14294
+ }
14295
+ if (parent) {
14296
+ getEnvVars(parent, envVars)
14297
+ }
14298
+ if (obj.env) {
14299
+ obj.env.forEach(env => {
14300
+ envVars[env.name] = obj
14301
+ })
14302
+ }
14303
+ contextKnownKeys.env[obj.id] = envVars
14304
+ return envVars
14305
+ }
14306
+
14307
+ const envAutoComplete = function (val) {
14308
+ const editStack = RED.editor.getEditStack()
14309
+ if (editStack.length === 0) {
14310
+ done([])
14311
+ return
14312
+ }
14313
+ const editingNode = editStack.pop()
14314
+ if (!editingNode) {
14315
+ return []
14316
+ }
14317
+ const envVarsMap = getEnvVars(editingNode)
14318
+ const envVars = Object.keys(envVarsMap)
14319
+ const matches = []
14320
+ const i = val.lastIndexOf('${')
14321
+ let searchKey = val
14322
+ let isSubkey = false
14323
+ if (i > -1) {
14324
+ if (val.lastIndexOf('}') < i) {
14325
+ searchKey = val.substring(i+2)
14326
+ isSubkey = true
14327
+ }
14328
+ }
14329
+ envVars.forEach(v => {
14330
+ let valMatch = getMatch(v, searchKey);
14331
+ if (valMatch.found) {
14332
+ const optSrc = envVarsMap[v]
14333
+ const element = $('<div>',{style: "display: flex"});
14334
+ const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
14335
+ valEl.append(generateSpans(valMatch))
14336
+ valEl.appendTo(element)
14337
+
14338
+ if (optSrc) {
14339
+ const optEl = $('<div>').css({ "font-size": "0.8em" });
14340
+ let label
14341
+ if (optSrc.type === 'global-config') {
14342
+ label = RED._('sidebar.context.global')
14343
+ } else if (optSrc.type === 'group') {
14344
+ label = RED.utils.getNodeLabel(optSrc) || (RED._('sidebar.info.group') + ': '+optSrc.id)
14345
+ } else {
14346
+ label = RED.utils.getNodeLabel(optSrc) || optSrc.id
14347
+ }
14348
+
14349
+ optEl.append(generateSpans({ match: label }));
14350
+ optEl.appendTo(element);
14351
+ }
14352
+ matches.push({
14353
+ value: isSubkey ? val + v + '}' : v,
14354
+ label: element,
14355
+ i: valMatch.index
14356
+ });
14357
+ }
14358
+ })
14359
+ matches.sort(function(A,B){return A.i-B.i})
14360
+ return matches
14361
+ }
14362
+
14363
+ let contextKnownKeys = {}
14364
+ let contextCache = {}
14365
+ if (RED.events) {
14366
+ RED.events.on("editor:close", function () {
14367
+ contextCache = {}
14368
+ contextKnownKeys = {}
14369
+ });
14370
+ }
14371
+
14372
+ const contextAutoComplete = function() {
14373
+ const that = this
14374
+ const getContextKeysFromRuntime = function(scope, store, searchKey, done) {
14375
+ contextKnownKeys[scope] = contextKnownKeys[scope] || {}
14376
+ contextKnownKeys[scope][store] = contextKnownKeys[scope][store] || new Set()
14377
+ if (searchKey.length > 0) {
14378
+ try {
14379
+ RED.utils.normalisePropertyExpression(searchKey)
14380
+ } catch (err) {
14381
+ // Not a valid context key, so don't try looking up
14382
+ done()
14383
+ return
14384
+ }
14385
+ }
14386
+ const url = `context/${scope}/${encodeURIComponent(searchKey)}?store=${store}&keysOnly`
14387
+ if (contextCache[url]) {
14388
+ // console.log('CACHED', url)
14389
+ done()
14390
+ } else {
14391
+ // console.log('GET', url)
14392
+ $.getJSON(url, function(data) {
14393
+ // console.log(data)
14394
+ contextCache[url] = true
14395
+ const result = data[store] || {}
14396
+ const keys = result.keys || []
14397
+ const keyPrefix = searchKey + (searchKey.length > 0 ? '.' : '')
14398
+ keys.forEach(key => {
14399
+ if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(key)) {
14400
+ contextKnownKeys[scope][store].add(keyPrefix + key)
14401
+ } else {
14402
+ contextKnownKeys[scope][store].add(searchKey + "[\""+key.replace(/"/,"\\\"")+"\"]")
14403
+ }
14404
+ })
14405
+ done()
14406
+ })
14407
+ }
14408
+ }
14409
+ const getContextKeys = function(key, done) {
14410
+ const keyParts = key.split('.')
14411
+ const partialKey = keyParts.pop()
14412
+ let scope = that.propertyType
14413
+ if (scope === 'flow') {
14414
+ // Get the flow id of the node we're editing
14415
+ const editStack = RED.editor.getEditStack()
14416
+ if (editStack.length === 0) {
14417
+ done([])
14418
+ return
14419
+ }
14420
+ const editingNode = editStack.pop()
14421
+ if (editingNode.z) {
14422
+ scope = `${scope}/${editingNode.z}`
14423
+ } else {
14424
+ done([])
14425
+ return
14426
+ }
14427
+ }
14428
+ const store = (contextStoreOptions.length === 1) ? contextStoreOptions[0].value : that.optionValue
14429
+ const searchKey = keyParts.join('.')
14430
+
14431
+ getContextKeysFromRuntime(scope, store, searchKey, function() {
14432
+ if (contextKnownKeys[scope][store].has(key) || key.endsWith(']')) {
14433
+ getContextKeysFromRuntime(scope, store, key, function() {
14434
+ done(contextKnownKeys[scope][store])
14435
+ })
14436
+ }
14437
+ done(contextKnownKeys[scope][store])
14438
+ })
14439
+ }
14440
+
14441
+ return function(val, done) {
14442
+ getContextKeys(val, function (keys) {
14443
+ const matches = []
14444
+ keys.forEach(v => {
14445
+ let optVal = v
14446
+ let valMatch = getMatch(optVal, val);
14447
+ if (!valMatch.found && val.length > 0 && val.endsWith('.')) {
14448
+ // Search key ends in '.' - but doesn't match. Check again
14449
+ // with [" at the end instead so we match bracket notation
14450
+ valMatch = getMatch(optVal, val.substring(0, val.length - 1) + '["')
14451
+ }
14452
+ if (valMatch.found) {
14453
+ const element = $('<div>',{style: "display: flex"});
14454
+ const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
14455
+ valEl.append(generateSpans(valMatch))
14456
+ valEl.appendTo(element)
14457
+ matches.push({
14458
+ value: optVal,
14459
+ label: element,
14460
+ });
14461
+ }
14462
+ })
14463
+ matches.sort(function(a, b) { return a.value.localeCompare(b.value) });
14464
+ done(matches);
14465
+ })
14466
+ }
14467
+ }
14468
+
14203
14469
  // This is a hand-generated list of completions for the core nodes (based on the node help html).
14204
14470
  var msgCompletions = [
14205
14471
  { value: "payload" },
@@ -14264,20 +14530,22 @@ RED.stack = (function() {
14264
14530
  { value: "_session", source: ["websocket out","tcp out"] },
14265
14531
  ]
14266
14532
  var allOptions = {
14267
- msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: autoComplete(msgCompletions)},
14533
+ msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: msgAutoComplete(msgCompletions)},
14268
14534
  flow: {value:"flow",label:"flow.",hasValue:true,
14269
14535
  options:[],
14270
14536
  validate:RED.utils.validatePropertyExpression,
14271
14537
  parse: contextParse,
14272
14538
  export: contextExport,
14273
- valueLabel: contextLabel
14539
+ valueLabel: contextLabel,
14540
+ autoComplete: contextAutoComplete
14274
14541
  },
14275
14542
  global: {value:"global",label:"global.",hasValue:true,
14276
14543
  options:[],
14277
14544
  validate:RED.utils.validatePropertyExpression,
14278
14545
  parse: contextParse,
14279
14546
  export: contextExport,
14280
- valueLabel: contextLabel
14547
+ valueLabel: contextLabel,
14548
+ autoComplete: contextAutoComplete
14281
14549
  },
14282
14550
  str: {value:"str",label:"string",icon:"red/images/typedInput/az.svg"},
14283
14551
  num: {value:"num",label:"number",icon:"red/images/typedInput/09.svg",validate: function(v) {
@@ -14312,7 +14580,25 @@ RED.stack = (function() {
14312
14580
  }
14313
14581
  },
14314
14582
  re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.svg"},
14315
- date: {value:"date",label:"timestamp",icon:"fa fa-clock-o",hasValue:false},
14583
+ date: {
14584
+ value:"date",
14585
+ label:"timestamp",
14586
+ icon:"fa fa-clock-o",
14587
+ options:[
14588
+ {
14589
+ label: 'milliseconds since epoch',
14590
+ value: ''
14591
+ },
14592
+ {
14593
+ label: 'YYYY-MM-DDTHH:mm:ss.sssZ',
14594
+ value: 'iso'
14595
+ },
14596
+ {
14597
+ label: 'JavaScript Date Object',
14598
+ value: 'object'
14599
+ }
14600
+ ]
14601
+ },
14316
14602
  jsonata: {
14317
14603
  value: "jsonata",
14318
14604
  label: "expression",
@@ -14349,7 +14635,8 @@ RED.stack = (function() {
14349
14635
  env: {
14350
14636
  value: "env",
14351
14637
  label: "env variable",
14352
- icon: "red/images/typedInput/env.svg"
14638
+ icon: "red/images/typedInput/env.svg",
14639
+ autoComplete: envAutoComplete
14353
14640
  },
14354
14641
  node: {
14355
14642
  value: "node",
@@ -14481,18 +14768,75 @@ RED.stack = (function() {
14481
14768
  eyeButton.show();
14482
14769
  }
14483
14770
  }
14771
+ },
14772
+ 'conf-types': {
14773
+ value: "conf-types",
14774
+ label: "config",
14775
+ icon: "fa fa-cog",
14776
+ // hasValue: false,
14777
+ valueLabel: function (container, value) {
14778
+ // get the selected option (for access to the "name" and "module" properties)
14779
+ const _options = this._optionsCache || this.typeList.find(opt => opt.value === value)?.options || []
14780
+ const selectedOption = _options.find(opt => opt.value === value) || {
14781
+ title: '',
14782
+ name: '',
14783
+ module: ''
14784
+ }
14785
+ container.attr("title", selectedOption.title) // set tooltip to the full path/id of the module/node
14786
+ container.text(selectedOption.name) // apply the "name" of the selected option
14787
+ // set "line-height" such as to make the "name" appear further up, giving room for the "module" to be displayed below the value
14788
+ container.css("line-height", "1.4em")
14789
+ // add the module name in smaller, lighter font below the value
14790
+ $('<div></div>').text(selectedOption.module).css({
14791
+ // "font-family": "var(--red-ui-monospace-font)",
14792
+ color: "var(--red-ui-tertiary-text-color)",
14793
+ "font-size": "0.8em",
14794
+ "line-height": "1em",
14795
+ opacity: 0.8
14796
+ }).appendTo(container);
14797
+ },
14798
+ // hasValue: false,
14799
+ options: function () {
14800
+ if (this._optionsCache) {
14801
+ return this._optionsCache
14802
+ }
14803
+ const configNodes = RED.nodes.registry.getNodeDefinitions({configOnly: true, filter: (def) => def.type !== "global-config"}).map((def) => {
14804
+ // create a container with with 2 rows (row 1 for the name, row 2 for the module name in smaller, lighter font)
14805
+ const container = $('<div style="display: flex; flex-direction: column; justify-content: space-between; row-gap: 1px;">')
14806
+ const row1Name = $('<div>').text(def.type)
14807
+ const row2Module = $('<div style="font-size: 0.8em; color: var(--red-ui-tertiary-text-color);">').text(def.set.module)
14808
+ container.append(row1Name, row2Module)
14809
+
14810
+ return {
14811
+ value: def.type,
14812
+ name: def.type,
14813
+ enabled: def.set.enabled ?? true,
14814
+ local: def.set.local,
14815
+ title: def.set.id, // tooltip e.g. "node-red-contrib-foo/bar"
14816
+ module: def.set.module,
14817
+ icon: container[0].outerHTML.trim(), // the typeInput will interpret this as html text and render it in the anchor
14818
+ }
14819
+ })
14820
+ this._optionsCache = configNodes
14821
+ return configNodes
14822
+ }
14484
14823
  }
14485
14824
  };
14486
14825
 
14826
+
14487
14827
  // For a type with options, check value is a valid selection
14488
14828
  // If !opt.multiple, returns the valid option object
14489
14829
  // if opt.multiple, returns an array of valid option objects
14490
14830
  // If not valid, returns null;
14491
14831
 
14492
14832
  function isOptionValueValid(opt, currentVal) {
14833
+ let _options = opt.options
14834
+ if (typeof _options === "function") {
14835
+ _options = _options.call(this)
14836
+ }
14493
14837
  if (!opt.multiple) {
14494
- for (var i=0;i<opt.options.length;i++) {
14495
- op = opt.options[i];
14838
+ for (var i=0;i<_options.length;i++) {
14839
+ op = _options[i];
14496
14840
  if (typeof op === "string" && op === currentVal) {
14497
14841
  return {value:currentVal}
14498
14842
  } else if (op.value === currentVal) {
@@ -14509,8 +14853,8 @@ RED.stack = (function() {
14509
14853
  currentValues[v] = true;
14510
14854
  }
14511
14855
  });
14512
- for (var i=0;i<opt.options.length;i++) {
14513
- op = opt.options[i];
14856
+ for (var i=0;i<_options.length;i++) {
14857
+ op = _options[i];
14514
14858
  var val = typeof op === "string" ? op : op.value;
14515
14859
  if (currentValues.hasOwnProperty(val)) {
14516
14860
  delete currentValues[val];
@@ -14525,6 +14869,7 @@ RED.stack = (function() {
14525
14869
  }
14526
14870
 
14527
14871
  var nlsd = false;
14872
+ let contextStoreOptions;
14528
14873
 
14529
14874
  $.widget( "nodered.typedInput", {
14530
14875
  _create: function() {
@@ -14536,7 +14881,7 @@ RED.stack = (function() {
14536
14881
  }
14537
14882
  }
14538
14883
  var contextStores = RED.settings.context.stores;
14539
- var contextOptions = contextStores.map(function(store) {
14884
+ contextStoreOptions = contextStores.map(function(store) {
14540
14885
  return {value:store,label: store, icon:'<i class="red-ui-typedInput-icon fa fa-database"></i>'}
14541
14886
  }).sort(function(A,B) {
14542
14887
  if (A.value === RED.settings.context.default) {
@@ -14547,13 +14892,17 @@ RED.stack = (function() {
14547
14892
  return A.value.localeCompare(B.value);
14548
14893
  }
14549
14894
  })
14550
- if (contextOptions.length < 2) {
14895
+ if (contextStoreOptions.length < 2) {
14551
14896
  allOptions.flow.options = [];
14552
14897
  allOptions.global.options = [];
14553
14898
  } else {
14554
- allOptions.flow.options = contextOptions;
14555
- allOptions.global.options = contextOptions;
14899
+ allOptions.flow.options = contextStoreOptions;
14900
+ allOptions.global.options = contextStoreOptions;
14556
14901
  }
14902
+ // Translate timestamp options
14903
+ allOptions.date.options.forEach(opt => {
14904
+ opt.label = RED._("typedInput.date.format." + (opt.value || 'timestamp'), {defaultValue: opt.label})
14905
+ })
14557
14906
  }
14558
14907
  nlsd = true;
14559
14908
  var that = this;
@@ -14642,7 +14991,7 @@ RED.stack = (function() {
14642
14991
  that.element.trigger('paste',evt);
14643
14992
  });
14644
14993
  this.input.on('keydown', function(evt) {
14645
- if (that.typeMap[that.propertyType].autoComplete) {
14994
+ if (that.typeMap[that.propertyType].autoComplete || that.input.hasClass('red-ui-autoComplete')) {
14646
14995
  return
14647
14996
  }
14648
14997
  if (evt.keyCode >= 37 && evt.keyCode <= 40) {
@@ -14936,7 +15285,9 @@ RED.stack = (function() {
14936
15285
  if (this.optionMenu) {
14937
15286
  this.optionMenu.remove();
14938
15287
  }
14939
- this.menu.remove();
15288
+ if (this.menu) {
15289
+ this.menu.remove();
15290
+ }
14940
15291
  this.uiSelect.remove();
14941
15292
  },
14942
15293
  types: function(types) {
@@ -14969,7 +15320,7 @@ RED.stack = (function() {
14969
15320
  this.menu = this._createMenu(this.typeList,{},function(v) { that.type(v) });
14970
15321
  if (currentType && !this.typeMap.hasOwnProperty(currentType)) {
14971
15322
  if (!firstCall) {
14972
- this.type(this.typeList[0].value);
15323
+ this.type(this.typeList[0]?.value || ""); // permit empty typeList
14973
15324
  }
14974
15325
  } else {
14975
15326
  this.propertyType = null;
@@ -15006,6 +15357,11 @@ RED.stack = (function() {
15006
15357
  var selectedOption = [];
15007
15358
  var valueToCheck = value;
15008
15359
  if (opt.options) {
15360
+ let _options = opt.options
15361
+ if (typeof opt.options === "function") {
15362
+ _options = opt.options.call(this)
15363
+ }
15364
+
15009
15365
  if (opt.hasValue && opt.parse) {
15010
15366
  var parts = opt.parse(value);
15011
15367
  if (this.options.debug) { console.log(this.identifier,"new parse",parts) }
@@ -15019,8 +15375,8 @@ RED.stack = (function() {
15019
15375
  checkValues = valueToCheck.split(",");
15020
15376
  }
15021
15377
  checkValues.forEach(function(valueToCheck) {
15022
- for (var i=0;i<opt.options.length;i++) {
15023
- var op = opt.options[i];
15378
+ for (var i=0;i<_options.length;i++) {
15379
+ var op = _options[i];
15024
15380
  if (typeof op === "string") {
15025
15381
  if (op === valueToCheck || op === ""+valueToCheck) {
15026
15382
  selectedOption.push(that.activeOptions[op]);
@@ -15055,7 +15411,7 @@ RED.stack = (function() {
15055
15411
  },
15056
15412
  type: function(type) {
15057
15413
  if (!arguments.length) {
15058
- return this.propertyType;
15414
+ return this.propertyType || this.options?.default || '';
15059
15415
  } else {
15060
15416
  var that = this;
15061
15417
  if (this.options.debug) { console.log(this.identifier,"----- SET TYPE -----",type) }
@@ -15065,6 +15421,9 @@ RED.stack = (function() {
15065
15421
  // If previousType is !null, then this is a change of the type, rather than the initialisation
15066
15422
  var previousType = this.typeMap[this.propertyType];
15067
15423
  previousValue = this.input.val();
15424
+ if (this.input.hasClass('red-ui-autoComplete')) {
15425
+ this.input.autoComplete("destroy");
15426
+ }
15068
15427
 
15069
15428
  if (previousType && this.typeChanged) {
15070
15429
  if (this.options.debug) { console.log(this.identifier,"typeChanged",{previousType,previousValue}) }
@@ -15111,7 +15470,9 @@ RED.stack = (function() {
15111
15470
  this.input.val(this.oldValues.hasOwnProperty("_")?this.oldValues["_"]:(opt.default||""))
15112
15471
  }
15113
15472
  if (previousType.autoComplete) {
15114
- this.input.autoComplete("destroy");
15473
+ if (this.input.hasClass('red-ui-autoComplete')) {
15474
+ this.input.autoComplete("destroy");
15475
+ }
15115
15476
  }
15116
15477
  }
15117
15478
  this.propertyType = type;
@@ -15151,6 +15512,10 @@ RED.stack = (function() {
15151
15512
  this.optionMenu = null;
15152
15513
  }
15153
15514
  if (opt.options) {
15515
+ let _options = opt.options
15516
+ if (typeof _options === "function") {
15517
+ _options = opt.options.call(this);
15518
+ }
15154
15519
  if (this.optionExpandButton) {
15155
15520
  this.optionExpandButton.hide();
15156
15521
  this.optionExpandButton.shown = false;
@@ -15167,7 +15532,7 @@ RED.stack = (function() {
15167
15532
  this.valueLabelContainer.hide();
15168
15533
  }
15169
15534
  this.activeOptions = {};
15170
- opt.options.forEach(function(o) {
15535
+ _options.forEach(function(o) {
15171
15536
  if (typeof o === 'string') {
15172
15537
  that.activeOptions[o] = {label:o,value:o};
15173
15538
  } else {
@@ -15187,7 +15552,7 @@ RED.stack = (function() {
15187
15552
  if (validValues) {
15188
15553
  that._updateOptionSelectLabel(validValues)
15189
15554
  } else {
15190
- op = opt.options[0];
15555
+ op = _options[0] || {value:""}; // permit zero options
15191
15556
  if (typeof op === "string") {
15192
15557
  this.value(op);
15193
15558
  that._updateOptionSelectLabel({value:op});
@@ -15206,7 +15571,7 @@ RED.stack = (function() {
15206
15571
  that._updateOptionSelectLabel(validValues);
15207
15572
  }
15208
15573
  } else {
15209
- var selectedOption = this.optionValue||opt.options[0];
15574
+ var selectedOption = this.optionValue||_options[0];
15210
15575
  if (opt.parse) {
15211
15576
  var selectedOptionObj = typeof selectedOption === "string"?{value:selectedOption}:selectedOption
15212
15577
  var parts = opt.parse(this.input.val(),selectedOptionObj);
@@ -15239,8 +15604,18 @@ RED.stack = (function() {
15239
15604
  } else {
15240
15605
  this.optionSelectTrigger.hide();
15241
15606
  }
15607
+ if (opt.autoComplete) {
15608
+ let searchFunction = opt.autoComplete
15609
+ if (searchFunction.length === 0) {
15610
+ searchFunction = opt.autoComplete.call(this)
15611
+ }
15612
+ this.input.autoComplete({
15613
+ search: searchFunction,
15614
+ minLength: 0
15615
+ })
15616
+ }
15242
15617
  }
15243
- this.optionMenu = this._createMenu(opt.options,opt,function(v){
15618
+ this.optionMenu = this._createMenu(_options,opt,function(v){
15244
15619
  if (!opt.multiple) {
15245
15620
  that._updateOptionSelectLabel(that.activeOptions[v]);
15246
15621
  if (!opt.hasValue) {
@@ -15281,8 +15656,12 @@ RED.stack = (function() {
15281
15656
  this.valueLabelContainer.hide();
15282
15657
  this.elementDiv.show();
15283
15658
  if (opt.autoComplete) {
15659
+ let searchFunction = opt.autoComplete
15660
+ if (searchFunction.length === 0) {
15661
+ searchFunction = opt.autoComplete.call(this)
15662
+ }
15284
15663
  this.input.autoComplete({
15285
- search: opt.autoComplete,
15664
+ search: searchFunction,
15286
15665
  minLength: 0
15287
15666
  })
15288
15667
  }
@@ -26791,6 +27170,10 @@ RED.view = (function() {
26791
27170
  }
26792
27171
  })
26793
27172
  }
27173
+ if (selection.links) {
27174
+ selectedLinks.clear();
27175
+ selection.links.forEach(selectedLinks.add);
27176
+ }
26794
27177
  }
26795
27178
  }
26796
27179
  updateSelection();
@@ -33948,47 +34331,78 @@ RED.editor = (function() {
33948
34331
 
33949
34332
  /**
33950
34333
  * Create a config-node select box for this property
33951
- * @param node - the node being edited
33952
- * @param property - the name of the field
33953
- * @param type - the type of the config-node
34334
+ * @param {Object} node - the node being edited
34335
+ * @param {String} property - the name of the node property
34336
+ * @param {String} type - the type of the config-node
34337
+ * @param {"node-config-input"|"node-input"|"node-input-subflow-env"} prefix - the prefix to use in the input element ids
34338
+ * @param {Function} [filter] - a function to filter the list of config nodes
34339
+ * @param {Object} [env] - the environment variable object (only used for subflow env vars)
33954
34340
  */
33955
- function prepareConfigNodeSelect(node,property,type,prefix,filter) {
33956
- var input = $("#"+prefix+"-"+property);
33957
- if (input.length === 0 ) {
34341
+ function prepareConfigNodeSelect(node, property, type, prefix, filter, env) {
34342
+ let nodeValue
34343
+ if (prefix === 'node-input-subflow-env') {
34344
+ nodeValue = env?.value
34345
+ } else {
34346
+ nodeValue = node[property]
34347
+ }
34348
+
34349
+ const buttonId = `${prefix}-lookup-${property}`
34350
+ const selectId = prefix + '-' + property
34351
+ const input = $(`#${selectId}`);
34352
+ if (input.length === 0) {
33958
34353
  return;
33959
34354
  }
33960
- var newWidth = input.width();
33961
- var attrStyle = input.attr('style');
33962
- var m;
34355
+ const attrStyle = input.attr('style');
34356
+ let newWidth;
34357
+ let m;
33963
34358
  if ((m = /(^|\s|;)width\s*:\s*([^;]+)/i.exec(attrStyle)) !== null) {
33964
34359
  newWidth = m[2].trim();
33965
34360
  } else {
33966
34361
  newWidth = "70%";
33967
34362
  }
33968
- var outerWrap = $("<div></div>").css({
34363
+ const outerWrap = $("<div></div>").css({
33969
34364
  width: newWidth,
33970
- display:'inline-flex'
34365
+ display: 'inline-flex'
33971
34366
  });
33972
- var select = $('<select id="'+prefix+'-'+property+'"></select>').appendTo(outerWrap);
34367
+ const select = $('<select id="' + selectId + '"></select>').appendTo(outerWrap);
33973
34368
  input.replaceWith(outerWrap);
33974
34369
  // set the style attr directly - using width() on FF causes a value of 114%...
33975
34370
  select.css({
33976
34371
  'flex-grow': 1
33977
34372
  });
33978
- updateConfigNodeSelect(property,type,node[property],prefix,filter);
33979
- $('<a id="'+prefix+'-lookup-'+property+'" class="red-ui-button"><i class="fa fa-pencil"></i></a>')
33980
- .css({"margin-left":"10px"})
34373
+ updateConfigNodeSelect(property, type, nodeValue, prefix, filter);
34374
+ const disableButton = function(disabled) {
34375
+ btn.prop( "disabled", !!disabled)
34376
+ btn.toggleClass("disabled", !!disabled)
34377
+ }
34378
+ // create the edit button
34379
+ const btn = $('<a id="' + buttonId + '" class="red-ui-button"><i class="fa fa-pencil"></i></a>')
34380
+ .css({ "margin-left": "10px" })
33981
34381
  .appendTo(outerWrap);
33982
- $('#'+prefix+'-lookup-'+property).on("click", function(e) {
33983
- showEditConfigNodeDialog(property,type,select.find(":selected").val(),prefix,node);
34382
+
34383
+ // add the click handler
34384
+ btn.on("click", function (e) {
34385
+ const selectedOpt = select.find(":selected")
34386
+ if (selectedOpt.data('env')) { return } // don't show the dialog for env vars items (MVP. Future enhancement: lookup the env, if present, show the associated edit dialog)
34387
+ if (btn.prop("disabled")) { return }
34388
+ showEditConfigNodeDialog(property, type, selectedOpt.val(), prefix, node);
33984
34389
  e.preventDefault();
33985
34390
  });
34391
+
34392
+ // dont permit the user to click the button if the selected option is an env var
34393
+ select.on("change", function () {
34394
+ const selectedOpt = select.find(":selected")
34395
+ if (selectedOpt?.data('env')) {
34396
+ disableButton(true)
34397
+ } else {
34398
+ disableButton(false)
34399
+ }
34400
+ });
33986
34401
  var label = "";
33987
- var configNode = RED.nodes.node(node[property]);
33988
- var node_def = RED.nodes.getType(type);
34402
+ var configNode = RED.nodes.node(nodeValue);
33989
34403
 
33990
34404
  if (configNode) {
33991
- label = RED.utils.getNodeLabel(configNode,configNode.id);
34405
+ label = RED.utils.getNodeLabel(configNode, configNode.id);
33992
34406
  }
33993
34407
  input.val(label);
33994
34408
  }
@@ -34390,12 +34804,9 @@ RED.editor = (function() {
34390
34804
  }
34391
34805
 
34392
34806
  function defaultConfigNodeSort(A,B) {
34393
- if (A.__label__ < B.__label__) {
34394
- return -1;
34395
- } else if (A.__label__ > B.__label__) {
34396
- return 1;
34397
- }
34398
- return 0;
34807
+ // sort case insensitive so that `[env] node-name` items are at the top and
34808
+ // not mixed inbetween the the lower and upper case items
34809
+ return (A.__label__ || '').localeCompare((B.__label__ || ''), undefined, {sensitivity: 'base'})
34399
34810
  }
34400
34811
 
34401
34812
  function updateConfigNodeSelect(name,type,value,prefix,filter) {
@@ -34410,7 +34821,7 @@ RED.editor = (function() {
34410
34821
  }
34411
34822
  $("#"+prefix+"-"+name).val(value);
34412
34823
  } else {
34413
-
34824
+ let inclSubflowEnvvars = false
34414
34825
  var select = $("#"+prefix+"-"+name);
34415
34826
  var node_def = RED.nodes.getType(type);
34416
34827
  select.children().remove();
@@ -34418,6 +34829,7 @@ RED.editor = (function() {
34418
34829
  var activeWorkspace = RED.nodes.workspace(RED.workspaces.active());
34419
34830
  if (!activeWorkspace) {
34420
34831
  activeWorkspace = RED.nodes.subflow(RED.workspaces.active());
34832
+ inclSubflowEnvvars = true
34421
34833
  }
34422
34834
 
34423
34835
  var configNodes = [];
@@ -34433,6 +34845,31 @@ RED.editor = (function() {
34433
34845
  }
34434
34846
  }
34435
34847
  });
34848
+
34849
+ // as includeSubflowEnvvars is true, this is a subflow.
34850
+ // include any 'conf-types' env vars as a list of avaiable configs
34851
+ // in the config dropdown as `[env] node-name`
34852
+ if (inclSubflowEnvvars && activeWorkspace.env) {
34853
+ const parentEnv = activeWorkspace.env.filter(env => env.ui?.type === 'conf-types' && env.type === type)
34854
+ if (parentEnv && parentEnv.length > 0) {
34855
+ const locale = RED.i18n.lang()
34856
+ for (let i = 0; i < parentEnv.length; i++) {
34857
+ const tenv = parentEnv[i]
34858
+ const ui = tenv.ui || {}
34859
+ const labels = ui.label || {}
34860
+ const labelText = RED.editor.envVarList.lookupLabel(labels, labels["en-US"] || tenv.name, locale)
34861
+ const config = {
34862
+ env: tenv,
34863
+ id: '${' + parentEnv[0].name + '}',
34864
+ type: type,
34865
+ label: labelText,
34866
+ __label__: `[env] ${labelText}`
34867
+ }
34868
+ configNodes.push(config)
34869
+ }
34870
+ }
34871
+ }
34872
+
34436
34873
  var configSortFn = defaultConfigNodeSort;
34437
34874
  if (typeof node_def.sort == "function") {
34438
34875
  configSortFn = node_def.sort;
@@ -34444,7 +34881,10 @@ RED.editor = (function() {
34444
34881
  }
34445
34882
 
34446
34883
  configNodes.forEach(function(cn) {
34447
- $('<option value="'+cn.id+'"'+(value==cn.id?" selected":"")+'></option>').text(RED.text.bidi.enforceTextDirectionWithUCC(cn.__label__)).appendTo(select);
34884
+ const option = $('<option value="'+cn.id+'"'+(value==cn.id?" selected":"")+'></option>').text(RED.text.bidi.enforceTextDirectionWithUCC(cn.__label__)).appendTo(select);
34885
+ if (cn.env) {
34886
+ option.data('env', cn.env) // set a data attribute to indicate this is an env var (to inhibit the edit button)
34887
+ }
34448
34888
  delete cn.__label__;
34449
34889
  });
34450
34890
 
@@ -35105,9 +35545,16 @@ RED.editor = (function() {
35105
35545
  }
35106
35546
  RED.tray.close(function() {
35107
35547
  var filter = null;
35108
- if (editContext && typeof editContext._def.defaults[configProperty].filter === 'function') {
35109
- filter = function(n) {
35110
- return editContext._def.defaults[configProperty].filter.call(editContext,n);
35548
+ // when editing a config via subflow edit panel, the `configProperty` will not
35549
+ // necessarily be a property of the editContext._def.defaults object
35550
+ // Also, when editing via dashboard sidebar, editContext can be null
35551
+ // so we need to guard both scenarios
35552
+ if (editContext?._def) {
35553
+ const isSubflow = (editContext._def.type === 'subflow' || /subflow:.*/.test(editContext._def.type))
35554
+ if (editContext && !isSubflow && typeof editContext._def.defaults?.[configProperty]?.filter === 'function') {
35555
+ filter = function(n) {
35556
+ return editContext._def.defaults[configProperty].filter.call(editContext,n);
35557
+ }
35111
35558
  }
35112
35559
  }
35113
35560
  updateConfigNodeSelect(configProperty,configType,editing_config_node.id,prefix,filter);
@@ -35168,7 +35615,7 @@ RED.editor = (function() {
35168
35615
  RED.history.push(historyEvent);
35169
35616
  RED.tray.close(function() {
35170
35617
  var filter = null;
35171
- if (editContext && typeof editContext._def.defaults[configProperty].filter === 'function') {
35618
+ if (editContext && typeof editContext._def.defaults[configProperty]?.filter === 'function') {
35172
35619
  filter = function(n) {
35173
35620
  return editContext._def.defaults[configProperty].filter.call(editContext,n);
35174
35621
  }
@@ -35709,6 +36156,7 @@ RED.editor = (function() {
35709
36156
  }
35710
36157
  },
35711
36158
  editBuffer: function(options) { showTypeEditor("_buffer", options) },
36159
+ getEditStack: function () { return [...editStack] },
35712
36160
  buildEditForm: buildEditForm,
35713
36161
  validateNode: validateNode,
35714
36162
  updateNodeProperties: updateNodeProperties,
@@ -35753,7 +36201,8 @@ RED.editor = (function() {
35753
36201
  filteredEditPanes[type] = filter
35754
36202
  }
35755
36203
  editPanes[type] = definition;
35756
- }
36204
+ },
36205
+ prepareConfigNodeSelect: prepareConfigNodeSelect,
35757
36206
  }
35758
36207
  })();
35759
36208
  ;;(function() {
@@ -37407,8 +37856,9 @@ RED.editor = (function() {
37407
37856
  ;RED.editor.envVarList = (function() {
37408
37857
 
37409
37858
  var currentLocale = 'en-US';
37410
- var DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env'];
37411
- var DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred','jsonata'];
37859
+ const DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env'];
37860
+ const DEFAULT_ENV_TYPE_LIST_INC_CONFTYPES = ['str','num','bool','json','bin','env','conf-types'];
37861
+ const DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred','jsonata'];
37412
37862
 
37413
37863
  /**
37414
37864
  * Create env var edit interface
@@ -37416,8 +37866,8 @@ RED.editor = (function() {
37416
37866
  * @param node - subflow node
37417
37867
  */
37418
37868
  function buildPropertiesList(envContainer, node) {
37419
-
37420
- var isTemplateNode = (node.type === "subflow");
37869
+ if(RED.editor.envVarList.debug) { console.log('envVarList: buildPropertiesList', envContainer, node) }
37870
+ const isTemplateNode = (node.type === "subflow");
37421
37871
 
37422
37872
  envContainer
37423
37873
  .css({
@@ -37489,7 +37939,14 @@ RED.editor = (function() {
37489
37939
  // if `opt.ui` does not exist, then apply defaults. If these
37490
37940
  // defaults do not change then they will get stripped off
37491
37941
  // before saving.
37492
- if (opt.type === 'cred') {
37942
+ if (opt.type === 'conf-types') {
37943
+ opt.ui = opt.ui || {
37944
+ icon: "fa fa-cog",
37945
+ type: "conf-types",
37946
+ opts: {opts:[]}
37947
+ }
37948
+ opt.ui.type = "conf-types";
37949
+ } else if (opt.type === 'cred') {
37493
37950
  opt.ui = opt.ui || {
37494
37951
  icon: "",
37495
37952
  type: "cred"
@@ -37525,7 +37982,7 @@ RED.editor = (function() {
37525
37982
  }
37526
37983
  });
37527
37984
 
37528
- buildEnvEditRow(uiRow, opt.ui, nameField, valueField);
37985
+ buildEnvEditRow(uiRow, opt, nameField, valueField);
37529
37986
  nameField.trigger('change');
37530
37987
  }
37531
37988
  },
@@ -37587,21 +38044,23 @@ RED.editor = (function() {
37587
38044
  * @param nameField - name field of env var
37588
38045
  * @param valueField - value field of env var
37589
38046
  */
37590
- function buildEnvEditRow(container, ui, nameField, valueField) {
38047
+ function buildEnvEditRow(container, opt, nameField, valueField) {
38048
+ const ui = opt.ui
38049
+ if(RED.editor.envVarList.debug) { console.log('envVarList: buildEnvEditRow', container, ui, nameField, valueField) }
37591
38050
  container.addClass("red-ui-editor-subflow-env-ui-row")
37592
38051
  var topRow = $('<div></div>').appendTo(container);
37593
38052
  $('<div></div>').appendTo(topRow);
37594
38053
  $('<div>').text(RED._("editor.icon")).appendTo(topRow);
37595
38054
  $('<div>').text(RED._("editor.label")).appendTo(topRow);
37596
- $('<div>').text(RED._("editor.inputType")).appendTo(topRow);
38055
+ $('<div class="red-env-ui-input-type-col">').text(RED._("editor.inputType")).appendTo(topRow);
37597
38056
 
37598
38057
  var row = $('<div></div>').appendTo(container);
37599
38058
  $('<div><i class="red-ui-editableList-item-handle fa fa-bars"></i></div>').appendTo(row);
37600
38059
  var typeOptions = {
37601
- 'input': {types:DEFAULT_ENV_TYPE_LIST},
37602
- 'select': {opts:[]},
37603
- 'spinner': {},
37604
- 'cred': {}
38060
+ 'input': {types:DEFAULT_ENV_TYPE_LIST_INC_CONFTYPES},
38061
+ 'select': {opts:[]},
38062
+ 'spinner': {},
38063
+ 'cred': {}
37605
38064
  };
37606
38065
  if (ui.opts) {
37607
38066
  typeOptions[ui.type] = ui.opts;
@@ -37666,15 +38125,16 @@ RED.editor = (function() {
37666
38125
  labelInput.attr("placeholder",$(this).val())
37667
38126
  });
37668
38127
 
37669
- var inputCell = $('<div></div>').appendTo(row);
37670
- var inputCellInput = $('<input type="text">').css("width","100%").appendTo(inputCell);
38128
+ var inputCell = $('<div class="red-env-ui-input-type-col"></div>').appendTo(row);
38129
+ var uiInputTypeInput = $('<input type="text">').css("width","100%").appendTo(inputCell);
37671
38130
  if (ui.type === "input") {
37672
- inputCellInput.val(ui.opts.types.join(","));
38131
+ uiInputTypeInput.val(ui.opts.types.join(","));
37673
38132
  }
37674
38133
  var checkbox;
37675
38134
  var selectBox;
37676
38135
 
37677
- inputCellInput.typedInput({
38136
+ // the options presented in the UI section for an "input" type selection
38137
+ uiInputTypeInput.typedInput({
37678
38138
  types: [
37679
38139
  {
37680
38140
  value:"input",
@@ -37835,7 +38295,7 @@ RED.editor = (function() {
37835
38295
  }
37836
38296
  });
37837
38297
  ui.opts.opts = vals;
37838
- inputCellInput.typedInput('value',Date.now())
38298
+ uiInputTypeInput.typedInput('value',Date.now())
37839
38299
  }
37840
38300
  }
37841
38301
  }
@@ -37902,12 +38362,13 @@ RED.editor = (function() {
37902
38362
  } else {
37903
38363
  delete ui.opts.max;
37904
38364
  }
37905
- inputCellInput.typedInput('value',Date.now())
38365
+ uiInputTypeInput.typedInput('value',Date.now())
37906
38366
  }
37907
38367
  }
37908
38368
  }
37909
38369
  }
37910
38370
  },
38371
+ 'conf-types',
37911
38372
  {
37912
38373
  value:"none",
37913
38374
  label:RED._("editor.inputs.none"), icon:"fa fa-times",hasValue:false
@@ -37925,14 +38386,20 @@ RED.editor = (function() {
37925
38386
  // In the case of 'input' type, the typedInput uses the multiple-option
37926
38387
  // mode. Its value needs to be set to a comma-separately list of the
37927
38388
  // selected options.
37928
- inputCellInput.typedInput('value',ui.opts.types.join(","))
38389
+ uiInputTypeInput.typedInput('value',ui.opts.types.join(","))
38390
+ } else if (ui.type === 'conf-types') {
38391
+ // In the case of 'conf-types' type, the typedInput will be populated
38392
+ // with a list of all config nodes types installed.
38393
+ // Restore the value to the last selected type
38394
+ uiInputTypeInput.typedInput('value', opt.type)
37929
38395
  } else {
37930
38396
  // No other type cares about `value`, but doing this will
37931
38397
  // force a refresh of the label now that `ui.opts` has
37932
38398
  // been updated.
37933
- inputCellInput.typedInput('value',Date.now())
38399
+ uiInputTypeInput.typedInput('value',Date.now())
37934
38400
  }
37935
38401
 
38402
+ if(RED.editor.envVarList.debug) { console.log('envVarList: inputCellInput on:typedinputtypechange. ui.type = ' + ui.type) }
37936
38403
  switch (ui.type) {
37937
38404
  case 'input':
37938
38405
  valueField.typedInput('types',ui.opts.types);
@@ -37950,7 +38417,7 @@ RED.editor = (function() {
37950
38417
  valueField.typedInput('types',['cred']);
37951
38418
  break;
37952
38419
  default:
37953
- valueField.typedInput('types',DEFAULT_ENV_TYPE_LIST)
38420
+ valueField.typedInput('types', DEFAULT_ENV_TYPE_LIST);
37954
38421
  }
37955
38422
  if (ui.type === 'checkbox') {
37956
38423
  valueField.typedInput('type','bool');
@@ -37962,8 +38429,46 @@ RED.editor = (function() {
37962
38429
  }
37963
38430
 
37964
38431
  }).on("change", function(evt,type) {
37965
- if (ui.type === 'input') {
37966
- var types = inputCellInput.typedInput('value');
38432
+ const selectedType = $(this).typedInput('type') // the UI typedInput type
38433
+ if(RED.editor.envVarList.debug) { console.log('envVarList: inputCellInput on:change. selectedType = ' + selectedType) }
38434
+ if (selectedType === 'conf-types') {
38435
+ const selectedConfigType = $(this).typedInput('value') || opt.type
38436
+ let activeWorkspace = RED.nodes.workspace(RED.workspaces.active());
38437
+ if (!activeWorkspace) {
38438
+ activeWorkspace = RED.nodes.subflow(RED.workspaces.active());
38439
+ }
38440
+
38441
+ // get a list of all config nodes matching the selectedValue
38442
+ const configNodes = [];
38443
+ RED.nodes.eachConfig(function(config) {
38444
+ if (config.type == selectedConfigType && (!config.z || config.z === activeWorkspace.id)) {
38445
+ const modulePath = config._def?.set?.id || ''
38446
+ let label = RED.utils.getNodeLabel(config, config.id) || config.id;
38447
+ label += config.d ? ' ['+RED._('workspace.disabled')+']' : '';
38448
+ const _config = {
38449
+ _type: selectedConfigType,
38450
+ value: config.id,
38451
+ label: label,
38452
+ title: modulePath ? modulePath + ' - ' + label : label,
38453
+ enabled: config.d !== true,
38454
+ disabled: config.d === true,
38455
+ }
38456
+ configNodes.push(_config);
38457
+ }
38458
+ });
38459
+ const tiTypes = {
38460
+ value: selectedConfigType,
38461
+ label: "config",
38462
+ icon: "fa fa-cog",
38463
+ options: configNodes,
38464
+ }
38465
+ valueField.typedInput('types', [tiTypes]);
38466
+ valueField.typedInput('type', selectedConfigType);
38467
+ valueField.typedInput('value', opt.value);
38468
+
38469
+
38470
+ } else if (ui.type === 'input') {
38471
+ var types = uiInputTypeInput.typedInput('value');
37967
38472
  ui.opts.types = (types === "") ? ["str"] : types.split(",");
37968
38473
  valueField.typedInput('types',ui.opts.types);
37969
38474
  }
@@ -37975,7 +38480,7 @@ RED.editor = (function() {
37975
38480
  })
37976
38481
  // Set the input to the right type. This will trigger the 'typedinputtypechange'
37977
38482
  // event handler (just above ^^) to update the value if needed
37978
- inputCellInput.typedInput('type',ui.type)
38483
+ uiInputTypeInput.typedInput('type',ui.type)
37979
38484
  }
37980
38485
 
37981
38486
  function setLocale(l, list) {
@@ -46896,17 +47401,19 @@ RED.subflow = (function() {
46896
47401
 
46897
47402
 
46898
47403
  /**
46899
- * Create interface for controlling env var UI definition
47404
+ * Build the edit dialog for a subflow template (creating/modifying a subflow template)
47405
+ * @param {Object} uiContainer - the jQuery container for the environment variable list
47406
+ * @param {Object} node - the subflow template node
46900
47407
  */
46901
- function buildEnvControl(envList,node) {
47408
+ function buildEnvControl(uiContainer,node) {
46902
47409
  var tabs = RED.tabs.create({
46903
47410
  id: "subflow-env-tabs",
46904
47411
  onchange: function(tab) {
46905
47412
  if (tab.id === "subflow-env-tab-preview") {
46906
47413
  var inputContainer = $("#subflow-input-ui");
46907
- var list = envList.editableList("items");
47414
+ var list = uiContainer.editableList("items");
46908
47415
  var exportedEnv = exportEnvList(list, true);
46909
- buildEnvUI(inputContainer, exportedEnv,node);
47416
+ buildEnvUI(inputContainer, exportedEnv, node);
46910
47417
  }
46911
47418
  $("#subflow-env-tabs-content").children().hide();
46912
47419
  $("#" + tab.id).show();
@@ -46944,12 +47451,33 @@ RED.subflow = (function() {
46944
47451
  RED.editor.envVarList.setLocale(locale);
46945
47452
  }
46946
47453
 
46947
-
46948
- function buildEnvUIRow(row, tenv, ui, node) {
47454
+ /**
47455
+ * Build a UI row for a subflow instance environment variable
47456
+ * Also used to build the UI row for subflow template preview
47457
+ * @param {JQuery} row - A form row element
47458
+ * @param {Object} tenv - A template environment variable
47459
+ * @param {String} tenv.name - The name of the environment variable
47460
+ * @param {String} tenv.type - The type of the environment variable
47461
+ * @param {String} tenv.value - The value set for this environment variable
47462
+ * @param {Object} tenv.parent - The parent environment variable
47463
+ * @param {String} tenv.parent.value - The value set for the parent environment variable
47464
+ * @param {String} tenv.parent.type - The type of the parent environment variable
47465
+ * @param {Object} tenv.ui - The UI configuration for the environment variable
47466
+ * @param {String} tenv.ui.icon - The icon for the environment variable
47467
+ * @param {Object} tenv.ui.label - The label for the environment variable
47468
+ * @param {String} tenv.ui.type - The type of the UI control for the environment variable
47469
+ * @param {Object} node - The subflow instance node
47470
+ */
47471
+ function buildEnvUIRow(row, tenv, node) {
47472
+ if(RED.subflow.debug) { console.log("buildEnvUIRow", tenv) }
47473
+ const ui = tenv.ui || {}
46949
47474
  ui.label = ui.label||{};
46950
47475
  if ((tenv.type === "cred" || (tenv.parent && tenv.parent.type === "cred")) && !ui.type) {
46951
47476
  ui.type = "cred";
46952
47477
  ui.opts = {};
47478
+ } else if (tenv.type === "conf-types") {
47479
+ ui.type = "conf-types"
47480
+ ui.opts = { types: ['conf-types'] }
46953
47481
  } else if (!ui.type) {
46954
47482
  ui.type = "input";
46955
47483
  ui.opts = { types: RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST }
@@ -46993,9 +47521,10 @@ RED.subflow = (function() {
46993
47521
  if (tenv.hasOwnProperty('type')) {
46994
47522
  val.type = tenv.type;
46995
47523
  }
47524
+ const elId = getSubflowEnvPropertyName(tenv.name)
46996
47525
  switch(ui.type) {
46997
47526
  case "input":
46998
- input = $('<input type="text">').css('width','70%').appendTo(row);
47527
+ input = $('<input type="text">').css('width','70%').attr('id', elId).appendTo(row);
46999
47528
  if (ui.opts.types && ui.opts.types.length > 0) {
47000
47529
  var inputType = val.type;
47001
47530
  if (ui.opts.types.indexOf(inputType) === -1) {
@@ -47022,7 +47551,7 @@ RED.subflow = (function() {
47022
47551
  }
47023
47552
  break;
47024
47553
  case "select":
47025
- input = $('<select>').css('width','70%').appendTo(row);
47554
+ input = $('<select>').css('width','70%').attr('id', elId).appendTo(row);
47026
47555
  if (ui.opts.opts) {
47027
47556
  ui.opts.opts.forEach(function(o) {
47028
47557
  $('<option>').val(o.v).text(RED.editor.envVarList.lookupLabel(o.l, o.l['en-US']||o.v, locale)).appendTo(input);
@@ -47033,7 +47562,7 @@ RED.subflow = (function() {
47033
47562
  case "checkbox":
47034
47563
  label.css("cursor","default");
47035
47564
  var cblabel = $('<label>').css('width','70%').appendTo(row);
47036
- input = $('<input type="checkbox">').css({
47565
+ input = $('<input type="checkbox">').attr('id', elId).css({
47037
47566
  marginTop: 0,
47038
47567
  width: 'auto',
47039
47568
  height: '34px'
@@ -47051,7 +47580,7 @@ RED.subflow = (function() {
47051
47580
  input.prop("checked",boolVal);
47052
47581
  break;
47053
47582
  case "spinner":
47054
- input = $('<input>').css('width','70%').appendTo(row);
47583
+ input = $('<input>').css('width','70%').attr('id', elId).appendTo(row);
47055
47584
  var spinnerOpts = {};
47056
47585
  if (ui.opts.hasOwnProperty('min')) {
47057
47586
  spinnerOpts.min = ui.opts.min;
@@ -47080,18 +47609,25 @@ RED.subflow = (function() {
47080
47609
  default: 'cred'
47081
47610
  })
47082
47611
  break;
47083
- }
47084
- if (input) {
47085
- input.attr('id',getSubflowEnvPropertyName(tenv.name))
47612
+ case "conf-types":
47613
+ // let clsId = 'config-node-input-' + val.type + '-' + val.value + '-' + Math.floor(Math.random() * 100000);
47614
+ // clsId = clsId.replace(/\W/g, '-');
47615
+ // input = $('<input>').css('width','70%').addClass(clsId).attr('id', elId).appendTo(row);
47616
+ input = $('<input>').css('width','70%').attr('id', elId).appendTo(row);
47617
+ const _type = tenv.parent?.type || tenv.type;
47618
+ RED.editor.prepareConfigNodeSelect(node, tenv.name, _type, 'node-input-subflow-env', null, tenv);
47619
+ break;
47086
47620
  }
47087
47621
  }
47088
47622
 
47089
47623
  /**
47090
- * Create environment variable input UI
47624
+ * Build the edit form for a subflow instance
47625
+ * Also used to build the preview form in the subflow template edit dialog
47091
47626
  * @param uiContainer - container for UI
47092
47627
  * @param envList - env var definitions of template
47093
47628
  */
47094
47629
  function buildEnvUI(uiContainer, envList, node) {
47630
+ if(RED.subflow.debug) { console.log("buildEnvUI",envList) }
47095
47631
  uiContainer.empty();
47096
47632
  for (var i = 0; i < envList.length; i++) {
47097
47633
  var tenv = envList[i];
@@ -47099,7 +47635,7 @@ RED.subflow = (function() {
47099
47635
  continue;
47100
47636
  }
47101
47637
  var row = $("<div/>", { class: "form-row" }).appendTo(uiContainer);
47102
- buildEnvUIRow(row,tenv, tenv.ui || {}, node);
47638
+ buildEnvUIRow(row, tenv, node);
47103
47639
  }
47104
47640
  }
47105
47641
  // buildEnvUI
@@ -47172,6 +47708,9 @@ RED.subflow = (function() {
47172
47708
  delete ui.opts
47173
47709
  }
47174
47710
  break;
47711
+ case "conf-types":
47712
+ delete ui.opts;
47713
+ break;
47175
47714
  default:
47176
47715
  delete ui.opts;
47177
47716
  }
@@ -47194,8 +47733,9 @@ RED.subflow = (function() {
47194
47733
  if (/^subflow:/.test(node.type)) {
47195
47734
  var subflowDef = RED.nodes.subflow(node.type.substring(8));
47196
47735
  if (subflowDef.env) {
47197
- subflowDef.env.forEach(function(env) {
47736
+ subflowDef.env.forEach(function(env, i) {
47198
47737
  var item = {
47738
+ index: i,
47199
47739
  name:env.name,
47200
47740
  parent: {
47201
47741
  type: env.type,
@@ -47260,6 +47800,7 @@ RED.subflow = (function() {
47260
47800
  }
47261
47801
 
47262
47802
  function exportSubflowInstanceEnv(node) {
47803
+ if(RED.subflow.debug) { console.log("exportSubflowInstanceEnv",node) }
47263
47804
  var env = [];
47264
47805
  // First, get the values for the SubflowTemplate defined properties
47265
47806
  // - these are the ones with custom UI elements
@@ -47306,6 +47847,9 @@ RED.subflow = (function() {
47306
47847
  item.type = 'bool';
47307
47848
  item.value = ""+input.prop("checked");
47308
47849
  break;
47850
+ case "conf-types":
47851
+ item.value = input.val()
47852
+ item.type = data.parent.value;
47309
47853
  }
47310
47854
  if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) {
47311
47855
  env.push(item);
@@ -47319,8 +47863,15 @@ RED.subflow = (function() {
47319
47863
  return 'node-input-subflow-env-'+name.replace(/[^a-z0-9-_]/ig,"_");
47320
47864
  }
47321
47865
 
47322
- // Called by subflow.oneditprepare for both instances and templates
47866
+
47867
+ /**
47868
+ * Build the subflow edit form
47869
+ * Called by subflow.oneditprepare for both instances and templates
47870
+ * @param {"subflow"|"subflow-template"} type - the type of subflow being edited
47871
+ * @param {Object} node - the node being edited
47872
+ */
47323
47873
  function buildEditForm(type,node) {
47874
+ if(RED.subflow.debug) { console.log("buildEditForm",type,node) }
47324
47875
  if (type === "subflow-template") {
47325
47876
  // This is the tabbed UI that offers the env list - with UI options
47326
47877
  // plus the preview tab
@@ -54982,10 +55533,15 @@ RED.touch.radialMenu = (function() {
54982
55533
 
54983
55534
  function listTour() {
54984
55535
  return [
55536
+ {
55537
+ id: "4_0",
55538
+ label: "4.0",
55539
+ path: "./tours/welcome.js"
55540
+ },
54985
55541
  {
54986
55542
  id: "3_1",
54987
55543
  label: "3.1",
54988
- path: "./tours/welcome.js"
55544
+ path: "./tours/3.1/welcome.js"
54989
55545
  },
54990
55546
  {
54991
55547
  id: "3_0",