@node-red/editor-client 3.1.8 → 4.0.0-beta.2

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.
Files changed (149) hide show
  1. package/locales/de/editor.json +2 -0
  2. package/locales/en-US/editor.json +10 -1
  3. package/locales/fr/editor.json +8 -1
  4. package/locales/ja/editor.json +10 -3
  5. package/package.json +1 -1
  6. package/public/red/about +52 -0
  7. package/public/red/red.js +1433 -263
  8. package/public/red/red.min.js +3 -3
  9. package/public/red/style.min.css +2 -2
  10. package/public/red/tours/3.1/welcome.js +231 -0
  11. package/public/red/tours/images/nr4-auto-complete.png +0 -0
  12. package/public/red/tours/images/nr4-config-select.png +0 -0
  13. package/public/red/tours/images/nr4-multiplayer.png +0 -0
  14. package/public/red/tours/images/nr4-plugins.png +0 -0
  15. package/public/red/tours/images/nr4-sf-config.png +0 -0
  16. package/public/red/tours/images/nr4-timestamp-formatting.png +0 -0
  17. package/public/red/tours/welcome.js +105 -161
  18. package/public/types/node/assert/strict.d.ts +1 -1
  19. package/public/types/node/assert.d.ts +8 -9
  20. package/public/types/node/async_hooks.d.ts +9 -5
  21. package/public/types/node/buffer.d.ts +43 -18
  22. package/public/types/node/child_process.d.ts +8 -5
  23. package/public/types/node/cluster.d.ts +15 -19
  24. package/public/types/node/console.d.ts +2 -2
  25. package/public/types/node/crypto.d.ts +165 -70
  26. package/public/types/node/dgram.d.ts +4 -4
  27. package/public/types/node/diagnostics_channel.d.ts +8 -7
  28. package/public/types/node/dns/promises.d.ts +11 -9
  29. package/public/types/node/dns.d.ts +18 -13
  30. package/public/types/node/dom-events.d.ts +129 -0
  31. package/public/types/node/domain.d.ts +2 -2
  32. package/public/types/node/events.d.ts +49 -12
  33. package/public/types/node/fs/promises.d.ts +68 -24
  34. package/public/types/node/fs.d.ts +132 -59
  35. package/public/types/node/globals.d.ts +31 -17
  36. package/public/types/node/http.d.ts +138 -27
  37. package/public/types/node/http2.d.ts +38 -5
  38. package/public/types/node/https.d.ts +12 -3
  39. package/public/types/node/module.d.ts +1 -2
  40. package/public/types/node/net.d.ts +69 -28
  41. package/public/types/node/os.d.ts +16 -5
  42. package/public/types/node/path.d.ts +5 -5
  43. package/public/types/node/perf_hooks.d.ts +48 -9
  44. package/public/types/node/process.d.ts +18 -17
  45. package/public/types/node/querystring.d.ts +2 -2
  46. package/public/types/node/readline/promises.d.ts +146 -0
  47. package/public/types/node/readline.d.ts +141 -31
  48. package/public/types/node/stream/consumers.d.ts +2 -2
  49. package/public/types/node/stream/promises.d.ts +1 -1
  50. package/public/types/node/stream/web.d.ts +4 -66
  51. package/public/types/node/stream.d.ts +96 -118
  52. package/public/types/node/string_decoder.d.ts +2 -2
  53. package/public/types/node/test.d.ts +200 -16
  54. package/public/types/node/timers/promises.d.ts +1 -26
  55. package/public/types/node/timers.d.ts +2 -2
  56. package/public/types/node/tls.d.ts +21 -12
  57. package/public/types/node/trace_events.d.ts +12 -2
  58. package/public/types/node/ts4.8/assert/strict.d.ts +11 -0
  59. package/public/types/node/ts4.8/assert.d.ts +964 -0
  60. package/public/types/node/ts4.8/async_hooks.d.ts +504 -0
  61. package/public/types/node/ts4.8/buffer.d.ts +2262 -0
  62. package/public/types/node/ts4.8/child_process.d.ts +1372 -0
  63. package/public/types/node/ts4.8/cluster.d.ts +413 -0
  64. package/public/types/node/ts4.8/console.d.ts +415 -0
  65. package/public/types/node/ts4.8/crypto.d.ts +3967 -0
  66. package/public/types/node/ts4.8/dgram.d.ts +548 -0
  67. package/public/types/node/ts4.8/diagnostics_channel.d.ts +156 -0
  68. package/public/types/node/ts4.8/dns/promises.d.ts +373 -0
  69. package/public/types/node/ts4.8/dns.d.ts +662 -0
  70. package/public/types/node/ts4.8/dom-events.d.ts +129 -0
  71. package/public/types/node/ts4.8/domain.d.ts +173 -0
  72. package/public/types/node/ts4.8/events.d.ts +681 -0
  73. package/public/types/node/ts4.8/fs/promises.d.ts +1141 -0
  74. package/public/types/node/ts4.8/fs.d.ts +3875 -0
  75. package/public/types/node/ts4.8/globals.d.ts +297 -0
  76. package/public/types/node/ts4.8/http.d.ts +1617 -0
  77. package/public/types/node/ts4.8/http2.d.ts +2137 -0
  78. package/public/types/node/ts4.8/https.d.ts +544 -0
  79. package/public/types/node/ts4.8/module.d.ts +117 -0
  80. package/public/types/node/ts4.8/net.d.ts +872 -0
  81. package/public/types/node/ts4.8/os.d.ts +469 -0
  82. package/public/types/node/ts4.8/path.d.ts +194 -0
  83. package/public/types/node/ts4.8/perf_hooks.d.ts +628 -0
  84. package/public/types/node/ts4.8/process.d.ts +1485 -0
  85. package/public/types/node/ts4.8/querystring.d.ts +134 -0
  86. package/public/types/node/ts4.8/readline/promises.d.ts +146 -0
  87. package/public/types/node/ts4.8/readline.d.ts +656 -0
  88. package/public/types/node/ts4.8/stream/consumers.d.ts +15 -0
  89. package/public/types/node/ts4.8/stream/promises.d.ts +45 -0
  90. package/public/types/node/ts4.8/stream/web.d.ts +333 -0
  91. package/public/types/node/ts4.8/stream.d.ts +1343 -0
  92. package/public/types/node/ts4.8/string_decoder.d.ts +70 -0
  93. package/public/types/node/ts4.8/test.d.ts +377 -0
  94. package/public/types/node/ts4.8/timers/promises.d.ts +71 -0
  95. package/public/types/node/ts4.8/timers.d.ts +97 -0
  96. package/public/types/node/ts4.8/tls.d.ts +1031 -0
  97. package/public/types/node/ts4.8/trace_events.d.ts +174 -0
  98. package/public/types/node/ts4.8/tty.d.ts +209 -0
  99. package/public/types/node/ts4.8/url.d.ts +900 -0
  100. package/public/types/node/ts4.8/util.d.ts +1853 -0
  101. package/public/types/node/ts4.8/v8.d.ts +399 -0
  102. package/public/types/node/ts4.8/vm.d.ts +512 -0
  103. package/public/types/node/ts4.8/wasi.d.ts +161 -0
  104. package/public/types/node/ts4.8/worker_threads.d.ts +692 -0
  105. package/public/types/node/ts4.8/zlib.d.ts +520 -0
  106. package/public/types/node/tty.d.ts +5 -3
  107. package/public/types/node/url.d.ts +81 -39
  108. package/public/types/node/util.d.ts +269 -13
  109. package/public/types/node/v8.d.ts +22 -4
  110. package/public/types/node/vm.d.ts +7 -5
  111. package/public/types/node/wasi.d.ts +2 -2
  112. package/public/types/node/worker_threads.d.ts +51 -11
  113. package/public/types/node/zlib.d.ts +2 -2
  114. package/public/types/node-red/func.d.ts +26 -17
  115. package/public/vendor/ace/worker-jsonata.js +1 -1
  116. package/public/vendor/monaco/dist/{fa2cc0ab9f0bec2b3365.ttf → 0c718f5b7d2bce997c5f.ttf} +0 -0
  117. package/public/vendor/monaco/dist/css.worker.js +1 -1
  118. package/public/vendor/monaco/dist/css.worker.js.LICENSE.txt +1 -1
  119. package/public/vendor/monaco/dist/editor.js +1 -29
  120. package/public/vendor/monaco/dist/editor.js.LICENSE.txt +2 -2
  121. package/public/vendor/monaco/dist/editor.worker.js +1 -1
  122. package/public/vendor/monaco/dist/html.worker.js +1 -1
  123. package/public/vendor/monaco/dist/html.worker.js.LICENSE.txt +1 -1
  124. package/public/vendor/monaco/dist/json.worker.js +1 -1
  125. package/public/vendor/monaco/dist/json.worker.js.LICENSE.txt +1 -1
  126. package/public/vendor/monaco/dist/locale/cs.js +324 -106
  127. package/public/vendor/monaco/dist/locale/de.js +336 -118
  128. package/public/vendor/monaco/dist/locale/es.js +329 -111
  129. package/public/vendor/monaco/dist/locale/fr.js +334 -116
  130. package/public/vendor/monaco/dist/locale/it.js +327 -109
  131. package/public/vendor/monaco/dist/locale/ja.js +329 -111
  132. package/public/vendor/monaco/dist/locale/ko.js +330 -112
  133. package/public/vendor/monaco/dist/locale/pl.js +329 -111
  134. package/public/vendor/monaco/dist/locale/pt-br.js +329 -111
  135. package/public/vendor/monaco/dist/locale/qps-ploc.js +330 -112
  136. package/public/vendor/monaco/dist/locale/ru.js +331 -113
  137. package/public/vendor/monaco/dist/locale/tr.js +329 -111
  138. package/public/vendor/monaco/dist/locale/zh-hans.js +331 -113
  139. package/public/vendor/monaco/dist/locale/zh-hant.js +331 -113
  140. package/public/vendor/monaco/dist/ts.worker.js +2 -2
  141. package/public/vendor/vendor.js +1 -1
  142. package/public/vendor/monaco/dist/7064e66c3890a12c47b4.ttf +0 -0
  143. /package/public/red/tours/{images → 3.1/images}/context-menu.png +0 -0
  144. /package/public/red/tours/{images → 3.1/images}/global-env-vars.png +0 -0
  145. /package/public/red/tours/{images → 3.1/images}/hiding-flows.png +0 -0
  146. /package/public/red/tours/{images → 3.1/images}/locking-flows.png +0 -0
  147. /package/public/red/tours/{images → 3.1/images}/mermaid.png +0 -0
  148. /package/public/red/tours/{images → 3.1/images}/node-help.png +0 -0
  149. /package/public/red/tours/{images → 3.1/images}/tab-changes.png +0 -0
