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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/red/red.js CHANGED
@@ -389,6 +389,7 @@ var RED = (function() {
389
389
  RED.workspaces.show(workspaces[0]);
390
390
  }
391
391
  }
392
+ RED.events.emit('flows:loaded')
392
393
  } catch(err) {
393
394
  console.warn(err);
394
395
  RED.notify(
@@ -1877,6 +1878,8 @@ RED.user = (function() {
1877
1878
 
1878
1879
  function updateUserMenu() {
1879
1880
  $("#red-ui-header-button-user-submenu li").remove();
1881
+ const userMenu = $("#red-ui-header-button-user")
1882
+ userMenu.empty()
1880
1883
  if (RED.settings.user.anonymous) {
1881
1884
  RED.menu.addItem("red-ui-header-button-user",{
1882
1885
  id:"usermenu-item-login",
@@ -1891,7 +1894,6 @@ RED.user = (function() {
1891
1894
  });
1892
1895
  }
1893
1896
  });
1894
- $('<i class="fa fa-user"></i>').appendTo("#red-ui-header-button-user");
1895
1897
  } else {
1896
1898
  RED.menu.addItem("red-ui-header-button-user",{
1897
1899
  id:"usermenu-item-username",
@@ -1904,17 +1906,9 @@ RED.user = (function() {
1904
1906
  RED.user.logout();
1905
1907
  }
1906
1908
  });
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
- }
1916
1909
  }
1917
-
1910
+ const userIcon = generateUserIcon(RED.settings.user)
1911
+ userIcon.appendTo(userMenu);
1918
1912
  }
1919
1913
 
1920
1914
  function init() {
@@ -1985,12 +1979,30 @@ RED.user = (function() {
1985
1979
  return false;
1986
1980
  }
1987
1981
 
1982
+ function generateUserIcon(user) {
1983
+ const userIcon = $('<span class="red-ui-user-profile"></span>')
1984
+ if (user.image) {
1985
+ userIcon.addClass('has_profile_image')
1986
+ userIcon.css({
1987
+ backgroundImage: "url("+user.image+")",
1988
+ })
1989
+ } else if (user.anonymous) {
1990
+ $('<i class="fa fa-user"></i>').appendTo(userIcon);
1991
+ } else {
1992
+ $('<span>').text(user.username.substring(0,2)).appendTo(userIcon);
1993
+ }
1994
+ if (user.profileColor !== undefined) {
1995
+ userIcon.addClass('red-ui-user-profile-color-' + user.profileColor)
1996
+ }
1997
+ return userIcon
1998
+ }
1988
1999
 
1989
2000
  return {
1990
2001
  init: init,
1991
2002
  login: login,
1992
2003
  logout: logout,
1993
- hasPermission: hasPermission
2004
+ hasPermission: hasPermission,
2005
+ generateUserIcon
1994
2006
  }
1995
2007
 
1996
2008
  })();
@@ -2274,115 +2286,93 @@ RED.comms = (function() {
2274
2286
  })()
2275
2287
  ;RED.multiplayer = (function () {
2276
2288
 
2277
- // sessionId - used to identify sessions across websocket reconnects
2278
- let sessionId
2289
+ // activeSessionId - used to identify sessions across websocket reconnects
2290
+ let activeSessionId
2279
2291
 
2280
2292
  let headerWidget
2281
2293
  // Map of session id to { session:'', user:{}, location:{}}
2282
- let connections = {}
2283
- // Map of username to { user:{}, connections:[] }
2294
+ let sessions = {}
2295
+ // Map of username to { user:{}, sessions:[] }
2284
2296
  let users = {}
2285
2297
 
2286
- function addUserConnection (connection) {
2287
- if (connections[connection.session]) {
2298
+ function addUserSession (session) {
2299
+ if (sessions[session.session]) {
2288
2300
  // 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])
2301
+ const existingSession = sessions[session.session]
2302
+ if (existingSession.user.username !== session.user.username) {
2303
+ removeUserHeaderButton(users[existingSession.user.username])
2292
2304
  }
2293
2305
  }
2294
- connections[connection.session] = connection
2295
- const user = users[connection.user.username] = users[connection.user.username] || {
2296
- user: connection.user,
2297
- connections: []
2306
+ sessions[session.session] = session
2307
+ const user = users[session.user.username] = users[session.user.username] || {
2308
+ user: session.user,
2309
+ sessions: []
2310
+ }
2311
+ if (session.user.profileColor === undefined) {
2312
+ session.user.profileColor = (1 + Math.floor(Math.random() * 5))
2298
2313
  }
2299
- connection.location = connection.location || {}
2300
- user.connections.push(connection)
2314
+ session.location = session.location || {}
2315
+ user.sessions.push(session)
2301
2316
 
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
2317
+ if (session.session === activeSessionId) {
2318
+ // This is the current user session - do not add a extra button for them
2306
2319
  } else {
2307
- if (user.connections.length === 1) {
2320
+ if (user.sessions.length === 1) {
2308
2321
  if (user.button) {
2309
2322
  clearTimeout(user.inactiveTimeout)
2310
2323
  clearTimeout(user.removeTimeout)
2311
2324
  user.button.removeClass('inactive')
2312
2325
  } else {
2313
- addUserButton(user)
2326
+ addUserHeaderButton(user)
2314
2327
  }
2315
2328
  }
2329
+ sessions[session.session].location = session.location
2330
+ updateUserLocation(session.session)
2316
2331
  }
2317
2332
  }
2318
2333
 
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)
2334
+ function removeUserSession (sessionId, isDisconnected) {
2335
+ removeUserLocation(sessionId)
2336
+ const session = sessions[sessionId]
2337
+ delete sessions[sessionId]
2338
+ const user = users[session.user.username]
2339
+ const i = user.sessions.indexOf(session)
2340
+ user.sessions.splice(i, 1)
2325
2341
  if (isDisconnected) {
2326
- removeUserButton(user)
2342
+ removeUserHeaderButton(user)
2327
2343
  } else {
2328
- if (user.connections.length === 0) {
2344
+ if (user.sessions.length === 0) {
2329
2345
  // Give the user 5s to reconnect before marking inactive
2330
2346
  user.inactiveTimeout = setTimeout(() => {
2331
2347
  user.button.addClass('inactive')
2332
2348
  // Give the user further 20 seconds to reconnect before removing them
2333
2349
  // from the user toolbar entirely
2334
2350
  user.removeTimeout = setTimeout(() => {
2335
- removeUserButton(user)
2351
+ removeUserHeaderButton(user)
2336
2352
  }, 20000)
2337
2353
  }, 5000)
2338
2354
  }
2339
2355
  }
2340
2356
  }
2341
2357
 
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>')
2358
+ function addUserHeaderButton (user) {
2359
+ user.button = $('<li class="red-ui-multiplayer-user"><button type="button" class="red-ui-multiplayer-user-icon"></button></li>')
2344
2360
  .attr('data-username', user.user.username)
2345
2361
  .prependTo("#red-ui-multiplayer-user-list");
2346
2362
  var button = user.button.find("button")
2363
+ RED.popover.tooltip(button, user.user.username)
2347
2364
  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()
2365
+ const location = user.sessions[0].location
2366
+ revealUser(location)
2378
2367
  })
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
- }
2368
+
2369
+ const userProfile = RED.user.generateUserIcon(user.user)
2370
+ userProfile.appendTo(button)
2371
+ }
2372
+
2373
+ function removeUserHeaderButton (user) {
2374
+ user.button.remove()
2375
+ delete user.button
2386
2376
  }
2387
2377
 
2388
2378
  function getLocation () {
@@ -2398,7 +2388,7 @@ RED.comms = (function() {
2398
2388
  }
2399
2389
  return location
2400
2390
  }
2401
- function updateLocation () {
2391
+ function publishLocation () {
2402
2392
  const location = getLocation()
2403
2393
  if (location.workspace !== 0) {
2404
2394
  log('send', 'multiplayer/location', location)
@@ -2406,31 +2396,314 @@ RED.comms = (function() {
2406
2396
  }
2407
2397
  }
2408
2398
 
2409
- function removeUserButton (user) {
2410
- user.button.remove()
2411
- delete user.button
2399
+ function revealUser(location, skipWorkspace) {
2400
+ if (location.node) {
2401
+ // Need to check if this is a known node, so we can fall back to revealing
2402
+ // the workspace instead
2403
+ const node = RED.nodes.node(location.node)
2404
+ if (node) {
2405
+ RED.view.reveal(location.node)
2406
+ } else if (!skipWorkspace && location.workspace) {
2407
+ RED.view.reveal(location.workspace)
2408
+ }
2409
+ } else if (!skipWorkspace && location.workspace) {
2410
+ RED.view.reveal(location.workspace)
2411
+ }
2412
2412
  }
2413
2413
 
2414
- function updateUserLocation (data) {
2415
- connections[data.session].location = data
2416
- delete data.session
2414
+ const workspaceTrays = {}
2415
+ function getWorkspaceTray(workspaceId) {
2416
+ // console.log('get tray for',workspaceId)
2417
+ if (!workspaceTrays[workspaceId]) {
2418
+ const tray = $('<div class="red-ui-multiplayer-users-tray"></div>')
2419
+ const users = []
2420
+ const userIcons = {}
2421
+
2422
+ const userCountIcon = $(`<div class="red-ui-multiplayer-user-location"><span class="red-ui-user-profile red-ui-multiplayer-user-count"><span></span></span></div>`)
2423
+ const userCountSpan = userCountIcon.find('span span')
2424
+ userCountIcon.hide()
2425
+ userCountSpan.text('')
2426
+ userCountIcon.appendTo(tray)
2427
+ const userCountTooltip = RED.popover.tooltip(userCountIcon, function () {
2428
+ const content = $('<div>')
2429
+ users.forEach(sessionId => {
2430
+ $('<div>').append($('<a href="#">').text(sessions[sessionId].user.username).on('click', function (evt) {
2431
+ evt.preventDefault()
2432
+ revealUser(sessions[sessionId].location, true)
2433
+ userCountTooltip.close()
2434
+ })).appendTo(content)
2435
+ })
2436
+ return content
2437
+ },
2438
+ null,
2439
+ true
2440
+ )
2441
+
2442
+ const updateUserCount = function () {
2443
+ const maxShown = 2
2444
+ const children = tray.children()
2445
+ children.each(function (index, element) {
2446
+ const i = users.length - index
2447
+ if (i > maxShown) {
2448
+ $(this).hide()
2449
+ } else if (i >= 0) {
2450
+ $(this).show()
2451
+ }
2452
+ })
2453
+ if (users.length < maxShown + 1) {
2454
+ userCountIcon.hide()
2455
+ } else {
2456
+ userCountSpan.text('+'+(users.length - maxShown))
2457
+ userCountIcon.show()
2458
+ }
2459
+ }
2460
+ workspaceTrays[workspaceId] = {
2461
+ attached: false,
2462
+ tray,
2463
+ users,
2464
+ userIcons,
2465
+ addUser: function (sessionId) {
2466
+ if (users.indexOf(sessionId) === -1) {
2467
+ // console.log(`addUser ws:${workspaceId} session:${sessionId}`)
2468
+ users.push(sessionId)
2469
+ const userLocationId = `red-ui-multiplayer-user-location-${sessionId}`
2470
+ const userLocationIcon = $(`<div class="red-ui-multiplayer-user-location" id="${userLocationId}"></div>`)
2471
+ RED.user.generateUserIcon(sessions[sessionId].user).appendTo(userLocationIcon)
2472
+ userLocationIcon.prependTo(tray)
2473
+ RED.popover.tooltip(userLocationIcon, sessions[sessionId].user.username)
2474
+ userIcons[sessionId] = userLocationIcon
2475
+ updateUserCount()
2476
+ }
2477
+ },
2478
+ removeUser: function (sessionId) {
2479
+ // console.log(`removeUser ws:${workspaceId} session:${sessionId}`)
2480
+ const userLocationId = `red-ui-multiplayer-user-location-${sessionId}`
2481
+ const index = users.indexOf(sessionId)
2482
+ if (index > -1) {
2483
+ users.splice(index, 1)
2484
+ userIcons[sessionId].remove()
2485
+ delete userIcons[sessionId]
2486
+ }
2487
+ updateUserCount()
2488
+ },
2489
+ updateUserCount
2490
+ }
2491
+ }
2492
+ const trayDef = workspaceTrays[workspaceId]
2493
+ if (!trayDef.attached) {
2494
+ const workspaceTab = $(`#red-ui-tab-${workspaceId}`)
2495
+ if (workspaceTab.length > 0) {
2496
+ trayDef.attached = true
2497
+ trayDef.tray.appendTo(workspaceTab)
2498
+ trayDef.users.forEach(sessionId => {
2499
+ trayDef.userIcons[sessionId].on('click', function (evt) {
2500
+ revealUser(sessions[sessionId].location, true)
2501
+ })
2502
+ })
2503
+ }
2504
+ }
2505
+ return workspaceTrays[workspaceId]
2506
+ }
2507
+ function attachWorkspaceTrays () {
2508
+ let viewTouched = false
2509
+ for (let sessionId of Object.keys(sessions)) {
2510
+ const location = sessions[sessionId].location
2511
+ if (location) {
2512
+ if (location.workspace) {
2513
+ getWorkspaceTray(location.workspace).updateUserCount()
2514
+ }
2515
+ if (location.node) {
2516
+ addUserToNode(sessionId, location.node)
2517
+ viewTouched = true
2518
+ }
2519
+ }
2520
+ }
2521
+ if (viewTouched) {
2522
+ RED.view.redraw()
2523
+ }
2524
+ }
2525
+
2526
+ function addUserToNode(sessionId, nodeId) {
2527
+ const node = RED.nodes.node(nodeId)
2528
+ if (node) {
2529
+ if (!node._multiplayer) {
2530
+ node._multiplayer = {
2531
+ users: [sessionId]
2532
+ }
2533
+ node._multiplayer_refresh = true
2534
+ } else {
2535
+ if (node._multiplayer.users.indexOf(sessionId) === -1) {
2536
+ node._multiplayer.users.push(sessionId)
2537
+ node._multiplayer_refresh = true
2538
+ }
2539
+ }
2540
+ }
2541
+ }
2542
+ function removeUserFromNode(sessionId, nodeId) {
2543
+ const node = RED.nodes.node(nodeId)
2544
+ if (node && node._multiplayer) {
2545
+ const i = node._multiplayer.users.indexOf(sessionId)
2546
+ if (i > -1) {
2547
+ node._multiplayer.users.splice(i, 1)
2548
+ }
2549
+ if (node._multiplayer.users.length === 0) {
2550
+ delete node._multiplayer
2551
+ } else {
2552
+ node._multiplayer_refresh = true
2553
+ }
2554
+ }
2555
+
2556
+ }
2557
+
2558
+ function removeUserLocation (sessionId) {
2559
+ updateUserLocation(sessionId, {})
2560
+ }
2561
+ function updateUserLocation (sessionId, location) {
2562
+ let viewTouched = false
2563
+ const oldLocation = sessions[sessionId].location
2564
+ if (location) {
2565
+ if (oldLocation.workspace !== location.workspace) {
2566
+ // console.log('removing', sessionId, oldLocation.workspace)
2567
+ workspaceTrays[oldLocation.workspace]?.removeUser(sessionId)
2568
+ }
2569
+ if (oldLocation.node !== location.node) {
2570
+ removeUserFromNode(sessionId, oldLocation.node)
2571
+ viewTouched = true
2572
+ }
2573
+ sessions[sessionId].location = location
2574
+ } else {
2575
+ location = sessions[sessionId].location
2576
+ }
2577
+ // console.log(`updateUserLocation sessionId:${sessionId} oldWS:${oldLocation?.workspace} newWS:${location.workspace}`)
2578
+ if (location.workspace) {
2579
+ getWorkspaceTray(location.workspace).addUser(sessionId)
2580
+ }
2581
+ if (location.node) {
2582
+ addUserToNode(sessionId, location.node)
2583
+ viewTouched = true
2584
+ }
2585
+ if (viewTouched) {
2586
+ RED.view.redraw()
2587
+ }
2417
2588
  }
2589
+
2590
+ // function refreshUserLocations () {
2591
+ // for (const session of Object.keys(sessions)) {
2592
+ // if (session !== activeSessionId) {
2593
+ // updateUserLocation(session)
2594
+ // }
2595
+ // }
2596
+ // }
2597
+
2418
2598
  return {
2419
2599
  init: function () {
2420
-
2421
2600
 
2422
- sessionId = RED.settings.getLocal('multiplayer:sessionId')
2423
- if (!sessionId) {
2424
- sessionId = RED.nodes.id()
2425
- RED.settings.setLocal('multiplayer:sessionId', sessionId)
2601
+ function createAnnotationUser(user) {
2602
+
2603
+ const group = document.createElementNS("http://www.w3.org/2000/svg","g");
2604
+ const badge = document.createElementNS("http://www.w3.org/2000/svg","circle");
2605
+ const radius = 20
2606
+ badge.setAttribute("cx",radius/2);
2607
+ badge.setAttribute("cy",radius/2);
2608
+ badge.setAttribute("r",radius/2);
2609
+ badge.setAttribute("class", "red-ui-multiplayer-annotation-background")
2610
+ group.appendChild(badge)
2611
+ if (user && user.profileColor !== undefined) {
2612
+ badge.setAttribute("class", "red-ui-multiplayer-annotation-background red-ui-user-profile-color-" + user.profileColor)
2613
+ }
2614
+ if (user && user.image) {
2615
+ const image = document.createElementNS("http://www.w3.org/2000/svg","image");
2616
+ image.setAttribute("width", radius)
2617
+ image.setAttribute("height", radius)
2618
+ image.setAttribute("href", user.image)
2619
+ image.setAttribute("clip-path", "circle("+Math.floor(radius/2)+")")
2620
+ group.appendChild(image)
2621
+ } else if (user && user.anonymous) {
2622
+ const anonIconHead = document.createElementNS("http://www.w3.org/2000/svg","circle");
2623
+ anonIconHead.setAttribute("cx", radius/2)
2624
+ anonIconHead.setAttribute("cy", radius/2 - 2)
2625
+ anonIconHead.setAttribute("r", 2.4)
2626
+ anonIconHead.setAttribute("class","red-ui-multiplayer-annotation-anon-label");
2627
+ group.appendChild(anonIconHead)
2628
+ const anonIconBody = document.createElementNS("http://www.w3.org/2000/svg","path");
2629
+ anonIconBody.setAttribute("class","red-ui-multiplayer-annotation-anon-label");
2630
+ // anonIconBody.setAttribute("d",`M ${radius/2 - 4} ${radius/2 + 1} h 8 v4 h -8 z`);
2631
+ anonIconBody.setAttribute("d",`M ${radius/2} ${radius/2 + 5} h -2.5 c -2 1 -2 -5 0.5 -4.5 c 2 1 2 1 4 0 c 2.5 -0.5 2.5 5.5 0 4.5 z`);
2632
+ group.appendChild(anonIconBody)
2633
+ } else {
2634
+ const labelText = user.username ? user.username.substring(0,2) : user
2635
+ const label = document.createElementNS("http://www.w3.org/2000/svg","text");
2636
+ if (user.username) {
2637
+ label.setAttribute("class","red-ui-multiplayer-annotation-label");
2638
+ label.textContent = user.username.substring(0,2)
2639
+ } else {
2640
+ label.setAttribute("class","red-ui-multiplayer-annotation-label red-ui-multiplayer-user-count")
2641
+ label.textContent = user
2642
+ }
2643
+ label.setAttribute("text-anchor", "middle")
2644
+ label.setAttribute("x",radius/2);
2645
+ label.setAttribute("y",radius/2 + 3);
2646
+ group.appendChild(label)
2647
+ }
2648
+ const border = document.createElementNS("http://www.w3.org/2000/svg","circle");
2649
+ border.setAttribute("cx",radius/2);
2650
+ border.setAttribute("cy",radius/2);
2651
+ border.setAttribute("r",radius/2);
2652
+ border.setAttribute("class", "red-ui-multiplayer-annotation-border")
2653
+ group.appendChild(border)
2654
+
2655
+
2656
+
2657
+ return group
2426
2658
  }
2427
2659
 
2660
+ RED.view.annotations.register("red-ui-multiplayer",{
2661
+ type: 'badge',
2662
+ align: 'left',
2663
+ class: "red-ui-multiplayer-annotation",
2664
+ show: "_multiplayer",
2665
+ refresh: "_multiplayer_refresh",
2666
+ element: function(node) {
2667
+ const containerGroup = document.createElementNS("http://www.w3.org/2000/svg","g");
2668
+ containerGroup.setAttribute("transform","translate(0,-4)")
2669
+ if (node._multiplayer) {
2670
+ let y = 0
2671
+ for (let i = Math.min(1, node._multiplayer.users.length - 1); i >= 0; i--) {
2672
+ const user = sessions[node._multiplayer.users[i]].user
2673
+ const group = createAnnotationUser(user)
2674
+ group.setAttribute("transform","translate("+y+",0)")
2675
+ y += 15
2676
+ containerGroup.appendChild(group)
2677
+ }
2678
+ if (node._multiplayer.users.length > 2) {
2679
+ const group = createAnnotationUser('+'+(node._multiplayer.users.length - 2))
2680
+ group.setAttribute("transform","translate("+y+",0)")
2681
+ y += 12
2682
+ containerGroup.appendChild(group)
2683
+ }
2684
+
2685
+ }
2686
+ return containerGroup;
2687
+ },
2688
+ tooltip: node => { return node._multiplayer.users.map(u => sessions[u].user.username).join('\n') }
2689
+ });
2690
+
2691
+
2692
+ // activeSessionId = RED.settings.getLocal('multiplayer:sessionId')
2693
+ // if (!activeSessionId) {
2694
+ activeSessionId = RED.nodes.id()
2695
+ // RED.settings.setLocal('multiplayer:sessionId', activeSessionId)
2696
+ // log('Session ID (new)', activeSessionId)
2697
+ // } else {
2698
+ log('Session ID', activeSessionId)
2699
+ // }
2700
+
2428
2701
  headerWidget = $('<li><ul id="red-ui-multiplayer-user-list"></ul></li>').prependTo('.red-ui-header-toolbar')
2429
2702
 
2430
2703
  RED.comms.on('connect', () => {
2431
2704
  const location = getLocation()
2432
2705
  const connectInfo = {
2433
- session: sessionId
2706
+ session: activeSessionId
2434
2707
  }
2435
2708
  if (location.workspace !== 0) {
2436
2709
  connectInfo.location = location
@@ -2442,40 +2715,52 @@ RED.comms = (function() {
2442
2715
  if (topic === 'multiplayer/init') {
2443
2716
  // We have just reconnected, runtime has sent state to
2444
2717
  // initialise the world
2445
- connections = {}
2718
+ sessions = {}
2446
2719
  users = {}
2447
2720
  $('#red-ui-multiplayer-user-list').empty()
2448
2721
 
2449
- msg.forEach(connection => {
2450
- addUserConnection(connection)
2722
+ msg.sessions.forEach(session => {
2723
+ addUserSession(session)
2451
2724
  })
2452
2725
  } else if (topic === 'multiplayer/connection-added') {
2453
- addUserConnection(msg)
2726
+ addUserSession(msg)
2454
2727
  } else if (topic === 'multiplayer/connection-removed') {
2455
- removeUserConnection(msg.session, msg.disconnected)
2728
+ removeUserSession(msg.session, msg.disconnected)
2456
2729
  } else if (topic === 'multiplayer/location') {
2457
- updateUserLocation(msg)
2730
+ const session = msg.session
2731
+ delete msg.session
2732
+ updateUserLocation(session, msg)
2458
2733
  }
2459
2734
  })
2460
2735
 
2461
2736
  RED.events.on('workspace:change', (event) => {
2462
- updateLocation()
2737
+ getWorkspaceTray(event.workspace)
2738
+ publishLocation()
2463
2739
  })
2464
2740
  RED.events.on('editor:open', () => {
2465
- updateLocation()
2741
+ publishLocation()
2466
2742
  })
2467
2743
  RED.events.on('editor:close', () => {
2468
- updateLocation()
2744
+ publishLocation()
2469
2745
  })
2470
2746
  RED.events.on('editor:change', () => {
2471
- updateLocation()
2747
+ publishLocation()
2472
2748
  })
2473
2749
  RED.events.on('login', () => {
2474
- updateLocation()
2750
+ publishLocation()
2751
+ })
2752
+ RED.events.on('flows:loaded', () => {
2753
+ attachWorkspaceTrays()
2754
+ })
2755
+ RED.events.on('workspace:close', (event) => {
2756
+ // A subflow tab has been closed. Need to mark its tray as detached
2757
+ if (workspaceTrays[event.workspace]) {
2758
+ workspaceTrays[event.workspace].attached = false
2759
+ }
2475
2760
  })
2476
2761
  RED.events.on('logout', () => {
2477
2762
  const disconnectInfo = {
2478
- session: sessionId
2763
+ session: activeSessionId
2479
2764
  }
2480
2765
  RED.comms.send('multiplayer/disconnect', disconnectInfo)
2481
2766
  RED.settings.removeLocal('multiplayer:sessionId')
@@ -8086,7 +8371,14 @@ RED.history = (function() {
8086
8371
  }
8087
8372
  return RED.nodes.junction(id);
8088
8373
  }
8089
-
8374
+ function ensureUnlocked(id, flowsToLock) {
8375
+ const flow = id && (RED.nodes.workspace(id) || RED.nodes.subflow(id) || null);
8376
+ const isLocked = flow ? flow.locked : false;
8377
+ if (flow && isLocked) {
8378
+ flow.locked = false;
8379
+ flowsToLock.add(flow)
8380
+ }
8381
+ }
8090
8382
  function undoEvent(ev) {
8091
8383
  var i;
8092
8384
  var len;
@@ -8116,18 +8408,46 @@ RED.history = (function() {
8116
8408
  t: 'replace',
8117
8409
  config: RED.nodes.createCompleteNodeSet(),
8118
8410
  changed: {},
8119
- rev: RED.nodes.version()
8411
+ moved: {},
8412
+ complete: true,
8413
+ rev: RED.nodes.version(),
8414
+ dirty: RED.nodes.dirty()
8120
8415
  };
8416
+ var selectedTab = RED.workspaces.active();
8417
+ inverseEv.config.forEach(n => {
8418
+ const node = RED.nodes.node(n.id)
8419
+ if (node) {
8420
+ inverseEv.changed[n.id] = node.changed
8421
+ inverseEv.moved[n.id] = node.moved
8422
+ }
8423
+ })
8121
8424
  RED.nodes.clear();
8122
8425
  var imported = RED.nodes.import(ev.config);
8426
+ // Clear all change flags from the import
8427
+ RED.nodes.dirty(false);
8428
+
8429
+ const flowsToLock = new Set()
8430
+
8123
8431
  imported.nodes.forEach(function(n) {
8124
8432
  if (ev.changed[n.id]) {
8433
+ ensureUnlocked(n.z, flowsToLock)
8125
8434
  n.changed = true;
8126
- inverseEv.changed[n.id] = true;
8435
+ }
8436
+ if (ev.moved[n.id]) {
8437
+ ensureUnlocked(n.z, flowsToLock)
8438
+ n.moved = true;
8127
8439
  }
8128
8440
  })
8441
+ flowsToLock.forEach(flow => {
8442
+ flow.locked = true
8443
+ })
8129
8444
 
8130
8445
  RED.nodes.version(ev.rev);
8446
+ RED.view.redraw(true);
8447
+ RED.palette.refresh();
8448
+ RED.workspaces.refresh();
8449
+ RED.workspaces.show(selectedTab, true);
8450
+ RED.sidebar.config.refresh();
8131
8451
  } else {
8132
8452
  var importMap = {};
8133
8453
  ev.config.forEach(function(n) {
@@ -12913,9 +13233,12 @@ RED.popover = (function() {
12913
13233
 
12914
13234
  return {
12915
13235
  create: createPopover,
12916
- tooltip: function(target,content, action) {
13236
+ tooltip: function(target,content, action, interactive) {
12917
13237
  var label = function() {
12918
13238
  var label = content;
13239
+ if (typeof content === 'function') {
13240
+ label = content()
13241
+ }
12919
13242
  if (action) {
12920
13243
  var shortcut = RED.keyboard.getShortcut(action);
12921
13244
  if (shortcut && shortcut.key) {
@@ -12931,6 +13254,7 @@ RED.popover = (function() {
12931
13254
  size: "small",
12932
13255
  direction: "bottom",
12933
13256
  content: label,
13257
+ interactive,
12934
13258
  delay: { show: 750, hide: 50 }
12935
13259
  });
12936
13260
  popover.setContent = function(newContent) {
@@ -13689,7 +14013,10 @@ RED.tabs = (function() {
13689
14013
 
13690
14014
  var thisTabA = thisTab.find("a");
13691
14015
  if (options.onclick) {
13692
- options.onclick(tabs[thisTabA.attr('href').slice(1)]);
14016
+ options.onclick(tabs[thisTabA.attr('href').slice(1)], evt);
14017
+ if (evt.isDefaultPrevented() && evt.isPropagationStopped()) {
14018
+ return false
14019
+ }
13693
14020
  }
13694
14021
  activateTab(thisTabA);
13695
14022
  if (fireSelectionChanged) {
@@ -13872,6 +14199,8 @@ RED.tabs = (function() {
13872
14199
  ul.find("li.red-ui-tab a")
13873
14200
  .on("mousedown", function(evt) { mousedownTab = evt.currentTarget })
13874
14201
  .on("mouseup",onTabClick)
14202
+ // prevent browser-default middle-click behaviour
14203
+ .on("auxclick", function(evt) { evt.preventDefault() })
13875
14204
  .on("click", function(evt) {evt.preventDefault(); })
13876
14205
  .on("dblclick", function(evt) {evt.stopPropagation(); evt.preventDefault(); })
13877
14206
 
@@ -14140,6 +14469,8 @@ RED.tabs = (function() {
14140
14469
  }
14141
14470
  link.on("mousedown", function(evt) { mousedownTab = evt.currentTarget })
14142
14471
  link.on("mouseup",onTabClick);
14472
+ // prevent browser-default middle-click behaviour
14473
+ link.on("auxclick", function(evt) { evt.preventDefault() })
14143
14474
  link.on("click", function(evt) { evt.preventDefault(); })
14144
14475
  link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); })
14145
14476
 
@@ -16507,6 +16838,8 @@ RED.deploy = (function() {
16507
16838
 
16508
16839
  var currentDiff = null;
16509
16840
 
16841
+ var activeBackgroundDeployNotification;
16842
+
16510
16843
  function changeDeploymentType(type) {
16511
16844
  deploymentType = type;
16512
16845
  $("#red-ui-header-button-deploy-icon").attr("src",deploymentTypes[type].img);
@@ -16585,51 +16918,59 @@ RED.deploy = (function() {
16585
16918
  RED.actions.add("core:set-deploy-type-to-modified-nodes",function() { RED.menu.setSelected("deploymenu-item-node",true); });
16586
16919
  }
16587
16920
 
16588
-
16921
+ window.addEventListener('beforeunload', function (event) {
16922
+ if (RED.nodes.dirty()) {
16923
+ event.preventDefault();
16924
+ event.stopImmediatePropagation()
16925
+ event.returnValue = RED._("deploy.confirm.undeployedChanges");
16926
+ return
16927
+ }
16928
+ })
16589
16929
 
16590
16930
  RED.events.on('workspace:dirty',function(state) {
16591
16931
  if (state.dirty) {
16592
- window.onbeforeunload = function() {
16593
- return RED._("deploy.confirm.undeployedChanges");
16594
- }
16932
+ // window.onbeforeunload = function() {
16933
+ // return
16934
+ // }
16595
16935
  $("#red-ui-header-button-deploy").removeClass("disabled");
16596
16936
  } else {
16597
- window.onbeforeunload = null;
16937
+ // window.onbeforeunload = null;
16598
16938
  $("#red-ui-header-button-deploy").addClass("disabled");
16599
16939
  }
16600
16940
  });
16601
16941
 
16602
- var activeNotifyMessage;
16603
16942
  RED.comms.subscribe("notification/runtime-deploy",function(topic,msg) {
16604
- if (!activeNotifyMessage) {
16605
- var currentRev = RED.nodes.version();
16606
- if (currentRev === null || deployInflight || currentRev === msg.revision) {
16607
- return;
16608
- }
16609
- var message = $('<p>').text(RED._('deploy.confirm.backgroundUpdate'));
16610
- activeNotifyMessage = RED.notify(message,{
16611
- modal: true,
16612
- fixed: true,
16613
- buttons: [
16614
- {
16615
- text: RED._('deploy.confirm.button.ignore'),
16616
- click: function() {
16617
- activeNotifyMessage.close();
16618
- activeNotifyMessage = null;
16619
- }
16620
- },
16621
- {
16622
- text: RED._('deploy.confirm.button.review'),
16623
- class: "primary",
16624
- click: function() {
16625
- activeNotifyMessage.close();
16626
- var nns = RED.nodes.createCompleteNodeSet();
16627
- resolveConflict(nns,false);
16628
- activeNotifyMessage = null;
16629
- }
16943
+ var currentRev = RED.nodes.version();
16944
+ if (currentRev === null || deployInflight || currentRev === msg.revision) {
16945
+ return;
16946
+ }
16947
+ if (activeBackgroundDeployNotification?.hidden && !activeBackgroundDeployNotification?.closed) {
16948
+ activeBackgroundDeployNotification.showNotification()
16949
+ return
16950
+ }
16951
+ const message = $('<p>').text(RED._('deploy.confirm.backgroundUpdate'));
16952
+ const options = {
16953
+ id: 'background-update',
16954
+ type: 'compact',
16955
+ modal: false,
16956
+ fixed: true,
16957
+ timeout: 10000,
16958
+ buttons: [
16959
+ {
16960
+ text: RED._('deploy.confirm.button.review'),
16961
+ class: "primary",
16962
+ click: function() {
16963
+ activeBackgroundDeployNotification.hideNotification();
16964
+ var nns = RED.nodes.createCompleteNodeSet();
16965
+ resolveConflict(nns,false);
16630
16966
  }
16631
- ]
16632
- });
16967
+ }
16968
+ ]
16969
+ }
16970
+ if (!activeBackgroundDeployNotification || activeBackgroundDeployNotification.closed) {
16971
+ activeBackgroundDeployNotification = RED.notify(message, options)
16972
+ } else {
16973
+ activeBackgroundDeployNotification.update(message, options)
16633
16974
  }
16634
16975
  });
16635
16976
  }
@@ -16686,7 +17027,11 @@ RED.deploy = (function() {
16686
17027
  class: "primary disabled",
16687
17028
  click: function() {
16688
17029
  if (!$("#red-ui-deploy-dialog-confirm-deploy-review").hasClass('disabled')) {
16689
- RED.diff.showRemoteDiff();
17030
+ RED.diff.showRemoteDiff(null, {
17031
+ onmerge: function () {
17032
+ activeBackgroundDeployNotification.close()
17033
+ }
17034
+ });
16690
17035
  conflictNotification.close();
16691
17036
  }
16692
17037
  }
@@ -16699,6 +17044,7 @@ RED.deploy = (function() {
16699
17044
  if (!$("#red-ui-deploy-dialog-confirm-deploy-merge").hasClass('disabled')) {
16700
17045
  RED.diff.mergeDiff(currentDiff);
16701
17046
  conflictNotification.close();
17047
+ activeBackgroundDeployNotification.close()
16702
17048
  }
16703
17049
  }
16704
17050
  }
@@ -16711,6 +17057,7 @@ RED.deploy = (function() {
16711
17057
  click: function() {
16712
17058
  save(true,activeDeploy);
16713
17059
  conflictNotification.close();
17060
+ activeBackgroundDeployNotification.close()
16714
17061
  }
16715
17062
  })
16716
17063
  }
@@ -16721,21 +17068,17 @@ RED.deploy = (function() {
16721
17068
  buttons: buttons
16722
17069
  });
16723
17070
 
16724
- var now = Date.now();
16725
17071
  RED.diff.getRemoteDiff(function(diff) {
16726
- var ellapsed = Math.max(1000 - (Date.now()-now), 0);
16727
17072
  currentDiff = diff;
16728
- setTimeout(function() {
16729
- conflictCheck.hide();
16730
- var d = Object.keys(diff.conflicts);
16731
- if (d.length === 0) {
16732
- conflictAutoMerge.show();
16733
- $("#red-ui-deploy-dialog-confirm-deploy-merge").removeClass('disabled')
16734
- } else {
16735
- conflictManualMerge.show();
16736
- }
16737
- $("#red-ui-deploy-dialog-confirm-deploy-review").removeClass('disabled')
16738
- },ellapsed);
17073
+ conflictCheck.hide();
17074
+ var d = Object.keys(diff.conflicts);
17075
+ if (d.length === 0) {
17076
+ conflictAutoMerge.show();
17077
+ $("#red-ui-deploy-dialog-confirm-deploy-merge").removeClass('disabled')
17078
+ } else {
17079
+ conflictManualMerge.show();
17080
+ }
17081
+ $("#red-ui-deploy-dialog-confirm-deploy-review").removeClass('disabled')
16739
17082
  })
16740
17083
  }
16741
17084
  function cropList(list) {
@@ -17085,7 +17428,10 @@ RED.deploy = (function() {
17085
17428
  }
17086
17429
  });
17087
17430
  RED.nodes.eachSubflow(function (subflow) {
17088
- subflow.changed = false;
17431
+ if (subflow.changed) {
17432
+ subflow.changed = false;
17433
+ RED.events.emit("subflows:change", subflow);
17434
+ }
17089
17435
  });
17090
17436
  RED.nodes.eachWorkspace(function (ws) {
17091
17437
  if (ws.changed || ws.added) {
@@ -17193,7 +17539,6 @@ RED.diagnostics = (function () {
17193
17539
  };
17194
17540
  })();
17195
17541
  ;RED.diff = (function() {
17196
-
17197
17542
  var currentDiff = {};
17198
17543
  var diffVisible = false;
17199
17544
  var diffList;
@@ -17256,12 +17601,14 @@ RED.diagnostics = (function () {
17256
17601
  addedCount:0,
17257
17602
  deletedCount:0,
17258
17603
  changedCount:0,
17604
+ movedCount:0,
17259
17605
  unchangedCount: 0
17260
17606
  },
17261
17607
  remote: {
17262
17608
  addedCount:0,
17263
17609
  deletedCount:0,
17264
17610
  changedCount:0,
17611
+ movedCount:0,
17265
17612
  unchangedCount: 0
17266
17613
  },
17267
17614
  conflicts: 0
@@ -17332,7 +17679,7 @@ RED.diagnostics = (function () {
17332
17679
  $(this).parent().toggleClass('collapsed');
17333
17680
  });
17334
17681
 
17335
- createNodePropertiesTable(def,tab,localTabNode,remoteTabNode,conflicts).appendTo(div);
17682
+ createNodePropertiesTable(def,tab,localTabNode,remoteTabNode).appendTo(div);
17336
17683
  selectState = "";
17337
17684
  if (conflicts[tab.id]) {
17338
17685
  flowStats.conflicts++;
@@ -17402,19 +17749,26 @@ RED.diagnostics = (function () {
17402
17749
  var localStats = $('<span>',{class:"red-ui-diff-list-flow-stats"}).appendTo(localCell);
17403
17750
  $('<span class="red-ui-diff-status"></span>').text(RED._('diff.nodeCount',{count:localNodeCount})).appendTo(localStats);
17404
17751
 
17405
- if (flowStats.conflicts + flowStats.local.addedCount + flowStats.local.changedCount + flowStats.local.deletedCount > 0) {
17752
+ if (flowStats.conflicts + flowStats.local.addedCount + flowStats.local.changedCount + flowStats.local.movedCount + flowStats.local.deletedCount > 0) {
17406
17753
  $('<span class="red-ui-diff-status"> [ </span>').appendTo(localStats);
17407
17754
  if (flowStats.conflicts > 0) {
17408
17755
  $('<span class="red-ui-diff-status-conflict"><span class="red-ui-diff-status"><i class="fa fa-exclamation"></i> '+flowStats.conflicts+'</span></span>').appendTo(localStats);
17409
17756
  }
17410
17757
  if (flowStats.local.addedCount > 0) {
17411
- $('<span class="red-ui-diff-status-added"><span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.local.addedCount+'</span></span>').appendTo(localStats);
17758
+ const cell = $('<span class="red-ui-diff-status-added"><span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.local.addedCount+'</span></span>').appendTo(localStats);
17759
+ RED.popover.tooltip(cell, RED._('diff.type.added'))
17412
17760
  }
17413
17761
  if (flowStats.local.changedCount > 0) {
17414
- $('<span class="red-ui-diff-status-changed"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.local.changedCount+'</span></span>').appendTo(localStats);
17762
+ const cell = $('<span class="red-ui-diff-status-changed"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.local.changedCount+'</span></span>').appendTo(localStats);
17763
+ RED.popover.tooltip(cell, RED._('diff.type.changed'))
17764
+ }
17765
+ if (flowStats.local.movedCount > 0) {
17766
+ const cell = $('<span class="red-ui-diff-status-moved"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.local.movedCount+'</span></span>').appendTo(localStats);
17767
+ RED.popover.tooltip(cell, RED._('diff.type.moved'))
17415
17768
  }
17416
17769
  if (flowStats.local.deletedCount > 0) {
17417
- $('<span class="red-ui-diff-status-deleted"><span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.local.deletedCount+'</span></span>').appendTo(localStats);
17770
+ const cell = $('<span class="red-ui-diff-status-deleted"><span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.local.deletedCount+'</span></span>').appendTo(localStats);
17771
+ RED.popover.tooltip(cell, RED._('diff.type.deleted'))
17418
17772
  }
17419
17773
  $('<span class="red-ui-diff-status"> ] </span>').appendTo(localStats);
17420
17774
  }
@@ -17440,19 +17794,26 @@ RED.diagnostics = (function () {
17440
17794
  }
17441
17795
  var remoteStats = $('<span>',{class:"red-ui-diff-list-flow-stats"}).appendTo(remoteCell);
17442
17796
  $('<span class="red-ui-diff-status"></span>').text(RED._('diff.nodeCount',{count:remoteNodeCount})).appendTo(remoteStats);
17443
- if (flowStats.conflicts + flowStats.remote.addedCount + flowStats.remote.changedCount + flowStats.remote.deletedCount > 0) {
17797
+ if (flowStats.conflicts + flowStats.remote.addedCount + flowStats.remote.changedCount + flowStats.remote.movedCount + flowStats.remote.deletedCount > 0) {
17444
17798
  $('<span class="red-ui-diff-status"> [ </span>').appendTo(remoteStats);
17445
17799
  if (flowStats.conflicts > 0) {
17446
17800
  $('<span class="red-ui-diff-status-conflict"><span class="red-ui-diff-status"><i class="fa fa-exclamation"></i> '+flowStats.conflicts+'</span></span>').appendTo(remoteStats);
17447
17801
  }
17448
17802
  if (flowStats.remote.addedCount > 0) {
17449
- $('<span class="red-ui-diff-status-added"><span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.remote.addedCount+'</span></span>').appendTo(remoteStats);
17803
+ const cell = $('<span class="red-ui-diff-status-added"><span class="red-ui-diff-status"><i class="fa fa-plus-square"></i> '+flowStats.remote.addedCount+'</span></span>').appendTo(remoteStats);
17804
+ RED.popover.tooltip(cell, RED._('diff.type.added'))
17450
17805
  }
17451
17806
  if (flowStats.remote.changedCount > 0) {
17452
- $('<span class="red-ui-diff-status-changed"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.remote.changedCount+'</span></span>').appendTo(remoteStats);
17807
+ const cell = $('<span class="red-ui-diff-status-changed"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.remote.changedCount+'</span></span>').appendTo(remoteStats);
17808
+ RED.popover.tooltip(cell, RED._('diff.type.changed'))
17809
+ }
17810
+ if (flowStats.remote.movedCount > 0) {
17811
+ const cell = $('<span class="red-ui-diff-status-moved"><span class="red-ui-diff-status"><i class="fa fa-square"></i> '+flowStats.remote.movedCount+'</span></span>').appendTo(remoteStats);
17812
+ RED.popover.tooltip(cell, RED._('diff.type.moved'))
17453
17813
  }
17454
17814
  if (flowStats.remote.deletedCount > 0) {
17455
- $('<span class="red-ui-diff-status-deleted"><span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.remote.deletedCount+'</span></span>').appendTo(remoteStats);
17815
+ const cell = $('<span class="red-ui-diff-status-deleted"><span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> '+flowStats.remote.deletedCount+'</span></span>').appendTo(remoteStats);
17816
+ RED.popover.tooltip(cell, RED._('diff.type.deleted'))
17456
17817
  }
17457
17818
  $('<span class="red-ui-diff-status"> ] </span>').appendTo(remoteStats);
17458
17819
  }
@@ -17487,7 +17848,7 @@ RED.diagnostics = (function () {
17487
17848
  if (options.mode === "merge") {
17488
17849
  diffPanel.addClass("red-ui-diff-panel-merge");
17489
17850
  }
17490
- var diffList = createDiffTable(diffPanel, diff);
17851
+ var diffList = createDiffTable(diffPanel, diff, options);
17491
17852
 
17492
17853
  var localDiff = diff.localDiff;
17493
17854
  var remoteDiff = diff.remoteDiff;
@@ -17710,7 +18071,6 @@ RED.diagnostics = (function () {
17710
18071
 
17711
18072
  var hasChanges = false; // exists in original and local/remote but with changes
17712
18073
  var unChanged = true; // existing in original,local,remote unchanged
17713
- var localChanged = false;
17714
18074
 
17715
18075
  if (localDiff.added[node.id]) {
17716
18076
  stats.local.addedCount++;
@@ -17729,12 +18089,20 @@ RED.diagnostics = (function () {
17729
18089
  unChanged = false;
17730
18090
  }
17731
18091
  if (localDiff.changed[node.id]) {
17732
- stats.local.changedCount++;
18092
+ if (localDiff.positionChanged[node.id]) {
18093
+ stats.local.movedCount++
18094
+ } else {
18095
+ stats.local.changedCount++;
18096
+ }
17733
18097
  hasChanges = true;
17734
18098
  unChanged = false;
17735
18099
  }
17736
18100
  if (remoteDiff && remoteDiff.changed[node.id]) {
17737
- stats.remote.changedCount++;
18101
+ if (remoteDiff.positionChanged[node.id]) {
18102
+ stats.remote.movedCount++
18103
+ } else {
18104
+ stats.remote.changedCount++;
18105
+ }
17738
18106
  hasChanges = true;
17739
18107
  unChanged = false;
17740
18108
  }
@@ -17799,27 +18167,32 @@ RED.diagnostics = (function () {
17799
18167
  localNodeDiv.addClass("red-ui-diff-status-moved");
17800
18168
  var localMovedMessage = "";
17801
18169
  if (node.z === localN.z) {
17802
- localMovedMessage = RED._("diff.type.movedFrom",{id:(localDiff.currentConfig.all[node.id].z||'global')});
18170
+ const movedFromNodeTab = localDiff.currentConfig.all[localDiff.currentConfig.all[node.id].z]
18171
+ const movedFromLabel = `'${movedFromNodeTab ? (movedFromNodeTab.label || movedFromNodeTab.id) : 'global'}'`
18172
+ localMovedMessage = RED._("diff.type.movedFrom",{id: movedFromLabel});
17803
18173
  } else {
17804
- localMovedMessage = RED._("diff.type.movedTo",{id:(localN.z||'global')});
18174
+ const movedToNodeTab = localDiff.newConfig.all[localN.z]
18175
+ const movedToLabel = `'${movedToNodeTab ? (movedToNodeTab.label || movedToNodeTab.id) : 'global'}'`
18176
+ localMovedMessage = RED._("diff.type.movedTo",{id:movedToLabel});
17805
18177
  }
17806
18178
  $('<span class="red-ui-diff-status"><i class="fa fa-caret-square-o-right"></i> '+localMovedMessage+'</span>').appendTo(localNodeDiv);
17807
18179
  }
17808
- localChanged = true;
17809
18180
  } else if (localDiff.deleted[node.z]) {
17810
18181
  localNodeDiv.addClass("red-ui-diff-empty");
17811
- localChanged = true;
17812
18182
  } else if (localDiff.deleted[node.id]) {
17813
18183
  localNodeDiv.addClass("red-ui-diff-status-deleted");
17814
18184
  $('<span class="red-ui-diff-status"><i class="fa fa-minus-square"></i> <span data-i18n="diff.type.deleted"></span></span>').appendTo(localNodeDiv);
17815
- localChanged = true;
17816
18185
  } else if (localDiff.changed[node.id]) {
17817
18186
  if (localDiff.newConfig.all[node.id].z !== node.z) {
17818
18187
  localNodeDiv.addClass("red-ui-diff-empty");
17819
18188
  } else {
17820
- localNodeDiv.addClass("red-ui-diff-status-changed");
17821
- $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(localNodeDiv);
17822
- localChanged = true;
18189
+ if (localDiff.positionChanged[node.id]) {
18190
+ localNodeDiv.addClass("red-ui-diff-status-moved");
18191
+ $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.moved"></span></span>').appendTo(localNodeDiv);
18192
+ } else {
18193
+ localNodeDiv.addClass("red-ui-diff-status-changed");
18194
+ $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(localNodeDiv);
18195
+ }
17823
18196
  }
17824
18197
  } else {
17825
18198
  if (localDiff.newConfig.all[node.id].z !== node.z) {
@@ -17840,9 +18213,13 @@ RED.diagnostics = (function () {
17840
18213
  remoteNodeDiv.addClass("red-ui-diff-status-moved");
17841
18214
  var remoteMovedMessage = "";
17842
18215
  if (node.z === remoteN.z) {
17843
- remoteMovedMessage = RED._("diff.type.movedFrom",{id:(remoteDiff.currentConfig.all[node.id].z||'global')});
18216
+ const movedFromNodeTab = remoteDiff.currentConfig.all[remoteDiff.currentConfig.all[node.id].z]
18217
+ const movedFromLabel = `'${movedFromNodeTab ? (movedFromNodeTab.label || movedFromNodeTab.id) : 'global'}'`
18218
+ remoteMovedMessage = RED._("diff.type.movedFrom",{id:movedFromLabel});
17844
18219
  } else {
17845
- remoteMovedMessage = RED._("diff.type.movedTo",{id:(remoteN.z||'global')});
18220
+ const movedToNodeTab = remoteDiff.newConfig.all[remoteN.z]
18221
+ const movedToLabel = `'${movedToNodeTab ? (movedToNodeTab.label || movedToNodeTab.id) : 'global'}'`
18222
+ remoteMovedMessage = RED._("diff.type.movedTo",{id:movedToLabel});
17846
18223
  }
17847
18224
  $('<span class="red-ui-diff-status"><i class="fa fa-caret-square-o-right"></i> '+remoteMovedMessage+'</span>').appendTo(remoteNodeDiv);
17848
18225
  }
@@ -17855,8 +18232,13 @@ RED.diagnostics = (function () {
17855
18232
  if (remoteDiff.newConfig.all[node.id].z !== node.z) {
17856
18233
  remoteNodeDiv.addClass("red-ui-diff-empty");
17857
18234
  } else {
17858
- remoteNodeDiv.addClass("red-ui-diff-status-changed");
17859
- $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(remoteNodeDiv);
18235
+ if (remoteDiff.positionChanged[node.id]) {
18236
+ remoteNodeDiv.addClass("red-ui-diff-status-moved");
18237
+ $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.moved"></span></span>').appendTo(remoteNodeDiv);
18238
+ } else {
18239
+ remoteNodeDiv.addClass("red-ui-diff-status-changed");
18240
+ $('<span class="red-ui-diff-status"><i class="fa fa-square"></i> <span data-i18n="diff.type.changed"></span></span>').appendTo(remoteNodeDiv);
18241
+ }
17860
18242
  }
17861
18243
  } else {
17862
18244
  if (remoteDiff.newConfig.all[node.id].z !== node.z) {
@@ -17982,7 +18364,7 @@ RED.diagnostics = (function () {
17982
18364
  $("<td>",{class:"red-ui-diff-list-cell-label"}).text("position").appendTo(row);
17983
18365
  localCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-local"}).appendTo(row);
17984
18366
  if (localNode) {
17985
- localCell.addClass("red-ui-diff-status-"+(localChanged?"changed":"unchanged"));
18367
+ localCell.addClass("red-ui-diff-status-"+(localChanged?"moved":"unchanged"));
17986
18368
  $('<span class="red-ui-diff-status">'+(localChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(localCell);
17987
18369
  element = $('<span class="red-ui-diff-list-element"></span>').appendTo(localCell);
17988
18370
  var localPosition = {x:localNode.x,y:localNode.y};
@@ -18007,7 +18389,7 @@ RED.diagnostics = (function () {
18007
18389
 
18008
18390
  if (remoteNode !== undefined) {
18009
18391
  remoteCell = $("<td>",{class:"red-ui-diff-list-cell red-ui-diff-list-node-remote"}).appendTo(row);
18010
- remoteCell.addClass("red-ui-diff-status-"+(remoteChanged?"changed":"unchanged"));
18392
+ remoteCell.addClass("red-ui-diff-status-"+(remoteChanged?"moved":"unchanged"));
18011
18393
  if (remoteNode) {
18012
18394
  $('<span class="red-ui-diff-status">'+(remoteChanged?'<i class="fa fa-square"></i>':'')+'</span>').appendTo(remoteCell);
18013
18395
  element = $('<span class="red-ui-diff-list-element"></span>').appendTo(remoteCell);
@@ -18293,11 +18675,11 @@ RED.diagnostics = (function () {
18293
18675
  // var diff = generateDiff(originalFlow,nns);
18294
18676
  // showDiff(diff);
18295
18677
  // }
18296
- function showRemoteDiff(diff) {
18297
- if (diff === undefined) {
18298
- getRemoteDiff(showRemoteDiff);
18678
+ function showRemoteDiff(diff, options = {}) {
18679
+ if (!diff) {
18680
+ getRemoteDiff((remoteDiff) => showRemoteDiff(remoteDiff, options));
18299
18681
  } else {
18300
- showDiff(diff,{mode:'merge'});
18682
+ showDiff(diff,{...options, mode:'merge'});
18301
18683
  }
18302
18684
  }
18303
18685
  function parseNodes(nodeList) {
@@ -18338,23 +18720,53 @@ RED.diagnostics = (function () {
18338
18720
  }
18339
18721
  }
18340
18722
  function generateDiff(currentNodes,newNodes) {
18341
- var currentConfig = parseNodes(currentNodes);
18342
- var newConfig = parseNodes(newNodes);
18343
- var added = {};
18344
- var deleted = {};
18345
- var changed = {};
18346
- var moved = {};
18723
+ const currentConfig = parseNodes(currentNodes);
18724
+ const newConfig = parseNodes(newNodes);
18725
+ const added = {};
18726
+ const deleted = {};
18727
+ const changed = {};
18728
+ const positionChanged = {};
18729
+ const moved = {};
18347
18730
 
18348
18731
  Object.keys(currentConfig.all).forEach(function(id) {
18349
- var node = RED.nodes.workspace(id)||RED.nodes.subflow(id)||RED.nodes.node(id);
18732
+ const node = RED.nodes.workspace(id)||RED.nodes.subflow(id)||RED.nodes.node(id);
18350
18733
  if (!newConfig.all.hasOwnProperty(id)) {
18351
18734
  deleted[id] = true;
18352
- } else if (JSON.stringify(currentConfig.all[id]) !== JSON.stringify(newConfig.all[id])) {
18735
+ return
18736
+ }
18737
+ const currentConfigJSON = JSON.stringify(currentConfig.all[id])
18738
+ const newConfigJSON = JSON.stringify(newConfig.all[id])
18739
+
18740
+ if (currentConfigJSON !== newConfigJSON) {
18353
18741
  changed[id] = true;
18354
-
18355
18742
  if (currentConfig.all[id].z !== newConfig.all[id].z) {
18356
18743
  moved[id] = true;
18744
+ } else if (
18745
+ currentConfig.all[id].x !== newConfig.all[id].x ||
18746
+ currentConfig.all[id].y !== newConfig.all[id].y ||
18747
+ currentConfig.all[id].w !== newConfig.all[id].w ||
18748
+ currentConfig.all[id].h !== newConfig.all[id].h
18749
+ ) {
18750
+ // This node's position on its parent has changed. We want to
18751
+ // check if this is the *only* change for this given node
18752
+ const currentNodeClone = JSON.parse(currentConfigJSON)
18753
+ const newNodeClone = JSON.parse(newConfigJSON)
18754
+
18755
+ delete currentNodeClone.x
18756
+ delete currentNodeClone.y
18757
+ delete currentNodeClone.w
18758
+ delete currentNodeClone.h
18759
+ delete newNodeClone.x
18760
+ delete newNodeClone.y
18761
+ delete newNodeClone.w
18762
+ delete newNodeClone.h
18763
+
18764
+ if (JSON.stringify(currentNodeClone) === JSON.stringify(newNodeClone)) {
18765
+ // Only the position has changed - everything else is the same
18766
+ positionChanged[id] = true
18767
+ }
18357
18768
  }
18769
+
18358
18770
  }
18359
18771
  });
18360
18772
  Object.keys(newConfig.all).forEach(function(id) {
@@ -18363,13 +18775,14 @@ RED.diagnostics = (function () {
18363
18775
  }
18364
18776
  });
18365
18777
 
18366
- var diff = {
18367
- currentConfig: currentConfig,
18368
- newConfig: newConfig,
18369
- added: added,
18370
- deleted: deleted,
18371
- changed: changed,
18372
- moved: moved
18778
+ const diff = {
18779
+ currentConfig,
18780
+ newConfig,
18781
+ added,
18782
+ deleted,
18783
+ changed,
18784
+ positionChanged,
18785
+ moved
18373
18786
  };
18374
18787
  return diff;
18375
18788
  }
@@ -18434,12 +18847,14 @@ RED.diagnostics = (function () {
18434
18847
  return diff;
18435
18848
  }
18436
18849
 
18437
- function showDiff(diff,options) {
18850
+ function showDiff(diff, options) {
18438
18851
  if (diffVisible) {
18439
18852
  return;
18440
18853
  }
18441
18854
  options = options || {};
18442
18855
  var mode = options.mode || 'merge';
18856
+
18857
+ options.hidePositionChanges = true
18443
18858
 
18444
18859
  var localDiff = diff.localDiff;
18445
18860
  var remoteDiff = diff.remoteDiff;
@@ -18509,6 +18924,9 @@ RED.diagnostics = (function () {
18509
18924
  if (!$("#red-ui-diff-view-diff-merge").hasClass('disabled')) {
18510
18925
  refreshConflictHeader(diff);
18511
18926
  mergeDiff(diff);
18927
+ if (options.onmerge) {
18928
+ options.onmerge()
18929
+ }
18512
18930
  RED.tray.close();
18513
18931
  }
18514
18932
  }
@@ -18539,6 +18957,7 @@ RED.diagnostics = (function () {
18539
18957
  var newConfig = [];
18540
18958
  var node;
18541
18959
  var nodeChangedStates = {};
18960
+ var nodeMovedStates = {};
18542
18961
  var localChangedStates = {};
18543
18962
  for (id in localDiff.newConfig.all) {
18544
18963
  if (localDiff.newConfig.all.hasOwnProperty(id)) {
@@ -18546,12 +18965,14 @@ RED.diagnostics = (function () {
18546
18965
  if (resolutions[id] === 'local') {
18547
18966
  if (node) {
18548
18967
  nodeChangedStates[id] = node.changed;
18968
+ nodeMovedStates[id] = node.moved;
18549
18969
  }
18550
18970
  newConfig.push(localDiff.newConfig.all[id]);
18551
18971
  } else if (resolutions[id] === 'remote') {
18552
18972
  if (!remoteDiff.deleted[id] && remoteDiff.newConfig.all.hasOwnProperty(id)) {
18553
18973
  if (node) {
18554
18974
  nodeChangedStates[id] = node.changed;
18975
+ nodeMovedStates[id] = node.moved;
18555
18976
  }
18556
18977
  localChangedStates[id] = 1;
18557
18978
  newConfig.push(remoteDiff.newConfig.all[id]);
@@ -18575,8 +18996,9 @@ RED.diagnostics = (function () {
18575
18996
  }
18576
18997
  return {
18577
18998
  config: newConfig,
18578
- nodeChangedStates: nodeChangedStates,
18579
- localChangedStates: localChangedStates
18999
+ nodeChangedStates,
19000
+ nodeMovedStates,
19001
+ localChangedStates
18580
19002
  }
18581
19003
  }
18582
19004
 
@@ -18587,6 +19009,7 @@ RED.diagnostics = (function () {
18587
19009
 
18588
19010
  var newConfig = appliedDiff.config;
18589
19011
  var nodeChangedStates = appliedDiff.nodeChangedStates;
19012
+ var nodeMovedStates = appliedDiff.nodeMovedStates;
18590
19013
  var localChangedStates = appliedDiff.localChangedStates;
18591
19014
 
18592
19015
  var isDirty = RED.nodes.dirty();
@@ -18595,33 +19018,56 @@ RED.diagnostics = (function () {
18595
19018
  t:"replace",
18596
19019
  config: RED.nodes.createCompleteNodeSet(),
18597
19020
  changed: nodeChangedStates,
19021
+ moved: nodeMovedStates,
19022
+ complete: true,
18598
19023
  dirty: isDirty,
18599
19024
  rev: RED.nodes.version()
18600
19025
  }
18601
19026
 
18602
19027
  RED.history.push(historyEvent);
18603
19028
 
18604
- var originalFlow = RED.nodes.originalFlow();
18605
- // originalFlow is what the editor things it loaded
18606
- // - add any newly added nodes from remote diff as they are now part of the record
18607
- for (var id in diff.remoteDiff.added) {
18608
- if (diff.remoteDiff.added.hasOwnProperty(id)) {
18609
- if (diff.remoteDiff.newConfig.all.hasOwnProperty(id)) {
18610
- originalFlow.push(JSON.parse(JSON.stringify(diff.remoteDiff.newConfig.all[id])));
18611
- }
18612
- }
18613
- }
19029
+ // var originalFlow = RED.nodes.originalFlow();
19030
+ // // originalFlow is what the editor thinks it loaded
19031
+ // // - add any newly added nodes from remote diff as they are now part of the record
19032
+ // for (var id in diff.remoteDiff.added) {
19033
+ // if (diff.remoteDiff.added.hasOwnProperty(id)) {
19034
+ // if (diff.remoteDiff.newConfig.all.hasOwnProperty(id)) {
19035
+ // originalFlow.push(JSON.parse(JSON.stringify(diff.remoteDiff.newConfig.all[id])));
19036
+ // }
19037
+ // }
19038
+ // }
18614
19039
 
18615
19040
  RED.nodes.clear();
18616
19041
  var imported = RED.nodes.import(newConfig);
18617
19042
 
18618
- // Restore the original flow so subsequent merge resolutions can properly
18619
- // identify new-vs-old
18620
- RED.nodes.originalFlow(originalFlow);
19043
+ // // Restore the original flow so subsequent merge resolutions can properly
19044
+ // // identify new-vs-old
19045
+ // RED.nodes.originalFlow(originalFlow);
19046
+
19047
+ // Clear all change flags from the import
19048
+ RED.nodes.dirty(false);
19049
+
19050
+ const flowsToLock = new Set()
19051
+ function ensureUnlocked(id) {
19052
+ const flow = id && (RED.nodes.workspace(id) || RED.nodes.subflow(id) || null);
19053
+ const isLocked = flow ? flow.locked : false;
19054
+ if (flow && isLocked) {
19055
+ flow.locked = false;
19056
+ flowsToLock.add(flow)
19057
+ }
19058
+ }
18621
19059
  imported.nodes.forEach(function(n) {
18622
- if (nodeChangedStates[n.id] || localChangedStates[n.id]) {
19060
+ if (nodeChangedStates[n.id]) {
19061
+ ensureUnlocked(n.z)
18623
19062
  n.changed = true;
18624
19063
  }
19064
+ if (nodeMovedStates[n.id]) {
19065
+ ensureUnlocked(n.z)
19066
+ n.moved = true;
19067
+ }
19068
+ })
19069
+ flowsToLock.forEach(flow => {
19070
+ flow.locked = true
18625
19071
  })
18626
19072
 
18627
19073
  RED.nodes.version(diff.remoteDiff.rev);
@@ -20681,11 +21127,17 @@ RED.workspaces = (function() {
20681
21127
  RED.sidebar.config.refresh();
20682
21128
  RED.view.focus();
20683
21129
  },
20684
- onclick: function(tab) {
20685
- if (tab.id !== activeWorkspace) {
20686
- addToViewStack(activeWorkspace);
21130
+ onclick: function(tab, evt) {
21131
+ if(evt.which === 2) {
21132
+ evt.preventDefault();
21133
+ evt.stopPropagation();
21134
+ RED.actions.invoke("core:hide-flow", tab)
21135
+ } else {
21136
+ if (tab.id !== activeWorkspace) {
21137
+ addToViewStack(activeWorkspace);
21138
+ }
21139
+ RED.view.focus();
20687
21140
  }
20688
- RED.view.focus();
20689
21141
  },
20690
21142
  ondblclick: function(tab) {
20691
21143
  if (tab.type != "subflow") {
@@ -20723,6 +21175,7 @@ RED.workspaces = (function() {
20723
21175
  if (tab.type === "tab") {
20724
21176
  workspaceTabCount--;
20725
21177
  } else {
21178
+ RED.events.emit("workspace:close",{workspace: tab.id})
20726
21179
  hideStack.push(tab.id);
20727
21180
  }
20728
21181
  RED.menu.setDisabled("menu-item-workspace-delete",activeWorkspace === 0 || workspaceTabCount <= 1);
@@ -21938,120 +22391,128 @@ RED.view = (function() {
21938
22391
  }
21939
22392
  d3.event = event;
21940
22393
  var selected_tool = $(ui.draggable[0]).attr("data-palette-type");
21941
- var result = createNode(selected_tool);
21942
- if (!result) {
21943
- return;
21944
- }
21945
- var historyEvent = result.historyEvent;
21946
- var nn = RED.nodes.add(result.node);
22394
+ try {
22395
+ var result = createNode(selected_tool);
22396
+ if (!result) {
22397
+ return;
22398
+ }
22399
+ var historyEvent = result.historyEvent;
22400
+ var nn = RED.nodes.add(result.node);
21947
22401
 
21948
- var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label");
21949
- if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) {
21950
- nn.l = showLabel;
21951
- }
22402
+ var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label");
22403
+ if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) {
22404
+ nn.l = showLabel;
22405
+ }
21952
22406
 
21953
- var helperOffset = d3.touches(ui.helper.get(0))[0]||d3.mouse(ui.helper.get(0));
21954
- var helperWidth = ui.helper.width();
21955
- var helperHeight = ui.helper.height();
21956
- var mousePos = d3.touches(this)[0]||d3.mouse(this);
22407
+ var helperOffset = d3.touches(ui.helper.get(0))[0]||d3.mouse(ui.helper.get(0));
22408
+ var helperWidth = ui.helper.width();
22409
+ var helperHeight = ui.helper.height();
22410
+ var mousePos = d3.touches(this)[0]||d3.mouse(this);
21957
22411
 
21958
- try {
21959
- var isLink = (nn.type === "link in" || nn.type === "link out")
21960
- var hideLabel = nn.hasOwnProperty('l')?!nn.l : isLink;
21961
-
21962
- var label = RED.utils.getNodeLabel(nn, nn.type);
21963
- var labelParts = getLabelParts(label, "red-ui-flow-node-label");
21964
- if (hideLabel) {
21965
- nn.w = node_height;
21966
- nn.h = Math.max(node_height,(nn.outputs || 0) * 15);
21967
- } else {
21968
- nn.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(nn._def.inputs>0?7:0))/20)) );
21969
- nn.h = Math.max(6+24*labelParts.lines.length,(nn.outputs || 0) * 15, 30);
21970
- }
21971
- } catch(err) {
21972
- }
22412
+ try {
22413
+ var isLink = (nn.type === "link in" || nn.type === "link out")
22414
+ var hideLabel = nn.hasOwnProperty('l')?!nn.l : isLink;
21973
22415
 
21974
- mousePos[1] += this.scrollTop + ((helperHeight/2)-helperOffset[1]);
21975
- mousePos[0] += this.scrollLeft + ((helperWidth/2)-helperOffset[0]);
21976
- mousePos[1] /= scaleFactor;
21977
- mousePos[0] /= scaleFactor;
22416
+ var label = RED.utils.getNodeLabel(nn, nn.type);
22417
+ var labelParts = getLabelParts(label, "red-ui-flow-node-label");
22418
+ if (hideLabel) {
22419
+ nn.w = node_height;
22420
+ nn.h = Math.max(node_height,(nn.outputs || 0) * 15);
22421
+ } else {
22422
+ nn.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(nn._def.inputs>0?7:0))/20)) );
22423
+ nn.h = Math.max(6+24*labelParts.lines.length,(nn.outputs || 0) * 15, 30);
22424
+ }
22425
+ } catch(err) {
22426
+ }
21978
22427
 
21979
- nn.x = mousePos[0];
21980
- nn.y = mousePos[1];
22428
+ mousePos[1] += this.scrollTop + ((helperHeight/2)-helperOffset[1]);
22429
+ mousePos[0] += this.scrollLeft + ((helperWidth/2)-helperOffset[0]);
22430
+ mousePos[1] /= scaleFactor;
22431
+ mousePos[0] /= scaleFactor;
21981
22432
 
21982
- var minX = nn.w/2 -5;
21983
- if (nn.x < minX) {
21984
- nn.x = minX;
21985
- }
21986
- var minY = nn.h/2 -5;
21987
- if (nn.y < minY) {
21988
- nn.y = minY;
21989
- }
21990
- var maxX = space_width -nn.w/2 +5;
21991
- if (nn.x > maxX) {
21992
- nn.x = maxX;
21993
- }
21994
- var maxY = space_height -nn.h +5;
21995
- if (nn.y > maxY) {
21996
- nn.y = maxY;
21997
- }
22433
+ nn.x = mousePos[0];
22434
+ nn.y = mousePos[1];
21998
22435
 
21999
- if (snapGrid) {
22000
- var gridOffset = RED.view.tools.calculateGridSnapOffsets(nn);
22001
- nn.x -= gridOffset.x;
22002
- nn.y -= gridOffset.y;
22003
- }
22436
+ var minX = nn.w/2 -5;
22437
+ if (nn.x < minX) {
22438
+ nn.x = minX;
22439
+ }
22440
+ var minY = nn.h/2 -5;
22441
+ if (nn.y < minY) {
22442
+ nn.y = minY;
22443
+ }
22444
+ var maxX = space_width -nn.w/2 +5;
22445
+ if (nn.x > maxX) {
22446
+ nn.x = maxX;
22447
+ }
22448
+ var maxY = space_height -nn.h +5;
22449
+ if (nn.y > maxY) {
22450
+ nn.y = maxY;
22451
+ }
22004
22452
 
22005
- var linkToSplice = $(ui.helper).data("splice");
22006
- if (linkToSplice) {
22007
- spliceLink(linkToSplice, nn, historyEvent)
22008
- }
22453
+ if (snapGrid) {
22454
+ var gridOffset = RED.view.tools.calculateGridSnapOffsets(nn);
22455
+ nn.x -= gridOffset.x;
22456
+ nn.y -= gridOffset.y;
22457
+ }
22009
22458
 
22010
- var group = $(ui.helper).data("group");
22011
- if (group) {
22012
- var oldX = group.x;
22013
- var oldY = group.y;
22014
- RED.group.addToGroup(group, nn);
22015
- var moveEvent = null;
22016
- if ((group.x !== oldX) ||
22017
- (group.y !== oldY)) {
22018
- moveEvent = {
22019
- t: "move",
22020
- nodes: [{n: group,
22021
- ox: oldX, oy: oldY,
22022
- dx: group.x -oldX,
22023
- dy: group.y -oldY}],
22024
- dirty: true
22025
- };
22459
+ var linkToSplice = $(ui.helper).data("splice");
22460
+ if (linkToSplice) {
22461
+ spliceLink(linkToSplice, nn, historyEvent)
22026
22462
  }
22027
- historyEvent = {
22028
- t: 'multi',
22029
- events: [historyEvent],
22030
22463
 
22031
- };
22032
- if (moveEvent) {
22033
- historyEvent.events.push(moveEvent)
22464
+ var group = $(ui.helper).data("group");
22465
+ if (group) {
22466
+ var oldX = group.x;
22467
+ var oldY = group.y;
22468
+ RED.group.addToGroup(group, nn);
22469
+ var moveEvent = null;
22470
+ if ((group.x !== oldX) ||
22471
+ (group.y !== oldY)) {
22472
+ moveEvent = {
22473
+ t: "move",
22474
+ nodes: [{n: group,
22475
+ ox: oldX, oy: oldY,
22476
+ dx: group.x -oldX,
22477
+ dy: group.y -oldY}],
22478
+ dirty: true
22479
+ };
22480
+ }
22481
+ historyEvent = {
22482
+ t: 'multi',
22483
+ events: [historyEvent],
22484
+
22485
+ };
22486
+ if (moveEvent) {
22487
+ historyEvent.events.push(moveEvent)
22488
+ }
22489
+ historyEvent.events.push({
22490
+ t: "addToGroup",
22491
+ group: group,
22492
+ nodes: nn
22493
+ })
22034
22494
  }
22035
- historyEvent.events.push({
22036
- t: "addToGroup",
22037
- group: group,
22038
- nodes: nn
22039
- })
22040
- }
22041
22495
 
22042
- RED.history.push(historyEvent);
22043
- RED.editor.validateNode(nn);
22044
- RED.nodes.dirty(true);
22045
- // auto select dropped node - so info shows (if visible)
22046
- clearSelection();
22047
- nn.selected = true;
22048
- movingSet.add(nn);
22049
- updateActiveNodes();
22050
- updateSelection();
22051
- redraw();
22496
+ RED.history.push(historyEvent);
22497
+ RED.editor.validateNode(nn);
22498
+ RED.nodes.dirty(true);
22499
+ // auto select dropped node - so info shows (if visible)
22500
+ clearSelection();
22501
+ nn.selected = true;
22502
+ movingSet.add(nn);
22503
+ updateActiveNodes();
22504
+ updateSelection();
22505
+ redraw();
22052
22506
 
22053
- if (nn._def.autoedit) {
22054
- RED.editor.edit(nn);
22507
+ if (nn._def.autoedit) {
22508
+ RED.editor.edit(nn);
22509
+ }
22510
+ } catch (error) {
22511
+ if (error.code != "NODE_RED") {
22512
+ RED.notify(RED._("notification.error",{message:error.toString()}),"error");
22513
+ } else {
22514
+ RED.notify(RED._("notification.error",{message:error.message}),"error");
22515
+ }
22055
22516
  }
22056
22517
  }
22057
22518
  });
@@ -27355,14 +27816,19 @@ RED.view = (function() {
27355
27816
  function createNode(type, x, y, z) {
27356
27817
  const wasDirty = RED.nodes.dirty()
27357
27818
  var m = /^subflow:(.+)$/.exec(type);
27358
- var activeSubflow = z ? RED.nodes.subflow(z) : null;
27819
+ var activeSubflow = (z || RED.workspaces.active()) ? RED.nodes.subflow(z || RED.workspaces.active()) : null;
27820
+
27359
27821
  if (activeSubflow && m) {
27360
27822
  var subflowId = m[1];
27823
+ let err
27361
27824
  if (subflowId === activeSubflow.id) {
27362
- throw new Error(RED._("notification.error", { message: RED._("notification.errors.cannotAddSubflowToItself") }))
27825
+ err = new Error(RED._("notification.errors.cannotAddSubflowToItself"))
27826
+ } else if (RED.nodes.subflowContains(m[1], activeSubflow.id)) {
27827
+ err = new Error(RED._("notification.errors.cannotAddCircularReference"))
27363
27828
  }
27364
- if (RED.nodes.subflowContains(m[1], activeSubflow.id)) {
27365
- throw new Error(RED._("notification.error", { message: RED._("notification.errors.cannotAddCircularReference") }))
27829
+ if (err) {
27830
+ err.code = 'NODE_RED'
27831
+ throw err
27366
27832
  }
27367
27833
  }
27368
27834
 
@@ -27751,14 +28217,27 @@ RED.view = (function() {
27751
28217
  addAnnotation(evt.node.__pendingAnnotation__,evt);
27752
28218
  delete evt.node.__pendingAnnotation__;
27753
28219
  }
27754
- var badgeDX = 0;
27755
- var controlDX = 0;
27756
- for (var i=0,l=evt.el.__annotations__.length;i<l;i++) {
27757
- var annotation = evt.el.__annotations__[i];
28220
+ let badgeRDX = 0;
28221
+ let badgeLDX = 0;
28222
+
28223
+ for (let i=0,l=evt.el.__annotations__.length;i<l;i++) {
28224
+ const annotation = evt.el.__annotations__[i];
27758
28225
  if (annotations.hasOwnProperty(annotation.id)) {
27759
- var opts = annotations[annotation.id];
27760
- var showAnnotation = true;
27761
- var isBadge = opts.type === 'badge';
28226
+ const opts = annotations[annotation.id];
28227
+ let showAnnotation = true;
28228
+ const isBadge = opts.type === 'badge';
28229
+ if (opts.refresh !== undefined) {
28230
+ let refreshAnnotation = false
28231
+ if (typeof opts.refresh === "string") {
28232
+ refreshAnnotation = !!evt.node[opts.refresh]
28233
+ delete evt.node[opts.refresh]
28234
+ } else if (typeof opts.refresh === "function") {
28235
+ refreshAnnotation = opts.refresh(evnt.node)
28236
+ }
28237
+ if (refreshAnnotation) {
28238
+ refreshAnnotationElement(annotation.id, annotation.node, annotation.element)
28239
+ }
28240
+ }
27762
28241
  if (opts.show !== undefined) {
27763
28242
  if (typeof opts.show === "string") {
27764
28243
  showAnnotation = !!evt.node[opts.show]
@@ -27771,17 +28250,24 @@ RED.view = (function() {
27771
28250
  }
27772
28251
  if (isBadge) {
27773
28252
  if (showAnnotation) {
27774
- var rect = annotation.element.getBoundingClientRect();
27775
- badgeDX += rect.width;
27776
- annotation.element.setAttribute("transform", "translate("+(evt.node.w-3-badgeDX)+", -8)");
27777
- badgeDX += 4;
27778
- }
27779
- } else {
27780
- if (showAnnotation) {
27781
- var rect = annotation.element.getBoundingClientRect();
27782
- annotation.element.setAttribute("transform", "translate("+(3+controlDX)+", -12)");
27783
- controlDX += rect.width + 4;
28253
+ const rect = annotation.element.getBoundingClientRect();
28254
+ let annotationX
28255
+ if (!opts.align || opts.align === 'right') {
28256
+ annotationX = evt.node.w - 3 - badgeRDX - rect.width
28257
+ badgeRDX += rect.width + 4;
28258
+
28259
+ } else if (opts.align === 'left') {
28260
+ annotationX = 3 + badgeLDX
28261
+ badgeLDX += rect.width + 4;
28262
+ }
28263
+ annotation.element.setAttribute("transform", "translate("+annotationX+", -8)");
27784
28264
  }
28265
+ // } else {
28266
+ // if (showAnnotation) {
28267
+ // var rect = annotation.element.getBoundingClientRect();
28268
+ // annotation.element.setAttribute("transform", "translate("+(3+controlDX)+", -12)");
28269
+ // controlDX += rect.width + 4;
28270
+ // }
27785
28271
  }
27786
28272
  } else {
27787
28273
  annotation.element.parentNode.removeChild(annotation.element);
@@ -27837,15 +28323,25 @@ RED.view = (function() {
27837
28323
  annotationGroup.setAttribute("class",opts.class || "");
27838
28324
  evt.el.__annotations__.push({
27839
28325
  id:id,
28326
+ node: evt.node,
27840
28327
  element: annotationGroup
27841
28328
  });
27842
- var annotation = opts.element(evt.node);
28329
+ refreshAnnotationElement(id, evt.node, annotationGroup)
28330
+ evt.el.appendChild(annotationGroup);
28331
+ }
28332
+
28333
+ function refreshAnnotationElement(id, node, annotationGroup) {
28334
+ const opts = annotations[id];
28335
+ const annotation = opts.element(node);
27843
28336
  if (opts.tooltip) {
27844
- annotation.addEventListener("mouseenter", getAnnotationMouseEnter(annotation,evt.node,opts.tooltip));
28337
+ annotation.addEventListener("mouseenter", getAnnotationMouseEnter(annotation, node, opts.tooltip));
27845
28338
  annotation.addEventListener("mouseleave", annotationMouseLeave);
27846
28339
  }
28340
+ if (annotationGroup.hasChildNodes()) {
28341
+ annotationGroup.removeChild(annotationGroup.firstChild)
28342
+ }
27847
28343
  annotationGroup.appendChild(annotation);
27848
- evt.el.appendChild(annotationGroup);
28344
+
27849
28345
  }
27850
28346
 
27851
28347
 
@@ -33287,7 +33783,7 @@ RED.palette.editor = (function() {
33287
33783
  }).done(function(data,textStatus,xhr) {
33288
33784
  callback();
33289
33785
  }).fail(function(xhr,textStatus,err) {
33290
- callback(xhr);
33786
+ callback(xhr,textStatus,err);
33291
33787
  });
33292
33788
  }
33293
33789
  function removeNodeModule(id,callback) {
@@ -34500,13 +34996,13 @@ RED.palette.editor = (function() {
34500
34996
  });
34501
34997
 
34502
34998
  if (!found_onremove) {
34503
- let removeNotify = RED.notify("Removed plugin " + entry.name + ". Please reload the editor to clear left-overs.",{
34999
+ let removeNotify = RED.notify(RED._("palette.editor.confirm.removePlugin.body",{module:entry.name}),{
34504
35000
  modal: true,
34505
35001
  fixed: true,
34506
35002
  type: 'warning',
34507
35003
  buttons: [
34508
35004
  {
34509
- text: "Understood",
35005
+ text: RED._("palette.editor.confirm.button.understood"),
34510
35006
  class:"primary",
34511
35007
  click: function(e) {
34512
35008
  removeNotify.close();
@@ -34559,9 +35055,28 @@ RED.palette.editor = (function() {
34559
35055
  RED.actions.invoke("core:show-event-log");
34560
35056
  });
34561
35057
  RED.eventLog.startEvent(RED._("palette.editor.confirm.button.install")+" : "+entry.id+" "+entry.version);
34562
- installNodeModule(entry.id,entry.version,entry.pkg_url,function(xhr) {
35058
+ installNodeModule(entry.id,entry.version,entry.pkg_url,function(xhr, textStatus,err) {
34563
35059
  spinner.remove();
34564
- if (xhr) {
35060
+ if (err && xhr.status === 504) {
35061
+ var notification = RED.notify(RED._("palette.editor.errors.installTimeout"), {
35062
+ modal: true,
35063
+ fixed: true,
35064
+ buttons: [
35065
+ {
35066
+ text: RED._("common.label.close"),
35067
+ click: function() {
35068
+ notification.close();
35069
+ }
35070
+ },{
35071
+ text: RED._("eventLog.view"),
35072
+ click: function() {
35073
+ notification.close();
35074
+ RED.actions.invoke("core:show-event-log");
35075
+ }
35076
+ }
35077
+ ]
35078
+ })
35079
+ } else if (xhr) {
34565
35080
  if (xhr.responseJSON) {
34566
35081
  var notification = RED.notify(RED._('palette.editor.errors.installFailed',{module: entry.id,message:xhr.responseJSON.message}),{
34567
35082
  type: 'error',
@@ -36325,8 +36840,8 @@ RED.editor = (function() {
36325
36840
  }
36326
36841
 
36327
36842
  if (!isSameObj(old_env, new_env)) {
36328
- editing_node.env = new_env;
36329
36843
  editState.changes.env = editing_node.env;
36844
+ editing_node.env = new_env;
36330
36845
  editState.changed = true;
36331
36846
  }
36332
36847
 
@@ -41569,7 +42084,7 @@ RED.editor.codeEditor.monaco = (function() {
41569
42084
  _monaco.languages.json.jsonDefaults.setDiagnosticsOptions(diagnosticOptions);
41570
42085
  if(modeConfiguration) { _monaco.languages.json.jsonDefaults.setModeConfiguration(modeConfiguration); }
41571
42086
  } catch (error) {
41572
- console.warn("monaco - Error setting up json options", err)
42087
+ console.warn("monaco - Error setting up json options", error)
41573
42088
  }
41574
42089
  }
41575
42090
 
@@ -41581,7 +42096,7 @@ RED.editor.codeEditor.monaco = (function() {
41581
42096
  if(htmlDefaults) { _monaco.languages.html.htmlDefaults.setOptions(htmlDefaults); }
41582
42097
  if(handlebarDefaults) { _monaco.languages.html.handlebarDefaults.setOptions(handlebarDefaults); }
41583
42098
  } catch (error) {
41584
- console.warn("monaco - Error setting up html options", err)
42099
+ console.warn("monaco - Error setting up html options", error)
41585
42100
  }
41586
42101
  }
41587
42102
 
@@ -41601,7 +42116,7 @@ RED.editor.codeEditor.monaco = (function() {
41601
42116
  if(lessDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(lessDefaults_modeConfiguration); }
41602
42117
  if(scssDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(scssDefaults_modeConfiguration); }
41603
42118
  } catch (error) {
41604
- console.warn("monaco - Error setting up CSS/SCSS/LESS options", err)
42119
+ console.warn("monaco - Error setting up CSS/SCSS/LESS options", error)
41605
42120
  }
41606
42121
  }
41607
42122
 
@@ -45419,12 +45934,12 @@ RED.notifications = (function() {
45419
45934
  if (newType) {
45420
45935
  n.className = "red-ui-notification red-ui-notification-"+newType;
45421
45936
  }
45422
-
45937
+ newTimeout = newOptions.hasOwnProperty('timeout')?newOptions.timeout:timeout
45423
45938
  if (!fixed || newOptions.fixed === false) {
45424
- newTimeout = (newOptions.hasOwnProperty('timeout')?newOptions.timeout:timeout)||5000;
45939
+ newTimeout = newTimeout || 5000
45425
45940
  }
45426
45941
  if (newOptions.buttons) {
45427
- var buttonSet = $('<div style="margin-top: 20px;" class="ui-dialog-buttonset"></div>').appendTo(nn)
45942
+ var buttonSet = $('<div class="ui-dialog-buttonset"></div>').appendTo(nn)
45428
45943
  newOptions.buttons.forEach(function(buttonDef) {
45429
45944
  var b = $('<button>').text(buttonDef.text).on("click", buttonDef.click).appendTo(buttonSet);
45430
45945
  if (buttonDef.id) {
@@ -45470,6 +45985,15 @@ RED.notifications = (function() {
45470
45985
  };
45471
45986
  })());
45472
45987
  n.timeoutid = window.setTimeout(n.close,timeout||5000);
45988
+ } else if (timeout) {
45989
+ $(n).on("click.red-ui-notification-close", (function() {
45990
+ var nn = n;
45991
+ return function() {
45992
+ nn.hideNotification();
45993
+ window.clearTimeout(nn.timeoutid);
45994
+ };
45995
+ })());
45996
+ n.timeoutid = window.setTimeout(n.hideNotification,timeout||5000);
45473
45997
  }
45474
45998
  currentNotifications.push(n);
45475
45999
  if (options.id) {
@@ -48502,7 +49026,7 @@ RED.subflow = (function() {
48502
49026
  break;
48503
49027
  case "conf-types":
48504
49028
  item.value = input.val()
48505
- item.type = data.parent.value;
49029
+ item.type = "conf-type"
48506
49030
  }
48507
49031
  if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) {
48508
49032
  env.push(item);
@@ -52355,7 +52879,7 @@ RED.projects.settings = (function() {
52355
52879
  var notInstalledCount = 0;
52356
52880
 
52357
52881
  for (var m in modulesInUse) {
52358
- if (modulesInUse.hasOwnProperty(m)) {
52882
+ if (modulesInUse.hasOwnProperty(m) && !activeProject.dependencies.hasOwnProperty(m)) {
52359
52883
  depsList.editableList('addItem',{
52360
52884
  id: modulesInUse[m].module,
52361
52885
  version: modulesInUse[m].version,
@@ -52375,8 +52899,8 @@ RED.projects.settings = (function() {
52375
52899
 
52376
52900
  if (activeProject.dependencies) {
52377
52901
  for (var m in activeProject.dependencies) {
52378
- if (activeProject.dependencies.hasOwnProperty(m) && !modulesInUse.hasOwnProperty(m)) {
52379
- var installed = !!RED.nodes.registry.getModule(m);
52902
+ if (activeProject.dependencies.hasOwnProperty(m)) {
52903
+ var installed = !!RED.nodes.registry.getModule(m) && activeProject.dependencies[m] === modulesInUse[m].version;
52380
52904
  depsList.editableList('addItem',{
52381
52905
  id: m,
52382
52906
  version: activeProject.dependencies[m], //RED.nodes.registry.getModule(module).version,