mirror of
https://github.com/node-red/node-red.git
synced 2025-03-01 10:36:34 +00:00
Compare commits
74 Commits
pr_4387
...
allow-blan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
236e668201 | ||
|
|
c9b902c2b4 | ||
|
|
ac8b1e19b7 | ||
|
|
148e64c3da | ||
|
|
c6289ebb2c | ||
|
|
5f4ece6813 | ||
|
|
c990ec39d6 | ||
|
|
1fdc600ecd | ||
|
|
e354d2ce29 | ||
|
|
d218af8619 | ||
|
|
29ed5b2792 | ||
|
|
e39216e65a | ||
|
|
7ac7f9b4c8 | ||
|
|
4709eb9d49 | ||
|
|
c13b8266dd | ||
|
|
bd58431603 | ||
|
|
3075b82792 | ||
|
|
240082481f | ||
|
|
ea95552285 | ||
|
|
5358b06123 | ||
|
|
99391431da | ||
|
|
d396f50a9a | ||
|
|
affa8ea42b | ||
|
|
d711b01fe5 | ||
|
|
6e7fa6f921 | ||
|
|
343cde75a2 | ||
|
|
2dc446e45b | ||
|
|
884b7fa16a | ||
|
|
173e065b68 | ||
|
|
9a3cb0b2b5 | ||
|
|
6beae5a806 | ||
|
|
66f4008bb8 | ||
|
|
a0636632a1 | ||
|
|
5dfa47ab6c | ||
|
|
e9efe493f9 | ||
|
|
3bd782e62a | ||
|
|
963fe87f14 | ||
|
|
ade4679e8c | ||
|
|
40060a470b | ||
|
|
a6e8fbb54a | ||
|
|
410b938442 | ||
|
|
ab7e9f94fa | ||
|
|
28e9ccd372 | ||
|
|
9a66d9addd | ||
|
|
8843bda477 | ||
|
|
3278303eec | ||
|
|
f5fd6e3a36 | ||
|
|
a173e8e70f | ||
|
|
19dcc3a683 | ||
|
|
20d067c1ea | ||
|
|
9526566799 | ||
|
|
0b9dd82c91 | ||
|
|
19213434f9 | ||
|
|
014691346a | ||
|
|
6738b95c29 | ||
|
|
6a8230ec1e | ||
|
|
5679d264b6 | ||
|
|
b20c5f3a8d | ||
|
|
014f206e9c | ||
|
|
068b93befa | ||
|
|
65d8872cea | ||
|
|
bffd1d61b2 | ||
|
|
4788b81220 | ||
|
|
9a07fc03c6 | ||
|
|
954f518030 | ||
|
|
9f8ff71757 | ||
|
|
06dd59dc81 | ||
|
|
37265cf4ef | ||
|
|
8a63275989 | ||
|
|
7fc64a84e8 | ||
|
|
02f7cdd5aa | ||
|
|
d7dcceef60 | ||
|
|
ae5e1570ae | ||
|
|
3ca045394a |
37
CHANGELOG.md
37
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
|
||||
@@ -29,6 +50,22 @@ Nodes
|
||||
- Let debug node status msg length be settable via settings (#4402) @dceejay
|
||||
- Feat: Add ability to set headers for WebSocket client (#4436) @marcus-j-davies
|
||||
|
||||
#### 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
|
||||
- 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
|
||||
|
||||
@@ -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",
|
||||
|
||||
10
package.json
10
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",
|
||||
@@ -41,7 +41,7 @@
|
||||
"cors": "2.8.5",
|
||||
"cronosjs": "1.7.1",
|
||||
"denque": "2.1.0",
|
||||
"express": "4.18.2",
|
||||
"express": "4.19.2",
|
||||
"express-session": "1.17.3",
|
||||
"form-data": "4.0.0",
|
||||
"fs-extra": "11.1.1",
|
||||
@@ -64,7 +64,7 @@
|
||||
"mqtt": "4.3.7",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"mustache": "4.2.0",
|
||||
"node-red-admin": "^3.1.2",
|
||||
"node-red-admin": "^3.1.3",
|
||||
"node-watch": "0.7.4",
|
||||
"nopt": "5.0.0",
|
||||
"oauth2orize": "1.11.1",
|
||||
@@ -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",
|
||||
@@ -112,7 +112,7 @@
|
||||
"mermaid": "^10.4.0",
|
||||
"minami": "1.2.3",
|
||||
"mocha": "9.2.2",
|
||||
"node-red-node-test-helper": "^0.3.2",
|
||||
"node-red-node-test-helper": "^0.3.3",
|
||||
"nodemon": "2.0.20",
|
||||
"proxy": "^1.0.2",
|
||||
"sass": "1.62.1",
|
||||
|
||||
@@ -77,6 +77,53 @@ function CommsConnection(ws, user) {
|
||||
log.trace("comms.close "+self.session);
|
||||
removeActiveConnection(self);
|
||||
});
|
||||
|
||||
const handleAuthPacket = function(msg) {
|
||||
Tokens.get(msg.auth).then(function(client) {
|
||||
if (client) {
|
||||
Users.get(client.user).then(function(user) {
|
||||
if (user) {
|
||||
self.user = user;
|
||||
log.audit({event: "comms.auth",user:self.user});
|
||||
completeConnection(msg, client.scope,msg.auth,true);
|
||||
} else {
|
||||
log.audit({event: "comms.auth.fail"});
|
||||
completeConnection(msg, null,null,false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Users.tokens(msg.auth).then(function(user) {
|
||||
if (user) {
|
||||
self.user = user;
|
||||
log.audit({event: "comms.auth",user:self.user});
|
||||
completeConnection(msg, user.permissions,msg.auth,true);
|
||||
} else {
|
||||
log.audit({event: "comms.auth.fail"});
|
||||
completeConnection(msg, null,null,false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
const completeConnection = function(msg, userScope, session, sendAck) {
|
||||
try {
|
||||
if (!userScope || !Permissions.hasPermission(userScope,"status.read")) {
|
||||
ws.send(JSON.stringify({auth:"fail"}));
|
||||
ws.close();
|
||||
} else {
|
||||
pendingAuth = false;
|
||||
addActiveConnection(self);
|
||||
self.token = msg.auth;
|
||||
if (sendAck) {
|
||||
ws.send(JSON.stringify({auth:"ok"}));
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
console.log(err.stack);
|
||||
// Just in case the socket closes before we attempt
|
||||
// to send anything.
|
||||
}
|
||||
}
|
||||
ws.on('message', function(data,flags) {
|
||||
var msg = null;
|
||||
try {
|
||||
@@ -86,68 +133,34 @@ function CommsConnection(ws, user) {
|
||||
return;
|
||||
}
|
||||
if (!pendingAuth) {
|
||||
if (msg.subscribe) {
|
||||
if (msg.auth) {
|
||||
handleAuthPacket(msg)
|
||||
} else if (msg.subscribe) {
|
||||
self.subscribe(msg.subscribe);
|
||||
// handleRemoteSubscription(ws,msg.subscribe);
|
||||
} else if (msg.topic) {
|
||||
runtimeAPI.comms.receive({
|
||||
user: self.user,
|
||||
client: self,
|
||||
topic: msg.topic,
|
||||
data: msg.data
|
||||
})
|
||||
}
|
||||
} else {
|
||||
var completeConnection = function(userScope,session,sendAck) {
|
||||
try {
|
||||
if (!userScope || !Permissions.hasPermission(userScope,"status.read")) {
|
||||
ws.send(JSON.stringify({auth:"fail"}));
|
||||
ws.close();
|
||||
} else {
|
||||
pendingAuth = false;
|
||||
addActiveConnection(self);
|
||||
self.token = msg.auth;
|
||||
if (sendAck) {
|
||||
ws.send(JSON.stringify({auth:"ok"}));
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
console.log(err.stack);
|
||||
// Just in case the socket closes before we attempt
|
||||
// to send anything.
|
||||
}
|
||||
}
|
||||
if (msg.auth) {
|
||||
Tokens.get(msg.auth).then(function(client) {
|
||||
if (client) {
|
||||
Users.get(client.user).then(function(user) {
|
||||
if (user) {
|
||||
self.user = user;
|
||||
log.audit({event: "comms.auth",user:self.user});
|
||||
completeConnection(client.scope,msg.auth,true);
|
||||
} else {
|
||||
log.audit({event: "comms.auth.fail"});
|
||||
completeConnection(null,null,false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Users.tokens(msg.auth).then(function(user) {
|
||||
if (user) {
|
||||
self.user = user;
|
||||
log.audit({event: "comms.auth",user:self.user});
|
||||
completeConnection(user.permissions,msg.auth,true);
|
||||
} else {
|
||||
log.audit({event: "comms.auth.fail"});
|
||||
completeConnection(null,null,false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
handleAuthPacket(msg)
|
||||
} else {
|
||||
if (anonymousUser) {
|
||||
log.audit({event: "comms.auth",user:anonymousUser});
|
||||
self.user = anonymousUser;
|
||||
completeConnection(anonymousUser.permissions,null,false);
|
||||
completeConnection(msg, anonymousUser.permissions, null, false);
|
||||
//TODO: duplicated code - pull non-auth message handling out
|
||||
if (msg.subscribe) {
|
||||
self.subscribe(msg.subscribe);
|
||||
}
|
||||
} else {
|
||||
log.audit({event: "comms.auth.fail"});
|
||||
completeConnection(null,null,false);
|
||||
completeConnection(msg, null,null,false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,14 +16,14 @@
|
||||
}
|
||||
],
|
||||
"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",
|
||||
"cors": "2.8.5",
|
||||
"express-session": "1.17.3",
|
||||
"express": "4.18.2",
|
||||
"express": "4.19.2",
|
||||
"memorystore": "1.6.7",
|
||||
"mime": "3.0.0",
|
||||
"multer": "1.4.5-lts.1",
|
||||
|
||||
@@ -643,6 +643,7 @@
|
||||
"errors": {
|
||||
"catalogLoadFailed": "<p>Failed to load node catalogue.</p><p>Check the browser console for more information</p>",
|
||||
"installFailed": "<p>Failed to install: __module__</p><p>__message__</p><p>Check the log for more information</p>",
|
||||
"installTimeout": "<p>Install continuing the background.</p><p>Nodes will appear in palette when complete. Check the log for more information.</p>",
|
||||
"removeFailed": "<p>Failed to remove: __module__</p><p>__message__</p><p>Check the log for more information</p>",
|
||||
"updateFailed": "<p>Failed to update: __module__</p><p>__message__</p><p>Check the log for more information</p>",
|
||||
"enableFailed": "<p>Failed to enable: __module__</p><p>__message__</p><p>Check the log for more information</p>",
|
||||
|
||||
@@ -924,7 +924,14 @@
|
||||
"date": "horodatage",
|
||||
"jsonata": "expression",
|
||||
"env": "variable d'environnement",
|
||||
"cred": "identifiant"
|
||||
"cred": "identifiant",
|
||||
"conf-types": "noeud de configuration"
|
||||
},
|
||||
"date": {
|
||||
"format": {
|
||||
"timestamp": "millisecondes depuis l'époque",
|
||||
"object": "Objet de date JavaScript"
|
||||
}
|
||||
}
|
||||
},
|
||||
"editableList": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -26,6 +26,15 @@ RED.comms = (function() {
|
||||
var reconnectAttempts = 0;
|
||||
var active = false;
|
||||
|
||||
RED.events.on('login', function(username) {
|
||||
// User has logged in
|
||||
// Need to upgrade the connection to be authenticated
|
||||
if (ws && ws.readyState == 1) {
|
||||
const auth_tokens = RED.settings.get("auth-tokens");
|
||||
ws.send(JSON.stringify({auth:auth_tokens.access_token}))
|
||||
}
|
||||
})
|
||||
|
||||
function connectWS() {
|
||||
active = true;
|
||||
var wspath;
|
||||
@@ -56,6 +65,7 @@ RED.comms = (function() {
|
||||
ws.send(JSON.stringify({subscribe:t}));
|
||||
}
|
||||
}
|
||||
emit('connect')
|
||||
}
|
||||
|
||||
ws = new WebSocket(wspath);
|
||||
@@ -180,9 +190,53 @@ RED.comms = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function send(topic, msg) {
|
||||
if (ws && ws.readyState == 1) {
|
||||
ws.send(JSON.stringify({
|
||||
topic,
|
||||
data: msg
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const eventHandlers = {};
|
||||
function on(evt,func) {
|
||||
eventHandlers[evt] = eventHandlers[evt]||[];
|
||||
eventHandlers[evt].push(func);
|
||||
}
|
||||
function off(evt,func) {
|
||||
const handler = eventHandlers[evt];
|
||||
if (handler) {
|
||||
for (let i=0;i<handler.length;i++) {
|
||||
if (handler[i] === func) {
|
||||
handler.splice(i,1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function emit() {
|
||||
const evt = arguments[0]
|
||||
const args = Array.prototype.slice.call(arguments,1);
|
||||
if (eventHandlers[evt]) {
|
||||
let cpyHandlers = [...eventHandlers[evt]];
|
||||
for (let i=0;i<cpyHandlers.length;i++) {
|
||||
try {
|
||||
cpyHandlers[i].apply(null, args);
|
||||
} catch(err) {
|
||||
console.warn("RED.comms.emit error: ["+evt+"] "+(err.toString()));
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
connect: connectWS,
|
||||
subscribe: subscribe,
|
||||
unsubscribe:unsubscribe
|
||||
unsubscribe:unsubscribe,
|
||||
on,
|
||||
off,
|
||||
send
|
||||
}
|
||||
})();
|
||||
|
||||
217
packages/node_modules/@node-red/editor-client/src/js/multiplayer.js
vendored
Normal file
217
packages/node_modules/@node-red/editor-client/src/js/multiplayer.js
vendored
Normal file
@@ -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 = $('<li class="red-ui-multiplayer-user"><button type="button" class="red-ui-multiplayer-user-icon" href="#"></button></li>')
|
||||
.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 = $('<div>')
|
||||
$('<div style="text-align: center">').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) {
|
||||
$('<div>').text(`${ws.type}: ${ws.label||ws.name||ws.id}`).appendTo(content)
|
||||
} else {
|
||||
$('<div>').text(`tab: unknown`).appendTo(content)
|
||||
}
|
||||
}
|
||||
if (location.node) {
|
||||
const node = RED.nodes.node(location.node)
|
||||
if (node) {
|
||||
$('<div>').text(`node: ${node.id}`).appendTo(content)
|
||||
} else {
|
||||
$('<div>').text(`node: unknown`).appendTo(content)
|
||||
}
|
||||
}
|
||||
return content
|
||||
},
|
||||
}).open()
|
||||
})
|
||||
if (!user.user.image) {
|
||||
$('<i class="fa fa-user"></i>').appendTo(button);
|
||||
} else {
|
||||
$('<span class="user-profile"></span>').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 = $('<li><ul id="red-ui-multiplayer-user-list"></ul></li>').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)
|
||||
}
|
||||
}
|
||||
})();
|
||||
@@ -574,12 +574,16 @@ RED.nodes = (function() {
|
||||
* @param {String} z tab id
|
||||
*/
|
||||
checkTabState: function (z) {
|
||||
const ws = workspaces[z]
|
||||
const ws = workspaces[z] || subflows[z]
|
||||
if (ws) {
|
||||
const contentsChanged = tabDirtyMap[z].size > 0 || tabDeletedNodesMap[z].size > 0
|
||||
if (Boolean(ws.contentsChanged) !== contentsChanged) {
|
||||
ws.contentsChanged = contentsChanged
|
||||
RED.events.emit("flows:change", ws);
|
||||
if (ws.type === 'tab') {
|
||||
RED.events.emit("flows:change", ws);
|
||||
} else {
|
||||
RED.events.emit("subflows:change", ws);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1052,7 +1056,22 @@ RED.nodes = (function() {
|
||||
RED.nodes.registerType("subflow:"+sf.id, {
|
||||
defaults:{
|
||||
name:{value:""},
|
||||
env:{value:[]}
|
||||
env:{value:[], validate: function(value) {
|
||||
const errors = []
|
||||
if (value) {
|
||||
value.forEach(env => {
|
||||
const r = RED.utils.validateTypedProperty(env.value, env.type)
|
||||
if (r !== true) {
|
||||
errors.push(env.name+': '+r)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (errors.length === 0) {
|
||||
return true
|
||||
} else {
|
||||
return errors
|
||||
}
|
||||
}}
|
||||
},
|
||||
icon: function() { return sf.icon||"subflow.svg" },
|
||||
category: sf.category || "subflows",
|
||||
|
||||
@@ -839,6 +839,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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -118,10 +118,16 @@ RED.contextMenu = (function () {
|
||||
onselect: 'core:split-wire-with-link-nodes',
|
||||
disabled: !canEdit || !hasLinks
|
||||
},
|
||||
null,
|
||||
{ onselect: 'core:show-import-dialog', label: RED._('common.label.import')},
|
||||
{ onselect: 'core:show-examples-import-dialog', label: RED._('menu.label.importExample') }
|
||||
null
|
||||
)
|
||||
if (RED.settings.theme("menu.menu-item-import-library", true)) {
|
||||
insertOptions.push(
|
||||
{ onselect: 'core:show-import-dialog', label: RED._('common.label.import')},
|
||||
{ onselect: 'core:show-examples-import-dialog', label: RED._('menu.label.importExample') }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
if (hasSelection && canEdit) {
|
||||
const nodeOptions = []
|
||||
if (!hasMultipleSelection && !isGroup) {
|
||||
@@ -194,8 +200,14 @@ RED.contextMenu = (function () {
|
||||
{ onselect: 'core:paste-from-internal-clipboard', label: RED._("keyboard.pasteNode"), disabled: !canEdit || !RED.view.clipboard() },
|
||||
{ onselect: 'core:delete-selection', label: RED._('keyboard.deleteSelected'), disabled: !canEdit || !canDelete },
|
||||
{ onselect: 'core:delete-selection-and-reconnect', label: RED._('keyboard.deleteReconnect'), disabled: !canEdit || !canDelete },
|
||||
{ onselect: 'core:show-export-dialog', label: RED._("menu.label.export") },
|
||||
{ onselect: 'core:select-all-nodes', label: RED._("keyboard.selectAll") },
|
||||
)
|
||||
if (RED.settings.theme("menu.menu-item-export-library", true)) {
|
||||
menuItems.push(
|
||||
{ onselect: 'core:show-export-dialog', label: RED._("menu.label.export") }
|
||||
)
|
||||
}
|
||||
menuItems.push(
|
||||
{ onselect: 'core:select-all-nodes', label: RED._("keyboard.selectAll") }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -341,8 +341,9 @@ RED.editor = (function() {
|
||||
nodeValue = node[property]
|
||||
}
|
||||
|
||||
const buttonId = `${prefix}-lookup-${property}`
|
||||
const selectId = prefix + '-' + property
|
||||
const addBtnId = `${prefix}-btn-${property}-add`;
|
||||
const editBtnId = `${prefix}-btn-${property}-edit`;
|
||||
const selectId = prefix + '-' + property;
|
||||
const input = $(`#${selectId}`);
|
||||
if (input.length === 0) {
|
||||
return;
|
||||
@@ -365,40 +366,68 @@ 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 = $('<a id="' + buttonId + '" class="red-ui-button"><i class="fa fa-pencil"></i></a>')
|
||||
const editButton = $('<a id="' + editBtnId + '" class="red-ui-button"><i class="fa fa-pencil"></i></a>')
|
||||
.css({ "margin-left": "10px" })
|
||||
.appendTo(outerWrap);
|
||||
|
||||
RED.popover.tooltip(editButton, RED._('editor.editConfig', { type }));
|
||||
|
||||
// create the add button
|
||||
const addButton = $('<a id="' + addBtnId + '" class="red-ui-button"><i class="fa fa-plus"></i></a>')
|
||||
.css({ "margin-left": "10px" })
|
||||
.appendTo(outerWrap);
|
||||
RED.popover.tooltip(addButton, RED._('editor.addNewConfig', { type }));
|
||||
|
||||
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 if (selectedOpt.val() === "") {
|
||||
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 +921,12 @@ RED.editor = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
select.append('<option value="_ADD_"'+(value===""?" selected":"")+'>'+RED._("editor.addNewType", {type:label})+'</option>');
|
||||
if (!configNodes.length) {
|
||||
select.append('<option value="_ADD_" selected>' + RED._("editor.addNewType", { type: label }) + '</option>');
|
||||
} else {
|
||||
select.append('<option value="">' + RED._("editor.inputs.none") + '</option>');
|
||||
}
|
||||
|
||||
window.setTimeout(function() { select.trigger("change");},50);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,10 +153,6 @@ RED.envVar = (function() {
|
||||
}
|
||||
|
||||
function init(done) {
|
||||
if (!RED.user.hasPermission("settings.write")) {
|
||||
RED.notify(RED._("user.errors.settings"),"error");
|
||||
return;
|
||||
}
|
||||
RED.userSettings.add({
|
||||
id:'envvar',
|
||||
title: RED._("env-var.environment"),
|
||||
|
||||
@@ -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) {
|
||||
@@ -1405,9 +1405,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.errors.installTimeout"), {
|
||||
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',
|
||||
|
||||
@@ -35,6 +35,10 @@ RED.palette = (function() {
|
||||
var categoryContainers = {};
|
||||
var sidebarControls;
|
||||
|
||||
let paletteState = { filter: "", collapsed: [] };
|
||||
|
||||
let filterRefreshTimeout
|
||||
|
||||
function createCategory(originalCategory,rootCategory,category,ns) {
|
||||
if ($("#red-ui-palette-base-category-"+rootCategory).length === 0) {
|
||||
createCategoryContainer(originalCategory,rootCategory, ns+":palette.label."+rootCategory);
|
||||
@@ -60,20 +64,57 @@ RED.palette = (function() {
|
||||
catDiv.data('label',label);
|
||||
categoryContainers[category] = {
|
||||
container: catDiv,
|
||||
close: function() {
|
||||
hide: function (instant) {
|
||||
if (instant) {
|
||||
catDiv.hide()
|
||||
} else {
|
||||
catDiv.slideUp()
|
||||
}
|
||||
},
|
||||
show: function () {
|
||||
catDiv.show()
|
||||
},
|
||||
isOpen: function () {
|
||||
return !!catDiv.hasClass("red-ui-palette-open")
|
||||
},
|
||||
getNodeCount: function (visibleOnly) {
|
||||
const nodes = catDiv.find(".red-ui-palette-node")
|
||||
if (visibleOnly) {
|
||||
return nodes.filter(function() { return $(this).css('display') !== 'none'}).length
|
||||
} else {
|
||||
return nodes.length
|
||||
}
|
||||
},
|
||||
close: function(instant, skipSaveState) {
|
||||
catDiv.removeClass("red-ui-palette-open");
|
||||
catDiv.addClass("red-ui-palette-closed");
|
||||
$("#red-ui-palette-base-category-"+category).slideUp();
|
||||
if (instant) {
|
||||
$("#red-ui-palette-base-category-"+category).hide();
|
||||
} else {
|
||||
$("#red-ui-palette-base-category-"+category).slideUp();
|
||||
}
|
||||
$("#red-ui-palette-header-"+category+" i").removeClass("expanded");
|
||||
if (!skipSaveState) {
|
||||
if (!paletteState.collapsed.includes(category)) {
|
||||
paletteState.collapsed.push(category);
|
||||
savePaletteState();
|
||||
}
|
||||
}
|
||||
},
|
||||
open: function() {
|
||||
open: function(skipSaveState) {
|
||||
catDiv.addClass("red-ui-palette-open");
|
||||
catDiv.removeClass("red-ui-palette-closed");
|
||||
$("#red-ui-palette-base-category-"+category).slideDown();
|
||||
$("#red-ui-palette-header-"+category+" i").addClass("expanded");
|
||||
if (!skipSaveState) {
|
||||
if (paletteState.collapsed.includes(category)) {
|
||||
paletteState.collapsed.splice(paletteState.collapsed.indexOf(category), 1);
|
||||
savePaletteState();
|
||||
}
|
||||
}
|
||||
},
|
||||
toggle: function() {
|
||||
if (catDiv.hasClass("red-ui-palette-open")) {
|
||||
if (categoryContainers[category].isOpen()) {
|
||||
categoryContainers[category].close();
|
||||
} else {
|
||||
categoryContainers[category].open();
|
||||
@@ -415,8 +456,16 @@ RED.palette = (function() {
|
||||
|
||||
var categoryNode = $("#red-ui-palette-container-"+rootCategory);
|
||||
if (categoryNode.find(".red-ui-palette-node").length === 1) {
|
||||
categoryContainers[rootCategory].open();
|
||||
if (!paletteState?.collapsed?.includes(rootCategory)) {
|
||||
categoryContainers[rootCategory].open();
|
||||
} else {
|
||||
categoryContainers[rootCategory].close(true);
|
||||
}
|
||||
}
|
||||
clearTimeout(filterRefreshTimeout)
|
||||
filterRefreshTimeout = setTimeout(() => {
|
||||
refreshFilter()
|
||||
}, 200)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -516,7 +565,8 @@ RED.palette = (function() {
|
||||
paletteNode.css("backgroundColor", sf.color);
|
||||
}
|
||||
|
||||
function filterChange(val) {
|
||||
function refreshFilter() {
|
||||
const val = $("#red-ui-palette-search input").val()
|
||||
var re = new RegExp(val.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'),'i');
|
||||
$("#red-ui-palette-container .red-ui-palette-node").each(function(i,el) {
|
||||
var currentLabel = $(el).attr("data-palette-label");
|
||||
@@ -528,16 +578,26 @@ RED.palette = (function() {
|
||||
}
|
||||
});
|
||||
|
||||
for (var category in categoryContainers) {
|
||||
for (let category in categoryContainers) {
|
||||
if (categoryContainers.hasOwnProperty(category)) {
|
||||
if (categoryContainers[category].container
|
||||
.find(".red-ui-palette-node")
|
||||
.filter(function() { return $(this).css('display') !== 'none'}).length === 0) {
|
||||
categoryContainers[category].close();
|
||||
categoryContainers[category].container.slideUp();
|
||||
const categorySection = categoryContainers[category]
|
||||
if (categorySection.getNodeCount(true) === 0) {
|
||||
categorySection.hide()
|
||||
} else {
|
||||
categoryContainers[category].open();
|
||||
categoryContainers[category].container.show();
|
||||
categorySection.show()
|
||||
if (val) {
|
||||
// There is a filter being applied and it has matched
|
||||
// something in this category - show the contents
|
||||
categorySection.open(true)
|
||||
} else {
|
||||
// No filter. Only show the category if it isn't in lastState
|
||||
if (!paletteState.collapsed.includes(category)) {
|
||||
categorySection.open(true)
|
||||
} else if (categorySection.isOpen()) {
|
||||
// This section should be collapsed but isn't - so make it so
|
||||
categorySection.close(true, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -553,6 +613,9 @@ RED.palette = (function() {
|
||||
|
||||
$("#red-ui-palette > .red-ui-palette-spinner").show();
|
||||
|
||||
RED.events.on('logout', function () {
|
||||
RED.settings.removeLocal('palette-state')
|
||||
})
|
||||
|
||||
RED.events.on('registry:node-type-added', function(nodeType) {
|
||||
var def = RED.nodes.getType(nodeType);
|
||||
@@ -596,14 +659,14 @@ RED.palette = (function() {
|
||||
|
||||
RED.events.on("subflows:change",refreshSubflow);
|
||||
|
||||
|
||||
|
||||
$("#red-ui-palette-search input").searchBox({
|
||||
delay: 100,
|
||||
change: function() {
|
||||
filterChange($(this).val());
|
||||
refreshFilter();
|
||||
paletteState.filter = $(this).val();
|
||||
savePaletteState();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
sidebarControls = $('<div class="red-ui-sidebar-control-left"><i class="fa fa-chevron-left"></i></div>').appendTo($("#red-ui-palette"));
|
||||
RED.popover.tooltip(sidebarControls,RED._("keyboard.togglePalette"),"core:toggle-palette");
|
||||
@@ -669,7 +732,23 @@ RED.palette = (function() {
|
||||
togglePalette(state);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
paletteState = JSON.parse(RED.settings.getLocal("palette-state") || '{"filter":"", "collapsed": []}');
|
||||
if (paletteState.filter) {
|
||||
// Apply the category filter
|
||||
$("#red-ui-palette-search input").searchBox("value", paletteState.filter);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Unexpected error loading palette state from localStorage: ", error);
|
||||
}
|
||||
setTimeout(() => {
|
||||
// Lazily tidy up any categories that haven't been reloaded
|
||||
paletteState.collapsed = paletteState.collapsed.filter(category => !!categoryContainers[category])
|
||||
savePaletteState()
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
function togglePalette(state) {
|
||||
if (!state) {
|
||||
$("#red-ui-main-container").addClass("red-ui-palette-closed");
|
||||
@@ -689,6 +768,15 @@ RED.palette = (function() {
|
||||
})
|
||||
return categories;
|
||||
}
|
||||
|
||||
function savePaletteState() {
|
||||
try {
|
||||
RED.settings.setLocal("palette-state", JSON.stringify(paletteState));
|
||||
} catch (error) {
|
||||
console.error("Unexpected error saving palette state to localStorage: ", error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
add:addNodeType,
|
||||
|
||||
@@ -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 = {
|
||||
@@ -1357,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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -491,6 +491,11 @@ RED.workspaces = (function() {
|
||||
createWorkspaceTabs();
|
||||
RED.events.on("sidebar:resize",workspace_tabs.resize);
|
||||
|
||||
RED.events.on("workspace:clear", () => {
|
||||
// Reset the index used to generate new flow names
|
||||
workspaceIndex = 0
|
||||
})
|
||||
|
||||
RED.actions.add("core:show-next-tab",function() {
|
||||
var oldActive = activeWorkspace;
|
||||
workspace_tabs.nextTab();
|
||||
@@ -657,6 +662,9 @@ RED.workspaces = (function() {
|
||||
RED.events.on("flows:change", (ws) => {
|
||||
$("#red-ui-tab-"+(ws.id.replace(".","-"))).toggleClass('red-ui-workspace-changed',!!(ws.contentsChanged || ws.changed || ws.added));
|
||||
})
|
||||
RED.events.on("subflows:change", (ws) => {
|
||||
$("#red-ui-tab-"+(ws.id.replace(".","-"))).toggleClass('red-ui-workspace-changed',!!(ws.contentsChanged || ws.changed || ws.added));
|
||||
})
|
||||
|
||||
hideWorkspace();
|
||||
}
|
||||
|
||||
@@ -187,6 +187,7 @@ RED.user = (function() {
|
||||
}
|
||||
|
||||
function logout() {
|
||||
RED.events.emit('logout')
|
||||
var tokens = RED.settings.get("auth-tokens");
|
||||
var token = tokens?tokens.access_token:"";
|
||||
$.ajax({
|
||||
@@ -225,6 +226,7 @@ RED.user = (function() {
|
||||
});
|
||||
}
|
||||
});
|
||||
$('<i class="fa fa-user"></i>').appendTo("#red-ui-header-button-user");
|
||||
} else {
|
||||
RED.menu.addItem("red-ui-header-button-user",{
|
||||
id:"usermenu-item-username",
|
||||
@@ -237,6 +239,15 @@ RED.user = (function() {
|
||||
RED.user.logout();
|
||||
}
|
||||
});
|
||||
const userMenu = $("#red-ui-header-button-user")
|
||||
userMenu.empty()
|
||||
if (RED.settings.user.image) {
|
||||
$('<span class="user-profile"></span>').css({
|
||||
backgroundImage: "url("+RED.settings.user.image+")",
|
||||
}).appendTo(userMenu);
|
||||
} else {
|
||||
$('<i class="fa fa-user"></i>').appendTo(userMenu);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -247,14 +258,6 @@ RED.user = (function() {
|
||||
|
||||
var userMenu = $('<li><a id="red-ui-header-button-user" class="button hide" href="#"></a></li>')
|
||||
.prependTo(".red-ui-header-toolbar");
|
||||
if (RED.settings.user.image) {
|
||||
$('<span class="user-profile"></span>').css({
|
||||
backgroundImage: "url("+RED.settings.user.image+")",
|
||||
}).appendTo(userMenu.find("a"));
|
||||
} else {
|
||||
$('<i class="fa fa-user"></i>').appendTo(userMenu.find("a"));
|
||||
}
|
||||
|
||||
RED.menu.init({id:"red-ui-header-button-user",
|
||||
options: []
|
||||
});
|
||||
|
||||
@@ -63,25 +63,29 @@
|
||||
}
|
||||
|
||||
.red-ui-header-toolbar {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
float: right;
|
||||
|
||||
> li {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
height: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
display: inline-block;
|
||||
font-size: 20px;
|
||||
padding: 0px 12px;
|
||||
text-decoration: none;
|
||||
@@ -271,13 +275,13 @@
|
||||
color: var(--red-ui-header-menu-heading-color);
|
||||
}
|
||||
|
||||
#red-ui-header-button-user .user-profile {
|
||||
.user-profile {
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 35px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
48
packages/node_modules/@node-red/editor-client/src/sass/multiplayer.scss
vendored
Normal file
48
packages/node_modules/@node-red/editor-client/src/sass/multiplayer.scss
vendored
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -73,3 +73,5 @@
|
||||
@import "radialMenu";
|
||||
|
||||
@import "tourGuide";
|
||||
|
||||
@import "multiplayer";
|
||||
|
||||
BIN
packages/node_modules/@node-red/editor-client/src/tours/images/nr4-config-select.png
vendored
Normal file
BIN
packages/node_modules/@node-red/editor-client/src/tours/images/nr4-config-select.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
BIN
packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer.png
vendored
Normal file
BIN
packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
packages/node_modules/@node-red/editor-client/src/tours/images/nr4-plugins.png
vendored
Normal file
BIN
packages/node_modules/@node-red/editor-client/src/tours/images/nr4-plugins.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -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": "<p>Let's take a moment to discover the new features in this release.</p>",
|
||||
@@ -14,10 +14,63 @@ export default {
|
||||
"fr": "<p>Prenons un moment pour découvrir les nouvelles fonctionnalités de cette version.</p>"
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Multiplayer Mode"
|
||||
},
|
||||
image: 'images/nr4-multiplayer.png',
|
||||
description: {
|
||||
"en-US": `<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>Check the release post for details on how to enable this feature in your settings file.</p>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Better Configuration Node UX"
|
||||
},
|
||||
image: 'images/nr4-config-select.png',
|
||||
description: {
|
||||
"en-US": `<p>The Configuration node selection UI has had a small update to have a dedicated 'add' button
|
||||
next to the select box.</p>
|
||||
<p>It's a small change, but should make it easier to work with your config nodes.</p>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Remembering palette state"
|
||||
},
|
||||
description: {
|
||||
"en-US": `<p>The palette now remembers what categories you have hidden between reloads - as well as any
|
||||
filter you have applied.</p>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Plugins shown in the Palette Manager"
|
||||
},
|
||||
image: 'images/nr4-plugins.png',
|
||||
description: {
|
||||
"en-US": `<p>The palette manager now shows any plugin modules you have installed, such as
|
||||
<code>node-red-debugger</code>. Previously they would only be shown if they plugin include
|
||||
nodes for the palette.</p>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "That's if for Beta 2!"
|
||||
},
|
||||
description: {
|
||||
"en-US": `<p>Keep clicking through to see what was added in Beta 1</p>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Timestamp formatting options",
|
||||
"ja": "タイムスタンプの形式の項目"
|
||||
"ja": "タイムスタンプの形式の項目",
|
||||
"fr": "Options de formatage de l'horodatage"
|
||||
},
|
||||
image: 'images/nr4-timestamp-formatting.png',
|
||||
description: {
|
||||
@@ -34,13 +87,21 @@ export default {
|
||||
<li>エポックからのミリ秒 - 従来動作と同じになるタイムスタンプの項目</li>
|
||||
<li>ISO 8601 - 多くのシステムで使用されている共通の形式</li>
|
||||
<li>JavaScript日付オブジェクト</li>
|
||||
</ul>`,
|
||||
"fr": `<p>Les noeuds qui vous permettent de définir un horodatage disposent désormais d'options sur le format dans lequel cet horodatage peut être défini.</p>
|
||||
<p>Nous gardons les choses simples en proposant trois options :<p>
|
||||
<ul>
|
||||
<li>Millisecondes depuis l'époque : il s'agit du comportement existant de l'option d'horodatage</li>
|
||||
<li>ISO 8601 : un format commun utilisé par de nombreux systèmes</li>
|
||||
<li>Objet Date JavaScript</li>
|
||||
</ul>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Auto-complete of flow/global and env types",
|
||||
"ja": "フロー/グローバル、環境変数の型の自動補完"
|
||||
"ja": "フロー/グローバル、環境変数の型の自動補完",
|
||||
"fr": "Saisie automatique des types de flux/global et env"
|
||||
},
|
||||
image: 'images/nr4-auto-complete.png',
|
||||
description: {
|
||||
@@ -48,13 +109,17 @@ export default {
|
||||
now all include auto-complete suggestions based on the live state of your flows.</p>
|
||||
`,
|
||||
"ja": `<p><code>flow</code>/<code>global</code>コンテキストや<code>env</code>の入力を、現在のフローの状態をもとに自動補完で提案するようになりました。</p>
|
||||
`
|
||||
`,
|
||||
"fr": `<p>Les entrées contextuelles <code>flow</code>/<code>global</code> et l'entrée <code>env</code>
|
||||
incluent désormais des suggestions de saisie semi-automatique basées sur l'état actuel de vos flux.</p>
|
||||
`,
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Config node customisation in Subflows",
|
||||
"ja": "サブフローでの設定ノードのカスタマイズ"
|
||||
"ja": "サブフローでの設定ノードのカスタマイズ",
|
||||
"fr": "Personnalisation du noeud de configuration dans les sous-flux"
|
||||
},
|
||||
image: 'images/nr4-sf-config.png',
|
||||
description: {
|
||||
@@ -65,6 +130,11 @@ export default {
|
||||
`,
|
||||
"ja": `<p>サブフローをカスタマイズして、選択した型の異なる設定ノードを各インスタンスが使用できるようになりました。</p>
|
||||
<p>例えば、MQTTブローカへ接続し、メッセージ受信と後処理を行うサブフローの各インスタンスに異なるブローカを指定することも可能です。</p>
|
||||
`,
|
||||
"fr": `<p>Les sous-flux peuvent désormais être personnalisés pour permettre à chaque instance d'utiliser un
|
||||
noeud de configuration d'un type sélectionné.</p>
|
||||
<p>Par exemple, chaque instance d'un sous-flux qui se connecte à un courtier MQTT et effectue un post-traitement
|
||||
des messages reçus peut être pointée vers un autre courtier.</p>
|
||||
`
|
||||
}
|
||||
},
|
||||
@@ -90,6 +160,14 @@ export default {
|
||||
<li>WebSocketノードのカスタマイズ可能なヘッダ</li>
|
||||
<li>Splitノードは、メッセージプロパティで操作できるようになりました</li>
|
||||
<li>他にも沢山あります...</li>
|
||||
</ul>`,
|
||||
"fr": `<p>Les noeuds principaux ont reçu de nombreux correctifs mineurs ainsi que des améliorations. La documentation a été mise à jour.
|
||||
Consultez le journal des modifications dans la barre latérale d'aide pour une liste complète. Ci-dessous, les changements les plus importants :</p>
|
||||
<ul>
|
||||
<li>Un mode CSV entièrement conforme à la norme RFC4180</li>
|
||||
<li>En-têtes personnalisables pour le noeud WebSocket</li>
|
||||
<li>Le noeud Split peut désormais fonctionner sur n'importe quelle propriété de message</li>
|
||||
<li>Et bien plus encore...</li>
|
||||
</ul>`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
3
packages/node_modules/@node-red/nodes/locales/fr/common/91-global-config.html
vendored
Normal file
3
packages/node_modules/@node-red/nodes/locales/fr/common/91-global-config.html
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<script type="text/html" data-help-name="global-config">
|
||||
<p>Un noeud pour contenir la configuration globale des flux.</p>
|
||||
</script>
|
||||
@@ -94,6 +94,7 @@
|
||||
},
|
||||
"catch": {
|
||||
"catch": "catch : tout",
|
||||
"catchGroup": "catch: groupe",
|
||||
"catchNodes": "catch : __number__",
|
||||
"catchUncaught": "catch : non capturé",
|
||||
"label": {
|
||||
@@ -109,6 +110,7 @@
|
||||
},
|
||||
"status": {
|
||||
"status": "statut : tout",
|
||||
"statusGroup": "statut: groupe",
|
||||
"statusNodes": "statut : __number__",
|
||||
"label": {
|
||||
"source": "Signaler l'état de",
|
||||
@@ -250,7 +252,8 @@
|
||||
"initialize": "Au démarrage",
|
||||
"finalize": "À l'arrêt",
|
||||
"outputs": "Sorties",
|
||||
"modules": "Modules"
|
||||
"modules": "Modules",
|
||||
"timeout": "Délai d'attente"
|
||||
},
|
||||
"text": {
|
||||
"initialize": "// Le code ajouté ici sera exécuté une fois\n// à chaque démarrage du noeud.\n",
|
||||
@@ -847,7 +850,13 @@
|
||||
"newline": "Nouvelle ligne",
|
||||
"usestrings": "Analyser les valeurs numériques",
|
||||
"include_empty_strings": "Inclure les chaînes vides",
|
||||
"include_null_values": "Inclure les valeurs nulles"
|
||||
"include_null_values": "Inclure les valeurs nulles",
|
||||
"spec": "Analyseur"
|
||||
},
|
||||
"spec": {
|
||||
"rfc": "RFC4180",
|
||||
"legacy": "Hérité (Legacy)",
|
||||
"legacy_warning": "Le mode hérité sera supprimé dans une prochaine version."
|
||||
},
|
||||
"placeholder": {
|
||||
"columns": "noms de colonnes séparés par des virgules"
|
||||
@@ -876,6 +885,7 @@
|
||||
"once": "envoyer les en-têtes une fois, jusqu'à msg.reset"
|
||||
},
|
||||
"errors": {
|
||||
"bad_template": "Colonnes du modèle mal formées.",
|
||||
"csv_js": "Ce noeud ne gère que les chaînes CSV ou les objets js.",
|
||||
"obj_csv": "Aucun modèle de colonnes spécifié pour l'objet -> CSV.",
|
||||
"bad_csv": "Données CSV mal formées - sortie probablement corrompue."
|
||||
@@ -885,12 +895,14 @@
|
||||
"label": {
|
||||
"select": "Sélecteur",
|
||||
"output": "Sortie",
|
||||
"in": "dans"
|
||||
"in": "dans",
|
||||
"prefix": "Nom de la propriété pour le contenu HTML"
|
||||
},
|
||||
"output": {
|
||||
"html": "le contenu html des éléments",
|
||||
"text": "uniquement le contenu textuel des éléments",
|
||||
"attr": "un objet de n'importe quel attribut des éléments"
|
||||
"attr": "un objet de n'importe quel attribut des éléments",
|
||||
"compl": "un objet pour tous les attributs de tous les éléments ainsi que du contenu HTML"
|
||||
},
|
||||
"format": {
|
||||
"single": "comme un seul message contenant un tableau",
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
avant d'être envoyé.</p>
|
||||
<p>Si <code>msg._session</code> n'est pas présent, la charge utile est
|
||||
envoyé à <b>tous</b> les clients connectés.</p>
|
||||
<p>En mode Répondre à, définir <code>msg.reset = true</code> réinitialisera la connexion
|
||||
spécifiée par _session.id ou toutes les connexions si aucun _session.id n'est spécifié.</p>
|
||||
<p><b>Remarque</b> : Sur certains systèmes, vous aurez peut-être besoin d'un accès root ou administrateur
|
||||
pour accéder aux ports inférieurs à 1024.</p>
|
||||
</script>
|
||||
@@ -40,6 +42,8 @@
|
||||
caractères renvoyés dans un tampon fixe, correspondant à un caractère spécifié avant de revenir,
|
||||
attendre un délai fixe à partir de la première réponse, puis revenir, s'installer et attender les données, ou envoie puis ferme la connexion
|
||||
immédiatement, sans attendre de réponse.</p>
|
||||
<p>Dans le cas du mode veille (maintien de la connexion), vous pouvez envoyer <code>msg.reset = true</code> ou <code>msg.reset = "host:port"</code> pour forcer une interruption
|
||||
de la connexion et une reconnexion automatique.</p>
|
||||
<p>La réponse sortira dans <code>msg.payload</code> en tant que tampon, vous pouvez alors utiliser la fonction .toString().</p>
|
||||
<p>Si vous laissez l'hôte ou le port tcp vide, ils doivent être définis à l'aide des propriétés <code>msg.host</code> et <code>msg.port</code> dans chaque message envoyé au noeud.</ p>
|
||||
</script>
|
||||
@@ -36,7 +36,9 @@
|
||||
</dl>
|
||||
<h3>Détails</h3>
|
||||
<p>Le modèle de colonne peut contenir une liste ordonnée de noms de colonnes. Lors de la conversion de CSV en objet, les noms de colonne
|
||||
seront utilisés comme noms de propriété. Alternativement, les noms de colonne peuvent être tirés de la première ligne du CSV.</p>
|
||||
seront utilisés comme noms de propriété. Alternativement, les noms de colonne peuvent être tirés de la première ligne du CSV.
|
||||
<p>Lorsque l'analyseur RFC est sélectionné, le modèle de colonne doit être conforme à la norme RFC4180.</p>
|
||||
</p>
|
||||
<p>Lors de la conversion au format CSV, le modèle de colonnes est utilisé pour identifier les propriétés à extraire de l'objet et dans quel ordre.</p>
|
||||
<p>Si le modèle de colonnes est vide, vous pouvez utiliser une simple liste de propriétés séparées par des virgules fournies dans <code>msg.columns</code> pour
|
||||
déterminer quoi extraire et dans quel ordre. Si ni l'un ni l'autre n'est présent, toutes les propriétés de l'objet sont sorties dans l'ordre
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -25,14 +25,17 @@ 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 plugins = require("./plugins");
|
||||
|
||||
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$/;
|
||||
|
||||
@@ -227,7 +230,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");
|
||||
}
|
||||
@@ -262,7 +265,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;
|
||||
})
|
||||
}
|
||||
@@ -366,7 +369,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}));
|
||||
@@ -525,7 +528,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");
|
||||
}
|
||||
@@ -592,7 +595,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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,11 +16,11 @@
|
||||
}
|
||||
],
|
||||
"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",
|
||||
"tar": "6.1.13",
|
||||
"tar": "6.2.1",
|
||||
"uglify-js": "3.17.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ var connections = [];
|
||||
const events = require("@node-red/util").events;
|
||||
|
||||
function handleCommsEvent(event) {
|
||||
publish(event.topic,event.data,event.retain);
|
||||
publish(event.topic,event.data,event.retain,event.session,event.excludeSession);
|
||||
}
|
||||
function handleStatusEvent(event) {
|
||||
if (!event.status) {
|
||||
@@ -74,13 +74,17 @@ function handleEventLog(event) {
|
||||
publish("event-log/"+event.id,event.payload||{});
|
||||
}
|
||||
|
||||
function publish(topic,data,retain) {
|
||||
function publish(topic, data, retain, session, excludeSession) {
|
||||
if (retain) {
|
||||
retained[topic] = data;
|
||||
} else {
|
||||
delete retained[topic];
|
||||
}
|
||||
connections.forEach(connection => connection.send(topic,data))
|
||||
connections.forEach(connection => {
|
||||
if ((!session || connection.session === session) && (!excludeSession || connection.session !== excludeSession)) {
|
||||
connection.send(topic,data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +113,10 @@ var api = module.exports = {
|
||||
*/
|
||||
addConnection: async function(opts) {
|
||||
connections.push(opts.client);
|
||||
events.emit('comms:connection-added', {
|
||||
session: opts.client.session,
|
||||
user: opts.client.user
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -126,6 +134,9 @@ var api = module.exports = {
|
||||
break;
|
||||
}
|
||||
}
|
||||
events.emit('comms:connection-removed', {
|
||||
session: opts.client.session
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -157,5 +168,23 @@ var api = module.exports = {
|
||||
* @return {Promise<Object>} - resolves when complete
|
||||
* @memberof @node-red/runtime_comms
|
||||
*/
|
||||
unsubscribe: async function(opts) {}
|
||||
unsubscribe: async function(opts) {},
|
||||
|
||||
/**
|
||||
* @param {Object} opts
|
||||
* @param {User} opts.user - the user calling the api
|
||||
* @param {CommsConnection} opts.client - the client connection
|
||||
* @param {String} opts.topic - the message topic
|
||||
* @param {String} opts.data - the message data
|
||||
* @return {Promise<Object>} - resolves when complete
|
||||
*/
|
||||
receive: async function (opts) {
|
||||
if (opts.topic) {
|
||||
events.emit('comms:message:' + opts.topic, {
|
||||
session: opts.client.session,
|
||||
user: opts.user,
|
||||
data: opts.data
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -106,14 +106,22 @@ async function evaluateEnvProperties(flow, env, credentials) {
|
||||
result = { value: result, __clone__: true}
|
||||
}
|
||||
evaluatedEnv[name] = result
|
||||
} else {
|
||||
evaluatedEnv[name] = undefined
|
||||
flow.error(`Error evaluating env property '${name}': ${err.toString()}`)
|
||||
}
|
||||
resolve()
|
||||
});
|
||||
}))
|
||||
} else {
|
||||
value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null);
|
||||
if (typeof value === 'object') {
|
||||
value = { value: value, __clone__: true}
|
||||
try {
|
||||
value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null);
|
||||
if (typeof value === 'object') {
|
||||
value = { value: value, __clone__: true}
|
||||
}
|
||||
} catch (err) {
|
||||
value = undefined
|
||||
flow.error(`Error evaluating env property '${name}': ${err.toString()}`)
|
||||
}
|
||||
}
|
||||
evaluatedEnv[name] = value
|
||||
|
||||
@@ -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) {
|
||||
|
||||
119
packages/node_modules/@node-red/runtime/lib/multiplayer/index.js
vendored
Normal file
119
packages/node_modules/@node-red/runtime/lib/multiplayer/index.js
vendored
Normal file
@@ -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
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,11 +16,11 @@
|
||||
}
|
||||
],
|
||||
"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.18.2",
|
||||
"express": "4.19.2",
|
||||
"fs-extra": "11.1.1",
|
||||
"json-stringify-safe": "5.0.1"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
14
packages/node_modules/node-red/package.json
vendored
14
packages/node_modules/node-red/package.json
vendored
@@ -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,15 +31,15 @@
|
||||
"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.18.2",
|
||||
"express": "4.19.2",
|
||||
"fs-extra": "11.1.1",
|
||||
"node-red-admin": "^3.1.2",
|
||||
"node-red-admin": "^3.1.3",
|
||||
"nopt": "5.0.0",
|
||||
"semver": "7.5.4"
|
||||
},
|
||||
|
||||
4
packages/node_modules/node-red/settings.js
vendored
4
packages/node_modules/node-red/settings.js
vendored
@@ -437,6 +437,10 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
multiplayer: {
|
||||
/** To enable the Multiplayer feature, set this value to true */
|
||||
enabled: false
|
||||
},
|
||||
},
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
@@ -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:[]}];
|
||||
@@ -993,20 +1012,28 @@ 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:[]}];
|
||||
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' } });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ describe('HTTP Request Node', function() {
|
||||
function startServer(done) {
|
||||
testPort += 1;
|
||||
testServer = stoppable(http.createServer(testApp));
|
||||
const promises = []
|
||||
testServer.listen(testPort,function(err) {
|
||||
testSslPort += 1;
|
||||
console.log("ssl port", testSslPort);
|
||||
@@ -81,13 +82,17 @@ describe('HTTP Request Node', function() {
|
||||
*/
|
||||
};
|
||||
testSslServer = stoppable(https.createServer(sslOptions,testApp));
|
||||
testSslServer.listen(testSslPort, function(err){
|
||||
if (err) {
|
||||
console.log(err);
|
||||
} else {
|
||||
console.log("started testSslServer");
|
||||
}
|
||||
});
|
||||
console.log('> start testSslServer')
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
testSslServer.listen(testSslPort, function(err){
|
||||
console.log(' done testSslServer')
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
});
|
||||
}))
|
||||
|
||||
testSslClientPort += 1;
|
||||
var sslClientOptions = {
|
||||
@@ -97,10 +102,17 @@ describe('HTTP Request Node', function() {
|
||||
requestCert: true
|
||||
};
|
||||
testSslClientServer = stoppable(https.createServer(sslClientOptions, testApp));
|
||||
testSslClientServer.listen(testSslClientPort, function(err){
|
||||
console.log("ssl-client", err)
|
||||
});
|
||||
|
||||
console.log('> start testSslClientServer')
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
testSslClientServer.listen(testSslClientPort, function(err){
|
||||
console.log(' done testSslClientServer')
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
});
|
||||
}))
|
||||
testProxyPort += 1;
|
||||
testProxyServer = stoppable(httpProxy(http.createServer()))
|
||||
|
||||
@@ -109,7 +121,17 @@ describe('HTTP Request Node', function() {
|
||||
res.setHeader("x-testproxy-header", "foobar")
|
||||
}
|
||||
})
|
||||
testProxyServer.listen(testProxyPort)
|
||||
console.log('> testProxyServer')
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
testProxyServer.listen(testProxyPort, function(err) {
|
||||
console.log(' done testProxyServer')
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
testProxyAuthPort += 1
|
||||
testProxyServerAuth = stoppable(httpProxy(http.createServer()))
|
||||
@@ -131,9 +153,19 @@ describe('HTTP Request Node', function() {
|
||||
res.setHeader("x-testproxy-header", "foobar")
|
||||
}
|
||||
})
|
||||
testProxyServerAuth.listen(testProxyAuthPort)
|
||||
console.log('> testProxyServerAuth')
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
testProxyServerAuth.listen(testProxyAuthPort, function(err) {
|
||||
console.log(' done testProxyServerAuth')
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
done(err);
|
||||
Promise.all(promises).then(() => { done() }).catch(done)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -429,7 +461,11 @@ describe('HTTP Request Node', function() {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
helper.startServer(done);
|
||||
console.log('> helper.startServer')
|
||||
helper.startServer(function(err) {
|
||||
console.log('> helper started')
|
||||
done(err)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
describe('multiplayer', function() {
|
||||
})
|
||||
Reference in New Issue
Block a user