package/public/red/red.js CHANGED
@@ -116,6 +116,7 @@ var RED = (function() {
116
116
  cache: false,
117
117
  url: 'plugins',
118
118
  success: function(data) {
119
+ RED.plugins.setPluginList(data);
119
120
  loader.reportProgress(RED._("event.loadPlugins"), 13)
120
121
  RED.i18n.loadPluginCatalogs(function() {
121
122
  loadPlugins(function() {
@@ -625,6 +626,41 @@ var RED = (function() {
625
626
  RED.view.redrawStatus(node);
626
627
  }
627
628
  });
629
+ RED.comms.subscribe("notification/plugin/#",function(topic,msg) {
630
+ if (topic == "notification/plugin/added") {
631
+ RED.settings.refreshSettings(function(err, data) {
632
+ let addedPlugins = [];
633
+ msg.forEach(function(m) {
634
+ let id = m.id;
635
+ RED.plugins.addPlugin(m);
636
+
637
+ m.plugins.forEach((p) => {
638
+ addedPlugins.push(p.id);
639
+ })
640
+
641
+ RED.i18n.loadNodeCatalog(id, function() {
642
+ var lang = localStorage.getItem("editor-language")||RED.i18n.detectLanguage();
643
+ $.ajax({
644
+ headers: {
645
+ "Accept":"text/html",
646
+ "Accept-Language": lang
647
+ },
648
+ cache: false,
649
+ url: 'plugins/'+id,
650
+ success: function(data) {
651
+ appendPluginConfig(data);
652
+ }
653
+ });
654
+ });
655
+ });
656
+ if (addedPlugins.length) {
657
+ let pluginList = "<ul><li>"+addedPlugins.map(RED.utils.sanitize).join("</li><li>")+"</li></ul>";
658
+ // ToDo: Adapt notification (node -> plugin)
659
+ RED.notify(RED._("palette.event.nodeAdded", {count:addedPlugins.length})+pluginList,"success");
660
+ }
661
+ })
662
+ }
663
+ });
628
664
 
629
665
  let pendingNodeRemovedNotifications = []
630
666
  let pendingNodeRemovedTimeout
@@ -894,6 +930,10 @@ var RED = (function() {
894
930
 
895
931
  RED.nodes.init();
896
932
  RED.runtime.init()
933
+
934
+ if (RED.settings.theme("multiplayer.enabled",false)) {
935
+ RED.multiplayer.init()
936
+ }
897
937
  RED.comms.connect();
898
938
 
899
939
  $("#red-ui-main-container").show();
@@ -1812,6 +1852,7 @@ RED.user = (function() {
1812
1852
  }
1813
1853
 
1814
1854
  function logout() {
1855
+ RED.events.emit('logout')
1815
1856
  var tokens = RED.settings.get("auth-tokens");
1816
1857
  var token = tokens?tokens.access_token:"";
1817
1858
  $.ajax({
@@ -1850,6 +1891,7 @@ RED.user = (function() {
1850
1891
  });
1851
1892
  }
1852
1893
  });
1894
+ $('<i class="fa fa-user"></i>').appendTo("#red-ui-header-button-user");
1853
1895
  } else {
1854
1896
  RED.menu.addItem("red-ui-header-button-user",{
1855
1897
  id:"usermenu-item-username",
@@ -1862,6 +1904,15 @@ RED.user = (function() {
1862
1904
  RED.user.logout();
1863
1905
  }
1864
1906
  });
1907
+ const userMenu = $("#red-ui-header-button-user")
1908
+ userMenu.empty()
1909
+ if (RED.settings.user.image) {
1910
+ $('<span class="user-profile"></span>').css({
1911
+ backgroundImage: "url("+RED.settings.user.image+")",
1912
+ }).appendTo(userMenu);
1913
+ } else {
1914
+ $('<i class="fa fa-user"></i>').appendTo(userMenu);
1915
+ }
1865
1916
  }
1866
1917
 
1867
1918
  }
@@ -1872,14 +1923,6 @@ RED.user = (function() {
1872
1923
 
1873
1924
  var userMenu = $('<li><a id="red-ui-header-button-user" class="button hide" href="#"></a></li>')
1874
1925
  .prependTo(".red-ui-header-toolbar");
1875
- if (RED.settings.user.image) {
1876
- $('<span class="user-profile"></span>').css({
1877
- backgroundImage: "url("+RED.settings.user.image+")",
1878
- }).appendTo(userMenu.find("a"));
1879
- } else {
1880
- $('<i class="fa fa-user"></i>').appendTo(userMenu.find("a"));
1881
- }
1882
-
1883
1926
  RED.menu.init({id:"red-ui-header-button-user",
1884
1927
  options: []
1885
1928
  });
@@ -1979,6 +2022,15 @@ RED.comms = (function() {
1979
2022
  var reconnectAttempts = 0;
1980
2023
  var active = false;
1981
2024
 
2025
+ RED.events.on('login', function(username) {
2026
+ // User has logged in
2027
+ // Need to upgrade the connection to be authenticated
2028
+ if (ws && ws.readyState == 1) {
2029
+ const auth_tokens = RED.settings.get("auth-tokens");
2030
+ ws.send(JSON.stringify({auth:auth_tokens.access_token}))
2031
+ }
2032
+ })
2033
+
1982
2034
  function connectWS() {
1983
2035
  active = true;
1984
2036
  var wspath;
@@ -2009,6 +2061,7 @@ RED.comms = (function() {
2009
2061
  ws.send(JSON.stringify({subscribe:t}));
2010
2062
  }
2011
2063
  }
2064
+ emit('connect')
2012
2065
  }
2013
2066
 
2014
2067
  ws = new WebSocket(wspath);
@@ -2133,10 +2186,54 @@ RED.comms = (function() {
2133
2186
  }
2134
2187
  }
2135
2188
 
2189
+ function send(topic, msg) {
2190
+ if (ws && ws.readyState == 1) {
2191
+ ws.send(JSON.stringify({
2192
+ topic,
2193
+ data: msg
2194
+ }))
2195
+ }
2196
+ }
2197
+
2198
+ const eventHandlers = {};
2199
+ function on(evt,func) {
2200
+ eventHandlers[evt] = eventHandlers[evt]||[];
2201
+ eventHandlers[evt].push(func);
2202
+ }
2203
+ function off(evt,func) {
2204
+ const handler = eventHandlers[evt];
2205
+ if (handler) {
2206
+ for (let i=0;i<handler.length;i++) {
2207
+ if (handler[i] === func) {
2208
+ handler.splice(i,1);
2209
+ return;
2210
+ }
2211
+ }
2212
+ }
2213
+ }
2214
+ function emit() {
2215
+ const evt = arguments[0]
2216
+ const args = Array.prototype.slice.call(arguments,1);
2217
+ if (eventHandlers[evt]) {
2218
+ let cpyHandlers = [...eventHandlers[evt]];
2219
+ for (let i=0;i<cpyHandlers.length;i++) {
2220
+ try {
2221
+ cpyHandlers[i].apply(null, args);
2222
+ } catch(err) {
2223
+ console.warn("RED.comms.emit error: ["+evt+"] "+(err.toString()));
2224
+ console.warn(err);
2225
+ }
2226
+ }
2227
+ }
2228
+ }
2229
+
2136
2230
  return {
2137
2231
  connect: connectWS,
2138
2232
  subscribe: subscribe,
2139
- unsubscribe:unsubscribe
2233
+ unsubscribe:unsubscribe,
2234
+ on,
2235
+ off,
2236
+ send
2140
2237
  }
2141
2238
  })();
2142
2239
  ;RED.runtime = (function() {
@@ -2175,6 +2272,223 @@ RED.comms = (function() {
2175
2272
  }
2176
2273
  }
2177
2274
  })()
2275
+ ;RED.multiplayer = (function () {
2276
+
2277
+ // sessionId - used to identify sessions across websocket reconnects
2278
+ let sessionId
2279
+
2280
+ let headerWidget
2281
+ // Map of session id to { session:'', user:{}, location:{}}
2282
+ let connections = {}
2283
+ // Map of username to { user:{}, connections:[] }
2284
+ let users = {}
2285
+
2286
+ function addUserConnection (connection) {
2287
+ if (connections[connection.session]) {
2288
+ // This is an existing connection that has been authenticated
2289
+ const existingConnection = connections[connection.session]
2290
+ if (existingConnection.user.username !== connection.user.username) {
2291
+ removeUserButton(users[existingConnection.user.username])
2292
+ }
2293
+ }
2294
+ connections[connection.session] = connection
2295
+ const user = users[connection.user.username] = users[connection.user.username] || {
2296
+ user: connection.user,
2297
+ connections: []
2298
+ }
2299
+ connection.location = connection.location || {}
2300
+ user.connections.push(connection)
2301
+
2302
+ if (connection.user.username === RED.settings.user?.username ||
2303
+ connection.session === sessionId
2304
+ ) {
2305
+ // This is the current user - do not add a extra button for them
2306
+ } else {
2307
+ if (user.connections.length === 1) {
2308
+ if (user.button) {
2309
+ clearTimeout(user.inactiveTimeout)
2310
+ clearTimeout(user.removeTimeout)
2311
+ user.button.removeClass('inactive')
2312
+ } else {
2313
+ addUserButton(user)
2314
+ }
2315
+ }
2316
+ }
2317
+ }
2318
+
2319
+ function removeUserConnection (session, isDisconnected) {
2320
+ const connection = connections[session]
2321
+ delete connections[session]
2322
+ const user = users[connection.user.username]
2323
+ const i = user.connections.indexOf(connection)
2324
+ user.connections.splice(i, 1)
2325
+ if (isDisconnected) {
2326
+ removeUserButton(user)
2327
+ } else {
2328
+ if (user.connections.length === 0) {
2329
+ // Give the user 5s to reconnect before marking inactive
2330
+ user.inactiveTimeout = setTimeout(() => {
2331
+ user.button.addClass('inactive')
2332
+ // Give the user further 20 seconds to reconnect before removing them
2333
+ // from the user toolbar entirely
2334
+ user.removeTimeout = setTimeout(() => {
2335
+ removeUserButton(user)
2336
+ }, 20000)
2337
+ }, 5000)
2338
+ }
2339
+ }
2340
+ }
2341
+
2342
+ function addUserButton (user) {
2343
+ user.button = $('<li class="red-ui-multiplayer-user"><button type="button" class="red-ui-multiplayer-user-icon" href="#"></button></li>')
2344
+ .attr('data-username', user.user.username)
2345
+ .prependTo("#red-ui-multiplayer-user-list");
2346
+ var button = user.button.find("button")
2347
+ button.on('click', function () {
2348
+ RED.popover.create({
2349
+ target:button,
2350
+ trigger: 'modal',
2351
+ interactive: true,
2352
+ width: "250px",
2353
+ direction: 'bottom',
2354
+ content: () => {
2355
+ const content = $('<div>')
2356
+ $('<div style="text-align: center">').text(user.user.username).appendTo(content)
2357
+
2358
+ const location = user.connections[0].location
2359
+ if (location.workspace) {
2360
+ const ws = RED.nodes.workspace(location.workspace) || RED.nodes.subflow(location.workspace)
2361
+ if (ws) {
2362
+ $('<div>').text(`${ws.type}: ${ws.label||ws.name||ws.id}`).appendTo(content)
2363
+ } else {
2364
+ $('<div>').text(`tab: unknown`).appendTo(content)
2365
+ }
2366
+ }
2367
+ if (location.node) {
2368
+ const node = RED.nodes.node(location.node)
2369
+ if (node) {
2370
+ $('<div>').text(`node: ${node.id}`).appendTo(content)
2371
+ } else {
2372
+ $('<div>').text(`node: unknown`).appendTo(content)
2373
+ }
2374
+ }
2375
+ return content
2376
+ },
2377
+ }).open()
2378
+ })
2379
+ if (!user.user.image) {
2380
+ $('<i class="fa fa-user"></i>').appendTo(button);
2381
+ } else {
2382
+ $('<span class="user-profile"></span>').css({
2383
+ backgroundImage: "url("+user.user.image+")",
2384
+ }).appendTo(button);
2385
+ }
2386
+ }
2387
+
2388
+ function getLocation () {
2389
+ const location = {
2390
+ workspace: RED.workspaces.active()
2391
+ }
2392
+ const editStack = RED.editor.getEditStack()
2393
+ for (let i = editStack.length - 1; i >= 0; i--) {
2394
+ if (editStack[i].id) {
2395
+ location.node = editStack[i].id
2396
+ break
2397
+ }
2398
+ }
2399
+ return location
2400
+ }
2401
+ function updateLocation () {
2402
+ const location = getLocation()
2403
+ if (location.workspace !== 0) {
2404
+ log('send', 'multiplayer/location', location)
2405
+ RED.comms.send('multiplayer/location', location)
2406
+ }
2407
+ }
2408
+
2409
+ function removeUserButton (user) {
2410
+ user.button.remove()
2411
+ delete user.button
2412
+ }
2413
+
2414
+ function updateUserLocation (data) {
2415
+ connections[data.session].location = data
2416
+ delete data.session
2417
+ }
2418
+ return {
2419
+ init: function () {
2420
+
2421
+
2422
+ sessionId = RED.settings.getLocal('multiplayer:sessionId')
2423
+ if (!sessionId) {
2424
+ sessionId = RED.nodes.id()
2425
+ RED.settings.setLocal('multiplayer:sessionId', sessionId)
2426
+ }
2427
+
2428
+ headerWidget = $('<li><ul id="red-ui-multiplayer-user-list"></ul></li>').prependTo('.red-ui-header-toolbar')
2429
+
2430
+ RED.comms.on('connect', () => {
2431
+ const location = getLocation()
2432
+ const connectInfo = {
2433
+ session: sessionId
2434
+ }
2435
+ if (location.workspace !== 0) {
2436
+ connectInfo.location = location
2437
+ }
2438
+ RED.comms.send('multiplayer/connect', connectInfo)
2439
+ })
2440
+ RED.comms.subscribe('multiplayer/#', (topic, msg) => {
2441
+ log('recv', topic, msg)
2442
+ if (topic === 'multiplayer/init') {
2443
+ // We have just reconnected, runtime has sent state to
2444
+ // initialise the world
2445
+ connections = {}
2446
+ users = {}
2447
+ $('#red-ui-multiplayer-user-list').empty()
2448
+
2449
+ msg.forEach(connection => {
2450
+ addUserConnection(connection)
2451
+ })
2452
+ } else if (topic === 'multiplayer/connection-added') {
2453
+ addUserConnection(msg)
2454
+ } else if (topic === 'multiplayer/connection-removed') {
2455
+ removeUserConnection(msg.session, msg.disconnected)
2456
+ } else if (topic === 'multiplayer/location') {
2457
+ updateUserLocation(msg)
2458
+ }
2459
+ })
2460
+
2461
+ RED.events.on('workspace:change', (event) => {
2462
+ updateLocation()
2463
+ })
2464
+ RED.events.on('editor:open', () => {
2465
+ updateLocation()
2466
+ })
2467
+ RED.events.on('editor:close', () => {
2468
+ updateLocation()
2469
+ })
2470
+ RED.events.on('editor:change', () => {
2471
+ updateLocation()
2472
+ })
2473
+ RED.events.on('login', () => {
2474
+ updateLocation()
2475
+ })
2476
+ RED.events.on('logout', () => {
2477
+ const disconnectInfo = {
2478
+ session: sessionId
2479
+ }
2480
+ RED.comms.send('multiplayer/disconnect', disconnectInfo)
2481
+ RED.settings.removeLocal('multiplayer:sessionId')
2482
+ })
2483
+ }
2484
+ }
2485
+
2486
+ function log() {
2487
+ if (RED.multiplayer.DEBUG) {
2488
+ console.log('[multiplayer]', ...arguments)
2489
+ }
2490
+ }
2491
+ })();
2178
2492
  ;/**
2179
2493
  * Copyright JS Foundation and other contributors, http://js.foundation
2180
2494
  *
@@ -3672,6 +3986,7 @@ RED.state = {
3672
3986
  ;RED.plugins = (function() {
3673
3987
  var plugins = {};
3674
3988
  var pluginsByType = {};
3989
+ var moduleList = {};
3675
3990
 
3676
3991
  function registerPlugin(id,definition) {
3677
3992
  plugins[id] = definition;
@@ -3709,10 +4024,44 @@ RED.state = {
3709
4024
  function getPluginsByType(type) {
3710
4025
  return pluginsByType[type] || [];
3711
4026
  }
4027
+
4028
+ function setPluginList(list) {
4029
+ for(let i=0;i<list.length;i++) {
4030
+ let p = list[i];
4031
+ addPlugin(p);
4032
+ }
4033
+ }
4034
+
4035
+ function addPlugin(p) {
4036
+
4037
+ moduleList[p.module] = moduleList[p.module] || {
4038
+ name:p.module,
4039
+ version:p.version,
4040
+ local:p.local,
4041
+ sets:{},
4042
+ plugin: true,
4043
+ id: p.id
4044
+ };
4045
+ if (p.pending_version) {
4046
+ moduleList[p.module].pending_version = p.pending_version;
4047
+ }
4048
+ moduleList[p.module].sets[p.name] = p;
4049
+
4050
+ RED.events.emit("registry:plugin-module-added",p.module);
4051
+ }
4052
+
4053
+ function getModule(module) {
4054
+ return moduleList[module];
4055
+ }
4056
+
3712
4057
  return {
3713
4058
  registerPlugin: registerPlugin,
3714
4059
  getPlugin: getPlugin,
3715
- getPluginsByType: getPluginsByType
4060
+ getPluginsByType: getPluginsByType,
4061
+
4062
+ setPluginList: setPluginList,
4063
+ addPlugin: addPlugin,
4064
+ getModule: getModule
3716
4065
  }
3717
4066
  })();
3718
4067
  ;/**
@@ -3808,6 +4157,31 @@ RED.nodes = (function() {
3808
4157
  getNodeTypes: function() {
3809
4158
  return Object.keys(nodeDefinitions);
3810
4159
  },
4160
+ /**
4161
+ * Get an array of node definitions
4162
+ * @param {Object} options - options object
4163
+ * @param {boolean} [options.configOnly] - if true, only return config nodes
4164
+ * @param {function} [options.filter] - a filter function to apply to the list of nodes
4165
+ * @returns array of node definitions
4166
+ */
4167
+ getNodeDefinitions: function(options) {
4168
+ const result = []
4169
+ const configOnly = (options && options.configOnly)
4170
+ const filter = (options && options.filter)
4171
+ const keys = Object.keys(nodeDefinitions)
4172
+ for (const key of keys) {
4173
+ const def = nodeDefinitions[key]
4174
+ if(!def) { continue }
4175
+ if (configOnly && def.category !== "config") {
4176
+ continue
4177
+ }
4178
+ if (filter && !filter(nodeDefinitions[key])) {
4179
+ continue
4180
+ }
4181
+ result.push(nodeDefinitions[key])
4182
+ }
4183
+ return result
4184
+ },
3811
4185
  setNodeList: function(list) {
3812
4186
  nodeList = [];
3813
4187
  for(var i=0;i<list.length;i++) {
@@ -3841,6 +4215,8 @@ RED.nodes = (function() {
3841
4215
  },
3842
4216
  removeNodeSet: function(id) {
3843
4217
  var ns = nodeSets[id];
4218
+ if (!ns) { return {} }
4219
+
3844
4220
  for (var j=0;j<ns.types.length;j++) {
3845
4221
  delete typeToId[ns.types[j]];
3846
4222
  }
@@ -9015,6 +9391,16 @@ RED.utils = (function() {
9015
9391
  $('<span class="red-ui-debug-msg-type-string-swatch"></span>').css('backgroundColor',obj).appendTo(e);
9016
9392
  }
9017
9393
 
9394
+ let n = RED.nodes.node(obj) ?? RED.nodes.workspace(obj);
9395
+ if (n) {
9396
+ if (options.nodeSelector && "function" == typeof options.nodeSelector) {
9397
+ e.css('cursor', 'pointer').on("click", function(evt) {
9398
+ evt.preventDefault();
9399
+ options.nodeSelector(n.id);
9400
+ })
9401
+ }
9402
+ }
9403
+
9018
9404
  } else if (typeof obj === 'number') {
9019
9405
  e = $('<span class="red-ui-debug-msg-type-number"></span>').appendTo(entryObj);
9020
9406
 
@@ -9121,6 +9507,7 @@ RED.utils = (function() {
9121
9507
  exposeApi: exposeApi,
9122
9508
  // tools: tools // Do not pass tools down as we
9123
9509
  // keep them attached to the top-level header
9510
+ nodeSelector: options.nodeSelector,
9124
9511
  }
9125
9512
  ).appendTo(row);
9126
9513
  }
@@ -9151,6 +9538,7 @@ RED.utils = (function() {
9151
9538
  exposeApi: exposeApi,
9152
9539
  // tools: tools // Do not pass tools down as we
9153
9540
  // keep them attached to the top-level header
9541
+ nodeSelector: options.nodeSelector,
9154
9542
  }
9155
9543
  ).appendTo(row);
9156
9544
  }
@@ -9207,6 +9595,7 @@ RED.utils = (function() {
9207
9595
  exposeApi: exposeApi,
9208
9596
  // tools: tools // Do not pass tools down as we
9209
9597
  // keep them attached to the top-level header
9598
+ nodeSelector: options.nodeSelector,
9210
9599
  }
9211
9600
  ).appendTo(row);
9212
9601
  }
@@ -10191,12 +10580,24 @@ RED.utils = (function() {
10191
10580
  this.uiContainer.width(m[1]);
10192
10581
  }
10193
10582
  if (this.options.sortable) {
10583
+ var isCanceled = false; // Flag to track if an item has been canceled from being dropped into a different list
10584
+ var noDrop = false; // Flag to track if an item is being dragged into a different list
10194
10585
  var handle = (typeof this.options.sortable === 'string')?
10195
10586
  this.options.sortable :
10196
10587
  ".red-ui-editableList-item-handle";
10197
10588
  var sortOptions = {
10198
10589
  axis: "y",
10199
10590
  update: function( event, ui ) {
10591
+ // dont trigger update if the item is being canceled
10592
+ const targetList = $(event.target);
10593
+ const draggedItem = ui.item;
10594
+ const draggedItemParent = draggedItem.parent();
10595
+ if (!targetList.is(draggedItemParent) && draggedItem.hasClass("red-ui-editableList-item-constrained")) {
10596
+ noDrop = true;
10597
+ }
10598
+ if (isCanceled || noDrop) {
10599
+ return;
10600
+ }
10200
10601
  if (that.options.sortItems) {
10201
10602
  that.options.sortItems(that.items());
10202
10603
  }
@@ -10206,8 +10607,32 @@ RED.utils = (function() {
10206
10607
  tolerance: "pointer",
10207
10608
  forcePlaceholderSize:true,
10208
10609
  placeholder: "red-ui-editabelList-item-placeholder",
10209
- start: function(e, ui){
10210
- ui.placeholder.height(ui.item.height()-4);
10610
+ start: function (event, ui) {
10611
+ isCanceled = false;
10612
+ ui.placeholder.height(ui.item.height() - 4);
10613
+ ui.item.css('cursor', 'grabbing'); // TODO: this doesn't seem to work, use a class instead?
10614
+ },
10615
+ stop: function (event, ui) {
10616
+ ui.item.css('cursor', 'auto');
10617
+ },
10618
+ receive: function (event, ui) {
10619
+ if (ui.item.hasClass("red-ui-editableList-item-constrained")) {
10620
+ isCanceled = true;
10621
+ $(ui.sender).sortable('cancel');
10622
+ }
10623
+ },
10624
+ over: function (event, ui) {
10625
+ // if the dragged item is constrained, prevent it from being dropped into a different list
10626
+ const targetList = $(event.target);
10627
+ const draggedItem = ui.item;
10628
+ const draggedItemParent = draggedItem.parent();
10629
+ if (!targetList.is(draggedItemParent) && draggedItem.hasClass("red-ui-editableList-item-constrained")) {
10630
+ noDrop = true;
10631
+ draggedItem.css('cursor', 'no-drop'); // TODO: this doesn't seem to work, use a class instead?
10632
+ } else {
10633
+ noDrop = false;
10634
+ draggedItem.css('cursor', 'grabbing'); // TODO: this doesn't seem to work, use a class instead?
10635
+ }
10211
10636
  }
10212
10637
  };
10213
10638
  if (this.options.connectWith) {
@@ -12254,7 +12679,7 @@ RED.popover = (function() {
12254
12679
  closePopup(true);
12255
12680
  });
12256
12681
  }
12257
- if (trigger === 'hover' && options.interactive) {
12682
+ if (/*trigger === 'hover' && */options.interactive) {
12258
12683
  div.on('mouseenter', function(e) {
12259
12684
  clearTimeout(timer);
12260
12685
  active = true;
@@ -14171,25 +14596,26 @@ RED.stack = (function() {
14171
14596
  return icon;
14172
14597
  }
14173
14598
 
14174
- var autoComplete = function(options) {
14175
- function getMatch(value, searchValue) {
14176
- const idx = value.toLowerCase().indexOf(searchValue.toLowerCase());
14177
- const len = idx > -1 ? searchValue.length : 0;
14178
- return {
14179
- index: idx,
14180
- found: idx > -1,
14181
- pre: value.substring(0,idx),
14182
- match: value.substring(idx,idx+len),
14183
- post: value.substring(idx+len),
14184
- }
14185
- }
14186
- function generateSpans(match) {
14187
- const els = [];
14188
- if(match.pre) { els.push($('<span/>').text(match.pre)); }
14189
- if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); }
14190
- if(match.post) { els.push($('<span/>').text(match.post)); }
14191
- return els;
14192
- }
14599
+ function getMatch(value, searchValue) {
14600
+ const idx = value.toLowerCase().indexOf(searchValue.toLowerCase());
14601
+ const len = idx > -1 ? searchValue.length : 0;
14602
+ return {
14603
+ index: idx,
14604
+ found: idx > -1,
14605
+ pre: value.substring(0,idx),
14606
+ match: value.substring(idx,idx+len),
14607
+ post: value.substring(idx+len),
14608
+ }
14609
+ }
14610
+ function generateSpans(match) {
14611
+ const els = [];
14612
+ if(match.pre) { els.push($('<span/>').text(match.pre)); }
14613
+ if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); }
14614
+ if(match.post) { els.push($('<span/>').text(match.post)); }
14615
+ return els;
14616
+ }
14617
+
14618
+ const msgAutoComplete = function(options) {
14193
14619
  return function(val) {
14194
14620
  var matches = [];
14195
14621
  options.forEach(opt => {
@@ -14219,6 +14645,197 @@ RED.stack = (function() {
14219
14645
  }
14220
14646
  }
14221
14647
 
14648
+ function getEnvVars (obj, envVars = {}) {
14649
+ contextKnownKeys.env = contextKnownKeys.env || {}
14650
+ if (contextKnownKeys.env[obj.id]) {
14651
+ return contextKnownKeys.env[obj.id]
14652
+ }
14653
+ let parent
14654
+ if (obj.type === 'tab' || obj.type === 'subflow') {
14655
+ RED.nodes.eachConfig(function (conf) {
14656
+ if (conf.type === "global-config") {
14657
+ parent = conf;
14658
+ }
14659
+ })
14660
+ } else if (obj.g) {
14661
+ parent = RED.nodes.group(obj.g)
14662
+ } else if (obj.z) {
14663
+ parent = RED.nodes.workspace(obj.z) || RED.nodes.subflow(obj.z)
14664
+ }
14665
+ if (parent) {
14666
+ getEnvVars(parent, envVars)
14667
+ }
14668
+ if (obj.env) {
14669
+ obj.env.forEach(env => {
14670
+ envVars[env.name] = obj
14671
+ })
14672
+ }
14673
+ contextKnownKeys.env[obj.id] = envVars
14674
+ return envVars
14675
+ }
14676
+
14677
+ const envAutoComplete = function (val) {
14678
+ const editStack = RED.editor.getEditStack()
14679
+ if (editStack.length === 0) {
14680
+ done([])
14681
+ return
14682
+ }
14683
+ const editingNode = editStack.pop()
14684
+ if (!editingNode) {
14685
+ return []
14686
+ }
14687
+ const envVarsMap = getEnvVars(editingNode)
14688
+ const envVars = Object.keys(envVarsMap)
14689
+ const matches = []
14690
+ const i = val.lastIndexOf('${')
14691
+ let searchKey = val
14692
+ let isSubkey = false
14693
+ if (i > -1) {
14694
+ if (val.lastIndexOf('}') < i) {
14695
+ searchKey = val.substring(i+2)
14696
+ isSubkey = true
14697
+ }
14698
+ }
14699
+ envVars.forEach(v => {
14700
+ let valMatch = getMatch(v, searchKey);
14701
+ if (valMatch.found) {
14702
+ const optSrc = envVarsMap[v]
14703
+ const element = $('<div>',{style: "display: flex"});
14704
+ const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
14705
+ valEl.append(generateSpans(valMatch))
14706
+ valEl.appendTo(element)
14707
+
14708
+ if (optSrc) {
14709
+ const optEl = $('<div>').css({ "font-size": "0.8em" });
14710
+ let label
14711
+ if (optSrc.type === 'global-config') {
14712
+ label = RED._('sidebar.context.global')
14713
+ } else if (optSrc.type === 'group') {
14714
+ label = RED.utils.getNodeLabel(optSrc) || (RED._('sidebar.info.group') + ': '+optSrc.id)
14715
+ } else {
14716
+ label = RED.utils.getNodeLabel(optSrc) || optSrc.id
14717
+ }
14718
+
14719
+ optEl.append(generateSpans({ match: label }));
14720
+ optEl.appendTo(element);
14721
+ }
14722
+ matches.push({
14723
+ value: isSubkey ? val + v + '}' : v,
14724
+ label: element,
14725
+ i: valMatch.index
14726
+ });
14727
+ }
14728
+ })
14729
+ matches.sort(function(A,B){return A.i-B.i})
14730
+ return matches
14731
+ }
14732
+
14733
+ let contextKnownKeys = {}
14734
+ let contextCache = {}
14735
+ if (RED.events) {
14736
+ RED.events.on("editor:close", function () {
14737
+ contextCache = {}
14738
+ contextKnownKeys = {}
14739
+ });
14740
+ }
14741
+
14742
+ const contextAutoComplete = function() {
14743
+ const that = this
14744
+ const getContextKeysFromRuntime = function(scope, store, searchKey, done) {
14745
+ contextKnownKeys[scope] = contextKnownKeys[scope] || {}
14746
+ contextKnownKeys[scope][store] = contextKnownKeys[scope][store] || new Set()
14747
+ if (searchKey.length > 0) {
14748
+ try {
14749
+ RED.utils.normalisePropertyExpression(searchKey)
14750
+ } catch (err) {
14751
+ // Not a valid context key, so don't try looking up
14752
+ done()
14753
+ return
14754
+ }
14755
+ }
14756
+ const url = `context/${scope}/${encodeURIComponent(searchKey)}?store=${store}&keysOnly`
14757
+ if (contextCache[url]) {
14758
+ // console.log('CACHED', url)
14759
+ done()
14760
+ } else {
14761
+ // console.log('GET', url)
14762
+ $.getJSON(url, function(data) {
14763
+ // console.log(data)
14764
+ contextCache[url] = true
14765
+ const result = data[store] || {}
14766
+ const keys = result.keys || []
14767
+ const keyPrefix = searchKey + (searchKey.length > 0 ? '.' : '')
14768
+ keys.forEach(key => {
14769
+ if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(key)) {
14770
+ contextKnownKeys[scope][store].add(keyPrefix + key)
14771
+ } else {
14772
+ contextKnownKeys[scope][store].add(searchKey + "[\""+key.replace(/"/,"\\\"")+"\"]")
14773
+ }
14774
+ })
14775
+ done()
14776
+ })
14777
+ }
14778
+ }
14779
+ const getContextKeys = function(key, done) {
14780
+ const keyParts = key.split('.')
14781
+ const partialKey = keyParts.pop()
14782
+ let scope = that.propertyType
14783
+ if (scope === 'flow') {
14784
+ // Get the flow id of the node we're editing
14785
+ const editStack = RED.editor.getEditStack()
14786
+ if (editStack.length === 0) {
14787
+ done([])
14788
+ return
14789
+ }
14790
+ const editingNode = editStack.pop()
14791
+ if (editingNode.z) {
14792
+ scope = `${scope}/${editingNode.z}`
14793
+ } else {
14794
+ done([])
14795
+ return
14796
+ }
14797
+ }
14798
+ const store = (contextStoreOptions.length === 1) ? contextStoreOptions[0].value : that.optionValue
14799
+ const searchKey = keyParts.join('.')
14800
+
14801
+ getContextKeysFromRuntime(scope, store, searchKey, function() {
14802
+ if (contextKnownKeys[scope][store].has(key) || key.endsWith(']')) {
14803
+ getContextKeysFromRuntime(scope, store, key, function() {
14804
+ done(contextKnownKeys[scope][store])
14805
+ })
14806
+ }
14807
+ done(contextKnownKeys[scope][store])
14808
+ })
14809
+ }
14810
+
14811
+ return function(val, done) {
14812
+ getContextKeys(val, function (keys) {
14813
+ const matches = []
14814
+ keys.forEach(v => {
14815
+ let optVal = v
14816
+ let valMatch = getMatch(optVal, val);
14817
+ if (!valMatch.found && val.length > 0 && val.endsWith('.')) {
14818
+ // Search key ends in '.' - but doesn't match. Check again
14819
+ // with [" at the end instead so we match bracket notation
14820
+ valMatch = getMatch(optVal, val.substring(0, val.length - 1) + '["')
14821
+ }
14822
+ if (valMatch.found) {
14823
+ const element = $('<div>',{style: "display: flex"});
14824
+ const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
14825
+ valEl.append(generateSpans(valMatch))
14826
+ valEl.appendTo(element)
14827
+ matches.push({
14828
+ value: optVal,
14829
+ label: element,
14830
+ });
14831
+ }
14832
+ })
14833
+ matches.sort(function(a, b) { return a.value.localeCompare(b.value) });
14834
+ done(matches);
14835
+ })
14836
+ }
14837
+ }
14838
+
14222
14839
  // This is a hand-generated list of completions for the core nodes (based on the node help html).
14223
14840
  var msgCompletions = [
14224
14841
  { value: "payload" },
@@ -14283,20 +14900,22 @@ RED.stack = (function() {
14283
14900
  { value: "_session", source: ["websocket out","tcp out"] },
14284
14901
  ]
14285
14902
  var allOptions = {
14286
- msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: autoComplete(msgCompletions)},
14903
+ msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: msgAutoComplete(msgCompletions)},
14287
14904
  flow: {value:"flow",label:"flow.",hasValue:true,
14288
14905
  options:[],
14289
14906
  validate:RED.utils.validatePropertyExpression,
14290
14907
  parse: contextParse,
14291
14908
  export: contextExport,
14292
- valueLabel: contextLabel
14909
+ valueLabel: contextLabel,
14910
+ autoComplete: contextAutoComplete
14293
14911
  },
14294
14912
  global: {value:"global",label:"global.",hasValue:true,
14295
14913
  options:[],
14296
14914
  validate:RED.utils.validatePropertyExpression,
14297
14915
  parse: contextParse,
14298
14916
  export: contextExport,
14299
- valueLabel: contextLabel
14917
+ valueLabel: contextLabel,
14918
+ autoComplete: contextAutoComplete
14300
14919
  },
14301
14920
  str: {value:"str",label:"string",icon:"red/images/typedInput/az.svg"},
14302
14921
  num: {value:"num",label:"number",icon:"red/images/typedInput/09.svg",validate: function(v) {
@@ -14331,7 +14950,25 @@ RED.stack = (function() {
14331
14950
  }
14332
14951
  },
14333
14952
  re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.svg"},
14334
- date: {value:"date",label:"timestamp",icon:"fa fa-clock-o",hasValue:false},
14953
+ date: {
14954
+ value:"date",
14955
+ label:"timestamp",
14956
+ icon:"fa fa-clock-o",
14957
+ options:[
14958
+ {
14959
+ label: 'milliseconds since epoch',
14960
+ value: ''
14961
+ },
14962
+ {
14963
+ label: 'YYYY-MM-DDTHH:mm:ss.sssZ',
14964
+ value: 'iso'
14965
+ },
14966
+ {
14967
+ label: 'JavaScript Date Object',
14968
+ value: 'object'
14969
+ }
14970
+ ]
14971
+ },
14335
14972
  jsonata: {
14336
14973
  value: "jsonata",
14337
14974
  label: "expression",
@@ -14368,7 +15005,8 @@ RED.stack = (function() {
14368
15005
  env: {
14369
15006
  value: "env",
14370
15007
  label: "env variable",
14371
- icon: "red/images/typedInput/env.svg"
15008
+ icon: "red/images/typedInput/env.svg",
15009
+ autoComplete: envAutoComplete
14372
15010
  },
14373
15011
  node: {
14374
15012
  value: "node",
@@ -14500,18 +15138,75 @@ RED.stack = (function() {
14500
15138
  eyeButton.show();
14501
15139
  }
14502
15140
  }
15141
+ },
15142
+ 'conf-types': {
15143
+ value: "conf-types",
15144
+ label: "config",
15145
+ icon: "fa fa-cog",
15146
+ // hasValue: false,
15147
+ valueLabel: function (container, value) {
15148
+ // get the selected option (for access to the "name" and "module" properties)
15149
+ const _options = this._optionsCache || this.typeList.find(opt => opt.value === value)?.options || []
15150
+ const selectedOption = _options.find(opt => opt.value === value) || {
15151
+ title: '',
15152
+ name: '',
15153
+ module: ''
15154
+ }
15155
+ container.attr("title", selectedOption.title) // set tooltip to the full path/id of the module/node
15156
+ container.text(selectedOption.name) // apply the "name" of the selected option
15157
+ // set "line-height" such as to make the "name" appear further up, giving room for the "module" to be displayed below the value
15158
+ container.css("line-height", "1.4em")
15159
+ // add the module name in smaller, lighter font below the value
15160
+ $('<div></div>').text(selectedOption.module).css({
15161
+ // "font-family": "var(--red-ui-monospace-font)",
15162
+ color: "var(--red-ui-tertiary-text-color)",
15163
+ "font-size": "0.8em",
15164
+ "line-height": "1em",
15165
+ opacity: 0.8
15166
+ }).appendTo(container);
15167
+ },
15168
+ // hasValue: false,
15169
+ options: function () {
15170
+ if (this._optionsCache) {
15171
+ return this._optionsCache
15172
+ }
15173
+ const configNodes = RED.nodes.registry.getNodeDefinitions({configOnly: true, filter: (def) => def.type !== "global-config"}).map((def) => {
15174
+ // create a container with with 2 rows (row 1 for the name, row 2 for the module name in smaller, lighter font)
15175
+ const container = $('<div style="display: flex; flex-direction: column; justify-content: space-between; row-gap: 1px;">')
15176
+ const row1Name = $('<div>').text(def.type)
15177
+ const row2Module = $('<div style="font-size: 0.8em; color: var(--red-ui-tertiary-text-color);">').text(def.set.module)
15178
+ container.append(row1Name, row2Module)
15179
+
15180
+ return {
15181
+ value: def.type,
15182
+ name: def.type,
15183
+ enabled: def.set.enabled ?? true,
15184
+ local: def.set.local,
15185
+ title: def.set.id, // tooltip e.g. "node-red-contrib-foo/bar"
15186
+ module: def.set.module,
15187
+ icon: container[0].outerHTML.trim(), // the typeInput will interpret this as html text and render it in the anchor
15188
+ }
15189
+ })
15190
+ this._optionsCache = configNodes
15191
+ return configNodes
15192
+ }
14503
15193
  }
14504
15194
  };
14505
15195
 
15196
+
14506
15197
  // For a type with options, check value is a valid selection
14507
15198
  // If !opt.multiple, returns the valid option object
14508
15199
  // if opt.multiple, returns an array of valid option objects
14509
15200
  // If not valid, returns null;
14510
15201
 
14511
15202
  function isOptionValueValid(opt, currentVal) {
15203
+ let _options = opt.options
15204
+ if (typeof _options === "function") {
15205
+ _options = _options.call(this)
15206
+ }
14512
15207
  if (!opt.multiple) {
14513
- for (var i=0;i<opt.options.length;i++) {
14514
- op = opt.options[i];
15208
+ for (var i=0;i<_options.length;i++) {
15209
+ op = _options[i];
14515
15210
  if (typeof op === "string" && op === currentVal) {
14516
15211
  return {value:currentVal}
14517
15212
  } else if (op.value === currentVal) {
@@ -14528,8 +15223,8 @@ RED.stack = (function() {
14528
15223
  currentValues[v] = true;
14529
15224
  }
14530
15225
  });
14531
- for (var i=0;i<opt.options.length;i++) {
14532
- op = opt.options[i];
15226
+ for (var i=0;i<_options.length;i++) {
15227
+ op = _options[i];
14533
15228
  var val = typeof op === "string" ? op : op.value;
14534
15229
  if (currentValues.hasOwnProperty(val)) {
14535
15230
  delete currentValues[val];
@@ -14544,6 +15239,7 @@ RED.stack = (function() {
14544
15239
  }
14545
15240
 
14546
15241
  var nlsd = false;
15242
+ let contextStoreOptions;
14547
15243
 
14548
15244
  $.widget( "nodered.typedInput", {
14549
15245
  _create: function() {
@@ -14555,7 +15251,7 @@ RED.stack = (function() {
14555
15251
  }
14556
15252
  }
14557
15253
  var contextStores = RED.settings.context.stores;
14558
- var contextOptions = contextStores.map(function(store) {
15254
+ contextStoreOptions = contextStores.map(function(store) {
14559
15255
  return {value:store,label: store, icon:'<i class="red-ui-typedInput-icon fa fa-database"></i>'}
14560
15256
  }).sort(function(A,B) {
14561
15257
  if (A.value === RED.settings.context.default) {
@@ -14566,13 +15262,17 @@ RED.stack = (function() {
14566
15262
  return A.value.localeCompare(B.value);
14567
15263
  }
14568
15264
  })
14569
- if (contextOptions.length < 2) {
15265
+ if (contextStoreOptions.length < 2) {
14570
15266
  allOptions.flow.options = [];
14571
15267
  allOptions.global.options = [];
14572
15268
  } else {
14573
- allOptions.flow.options = contextOptions;
14574
- allOptions.global.options = contextOptions;
15269
+ allOptions.flow.options = contextStoreOptions;
15270
+ allOptions.global.options = contextStoreOptions;
14575
15271
  }
15272
+ // Translate timestamp options
15273
+ allOptions.date.options.forEach(opt => {
15274
+ opt.label = RED._("typedInput.date.format." + (opt.value || 'timestamp'), {defaultValue: opt.label})
15275
+ })
14576
15276
  }
14577
15277
  nlsd = true;
14578
15278
  var that = this;
@@ -14661,7 +15361,7 @@ RED.stack = (function() {
14661
15361
  that.element.trigger('paste',evt);
14662
15362
  });
14663
15363
  this.input.on('keydown', function(evt) {
14664
- if (that.typeMap[that.propertyType].autoComplete) {
15364
+ if (that.typeMap[that.propertyType].autoComplete || that.input.hasClass('red-ui-autoComplete')) {
14665
15365
  return
14666
15366
  }
14667
15367
  if (evt.keyCode >= 37 && evt.keyCode <= 40) {
@@ -14955,7 +15655,9 @@ RED.stack = (function() {
14955
15655
  if (this.optionMenu) {
14956
15656
  this.optionMenu.remove();
14957
15657
  }
14958
- this.menu.remove();
15658
+ if (this.menu) {
15659
+ this.menu.remove();
15660
+ }
14959
15661
  this.uiSelect.remove();
14960
15662
  },
14961
15663
  types: function(types) {
@@ -14988,7 +15690,7 @@ RED.stack = (function() {
14988
15690
  this.menu = this._createMenu(this.typeList,{},function(v) { that.type(v) });
14989
15691
  if (currentType && !this.typeMap.hasOwnProperty(currentType)) {
14990
15692
  if (!firstCall) {
14991
- this.type(this.typeList[0].value);
15693
+ this.type(this.typeList[0]?.value || ""); // permit empty typeList
14992
15694
  }
14993
15695
  } else {
14994
15696
  this.propertyType = null;
@@ -15025,6 +15727,11 @@ RED.stack = (function() {
15025
15727
  var selectedOption = [];
15026
15728
  var valueToCheck = value;
15027
15729
  if (opt.options) {
15730
+ let _options = opt.options
15731
+ if (typeof opt.options === "function") {
15732
+ _options = opt.options.call(this)
15733
+ }
15734
+
15028
15735
  if (opt.hasValue && opt.parse) {
15029
15736
  var parts = opt.parse(value);
15030
15737
  if (this.options.debug) { console.log(this.identifier,"new parse",parts) }
@@ -15038,8 +15745,8 @@ RED.stack = (function() {
15038
15745
  checkValues = valueToCheck.split(",");
15039
15746
  }
15040
15747
  checkValues.forEach(function(valueToCheck) {
15041
- for (var i=0;i<opt.options.length;i++) {
15042
- var op = opt.options[i];
15748
+ for (var i=0;i<_options.length;i++) {
15749
+ var op = _options[i];
15043
15750
  if (typeof op === "string") {
15044
15751
  if (op === valueToCheck || op === ""+valueToCheck) {
15045
15752
  selectedOption.push(that.activeOptions[op]);
@@ -15074,7 +15781,7 @@ RED.stack = (function() {
15074
15781
  },
15075
15782
  type: function(type) {
15076
15783
  if (!arguments.length) {
15077
- return this.propertyType;
15784
+ return this.propertyType || this.options?.default || '';
15078
15785
  } else {
15079
15786
  var that = this;
15080
15787
  if (this.options.debug) { console.log(this.identifier,"----- SET TYPE -----",type) }
@@ -15084,6 +15791,9 @@ RED.stack = (function() {
15084
15791
  // If previousType is !null, then this is a change of the type, rather than the initialisation
15085
15792
  var previousType = this.typeMap[this.propertyType];
15086
15793
  previousValue = this.input.val();
15794
+ if (this.input.hasClass('red-ui-autoComplete')) {
15795
+ this.input.autoComplete("destroy");
15796
+ }
15087
15797
 
15088
15798
  if (previousType && this.typeChanged) {
15089
15799
  if (this.options.debug) { console.log(this.identifier,"typeChanged",{previousType,previousValue}) }
@@ -15130,7 +15840,9 @@ RED.stack = (function() {
15130
15840
  this.input.val(this.oldValues.hasOwnProperty("_")?this.oldValues["_"]:(opt.default||""))
15131
15841
  }
15132
15842
  if (previousType.autoComplete) {
15133
- this.input.autoComplete("destroy");
15843
+ if (this.input.hasClass('red-ui-autoComplete')) {
15844
+ this.input.autoComplete("destroy");
15845
+ }
15134
15846
  }
15135
15847
  }
15136
15848
  this.propertyType = type;
@@ -15170,6 +15882,10 @@ RED.stack = (function() {
15170
15882
  this.optionMenu = null;
15171
15883
  }
15172
15884
  if (opt.options) {
15885
+ let _options = opt.options
15886
+ if (typeof _options === "function") {
15887
+ _options = opt.options.call(this);
15888
+ }
15173
15889
  if (this.optionExpandButton) {
15174
15890
  this.optionExpandButton.hide();
15175
15891
  this.optionExpandButton.shown = false;
@@ -15186,7 +15902,7 @@ RED.stack = (function() {
15186
15902
  this.valueLabelContainer.hide();
15187
15903
  }
15188
15904
  this.activeOptions = {};
15189
- opt.options.forEach(function(o) {
15905
+ _options.forEach(function(o) {
15190
15906
  if (typeof o === 'string') {
15191
15907
  that.activeOptions[o] = {label:o,value:o};
15192
15908
  } else {
@@ -15206,7 +15922,7 @@ RED.stack = (function() {
15206
15922
  if (validValues) {
15207
15923
  that._updateOptionSelectLabel(validValues)
15208
15924
  } else {
15209
- op = opt.options[0];
15925
+ op = _options[0] || {value:""}; // permit zero options
15210
15926
  if (typeof op === "string") {
15211
15927
  this.value(op);
15212
15928
  that._updateOptionSelectLabel({value:op});
@@ -15225,7 +15941,7 @@ RED.stack = (function() {
15225
15941
  that._updateOptionSelectLabel(validValues);
15226
15942
  }
15227
15943
  } else {
15228
- var selectedOption = this.optionValue||opt.options[0];
15944
+ var selectedOption = this.optionValue||_options[0];
15229
15945
  if (opt.parse) {
15230
15946
  var selectedOptionObj = typeof selectedOption === "string"?{value:selectedOption}:selectedOption
15231
15947
  var parts = opt.parse(this.input.val(),selectedOptionObj);
@@ -15258,8 +15974,18 @@ RED.stack = (function() {
15258
15974
  } else {
15259
15975
  this.optionSelectTrigger.hide();
15260
15976
  }
15977
+ if (opt.autoComplete) {
15978
+ let searchFunction = opt.autoComplete
15979
+ if (searchFunction.length === 0) {
15980
+ searchFunction = opt.autoComplete.call(this)
15981
+ }
15982
+ this.input.autoComplete({
15983
+ search: searchFunction,
15984
+ minLength: 0
15985
+ })
15986
+ }
15261
15987
  }
15262
- this.optionMenu = this._createMenu(opt.options,opt,function(v){
15988
+ this.optionMenu = this._createMenu(_options,opt,function(v){
15263
15989
  if (!opt.multiple) {
15264
15990
  that._updateOptionSelectLabel(that.activeOptions[v]);
15265
15991
  if (!opt.hasValue) {
@@ -15300,8 +16026,12 @@ RED.stack = (function() {
15300
16026
  this.valueLabelContainer.hide();
15301
16027
  this.elementDiv.show();
15302
16028
  if (opt.autoComplete) {
16029
+ let searchFunction = opt.autoComplete
16030
+ if (searchFunction.length === 0) {
16031
+ searchFunction = opt.autoComplete.call(this)
16032
+ }
15303
16033
  this.input.autoComplete({
15304
- search: opt.autoComplete,
16034
+ search: searchFunction,
15305
16035
  minLength: 0
15306
16036
  })
15307
16037
  }
@@ -19560,10 +20290,6 @@ RED.keyboard = (function() {
19560
20290
  }
19561
20291
 
19562
20292
  function init(done) {
19563
- if (!RED.user.hasPermission("settings.write")) {
19564
- RED.notify(RED._("user.errors.settings"),"error");
19565
- return;
19566
- }
19567
20293
  RED.userSettings.add({
19568
20294
  id:'envvar',
19569
20295
  title: RED._("env-var.environment"),
@@ -26818,6 +27544,10 @@ RED.view = (function() {
26818
27544
  }
26819
27545
  })
26820
27546
  }
27547
+ if (selection.links) {
27548
+ selectedLinks.clear();
27549
+ selection.links.forEach(selectedLinks.add);
27550
+ }
26821
27551
  }
26822
27552
  }
26823
27553
  updateSelection();
@@ -29071,6 +29801,10 @@ RED.palette = (function() {
29071
29801
  var categoryContainers = {};
29072
29802
  var sidebarControls;
29073
29803
 
29804
+ let paletteState = { filter: "", collapsed: [] };
29805
+
29806
+ let filterRefreshTimeout
29807
+
29074
29808
  function createCategory(originalCategory,rootCategory,category,ns) {
29075
29809
  if ($("#red-ui-palette-base-category-"+rootCategory).length === 0) {
29076
29810
  createCategoryContainer(originalCategory,rootCategory, ns+":palette.label."+rootCategory);
@@ -29096,20 +29830,57 @@ RED.palette = (function() {
29096
29830
  catDiv.data('label',label);
29097
29831
  categoryContainers[category] = {
29098
29832
  container: catDiv,
29099
- close: function() {
29833
+ hide: function (instant) {
29834
+ if (instant) {
29835
+ catDiv.hide()
29836
+ } else {
29837
+ catDiv.slideUp()
29838
+ }
29839
+ },
29840
+ show: function () {
29841
+ catDiv.show()
29842
+ },
29843
+ isOpen: function () {
29844
+ return !!catDiv.hasClass("red-ui-palette-open")
29845
+ },
29846
+ getNodeCount: function (visibleOnly) {
29847
+ const nodes = catDiv.find(".red-ui-palette-node")
29848
+ if (visibleOnly) {
29849
+ return nodes.filter(function() { return $(this).css('display') !== 'none'}).length
29850
+ } else {
29851
+ return nodes.length
29852
+ }
29853
+ },
29854
+ close: function(instant, skipSaveState) {
29100
29855
  catDiv.removeClass("red-ui-palette-open");
29101
29856
  catDiv.addClass("red-ui-palette-closed");
29102
- $("#red-ui-palette-base-category-"+category).slideUp();
29857
+ if (instant) {
29858
+ $("#red-ui-palette-base-category-"+category).hide();
29859
+ } else {
29860
+ $("#red-ui-palette-base-category-"+category).slideUp();
29861
+ }
29103
29862
  $("#red-ui-palette-header-"+category+" i").removeClass("expanded");
29863
+ if (!skipSaveState) {
29864
+ if (!paletteState.collapsed.includes(category)) {
29865
+ paletteState.collapsed.push(category);
29866
+ savePaletteState();
29867
+ }
29868
+ }
29104
29869
  },
29105
- open: function() {
29870
+ open: function(skipSaveState) {
29106
29871
  catDiv.addClass("red-ui-palette-open");
29107
29872
  catDiv.removeClass("red-ui-palette-closed");
29108
29873
  $("#red-ui-palette-base-category-"+category).slideDown();
29109
29874
  $("#red-ui-palette-header-"+category+" i").addClass("expanded");
29875
+ if (!skipSaveState) {
29876
+ if (paletteState.collapsed.includes(category)) {
29877
+ paletteState.collapsed.splice(paletteState.collapsed.indexOf(category), 1);
29878
+ savePaletteState();
29879
+ }
29880
+ }
29110
29881
  },
29111
29882
  toggle: function() {
29112
- if (catDiv.hasClass("red-ui-palette-open")) {
29883
+ if (categoryContainers[category].isOpen()) {
29113
29884
  categoryContainers[category].close();
29114
29885
  } else {
29115
29886
  categoryContainers[category].open();
@@ -29451,8 +30222,16 @@ RED.palette = (function() {
29451
30222
 
29452
30223
  var categoryNode = $("#red-ui-palette-container-"+rootCategory);
29453
30224
  if (categoryNode.find(".red-ui-palette-node").length === 1) {
29454
- categoryContainers[rootCategory].open();
30225
+ if (!paletteState?.collapsed?.includes(rootCategory)) {
30226
+ categoryContainers[rootCategory].open();
30227
+ } else {
30228
+ categoryContainers[rootCategory].close(true);
30229
+ }
29455
30230
  }
30231
+ clearTimeout(filterRefreshTimeout)
30232
+ filterRefreshTimeout = setTimeout(() => {
30233
+ refreshFilter()
30234
+ }, 200)
29456
30235
 
29457
30236
  }
29458
30237
  }
@@ -29552,7 +30331,8 @@ RED.palette = (function() {
29552
30331
  paletteNode.css("backgroundColor", sf.color);
29553
30332
  }
29554
30333
 
29555
- function filterChange(val) {
30334
+ function refreshFilter() {
30335
+ const val = $("#red-ui-palette-search input").val()
29556
30336
  var re = new RegExp(val.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'),'i');
29557
30337
  $("#red-ui-palette-container .red-ui-palette-node").each(function(i,el) {
29558
30338
  var currentLabel = $(el).attr("data-palette-label");
@@ -29564,16 +30344,26 @@ RED.palette = (function() {
29564
30344
  }
29565
30345
  });
29566
30346
 
29567
- for (var category in categoryContainers) {
30347
+ for (let category in categoryContainers) {
29568
30348
  if (categoryContainers.hasOwnProperty(category)) {
29569
- if (categoryContainers[category].container
29570
- .find(".red-ui-palette-node")
29571
- .filter(function() { return $(this).css('display') !== 'none'}).length === 0) {
29572
- categoryContainers[category].close();
29573
- categoryContainers[category].container.slideUp();
30349
+ const categorySection = categoryContainers[category]
30350
+ if (categorySection.getNodeCount(true) === 0) {
30351
+ categorySection.hide()
29574
30352
  } else {
29575
- categoryContainers[category].open();
29576
- categoryContainers[category].container.show();
30353
+ categorySection.show()
30354
+ if (val) {
30355
+ // There is a filter being applied and it has matched
30356
+ // something in this category - show the contents
30357
+ categorySection.open(true)
30358
+ } else {
30359
+ // No filter. Only show the category if it isn't in lastState
30360
+ if (!paletteState.collapsed.includes(category)) {
30361
+ categorySection.open(true)
30362
+ } else if (categorySection.isOpen()) {
30363
+ // This section should be collapsed but isn't - so make it so
30364
+ categorySection.close(true, true)
30365
+ }
30366
+ }
29577
30367
  }
29578
30368
  }
29579
30369
  }
@@ -29589,6 +30379,9 @@ RED.palette = (function() {
29589
30379
 
29590
30380
  $("#red-ui-palette > .red-ui-palette-spinner").show();
29591
30381
 
30382
+ RED.events.on('logout', function () {
30383
+ RED.settings.removeLocal('palette-state')
30384
+ })
29592
30385
 
29593
30386
  RED.events.on('registry:node-type-added', function(nodeType) {
29594
30387
  var def = RED.nodes.getType(nodeType);
@@ -29632,14 +30425,14 @@ RED.palette = (function() {
29632
30425
 
29633
30426
  RED.events.on("subflows:change",refreshSubflow);
29634
30427
 
29635
-
29636
-
29637
30428
  $("#red-ui-palette-search input").searchBox({
29638
30429
  delay: 100,
29639
30430
  change: function() {
29640
- filterChange($(this).val());
30431
+ refreshFilter();
30432
+ paletteState.filter = $(this).val();
30433
+ savePaletteState();
29641
30434
  }
29642
- })
30435
+ });
29643
30436
 
29644
30437
  sidebarControls = $('<div class="red-ui-sidebar-control-left"><i class="fa fa-chevron-left"></i></div>').appendTo($("#red-ui-palette"));
29645
30438
  RED.popover.tooltip(sidebarControls,RED._("keyboard.togglePalette"),"core:toggle-palette");
@@ -29705,7 +30498,23 @@ RED.palette = (function() {
29705
30498
  togglePalette(state);
29706
30499
  }
29707
30500
  });
30501
+
30502
+ try {
30503
+ paletteState = JSON.parse(RED.settings.getLocal("palette-state") || '{"filter":"", "collapsed": []}');
30504
+ if (paletteState.filter) {
30505
+ // Apply the category filter
30506
+ $("#red-ui-palette-search input").searchBox("value", paletteState.filter);
30507
+ }
30508
+ } catch (error) {
30509
+ console.error("Unexpected error loading palette state from localStorage: ", error);
30510
+ }
30511
+ setTimeout(() => {
30512
+ // Lazily tidy up any categories that haven't been reloaded
30513
+ paletteState.collapsed = paletteState.collapsed.filter(category => !!categoryContainers[category])
30514
+ savePaletteState()
30515
+ }, 10000)
29708
30516
  }
30517
+
29709
30518
  function togglePalette(state) {
29710
30519
  if (!state) {
29711
30520
  $("#red-ui-main-container").addClass("red-ui-palette-closed");
@@ -29725,6 +30534,15 @@ RED.palette = (function() {
29725
30534
  })
29726
30535
  return categories;
29727
30536
  }
30537
+
30538
+ function savePaletteState() {
30539
+ try {
30540
+ RED.settings.setLocal("palette-state", JSON.stringify(paletteState));
30541
+ } catch (error) {
30542
+ console.error("Unexpected error saving palette state to localStorage: ", error);
30543
+ }
30544
+ }
30545
+
29728
30546
  return {
29729
30547
  init: init,
29730
30548
  add:addNodeType,
@@ -31214,8 +32032,10 @@ RED.sidebar.help = (function() {
31214
32032
 
31215
32033
  function refreshSubflow(sf) {
31216
32034
  var item = treeList.treeList('get',"node-type:subflow:"+sf.id);
31217
- item.subflowLabel = sf._def.label().toLowerCase();
31218
- item.treeList.replaceElement(getNodeLabel({_def:sf._def,type:sf._def.label()}));
32035
+ if (item) {
32036
+ item.subflowLabel = sf._def.label().toLowerCase();
32037
+ item.treeList.replaceElement(getNodeLabel({_def:sf._def,type:sf._def.label()}));
32038
+ }
31219
32039
  }
31220
32040
 
31221
32041
  function hideTOC() {
@@ -32582,86 +33402,106 @@ RED.palette.editor = (function() {
32582
33402
  var moduleInfo = nodeEntries[module].info;
32583
33403
  var nodeEntry = nodeEntries[module].elements;
32584
33404
  if (nodeEntry) {
32585
- var activeTypeCount = 0;
32586
- var typeCount = 0;
32587
- var errorCount = 0;
32588
- nodeEntry.errorList.empty();
32589
- nodeEntries[module].totalUseCount = 0;
32590
- nodeEntries[module].setUseCount = {};
32591
-
32592
- for (var setName in moduleInfo.sets) {
32593
- if (moduleInfo.sets.hasOwnProperty(setName)) {
32594
- var inUseCount = 0;
32595
- var set = moduleInfo.sets[setName];
32596
- var setElements = nodeEntry.sets[setName];
32597
- if (set.err) {
32598
- errorCount++;
32599
- var errMessage = set.err;
32600
- if (set.err.message) {
32601
- errMessage = set.err.message;
32602
- } else if (set.err.code) {
32603
- errMessage = set.err.code;
32604
- }
32605
- $("<li>").text(errMessage).appendTo(nodeEntry.errorList);
32606
- }
32607
- if (set.enabled) {
32608
- activeTypeCount += set.types.length;
32609
- }
32610
- typeCount += set.types.length;
32611
- for (var i=0;i<moduleInfo.sets[setName].types.length;i++) {
32612
- var t = moduleInfo.sets[setName].types[i];
32613
- inUseCount += (typesInUse[t]||0);
32614
- var swatch = setElements.swatches[t];
32615
- if (set.enabled) {
32616
- var def = RED.nodes.getType(t);
32617
- if (def && def.color) {
32618
- swatch.css({background:RED.utils.getNodeColor(t,def)});
32619
- swatch.css({border: "1px solid "+getContrastingBorder(swatch.css('backgroundColor'))})
32620
- }
33405
+ if (moduleInfo.plugin) {
33406
+ nodeEntry.enableButton.hide();
33407
+ nodeEntry.removeButton.show();
33408
+
33409
+ let pluginCount = 0;
33410
+ for (let setName in moduleInfo.sets) {
33411
+ if (moduleInfo.sets.hasOwnProperty(setName)) {
33412
+ let set = moduleInfo.sets[setName];
33413
+ if (set.plugins) {
33414
+ pluginCount += set.plugins.length;
32621
33415
  }
32622
33416
  }
32623
- nodeEntries[module].setUseCount[setName] = inUseCount;
32624
- nodeEntries[module].totalUseCount += inUseCount;
33417
+ }
33418
+
33419
+ nodeEntry.setCount.text(RED._('palette.editor.pluginCount',{count:pluginCount,label:pluginCount}));
32625
33420
 
32626
- if (inUseCount > 0) {
32627
- setElements.enableButton.text(RED._('palette.editor.inuse'));
32628
- setElements.enableButton.addClass('disabled');
32629
- } else {
32630
- setElements.enableButton.removeClass('disabled');
33421
+ } else {
33422
+ var activeTypeCount = 0;
33423
+ var typeCount = 0;
33424
+ var errorCount = 0;
33425
+ nodeEntry.errorList.empty();
33426
+ nodeEntries[module].totalUseCount = 0;
33427
+ nodeEntries[module].setUseCount = {};
33428
+
33429
+ for (var setName in moduleInfo.sets) {
33430
+ if (moduleInfo.sets.hasOwnProperty(setName)) {
33431
+ var inUseCount = 0;
33432
+ const set = moduleInfo.sets[setName];
33433
+ const setElements = nodeEntry.sets[setName]
33434
+
33435
+ if (set.err) {
33436
+ errorCount++;
33437
+ var errMessage = set.err;
33438
+ if (set.err.message) {
33439
+ errMessage = set.err.message;
33440
+ } else if (set.err.code) {
33441
+ errMessage = set.err.code;
33442
+ }
33443
+ $("<li>").text(errMessage).appendTo(nodeEntry.errorList);
33444
+ }
32631
33445
  if (set.enabled) {
32632
- setElements.enableButton.text(RED._('palette.editor.disable'));
32633
- } else {
32634
- setElements.enableButton.text(RED._('palette.editor.enable'));
33446
+ activeTypeCount += set.types.length;
33447
+ }
33448
+ typeCount += set.types.length;
33449
+ for (var i=0;i<moduleInfo.sets[setName].types.length;i++) {
33450
+ var t = moduleInfo.sets[setName].types[i];
33451
+ inUseCount += (typesInUse[t]||0);
33452
+ if (setElements && set.enabled) {
33453
+ var def = RED.nodes.getType(t);
33454
+ if (def && def.color) {
33455
+ setElements.swatches[t].css({background:RED.utils.getNodeColor(t,def)});
33456
+ setElements.swatches[t].css({border: "1px solid "+getContrastingBorder(setElements.swatches[t].css('backgroundColor'))})
33457
+ }
33458
+ }
33459
+ }
33460
+ nodeEntries[module].setUseCount[setName] = inUseCount;
33461
+ nodeEntries[module].totalUseCount += inUseCount;
33462
+
33463
+ if (setElements) {
33464
+ if (inUseCount > 0) {
33465
+ setElements.enableButton.text(RED._('palette.editor.inuse'));
33466
+ setElements.enableButton.addClass('disabled');
33467
+ } else {
33468
+ setElements.enableButton.removeClass('disabled');
33469
+ if (set.enabled) {
33470
+ setElements.enableButton.text(RED._('palette.editor.disable'));
33471
+ } else {
33472
+ setElements.enableButton.text(RED._('palette.editor.enable'));
33473
+ }
33474
+ }
33475
+ setElements.setRow.toggleClass("red-ui-palette-module-set-disabled",!set.enabled);
32635
33476
  }
32636
33477
  }
32637
- setElements.setRow.toggleClass("red-ui-palette-module-set-disabled",!set.enabled);
32638
33478
  }
32639
- }
32640
33479
 
32641
- if (errorCount === 0) {
32642
- nodeEntry.errorRow.hide()
32643
- } else {
32644
- nodeEntry.errorRow.show();
32645
- }
33480
+ if (errorCount === 0) {
33481
+ nodeEntry.errorRow.hide()
33482
+ } else {
33483
+ nodeEntry.errorRow.show();
33484
+ }
32646
33485
 
32647
- var nodeCount = (activeTypeCount === typeCount)?typeCount:activeTypeCount+" / "+typeCount;
32648
- nodeEntry.setCount.text(RED._('palette.editor.nodeCount',{count:typeCount,label:nodeCount}));
33486
+ var nodeCount = (activeTypeCount === typeCount)?typeCount:activeTypeCount+" / "+typeCount;
33487
+ nodeEntry.setCount.text(RED._('palette.editor.nodeCount',{count:typeCount,label:nodeCount}));
32649
33488
 
32650
- if (nodeEntries[module].totalUseCount > 0) {
32651
- nodeEntry.enableButton.text(RED._('palette.editor.inuse'));
32652
- nodeEntry.enableButton.addClass('disabled');
32653
- nodeEntry.removeButton.hide();
32654
- } else {
32655
- nodeEntry.enableButton.removeClass('disabled');
32656
- if (moduleInfo.local) {
32657
- nodeEntry.removeButton.css('display', 'inline-block');
32658
- }
32659
- if (activeTypeCount === 0) {
32660
- nodeEntry.enableButton.text(RED._('palette.editor.enableall'));
33489
+ if (nodeEntries[module].totalUseCount > 0) {
33490
+ nodeEntry.enableButton.text(RED._('palette.editor.inuse'));
33491
+ nodeEntry.enableButton.addClass('disabled');
33492
+ nodeEntry.removeButton.hide();
32661
33493
  } else {
32662
- nodeEntry.enableButton.text(RED._('palette.editor.disableall'));
33494
+ nodeEntry.enableButton.removeClass('disabled');
33495
+ if (moduleInfo.local) {
33496
+ nodeEntry.removeButton.css('display', 'inline-block');
33497
+ }
33498
+ if (activeTypeCount === 0) {
33499
+ nodeEntry.enableButton.text(RED._('palette.editor.enableall'));
33500
+ } else {
33501
+ nodeEntry.enableButton.text(RED._('palette.editor.disableall'));
33502
+ }
33503
+ nodeEntry.container.toggleClass("disabled",(activeTypeCount === 0));
32663
33504
  }
32664
- nodeEntry.container.toggleClass("disabled",(activeTypeCount === 0));
32665
33505
  }
32666
33506
  }
32667
33507
  if (moduleInfo.pending_version) {
@@ -33012,6 +33852,33 @@ RED.palette.editor = (function() {
33012
33852
  }
33013
33853
  }
33014
33854
  })
33855
+
33856
+ RED.events.on("registry:plugin-module-added", function(module) {
33857
+
33858
+ if (!nodeEntries.hasOwnProperty(module)) {
33859
+ nodeEntries[module] = {info:RED.plugins.getModule(module)};
33860
+ var index = [module];
33861
+ for (var s in nodeEntries[module].info.sets) {
33862
+ if (nodeEntries[module].info.sets.hasOwnProperty(s)) {
33863
+ index.push(s);
33864
+ index = index.concat(nodeEntries[module].info.sets[s].types)
33865
+ }
33866
+ }
33867
+ nodeEntries[module].index = index.join(",").toLowerCase();
33868
+ nodeList.editableList('addItem', nodeEntries[module]);
33869
+ } else {
33870
+ _refreshNodeModule(module);
33871
+ }
33872
+
33873
+ for (var i=0;i<filteredList.length;i++) {
33874
+ if (filteredList[i].info.id === module) {
33875
+ var installButton = filteredList[i].elements.installButton;
33876
+ installButton.addClass('disabled');
33877
+ installButton.text(RED._('palette.editor.installed'));
33878
+ break;
33879
+ }
33880
+ }
33881
+ });
33015
33882
  }
33016
33883
 
33017
33884
  var settingsPane;
@@ -33138,6 +34005,7 @@ RED.palette.editor = (function() {
33138
34005
  errorRow: errorRow,
33139
34006
  errorList: errorList,
33140
34007
  setCount: setCount,
34008
+ setButton: setButton,
33141
34009
  container: container,
33142
34010
  shade: shade,
33143
34011
  versionSpan: versionSpan,
@@ -33148,49 +34016,88 @@ RED.palette.editor = (function() {
33148
34016
  if (container.hasClass('expanded')) {
33149
34017
  container.removeClass('expanded');
33150
34018
  contentRow.slideUp();
34019
+ setTimeout(() => {
34020
+ contentRow.empty()
34021
+ }, 200)
34022
+ object.elements.sets = {}
33151
34023
  } else {
33152
34024
  container.addClass('expanded');
34025
+ populateSetList()
33153
34026
  contentRow.slideDown();
33154
34027
  }
33155
34028
  })
33156
-
33157
- var setList = Object.keys(entry.sets)
33158
- setList.sort(function(A,B) {
33159
- return A.toLowerCase().localeCompare(B.toLowerCase());
33160
- });
33161
- setList.forEach(function(setName) {
33162
- var set = entry.sets[setName];
33163
- var setRow = $('<div>',{class:"red-ui-palette-module-set"}).appendTo(contentRow);
33164
- var buttonGroup = $('<div>',{class:"red-ui-palette-module-set-button-group"}).appendTo(setRow);
33165
- var typeSwatches = {};
33166
- set.types.forEach(function(t) {
33167
- var typeDiv = $('<div>',{class:"red-ui-palette-module-type"}).appendTo(setRow);
33168
- typeSwatches[t] = $('<span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv);
33169
- $('<span>',{class:"red-ui-palette-module-type-node"}).text(t).appendTo(typeDiv);
33170
- })
33171
- var enableButton = $('<a href="#" class="red-ui-button red-ui-button-small"></a>').appendTo(buttonGroup);
33172
- enableButton.on("click", function(evt) {
33173
- evt.preventDefault();
33174
- if (object.setUseCount[setName] === 0) {
33175
- var currentSet = RED.nodes.registry.getNodeSet(set.id);
33176
- shade.show();
33177
- var newState = !currentSet.enabled
33178
- changeNodeState(set.id,newState,shade,function(xhr){
33179
- if (xhr) {
33180
- if (xhr.responseJSON) {
33181
- RED.notify(RED._('palette.editor.errors.'+(newState?'enable':'disable')+'Failed',{module: id,message:xhr.responseJSON.message}));
34029
+ const populateSetList = function () {
34030
+ var setList = Object.keys(entry.sets)
34031
+ setList.sort(function(A,B) {
34032
+ return A.toLowerCase().localeCompare(B.toLowerCase());
34033
+ });
34034
+ setList.forEach(function(setName) {
34035
+ var set = entry.sets[setName];
34036
+ var setRow = $('<div>',{class:"red-ui-palette-module-set"}).appendTo(contentRow);
34037
+ var buttonGroup = $('<div>',{class:"red-ui-palette-module-set-button-group"}).appendTo(setRow);
34038
+ var typeSwatches = {};
34039
+ let enableButton;
34040
+ if (set.types) {
34041
+ set.types.forEach(function(t) {
34042
+ var typeDiv = $('<div>',{class:"red-ui-palette-module-type"}).appendTo(setRow);
34043
+ typeSwatches[t] = $('<span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv);
34044
+ if (set.enabled) {
34045
+ var def = RED.nodes.getType(t);
34046
+ if (def && def.color) {
34047
+ typeSwatches[t].css({background:RED.utils.getNodeColor(t,def)});
34048
+ typeSwatches[t].css({border: "1px solid "+getContrastingBorder(typeSwatches[t].css('backgroundColor'))})
33182
34049
  }
33183
34050
  }
33184
- });
34051
+ $('<span>',{class:"red-ui-palette-module-type-node"}).text(t).appendTo(typeDiv);
34052
+ })
34053
+ enableButton = $('<a href="#" class="red-ui-button red-ui-button-small"></a>').appendTo(buttonGroup);
34054
+ enableButton.on("click", function(evt) {
34055
+ evt.preventDefault();
34056
+ if (object.setUseCount[setName] === 0) {
34057
+ var currentSet = RED.nodes.registry.getNodeSet(set.id);
34058
+ shade.show();
34059
+ var newState = !currentSet.enabled
34060
+ changeNodeState(set.id,newState,shade,function(xhr){
34061
+ if (xhr) {
34062
+ if (xhr.responseJSON) {
34063
+ RED.notify(RED._('palette.editor.errors.'+(newState?'enable':'disable')+'Failed',{module: id,message:xhr.responseJSON.message}));
34064
+ }
34065
+ }
34066
+ });
34067
+ }
34068
+ })
34069
+
34070
+ if (object.setUseCount[setName] > 0) {
34071
+ enableButton.text(RED._('palette.editor.inuse'));
34072
+ enableButton.addClass('disabled');
34073
+ } else {
34074
+ enableButton.removeClass('disabled');
34075
+ if (set.enabled) {
34076
+ enableButton.text(RED._('palette.editor.disable'));
34077
+ } else {
34078
+ enableButton.text(RED._('palette.editor.enable'));
34079
+ }
34080
+ }
34081
+ setRow.toggleClass("red-ui-palette-module-set-disabled",!set.enabled);
34082
+
34083
+
34084
+ }
34085
+ if (set.plugins) {
34086
+ set.plugins.forEach(function(p) {
34087
+ var typeDiv = $('<div>',{class:"red-ui-palette-module-type"}).appendTo(setRow);
34088
+ // typeSwatches[p.id] = $('<span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv);
34089
+ $('<span><i class="fa fa-puzzle-piece" aria-hidden="true"></i> </span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv);
34090
+ $('<span>',{class:"red-ui-palette-module-type-node"}).text(p.id).appendTo(typeDiv);
34091
+ })
33185
34092
  }
33186
- })
33187
34093
 
33188
- object.elements.sets[set.name] = {
33189
- setRow: setRow,
33190
- enableButton: enableButton,
33191
- swatches: typeSwatches
33192
- };
33193
- });
34094
+ object.elements.sets[set.name] = {
34095
+ setRow: setRow,
34096
+ enableButton: enableButton,
34097
+ swatches: typeSwatches
34098
+ };
34099
+ });
34100
+ }
33194
34101
  enableButton.on("click", function(evt) {
33195
34102
  evt.preventDefault();
33196
34103
  if (object.totalUseCount === 0) {
@@ -33560,7 +34467,55 @@ RED.palette.editor = (function() {
33560
34467
  }
33561
34468
  }
33562
34469
  ]
33563
- }); }
34470
+ });
34471
+ }
34472
+ } else {
34473
+ // dedicated list management for plugins
34474
+ if (entry.plugin) {
34475
+
34476
+ let e = nodeEntries[entry.name];
34477
+ if (e) {
34478
+ nodeList.editableList('removeItem', e);
34479
+ delete nodeEntries[entry.name];
34480
+ }
34481
+
34482
+ // We assume that a plugin that implements onremove
34483
+ // cleans the editor accordingly of its left-overs.
34484
+ let found_onremove = true;
34485
+
34486
+ let keys = Object.keys(entry.sets);
34487
+ keys.forEach((key) => {
34488
+ let set = entry.sets[key];
34489
+ for (let i=0; i<set.plugins?.length; i++) {
34490
+ let plgn = RED.plugins.getPlugin(set.plugins[i].id);
34491
+ if (plgn && plgn.onremove && typeof plgn.onremove === 'function') {
34492
+ plgn.onremove();
34493
+ } else {
34494
+ if (plgn && plgn.onadd && typeof plgn.onadd === 'function') {
34495
+ // if there's no 'onadd', there shouldn't be any left-overs
34496
+ found_onremove = false;
34497
+ }
34498
+ }
34499
+ }
34500
+ });
34501
+
34502
+ if (!found_onremove) {
34503
+ let removeNotify = RED.notify("Removed plugin " + entry.name + ". Please reload the editor to clear left-overs.",{
34504
+ modal: true,
34505
+ fixed: true,
34506
+ type: 'warning',
34507
+ buttons: [
34508
+ {
34509
+ text: "Understood",
34510
+ class:"primary",
34511
+ click: function(e) {
34512
+ removeNotify.close();
34513
+ }
34514
+ }
34515
+ ]
34516
+ });
34517
+ }
34518
+ }
33564
34519
  }
33565
34520
  })
33566
34521
  notification.close();
@@ -33975,48 +34930,108 @@ RED.editor = (function() {
33975
34930
 
33976
34931
  /**
33977
34932
  * Create a config-node select box for this property
33978
- * @param node - the node being edited
33979
- * @param property - the name of the field
33980
- * @param type - the type of the config-node
34933
+ * @param {Object} node - the node being edited
34934
+ * @param {String} property - the name of the node property
34935
+ * @param {String} type - the type of the config-node
34936
+ * @param {"node-config-input"|"node-input"|"node-input-subflow-env"} prefix - the prefix to use in the input element ids
34937
+ * @param {Function} [filter] - a function to filter the list of config nodes
34938
+ * @param {Object} [env] - the environment variable object (only used for subflow env vars)
33981
34939
  */
33982
- function prepareConfigNodeSelect(node,property,type,prefix,filter) {
33983
- var input = $("#"+prefix+"-"+property);
33984
- if (input.length === 0 ) {
34940
+ function prepareConfigNodeSelect(node, property, type, prefix, filter, env) {
34941
+ let nodeValue
34942
+ if (prefix === 'node-input-subflow-env') {
34943
+ nodeValue = env?.value
34944
+ } else {
34945
+ nodeValue = node[property]
34946
+ }
34947
+
34948
+ const addBtnId = `${prefix}-btn-${property}-add`;
34949
+ const editBtnId = `${prefix}-btn-${property}-edit`;
34950
+ const selectId = prefix + '-' + property;
34951
+ const input = $(`#${selectId}`);
34952
+ if (input.length === 0) {
33985
34953
  return;
33986
34954
  }
33987
- var newWidth = input.width();
33988
- var attrStyle = input.attr('style');
33989
- var m;
34955
+ const attrStyle = input.attr('style');
34956
+ let newWidth;
34957
+ let m;
33990
34958
  if ((m = /(^|\s|;)width\s*:\s*([^;]+)/i.exec(attrStyle)) !== null) {
33991
34959
  newWidth = m[2].trim();
33992
34960
  } else {
33993
34961
  newWidth = "70%";
33994
34962
  }
33995
- var outerWrap = $("<div></div>").css({
34963
+ const outerWrap = $("<div></div>").css({
33996
34964
  width: newWidth,
33997
- display:'inline-flex'
34965
+ display: 'inline-flex'
33998
34966
  });
33999
- var select = $('<select id="'+prefix+'-'+property+'"></select>').appendTo(outerWrap);
34967
+ const select = $('<select id="' + selectId + '"></select>').appendTo(outerWrap);
34000
34968
  input.replaceWith(outerWrap);
34001
34969
  // set the style attr directly - using width() on FF causes a value of 114%...
34002
34970
  select.css({
34003
34971
  'flex-grow': 1
34004
34972
  });
34005
- updateConfigNodeSelect(property,type,node[property],prefix,filter);
34006
- $('<a id="'+prefix+'-lookup-'+property+'" class="red-ui-button"><i class="fa fa-pencil"></i></a>')
34007
- .css({"margin-left":"10px"})
34973
+
34974
+ updateConfigNodeSelect(property, type, nodeValue, prefix, filter);
34975
+
34976
+ // create the edit button
34977
+ const editButton = $('<a id="' + editBtnId + '" class="red-ui-button"><i class="fa fa-pencil"></i></a>')
34978
+ .css({ "margin-left": "10px" })
34008
34979
  .appendTo(outerWrap);
34009
- $('#'+prefix+'-lookup-'+property).on("click", function(e) {
34010
- showEditConfigNodeDialog(property,type,select.find(":selected").val(),prefix,node);
34980
+
34981
+ RED.popover.tooltip(editButton, RED._('editor.editConfig', { type }));
34982
+
34983
+ // create the add button
34984
+ const addButton = $('<a id="' + addBtnId + '" class="red-ui-button"><i class="fa fa-plus"></i></a>')
34985
+ .css({ "margin-left": "10px" })
34986
+ .appendTo(outerWrap);
34987
+ RED.popover.tooltip(addButton, RED._('editor.addNewConfig', { type }));
34988
+
34989
+ const disableButton = function(button, disabled) {
34990
+ $(button).prop("disabled", !!disabled)
34991
+ $(button).toggleClass("disabled", !!disabled)
34992
+ };
34993
+
34994
+ // add the click handler
34995
+ addButton.on("click", function (e) {
34996
+ if (addButton.prop("disabled")) { return }
34997
+ showEditConfigNodeDialog(property, type, "_ADD_", prefix, node);
34998
+ e.preventDefault();
34999
+ });
35000
+ editButton.on("click", function (e) {
35001
+ const selectedOpt = select.find(":selected")
35002
+ 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)
35003
+ if (editButton.prop("disabled")) { return }
35004
+ showEditConfigNodeDialog(property, type, selectedOpt.val(), prefix, node);
34011
35005
  e.preventDefault();
34012
35006
  });
35007
+
35008
+ // dont permit the user to click the button if the selected option is an env var
35009
+ select.on("change", function () {
35010
+ const selectedOpt = select.find(":selected");
35011
+ const optionsLength = select.find("option").length;
35012
+ if (selectedOpt?.data('env')) {
35013
+ disableButton(addButton, true);
35014
+ disableButton(editButton, true);
35015
+ // disable the edit button if no options available
35016
+ } else if (optionsLength === 1 && selectedOpt.val() === "_ADD_") {
35017
+ disableButton(addButton, false);
35018
+ disableButton(editButton, true);
35019
+ } else if (selectedOpt.val() === "") {
35020
+ disableButton(addButton, false);
35021
+ disableButton(editButton, true);
35022
+ } else {
35023
+ disableButton(addButton, false);
35024
+ disableButton(editButton, false);
35025
+ }
35026
+ });
35027
+
34013
35028
  var label = "";
34014
- var configNode = RED.nodes.node(node[property]);
34015
- var node_def = RED.nodes.getType(type);
35029
+ var configNode = RED.nodes.node(nodeValue);
34016
35030
 
34017
35031
  if (configNode) {
34018
- label = RED.utils.getNodeLabel(configNode,configNode.id);
35032
+ label = RED.utils.getNodeLabel(configNode, configNode.id);
34019
35033
  }
35034
+
34020
35035
  input.val(label);
34021
35036
  }
34022
35037
 
@@ -34417,12 +35432,9 @@ RED.editor = (function() {
34417
35432
  }
34418
35433
 
34419
35434
  function defaultConfigNodeSort(A,B) {
34420
- if (A.__label__ < B.__label__) {
34421
- return -1;
34422
- } else if (A.__label__ > B.__label__) {
34423
- return 1;
34424
- }
34425
- return 0;
35435
+ // sort case insensitive so that `[env] node-name` items are at the top and
35436
+ // not mixed inbetween the the lower and upper case items
35437
+ return (A.__label__ || '').localeCompare((B.__label__ || ''), undefined, {sensitivity: 'base'})
34426
35438
  }
34427
35439
 
34428
35440
  function updateConfigNodeSelect(name,type,value,prefix,filter) {
@@ -34437,7 +35449,7 @@ RED.editor = (function() {
34437
35449
  }
34438
35450
  $("#"+prefix+"-"+name).val(value);
34439
35451
  } else {
34440
-
35452
+ let inclSubflowEnvvars = false
34441
35453
  var select = $("#"+prefix+"-"+name);
34442
35454
  var node_def = RED.nodes.getType(type);
34443
35455
  select.children().remove();
@@ -34445,6 +35457,7 @@ RED.editor = (function() {
34445
35457
  var activeWorkspace = RED.nodes.workspace(RED.workspaces.active());
34446
35458
  if (!activeWorkspace) {
34447
35459
  activeWorkspace = RED.nodes.subflow(RED.workspaces.active());
35460
+ inclSubflowEnvvars = true
34448
35461
  }
34449
35462
 
34450
35463
  var configNodes = [];
@@ -34460,6 +35473,31 @@ RED.editor = (function() {
34460
35473
  }
34461
35474
  }
34462
35475
  });
35476
+
35477
+ // as includeSubflowEnvvars is true, this is a subflow.
35478
+ // include any 'conf-types' env vars as a list of avaiable configs
35479
+ // in the config dropdown as `[env] node-name`
35480
+ if (inclSubflowEnvvars && activeWorkspace.env) {
35481
+ const parentEnv = activeWorkspace.env.filter(env => env.ui?.type === 'conf-types' && env.type === type)
35482
+ if (parentEnv && parentEnv.length > 0) {
35483
+ const locale = RED.i18n.lang()
35484
+ for (let i = 0; i < parentEnv.length; i++) {
35485
+ const tenv = parentEnv[i]
35486
+ const ui = tenv.ui || {}
35487
+ const labels = ui.label || {}
35488
+ const labelText = RED.editor.envVarList.lookupLabel(labels, labels["en-US"] || tenv.name, locale)
35489
+ const config = {
35490
+ env: tenv,
35491
+ id: '${' + parentEnv[0].name + '}',
35492
+ type: type,
35493
+ label: labelText,
35494
+ __label__: `[env] ${labelText}`
35495
+ }
35496
+ configNodes.push(config)
35497
+ }
35498
+ }
35499
+ }
35500
+
34463
35501
  var configSortFn = defaultConfigNodeSort;
34464
35502
  if (typeof node_def.sort == "function") {
34465
35503
  configSortFn = node_def.sort;
@@ -34471,7 +35509,10 @@ RED.editor = (function() {
34471
35509
  }
34472
35510
 
34473
35511
  configNodes.forEach(function(cn) {
34474
- $('<option value="'+cn.id+'"'+(value==cn.id?" selected":"")+'></option>').text(RED.text.bidi.enforceTextDirectionWithUCC(cn.__label__)).appendTo(select);
35512
+ const option = $('<option value="'+cn.id+'"'+(value==cn.id?" selected":"")+'></option>').text(RED.text.bidi.enforceTextDirectionWithUCC(cn.__label__)).appendTo(select);
35513
+ if (cn.env) {
35514
+ option.data('env', cn.env) // set a data attribute to indicate this is an env var (to inhibit the edit button)
35515
+ }
34475
35516
  delete cn.__label__;
34476
35517
  });
34477
35518
 
@@ -34484,7 +35525,12 @@ RED.editor = (function() {
34484
35525
  }
34485
35526
  }
34486
35527
 
34487
- select.append('<option value="_ADD_"'+(value===""?" selected":"")+'>'+RED._("editor.addNewType", {type:label})+'</option>');
35528
+ if (!configNodes.length) {
35529
+ select.append('<option value="_ADD_" selected>' + RED._("editor.addNewType", { type: label }) + '</option>');
35530
+ } else {
35531
+ select.append('<option value="">' + RED._("editor.inputs.none") + '</option>');
35532
+ }
35533
+
34488
35534
  window.setTimeout(function() { select.trigger("change");},50);
34489
35535
  }
34490
35536
  }
@@ -35132,9 +36178,16 @@ RED.editor = (function() {
35132
36178
  }
35133
36179
  RED.tray.close(function() {
35134
36180
  var filter = null;
35135
- if (editContext && typeof editContext._def.defaults[configProperty].filter === 'function') {
35136
- filter = function(n) {
35137
- return editContext._def.defaults[configProperty].filter.call(editContext,n);
36181
+ // when editing a config via subflow edit panel, the `configProperty` will not
36182
+ // necessarily be a property of the editContext._def.defaults object
36183
+ // Also, when editing via dashboard sidebar, editContext can be null
36184
+ // so we need to guard both scenarios
36185
+ if (editContext?._def) {
36186
+ const isSubflow = (editContext._def.type === 'subflow' || /subflow:.*/.test(editContext._def.type))
36187
+ if (editContext && !isSubflow && typeof editContext._def.defaults?.[configProperty]?.filter === 'function') {
36188
+ filter = function(n) {
36189
+ return editContext._def.defaults[configProperty].filter.call(editContext,n);
36190
+ }
35138
36191
  }
35139
36192
  }
35140
36193
  updateConfigNodeSelect(configProperty,configType,editing_config_node.id,prefix,filter);
@@ -35195,7 +36248,7 @@ RED.editor = (function() {
35195
36248
  RED.history.push(historyEvent);
35196
36249
  RED.tray.close(function() {
35197
36250
  var filter = null;
35198
- if (editContext && typeof editContext._def.defaults[configProperty].filter === 'function') {
36251
+ if (editContext && typeof editContext._def.defaults[configProperty]?.filter === 'function') {
35199
36252
  filter = function(n) {
35200
36253
  return editContext._def.defaults[configProperty].filter.call(editContext,n);
35201
36254
  }
@@ -35736,6 +36789,7 @@ RED.editor = (function() {
35736
36789
  }
35737
36790
  },
35738
36791
  editBuffer: function(options) { showTypeEditor("_buffer", options) },
36792
+ getEditStack: function () { return [...editStack] },
35739
36793
  buildEditForm: buildEditForm,
35740
36794
  validateNode: validateNode,
35741
36795
  updateNodeProperties: updateNodeProperties,
@@ -35780,7 +36834,8 @@ RED.editor = (function() {
35780
36834
  filteredEditPanes[type] = filter
35781
36835
  }
35782
36836
  editPanes[type] = definition;
35783
- }
36837
+ },
36838
+ prepareConfigNodeSelect: prepareConfigNodeSelect,
35784
36839
  }
35785
36840
  })();
35786
36841
  ;;(function() {
@@ -37434,8 +38489,9 @@ RED.editor = (function() {
37434
38489
  ;RED.editor.envVarList = (function() {
37435
38490
 
37436
38491
  var currentLocale = 'en-US';
37437
- var DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env'];
37438
- var DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred','jsonata'];
38492
+ const DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env'];
38493
+ const DEFAULT_ENV_TYPE_LIST_INC_CONFTYPES = ['str','num','bool','json','bin','env','conf-types'];
38494
+ const DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred','jsonata'];
37439
38495
 
37440
38496
  /**
37441
38497
  * Create env var edit interface
@@ -37443,8 +38499,8 @@ RED.editor = (function() {
37443
38499
  * @param node - subflow node
37444
38500
  */
37445
38501
  function buildPropertiesList(envContainer, node) {
37446
-
37447
- var isTemplateNode = (node.type === "subflow");
38502
+ if(RED.editor.envVarList.debug) { console.log('envVarList: buildPropertiesList', envContainer, node) }
38503
+ const isTemplateNode = (node.type === "subflow");
37448
38504
 
37449
38505
  envContainer
37450
38506
  .css({
@@ -37516,7 +38572,14 @@ RED.editor = (function() {
37516
38572
  // if `opt.ui` does not exist, then apply defaults. If these
37517
38573
  // defaults do not change then they will get stripped off
37518
38574
  // before saving.
37519
- if (opt.type === 'cred') {
38575
+ if (opt.type === 'conf-types') {
38576
+ opt.ui = opt.ui || {
38577
+ icon: "fa fa-cog",
38578
+ type: "conf-types",
38579
+ opts: {opts:[]}
38580
+ }
38581
+ opt.ui.type = "conf-types";
38582
+ } else if (opt.type === 'cred') {
37520
38583
  opt.ui = opt.ui || {
37521
38584
  icon: "",
37522
38585
  type: "cred"
@@ -37552,7 +38615,7 @@ RED.editor = (function() {
37552
38615
  }
37553
38616
  });
37554
38617
 
37555
- buildEnvEditRow(uiRow, opt.ui, nameField, valueField);
38618
+ buildEnvEditRow(uiRow, opt, nameField, valueField);
37556
38619
  nameField.trigger('change');
37557
38620
  }
37558
38621
  },
@@ -37614,21 +38677,23 @@ RED.editor = (function() {
37614
38677
  * @param nameField - name field of env var
37615
38678
  * @param valueField - value field of env var
37616
38679
  */
37617
- function buildEnvEditRow(container, ui, nameField, valueField) {
38680
+ function buildEnvEditRow(container, opt, nameField, valueField) {
38681
+ const ui = opt.ui
38682
+ if(RED.editor.envVarList.debug) { console.log('envVarList: buildEnvEditRow', container, ui, nameField, valueField) }
37618
38683
  container.addClass("red-ui-editor-subflow-env-ui-row")
37619
38684
  var topRow = $('<div></div>').appendTo(container);
37620
38685
  $('<div></div>').appendTo(topRow);
37621
38686
  $('<div>').text(RED._("editor.icon")).appendTo(topRow);
37622
38687
  $('<div>').text(RED._("editor.label")).appendTo(topRow);
37623
- $('<div>').text(RED._("editor.inputType")).appendTo(topRow);
38688
+ $('<div class="red-env-ui-input-type-col">').text(RED._("editor.inputType")).appendTo(topRow);
37624
38689
 
37625
38690
  var row = $('<div></div>').appendTo(container);
37626
38691
  $('<div><i class="red-ui-editableList-item-handle fa fa-bars"></i></div>').appendTo(row);
37627
38692
  var typeOptions = {
37628
- 'input': {types:DEFAULT_ENV_TYPE_LIST},
37629
- 'select': {opts:[]},
37630
- 'spinner': {},
37631
- 'cred': {}
38693
+ 'input': {types:DEFAULT_ENV_TYPE_LIST_INC_CONFTYPES},
38694
+ 'select': {opts:[]},
38695
+ 'spinner': {},
38696
+ 'cred': {}
37632
38697
  };
37633
38698
  if (ui.opts) {
37634
38699
  typeOptions[ui.type] = ui.opts;
@@ -37693,15 +38758,16 @@ RED.editor = (function() {
37693
38758
  labelInput.attr("placeholder",$(this).val())
37694
38759
  });
37695
38760
 
37696
- var inputCell = $('<div></div>').appendTo(row);
37697
- var inputCellInput = $('<input type="text">').css("width","100%").appendTo(inputCell);
38761
+ var inputCell = $('<div class="red-env-ui-input-type-col"></div>').appendTo(row);
38762
+ var uiInputTypeInput = $('<input type="text">').css("width","100%").appendTo(inputCell);
37698
38763
  if (ui.type === "input") {
37699
- inputCellInput.val(ui.opts.types.join(","));
38764
+ uiInputTypeInput.val(ui.opts.types.join(","));
37700
38765
  }
37701
38766
  var checkbox;
37702
38767
  var selectBox;
37703
38768
 
37704
- inputCellInput.typedInput({
38769
+ // the options presented in the UI section for an "input" type selection
38770
+ uiInputTypeInput.typedInput({
37705
38771
  types: [
37706
38772
  {
37707
38773
  value:"input",
@@ -37862,7 +38928,7 @@ RED.editor = (function() {
37862
38928
  }
37863
38929
  });
37864
38930
  ui.opts.opts = vals;
37865
- inputCellInput.typedInput('value',Date.now())
38931
+ uiInputTypeInput.typedInput('value',Date.now())
37866
38932
  }
37867
38933
  }
37868
38934
  }
@@ -37929,12 +38995,13 @@ RED.editor = (function() {
37929
38995
  } else {
37930
38996
  delete ui.opts.max;
37931
38997
  }
37932
- inputCellInput.typedInput('value',Date.now())
38998
+ uiInputTypeInput.typedInput('value',Date.now())
37933
38999
  }
37934
39000
  }
37935
39001
  }
37936
39002
  }
37937
39003
  },
39004
+ 'conf-types',
37938
39005
  {
37939
39006
  value:"none",
37940
39007
  label:RED._("editor.inputs.none"), icon:"fa fa-times",hasValue:false
@@ -37952,14 +39019,20 @@ RED.editor = (function() {
37952
39019
  // In the case of 'input' type, the typedInput uses the multiple-option
37953
39020
  // mode. Its value needs to be set to a comma-separately list of the
37954
39021
  // selected options.
37955
- inputCellInput.typedInput('value',ui.opts.types.join(","))
39022
+ uiInputTypeInput.typedInput('value',ui.opts.types.join(","))
39023
+ } else if (ui.type === 'conf-types') {
39024
+ // In the case of 'conf-types' type, the typedInput will be populated
39025
+ // with a list of all config nodes types installed.
39026
+ // Restore the value to the last selected type
39027
+ uiInputTypeInput.typedInput('value', opt.type)
37956
39028
  } else {
37957
39029
  // No other type cares about `value`, but doing this will
37958
39030
  // force a refresh of the label now that `ui.opts` has
37959
39031
  // been updated.
37960
- inputCellInput.typedInput('value',Date.now())
39032
+ uiInputTypeInput.typedInput('value',Date.now())
37961
39033
  }
37962
39034
 
39035
+ if(RED.editor.envVarList.debug) { console.log('envVarList: inputCellInput on:typedinputtypechange. ui.type = ' + ui.type) }
37963
39036
  switch (ui.type) {
37964
39037
  case 'input':
37965
39038
  valueField.typedInput('types',ui.opts.types);
@@ -37977,7 +39050,7 @@ RED.editor = (function() {
37977
39050
  valueField.typedInput('types',['cred']);
37978
39051
  break;
37979
39052
  default:
37980
- valueField.typedInput('types',DEFAULT_ENV_TYPE_LIST)
39053
+ valueField.typedInput('types', DEFAULT_ENV_TYPE_LIST);
37981
39054
  }
37982
39055
  if (ui.type === 'checkbox') {
37983
39056
  valueField.typedInput('type','bool');
@@ -37989,8 +39062,46 @@ RED.editor = (function() {
37989
39062
  }
37990
39063
 
37991
39064
  }).on("change", function(evt,type) {
37992
- if (ui.type === 'input') {
37993
- var types = inputCellInput.typedInput('value');
39065
+ const selectedType = $(this).typedInput('type') // the UI typedInput type
39066
+ if(RED.editor.envVarList.debug) { console.log('envVarList: inputCellInput on:change. selectedType = ' + selectedType) }
39067
+ if (selectedType === 'conf-types') {
39068
+ const selectedConfigType = $(this).typedInput('value') || opt.type
39069
+ let activeWorkspace = RED.nodes.workspace(RED.workspaces.active());
39070
+ if (!activeWorkspace) {
39071
+ activeWorkspace = RED.nodes.subflow(RED.workspaces.active());
39072
+ }
39073
+
39074
+ // get a list of all config nodes matching the selectedValue
39075
+ const configNodes = [];
39076
+ RED.nodes.eachConfig(function(config) {
39077
+ if (config.type == selectedConfigType && (!config.z || config.z === activeWorkspace.id)) {
39078
+ const modulePath = config._def?.set?.id || ''
39079
+ let label = RED.utils.getNodeLabel(config, config.id) || config.id;
39080
+ label += config.d ? ' ['+RED._('workspace.disabled')+']' : '';
39081
+ const _config = {
39082
+ _type: selectedConfigType,
39083
+ value: config.id,
39084
+ label: label,
39085
+ title: modulePath ? modulePath + ' - ' + label : label,
39086
+ enabled: config.d !== true,
39087
+ disabled: config.d === true,
39088
+ }
39089
+ configNodes.push(_config);
39090
+ }
39091
+ });
39092
+ const tiTypes = {
39093
+ value: selectedConfigType,
39094
+ label: "config",
39095
+ icon: "fa fa-cog",
39096
+ options: configNodes,
39097
+ }
39098
+ valueField.typedInput('types', [tiTypes]);
39099
+ valueField.typedInput('type', selectedConfigType);
39100
+ valueField.typedInput('value', opt.value);
39101
+
39102
+
39103
+ } else if (ui.type === 'input') {
39104
+ var types = uiInputTypeInput.typedInput('value');
37994
39105
  ui.opts.types = (types === "") ? ["str"] : types.split(",");
37995
39106
  valueField.typedInput('types',ui.opts.types);
37996
39107
  }
@@ -38002,7 +39113,7 @@ RED.editor = (function() {
38002
39113
  })
38003
39114
  // Set the input to the right type. This will trigger the 'typedinputtypechange'
38004
39115
  // event handler (just above ^^) to update the value if needed
38005
- inputCellInput.typedInput('type',ui.type)
39116
+ uiInputTypeInput.typedInput('type',ui.type)
38006
39117
  }
38007
39118
 
38008
39119
  function setLocale(l, list) {
@@ -40529,7 +41640,7 @@ RED.editor.codeEditor.monaco = (function() {
40529
41640
  createMonacoCompletionItem("set (flow context)", 'flow.set("${1:name}", ${1:value});','Set a value in flow context',range),
40530
41641
  createMonacoCompletionItem("get (global context)", 'global.get("${1:name}");','Get a value from global context',range),
40531
41642
  createMonacoCompletionItem("set (global context)", 'global.set("${1:name}", ${1:value});','Set a value in global context',range),
40532
- createMonacoCompletionItem("get (env)", 'env.get("${1|NR_NODE_ID,NR_NODE_NAME,NR_NODE_PATH,NR_GROUP_ID,NR_GROUP_NAME,NR_FLOW_ID,NR_FLOW_NAME|}");','Get env variable value',range),
41643
+ createMonacoCompletionItem("get (env)", 'env.get("${1|NR_NODE_ID,NR_NODE_NAME,NR_NODE_PATH,NR_GROUP_ID,NR_GROUP_NAME,NR_FLOW_ID,NR_FLOW_NAME,NR_SUBFLOW_NAME,NR_SUBFLOW_ID,NR_SUBFLOW_PATH|}");','Get env variable value',range),
40533
41644
  createMonacoCompletionItem("cloneMessage (RED.util)", 'RED.util.cloneMessage(${1:msg});',
40534
41645
  ["```typescript",
40535
41646
  "RED.util.cloneMessage<T extends registry.NodeMessage>(msg: T): T",
@@ -41784,6 +42895,7 @@ RED.eventLog = (function() {
41784
42895
  setTimeout(function() {
41785
42896
  oldTray.tray.detach();
41786
42897
  showTray(options);
42898
+ RED.events.emit('editor:change')
41787
42899
  },250)
41788
42900
  } else {
41789
42901
  if (stack.length > 0) {
@@ -41853,6 +42965,7 @@ RED.eventLog = (function() {
41853
42965
  RED.view.focus();
41854
42966
  } else {
41855
42967
  stack[stack.length-1].tray.css("z-index", "auto");
42968
+ RED.events.emit('editor:change')
41856
42969
  }
41857
42970
  },250)
41858
42971
  }
@@ -46935,17 +48048,19 @@ RED.subflow = (function() {
46935
48048
 
46936
48049
 
46937
48050
  /**
46938
- * Create interface for controlling env var UI definition
48051
+ * Build the edit dialog for a subflow template (creating/modifying a subflow template)
48052
+ * @param {Object} uiContainer - the jQuery container for the environment variable list
48053
+ * @param {Object} node - the subflow template node
46939
48054
  */
46940
- function buildEnvControl(envList,node) {
48055
+ function buildEnvControl(uiContainer,node) {
46941
48056
  var tabs = RED.tabs.create({
46942
48057
  id: "subflow-env-tabs",
46943
48058
  onchange: function(tab) {
46944
48059
  if (tab.id === "subflow-env-tab-preview") {
46945
48060
  var inputContainer = $("#subflow-input-ui");
46946
- var list = envList.editableList("items");
48061
+ var list = uiContainer.editableList("items");
46947
48062
  var exportedEnv = exportEnvList(list, true);
46948
- buildEnvUI(inputContainer, exportedEnv,node);
48063
+ buildEnvUI(inputContainer, exportedEnv, node);
46949
48064
  }
46950
48065
  $("#subflow-env-tabs-content").children().hide();
46951
48066
  $("#" + tab.id).show();
@@ -46983,12 +48098,33 @@ RED.subflow = (function() {
46983
48098
  RED.editor.envVarList.setLocale(locale);
46984
48099
  }
46985
48100
 
46986
-
46987
- function buildEnvUIRow(row, tenv, ui, node) {
48101
+ /**
48102
+ * Build a UI row for a subflow instance environment variable
48103
+ * Also used to build the UI row for subflow template preview
48104
+ * @param {JQuery} row - A form row element
48105
+ * @param {Object} tenv - A template environment variable
48106
+ * @param {String} tenv.name - The name of the environment variable
48107
+ * @param {String} tenv.type - The type of the environment variable
48108
+ * @param {String} tenv.value - The value set for this environment variable
48109
+ * @param {Object} tenv.parent - The parent environment variable
48110
+ * @param {String} tenv.parent.value - The value set for the parent environment variable
48111
+ * @param {String} tenv.parent.type - The type of the parent environment variable
48112
+ * @param {Object} tenv.ui - The UI configuration for the environment variable
48113
+ * @param {String} tenv.ui.icon - The icon for the environment variable
48114
+ * @param {Object} tenv.ui.label - The label for the environment variable
48115
+ * @param {String} tenv.ui.type - The type of the UI control for the environment variable
48116
+ * @param {Object} node - The subflow instance node
48117
+ */
48118
+ function buildEnvUIRow(row, tenv, node) {
48119
+ if(RED.subflow.debug) { console.log("buildEnvUIRow", tenv) }
48120
+ const ui = tenv.ui || {}
46988
48121
  ui.label = ui.label||{};
46989
48122
  if ((tenv.type === "cred" || (tenv.parent && tenv.parent.type === "cred")) && !ui.type) {
46990
48123
  ui.type = "cred";
46991
48124
  ui.opts = {};
48125
+ } else if (tenv.type === "conf-types") {
48126
+ ui.type = "conf-types"
48127
+ ui.opts = { types: ['conf-types'] }
46992
48128
  } else if (!ui.type) {
46993
48129
  ui.type = "input";
46994
48130
  ui.opts = { types: RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST }
@@ -47032,9 +48168,10 @@ RED.subflow = (function() {
47032
48168
  if (tenv.hasOwnProperty('type')) {
47033
48169
  val.type = tenv.type;
47034
48170
  }
48171
+ const elId = getSubflowEnvPropertyName(tenv.name)
47035
48172
  switch(ui.type) {
47036
48173
  case "input":
47037
- input = $('<input type="text">').css('width','70%').appendTo(row);
48174
+ input = $('<input type="text">').css('width','70%').attr('id', elId).appendTo(row);
47038
48175
  if (ui.opts.types && ui.opts.types.length > 0) {
47039
48176
  var inputType = val.type;
47040
48177
  if (ui.opts.types.indexOf(inputType) === -1) {
@@ -47061,7 +48198,7 @@ RED.subflow = (function() {
47061
48198
  }
47062
48199
  break;
47063
48200
  case "select":
47064
- input = $('<select>').css('width','70%').appendTo(row);
48201
+ input = $('<select>').css('width','70%').attr('id', elId).appendTo(row);
47065
48202
  if (ui.opts.opts) {
47066
48203
  ui.opts.opts.forEach(function(o) {
47067
48204
  $('<option>').val(o.v).text(RED.editor.envVarList.lookupLabel(o.l, o.l['en-US']||o.v, locale)).appendTo(input);
@@ -47072,7 +48209,7 @@ RED.subflow = (function() {
47072
48209
  case "checkbox":
47073
48210
  label.css("cursor","default");
47074
48211
  var cblabel = $('<label>').css('width','70%').appendTo(row);
47075
- input = $('<input type="checkbox">').css({
48212
+ input = $('<input type="checkbox">').attr('id', elId).css({
47076
48213
  marginTop: 0,
47077
48214
  width: 'auto',
47078
48215
  height: '34px'
@@ -47090,7 +48227,7 @@ RED.subflow = (function() {
47090
48227
  input.prop("checked",boolVal);
47091
48228
  break;
47092
48229
  case "spinner":
47093
- input = $('<input>').css('width','70%').appendTo(row);
48230
+ input = $('<input>').css('width','70%').attr('id', elId).appendTo(row);
47094
48231
  var spinnerOpts = {};
47095
48232
  if (ui.opts.hasOwnProperty('min')) {
47096
48233
  spinnerOpts.min = ui.opts.min;
@@ -47119,18 +48256,25 @@ RED.subflow = (function() {
47119
48256
  default: 'cred'
47120
48257
  })
47121
48258
  break;
47122
- }
47123
- if (input) {
47124
- input.attr('id',getSubflowEnvPropertyName(tenv.name))
48259
+ case "conf-types":
48260
+ // let clsId = 'config-node-input-' + val.type + '-' + val.value + '-' + Math.floor(Math.random() * 100000);
48261
+ // clsId = clsId.replace(/\W/g, '-');
48262
+ // input = $('<input>').css('width','70%').addClass(clsId).attr('id', elId).appendTo(row);
48263
+ input = $('<input>').css('width','70%').attr('id', elId).appendTo(row);
48264
+ const _type = tenv.parent?.type || tenv.type;
48265
+ RED.editor.prepareConfigNodeSelect(node, tenv.name, _type, 'node-input-subflow-env', null, tenv);
48266
+ break;
47125
48267
  }
47126
48268
  }
47127
48269
 
47128
48270
  /**
47129
- * Create environment variable input UI
48271
+ * Build the edit form for a subflow instance
48272
+ * Also used to build the preview form in the subflow template edit dialog
47130
48273
  * @param uiContainer - container for UI
47131
48274
  * @param envList - env var definitions of template
47132
48275
  */
47133
48276
  function buildEnvUI(uiContainer, envList, node) {
48277
+ if(RED.subflow.debug) { console.log("buildEnvUI",envList) }
47134
48278
  uiContainer.empty();
47135
48279
  for (var i = 0; i < envList.length; i++) {
47136
48280
  var tenv = envList[i];
@@ -47138,7 +48282,7 @@ RED.subflow = (function() {
47138
48282
  continue;
47139
48283
  }
47140
48284
  var row = $("<div/>", { class: "form-row" }).appendTo(uiContainer);
47141
- buildEnvUIRow(row,tenv, tenv.ui || {}, node);
48285
+ buildEnvUIRow(row, tenv, node);
47142
48286
  }
47143
48287
  }
47144
48288
  // buildEnvUI
@@ -47211,6 +48355,9 @@ RED.subflow = (function() {
47211
48355
  delete ui.opts
47212
48356
  }
47213
48357
  break;
48358
+ case "conf-types":
48359
+ delete ui.opts;
48360
+ break;
47214
48361
  default:
47215
48362
  delete ui.opts;
47216
48363
  }
@@ -47233,8 +48380,9 @@ RED.subflow = (function() {
47233
48380
  if (/^subflow:/.test(node.type)) {
47234
48381
  var subflowDef = RED.nodes.subflow(node.type.substring(8));
47235
48382
  if (subflowDef.env) {
47236
- subflowDef.env.forEach(function(env) {
48383
+ subflowDef.env.forEach(function(env, i) {
47237
48384
  var item = {
48385
+ index: i,
47238
48386
  name:env.name,
47239
48387
  parent: {
47240
48388
  type: env.type,
@@ -47271,14 +48419,20 @@ RED.subflow = (function() {
47271
48419
  var nodePropValue = nodeProp;
47272
48420
  if (prop.ui && prop.ui.type === "cred") {
47273
48421
  nodePropType = "cred";
48422
+ } else if (prop.ui && prop.ui.type === "conf-types") {
48423
+ nodePropType = prop.value.type
47274
48424
  } else {
47275
48425
  switch(typeof nodeProp) {
47276
48426
  case "string": nodePropType = "str"; break;
47277
48427
  case "number": nodePropType = "num"; break;
47278
48428
  case "boolean": nodePropType = "bool"; nodePropValue = nodeProp?"true":"false"; break;
47279
48429
  default:
47280
- nodePropType = nodeProp.type;
47281
- nodePropValue = nodeProp.value;
48430
+ if (nodeProp) {
48431
+ nodePropType = nodeProp.type;
48432
+ nodePropValue = nodeProp.value;
48433
+ } else {
48434
+ nodePropType = 'str'
48435
+ }
47282
48436
  }
47283
48437
  }
47284
48438
  var item = {
@@ -47299,6 +48453,7 @@ RED.subflow = (function() {
47299
48453
  }
47300
48454
 
47301
48455
  function exportSubflowInstanceEnv(node) {
48456
+ if(RED.subflow.debug) { console.log("exportSubflowInstanceEnv",node) }
47302
48457
  var env = [];
47303
48458
  // First, get the values for the SubflowTemplate defined properties
47304
48459
  // - these are the ones with custom UI elements
@@ -47345,6 +48500,9 @@ RED.subflow = (function() {
47345
48500
  item.type = 'bool';
47346
48501
  item.value = ""+input.prop("checked");
47347
48502
  break;
48503
+ case "conf-types":
48504
+ item.value = input.val()
48505
+ item.type = data.parent.value;
47348
48506
  }
47349
48507
  if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) {
47350
48508
  env.push(item);
@@ -47358,8 +48516,15 @@ RED.subflow = (function() {
47358
48516
  return 'node-input-subflow-env-'+name.replace(/[^a-z0-9-_]/ig,"_");
47359
48517
  }
47360
48518
 
47361
- // Called by subflow.oneditprepare for both instances and templates
48519
+
48520
+ /**
48521
+ * Build the subflow edit form
48522
+ * Called by subflow.oneditprepare for both instances and templates
48523
+ * @param {"subflow"|"subflow-template"} type - the type of subflow being edited
48524
+ * @param {Object} node - the node being edited
48525
+ */
47362
48526
  function buildEditForm(type,node) {
48527
+ if(RED.subflow.debug) { console.log("buildEditForm",type,node) }
47363
48528
  if (type === "subflow-template") {
47364
48529
  // This is the tabbed UI that offers the env list - with UI options
47365
48530
  // plus the preview tab
@@ -55021,10 +56186,15 @@ RED.touch.radialMenu = (function() {
55021
56186
 
55022
56187
  function listTour() {
55023
56188
  return [
56189
+ {
56190
+ id: "4_0",
56191
+ label: "4.0",
56192
+ path: "./tours/welcome.js"
56193
+ },
55024
56194
  {
55025
56195
  id: "3_1",
55026
56196
  label: "3.1",
55027
- path: "./tours/welcome.js"
56197
+ path: "./tours/3.1/welcome.js"
55028
56198
  },
55029
56199
  {
55030
56200
  id: "3_0",