From a587655a5aa82d4ac1b05d54b8c6bed8797377a8 Mon Sep 17 00:00:00 2001 From: Patrick Wozniak Date: Sun, 21 Jan 2024 01:00:02 +0100 Subject: [PATCH 01/71] adding pollyfill for vm.createScript adds support for bun.sh --- .../@node-red/nodes/core/function/10-function.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js index 7f2250008..5ecda1102 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js @@ -22,6 +22,12 @@ module.exports = function(RED) { var acorn = require("acorn"); var acornWalk = require("acorn-walk"); + if (vm.createScript == null) { + vm.createScript = (code, scriptName) => { + return new vm.Script(code, { filename: scriptName }); + } + } + function sendResults(node,send,_msgid,msgs,cloneFirstMessage) { if (msgs == null) { return; From ec062d008f17d934ff9265c9cfa7dac29b56e25c Mon Sep 17 00:00:00 2001 From: Patrick Wozniak Date: Sun, 21 Jan 2024 01:13:00 +0100 Subject: [PATCH 02/71] replace vm.createScript in favor of vm.Script --- .../@node-red/nodes/core/function/10-function.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js index 5ecda1102..cc06ab560 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js @@ -22,12 +22,6 @@ module.exports = function(RED) { var acorn = require("acorn"); var acornWalk = require("acorn-walk"); - if (vm.createScript == null) { - vm.createScript = (code, scriptName) => { - return new vm.Script(code, { filename: scriptName }); - } - } - function sendResults(node,send,_msgid,msgs,cloneFirstMessage) { if (msgs == null) { return; @@ -380,7 +374,7 @@ module.exports = function(RED) { iniOpt.breakOnSigint = true; } } - node.script = vm.createScript(functionText, createVMOpt(node, "")); + node.script = vm.Script(functionText, {filename: createVMOpt(node, "")}); if (node.fin && (node.fin !== "")) { var finText = `(function () { var node = { From f83174c40a74dfa94e07f83d5d309b9b28d8e786 Mon Sep 17 00:00:00 2001 From: Patrick Wozniak Date: Sun, 21 Jan 2024 01:23:07 +0100 Subject: [PATCH 03/71] fix use of vm.Script by adding new --- .../node_modules/@node-red/nodes/core/function/10-function.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js index cc06ab560..bbc8d498e 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js @@ -374,7 +374,7 @@ module.exports = function(RED) { iniOpt.breakOnSigint = true; } } - node.script = vm.Script(functionText, {filename: createVMOpt(node, "")}); + node.script = new vm.Script(functionText, {filename: createVMOpt(node, "")}); if (node.fin && (node.fin !== "")) { var finText = `(function () { var node = { From 28907082f196fdc116f8dc1a13df3d84e56a5244 Mon Sep 17 00:00:00 2001 From: Patrick Wozniak Date: Sun, 21 Jan 2024 02:16:00 +0100 Subject: [PATCH 04/71] fix usage of vm.Script() --- .../node_modules/@node-red/nodes/core/function/10-function.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js index bbc8d498e..f5de3d2a3 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js @@ -374,7 +374,7 @@ module.exports = function(RED) { iniOpt.breakOnSigint = true; } } - node.script = new vm.Script(functionText, {filename: createVMOpt(node, "")}); + node.script = new vm.Script(functionText, createVMOpt(node, "")); if (node.fin && (node.fin !== "")) { var finText = `(function () { var node = { From 65d8872ceab155e3efe720c58798b6ed3fda7441 Mon Sep 17 00:00:00 2001 From: GogoVega <92022724+GogoVega@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:59:49 +0100 Subject: [PATCH 05/71] Separate the "add new config-node" option into a button --- .../editor-client/locales/en-US/editor.json | 2 +- .../editor-client/locales/fr/editor.json | 2 +- .../editor-client/src/js/ui/editor.js | 52 ++++++++++++++----- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index da9bf18a2..2e644ef15 100644 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -442,7 +442,7 @@ "addNewConfig": "Add new __type__ config node", "editNode": "Edit __type__ node", "editConfig": "Edit __type__ config node", - "addNewType": "Add new __type__...", + "addNewType": "Please add new __type__...", "nodeProperties": "node properties", "label": "Label", "color": "Color", diff --git a/packages/node_modules/@node-red/editor-client/locales/fr/editor.json b/packages/node_modules/@node-red/editor-client/locales/fr/editor.json index 6faa1ed24..57bc4ae14 100644 --- a/packages/node_modules/@node-red/editor-client/locales/fr/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/fr/editor.json @@ -442,7 +442,7 @@ "addNewConfig": "Ajouter un nouveau noeud de configuration __type__", "editNode": "Modifier le noeud __type__", "editConfig": "Modifier le noeud de configuration __type__", - "addNewType": "Ajouter un nouveau __type__...", + "addNewType": "Veuillez ajouter un nouveau __type__...", "nodeProperties": "Propriétés du noeud", "label": "Étiquette", "color": "Couleur", diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js index 1b73430dc..7a5433eea 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js @@ -341,8 +341,9 @@ RED.editor = (function() { nodeValue = node[property] } - const buttonId = `${prefix}-lookup-${property}` - const selectId = prefix + '-' + property + const addBtnId = `${prefix}-add-${property}`; + const editBtnId = `${prefix}-lookup-${property}`; + const selectId = prefix + '-' + property; const input = $(`#${selectId}`); if (input.length === 0) { return; @@ -365,40 +366,62 @@ RED.editor = (function() { select.css({ 'flex-grow': 1 }); + updateConfigNodeSelect(property, type, nodeValue, prefix, filter); - const disableButton = function(disabled) { - btn.prop( "disabled", !!disabled) - btn.toggleClass("disabled", !!disabled) - } + // create the edit button - const btn = $('') + const editButton = $('') .css({ "margin-left": "10px" }) .appendTo(outerWrap); + // create the add button + const addButton = $('') + .css({ "margin-left": "10px" }) + .appendTo(outerWrap); + + const disableButton = function(button, disabled) { + $(button).prop("disabled", !!disabled) + $(button).toggleClass("disabled", !!disabled) + }; + // add the click handler - btn.on("click", function (e) { + addButton.on("click", function (e) { + if (addButton.prop("disabled")) { return } + showEditConfigNodeDialog(property, type, "_ADD_", prefix, node); + e.preventDefault(); + }); + editButton.on("click", function (e) { const selectedOpt = select.find(":selected") 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) - if (btn.prop("disabled")) { return } + if (editButton.prop("disabled")) { return } showEditConfigNodeDialog(property, type, selectedOpt.val(), prefix, node); e.preventDefault(); }); // dont permit the user to click the button if the selected option is an env var select.on("change", function () { - const selectedOpt = select.find(":selected") + const selectedOpt = select.find(":selected"); + const optionsLength = select.find("option").length; if (selectedOpt?.data('env')) { - disableButton(true) + disableButton(addButton, true); + disableButton(editButton, true); + // disable the edit button if no options available + } else if (optionsLength === 1 && selectedOpt.val() === "_ADD_") { + disableButton(addButton, false); + disableButton(editButton, true); } else { - disableButton(false) + disableButton(addButton, false); + disableButton(editButton, false); } }); + var label = ""; var configNode = RED.nodes.node(nodeValue); if (configNode) { label = RED.utils.getNodeLabel(configNode, configNode.id); } + input.val(label); } @@ -892,7 +915,10 @@ RED.editor = (function() { } } - select.append(''); + if (!configNodes.length) { + select.append(''); + } + window.setTimeout(function() { select.trigger("change");},50); } } From 014f206e9c8fd08f217985a3a78dc3602210d544 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 27 Mar 2024 17:30:44 +0000 Subject: [PATCH 06/71] Initial multiplayer feature --- Gruntfile.js | 1 + .../@node-red/editor-api/lib/editor/theme.js | 4 + .../editor-client/src/js/multiplayer.js | 217 ++++++++++++++++++ .../@node-red/editor-client/src/js/red.js | 4 + .../editor-client/src/js/ui/common/popover.js | 2 +- .../@node-red/editor-client/src/js/ui/tray.js | 2 + .../editor-client/src/sass/multiplayer.scss | 48 ++++ .../editor-client/src/sass/style.scss | 2 + .../@node-red/runtime/lib/index.js | 2 + .../runtime/lib/multiplayer/index.js | 119 ++++++++++ packages/node_modules/node-red/settings.js | 4 + 11 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 packages/node_modules/@node-red/editor-client/src/js/multiplayer.js create mode 100644 packages/node_modules/@node-red/editor-client/src/sass/multiplayer.scss create mode 100644 packages/node_modules/@node-red/runtime/lib/multiplayer/index.js diff --git a/Gruntfile.js b/Gruntfile.js index 09b057837..b599d0b0f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -143,6 +143,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/user.js", "packages/node_modules/@node-red/editor-client/src/js/comms.js", "packages/node_modules/@node-red/editor-client/src/js/runtime.js", + "packages/node_modules/@node-red/editor-client/src/js/multiplayer.js", "packages/node_modules/@node-red/editor-client/src/js/text/bidi.js", "packages/node_modules/@node-red/editor-client/src/js/text/format.js", "packages/node_modules/@node-red/editor-client/src/js/ui/state.js", diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js index c3808a751..64e4f7ec3 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js @@ -233,6 +233,10 @@ module.exports = { themeSettings.projects = theme.projects; } + if (theme.hasOwnProperty("multiplayer")) { + themeSettings.multiplayer = theme.multiplayer; + } + if (theme.hasOwnProperty("keymap")) { themeSettings.keymap = theme.keymap; } diff --git a/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js b/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js new file mode 100644 index 000000000..926a118a9 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js @@ -0,0 +1,217 @@ +RED.multiplayer = (function () { + + // sessionId - used to identify sessions across websocket reconnects + let sessionId + + let headerWidget + // Map of session id to { session:'', user:{}, location:{}} + let connections = {} + // Map of username to { user:{}, connections:[] } + let users = {} + + function addUserConnection (connection) { + if (connections[connection.session]) { + // This is an existing connection that has been authenticated + const existingConnection = connections[connection.session] + if (existingConnection.user.username !== connection.user.username) { + removeUserButton(users[existingConnection.user.username]) + } + } + connections[connection.session] = connection + const user = users[connection.user.username] = users[connection.user.username] || { + user: connection.user, + connections: [] + } + connection.location = connection.location || {} + user.connections.push(connection) + + if (connection.user.username === RED.settings.user?.username || + connection.session === sessionId + ) { + // This is the current user - do not add a extra button for them + } else { + if (user.connections.length === 1) { + if (user.button) { + clearTimeout(user.inactiveTimeout) + clearTimeout(user.removeTimeout) + user.button.removeClass('inactive') + } else { + addUserButton(user) + } + } + } + } + + function removeUserConnection (session, isDisconnected) { + const connection = connections[session] + delete connections[session] + const user = users[connection.user.username] + const i = user.connections.indexOf(connection) + user.connections.splice(i, 1) + if (isDisconnected) { + removeUserButton(user) + } else { + if (user.connections.length === 0) { + // Give the user 5s to reconnect before marking inactive + user.inactiveTimeout = setTimeout(() => { + user.button.addClass('inactive') + // Give the user further 20 seconds to reconnect before removing them + // from the user toolbar entirely + user.removeTimeout = setTimeout(() => { + removeUserButton(user) + }, 20000) + }, 5000) + } + } + } + + function addUserButton (user) { + user.button = $('
  • ') + .attr('data-username', user.user.username) + .prependTo("#red-ui-multiplayer-user-list"); + var button = user.button.find("button") + button.on('click', function () { + RED.popover.create({ + target:button, + trigger: 'modal', + interactive: true, + width: "250px", + direction: 'bottom', + content: () => { + const content = $('
    ') + $('
    ').text(user.user.username).appendTo(content) + + const location = user.connections[0].location + if (location.workspace) { + const ws = RED.nodes.workspace(location.workspace) || RED.nodes.subflow(location.workspace) + if (ws) { + $('
    ').text(`${ws.type}: ${ws.label||ws.name||ws.id}`).appendTo(content) + } else { + $('
    ').text(`tab: unknown`).appendTo(content) + } + } + if (location.node) { + const node = RED.nodes.node(location.node) + if (node) { + $('
    ').text(`node: ${node.id}`).appendTo(content) + } else { + $('
    ').text(`node: unknown`).appendTo(content) + } + } + return content + }, + }).open() + }) + if (!user.user.image) { + $('').appendTo(button); + } else { + $('').css({ + backgroundImage: "url("+user.user.image+")", + }).appendTo(button); + } + } + + function getLocation () { + const location = { + workspace: RED.workspaces.active() + } + const editStack = RED.editor.getEditStack() + for (let i = editStack.length - 1; i >= 0; i--) { + if (editStack[i].id) { + location.node = editStack[i].id + break + } + } + return location + } + function updateLocation () { + const location = getLocation() + if (location.workspace !== 0) { + log('send', 'multiplayer/location', location) + RED.comms.send('multiplayer/location', location) + } + } + + function removeUserButton (user) { + user.button.remove() + delete user.button + } + + function updateUserLocation (data) { + connections[data.session].location = data + delete data.session + } + return { + init: function () { + + + sessionId = RED.settings.getLocal('multiplayer:sessionId') + if (!sessionId) { + sessionId = RED.nodes.id() + RED.settings.setLocal('multiplayer:sessionId', sessionId) + } + + headerWidget = $('
    • ').prependTo('.red-ui-header-toolbar') + + RED.comms.on('connect', () => { + const location = getLocation() + const connectInfo = { + session: sessionId + } + if (location.workspace !== 0) { + connectInfo.location = location + } + RED.comms.send('multiplayer/connect', connectInfo) + }) + RED.comms.subscribe('multiplayer/#', (topic, msg) => { + log('recv', topic, msg) + if (topic === 'multiplayer/init') { + // We have just reconnected, runtime has sent state to + // initialise the world + connections = {} + users = {} + $('#red-ui-multiplayer-user-list').empty() + + msg.forEach(connection => { + addUserConnection(connection) + }) + } else if (topic === 'multiplayer/connection-added') { + addUserConnection(msg) + } else if (topic === 'multiplayer/connection-removed') { + removeUserConnection(msg.session, msg.disconnected) + } else if (topic === 'multiplayer/location') { + updateUserLocation(msg) + } + }) + + RED.events.on('workspace:change', (event) => { + updateLocation() + }) + RED.events.on('editor:open', () => { + updateLocation() + }) + RED.events.on('editor:close', () => { + updateLocation() + }) + RED.events.on('editor:change', () => { + updateLocation() + }) + RED.events.on('login', () => { + updateLocation() + }) + RED.events.on('logout', () => { + const disconnectInfo = { + session: sessionId + } + RED.comms.send('multiplayer/disconnect', disconnectInfo) + RED.settings.removeLocal('multiplayer:sessionId') + }) + } + } + + function log() { + if (RED.multiplayer.DEBUG) { + console.log('[multiplayer]', ...arguments) + } + } +})(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index d13d7ca24..5bdd5af7d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -803,6 +803,10 @@ var RED = (function() { RED.nodes.init(); RED.runtime.init() + + if (RED.settings.theme("multiplayer.enabled",false)) { + RED.multiplayer.init() + } RED.comms.connect(); $("#red-ui-main-container").show(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js index 9ddd3d866..1a70839ae 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js @@ -211,7 +211,7 @@ RED.popover = (function() { closePopup(true); }); } - if (trigger === 'hover' && options.interactive) { + if (/*trigger === 'hover' && */options.interactive) { div.on('mouseenter', function(e) { clearTimeout(timer); active = true; diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tray.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tray.js index 0ea6d6044..d428d8a00 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tray.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tray.js @@ -264,6 +264,7 @@ setTimeout(function() { oldTray.tray.detach(); showTray(options); + RED.events.emit('editor:change') },250) } else { if (stack.length > 0) { @@ -333,6 +334,7 @@ RED.view.focus(); } else { stack[stack.length-1].tray.css("z-index", "auto"); + RED.events.emit('editor:change') } },250) } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/multiplayer.scss b/packages/node_modules/@node-red/editor-client/src/sass/multiplayer.scss new file mode 100644 index 000000000..58fb9472f --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/sass/multiplayer.scss @@ -0,0 +1,48 @@ +#red-ui-multiplayer-user-list { + display: inline-flex; + align-items: center; + margin: 0 5px; + li { + display: inline-flex; + align-items: center; + width: 30px; + margin: 0 2px; + } + +} + +.red-ui-multiplayer-user-icon { + background: var(--red-ui-header-background); + border: 2px solid var(--red-ui-header-menu-color); + border-radius: 30px; + display: inline-flex; + justify-content: center; + align-items: center; + width: 28px; + height: 28px; + text-align: center; + overflow: hidden; + box-sizing: border-box; + text-decoration: none; + color: var(--red-ui-header-menu-color); + padding: 0px; + margin: 0px; + vertical-align: middle; + + &:focus { + outline: none; + } + + .red-ui-multiplayer-user.inactive & { + opacity: 0.5; + } + .user-profile { + background-position: center center; + background-repeat: no-repeat; + background-size: contain; + display: inline-block; + vertical-align: middle; + width: 28px; + height: 28px; + } +} \ No newline at end of file diff --git a/packages/node_modules/@node-red/editor-client/src/sass/style.scss b/packages/node_modules/@node-red/editor-client/src/sass/style.scss index 412290f78..77148abde 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/style.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/style.scss @@ -73,3 +73,5 @@ @import "radialMenu"; @import "tourGuide"; + +@import "multiplayer"; diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js index 7fc9af041..4ac7cfb5b 100644 --- a/packages/node_modules/@node-red/runtime/lib/index.js +++ b/packages/node_modules/@node-red/runtime/lib/index.js @@ -22,6 +22,7 @@ var storage = require("./storage"); var library = require("./library"); var plugins = require("./plugins"); var settings = require("./settings"); +const multiplayer = require("./multiplayer"); var express = require("express"); var path = require('path'); @@ -135,6 +136,7 @@ function start() { .then(function() { return storage.init(runtime)}) .then(function() { return settings.load(storage)}) .then(function() { return library.init(runtime)}) + .then(function() { return multiplayer.init(runtime)}) .then(function() { if (settings.available()) { if (settings.get('instanceId') === undefined) { diff --git a/packages/node_modules/@node-red/runtime/lib/multiplayer/index.js b/packages/node_modules/@node-red/runtime/lib/multiplayer/index.js new file mode 100644 index 000000000..a4108e51f --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/multiplayer/index.js @@ -0,0 +1,119 @@ +let runtime + +/** + * Active sessions, mapped by multiplayer session ids + */ +const sessions = new Map() + +/** + * Active connections, mapping comms session to multiplayer session + */ +const connections = new Map() + + +function getSessionsList() { + return Array.from(sessions.values()).filter(session => session.active) +} + +module.exports = { + init: function(_runtime) { + runtime = _runtime + runtime.events.on('comms:connection-removed', (opts) => { + const existingSessionId = connections.get(opts.session) + if (existingSessionId) { + connections.delete(opts.session) + const session = sessions.get(existingSessionId) + session.active = false + session.idleTimeout = setTimeout(() => { + sessions.delete(existingSessionId) + }, 30000) + runtime.events.emit('comms', { + topic: "multiplayer/connection-removed", + data: { session: existingSessionId } + }) + } + }) + runtime.events.on('comms:message:multiplayer/connect', (opts) => { + let session + if (!sessions.has(opts.data.session)) { + // Brand new session + let user = opts.user + if (!user || user.anonymous) { + user = user || { anonymous: true } + user.username = `Anon ${Math.floor(Math.random()*100)}` + } + session = { + session: opts.data.session, + user, + active: true + } + sessions.set(opts.data.session, session) + connections.set(opts.session, opts.data.session) + runtime.log.trace(`multiplayer new session:${opts.data.session} user:${user.username}`) + } else { + // Reconnected connection - keep existing state + connections.set(opts.session, opts.data.session) + // const existingConnection = connections.get(opts.data.session) + session = sessions.get(opts.data.session) + session.active = true + runtime.log.trace(`multiplayer reconnected session:${opts.data.session} user:${session.user.username}`) + clearTimeout(session.idleTimeout) + } + // Tell existing sessions about the new connection + runtime.events.emit('comms', { + topic: "multiplayer/connection-added", + excludeSession: opts.session, + data: session + }) + + // Send init info to new connection + const initPacket = { + topic: "multiplayer/init", + data: getSessionsList(), + session: opts.session + } + // console.log('<<', initPacket) + runtime.events.emit('comms', initPacket) + }) + runtime.events.on('comms:message:multiplayer/disconnect', (opts) => { + const existingSessionId = connections.get(opts.session) + connections.delete(opts.session) + sessions.delete(existingSessionId) + + runtime.events.emit('comms', { + topic: "multiplayer/connection-removed", + data: { session: existingSessionId, disconnected: true } + }) + }) + runtime.events.on('comms:message:multiplayer/location', (opts) => { + // console.log('>>>', opts.user, opts.data) + + const sessionId = connections.get(opts.session) + const session = sessions.get(sessionId) + + if (opts.user) { + if (session.user.anonymous !== opts.user.anonymous) { + session.user = opts.user + runtime.events.emit('comms', { + topic: 'multiplayer/connection-added', + excludeSession: opts.session, + data: session + }) + } + } + + session.location = opts.data + + const payload = { + session: sessionId, + workspace: opts.data.workspace, + node: opts.data.node + } + runtime.events.emit('comms', { + topic: 'multiplayer/location', + data: payload, + excludeSession: opts.session + }) + }) + } +} \ No newline at end of file diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 864707538..cb7b58795 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -437,6 +437,10 @@ module.exports = { } }, + multiplayer: { + /** To enable the Multiplayer feature, set this value to true */ + enabled: false + }, }, /******************************************************************************* From a173e8e70f30d8dc2041604bc4267bd87a9b1c9f Mon Sep 17 00:00:00 2001 From: Gauthier Dandele <92022724+GogoVega@users.noreply.github.com> Date: Thu, 28 Mar 2024 12:57:04 +0100 Subject: [PATCH 07/71] Apply suggestions from code review Co-authored-by: Nick O'Leary --- .../@node-red/editor-client/locales/en-US/editor.json | 2 +- .../node_modules/@node-red/editor-client/locales/fr/editor.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 2e644ef15..da9bf18a2 100644 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -442,7 +442,7 @@ "addNewConfig": "Add new __type__ config node", "editNode": "Edit __type__ node", "editConfig": "Edit __type__ config node", - "addNewType": "Please add new __type__...", + "addNewType": "Add new __type__...", "nodeProperties": "node properties", "label": "Label", "color": "Color", diff --git a/packages/node_modules/@node-red/editor-client/locales/fr/editor.json b/packages/node_modules/@node-red/editor-client/locales/fr/editor.json index 57bc4ae14..6faa1ed24 100644 --- a/packages/node_modules/@node-red/editor-client/locales/fr/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/fr/editor.json @@ -442,7 +442,7 @@ "addNewConfig": "Ajouter un nouveau noeud de configuration __type__", "editNode": "Modifier le noeud __type__", "editConfig": "Modifier le noeud de configuration __type__", - "addNewType": "Veuillez ajouter un nouveau __type__...", + "addNewType": "Ajouter un nouveau __type__...", "nodeProperties": "Propriétés du noeud", "label": "Étiquette", "color": "Couleur", From 410b938442d514e87a120da067c7ceead459df79 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 28 Mar 2024 15:02:02 +0000 Subject: [PATCH 08/71] Bump for 3.1.8 --- CHANGELOG.md | 9 +++++++++ package.json | 2 +- .../node_modules/@node-red/editor-api/package.json | 6 +++--- .../node_modules/@node-red/editor-client/package.json | 2 +- packages/node_modules/@node-red/nodes/package.json | 2 +- packages/node_modules/@node-red/registry/package.json | 4 ++-- packages/node_modules/@node-red/runtime/package.json | 6 +++--- packages/node_modules/@node-red/util/package.json | 2 +- packages/node_modules/node-red/package.json | 10 +++++----- 9 files changed, 26 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68ea56f85..cc45650d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +#### 3.1.8: Maintenance Release + + - Add validation and error handling on subflow instance properties (#4632) @knolleary + - Hide import/export context menu if disabled in theme (#4633) @knolleary + - Show change indicator on subflow tabs (#4631) @knolleary + - Bump dependencies (#4630) @knolleary + - Reset workspace index when clearing nodes (#4619) @knolleary + - Remove typo in global config (#4613) @kazuhitoyokoi + #### 3.1.7: Maintenance Release - Add Japanese translation for v3.1.6 (#4603) @kazuhitoyokoi diff --git a/package.json b/package.json index 9876ddaf2..80478388e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "3.1.7", + "version": "3.1.8", "description": "Low-code programming for event-driven applications", "homepage": "https://nodered.org", "license": "Apache-2.0", diff --git a/packages/node_modules/@node-red/editor-api/package.json b/packages/node_modules/@node-red/editor-api/package.json index 9270731a5..5c9b70f2b 100644 --- a/packages/node_modules/@node-red/editor-api/package.json +++ b/packages/node_modules/@node-red/editor-api/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-api", - "version": "3.1.7", + "version": "3.1.8", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,8 +16,8 @@ } ], "dependencies": { - "@node-red/util": "3.1.7", - "@node-red/editor-client": "3.1.7", + "@node-red/util": "3.1.8", + "@node-red/editor-client": "3.1.8", "bcryptjs": "2.4.3", "body-parser": "1.20.2", "clone": "2.1.2", diff --git a/packages/node_modules/@node-red/editor-client/package.json b/packages/node_modules/@node-red/editor-client/package.json index 4012a1148..da7d416f1 100644 --- a/packages/node_modules/@node-red/editor-client/package.json +++ b/packages/node_modules/@node-red/editor-client/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-client", - "version": "3.1.7", + "version": "3.1.8", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/@node-red/nodes/package.json b/packages/node_modules/@node-red/nodes/package.json index b91d21385..3f6031f71 100644 --- a/packages/node_modules/@node-red/nodes/package.json +++ b/packages/node_modules/@node-red/nodes/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/nodes", - "version": "3.1.7", + "version": "3.1.8", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/@node-red/registry/package.json b/packages/node_modules/@node-red/registry/package.json index 60f0e5863..0e25aa515 100644 --- a/packages/node_modules/@node-red/registry/package.json +++ b/packages/node_modules/@node-red/registry/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/registry", - "version": "3.1.7", + "version": "3.1.8", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,7 +16,7 @@ } ], "dependencies": { - "@node-red/util": "3.1.7", + "@node-red/util": "3.1.8", "clone": "2.1.2", "fs-extra": "11.1.1", "semver": "7.5.4", diff --git a/packages/node_modules/@node-red/runtime/package.json b/packages/node_modules/@node-red/runtime/package.json index f5c415805..2b650f5d3 100644 --- a/packages/node_modules/@node-red/runtime/package.json +++ b/packages/node_modules/@node-red/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/runtime", - "version": "3.1.7", + "version": "3.1.8", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,8 +16,8 @@ } ], "dependencies": { - "@node-red/registry": "3.1.7", - "@node-red/util": "3.1.7", + "@node-red/registry": "3.1.8", + "@node-red/util": "3.1.8", "async-mutex": "0.4.0", "clone": "2.1.2", "express": "4.19.2", diff --git a/packages/node_modules/@node-red/util/package.json b/packages/node_modules/@node-red/util/package.json index eadb42586..828564bea 100644 --- a/packages/node_modules/@node-red/util/package.json +++ b/packages/node_modules/@node-red/util/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/util", - "version": "3.1.7", + "version": "3.1.8", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/node-red/package.json b/packages/node_modules/node-red/package.json index 2ad21450e..b4ad0ff83 100644 --- a/packages/node_modules/node-red/package.json +++ b/packages/node_modules/node-red/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "3.1.7", + "version": "3.1.8", "description": "Low-code programming for event-driven applications", "homepage": "https://nodered.org", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "flow" ], "dependencies": { - "@node-red/editor-api": "3.1.7", - "@node-red/runtime": "3.1.7", - "@node-red/util": "3.1.7", - "@node-red/nodes": "3.1.7", + "@node-red/editor-api": "3.1.8", + "@node-red/runtime": "3.1.8", + "@node-red/util": "3.1.8", + "@node-red/nodes": "3.1.8", "basic-auth": "2.0.1", "bcryptjs": "2.4.3", "express": "4.19.2", From 3bd782e62af3ea2f1fb675cdf1187619fc187cd2 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 2 Apr 2024 13:57:19 +0100 Subject: [PATCH 09/71] Fix change node handling of replacing with boolean Fixes #4638 --- .../nodes/core/function/15-change.js | 9 +++-- test/nodes/core/function/15-change_spec.js | 39 ++++++++++++++++--- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/function/15-change.js b/packages/node_modules/@node-red/nodes/core/function/15-change.js index 64ce00b88..d10bf657e 100644 --- a/packages/node_modules/@node-red/nodes/core/function/15-change.js +++ b/packages/node_modules/@node-red/nodes/core/function/15-change.js @@ -233,9 +233,12 @@ module.exports = function(RED) { // only replace if they match exactly RED.util.setMessageProperty(msg,property,value); } else { - // if target is boolean then just replace it - if (rule.tot === "bool") { current = value; } - else { current = current.replace(fromRE,value); } + current = current.replace(fromRE,value); + if (rule.tot === "bool" && current === ""+value) { + // If the target type is boolean, and the replace call has resulted in "true"/"false", + // convert to boolean type (which 'value' already is) + current = value + } RED.util.setMessageProperty(msg,property,current); } } else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') { diff --git a/test/nodes/core/function/15-change_spec.js b/test/nodes/core/function/15-change_spec.js index b23c1994a..1815470fa 100644 --- a/test/nodes/core/function/15-change_spec.js +++ b/test/nodes/core/function/15-change_spec.js @@ -918,7 +918,7 @@ describe('change Node', function() { }); }); - it('changes the value and type of the message property if a complete match', function(done) { + it('changes the value and type of the message property if a complete match - number', function(done) { var flow = [{"id":"changeNode1","type":"change",rules:[{ "t": "change", "p": "payload", "pt": "msg", "from": "123", "fromt": "str", "to": "456", "tot": "num" }],"reg":false,"name":"changeNode","wires":[["helperNode1"]]}, {id:"helperNode1", type:"helper", wires:[]}]; helper.load(changeNode, flow, function() { @@ -938,6 +938,25 @@ describe('change Node', function() { }); }); + it('changes the value and type of the message property if a complete match - boolean', function(done) { + var flow = [{"id":"changeNode1","type":"change",rules:[{ "t": "change", "p": "payload.a", "pt": "msg", "from": "123", "fromt": "str", "to": "true", "tot": "bool" }, { "t": "change", "p": "payload.b", "pt": "msg", "from": "456", "fromt": "str", "to": "false", "tot": "bool" }],"reg":false,"name":"changeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.a.should.equal(true); + msg.payload.b.should.equal(false); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.receive({payload: { a: "123", b: "456" }}); + }); + }); + it('changes the value of a multi-level message property', function(done) { var flow = [{"id":"changeNode1","type":"change","action":"change","property":"foo.bar","from":"Hello","to":"Goodbye","reg":false,"name":"changeNode","wires":[["helperNode1"]]}, {id:"helperNode1", type:"helper", wires:[]}]; @@ -992,21 +1011,29 @@ describe('change Node', function() { }); }); - it('changes the value of the message property based on a regex', function(done) { - var flow = [{"id":"changeNode1","type":"change","action":"change","property":"payload","from":"\\d+","to":"NUMBER","reg":true,"name":"changeNode","wires":[["helperNode1"]]}, - {id:"helperNode1", type:"helper", wires:[]}]; + it.only('changes the value of the message property based on a regex', function(done) { + const flow = [ + {"id":"changeNode1","type":"change",rules:[ + { "t": "change", "p": "payload.a", "pt": "msg", "from": "\\d+", "fromt": "re", "to": "NUMBER", "tot": "str" }, + { "t": "change", "p": "payload.b", "pt": "msg", "from": "on", "fromt": "re", "to": "true", "tot": "bool" }, + { "t": "change", "p": "payload.c", "pt": "msg", "from": "off", "fromt": "re", "to": "false", "tot": "bool" } + ],"reg":false,"name":"changeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]} + ]; helper.load(changeNode, flow, function() { var changeNode1 = helper.getNode("changeNode1"); var helperNode1 = helper.getNode("helperNode1"); helperNode1.on("input", function(msg) { try { - msg.payload.should.equal("Replace all numbers NUMBER and NUMBER"); + msg.payload.a.should.equal("Replace all numbers NUMBER and NUMBER"); + msg.payload.b.should.equal(true) + msg.payload.c.should.equal(false) done(); } catch(err) { done(err); } }); - changeNode1.receive({payload:"Replace all numbers 12 and 14"}); + changeNode1.receive({payload:{ a: "Replace all numbers 12 and 14", b: 'on', c: 'off' } }); }); }); From e9efe493f90307ed85b2380610129e5fbc2feb8e Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 2 Apr 2024 13:59:15 +0100 Subject: [PATCH 10/71] Remove only --- test/nodes/core/function/15-change_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/nodes/core/function/15-change_spec.js b/test/nodes/core/function/15-change_spec.js index 1815470fa..bfdb493c4 100644 --- a/test/nodes/core/function/15-change_spec.js +++ b/test/nodes/core/function/15-change_spec.js @@ -1011,7 +1011,7 @@ describe('change Node', function() { }); }); - it.only('changes the value of the message property based on a regex', function(done) { + it('changes the value of the message property based on a regex', function(done) { const flow = [ {"id":"changeNode1","type":"change",rules:[ { "t": "change", "p": "payload.a", "pt": "msg", "from": "\\d+", "fromt": "re", "to": "NUMBER", "tot": "str" }, From 5dfa47ab6c4dccfd3f25b2204d58352afea700d9 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 2 Apr 2024 15:54:34 +0100 Subject: [PATCH 11/71] Guard refresh of unknown subflow --- .../@node-red/editor-client/src/js/ui/tab-help.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-help.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-help.js index bf66611b1..8bfc5526e 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-help.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-help.js @@ -158,8 +158,10 @@ RED.sidebar.help = (function() { function refreshSubflow(sf) { var item = treeList.treeList('get',"node-type:subflow:"+sf.id); - item.subflowLabel = sf._def.label().toLowerCase(); - item.treeList.replaceElement(getNodeLabel({_def:sf._def,type:sf._def.label()})); + if (item) { + item.subflowLabel = sf._def.label().toLowerCase(); + item.treeList.replaceElement(getNodeLabel({_def:sf._def,type:sf._def.label()})); + } } function hideTOC() { From a0636632a1d9cce6a52bee6dc8b5032995473ad8 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 2 Apr 2024 17:42:19 +0100 Subject: [PATCH 12/71] Fix subflow module sending messages to debug sidebar Fixes #4641 --- packages/node_modules/@node-red/nodes/core/common/21-debug.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/nodes/core/common/21-debug.html b/packages/node_modules/@node-red/nodes/core/common/21-debug.html index 1c33ad848..82cb3bac7 100644 --- a/packages/node_modules/@node-red/nodes/core/common/21-debug.html +++ b/packages/node_modules/@node-red/nodes/core/common/21-debug.html @@ -378,7 +378,7 @@ return { id: id, label: RED.nodes.workspace(id).label } //flow id + name } else { const instanceNode = RED.nodes.node(id) - const pathLabel = (instanceNode.name || RED.nodes.subflow(instanceNode.type.substring(8)).name) + const pathLabel = (instanceNode.name || RED.nodes.subflow(instanceNode.type.substring(8))?.name || instanceNode.type) return { id: id, label: pathLabel } } }) From 66f4008bb873f4c05610d335e91bcb08b7e50da9 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 2 Apr 2024 20:01:48 +0100 Subject: [PATCH 13/71] Fix handling of subflow config-node select type in sf module --- .../@node-red/editor-client/src/js/ui/subflow.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js index 67369b80b..60ae87aee 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js @@ -1280,14 +1280,20 @@ RED.subflow = (function() { var nodePropValue = nodeProp; if (prop.ui && prop.ui.type === "cred") { nodePropType = "cred"; + } else if (prop.ui && prop.ui.type === "conf-types") { + nodePropType = prop.value.type } else { switch(typeof nodeProp) { case "string": nodePropType = "str"; break; case "number": nodePropType = "num"; break; case "boolean": nodePropType = "bool"; nodePropValue = nodeProp?"true":"false"; break; default: - nodePropType = nodeProp.type; - nodePropValue = nodeProp.value; + if (nodeProp) { + nodePropType = nodeProp.type; + nodePropValue = nodeProp.value; + } else { + nodePropType = 'str' + } } } var item = { From affa8ea42bab62359465cf55e0967fd517500e7b Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 4 Apr 2024 16:08:59 +0100 Subject: [PATCH 14/71] Apply suggestions from code review --- .../@node-red/editor-client/src/js/ui/editor.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js index 7a5433eea..9b3c8d731 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js @@ -341,8 +341,8 @@ RED.editor = (function() { nodeValue = node[property] } - const addBtnId = `${prefix}-add-${property}`; - const editBtnId = `${prefix}-lookup-${property}`; + const addBtnId = `${prefix}-btn-${property}-add`; + const editBtnId = `${prefix}-btn-${property}-edit`; const selectId = prefix + '-' + property; const input = $(`#${selectId}`); if (input.length === 0) { @@ -374,10 +374,13 @@ RED.editor = (function() { .css({ "margin-left": "10px" }) .appendTo(outerWrap); + RED.popover.tooltip(editButton, RED._('editor.editConfig', { type })); + // create the add button - const addButton = $('') + const addButton = $('') .css({ "margin-left": "10px" }) .appendTo(outerWrap); + RED.popover.tooltip(addButton, RED._('editor.addNewConfig', { type })); const disableButton = function(button, disabled) { $(button).prop("disabled", !!disabled) @@ -409,6 +412,9 @@ RED.editor = (function() { } else if (optionsLength === 1 && selectedOpt.val() === "_ADD_") { disableButton(addButton, false); disableButton(editButton, true); + } else if (selectedOpt.val() === "") { + disableButton(addButton, false); + disableButton(editButton, true); } else { disableButton(addButton, false); disableButton(editButton, false); @@ -917,6 +923,8 @@ RED.editor = (function() { if (!configNodes.length) { select.append(''); + } else { + select.append(''); } window.setTimeout(function() { select.trigger("change");},50); From 99391431da0437ac7eddea32904e9790fa5f851f Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 4 Apr 2024 16:17:30 +0100 Subject: [PATCH 15/71] Add placeholder tests for multiplayer --- test/unit/@node-red/runtime/lib/multiplayer/index_spec.js | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 test/unit/@node-red/runtime/lib/multiplayer/index_spec.js diff --git a/test/unit/@node-red/runtime/lib/multiplayer/index_spec.js b/test/unit/@node-red/runtime/lib/multiplayer/index_spec.js new file mode 100644 index 000000000..e86a08020 --- /dev/null +++ b/test/unit/@node-red/runtime/lib/multiplayer/index_spec.js @@ -0,0 +1,2 @@ +describe('multiplayer', function() { +}) \ No newline at end of file From ea95552285ea9d88d8b31a6b38c009bdb353f1de Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 4 Apr 2024 18:25:10 +0100 Subject: [PATCH 16/71] Bump for beta.2 --- CHANGELOG.md | 21 +++++++ package.json | 2 +- .../@node-red/editor-api/package.json | 6 +- .../@node-red/editor-client/package.json | 2 +- .../src/tours/images/nr4-config-select.png | Bin 0 -> 9657 bytes .../src/tours/images/nr4-multiplayer.png | Bin 0 -> 37251 bytes .../editor-client/src/tours/welcome.js | 59 ++++++++++++++++-- .../node_modules/@node-red/nodes/package.json | 2 +- .../@node-red/registry/package.json | 4 +- .../@node-red/runtime/package.json | 6 +- .../node_modules/@node-red/util/package.json | 2 +- packages/node_modules/node-red/package.json | 10 +-- 12 files changed, 93 insertions(+), 21 deletions(-) create mode 100644 packages/node_modules/@node-red/editor-client/src/tours/images/nr4-config-select.png create mode 100644 packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 219a42a9d..b778e4b35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +#### 4.0.0-beta.2: Beta Release + +Editor + + - Introduce multiplayer feature (#4629) @knolleary + - Separate the "add new config-node" option into a new (+) button (#4627) @GogoVega + - Retain Palette categories collapsed and filter to localStorage (#4634) @knolleary + - Ensure palette filter reapplies and clear up unknown categories (#4637) @knolleary + - Add support for plugin (only) modules to the palette manager (#4620) @knolleary + - Update monaco to latest and node types to 18 LTS (#4615) @Steve-Mcl + +Runtime + + - Fix handling of subflow config-node select type in sf module (#4643) @knolleary + - Comms API updates (#4628) @knolleary + - Add French translations for 4.0.0-beta.1 (#4621) @GogoVega + - Add Japanese translations for 4.0.0-beta.1 (#4612) @kazuhitoyokoi + +Nodes + - Fix change node handling of replacing with boolean (#4639) @knolleary + #### 4.0.0-beta.1: Beta Release Editor diff --git a/package.json b/package.json index d58680afd..2ea8653d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "4.0.0-beta.1", + "version": "4.0.0-beta.2", "description": "Low-code programming for event-driven applications", "homepage": "https://nodered.org", "license": "Apache-2.0", diff --git a/packages/node_modules/@node-red/editor-api/package.json b/packages/node_modules/@node-red/editor-api/package.json index d6ea03a76..62d26acb3 100644 --- a/packages/node_modules/@node-red/editor-api/package.json +++ b/packages/node_modules/@node-red/editor-api/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-api", - "version": "4.0.0-beta.1", + "version": "4.0.0-beta.2", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,8 +16,8 @@ } ], "dependencies": { - "@node-red/util": "4.0.0-beta.1", - "@node-red/editor-client": "4.0.0-beta.1", + "@node-red/util": "4.0.0-beta.2", + "@node-red/editor-client": "4.0.0-beta.2", "bcryptjs": "2.4.3", "body-parser": "1.20.2", "clone": "2.1.2", diff --git a/packages/node_modules/@node-red/editor-client/package.json b/packages/node_modules/@node-red/editor-client/package.json index c6d49fced..1f6a9fa92 100644 --- a/packages/node_modules/@node-red/editor-client/package.json +++ b/packages/node_modules/@node-red/editor-client/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-client", - "version": "4.0.0-beta.1", + "version": "4.0.0-beta.2", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-config-select.png b/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-config-select.png new file mode 100644 index 0000000000000000000000000000000000000000..f98ffe5ac7af24a8807deed63fc12189a54f93a6 GIT binary patch literal 9657 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 XtzbdtYcZp{_nK4`H05h#Eua1uG~4$| literal 0 HcmV?d00001 diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer.png b/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer.png new file mode 100644 index 0000000000000000000000000000000000000000..0d65ac56b1f9cc409115da7f09f38b0c1bef23dc GIT binary patch literal 37251 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 literal 0 HcmV?d00001 diff --git a/packages/node_modules/@node-red/editor-client/src/tours/welcome.js b/packages/node_modules/@node-red/editor-client/src/tours/welcome.js index e1c682183..63fdb3cd3 100644 --- a/packages/node_modules/@node-red/editor-client/src/tours/welcome.js +++ b/packages/node_modules/@node-red/editor-client/src/tours/welcome.js @@ -1,12 +1,12 @@ export default { - version: "4.0.0-beta.1", + version: "4.0.0-beta.2", steps: [ { titleIcon: "fa fa-map-o", title: { - "en-US": "Welcome to Node-RED 4.0 Beta 1!", - "ja": "Node-RED 4.0 Beta 1へようこそ!", - "fr": "Bienvenue dans Node-RED 4.0 Beta 1!" + "en-US": "Welcome to Node-RED 4.0 Beta 2!", + "ja": "Node-RED 4.0 Beta 2へようこそ!", + "fr": "Bienvenue dans Node-RED 4.0 Beta 2!" }, description: { "en-US": "

      Let's take a moment to discover the new features in this release.

      ", @@ -14,6 +14,57 @@ export default { "fr": "

      Prenons un moment pour découvrir les nouvelles fonctionnalités de cette version.

      " } }, + { + title: { + "en-US": "Multiplayer Mode" + }, + image: 'images/nr4-multiplayer.png', + description: { + "en-US": `

      This release includes the first small steps towards making Node-RED easier + to work with when you have multiple people editing flows at the same time.

      +

      When this feature is enabled, you will now see who else has the editor open and some + basic information on where they are in the editor.

      +

      Check the release post for details on how to enable this feature in your settings file.

      ` + } + }, + { + title: { + "en-US": "Better Configuration Node UX" + }, + image: 'images/nr4-config-select.png', + description: { + "en-US": `

      The Configuration node selection UI has had a small update to have a dedicated 'add' button + next to the select box.

      +

      It's a small change, but should make it easier to work with your config nodes.

      ` + } + }, + { + title: { + "en-US": "Remembering palette state" + }, + description: { + "en-US": `

      The palette now remembers what categories you have hidden between reloads - as well as any + filter you have applied.

      ` + } + }, + { + title: { + "en-US": "Plugins shown in the Palette Manager" + }, + description: { + "en-US": `

      The palette manager now shows any plugin modules you have installed, such as + node-red-debugger. Previously they would only be shown if they plugin include + nodes for the palette.

      ` + } + }, + { + title: { + "en-US": "Thats if for Beta 2!" + }, + description: { + "en-US": `

      Keep clicking through to see what was added in Beta 1

      ` + } + }, { title: { "en-US": "Timestamp formatting options", diff --git a/packages/node_modules/@node-red/nodes/package.json b/packages/node_modules/@node-red/nodes/package.json index 7feff2558..fc1ecdf91 100644 --- a/packages/node_modules/@node-red/nodes/package.json +++ b/packages/node_modules/@node-red/nodes/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/nodes", - "version": "4.0.0-beta.1", + "version": "4.0.0-beta.2", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/@node-red/registry/package.json b/packages/node_modules/@node-red/registry/package.json index a8af19005..1733279af 100644 --- a/packages/node_modules/@node-red/registry/package.json +++ b/packages/node_modules/@node-red/registry/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/registry", - "version": "4.0.0-beta.1", + "version": "4.0.0-beta.2", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,7 +16,7 @@ } ], "dependencies": { - "@node-red/util": "4.0.0-beta.1", + "@node-red/util": "4.0.0-beta.2", "clone": "2.1.2", "fs-extra": "11.1.1", "semver": "7.5.4", diff --git a/packages/node_modules/@node-red/runtime/package.json b/packages/node_modules/@node-red/runtime/package.json index d9d5c5b83..5a3ee8c47 100644 --- a/packages/node_modules/@node-red/runtime/package.json +++ b/packages/node_modules/@node-red/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/runtime", - "version": "4.0.0-beta.1", + "version": "4.0.0-beta.2", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,8 +16,8 @@ } ], "dependencies": { - "@node-red/registry": "4.0.0-beta.1", - "@node-red/util": "4.0.0-beta.1", + "@node-red/registry": "4.0.0-beta.2", + "@node-red/util": "4.0.0-beta.2", "async-mutex": "0.4.0", "clone": "2.1.2", "express": "4.19.2", diff --git a/packages/node_modules/@node-red/util/package.json b/packages/node_modules/@node-red/util/package.json index 4233abf59..6b78e7807 100644 --- a/packages/node_modules/@node-red/util/package.json +++ b/packages/node_modules/@node-red/util/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/util", - "version": "4.0.0-beta.1", + "version": "4.0.0-beta.2", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/node-red/package.json b/packages/node_modules/node-red/package.json index a94184448..19ae837cb 100644 --- a/packages/node_modules/node-red/package.json +++ b/packages/node_modules/node-red/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "4.0.0-beta.1", + "version": "4.0.0-beta.2", "description": "Low-code programming for event-driven applications", "homepage": "https://nodered.org", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "flow" ], "dependencies": { - "@node-red/editor-api": "4.0.0-beta.1", - "@node-red/runtime": "4.0.0-beta.1", - "@node-red/util": "4.0.0-beta.1", - "@node-red/nodes": "4.0.0-beta.1", + "@node-red/editor-api": "4.0.0-beta.2", + "@node-red/runtime": "4.0.0-beta.2", + "@node-red/util": "4.0.0-beta.2", + "@node-red/nodes": "4.0.0-beta.2", "basic-auth": "2.0.1", "bcryptjs": "2.4.3", "express": "4.19.2", From 3075b82792be6f9668376e66fe6cf3fc137902ff Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 5 Apr 2024 11:18:17 +0100 Subject: [PATCH 17/71] Add one more tour image --- .../src/tours/images/nr4-plugins.png | Bin 0 -> 20699 bytes .../editor-client/src/tours/welcome.js | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 packages/node_modules/@node-red/editor-client/src/tours/images/nr4-plugins.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-plugins.png b/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-plugins.png new file mode 100644 index 0000000000000000000000000000000000000000..75c8ec5ea21f7567dfccdab783645d4d43ca0866 GIT binary patch literal 20699 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 palette manager now shows any plugin modules you have installed, such as node-red-debugger. Previously they would only be shown if they plugin include @@ -59,7 +60,7 @@ export default { }, { title: { - "en-US": "Thats if for Beta 2!" + "en-US": "That's if for Beta 2!" }, description: { "en-US": `

      Keep clicking through to see what was added in Beta 1

      ` From bd58431603fb4169c1696f6b97a618b1acae5091 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 11 Apr 2024 14:40:29 +0100 Subject: [PATCH 18/71] Fix use of spawn on windows with cmd files --- .../@node-red/nodes/core/function/90-exec.js | 6 +++++- .../@node-red/registry/lib/externalModules.js | 2 +- .../@node-red/registry/lib/installer.js | 19 +++++++++++-------- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/function/90-exec.js b/packages/node_modules/@node-red/nodes/core/function/90-exec.js index 2ae9947dd..70aec8d2b 100644 --- a/packages/node_modules/@node-red/nodes/core/function/90-exec.js +++ b/packages/node_modules/@node-red/nodes/core/function/90-exec.js @@ -20,6 +20,7 @@ module.exports = function(RED) { var exec = require('child_process').exec; var fs = require('fs'); var isUtf8 = require('is-utf8'); + const isWindows = process.platform === 'win32' function ExecNode(n) { RED.nodes.createNode(this,n); @@ -85,9 +86,12 @@ module.exports = function(RED) { } }); var cmd = arg.shift(); + // Since 18.20.2/20.12.2, it is invalid to call spawn on Windows with a .bat/.cmd file + // without using shell: true. + const opts = isWindows ? { ...node.spawnOpt, shell: true } : node.spawnOpt /* istanbul ignore else */ node.debug(cmd+" ["+arg+"]"); - child = spawn(cmd,arg,node.spawnOpt); + child = spawn(cmd,arg,opts); node.status({fill:"blue",shape:"dot",text:"pid:"+child.pid}); var unknownCommand = (child.pid === undefined); if (node.timer !== 0) { diff --git a/packages/node_modules/@node-red/registry/lib/externalModules.js b/packages/node_modules/@node-red/registry/lib/externalModules.js index a8a2c634c..78765f80e 100644 --- a/packages/node_modules/@node-red/registry/lib/externalModules.js +++ b/packages/node_modules/@node-red/registry/lib/externalModules.js @@ -273,7 +273,7 @@ async function installModule(moduleDetails) { let extraArgs = triggerPayload.args || []; let args = ['install', ...extraArgs, installSpec] log.trace(NPM_COMMAND + JSON.stringify(args)); - return exec.run(NPM_COMMAND, args, { cwd: installDir },true) + return exec.run(NPM_COMMAND, args, { cwd: installDir, shell: true },true) } else { log.trace("skipping npm install"); } diff --git a/packages/node_modules/@node-red/registry/lib/installer.js b/packages/node_modules/@node-red/registry/lib/installer.js index 95022fbb1..d5949a546 100644 --- a/packages/node_modules/@node-red/registry/lib/installer.js +++ b/packages/node_modules/@node-red/registry/lib/installer.js @@ -25,12 +25,15 @@ const registryUtil = require("./util"); const library = require("./library"); const {exec,log,events,hooks} = require("@node-red/util"); const child_process = require('child_process'); -const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; -let installerEnabled = false; +const isWindows = process.platform === 'win32' +const npmCommand = isWindows ? 'npm.cmd' : 'npm'; + +let installerEnabled = false; let settings; + const moduleRe = /^(@[^/@]+?[/])?[^/@]+?$/; -const slashRe = process.platform === "win32" ? /\\|[/]/ : /[/]/; +const slashRe = isWindows ? /\\|[/]/ : /[/]/; const pkgurlRe = /^(https?|git(|\+https?|\+ssh|\+file)):\/\//; const localtgzRe = /^([a-zA-Z]:|\/).+tgz$/; @@ -225,7 +228,7 @@ async function installModule(module,version,url) { let extraArgs = triggerPayload.args || []; let args = ['install', ...extraArgs, installName] log.trace(npmCommand + JSON.stringify(args)); - return exec.run(npmCommand,args,{ cwd: installDir}, true) + return exec.run(npmCommand,args,{ cwd: installDir, shell: true }, true) } else { log.trace("skipping npm install"); } @@ -260,7 +263,7 @@ async function installModule(module,version,url) { log.warn("------------------------------------------"); e = new Error(log._("server.install.install-failed")+": "+err.toString()); if (err.hook === "postInstall") { - return exec.run(npmCommand,["remove",module],{ cwd: installDir}, false).finally(() => { + return exec.run(npmCommand,["remove",module],{ cwd: installDir, shell: true }, false).finally(() => { throw e; }) } @@ -356,7 +359,7 @@ async function getModuleVersionFromNPM(module, version) { } return new Promise((resolve, reject) => { - child_process.execFile(npmCommand,['info','--json',installName],function(err,stdout,stderr) { + child_process.execFile(npmCommand,['info','--json',installName],{ shell: true },function(err,stdout,stderr) { try { if (!stdout) { log.warn(log._("server.install.install-failed-not-found",{name:module})); @@ -511,7 +514,7 @@ function uninstallModule(module) { let extraArgs = triggerPayload.args || []; let args = ['remove', ...extraArgs, module] log.trace(npmCommand + JSON.stringify(args)); - return exec.run(npmCommand,args,{ cwd: installDir}, true) + return exec.run(npmCommand,args,{ cwd: installDir, shell: true }, true) } else { log.trace("skipping npm uninstall"); } @@ -578,7 +581,7 @@ async function checkPrereq() { installerEnabled = false; } else { return new Promise(resolve => { - child_process.execFile(npmCommand,['-v'],function(err,stdout) { + child_process.execFile(npmCommand,['-v'],{ shell: true },function(err,stdout) { if (err) { log.info(log._("server.palette-editor.npm-not-found")); installerEnabled = false; From c13b8266ddbbc0d5a077a34dae0caec2e84bb964 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 11 Apr 2024 17:05:10 +0100 Subject: [PATCH 19/71] Prevent subflow being added to itself --- .../@node-red/editor-client/src/js/ui/view.js | 225 +++++++++--------- 1 file changed, 119 insertions(+), 106 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index a71daaea1..a2571dc28 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -646,120 +646,128 @@ RED.view = (function() { } d3.event = event; var selected_tool = $(ui.draggable[0]).attr("data-palette-type"); - var result = createNode(selected_tool); - if (!result) { - return; - } - var historyEvent = result.historyEvent; - var nn = RED.nodes.add(result.node); - - var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label"); - if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) { - nn.l = showLabel; - } - - var helperOffset = d3.touches(ui.helper.get(0))[0]||d3.mouse(ui.helper.get(0)); - var helperWidth = ui.helper.width(); - var helperHeight = ui.helper.height(); - var mousePos = d3.touches(this)[0]||d3.mouse(this); - try { - var isLink = (nn.type === "link in" || nn.type === "link out") - var hideLabel = nn.hasOwnProperty('l')?!nn.l : isLink; - - var label = RED.utils.getNodeLabel(nn, nn.type); - var labelParts = getLabelParts(label, "red-ui-flow-node-label"); - if (hideLabel) { - nn.w = node_height; - nn.h = Math.max(node_height,(nn.outputs || 0) * 15); - } else { - nn.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(nn._def.inputs>0?7:0))/20)) ); - nn.h = Math.max(6+24*labelParts.lines.length,(nn.outputs || 0) * 15, 30); + var result = createNode(selected_tool); + if (!result) { + return; } - } catch(err) { - } + var historyEvent = result.historyEvent; + var nn = RED.nodes.add(result.node); - mousePos[1] += this.scrollTop + ((helperHeight/2)-helperOffset[1]); - mousePos[0] += this.scrollLeft + ((helperWidth/2)-helperOffset[0]); - mousePos[1] /= scaleFactor; - mousePos[0] /= scaleFactor; + var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label"); + if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) { + nn.l = showLabel; + } - nn.x = mousePos[0]; - nn.y = mousePos[1]; + var helperOffset = d3.touches(ui.helper.get(0))[0]||d3.mouse(ui.helper.get(0)); + var helperWidth = ui.helper.width(); + var helperHeight = ui.helper.height(); + var mousePos = d3.touches(this)[0]||d3.mouse(this); - var minX = nn.w/2 -5; - if (nn.x < minX) { - nn.x = minX; - } - var minY = nn.h/2 -5; - if (nn.y < minY) { - nn.y = minY; - } - var maxX = space_width -nn.w/2 +5; - if (nn.x > maxX) { - nn.x = maxX; - } - var maxY = space_height -nn.h +5; - if (nn.y > maxY) { - nn.y = maxY; - } + try { + var isLink = (nn.type === "link in" || nn.type === "link out") + var hideLabel = nn.hasOwnProperty('l')?!nn.l : isLink; - if (snapGrid) { - var gridOffset = RED.view.tools.calculateGridSnapOffsets(nn); - nn.x -= gridOffset.x; - nn.y -= gridOffset.y; - } + var label = RED.utils.getNodeLabel(nn, nn.type); + var labelParts = getLabelParts(label, "red-ui-flow-node-label"); + if (hideLabel) { + nn.w = node_height; + nn.h = Math.max(node_height,(nn.outputs || 0) * 15); + } else { + nn.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(nn._def.inputs>0?7:0))/20)) ); + nn.h = Math.max(6+24*labelParts.lines.length,(nn.outputs || 0) * 15, 30); + } + } catch(err) { + } - var linkToSplice = $(ui.helper).data("splice"); - if (linkToSplice) { - spliceLink(linkToSplice, nn, historyEvent) - } + mousePos[1] += this.scrollTop + ((helperHeight/2)-helperOffset[1]); + mousePos[0] += this.scrollLeft + ((helperWidth/2)-helperOffset[0]); + mousePos[1] /= scaleFactor; + mousePos[0] /= scaleFactor; + + nn.x = mousePos[0]; + nn.y = mousePos[1]; + + var minX = nn.w/2 -5; + if (nn.x < minX) { + nn.x = minX; + } + var minY = nn.h/2 -5; + if (nn.y < minY) { + nn.y = minY; + } + var maxX = space_width -nn.w/2 +5; + if (nn.x > maxX) { + nn.x = maxX; + } + var maxY = space_height -nn.h +5; + if (nn.y > maxY) { + nn.y = maxY; + } + + if (snapGrid) { + var gridOffset = RED.view.tools.calculateGridSnapOffsets(nn); + nn.x -= gridOffset.x; + nn.y -= gridOffset.y; + } + + var linkToSplice = $(ui.helper).data("splice"); + if (linkToSplice) { + spliceLink(linkToSplice, nn, historyEvent) + } + + var group = $(ui.helper).data("group"); + if (group) { + var oldX = group.x; + var oldY = group.y; + RED.group.addToGroup(group, nn); + var moveEvent = null; + if ((group.x !== oldX) || + (group.y !== oldY)) { + moveEvent = { + t: "move", + nodes: [{n: group, + ox: oldX, oy: oldY, + dx: group.x -oldX, + dy: group.y -oldY}], + dirty: true + }; + } + historyEvent = { + t: 'multi', + events: [historyEvent], - var group = $(ui.helper).data("group"); - if (group) { - var oldX = group.x; - var oldY = group.y; - RED.group.addToGroup(group, nn); - var moveEvent = null; - if ((group.x !== oldX) || - (group.y !== oldY)) { - moveEvent = { - t: "move", - nodes: [{n: group, - ox: oldX, oy: oldY, - dx: group.x -oldX, - dy: group.y -oldY}], - dirty: true }; + if (moveEvent) { + historyEvent.events.push(moveEvent) + } + historyEvent.events.push({ + t: "addToGroup", + group: group, + nodes: nn + }) } - historyEvent = { - t: 'multi', - events: [historyEvent], - }; - if (moveEvent) { - historyEvent.events.push(moveEvent) + RED.history.push(historyEvent); + RED.editor.validateNode(nn); + RED.nodes.dirty(true); + // auto select dropped node - so info shows (if visible) + clearSelection(); + nn.selected = true; + movingSet.add(nn); + updateActiveNodes(); + updateSelection(); + redraw(); + + if (nn._def.autoedit) { + RED.editor.edit(nn); + } + } catch (error) { + if (error.code != "NODE_RED") { + RED.notify(RED._("notification.error",{message:error.toString()}),"error"); + } else { + RED.notify(RED._("notification.error",{message:error.message}),"error"); } - historyEvent.events.push({ - t: "addToGroup", - group: group, - nodes: nn - }) - } - - RED.history.push(historyEvent); - RED.editor.validateNode(nn); - RED.nodes.dirty(true); - // auto select dropped node - so info shows (if visible) - clearSelection(); - nn.selected = true; - movingSet.add(nn); - updateActiveNodes(); - updateSelection(); - redraw(); - - if (nn._def.autoedit) { - RED.editor.edit(nn); } } }); @@ -6063,14 +6071,19 @@ RED.view = (function() { function createNode(type, x, y, z) { const wasDirty = RED.nodes.dirty() var m = /^subflow:(.+)$/.exec(type); - var activeSubflow = z ? RED.nodes.subflow(z) : null; + var activeSubflow = (z || RED.workspaces.active()) ? RED.nodes.subflow(z || RED.workspaces.active()) : null; + if (activeSubflow && m) { var subflowId = m[1]; + let err if (subflowId === activeSubflow.id) { - throw new Error(RED._("notification.error", { message: RED._("notification.errors.cannotAddSubflowToItself") })) + err = new Error(RED._("notification.errors.cannotAddSubflowToItself")) + } else if (RED.nodes.subflowContains(m[1], activeSubflow.id)) { + err = new Error(RED._("notification.errors.cannotAddCircularReference")) } - if (RED.nodes.subflowContains(m[1], activeSubflow.id)) { - throw new Error(RED._("notification.error", { message: RED._("notification.errors.cannotAddCircularReference") })) + if (err) { + err.code = 'NODE_RED' + throw err } } From e39216e65a32f7186d4f65ba4d2c2b9b097fde0d Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 11 Apr 2024 19:15:46 +0100 Subject: [PATCH 20/71] Bump for 3.1.9 release --- CHANGELOG.md | 7 +++++++ package.json | 4 ++-- .../node_modules/@node-red/editor-api/package.json | 6 +++--- .../node_modules/@node-red/editor-client/package.json | 2 +- packages/node_modules/@node-red/nodes/package.json | 2 +- packages/node_modules/@node-red/registry/package.json | 6 +++--- packages/node_modules/@node-red/runtime/package.json | 6 +++--- packages/node_modules/@node-red/util/package.json | 2 +- packages/node_modules/node-red/package.json | 10 +++++----- 9 files changed, 26 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc45650d2..59e951261 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +#### 3.1.9: Maintenance Release + + - Prevent subflow being added to itself (#4654) @knolleary + - Fix use of spawn on windows with cmd files (#4652) @knolleary + - Guard refresh of unknown subflow (#4640) @knolleary + - Fix subflow module sending messages to debug sidebar (#4642) @knolleary + #### 3.1.8: Maintenance Release - Add validation and error handling on subflow instance properties (#4632) @knolleary diff --git a/package.json b/package.json index 80478388e..26482b620 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "3.1.8", + "version": "3.1.9", "description": "Low-code programming for event-driven applications", "homepage": "https://nodered.org", "license": "Apache-2.0", @@ -74,7 +74,7 @@ "passport-oauth2-client-password": "0.1.2", "raw-body": "2.5.2", "semver": "7.5.4", - "tar": "6.1.13", + "tar": "6.2.1", "tough-cookie": "4.1.3", "uglify-js": "3.17.4", "uuid": "9.0.0", diff --git a/packages/node_modules/@node-red/editor-api/package.json b/packages/node_modules/@node-red/editor-api/package.json index 5c9b70f2b..61c36c924 100644 --- a/packages/node_modules/@node-red/editor-api/package.json +++ b/packages/node_modules/@node-red/editor-api/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-api", - "version": "3.1.8", + "version": "3.1.9", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,8 +16,8 @@ } ], "dependencies": { - "@node-red/util": "3.1.8", - "@node-red/editor-client": "3.1.8", + "@node-red/util": "3.1.9", + "@node-red/editor-client": "3.1.9", "bcryptjs": "2.4.3", "body-parser": "1.20.2", "clone": "2.1.2", diff --git a/packages/node_modules/@node-red/editor-client/package.json b/packages/node_modules/@node-red/editor-client/package.json index da7d416f1..dbdb68e0d 100644 --- a/packages/node_modules/@node-red/editor-client/package.json +++ b/packages/node_modules/@node-red/editor-client/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-client", - "version": "3.1.8", + "version": "3.1.9", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/@node-red/nodes/package.json b/packages/node_modules/@node-red/nodes/package.json index 3f6031f71..190f5cb73 100644 --- a/packages/node_modules/@node-red/nodes/package.json +++ b/packages/node_modules/@node-red/nodes/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/nodes", - "version": "3.1.8", + "version": "3.1.9", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/@node-red/registry/package.json b/packages/node_modules/@node-red/registry/package.json index 0e25aa515..a85bc5d1a 100644 --- a/packages/node_modules/@node-red/registry/package.json +++ b/packages/node_modules/@node-red/registry/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/registry", - "version": "3.1.8", + "version": "3.1.9", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,11 +16,11 @@ } ], "dependencies": { - "@node-red/util": "3.1.8", + "@node-red/util": "3.1.9", "clone": "2.1.2", "fs-extra": "11.1.1", "semver": "7.5.4", - "tar": "6.1.13", + "tar": "6.2.1", "uglify-js": "3.17.4" } } diff --git a/packages/node_modules/@node-red/runtime/package.json b/packages/node_modules/@node-red/runtime/package.json index 2b650f5d3..ca38553dd 100644 --- a/packages/node_modules/@node-red/runtime/package.json +++ b/packages/node_modules/@node-red/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/runtime", - "version": "3.1.8", + "version": "3.1.9", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,8 +16,8 @@ } ], "dependencies": { - "@node-red/registry": "3.1.8", - "@node-red/util": "3.1.8", + "@node-red/registry": "3.1.9", + "@node-red/util": "3.1.9", "async-mutex": "0.4.0", "clone": "2.1.2", "express": "4.19.2", diff --git a/packages/node_modules/@node-red/util/package.json b/packages/node_modules/@node-red/util/package.json index 828564bea..04316d5cc 100644 --- a/packages/node_modules/@node-red/util/package.json +++ b/packages/node_modules/@node-red/util/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/util", - "version": "3.1.8", + "version": "3.1.9", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/node-red/package.json b/packages/node_modules/node-red/package.json index b4ad0ff83..1d722b32a 100644 --- a/packages/node_modules/node-red/package.json +++ b/packages/node_modules/node-red/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "3.1.8", + "version": "3.1.9", "description": "Low-code programming for event-driven applications", "homepage": "https://nodered.org", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "flow" ], "dependencies": { - "@node-red/editor-api": "3.1.8", - "@node-red/runtime": "3.1.8", - "@node-red/util": "3.1.8", - "@node-red/nodes": "3.1.8", + "@node-red/editor-api": "3.1.9", + "@node-red/runtime": "3.1.9", + "@node-red/util": "3.1.9", + "@node-red/nodes": "3.1.9", "basic-auth": "2.0.1", "bcryptjs": "2.4.3", "express": "4.19.2", From d938e5fb6b94afed1a49a97c95ffbfd5058f74db Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Fri, 12 Apr 2024 11:42:55 +0100 Subject: [PATCH 21/71] close tab on middle mouse click --- .../@node-red/editor-client/src/js/ui/common/tabs.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js index abb76e622..db3ee35bd 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js @@ -818,6 +818,13 @@ RED.tabs = (function() { link.on("mouseup",onTabClick); link.on("click", function(evt) { evt.preventDefault(); }) link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); }) + // on middle click, close the tab + link.on("auxclick", function(evt) { + if (evt.which === 2) { + evt.preventDefault(); + removeTab(tab.id); + } + }); $('').appendTo(li); From e354d2ce2994846f30fafb3579aa144d3b843ec2 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 12 Apr 2024 14:08:07 +0100 Subject: [PATCH 22/71] Fix saving of conf-type properties in module packaged subflows --- .../node_modules/@node-red/editor-client/src/js/ui/subflow.js | 2 +- packages/node_modules/@node-red/registry/lib/subflow.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js index 60ae87aee..68e949f68 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js @@ -1363,7 +1363,7 @@ RED.subflow = (function() { break; case "conf-types": item.value = input.val() - item.type = data.parent.value; + item.type = "conf-type" } if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) { env.push(item); diff --git a/packages/node_modules/@node-red/registry/lib/subflow.js b/packages/node_modules/@node-red/registry/lib/subflow.js index 97516691e..39fe083ab 100644 --- a/packages/node_modules/@node-red/registry/lib/subflow.js +++ b/packages/node_modules/@node-red/registry/lib/subflow.js @@ -88,7 +88,7 @@ function generateSubflowConfig(subflow) { this.credentials['has_' + prop.name] = (this.credentials[prop.name] !== ""); } else { switch(prop.type) { - case "str": this[prop.name] = prop.value||""; break; + case "str": case "conf-type": this[prop.name] = prop.value||""; break; case "bool": this[prop.name] = (typeof prop.value === 'boolean')?prop.value:prop.value === "true" ; break; case "num": this[prop.name] = (typeof prop.value === 'number')?prop.value:Number(prop.value); break; default: From c855050bcf3929ab412ffd9072b10a41e44d3038 Mon Sep 17 00:00:00 2001 From: Joshua Carter Date: Mon, 15 Apr 2024 08:09:26 -0700 Subject: [PATCH 23/71] Fix three error typos in monaco.js --- .../editor-client/src/js/ui/editors/code-editors/monaco.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/monaco.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/monaco.js index eedc99c88..9f104faaf 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/monaco.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/monaco.js @@ -514,7 +514,7 @@ RED.editor.codeEditor.monaco = (function() { _monaco.languages.json.jsonDefaults.setDiagnosticsOptions(diagnosticOptions); if(modeConfiguration) { _monaco.languages.json.jsonDefaults.setModeConfiguration(modeConfiguration); } } catch (error) { - console.warn("monaco - Error setting up json options", err) + console.warn("monaco - Error setting up json options", error) } } @@ -526,7 +526,7 @@ RED.editor.codeEditor.monaco = (function() { if(htmlDefaults) { _monaco.languages.html.htmlDefaults.setOptions(htmlDefaults); } if(handlebarDefaults) { _monaco.languages.html.handlebarDefaults.setOptions(handlebarDefaults); } } catch (error) { - console.warn("monaco - Error setting up html options", err) + console.warn("monaco - Error setting up html options", error) } } @@ -546,7 +546,7 @@ RED.editor.codeEditor.monaco = (function() { if(lessDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(lessDefaults_modeConfiguration); } if(scssDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(scssDefaults_modeConfiguration); } } catch (error) { - console.warn("monaco - Error setting up CSS/SCSS/LESS options", err) + console.warn("monaco - Error setting up CSS/SCSS/LESS options", error) } } From 1fdc600ecd0c06e8c2f205e3b9bd2b963f887c15 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Thu, 18 Apr 2024 11:27:32 +0100 Subject: [PATCH 24/71] Add npm install timeout notification part of https://github.com/node-red/node-red/issues/4622 --- .../editor-client/locales/en-US/editor.json | 3 ++- .../editor-client/src/js/ui/palette-editor.js | 27 ++++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 34ed30cef..05bb7fc66 100644 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -668,7 +668,8 @@ "remove": "Remove", "update": "Update" } - } + }, + "timeout": "

      Install continuing the background, Nodes will appear in palette when complete.

      Or you can monitor the install logs

      " } }, "sidebar": { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js index 8d3815749..a1df37185 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js @@ -133,7 +133,7 @@ RED.palette.editor = (function() { }).done(function(data,textStatus,xhr) { callback(); }).fail(function(xhr,textStatus,err) { - callback(xhr); + callback(xhr,textStatus,err); }); } function removeNodeModule(id,callback) { @@ -143,7 +143,7 @@ RED.palette.editor = (function() { }).done(function(data,textStatus,xhr) { callback(); }).fail(function(xhr,textStatus,err) { - callback(xhr); + callback(xhr, textStatus, err); }) } @@ -1270,9 +1270,28 @@ RED.palette.editor = (function() { RED.actions.invoke("core:show-event-log"); }); RED.eventLog.startEvent(RED._("palette.editor.confirm.button.install")+" : "+entry.id+" "+entry.version); - installNodeModule(entry.id,entry.version,entry.pkg_url,function(xhr) { + installNodeModule(entry.id,entry.version,entry.pkg_url,function(xhr, textStatus,err) { spinner.remove(); - if (xhr) { + if (err && xhr.status === 504) { + var notification = RED.notify(RED._("palette.editor.timeout"), { + modal: true, + fixed: true, + buttons: [ + { + text: RED._("common.label.close"), + click: function() { + notification.close(); + } + },{ + text: RED._("eventLog.view"), + click: function() { + notification.close(); + RED.actions.invoke("core:show-event-log"); + } + } + ] + }) + } else if (xhr) { if (xhr.responseJSON) { var notification = RED.notify(RED._('palette.editor.errors.installFailed',{module: entry.id,message:xhr.responseJSON.message}),{ type: 'error', From c990ec39d603059b0249d24a17b9e4c48fd6ea92 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Thu, 18 Apr 2024 11:35:51 +0100 Subject: [PATCH 25/71] revert DELETE change --- .../@node-red/editor-client/src/js/ui/palette-editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js index a1df37185..05e6522b0 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js @@ -143,7 +143,7 @@ RED.palette.editor = (function() { }).done(function(data,textStatus,xhr) { callback(); }).fail(function(xhr,textStatus,err) { - callback(xhr, textStatus, err); + callback(xhr); }) } From 5f4ece68130c41a455a9af2144b6c06e081b30d2 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Thu, 18 Apr 2024 11:47:49 +0100 Subject: [PATCH 26/71] Move translation --- .../@node-red/editor-client/locales/en-US/editor.json | 4 ++-- .../@node-red/editor-client/src/js/ui/palette-editor.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 05bb7fc66..1578fb923 100644 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -641,6 +641,7 @@ "errors": { "catalogLoadFailed": "

      Failed to load node catalogue.

      Check the browser console for more information

      ", "installFailed": "

      Failed to install: __module__

      __message__

      Check the log for more information

      ", + "installTimeout": "

      Install continuing the background, Nodes will appear in palette when complete.

      Or you can monitor the install logs

      ", "removeFailed": "

      Failed to remove: __module__

      __message__

      Check the log for more information

      ", "updateFailed": "

      Failed to update: __module__

      __message__

      Check the log for more information

      ", "enableFailed": "

      Failed to enable: __module__

      __message__

      Check the log for more information

      ", @@ -668,8 +669,7 @@ "remove": "Remove", "update": "Update" } - }, - "timeout": "

      Install continuing the background, Nodes will appear in palette when complete.

      Or you can monitor the install logs

      " + } } }, "sidebar": { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js index 05e6522b0..e2830b755 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js @@ -1273,7 +1273,7 @@ RED.palette.editor = (function() { installNodeModule(entry.id,entry.version,entry.pkg_url,function(xhr, textStatus,err) { spinner.remove(); if (err && xhr.status === 504) { - var notification = RED.notify(RED._("palette.editor.timeout"), { + var notification = RED.notify(RED._("palette.editor.errors.installTimeout"), { modal: true, fixed: true, buttons: [ From 148e64c3daa39f064d83e74a0a212101c0350e6f Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Thu, 18 Apr 2024 14:22:50 +0100 Subject: [PATCH 27/71] Update packages/node_modules/@node-red/editor-client/locales/en-US/editor.json Co-authored-by: Nick O'Leary --- .../@node-red/editor-client/locales/en-US/editor.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index faf8b2f72..94abf9b0a 100644 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -643,7 +643,7 @@ "errors": { "catalogLoadFailed": "

      Failed to load node catalogue.

      Check the browser console for more information

      ", "installFailed": "

      Failed to install: __module__

      __message__

      Check the log for more information

      ", - "installTimeout": "

      Install continuing the background, Nodes will appear in palette when complete.

      Or you can monitor the install logs

      ", + "installTimeout": "

      Install continuing the background.

      Nodes will appear in palette when complete. Check the log for more information.

      ", "removeFailed": "

      Failed to remove: __module__

      __message__

      Check the log for more information

      ", "updateFailed": "

      Failed to update: __module__

      __message__

      Check the log for more information

      ", "enableFailed": "

      Failed to enable: __module__

      __message__

      Check the log for more information

      ", From c2e03a40b47665a7b3a17e6574763023ce8b058c Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Sat, 20 Apr 2024 14:20:59 +0200 Subject: [PATCH 28/71] docs: Add closing paragraph tag Minor change that only improves xpath parsing. --- .../@node-red/nodes/locales/en-US/sequence/17-split.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/sequence/17-split.html b/packages/node_modules/@node-red/nodes/locales/en-US/sequence/17-split.html index cf1697b5a..e64d73f17 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/sequence/17-split.html +++ b/packages/node_modules/@node-red/nodes/locales/en-US/sequence/17-split.html @@ -103,7 +103,7 @@

      Automatic mode

      Automatic mode uses the parts property of incoming messages to determine how the sequence should be joined. This allows it to automatically - reverse the action of a split node. + reverse the action of a split node.

      Manual mode

      When configured to join in manual mode, the node is able to join sequences From 789426f80e12ce3ebea3c606b4290b138be18e13 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 23 Apr 2024 09:27:35 +0200 Subject: [PATCH 29/71] Add user presence indication to tabs and nodes --- .../editor-client/src/js/multiplayer.js | 461 ++++++++++++++---- .../@node-red/editor-client/src/js/red.js | 1 + .../editor-client/src/js/ui/common/popover.js | 6 +- .../editor-client/src/js/ui/deploy.js | 17 +- .../src/js/ui/view-annotations.js | 70 ++- .../editor-client/src/js/ui/workspaces.js | 1 + .../@node-red/editor-client/src/js/user.js | 35 +- .../editor-client/src/sass/colors.scss | 10 + .../editor-client/src/sass/header.scss | 42 +- .../editor-client/src/sass/multiplayer.scss | 98 +++- .../editor-client/src/sass/variables.scss | 3 + .../runtime/lib/multiplayer/index.js | 2 +- 12 files changed, 590 insertions(+), 156 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js b/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js index 926a118a9..576541a4d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js +++ b/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js @@ -1,114 +1,92 @@ RED.multiplayer = (function () { - // sessionId - used to identify sessions across websocket reconnects - let sessionId + // activeSessionId - used to identify sessions across websocket reconnects + let activeSessionId let headerWidget // Map of session id to { session:'', user:{}, location:{}} - let connections = {} - // Map of username to { user:{}, connections:[] } + let sessions = {} + // Map of username to { user:{}, sessions:[] } let users = {} - function addUserConnection (connection) { - if (connections[connection.session]) { + function addUserSession (session) { + if (sessions[session.session]) { // This is an existing connection that has been authenticated - const existingConnection = connections[connection.session] - if (existingConnection.user.username !== connection.user.username) { - removeUserButton(users[existingConnection.user.username]) + const existingSession = sessions[session.session] + if (existingSession.user.username !== session.user.username) { + removeUserHeaderButton(users[existingSession.user.username]) } } - connections[connection.session] = connection - const user = users[connection.user.username] = users[connection.user.username] || { - user: connection.user, - connections: [] + sessions[session.session] = session + const user = users[session.user.username] = users[session.user.username] || { + user: session.user, + sessions: [] } - connection.location = connection.location || {} - user.connections.push(connection) + if (session.user.profileColor === undefined) { + session.user.profileColor = (1 + Math.floor(Math.random() * 5)) + } + session.location = session.location || {} + user.sessions.push(session) - if (connection.user.username === RED.settings.user?.username || - connection.session === sessionId - ) { - // This is the current user - do not add a extra button for them + if (session.session === activeSessionId) { + // This is the current user session - do not add a extra button for them } else { - if (user.connections.length === 1) { + if (user.sessions.length === 1) { if (user.button) { clearTimeout(user.inactiveTimeout) clearTimeout(user.removeTimeout) user.button.removeClass('inactive') } else { - addUserButton(user) + addUserHeaderButton(user) } } + sessions[session.session].location = session.location + updateUserLocation(session.session) } } - function removeUserConnection (session, isDisconnected) { - const connection = connections[session] - delete connections[session] - const user = users[connection.user.username] - const i = user.connections.indexOf(connection) - user.connections.splice(i, 1) + function removeUserSession (sessionId, isDisconnected) { + removeUserLocation(sessionId) + const session = sessions[sessionId] + delete sessions[sessionId] + const user = users[session.user.username] + const i = user.sessions.indexOf(session) + user.sessions.splice(i, 1) if (isDisconnected) { - removeUserButton(user) + removeUserHeaderButton(user) } else { - if (user.connections.length === 0) { + if (user.sessions.length === 0) { // Give the user 5s to reconnect before marking inactive user.inactiveTimeout = setTimeout(() => { user.button.addClass('inactive') // Give the user further 20 seconds to reconnect before removing them // from the user toolbar entirely user.removeTimeout = setTimeout(() => { - removeUserButton(user) + removeUserHeaderButton(user) }, 20000) }, 5000) } } } - function addUserButton (user) { - user.button = $('

    • ') + function addUserHeaderButton (user) { + user.button = $('
    • ') .attr('data-username', user.user.username) .prependTo("#red-ui-multiplayer-user-list"); var button = user.button.find("button") + RED.popover.tooltip(button, user.user.username) button.on('click', function () { - RED.popover.create({ - target:button, - trigger: 'modal', - interactive: true, - width: "250px", - direction: 'bottom', - content: () => { - const content = $('
      ') - $('
      ').text(user.user.username).appendTo(content) - - const location = user.connections[0].location - if (location.workspace) { - const ws = RED.nodes.workspace(location.workspace) || RED.nodes.subflow(location.workspace) - if (ws) { - $('
      ').text(`${ws.type}: ${ws.label||ws.name||ws.id}`).appendTo(content) - } else { - $('
      ').text(`tab: unknown`).appendTo(content) - } - } - if (location.node) { - const node = RED.nodes.node(location.node) - if (node) { - $('
      ').text(`node: ${node.id}`).appendTo(content) - } else { - $('
      ').text(`node: unknown`).appendTo(content) - } - } - return content - }, - }).open() + const location = user.sessions[0].location + revealUser(location) }) - if (!user.user.image) { - $('').appendTo(button); - } else { - $('').css({ - backgroundImage: "url("+user.user.image+")", - }).appendTo(button); - } + + const userProfile = RED.user.generateUserIcon(user.user) + userProfile.appendTo(button) + } + + function removeUserHeaderButton (user) { + user.button.remove() + delete user.button } function getLocation () { @@ -124,7 +102,7 @@ RED.multiplayer = (function () { } return location } - function updateLocation () { + function publishLocation () { const location = getLocation() if (location.workspace !== 0) { log('send', 'multiplayer/location', location) @@ -132,31 +110,314 @@ RED.multiplayer = (function () { } } - function removeUserButton (user) { - user.button.remove() - delete user.button + function revealUser(location, skipWorkspace) { + if (location.node) { + // Need to check if this is a known node, so we can fall back to revealing + // the workspace instead + const node = RED.nodes.node(location.node) + if (node) { + RED.view.reveal(location.node) + } else if (!skipWorkspace && location.workspace) { + RED.view.reveal(location.workspace) + } + } else if (!skipWorkspace && location.workspace) { + RED.view.reveal(location.workspace) + } } - function updateUserLocation (data) { - connections[data.session].location = data - delete data.session + const workspaceTrays = {} + function getWorkspaceTray(workspaceId) { + // console.log('get tray for',workspaceId) + if (!workspaceTrays[workspaceId]) { + const tray = $('
      ') + const users = [] + const userIcons = {} + + const userCountIcon = $(`
      `) + const userCountSpan = userCountIcon.find('span span') + userCountIcon.hide() + userCountSpan.text('') + userCountIcon.appendTo(tray) + const userCountTooltip = RED.popover.tooltip(userCountIcon, function () { + const content = $('
      ') + users.forEach(sessionId => { + $('
      ').append($('').text(sessions[sessionId].user.username).on('click', function (evt) { + evt.preventDefault() + revealUser(sessions[sessionId].location, true) + userCountTooltip.close() + })).appendTo(content) + }) + return content + }, + null, + true + ) + + function updateUserCount () { + const maxShown = 2 + const children = tray.children() + children.each(function (index, element) { + const i = users.length - index + if (i > maxShown) { + $(this).hide() + } else if (i >= 0) { + $(this).show() + } + }) + if (users.length < maxShown + 1) { + userCountIcon.hide() + } else { + userCountSpan.text('+'+(users.length - maxShown)) + userCountIcon.show() + } + } + workspaceTrays[workspaceId] = { + attached: false, + tray, + users, + userIcons, + addUser: function (sessionId) { + if (users.indexOf(sessionId) === -1) { + // console.log(`addUser ws:${workspaceId} session:${sessionId}`) + users.push(sessionId) + const userLocationId = `red-ui-multiplayer-user-location-${sessionId}` + const userLocationIcon = $(`
      `) + RED.user.generateUserIcon(sessions[sessionId].user).appendTo(userLocationIcon) + userLocationIcon.prependTo(tray) + RED.popover.tooltip(userLocationIcon, sessions[sessionId].user.username) + userIcons[sessionId] = userLocationIcon + updateUserCount() + } + }, + removeUser: function (sessionId) { + // console.log(`removeUser ws:${workspaceId} session:${sessionId}`) + const userLocationId = `red-ui-multiplayer-user-location-${sessionId}` + const index = users.indexOf(sessionId) + if (index > -1) { + users.splice(index, 1) + userIcons[sessionId].remove() + delete userIcons[sessionId] + } + updateUserCount() + }, + updateUserCount + } + } + const trayDef = workspaceTrays[workspaceId] + if (!trayDef.attached) { + const workspaceTab = $(`#red-ui-tab-${workspaceId}`) + if (workspaceTab.length > 0) { + trayDef.attached = true + trayDef.tray.appendTo(workspaceTab) + trayDef.users.forEach(sessionId => { + trayDef.userIcons[sessionId].on('click', function (evt) { + revealUser(sessions[sessionId].location, true) + }) + }) + } + } + return workspaceTrays[workspaceId] } + function attachWorkspaceTrays () { + let viewTouched = false + for (let sessionId of Object.keys(sessions)) { + const location = sessions[sessionId].location + if (location) { + if (location.workspace) { + getWorkspaceTray(location.workspace).updateUserCount() + } + if (location.node) { + addUserToNode(sessionId, location.node) + viewTouched = true + } + } + } + if (viewTouched) { + RED.view.redraw() + } + } + + function addUserToNode(sessionId, nodeId) { + const node = RED.nodes.node(nodeId) + if (node) { + if (!node._multiplayer) { + node._multiplayer = { + users: [sessionId] + } + node._multiplayer_refresh = true + } else { + if (node._multiplayer.users.indexOf(sessionId) === -1) { + node._multiplayer.users.push(sessionId) + node._multiplayer_refresh = true + } + } + } + } + function removeUserFromNode(sessionId, nodeId) { + const node = RED.nodes.node(nodeId) + if (node && node._multiplayer) { + const i = node._multiplayer.users.indexOf(sessionId) + if (i > -1) { + node._multiplayer.users.splice(i, 1) + } + if (node._multiplayer.users.length === 0) { + delete node._multiplayer + } else { + node._multiplayer_refresh = true + } + } + + } + + function removeUserLocation (sessionId) { + updateUserLocation(sessionId, {}) + } + function updateUserLocation (sessionId, location) { + let viewTouched = false + const oldLocation = sessions[sessionId].location + if (location) { + if (oldLocation.workspace !== location.workspace) { + // console.log('removing', sessionId, oldLocation.workspace) + workspaceTrays[oldLocation.workspace]?.removeUser(sessionId) + } + if (oldLocation.node !== location.node) { + removeUserFromNode(sessionId, oldLocation.node) + viewTouched = true + } + sessions[sessionId].location = location + } else { + location = sessions[sessionId].location + } + // console.log(`updateUserLocation sessionId:${sessionId} oldWS:${oldLocation?.workspace} newWS:${location.workspace}`) + if (location.workspace) { + getWorkspaceTray(location.workspace).addUser(sessionId) + } + if (location.node) { + addUserToNode(sessionId, location.node) + viewTouched = true + } + if (viewTouched) { + RED.view.redraw() + } + } + + // function refreshUserLocations () { + // for (const session of Object.keys(sessions)) { + // if (session !== activeSessionId) { + // updateUserLocation(session) + // } + // } + // } + return { init: function () { - - sessionId = RED.settings.getLocal('multiplayer:sessionId') - if (!sessionId) { - sessionId = RED.nodes.id() - RED.settings.setLocal('multiplayer:sessionId', sessionId) + function createAnnotationUser(user) { + + const group = document.createElementNS("http://www.w3.org/2000/svg","g"); + const badge = document.createElementNS("http://www.w3.org/2000/svg","circle"); + const radius = 20 + badge.setAttribute("cx",radius/2); + badge.setAttribute("cy",radius/2); + badge.setAttribute("r",radius/2); + badge.setAttribute("class", "red-ui-multiplayer-annotation-background") + group.appendChild(badge) + if (user && user.profileColor !== undefined) { + badge.setAttribute("class", "red-ui-multiplayer-annotation-background red-ui-user-profile-color-" + user.profileColor) + } + if (user && user.image) { + const image = document.createElementNS("http://www.w3.org/2000/svg","image"); + image.setAttribute("width", radius) + image.setAttribute("height", radius) + image.setAttribute("href", user.image) + image.setAttribute("clip-path", "circle("+Math.floor(radius/2)+")") + group.appendChild(image) + } else if (user && user.anonymous) { + const anonIconHead = document.createElementNS("http://www.w3.org/2000/svg","circle"); + anonIconHead.setAttribute("cx", radius/2) + anonIconHead.setAttribute("cy", radius/2 - 2) + anonIconHead.setAttribute("r", 2.4) + anonIconHead.setAttribute("class","red-ui-multiplayer-annotation-anon-label"); + group.appendChild(anonIconHead) + const anonIconBody = document.createElementNS("http://www.w3.org/2000/svg","path"); + anonIconBody.setAttribute("class","red-ui-multiplayer-annotation-anon-label"); + // anonIconBody.setAttribute("d",`M ${radius/2 - 4} ${radius/2 + 1} h 8 v4 h -8 z`); + 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`); + group.appendChild(anonIconBody) + } else { + const labelText = user.username ? user.username.substring(0,2) : user + const label = document.createElementNS("http://www.w3.org/2000/svg","text"); + if (user.username) { + label.setAttribute("class","red-ui-multiplayer-annotation-label"); + label.textContent = user.username.substring(0,2) + } else { + label.setAttribute("class","red-ui-multiplayer-annotation-label red-ui-multiplayer-user-count") + label.textContent = user + } + label.setAttribute("text-anchor", "middle") + label.setAttribute("x",radius/2); + label.setAttribute("y",radius/2 + 3); + group.appendChild(label) + } + const border = document.createElementNS("http://www.w3.org/2000/svg","circle"); + border.setAttribute("cx",radius/2); + border.setAttribute("cy",radius/2); + border.setAttribute("r",radius/2); + border.setAttribute("class", "red-ui-multiplayer-annotation-border") + group.appendChild(border) + + + + return group } + RED.view.annotations.register("red-ui-multiplayer",{ + type: 'badge', + align: 'left', + class: "red-ui-multiplayer-annotation", + show: "_multiplayer", + refresh: "_multiplayer_refresh", + element: function(node) { + const containerGroup = document.createElementNS("http://www.w3.org/2000/svg","g"); + containerGroup.setAttribute("transform","translate(0,-4)") + if (node._multiplayer) { + let y = 0 + for (let i = Math.min(1, node._multiplayer.users.length - 1); i >= 0; i--) { + const user = sessions[node._multiplayer.users[i]].user + const group = createAnnotationUser(user) + group.setAttribute("transform","translate("+y+",0)") + y += 15 + containerGroup.appendChild(group) + } + if (node._multiplayer.users.length > 2) { + const group = createAnnotationUser('+'+(node._multiplayer.users.length - 2)) + group.setAttribute("transform","translate("+y+",0)") + y += 12 + containerGroup.appendChild(group) + } + + } + return containerGroup; + }, + tooltip: node => { return node._multiplayer.users.map(u => sessions[u].user.username).join('\n') } + }); + + + // activeSessionId = RED.settings.getLocal('multiplayer:sessionId') + // if (!activeSessionId) { + activeSessionId = RED.nodes.id() + // RED.settings.setLocal('multiplayer:sessionId', activeSessionId) + // log('Session ID (new)', activeSessionId) + // } else { + log('Session ID', activeSessionId) + // } + headerWidget = $('
      • ').prependTo('.red-ui-header-toolbar') RED.comms.on('connect', () => { const location = getLocation() const connectInfo = { - session: sessionId + session: activeSessionId } if (location.workspace !== 0) { connectInfo.location = location @@ -168,40 +429,52 @@ RED.multiplayer = (function () { if (topic === 'multiplayer/init') { // We have just reconnected, runtime has sent state to // initialise the world - connections = {} + sessions = {} users = {} $('#red-ui-multiplayer-user-list').empty() - msg.forEach(connection => { - addUserConnection(connection) + msg.sessions.forEach(session => { + addUserSession(session) }) } else if (topic === 'multiplayer/connection-added') { - addUserConnection(msg) + addUserSession(msg) } else if (topic === 'multiplayer/connection-removed') { - removeUserConnection(msg.session, msg.disconnected) + removeUserSession(msg.session, msg.disconnected) } else if (topic === 'multiplayer/location') { - updateUserLocation(msg) + const session = msg.session + delete msg.session + updateUserLocation(session, msg) } }) RED.events.on('workspace:change', (event) => { - updateLocation() + getWorkspaceTray(event.workspace) + publishLocation() }) RED.events.on('editor:open', () => { - updateLocation() + publishLocation() }) RED.events.on('editor:close', () => { - updateLocation() + publishLocation() }) RED.events.on('editor:change', () => { - updateLocation() + publishLocation() }) RED.events.on('login', () => { - updateLocation() + publishLocation() + }) + RED.events.on('flows:loaded', () => { + attachWorkspaceTrays() + }) + RED.events.on('workspace:close', (event) => { + // A subflow tab has been closed. Need to mark its tray as detached + if (workspaceTrays[event.workspace]) { + workspaceTrays[event.workspace].attached = false + } }) RED.events.on('logout', () => { const disconnectInfo = { - session: sessionId + session: activeSessionId } RED.comms.send('multiplayer/disconnect', disconnectInfo) RED.settings.removeLocal('multiplayer:sessionId') diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index f377e7349..f8130a686 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -298,6 +298,7 @@ var RED = (function() { RED.workspaces.show(workspaces[0]); } } + RED.events.emit('flows:loaded') } catch(err) { console.warn(err); RED.notify( diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js index 1a70839ae..381bb9d3a 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js @@ -445,9 +445,12 @@ RED.popover = (function() { return { create: createPopover, - tooltip: function(target,content, action) { + tooltip: function(target,content, action, interactive) { var label = function() { var label = content; + if (typeof content === 'function') { + label = content() + } if (action) { var shortcut = RED.keyboard.getShortcut(action); if (shortcut && shortcut.key) { @@ -463,6 +466,7 @@ RED.popover = (function() { size: "small", direction: "bottom", content: label, + interactive, delay: { show: 750, hide: 50 } }); popover.setContent = function(newContent) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js index a09fdeb01..83c04d775 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js @@ -112,16 +112,23 @@ RED.deploy = (function() { RED.actions.add("core:set-deploy-type-to-modified-nodes",function() { RED.menu.setSelected("deploymenu-item-node",true); }); } - + window.addEventListener('beforeunload', function (event) { + if (RED.nodes.dirty()) { + event.preventDefault(); + event.stopImmediatePropagation() + event.returnValue = RED._("deploy.confirm.undeployedChanges"); + return + } + }) RED.events.on('workspace:dirty',function(state) { if (state.dirty) { - window.onbeforeunload = function() { - return RED._("deploy.confirm.undeployedChanges"); - } + // window.onbeforeunload = function() { + // return + // } $("#red-ui-header-button-deploy").removeClass("disabled"); } else { - window.onbeforeunload = null; + // window.onbeforeunload = null; $("#red-ui-header-button-deploy").addClass("disabled"); } }); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js index 1df8c4bd1..b08df80ae 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js @@ -9,14 +9,27 @@ RED.view.annotations = (function() { addAnnotation(evt.node.__pendingAnnotation__,evt); delete evt.node.__pendingAnnotation__; } - var badgeDX = 0; - var controlDX = 0; - for (var i=0,l=evt.el.__annotations__.length;i').appendTo("#red-ui-header-button-user"); } else { RED.menu.addItem("red-ui-header-button-user",{ id:"usermenu-item-username", @@ -239,17 +240,9 @@ RED.user = (function() { RED.user.logout(); } }); - const userMenu = $("#red-ui-header-button-user") - userMenu.empty() - if (RED.settings.user.image) { - $('').css({ - backgroundImage: "url("+RED.settings.user.image+")", - }).appendTo(userMenu); - } else { - $('').appendTo(userMenu); - } } - + const userIcon = generateUserIcon(RED.settings.user) + userIcon.appendTo(userMenu); } function init() { @@ -320,12 +313,30 @@ RED.user = (function() { return false; } + function generateUserIcon(user) { + const userIcon = $('') + if (user.image) { + userIcon.addClass('has_profile_image') + userIcon.css({ + backgroundImage: "url("+user.image+")", + }) + } else if (user.anonymous) { + $('').appendTo(userIcon); + } else { + $('').text(user.username.substring(0,2)).appendTo(userIcon); + } + if (user.profileColor !== undefined) { + userIcon.addClass('red-ui-user-profile-color-' + user.profileColor) + } + return userIcon + } return { init: init, login: login, logout: logout, - hasPermission: hasPermission + hasPermission: hasPermission, + generateUserIcon } })(); diff --git a/packages/node_modules/@node-red/editor-client/src/sass/colors.scss b/packages/node_modules/@node-red/editor-client/src/sass/colors.scss index b561fde16..f0a69e00a 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/colors.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/colors.scss @@ -314,6 +314,16 @@ $spinner-color: #999; $tab-icon-color: #dedede; +// Anonymous User Colors + +$user-profile-colors: ( + 1: #822e81, + 2: #955e42, + 3: #9c914f, + 4: #748e54, + 5: #06bcc1 +); + // Deprecated $text-color-green: $text-color-success; $info-text-code-color: $text-color-code; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/header.scss b/packages/node_modules/@node-red/editor-client/src/sass/header.scss index 5ce9e877d..b8db40f81 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/header.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/header.scss @@ -274,18 +274,44 @@ #usermenu-item-username > .red-ui-menu-label { color: var(--red-ui-header-menu-heading-color); } +} - .user-profile { - background-position: center center; - background-repeat: no-repeat; - background-size: contain; - display: inline-block; - width: 30px; - height: 30px; - vertical-align: middle; + +.red-ui-user-profile { + background-color: var(--red-ui-header-background); + border: 2px solid var(--red-ui-header-menu-color); + border-radius: 30px; + overflow: hidden; + + background-position: center center; + background-repeat: no-repeat; + background-size: contain; + display: inline-flex; + justify-content: center; + align-items: center; + vertical-align: middle; + width: 30px; + height: 30px; + font-size: 20px; + + &.red-ui-user-profile-color-1 { + background-color: var(--red-ui-user-profile-colors-1); + } + &.red-ui-user-profile-color-2 { + background-color: var(--red-ui-user-profile-colors-2); + } + &.red-ui-user-profile-color-3 { + background-color: var(--red-ui-user-profile-colors-3); + } + &.red-ui-user-profile-color-4 { + background-color: var(--red-ui-user-profile-colors-4); + } + &.red-ui-user-profile-color-5 { + background-color: var(--red-ui-user-profile-colors-5); } } + @media only screen and (max-width: 450px) { span.red-ui-header-logo > span { display: none; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/multiplayer.scss b/packages/node_modules/@node-red/editor-client/src/sass/multiplayer.scss index 58fb9472f..4aaab86b1 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/multiplayer.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/multiplayer.scss @@ -5,23 +5,18 @@ li { display: inline-flex; align-items: center; - width: 30px; margin: 0 2px; } } .red-ui-multiplayer-user-icon { - background: var(--red-ui-header-background); - border: 2px solid var(--red-ui-header-menu-color); - border-radius: 30px; + background: none; + border: none; display: inline-flex; justify-content: center; align-items: center; - width: 28px; - height: 28px; text-align: center; - overflow: hidden; box-sizing: border-box; text-decoration: none; color: var(--red-ui-header-menu-color); @@ -36,13 +31,86 @@ .red-ui-multiplayer-user.inactive & { opacity: 0.5; } - .user-profile { - background-position: center center; - background-repeat: no-repeat; - background-size: contain; - display: inline-block; - vertical-align: middle; - width: 28px; - height: 28px; + .red-ui-user-profile { + width: 20px; + border-radius: 20px; + height: 20px; + font-size: 12px + } +} +.red-ui-multiplayer-users-tray { + position: absolute; + top: 5px; + right: 20px; + line-height: normal; + cursor: pointer; + // &:hover { + // .red-ui-multiplayer-user-location { + // margin-left: 1px; + // } + // } +} +$multiplayer-user-icon-background: var(--red-ui-primary-background); +$multiplayer-user-icon-border: var(--red-ui-view-background); +$multiplayer-user-icon-text-color: var(--red-ui-header-menu-color); +$multiplayer-user-icon-count-text-color: var(--red-ui-primary-color); +$multiplayer-user-icon-shadow: 0px 0px 4px var(--red-ui-shadow); +.red-ui-multiplayer-user-location { + display: inline-block; + margin-left: -6px; + transition: margin-left 0.2s; + .red-ui-user-profile { + border: 1px solid $multiplayer-user-icon-border; + color: $multiplayer-user-icon-text-color; + width: 18px; + height: 18px; + border-radius: 18px; + font-size: 10px; + font-weight: normal; + box-shadow: $multiplayer-user-icon-shadow; + &.red-ui-multiplayer-user-count { + color: $multiplayer-user-icon-count-text-color; + background-color: $multiplayer-user-icon-background; + } + } +} + +.red-ui-multiplayer-annotation { + .red-ui-multiplayer-annotation-background { + filter: drop-shadow($multiplayer-user-icon-shadow); + fill: $multiplayer-user-icon-background; + &.red-ui-user-profile-color-1 { + fill: var(--red-ui-user-profile-colors-1); + } + &.red-ui-user-profile-color-2 { + fill: var(--red-ui-user-profile-colors-2); + } + &.red-ui-user-profile-color-3 { + fill: var(--red-ui-user-profile-colors-3); + } + &.red-ui-user-profile-color-4 { + fill: var(--red-ui-user-profile-colors-4); + } + &.red-ui-user-profile-color-5 { + fill: var(--red-ui-user-profile-colors-5); + } + } + .red-ui-multiplayer-annotation-border { + stroke: $multiplayer-user-icon-border; + stroke-width: 1px; + fill: none; + } + .red-ui-multiplayer-annotation-anon-label { + fill: $multiplayer-user-icon-text-color; + stroke: none; + } + text { + user-select: none; + fill: $multiplayer-user-icon-text-color; + stroke: none; + font-size: 10px; + &.red-ui-multiplayer-user-count { + fill: $multiplayer-user-icon-count-text-color; + } } } \ No newline at end of file diff --git a/packages/node_modules/@node-red/editor-client/src/sass/variables.scss b/packages/node_modules/@node-red/editor-client/src/sass/variables.scss index c04c26ff9..bc8f9da17 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/variables.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/variables.scss @@ -299,4 +299,7 @@ --red-ui-tab-icon-color: #{$tab-icon-color}; + @each $current-color in 1 2 3 4 5 { + --red-ui-user-profile-colors-#{"" + $current-color}: #{map-get($user-profile-colors, $current-color)}; + } } diff --git a/packages/node_modules/@node-red/runtime/lib/multiplayer/index.js b/packages/node_modules/@node-red/runtime/lib/multiplayer/index.js index a4108e51f..adfa63c28 100644 --- a/packages/node_modules/@node-red/runtime/lib/multiplayer/index.js +++ b/packages/node_modules/@node-red/runtime/lib/multiplayer/index.js @@ -69,7 +69,7 @@ module.exports = { // Send init info to new connection const initPacket = { topic: "multiplayer/init", - data: getSessionsList(), + data: { sessions: getSessionsList() }, session: opts.session } // console.log('<<', initPacket) From 595933d046680ec52e0251ab1f551a31e8355a15 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 23 Apr 2024 09:40:01 +0200 Subject: [PATCH 30/71] Fix linting --- .../node_modules/@node-red/editor-client/src/js/multiplayer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js b/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js index 576541a4d..ea836eaf4 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js +++ b/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js @@ -153,7 +153,7 @@ RED.multiplayer = (function () { true ) - function updateUserCount () { + const updateUserCount = function () { const maxShown = 2 const children = tray.children() children.each(function (index, element) { From de7339ae97f4fd2703c667fba7ae98f4126db279 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 23 Apr 2024 20:39:14 +0200 Subject: [PATCH 31/71] Fix undo of subflow env property edits --- .../node_modules/@node-red/editor-client/src/js/ui/editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js index 11fdac279..705743b34 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js @@ -1623,8 +1623,8 @@ RED.editor = (function() { } if (!isSameObj(old_env, new_env)) { - editing_node.env = new_env; editState.changes.env = editing_node.env; + editing_node.env = new_env; editState.changed = true; } From 960af87fb0420e31264f8da9509f23b8e01ff88c Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 23 Apr 2024 21:17:35 +0200 Subject: [PATCH 32/71] Ensure subflow change state is cleared after deploy --- .../node_modules/@node-red/editor-client/src/js/ui/deploy.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js index a09fdeb01..1e6cf954b 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js @@ -612,7 +612,10 @@ RED.deploy = (function() { } }); RED.nodes.eachSubflow(function (subflow) { - subflow.changed = false; + if (subflow.changed) { + subflow.changed = false; + RED.events.emit("subflows:change", subflow); + } }); RED.nodes.eachWorkspace(function (ws) { if (ws.changed || ws.added) { From 236e6682014c60201261c976dc0c6628d93414a5 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 24 Apr 2024 22:58:11 +0200 Subject: [PATCH 33/71] Allow blank strings to be used for env var property substitutions Fixes #4663 --- packages/node_modules/@node-red/runtime/lib/flows/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/runtime/lib/flows/util.js b/packages/node_modules/@node-red/runtime/lib/flows/util.js index e753075b5..6ec1ef982 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/util.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/util.js @@ -68,7 +68,7 @@ function mapEnvVarProperties(obj,prop,flow,config) { if (obj[prop][0] === "$" && (EnvVarPropertyRE_old.test(v) || EnvVarPropertyRE.test(v)) ) { const envVar = v.substring(2,v.length-1); const r = redUtil.getSetting(config, envVar, flow); - if (r !== undefined && r !== '') { + if (r !== undefined) { obj[prop] = r } } From c05d18ada10aab398c74b3f564e1c49d100ff5d3 Mon Sep 17 00:00:00 2001 From: Kazuhito Yokoi Date: Sun, 28 Apr 2024 21:22:15 +0900 Subject: [PATCH 34/71] Add Japanese translations for 4.0.0-beta.2 --- .../editor-client/locales/en-US/editor.json | 6 +++- .../editor-client/locales/ja/editor.json | 9 ++++- .../editor-client/src/js/ui/palette-editor.js | 4 +-- .../editor-client/src/tours/welcome.js | 33 +++++++++++++------ .../@node-red/runtime/locales/ja/runtime.json | 1 + 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 94abf9b0a..d53f84547 100644 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -658,6 +658,9 @@ "body": "

        Removing '__module__'

        Removing the node will uninstall it from Node-RED. The node may continue to use resources until Node-RED is restarted.

        ", "title": "Remove nodes" }, + "removePlugin": { + "body": "

        Removed plugin __module__. Please reload the editor to clear left-overs.

        " + }, "update": { "body": "

        Updating '__module__'

        Updating the node will require a restart of Node-RED to complete the update. This must be done manually.

        ", "title": "Update nodes" @@ -669,7 +672,8 @@ "review": "Open node information", "install": "Install", "remove": "Remove", - "update": "Update" + "update": "Update", + "understood": "Understood" } } } diff --git a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json index ad0ddb4ce..9c60dab42 100644 --- a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json @@ -614,6 +614,8 @@ }, "nodeCount": "__label__ 個のノード", "nodeCount_plural": "__label__ 個のノード", + "pluginCount": "__count__ 個のプラグイン", + "pluginCount_plural": "__count__ 個のプラグイン", "moduleCount": "__count__ 個のモジュール", "moduleCount_plural": "__count__ 個のモジュール", "inuse": "使用中", @@ -641,6 +643,7 @@ "errors": { "catalogLoadFailed": "

        ノードのカタログの読み込みに失敗しました。

        詳細はブラウザのコンソールを確認してください。

        ", "installFailed": "

        追加処理が失敗しました: __module__

        __message__

        詳細はログを確認してください。

        ", + "installTimeout": "

        バックグラウンドでインストールが継続されます。

        完了した時にノードが表示されます。詳細はログを確認してください。

        ", "removeFailed": "

        削除処理が失敗しました: __module__

        __message__

        詳細はログを確認してください。

        ", "updateFailed": "

        更新処理が失敗しました: __module__

        __message__

        詳細はログを確認してください。

        ", "enableFailed": "

        有効化処理が失敗しました: __module__

        __message__

        詳細はログを確認してください。

        ", @@ -655,6 +658,9 @@ "body": "

        __module__ を削除します。

        Node-REDからノードを削除します。ノードはNode-REDが再起動されるまで、リソースを使い続ける可能性があります。

        ", "title": "ノードを削除" }, + "removePlugin": { + "body": "

        プラグイン __module__ を削除しました。ブラウザを再読み込みして残った表示を消してください。

        " + }, "update": { "body": "

        __module__ を更新します。

        更新を完了するには手動でNode-REDを再起動する必要があります。

        ", "title": "ノードの更新" @@ -666,7 +672,8 @@ "review": "ノードの情報を参照", "install": "追加", "remove": "削除", - "update": "更新" + "update": "更新", + "understood": "了解" } } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js index 7d00d0b98..3d949f8ca 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js @@ -1346,13 +1346,13 @@ RED.palette.editor = (function() { }); if (!found_onremove) { - let removeNotify = RED.notify("Removed plugin " + entry.name + ". Please reload the editor to clear left-overs.",{ + let removeNotify = RED.notify(RED._("palette.editor.confirm.removePlugin.body",{module:entry.name}),{ modal: true, fixed: true, type: 'warning', buttons: [ { - text: "Understood", + text: RED._("palette.editor.confirm.button.understood"), class:"primary", click: function(e) { removeNotify.close(); diff --git a/packages/node_modules/@node-red/editor-client/src/tours/welcome.js b/packages/node_modules/@node-red/editor-client/src/tours/welcome.js index f18bcfb45..0c2b7546d 100644 --- a/packages/node_modules/@node-red/editor-client/src/tours/welcome.js +++ b/packages/node_modules/@node-red/editor-client/src/tours/welcome.js @@ -16,7 +16,8 @@ export default { }, { title: { - "en-US": "Multiplayer Mode" + "en-US": "Multiplayer Mode", + "ja": "複数ユーザ同時利用モード" }, image: 'images/nr4-multiplayer.png', description: { @@ -24,46 +25,58 @@ export default { to work with when you have multiple people editing flows at the same time.

        When this feature is enabled, you will now see who else has the editor open and some basic information on where they are in the editor.

        -

        Check the release post for details on how to enable this feature in your settings file.

        ` +

        Check the release post for details on how to enable this feature in your settings file.

        `, + "ja": `

        本リリースには、複数ユーザが同時にフローを編集する時に、Node-REDをより使いやすくするのための最初の微修正が入っています。

        +

        本機能を有効にすると、誰がエディタを開いているか、その人がエディタ上のどこにいるかの基本的な情報が表示されます。

        +

        設定ファイルで本機能を有効化する方法の詳細は、リリースの投稿を確認してください。

        ` } }, { title: { - "en-US": "Better Configuration Node UX" + "en-US": "Better Configuration Node UX", + "ja": "設定ノードのUXが向上" }, image: 'images/nr4-config-select.png', description: { "en-US": `

        The Configuration node selection UI has had a small update to have a dedicated 'add' button next to the select box.

        -

        It's a small change, but should make it easier to work with your config nodes.

        ` +

        It's a small change, but should make it easier to work with your config nodes.

        `, + "ja": `

        設定ノードを選択するUIが修正され、選択ボックスの隣に専用の「追加」ボタンが追加されました。

        +

        微修正ですが設定ノードの操作が容易になります。

        ` } }, { title: { - "en-US": "Remembering palette state" + "en-US": "Remembering palette state", + "ja": "パレットの状態を維持" }, description: { "en-US": `

        The palette now remembers what categories you have hidden between reloads - as well as any - filter you have applied.

        ` + filter you have applied.

        `, + "ja": `

        パレット上で非表示にしたカテゴリや適用したフィルタが、リロードしても記憶されるようになりました。

        ` } }, { title: { - "en-US": "Plugins shown in the Palette Manager" + "en-US": "Plugins shown in the Palette Manager", + "ja": "パレット管理にプラグインを表示" }, image: 'images/nr4-plugins.png', description: { "en-US": `

        The palette manager now shows any plugin modules you have installed, such as node-red-debugger. Previously they would only be shown if they plugin include - nodes for the palette.

        ` + nodes for the palette.

        `, + "ja": `

        パレットの管理に node-red-debugger の様なインストールしたプラグインが表示されます。以前はプラグインにパレット向けのノードが含まれている時のみ表示されていました。

        ` } }, { title: { - "en-US": "That's if for Beta 2!" + "en-US": "That's if for Beta 2!", + "ja": "ベータ2については以上です!" }, description: { - "en-US": `

        Keep clicking through to see what was added in Beta 1

        ` + "en-US": `

        Keep clicking through to see what was added in Beta 1

        `, + "ja": `

        クリックを続けてベータ1で追加された内容を確認してください。

        ` } }, { diff --git a/packages/node_modules/@node-red/runtime/locales/ja/runtime.json b/packages/node_modules/@node-red/runtime/locales/ja/runtime.json index 8e4bceebe..db082440c 100644 --- a/packages/node_modules/@node-red/runtime/locales/ja/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/ja/runtime.json @@ -25,6 +25,7 @@ "removing-modules": "設定からモジュールを削除します", "added-types": "追加したノード:", "removed-types": "削除したノード:", + "removed-plugins": "削除したプラグイン:", "install": { "invalid": "不正なモジュール名", "installing": "モジュール __name__, バージョン: __version__ をインストールします", From 437c28e2b8033766e567caa918e94ec83f8c32a7 Mon Sep 17 00:00:00 2001 From: Kazuhito Yokoi Date: Sun, 28 Apr 2024 21:27:25 +0900 Subject: [PATCH 35/71] Fix typos in welcome tour for 4.0.0-beta.2 --- .../node_modules/@node-red/editor-client/src/tours/welcome.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/tours/welcome.js b/packages/node_modules/@node-red/editor-client/src/tours/welcome.js index 0c2b7546d..9a8b5b024 100644 --- a/packages/node_modules/@node-red/editor-client/src/tours/welcome.js +++ b/packages/node_modules/@node-red/editor-client/src/tours/welcome.js @@ -64,14 +64,14 @@ export default { image: 'images/nr4-plugins.png', description: { "en-US": `

        The palette manager now shows any plugin modules you have installed, such as - node-red-debugger. Previously they would only be shown if they plugin include + node-red-debugger. Previously they would only be shown if the plugins include nodes for the palette.

        `, "ja": `

        パレットの管理に node-red-debugger の様なインストールしたプラグインが表示されます。以前はプラグインにパレット向けのノードが含まれている時のみ表示されていました。

        ` } }, { title: { - "en-US": "That's if for Beta 2!", + "en-US": "That's it for Beta 2!", "ja": "ベータ2については以上です!" }, description: { From bf0ca383506aed7ba43e4550175e7d247c87b0c4 Mon Sep 17 00:00:00 2001 From: Kazuhito Yokoi Date: Mon, 29 Apr 2024 02:12:08 +0900 Subject: [PATCH 36/71] Enable updating dependencies of package.json in project feature --- .../editor-client/src/js/ui/projects/projectSettings.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js index 37a5ef1a8..c5dbfbdbf 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js @@ -287,7 +287,7 @@ RED.projects.settings = (function() { var notInstalledCount = 0; for (var m in modulesInUse) { - if (modulesInUse.hasOwnProperty(m)) { + if (modulesInUse.hasOwnProperty(m) && !activeProject.dependencies.hasOwnProperty(m)) { depsList.editableList('addItem',{ id: modulesInUse[m].module, version: modulesInUse[m].version, @@ -307,8 +307,8 @@ RED.projects.settings = (function() { if (activeProject.dependencies) { for (var m in activeProject.dependencies) { - if (activeProject.dependencies.hasOwnProperty(m) && !modulesInUse.hasOwnProperty(m)) { - var installed = !!RED.nodes.registry.getModule(m); + if (activeProject.dependencies.hasOwnProperty(m)) { + var installed = !!RED.nodes.registry.getModule(m) && activeProject.dependencies[m] === modulesInUse[m].version; depsList.editableList('addItem',{ id: m, version: activeProject.dependencies[m], //RED.nodes.registry.getModule(module).version, From 361391ceb87139cc3ff60de475c6e690db07b53a Mon Sep 17 00:00:00 2001 From: Kazuhito Yokoi Date: Mon, 29 Apr 2024 16:02:56 +0900 Subject: [PATCH 37/71] Load the latest project files when retrieving project information --- .../runtime/lib/storage/localfilesystem/projects/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js index b2b8c7052..ca87d76e9 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js @@ -242,7 +242,9 @@ function loadProject(name) { function getProject(user, name) { checkActiveProject(name); - return Promise.resolve(activeProject.export()); + return loadProject(name).then(function () { + return Promise.resolve(activeProject.export()); + }); } function deleteProject(user, name) { From 67e716466f782a098bb44a3c27176b544240a4d0 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 29 Apr 2024 20:14:45 +0100 Subject: [PATCH 38/71] handle middle click hide-tab in onclick handler --- .../editor-client/src/js/ui/common/tabs.js | 14 ++++++-------- .../editor-client/src/js/ui/workspaces.js | 14 ++++++++++---- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js index db3ee35bd..269b2f357 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js @@ -365,7 +365,10 @@ RED.tabs = (function() { var thisTabA = thisTab.find("a"); if (options.onclick) { - options.onclick(tabs[thisTabA.attr('href').slice(1)]); + options.onclick(tabs[thisTabA.attr('href').slice(1)], evt); + if (evt.isDefaultPrevented() && evt.isPropagationStopped()) { + return false + } } activateTab(thisTabA); if (fireSelectionChanged) { @@ -548,6 +551,7 @@ RED.tabs = (function() { ul.find("li.red-ui-tab a") .on("mousedown", function(evt) { mousedownTab = evt.currentTarget }) .on("mouseup",onTabClick) + .on("auxclick", function(evt) { evt.preventDefault() }) .on("click", function(evt) {evt.preventDefault(); }) .on("dblclick", function(evt) {evt.stopPropagation(); evt.preventDefault(); }) @@ -816,15 +820,9 @@ RED.tabs = (function() { } link.on("mousedown", function(evt) { mousedownTab = evt.currentTarget }) link.on("mouseup",onTabClick); + link.on("auxclick", function(evt) { evt.preventDefault() }) link.on("click", function(evt) { evt.preventDefault(); }) link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); }) - // on middle click, close the tab - link.on("auxclick", function(evt) { - if (evt.which === 2) { - evt.preventDefault(); - removeTab(tab.id); - } - }); $('').appendTo(li); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js index 57a9de470..48efb5e4a 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js @@ -359,11 +359,17 @@ RED.workspaces = (function() { RED.sidebar.config.refresh(); RED.view.focus(); }, - onclick: function(tab) { - if (tab.id !== activeWorkspace) { - addToViewStack(activeWorkspace); + onclick: function(tab, evt) { + if(evt.which === 2) { + evt.preventDefault(); + evt.stopPropagation(); + RED.actions.invoke("core:hide-flow", tab) + } else { + if (tab.id !== activeWorkspace) { + addToViewStack(activeWorkspace); + } + RED.view.focus(); } - RED.view.focus(); }, ondblclick: function(tab) { if (tab.type != "subflow") { From 1a3cc06935f4afcde0f2e792e0f585033874bbed Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 3 May 2024 16:45:50 +0100 Subject: [PATCH 39/71] Use rfdc module for cloning when we know its pure JSON --- package.json | 1 + .../@node-red/runtime/lib/flows/Subflow.js | 15 ++++++----- .../@node-red/runtime/lib/flows/index.js | 26 +++++++++---------- .../@node-red/runtime/lib/flows/util.js | 10 +++---- .../@node-red/runtime/lib/nodes/index.js | 7 +++-- .../@node-red/runtime/package.json | 3 ++- 6 files changed, 32 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 14906bed8..7f2b28500 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "passport-http-bearer": "1.0.1", "passport-oauth2-client-password": "0.1.2", "raw-body": "2.5.2", + "rfdc": "^1.3.1", "semver": "7.5.4", "tar": "6.2.1", "tough-cookie": "4.1.3", diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js b/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js index c3a47e1f7..fea07fd2b 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js @@ -15,6 +15,7 @@ **/ const clone = require("clone"); +const jsonClone = require("rfdc")(); const Flow = require('./Flow').Flow; const context = require('../nodes/context'); const util = require("util"); @@ -108,7 +109,7 @@ class Subflow extends Flow { } } - subflowInternalFlowConfig.subflows = clone(subflowDef.subflows || {}); + subflowInternalFlowConfig.subflows = jsonClone(subflowDef.subflows || {}); remapSubflowNodes(subflowInternalFlowConfig.configs,node_map); remapSubflowNodes(subflowInternalFlowConfig.nodes,node_map); @@ -220,7 +221,7 @@ class Subflow extends Flow { } if (this.subflowDef.in) { subflowInstanceConfig.wires = this.subflowDef.in.map(function(n) { return n.wires.map(function(w) { return self.node_map[w.id].id;})}) - subflowInstanceConfig._originalWires = clone(subflowInstanceConfig.wires); + subflowInstanceConfig._originalWires = jsonClone(subflowInstanceConfig.wires); } this.node = new Node(subflowInstanceConfig); @@ -244,14 +245,14 @@ class Subflow extends Flow { if (self.subflowDef.out) { var node,wires,i,j; // Restore the original wiring to the internal nodes - subflowInstanceConfig.wires = clone(subflowInstanceConfig._originalWires); + subflowInstanceConfig.wires = jsonClone(subflowInstanceConfig._originalWires); for (i=0;i",nid, "(",node.type,")") // node_map[node.id] = node; diff --git a/packages/node_modules/@node-red/runtime/lib/flows/index.js b/packages/node_modules/@node-red/runtime/lib/flows/index.js index c955dfe1c..4354c92ae 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/index.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/index.js @@ -14,7 +14,7 @@ * limitations under the License. **/ -var clone = require("clone"); +const jsonClone = require("rfdc")(); var Flow = require('./Flow'); @@ -140,16 +140,16 @@ function setFlows(_config,_credentials,type,muteLog,forceStart,user) { if (type === "load") { isLoad = true; configSavePromise = loadFlows().then(function(_config) { - config = clone(_config.flows); - newFlowConfig = flowUtil.parseConfig(clone(config)); + config = jsonClone(_config.flows); + newFlowConfig = flowUtil.parseConfig(jsonClone(config)); type = "full"; return _config.rev; }); } else { // Clone the provided config so it can be manipulated - config = clone(_config); + config = jsonClone(_config); // Parse the configuration - newFlowConfig = flowUtil.parseConfig(clone(config)); + newFlowConfig = flowUtil.parseConfig(jsonClone(config)); // Generate a diff to identify what has changed diff = flowUtil.diffConfigs(activeFlowConfig,newFlowConfig); @@ -609,7 +609,7 @@ async function addFlow(flow, user) { nodes.push(node); } } - var newConfig = clone(activeConfig.flows); + var newConfig = jsonClone(activeConfig.flows); newConfig = newConfig.concat(nodes); return setFlows(newConfig, null, 'flows', true, null, user).then(function() { @@ -650,7 +650,7 @@ function getFlow(id) { var nodeIds = Object.keys(flow.nodes); if (nodeIds.length > 0) { result.nodes = nodeIds.map(function(nodeId) { - var node = clone(flow.nodes[nodeId]); + var node = jsonClone(flow.nodes[nodeId]); if (node.type === 'link out') { delete node.wires; } @@ -662,7 +662,7 @@ function getFlow(id) { if (flow.configs) { var configIds = Object.keys(flow.configs); result.configs = configIds.map(function(configId) { - const node = clone(flow.configs[configId]); + const node = jsonClone(flow.configs[configId]); delete node.credentials; return node @@ -674,17 +674,17 @@ function getFlow(id) { if (flow.subflows) { var subflowIds = Object.keys(flow.subflows); result.subflows = subflowIds.map(function(subflowId) { - var subflow = clone(flow.subflows[subflowId]); + var subflow = jsonClone(flow.subflows[subflowId]); var nodeIds = Object.keys(subflow.nodes); subflow.nodes = nodeIds.map(function(id) { - const node = clone(subflow.nodes[id]) + const node = jsonClone(subflow.nodes[id]) delete node.credentials return node }); if (subflow.configs) { var configIds = Object.keys(subflow.configs); subflow.configs = configIds.map(function(id) { - const node = clone(subflow.configs[id]) + const node = jsonClone(subflow.configs[id]) delete node.credentials return node }) @@ -709,7 +709,7 @@ async function updateFlow(id,newFlow, user) { } label = activeFlowConfig.flows[id].label; } - var newConfig = clone(activeConfig.flows); + var newConfig = jsonClone(activeConfig.flows); var nodes; if (id === 'global') { @@ -779,7 +779,7 @@ async function removeFlow(id, user) { throw e; } - var newConfig = clone(activeConfig.flows); + var newConfig = jsonClone(activeConfig.flows); newConfig = newConfig.filter(function(node) { return node.z !== id && node.id !== id; }); diff --git a/packages/node_modules/@node-red/runtime/lib/flows/util.js b/packages/node_modules/@node-red/runtime/lib/flows/util.js index 6ec1ef982..fa25a26d0 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/util.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/util.js @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ -const clone = require("clone"); +const jsonClone = require("rfdc")(); const redUtil = require("@node-red/util").util; const Log = require("@node-red/util").log; const typeRegistry = require("@node-red/registry"); @@ -175,7 +175,7 @@ async function createNode(flow,config) { try { var nodeTypeConstructor = typeRegistry.get(type); if (typeof nodeTypeConstructor === "function") { - var conf = clone(config); + var conf = jsonClone(config); delete conf.credentials; try { Object.defineProperty(conf,'_module', {value: typeRegistry.getNodeInfo(type), enumerable: false, writable: true }) @@ -202,8 +202,8 @@ async function createNode(flow,config) { var subflowInstanceConfig = subflowConfig.subflows[nodeTypeConstructor.subflow.id]; delete subflowConfig.subflows[nodeTypeConstructor.subflow.id]; subflowInstanceConfig.subflows = subflowConfig.subflows; - var instanceConfig = clone(config); - instanceConfig.env = clone(nodeTypeConstructor.subflow.env); + var instanceConfig = jsonClone(config); + instanceConfig.env = jsonClone(nodeTypeConstructor.subflow.env); instanceConfig.env = nodeTypeConstructor.subflow.env.map(nodeProp => { var nodePropType; @@ -256,7 +256,7 @@ function parseConfig(config) { flow.missingTypes = []; config.forEach(function (n) { - flow.allNodes[n.id] = clone(n); + flow.allNodes[n.id] = jsonClone(n); if (n.type === 'tab') { flow.flows[n.id] = n; flow.flows[n.id].subflows = {}; diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/index.js b/packages/node_modules/@node-red/runtime/lib/nodes/index.js index a852a86ea..8e7d45d05 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/index.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/index.js @@ -14,9 +14,8 @@ * limitations under the License. **/ -var path = require("path"); -var fs = require("fs"); -var clone = require("clone"); + +const jsonClone = require("rfdc")(); var util = require("util"); var registry = require("@node-red/registry"); @@ -98,7 +97,7 @@ function createNode(node,def) { } var creds = credentials.get(id); if (creds) { - creds = clone(creds); + creds = jsonClone(creds); //console.log("Attaching credentials to ",node.id); // allow $(foo) syntax to substitute env variables for credentials also... for (var p in creds) { diff --git a/packages/node_modules/@node-red/runtime/package.json b/packages/node_modules/@node-red/runtime/package.json index 5a3ee8c47..0fd6964f0 100644 --- a/packages/node_modules/@node-red/runtime/package.json +++ b/packages/node_modules/@node-red/runtime/package.json @@ -22,6 +22,7 @@ "clone": "2.1.2", "express": "4.19.2", "fs-extra": "11.1.1", - "json-stringify-safe": "5.0.1" + "json-stringify-safe": "5.0.1", + "rfdc": "^1.3.1" } } From 22cc8da0885d75a164b80a5ecc46f2945c9fe5b4 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 3 May 2024 16:59:17 +0100 Subject: [PATCH 40/71] Apply suggestions from code review --- .../@node-red/editor-client/src/js/ui/common/tabs.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js index 269b2f357..d9dc4b289 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js @@ -551,6 +551,7 @@ RED.tabs = (function() { ul.find("li.red-ui-tab a") .on("mousedown", function(evt) { mousedownTab = evt.currentTarget }) .on("mouseup",onTabClick) + // prevent browser-default middle-click behaviour .on("auxclick", function(evt) { evt.preventDefault() }) .on("click", function(evt) {evt.preventDefault(); }) .on("dblclick", function(evt) {evt.stopPropagation(); evt.preventDefault(); }) @@ -820,6 +821,7 @@ RED.tabs = (function() { } link.on("mousedown", function(evt) { mousedownTab = evt.currentTarget }) link.on("mouseup",onTabClick); + // prevent browser-default middle-click behaviour link.on("auxclick", function(evt) { evt.preventDefault() }) link.on("click", function(evt) { evt.preventDefault(); }) link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); }) From b02f69b77a997750105c748a09d369af57a062fe Mon Sep 17 00:00:00 2001 From: GogoVega <92022724+GogoVega@users.noreply.github.com> Date: Sun, 5 May 2024 15:00:42 +0200 Subject: [PATCH 41/71] Add translations for 4.0.0-beta.2 --- .../editor-client/locales/fr/editor.json | 11 ++++- .../editor-client/src/tours/welcome.js | 40 ++++++++++++++----- .../@node-red/runtime/locales/fr/runtime.json | 5 ++- 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/fr/editor.json b/packages/node_modules/@node-red/editor-client/locales/fr/editor.json index 1043f6f9d..e0ebdbb2a 100644 --- a/packages/node_modules/@node-red/editor-client/locales/fr/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/fr/editor.json @@ -614,6 +614,8 @@ }, "nodeCount": "__label__ noeud", "nodeCount_plural": "__label__ noeuds", + "pluginCount": "__count__ plugin", + "pluginCount_plural": "__count__ plugins", "moduleCount": "__count__ module disponible", "moduleCount_plural": "__count__ modules disponibles", "inuse": "En cours d'utilisation", @@ -641,6 +643,7 @@ "errors": { "catalogLoadFailed": "

        Échec du chargement du catalogue de noeuds.

        Vérifier la console du navigateur pour plus d'informations

        ", "installFailed": "

        Échec lors de l'installation : __module__

        __message__

        Consulter le journal pour plus d'informations

        ", + "installTimeout": "

        L'installation continue en arrière-plan.

        Les noeuds apparaîtront dans la palette une fois l'installation terminée. Consulter le journal pour plus d'informations.

        ", "removeFailed": "

        Échec lors de la suppression : __module__

        __message__

        Consulter le journal pour plus d'informations

        ", "updateFailed": "

        Échec lors de la mise à jour : __module__

        __message__

        Consulter le journal pour plus d'informations

        ", "enableFailed": "

        Échec lors de l'activation : __module__

        __message__

        Consulter le journal pour plus d'informations

        ", @@ -652,9 +655,12 @@ "title": "Installer les noeuds" }, "remove": { - "body": "

        Suppression de '__module__'

        La suppression du noeud le désinstallera de Node-RED. Le noeud peut continuer à utiliser des ressources jusqu'au redémarrage de Node-RED.

        ", + "body": "

        Suppression de '__module__'

        La suppression du noeud le désinstallera de Node-RED. Le noeud peut continuer à utiliser ses ressources jusqu'au redémarrage de Node-RED.

        ", "title": "Supprimer les noeuds" }, + "removePlugin": { + "body": "

        Suppression du plugin '__module__'. Veuillez recharger l'éditeur afin d'appliquer les changements.

        " + }, "update": { "body": "

        Mise à jour de '__module__'

        La mise à jour du noeud nécessitera un redémarrage de Node-RED pour terminer la mise à jour. Cela doit être fait manuellement.

        ", "title": "Mettre à jour les noeuds" @@ -666,7 +672,8 @@ "review": "Ouvrir la documentation", "install": "Installer", "remove": "Supprimer", - "update": "Mettre à jour" + "update": "Mettre à jour", + "understood": "Compris" } } } diff --git a/packages/node_modules/@node-red/editor-client/src/tours/welcome.js b/packages/node_modules/@node-red/editor-client/src/tours/welcome.js index 9a8b5b024..c89a4c35d 100644 --- a/packages/node_modules/@node-red/editor-client/src/tours/welcome.js +++ b/packages/node_modules/@node-red/editor-client/src/tours/welcome.js @@ -17,7 +17,8 @@ export default { { title: { "en-US": "Multiplayer Mode", - "ja": "複数ユーザ同時利用モード" + "ja": "複数ユーザ同時利用モード", + "fr": "Mode Multi-utilisateur" }, image: 'images/nr4-multiplayer.png', description: { @@ -28,13 +29,20 @@ export default {

        Check the release post for details on how to enable this feature in your settings file.

        `, "ja": `

        本リリースには、複数ユーザが同時にフローを編集する時に、Node-REDをより使いやすくするのための最初の微修正が入っています。

        本機能を有効にすると、誰がエディタを開いているか、その人がエディタ上のどこにいるかの基本的な情報が表示されます。

        -

        設定ファイルで本機能を有効化する方法の詳細は、リリースの投稿を確認してください。

        ` +

        設定ファイルで本機能を有効化する方法の詳細は、リリースの投稿を確認してください。

        `, + "fr": `

        Cette version inclut les premières étapes visant à rendre Node-RED plus facile à utiliser + lorsque plusieurs personnes modifient des flux en même temps.

        +

        Lorsque cette fonctionnalité est activée, vous pourrez désormais voir si d’autres utilisateurs ont + ouvert l'éditeur. Vous pourrez également savoir où ces utilisateurs se trouvent dans l'éditeur.

        +

        Consultez la note de publication pour plus de détails sur la façon d'activer cette fonctionnalité + dans votre fichier de paramètres.

        ` } }, { title: { "en-US": "Better Configuration Node UX", - "ja": "設定ノードのUXが向上" + "ja": "設定ノードのUXが向上", + "fr": "Meilleure expérience utilisateur du noeud de configuration" }, image: 'images/nr4-config-select.png', description: { @@ -42,41 +50,53 @@ export default { next to the select box.

        It's a small change, but should make it easier to work with your config nodes.

        `, "ja": `

        設定ノードを選択するUIが修正され、選択ボックスの隣に専用の「追加」ボタンが追加されました。

        -

        微修正ですが設定ノードの操作が容易になります。

        ` +

        微修正ですが設定ノードの操作が容易になります。

        `, + "fr": `

        L'interface utilisateur de la sélection du noeud de configuration a fait l'objet d'une petite + mise à jour afin de disposer d'un bouton « Ajouter » à côté de la zone de sélection.

        +

        C'est un petit changement, mais cela devrait faciliter le travail avec vos noeuds de configuration.

        ` } }, { title: { "en-US": "Remembering palette state", - "ja": "パレットの状態を維持" + "ja": "パレットの状態を維持", + "fr": "Mémorisation de l'état de la palette" }, description: { "en-US": `

        The palette now remembers what categories you have hidden between reloads - as well as any filter you have applied.

        `, - "ja": `

        パレット上で非表示にしたカテゴリや適用したフィルタが、リロードしても記憶されるようになりました。

        ` + "ja": `

        パレット上で非表示にしたカテゴリや適用したフィルタが、リロードしても記憶されるようになりました。

        `, + "fr": `

        La palette se souvient désormais des catégories que vous avez masquées entre les rechargements, + ainsi que le filtre que vous avez appliqué.

        ` } }, { title: { "en-US": "Plugins shown in the Palette Manager", - "ja": "パレット管理にプラグインを表示" + "ja": "パレット管理にプラグインを表示", + "fr": "Affichage des Plugins dans le gestionnaire de palettes" }, image: 'images/nr4-plugins.png', description: { "en-US": `

        The palette manager now shows any plugin modules you have installed, such as node-red-debugger. Previously they would only be shown if the plugins include nodes for the palette.

        `, - "ja": `

        パレットの管理に node-red-debugger の様なインストールしたプラグインが表示されます。以前はプラグインにパレット向けのノードが含まれている時のみ表示されていました。

        ` + "ja": `

        パレットの管理に node-red-debugger の様なインストールしたプラグインが表示されます。以前はプラグインにパレット向けのノードが含まれている時のみ表示されていました。

        `, + "fr": `

        Le gestionnaire de palettes affiche désormais tous les plugins que vous avez installés, + tels que node-red-debugger. Auparavant, ils n'étaient affichés que s'ils contenaient + des noeuds pour la palette.

        ` } }, { title: { "en-US": "That's it for Beta 2!", - "ja": "ベータ2については以上です!" + "ja": "ベータ2については以上です!", + "fr": "C'est tout pour la bêta 2 !" }, description: { "en-US": `

        Keep clicking through to see what was added in Beta 1

        `, - "ja": `

        クリックを続けてベータ1で追加された内容を確認してください。

        ` + "ja": `

        クリックを続けてベータ1で追加された内容を確認してください。

        `, + "fr": `

        Continuez à cliquer pour voir ce qui a été ajouté dans la version bêta 1

        ` } }, { diff --git a/packages/node_modules/@node-red/runtime/locales/fr/runtime.json b/packages/node_modules/@node-red/runtime/locales/fr/runtime.json index 0f5be3054..23d6d0269 100644 --- a/packages/node_modules/@node-red/runtime/locales/fr/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/fr/runtime.json @@ -20,10 +20,12 @@ "errors-help": "Exécuter avec -v pour plus de détails", "missing-modules": "Modules de noeud manquants :", "node-version-mismatch": "Le module de noeud ne peut pas être chargé sur cette version. Nécessite : __version__ ", + "set-has-no-types": "L'ensemble n'a aucun type. Nom : '__name__', module : '__module__', fichier : '__file__'", "type-already-registered": "'__type__' déjà enregistré par le module __module__", "removing-modules": "Suppression de modules de la configuration", "added-types": "Types de noeuds ajoutés :", "removed-types": "Types de noeuds supprimés :", + "removed-plugins": "Plugins supprimés :", "install": { "invalid": "Nom de module invalide", "installing": "Installation du module : __name__, version : __version__", @@ -134,7 +136,8 @@ "flow": { "unknown-type": "Type inconnu : __type__", "missing-types": "Types manquants", - "error-loop": "Le message a dépassé le nombre maximum de captures (catches)" + "error-loop": "Le message a dépassé le nombre maximum de captures (catches)", + "non-message-returned": "Le noeud a tenté d'envoyer un message du type __type__" }, "index": { "unrecognised-id": "Identifiant non reconnu : __id__", From b27483de9c082eaca77976cc3c6d951c2301481a Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 8 May 2024 15:09:51 +0100 Subject: [PATCH 42/71] Avoid login loops when autoLogin enabled but login fails Fixes #4363 --- .../node_modules/@node-red/editor-api/lib/auth/index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/node_modules/@node-red/editor-api/lib/auth/index.js b/packages/node_modules/@node-red/editor-api/lib/auth/index.js index eefdd85e0..e39e972db 100644 --- a/packages/node_modules/@node-red/editor-api/lib/auth/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/auth/index.js @@ -205,9 +205,10 @@ function genericStrategy(adminApp,strategy) { passport.use(new strategy.strategy(options, verify)); adminApp.get('/auth/strategy', - passport.authenticate(strategy.name, {session:false, + passport.authenticate(strategy.name, { + session:false, failureMessage: true, - failureRedirect: settings.httpAdminRoot + failureRedirect: settings.httpAdminRoot + '?session_message=Login Failed' }), completeGenerateStrategyAuth, handleStrategyError @@ -221,7 +222,7 @@ function genericStrategy(adminApp,strategy) { passport.authenticate(strategy.name, { session:false, failureMessage: true, - failureRedirect: settings.httpAdminRoot + failureRedirect: settings.httpAdminRoot + '?session_message=Login Failed' }), completeGenerateStrategyAuth, handleStrategyError From 66a667fe585722875c267b76e99cf981d94e47ce Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 9 May 2024 16:48:51 +0100 Subject: [PATCH 43/71] Pass full error object in Function node and copy over cause property Fixes #4683 --- .../@node-red/nodes/core/function/10-function.js | 15 ++++++++------- .../@node-red/runtime/lib/flows/Flow.js | 3 +++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js index a915a1623..580b115f0 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js @@ -438,10 +438,9 @@ module.exports = function(RED) { //store the error in msg to be used in flows msg.error = err; - - var line = 0; - var errorMessage; if (stack.length > 0) { + let line = 0; + let errorMessage; while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) { line++; } @@ -455,11 +454,13 @@ module.exports = function(RED) { errorMessage += " (line "+lineno+", col "+cha+")"; } } + if (errorMessage) { + err.message = errorMessage + } } - if (!errorMessage) { - errorMessage = err.toString(); - } - done(errorMessage); + // Pass the whole error object so any additional properties + // (such as cause) are preserved + done(err); } else if (typeof err === "string") { done(err); diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index dd3d335a2..c4f4e39a2 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -678,6 +678,9 @@ class Flow { if (logMessage.hasOwnProperty('stack')) { errorMessage.error.stack = logMessage.stack; } + if (logMessage.hasOwnProperty('cause')) { + errorMessage.error.cause = logMessage.cause; + } targetCatchNode.receive(errorMessage); handled = true; }); From 03648dc7e84ae6cc6c9c3b93659cc960eb6b5e9a Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 9 May 2024 17:25:47 +0100 Subject: [PATCH 44/71] Update tests for changed function node low-level output --- test/nodes/core/function/10-function_spec.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/nodes/core/function/10-function_spec.js b/test/nodes/core/function/10-function_spec.js index acb693208..371c810d9 100644 --- a/test/nodes/core/function/10-function_spec.js +++ b/test/nodes/core/function/10-function_spec.js @@ -390,7 +390,8 @@ describe('function node', function() { msg.should.have.property('level', helper.log().ERROR); msg.should.have.property('id', 'n1'); msg.should.have.property('type', 'function'); - msg.should.have.property('msg', 'ReferenceError: retunr is not defined (line 2, col 1)'); + msg.should.have.property('msg') + msg.msg.message.should.equal('ReferenceError: retunr is not defined (line 2, col 1)'); done(); } catch(err) { done(err); @@ -659,7 +660,8 @@ describe('function node', function() { msg.should.have.property('level', helper.log().ERROR); msg.should.have.property('id', name); msg.should.have.property('type', 'function'); - msg.should.have.property('msg', 'Error: Callback must be a function'); + msg.should.have.property('msg') + msg.msg.message.should.equal('Callback must be a function'); done(); } catch (e) { From aa372a1707da82c0e763445e28250079f621ee85 Mon Sep 17 00:00:00 2001 From: Rotzbua Date: Sun, 12 May 2024 18:16:55 +0200 Subject: [PATCH 45/71] Fix typo in source code comment --- .../@node-red/editor-client/src/types/node-red/util.d.ts | 2 +- packages/node_modules/@node-red/util/lib/util.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/types/node-red/util.d.ts b/packages/node_modules/@node-red/editor-client/src/types/node-red/util.d.ts index 255243d47..5d475cf57 100644 --- a/packages/node_modules/@node-red/editor-client/src/types/node-red/util.d.ts +++ b/packages/node_modules/@node-red/editor-client/src/types/node-red/util.d.ts @@ -76,7 +76,7 @@ declare namespace RED { */ function compareObjects(obj1: any, obj2: any): boolean; /** - * Generates a psuedo-unique-random id. + * Generates a pseudo-unique-random id. * @return {string} a random-ish id * @memberof @node-red/util_util */ diff --git a/packages/node_modules/@node-red/util/lib/util.js b/packages/node_modules/@node-red/util/lib/util.js index 4896789b6..06ed366a0 100644 --- a/packages/node_modules/@node-red/util/lib/util.js +++ b/packages/node_modules/@node-red/util/lib/util.js @@ -27,7 +27,7 @@ const util = require("util"); const { hasOwnProperty } = Object.prototype; const log = require("./log") /** - * Safely returns the object construtor name. + * Safely returns the object constructor name. * @return {String} the name of the object constructor if it exists, empty string otherwise. */ function constructorName(obj) { @@ -37,7 +37,7 @@ function constructorName(obj) { } /** - * Generates a psuedo-unique-random id. + * Generates a pseudo-unique-random id. * @return {String} a random-ish id * @memberof @node-red/util_util */ From 34345461f1947e7d3a4c90c54b567114369a52f1 Mon Sep 17 00:00:00 2001 From: Rotzbua Date: Sun, 12 May 2024 22:24:49 +0200 Subject: [PATCH 46/71] fix(dns): remove outdated node check Add reference to issue for this workaround. --- packages/node_modules/node-red/lib/red.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/node_modules/node-red/lib/red.js b/packages/node_modules/node-red/lib/red.js index 93c983ce3..a77105c58 100644 --- a/packages/node_modules/node-red/lib/red.js +++ b/packages/node_modules/node-red/lib/red.js @@ -25,11 +25,9 @@ var api = require("@node-red/editor-api"); var server = null; var apiEnabled = false; -const NODE_MAJOR_VERSION = process.versions.node.split('.')[0]; -if (NODE_MAJOR_VERSION >= 16) { - const dns = require('dns'); - dns.setDefaultResultOrder('ipv4first'); -} +// Workaround for WebSocket: https://github.com/node-red/node-red/issues/4010 +const dns = require('dns'); +dns.setDefaultResultOrder('ipv4first'); function checkVersion(userSettings) { var semver = require('semver'); From 20d2450cac637515d5303bd0a88b574a58d0cae0 Mon Sep 17 00:00:00 2001 From: Rotzbua Date: Sun, 12 May 2024 22:38:03 +0200 Subject: [PATCH 47/71] fix(polyfill): remove import module polyfill Was required for node <12.17. --- .../@node-red/registry/lib/externalModules.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/packages/node_modules/@node-red/registry/lib/externalModules.js b/packages/node_modules/@node-red/registry/lib/externalModules.js index f6cbae9bc..a7ce04f63 100644 --- a/packages/node_modules/@node-red/registry/lib/externalModules.js +++ b/packages/node_modules/@node-red/registry/lib/externalModules.js @@ -28,11 +28,6 @@ let installEnabled = true; let installAllowList = ['*']; let installDenyList = []; -let IMPORT_SUPPORTED = true; -const nodeVersionParts = process.versions.node.split(".").map(v => parseInt(v)); -if (nodeVersionParts[0] < 12 || (nodeVersionParts[0] === 12 && nodeVersionParts[1] < 17)) { - IMPORT_SUPPORTED = false; -} function getInstallDir() { return path.resolve(settings.userDir || process.env.NODE_RED_HOME || "."); @@ -110,18 +105,6 @@ function requireModule(module) { return require(moduleDir); } function importModule(module) { - if (!IMPORT_SUPPORTED) { - // On Node < 12.17 - fall back to try a require - return new Promise((resolve, reject) => { - try { - const mod = requireModule(module); - resolve(mod); - } catch(err) { - reject(err); - } - }); - } - if (!registryUtil.checkModuleAllowed( module, null,installAllowList,installDenyList)) { const e = new Error("Module not allowed"); e.code = "module_not_allowed"; From d706c9cb37d7b0886c5c2d8f6f09b30e0dd1db32 Mon Sep 17 00:00:00 2001 From: Rotzbua Date: Sun, 12 May 2024 22:49:58 +0200 Subject: [PATCH 48/71] fix: remove outdated Node 11+ check --- .../@node-red/runtime/locales/de/runtime.json | 1 - .../runtime/locales/en-US/runtime.json | 1 - .../runtime/locales/es-ES/runtime.json | 1 - .../@node-red/runtime/locales/fr/runtime.json | 1 - .../@node-red/runtime/locales/ja/runtime.json | 1 - .../runtime/locales/pt-BR/runtime.json | 1 - .../@node-red/runtime/locales/ru/runtime.json | 1 - packages/node_modules/node-red/red.js | 53 +++++++++---------- 8 files changed, 24 insertions(+), 36 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/locales/de/runtime.json b/packages/node_modules/@node-red/runtime/locales/de/runtime.json index 0249d638d..1c945f03e 100644 --- a/packages/node_modules/@node-red/runtime/locales/de/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/de/runtime.json @@ -56,7 +56,6 @@ "refresh-interval": "Erneuerung der https-Einstellungen erfolgt alle __interval__ Stunden", "settings-refreshed": "https-Einstellungen wurden erneuert", "refresh-failed": "Erneuerung der https-Einstellungen fehlgeschlagen: __message__", - "nodejs-version": "httpsRefreshInterval erfordert Node.js 11 oder höher", "function-required": "httpsRefreshInterval erfordert die https-Eigenschaft in Form einer Funktion" } }, diff --git a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json index c8033a3de..c3b0a0489 100644 --- a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json @@ -58,7 +58,6 @@ "refresh-interval": "Refreshing https settings every __interval__ hours", "settings-refreshed": "Server https settings have been refreshed", "refresh-failed": "Failed to refresh https settings: __message__", - "nodejs-version": "httpsRefreshInterval requires Node.js 11 or later", "function-required": "httpsRefreshInterval requires https property to be a function" } }, diff --git a/packages/node_modules/@node-red/runtime/locales/es-ES/runtime.json b/packages/node_modules/@node-red/runtime/locales/es-ES/runtime.json index 8f63cb01e..9fba64ad3 100644 --- a/packages/node_modules/@node-red/runtime/locales/es-ES/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/es-ES/runtime.json @@ -57,7 +57,6 @@ "refresh-interval": "Actualizando la configuración HTTPS cada __interval__ horas", "settings-refreshed": "La configuración HTTPS del servidor se ha actualizado", "refresh-failed": "No se pudo actualizar la configuración HTTPS: __message__", - "nodejs-version": "httpsRefreshInterval requiere Node.js 11 o superior", "function-required": "httpsRefreshInterval requiere que la propiedad HTTPS sea una función" } }, diff --git a/packages/node_modules/@node-red/runtime/locales/fr/runtime.json b/packages/node_modules/@node-red/runtime/locales/fr/runtime.json index 23d6d0269..a38e95a57 100644 --- a/packages/node_modules/@node-red/runtime/locales/fr/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/fr/runtime.json @@ -58,7 +58,6 @@ "refresh-interval": "Actualisation des paramètres https toutes les __interval__ heures", "settings-refreshed": "Les paramètres https du serveur ont été actualisés", "refresh-failed": "Échec de l'actualisation des paramètres https : __message__", - "nodejs-version": "httpsRefreshInterval nécessite Node.js 11 ou version ultérieure", "function-required": "httpsRefreshInterval nécessite que la propriété https soit une fonction" } }, diff --git a/packages/node_modules/@node-red/runtime/locales/ja/runtime.json b/packages/node_modules/@node-red/runtime/locales/ja/runtime.json index db082440c..684593031 100644 --- a/packages/node_modules/@node-red/runtime/locales/ja/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/ja/runtime.json @@ -58,7 +58,6 @@ "refresh-interval": "__interval__ 時間毎にhttps設定を更新します", "settings-refreshed": "サーバのhttps設定が更新されました", "refresh-failed": "https設定の更新で失敗しました: __message__", - "nodejs-version": "httpsRefreshIntervalにはNode.js 11以降が必要です", "function-required": "httpsRefreshIntervalでは、httpsプロパティはfunctionである必要があります" } }, diff --git a/packages/node_modules/@node-red/runtime/locales/pt-BR/runtime.json b/packages/node_modules/@node-red/runtime/locales/pt-BR/runtime.json index 7ef9c8676..59473f575 100644 --- a/packages/node_modules/@node-red/runtime/locales/pt-BR/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/pt-BR/runtime.json @@ -57,7 +57,6 @@ "refresh-interval": "Atualizando as configurações de https a cada __interval__ hora(s)", "settings-refreshed": "As configurações https do servidor foram atualizadas", "refresh-failed": "Falha ao atualizar as configurações https: __message__", - "nodejs-version": "httpsRefreshInterval requer Node.js 11 ou posterior", "function-required": "httpsRefreshInterval requer que a propriedade https seja uma função" } }, diff --git a/packages/node_modules/@node-red/runtime/locales/ru/runtime.json b/packages/node_modules/@node-red/runtime/locales/ru/runtime.json index 75a62b89e..fcf8f9657 100644 --- a/packages/node_modules/@node-red/runtime/locales/ru/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/ru/runtime.json @@ -55,7 +55,6 @@ "refresh-interval": "Обновление настроек https каждые __interval__ часов", "settings-refreshed": "Настройки сервера https обновлены", "refresh-failed": "Не удалось обновить настройки https: __message__", - "nodejs-version": "httpsRefreshInterval требует Node.js 11 или выше", "function-required": "httpsRefreshInterval требует, чтобы свойство https было функцией" } }, diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index 35ec090c9..1f5e1238a 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -240,39 +240,34 @@ httpsPromise.then(function(startupHttps) { // Max value based on (2^31-1)ms - the max that setInterval can accept httpsRefreshInterval = 596; } - // Check whether setSecureContext is available (Node.js 11+) - if (server.setSecureContext) { - // Check whether `http` is a callable function - if (typeof settings.https === "function") { - delayedLogItems.push({type:"info", id:"server.https.refresh-interval", params:{interval:httpsRefreshInterval}}); - setInterval(function () { - try { - // Get the result of the function, because createServer doesn't accept functions as input - Promise.resolve(settings.https()).then(function(refreshedHttps) { - if (refreshedHttps) { - // The key/cert needs to be updated in the NodeJs http(s) server, when no key/cert is yet available or when the key/cert has changed. - // Note that the refreshed key/cert can be supplied as a string or a buffer. - var updateKey = (server.key == undefined || (Buffer.isBuffer(server.key) && !server.key.equals(refreshedHttps.key)) || (typeof server.key == "string" && server.key != refreshedHttps.key)); - var updateCert = (server.cert == undefined || (Buffer.isBuffer(server.cert) && !server.cert.equals(refreshedHttps.cert)) || (typeof server.cert == "string" && server.cert != refreshedHttps.cert)); + // Check whether `http` is a callable function + if (typeof settings.https === "function") { + delayedLogItems.push({type:"info", id:"server.https.refresh-interval", params:{interval:httpsRefreshInterval}}); + setInterval(function () { + try { + // Get the result of the function, because createServer doesn't accept functions as input + Promise.resolve(settings.https()).then(function(refreshedHttps) { + if (refreshedHttps) { + // The key/cert needs to be updated in the NodeJs http(s) server, when no key/cert is yet available or when the key/cert has changed. + // Note that the refreshed key/cert can be supplied as a string or a buffer. + var updateKey = (server.key == undefined || (Buffer.isBuffer(server.key) && !server.key.equals(refreshedHttps.key)) || (typeof server.key == "string" && server.key != refreshedHttps.key)); + var updateCert = (server.cert == undefined || (Buffer.isBuffer(server.cert) && !server.cert.equals(refreshedHttps.cert)) || (typeof server.cert == "string" && server.cert != refreshedHttps.cert)); - // Only update the credentials in the server when key or cert has changed - if(updateKey || updateCert) { - server.setSecureContext(refreshedHttps); - RED.log.info(RED.log._("server.https.settings-refreshed")); - } + // Only update the credentials in the server when key or cert has changed + if(updateKey || updateCert) { + server.setSecureContext(refreshedHttps); + RED.log.info(RED.log._("server.https.settings-refreshed")); } - }).catch(function(err) { - RED.log.error(RED.log._("server.https.refresh-failed",{message:err})); - }); - } catch(err) { + } + }).catch(function(err) { RED.log.error(RED.log._("server.https.refresh-failed",{message:err})); - } - }, httpsRefreshInterval*60*60*1000); - } else { - delayedLogItems.push({type:"warn", id:"server.https.function-required"}); - } + }); + } catch(err) { + RED.log.error(RED.log._("server.https.refresh-failed",{message:err})); + } + }, httpsRefreshInterval*60*60*1000); } else { - delayedLogItems.push({type:"warn", id:"server.https.nodejs-version"}); + delayedLogItems.push({type:"warn", id:"server.https.function-required"}); } } } else { From 18d0fa22590b7ef11ab06a56a00a2c59080eadbc Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 13 May 2024 14:19:24 +0100 Subject: [PATCH 49/71] Improve background conflict handling --- .../@node-red/editor-client/src/js/history.js | 39 +++++++- .../editor-client/src/js/ui/deploy.js | 91 ++++++++++--------- .../@node-red/editor-client/src/js/ui/diff.js | 73 ++++++++++----- .../editor-client/src/js/ui/notifications.js | 15 ++- 4 files changed, 149 insertions(+), 69 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/history.js b/packages/node_modules/@node-red/editor-client/src/js/history.js index c3a966890..66411264b 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/history.js +++ b/packages/node_modules/@node-red/editor-client/src/js/history.js @@ -59,18 +59,53 @@ RED.history = (function() { t: 'replace', config: RED.nodes.createCompleteNodeSet(), changed: {}, - rev: RED.nodes.version() + moved: {}, + complete: true, + rev: RED.nodes.version(), + dirty: RED.nodes.dirty() }; + var selectedTab = RED.workspaces.active(); + inverseEv.config.forEach(n => { + const node = RED.nodes.node(n.id) + if (node) { + inverseEv.changed[n.id] = node.changed + inverseEv.moved[n.id] = node.moved + } + }) RED.nodes.clear(); var imported = RED.nodes.import(ev.config); + // Clear all change flags from the import + RED.nodes.dirty(false); + + const flowsToLock = new Set() + function ensureUnlocked(id) { + const flow = id && (RED.nodes.workspace(id) || RED.nodes.subflow(id) || null); + const isLocked = flow ? flow.locked : false; + if (flow && isLocked) { + flow.locked = false; + flowsToLock.add(flow) + } + } imported.nodes.forEach(function(n) { if (ev.changed[n.id]) { + ensureUnlocked(n.z) n.changed = true; - inverseEv.changed[n.id] = true; } + if (ev.moved[n.id]) { + ensureUnlocked(n.z) + n.moved = true; + } + }) + flowsToLock.forEach(flow => { + flow.locked = true }) RED.nodes.version(ev.rev); + RED.view.redraw(true); + RED.palette.refresh(); + RED.workspaces.refresh(); + RED.workspaces.show(selectedTab, true); + RED.sidebar.config.refresh(); } else { var importMap = {}; ev.config.forEach(function(n) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js index 83c04d775..b766c43eb 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js @@ -34,6 +34,8 @@ RED.deploy = (function() { var currentDiff = null; + var activeBackgroundDeployNotification; + function changeDeploymentType(type) { deploymentType = type; $("#red-ui-header-button-deploy-icon").attr("src",deploymentTypes[type].img); @@ -133,37 +135,38 @@ RED.deploy = (function() { } }); - var activeNotifyMessage; RED.comms.subscribe("notification/runtime-deploy",function(topic,msg) { - if (!activeNotifyMessage) { - var currentRev = RED.nodes.version(); - if (currentRev === null || deployInflight || currentRev === msg.revision) { - return; - } - var message = $('

        ').text(RED._('deploy.confirm.backgroundUpdate')); - activeNotifyMessage = RED.notify(message,{ - modal: true, - fixed: true, - buttons: [ - { - text: RED._('deploy.confirm.button.ignore'), - click: function() { - activeNotifyMessage.close(); - activeNotifyMessage = null; - } - }, - { - text: RED._('deploy.confirm.button.review'), - class: "primary", - click: function() { - activeNotifyMessage.close(); - var nns = RED.nodes.createCompleteNodeSet(); - resolveConflict(nns,false); - activeNotifyMessage = null; - } + var currentRev = RED.nodes.version(); + if (currentRev === null || deployInflight || currentRev === msg.revision) { + return; + } + if (activeBackgroundDeployNotification?.hidden) { + activeBackgroundDeployNotification.showNotification() + return + } + const message = $('

        ').text(RED._('deploy.confirm.backgroundUpdate')); + const options = { + id: 'background-update', + type: 'compact', + modal: false, + fixed: true, + timeout: 10000, + buttons: [ + { + text: RED._('deploy.confirm.button.review'), + class: "primary", + click: function() { + activeBackgroundDeployNotification.hideNotification(); + var nns = RED.nodes.createCompleteNodeSet(); + resolveConflict(nns,false); } - ] - }); + } + ] + } + if (!activeBackgroundDeployNotification || activeBackgroundDeployNotification.closed) { + activeBackgroundDeployNotification = RED.notify(message, options) + } else { + activeBackgroundDeployNotification.update(message, options) } }); } @@ -220,7 +223,11 @@ RED.deploy = (function() { class: "primary disabled", click: function() { if (!$("#red-ui-deploy-dialog-confirm-deploy-review").hasClass('disabled')) { - RED.diff.showRemoteDiff(); + RED.diff.showRemoteDiff(null, { + onmerge: function () { + activeBackgroundDeployNotification.close() + } + }); conflictNotification.close(); } } @@ -233,6 +240,7 @@ RED.deploy = (function() { if (!$("#red-ui-deploy-dialog-confirm-deploy-merge").hasClass('disabled')) { RED.diff.mergeDiff(currentDiff); conflictNotification.close(); + activeBackgroundDeployNotification.close() } } } @@ -245,6 +253,7 @@ RED.deploy = (function() { click: function() { save(true,activeDeploy); conflictNotification.close(); + activeBackgroundDeployNotification.close() } }) } @@ -255,21 +264,17 @@ RED.deploy = (function() { buttons: buttons }); - var now = Date.now(); RED.diff.getRemoteDiff(function(diff) { - var ellapsed = Math.max(1000 - (Date.now()-now), 0); currentDiff = diff; - setTimeout(function() { - conflictCheck.hide(); - var d = Object.keys(diff.conflicts); - if (d.length === 0) { - conflictAutoMerge.show(); - $("#red-ui-deploy-dialog-confirm-deploy-merge").removeClass('disabled') - } else { - conflictManualMerge.show(); - } - $("#red-ui-deploy-dialog-confirm-deploy-review").removeClass('disabled') - },ellapsed); + conflictCheck.hide(); + var d = Object.keys(diff.conflicts); + if (d.length === 0) { + conflictAutoMerge.show(); + $("#red-ui-deploy-dialog-confirm-deploy-merge").removeClass('disabled') + } else { + conflictManualMerge.show(); + } + $("#red-ui-deploy-dialog-confirm-deploy-review").removeClass('disabled') }) } function cropList(list) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/diff.js b/packages/node_modules/@node-red/editor-client/src/js/ui/diff.js index 3f73e29aa..12c19cf71 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/diff.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/diff.js @@ -1099,11 +1099,11 @@ RED.diff = (function() { // var diff = generateDiff(originalFlow,nns); // showDiff(diff); // } - function showRemoteDiff(diff) { - if (diff === undefined) { - getRemoteDiff(showRemoteDiff); + function showRemoteDiff(diff, options = {}) { + if (!diff) { + getRemoteDiff((remoteDiff) => showRemoteDiff(remoteDiff, options)); } else { - showDiff(diff,{mode:'merge'}); + showDiff(diff,{...options, mode:'merge'}); } } function parseNodes(nodeList) { @@ -1240,7 +1240,7 @@ RED.diff = (function() { return diff; } - function showDiff(diff,options) { + function showDiff(diff, options) { if (diffVisible) { return; } @@ -1315,6 +1315,9 @@ RED.diff = (function() { if (!$("#red-ui-diff-view-diff-merge").hasClass('disabled')) { refreshConflictHeader(diff); mergeDiff(diff); + if (options.onmerge) { + options.onmerge() + } RED.tray.close(); } } @@ -1345,6 +1348,7 @@ RED.diff = (function() { var newConfig = []; var node; var nodeChangedStates = {}; + var nodeMovedStates = {}; var localChangedStates = {}; for (id in localDiff.newConfig.all) { if (localDiff.newConfig.all.hasOwnProperty(id)) { @@ -1352,12 +1356,14 @@ RED.diff = (function() { if (resolutions[id] === 'local') { if (node) { nodeChangedStates[id] = node.changed; + nodeMovedStates[id] = node.moved; } newConfig.push(localDiff.newConfig.all[id]); } else if (resolutions[id] === 'remote') { if (!remoteDiff.deleted[id] && remoteDiff.newConfig.all.hasOwnProperty(id)) { if (node) { nodeChangedStates[id] = node.changed; + nodeMovedStates[id] = node.moved; } localChangedStates[id] = 1; newConfig.push(remoteDiff.newConfig.all[id]); @@ -1381,8 +1387,9 @@ RED.diff = (function() { } return { config: newConfig, - nodeChangedStates: nodeChangedStates, - localChangedStates: localChangedStates + nodeChangedStates, + nodeMovedStates, + localChangedStates } } @@ -1393,6 +1400,7 @@ RED.diff = (function() { var newConfig = appliedDiff.config; var nodeChangedStates = appliedDiff.nodeChangedStates; + var nodeMovedStates = appliedDiff.nodeMovedStates; var localChangedStates = appliedDiff.localChangedStates; var isDirty = RED.nodes.dirty(); @@ -1401,33 +1409,56 @@ RED.diff = (function() { t:"replace", config: RED.nodes.createCompleteNodeSet(), changed: nodeChangedStates, + moved: nodeMovedStates, + complete: true, dirty: isDirty, rev: RED.nodes.version() } RED.history.push(historyEvent); - var originalFlow = RED.nodes.originalFlow(); - // originalFlow is what the editor things it loaded - // - add any newly added nodes from remote diff as they are now part of the record - for (var id in diff.remoteDiff.added) { - if (diff.remoteDiff.added.hasOwnProperty(id)) { - if (diff.remoteDiff.newConfig.all.hasOwnProperty(id)) { - originalFlow.push(JSON.parse(JSON.stringify(diff.remoteDiff.newConfig.all[id]))); - } - } - } + // var originalFlow = RED.nodes.originalFlow(); + // // originalFlow is what the editor thinks it loaded + // // - add any newly added nodes from remote diff as they are now part of the record + // for (var id in diff.remoteDiff.added) { + // if (diff.remoteDiff.added.hasOwnProperty(id)) { + // if (diff.remoteDiff.newConfig.all.hasOwnProperty(id)) { + // originalFlow.push(JSON.parse(JSON.stringify(diff.remoteDiff.newConfig.all[id]))); + // } + // } + // } RED.nodes.clear(); var imported = RED.nodes.import(newConfig); - // Restore the original flow so subsequent merge resolutions can properly - // identify new-vs-old - RED.nodes.originalFlow(originalFlow); + // // Restore the original flow so subsequent merge resolutions can properly + // // identify new-vs-old + // RED.nodes.originalFlow(originalFlow); + + // Clear all change flags from the import + RED.nodes.dirty(false); + + const flowsToLock = new Set() + function ensureUnlocked(id) { + const flow = id && (RED.nodes.workspace(id) || RED.nodes.subflow(id) || null); + const isLocked = flow ? flow.locked : false; + if (flow && isLocked) { + flow.locked = false; + flowsToLock.add(flow) + } + } imported.nodes.forEach(function(n) { - if (nodeChangedStates[n.id] || localChangedStates[n.id]) { + if (nodeChangedStates[n.id]) { + ensureUnlocked(n.z) n.changed = true; } + if (nodeMovedStates[n.id]) { + ensureUnlocked(n.z) + n.moved = true; + } + }) + flowsToLock.forEach(flow => { + flow.locked = true }) RED.nodes.version(diff.remoteDiff.rev); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/notifications.js b/packages/node_modules/@node-red/editor-client/src/js/ui/notifications.js index 30dcc4bd5..ef75ff8c4 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/notifications.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/notifications.js @@ -221,12 +221,12 @@ RED.notifications = (function() { if (newType) { n.className = "red-ui-notification red-ui-notification-"+newType; } - + newTimeout = newOptions.hasOwnProperty('timeout')?newOptions.timeout:timeout if (!fixed || newOptions.fixed === false) { - newTimeout = (newOptions.hasOwnProperty('timeout')?newOptions.timeout:timeout)||5000; + newTimeout === newTimeout || 5000 } if (newOptions.buttons) { - var buttonSet = $('

        ').appendTo(nn) + var buttonSet = $('
        ').appendTo(nn) newOptions.buttons.forEach(function(buttonDef) { var b = $('