Compare commits

...

92 Commits

Author SHA1 Message Date
Stephen McLaughlin
e770830947 Merge branch 'dev' into 4651-in-out-prop 2024-04-25 18:15:41 +01:00
Steve-Mcl
640b29da10 add tests for property in & property out 2024-04-25 18:14:41 +01:00
Steve-Mcl
e45d14f3f4 dont error is property in does not exist 2024-04-25 18:14:08 +01:00
Nick O'Leary
cfb300ec06 Merge pull request #4672 from node-red/allow-blank-env-var-substitutions
Allow blank strings to be used for env var property substitutions
2024-04-24 23:37:23 +02:00
Nick O'Leary
236e668201 Allow blank strings to be used for env var property substitutions
Fixes #4663
2024-04-24 22:58:11 +02:00
Nick O'Leary
c9b902c2b4 Merge pull request #4658 from node-red/fix-subflow-conf-type
Fix saving of conf-type properties in module packaged subflows
2024-04-23 23:45:29 +02:00
Nick O'Leary
ac8b1e19b7 Merge pull request #4662 from hardillb/timeout-npm-install
Add npm install timeout notification
2024-04-23 23:44:51 +02:00
Ben Hardill
148e64c3da Update packages/node_modules/@node-red/editor-client/locales/en-US/editor.json
Co-authored-by: Nick O'Leary <nick.oleary@gmail.com>
2024-04-18 14:22:50 +01:00
Ben Hardill
c6289ebb2c Merge branch 'dev' into timeout-npm-install 2024-04-18 12:12:53 +01:00
Ben Hardill
5f4ece6813 Move translation 2024-04-18 11:47:49 +01:00
Ben Hardill
c990ec39d6 revert DELETE change 2024-04-18 11:35:51 +01:00
Ben Hardill
1fdc600ecd Add npm install timeout notification
part of https://github.com/node-red/node-red/issues/4622
2024-04-18 11:27:32 +01:00
Steve-Mcl
5951632693 fix msg property access for CSV legacy mode 2024-04-16 14:28:12 +01:00
Steve-Mcl
407f9e3537 fix msg property setting 2024-04-16 12:35:43 +01:00
Steve-Mcl
b543801856 squash windows test bugs
JSONata complains about invalid escape seq `\\`, so use posix format instead
2024-04-16 12:27:33 +01:00
Steve-Mcl
253de14967 fix: use msg not m 2024-04-16 12:24:44 +01:00
Nick O'Leary
e354d2ce29 Fix saving of conf-type properties in module packaged subflows 2024-04-12 14:08:07 +01:00
Nick O'Leary
d218af8619 Merge branch 'master' into dev 2024-04-12 13:04:54 +01:00
Steve-Mcl
94a999ba52 fix property in/out initialisations 2024-04-12 12:55:43 +01:00
Steve-Mcl
ab483781a2 update property out filed 2024-04-12 11:03:19 +01:00
Steve-Mcl
4a3b224ce2 Update file node to support Property Out 2024-04-12 11:02:43 +01:00
Steve-Mcl
f1e54c959b Update http request node to support Property In/Out 2024-04-12 11:02:26 +01:00
Steve-Mcl
dc36975083 Update range node to support Property In/Out 2024-04-12 11:02:15 +01:00
Steve-Mcl
4874e64387 Update CSV node to support Property In/Out 2024-04-12 11:02:06 +01:00
Steve-Mcl
2621a3a628 Update YAML node to support Property In/Out 2024-04-12 11:01:56 +01:00
Steve-Mcl
b67c9e1bdb Update XML node to support Property In/Out 2024-04-12 11:01:47 +01:00
Steve-Mcl
219b232cad Support Property In/Out on the JSON Node 2024-04-12 11:01:26 +01:00
Nick O'Leary
29ed5b2792 Merge pull request #4655 from node-red/rel319
Bump for 3.1.9 release
2024-04-11 19:22:24 +01:00
Nick O'Leary
e39216e65a Bump for 3.1.9 release 2024-04-11 19:15:46 +01:00
Nick O'Leary
7ac7f9b4c8 Merge pull request #4654 from node-red/fix-subflow-recursion-check
Prevent subflow being added to itself
2024-04-11 19:12:43 +01:00
Stephen McLaughlin
4709eb9d49 Merge pull request #4652 from node-red/fix-windows-spawn
Fix use of spawn on windows with cmd files
2024-04-11 17:51:13 +01:00
Nick O'Leary
c13b8266dd Prevent subflow being added to itself 2024-04-11 17:05:10 +01:00
Steve-Mcl
ef726e6a5f add message catalog entries for "Property In" and "Property Out" 2024-04-11 14:54:51 +01:00
Nick O'Leary
bd58431603 Fix use of spawn on windows with cmd files 2024-04-11 14:40:29 +01:00
Nick O'Leary
3075b82792 Add one more tour image 2024-04-05 11:18:17 +01:00
Nick O'Leary
240082481f Merge pull request #4646 from node-red/rel400b2
Bump for beta.2
2024-04-05 10:53:03 +01:00
Nick O'Leary
ea95552285 Bump for beta.2 2024-04-04 18:25:10 +01:00
Nick O'Leary
5358b06123 Merge pull request #4645 from node-red/fix-mp-tests
Add placeholder tests for multiplayer
2024-04-04 16:19:53 +01:00
Nick O'Leary
99391431da Add placeholder tests for multiplayer 2024-04-04 16:17:30 +01:00
Nick O'Leary
d396f50a9a Merge pull request #4627 from GogoVega/button-add-config-node
Separate the "add new config-node" option into a new (+) button
2024-04-04 16:11:52 +01:00
Nick O'Leary
affa8ea42b Apply suggestions from code review 2024-04-04 16:08:59 +01:00
Nick O'Leary
d711b01fe5 Merge pull request #4629 from node-red/multiplayer-1
Introduce multiplayer feature
2024-04-04 15:24:29 +01:00
Nick O'Leary
6e7fa6f921 Merge branch 'dev' into button-add-config-node 2024-04-03 14:02:40 +01:00
Nick O'Leary
343cde75a2 Merge pull request #4644 from node-red/resyn-dev
Resync recent fixes from master to dev
2024-04-03 14:02:17 +01:00
Nick O'Leary
2dc446e45b Merge branch 'dev' into resyn-dev 2024-04-03 13:57:10 +01:00
Nick O'Leary
884b7fa16a Merge pull request #4643 from node-red/fix-subflow-mod-config-select
Fix handling of subflow config-node select type in sf module
2024-04-03 13:54:41 +01:00
Nick O'Leary
173e065b68 Merge pull request #4639 from node-red/4638-fix-change-replace-bool
Fix change node handling of replacing with boolean
2024-04-02 20:07:01 +01:00
Nick O'Leary
9a3cb0b2b5 Merge pull request #4640 from node-red/fix-subflow-init-err
Guard refresh of unknown subflow
2024-04-02 20:06:47 +01:00
Nick O'Leary
6beae5a806 Merge pull request #4642 from node-red/4641-fix-subflow-module-debug-logging
Fix subflow module sending messages to debug sidebar
2024-04-02 20:06:31 +01:00
Nick O'Leary
66f4008bb8 Fix handling of subflow config-node select type in sf module 2024-04-02 20:01:48 +01:00
Nick O'Leary
a0636632a1 Fix subflow module sending messages to debug sidebar
Fixes #4641
2024-04-02 17:42:19 +01:00
Nick O'Leary
5dfa47ab6c Guard refresh of unknown subflow 2024-04-02 15:54:34 +01:00
Nick O'Leary
e9efe493f9 Remove only 2024-04-02 13:59:15 +01:00
Nick O'Leary
3bd782e62a Fix change node handling of replacing with boolean
Fixes #4638
2024-04-02 13:57:19 +01:00
Nick O'Leary
963fe87f14 Merge pull request #4637 from node-red/tidy-up-palette-state
Ensure palette filter reapplies and clear up unknown categories
2024-03-28 15:36:05 +00:00
Nick O'Leary
ade4679e8c Merge pull request #4636 from node-red/rel318
Bump for 3.1.8
2024-03-28 15:23:07 +00:00
Nick O'Leary
40060a470b Merge pull request #4635 from node-red/sync-dev
Sync dev
2024-03-28 15:22:57 +00:00
Nick O'Leary
a6e8fbb54a Ensure palette filter reapplies and clear up unknown categories 2024-03-28 15:21:04 +00:00
Nick O'Leary
410b938442 Bump for 3.1.8 2024-03-28 15:02:02 +00:00
Nick O'Leary
ab7e9f94fa Merge branch 'dev' into sync-dev 2024-03-28 14:56:36 +00:00
Nick O'Leary
28e9ccd372 Merge pull request #4634 from node-red/pr_4623
Retain Palette categories collapsed and filter to localStorage
2024-03-28 14:53:15 +00:00
Nick O'Leary
9a66d9addd Merge pull request #4620 from node-red/pr_4387
Add support for plugin (only) modules to the palette manager
2024-03-28 14:35:33 +00:00
Nick O'Leary
8843bda477 Merge pull request #4628 from node-red/comms-updates
Comms API updates
2024-03-28 14:33:10 +00:00
Nick O'Leary
3278303eec Clear localStorage state on logout 2024-03-28 14:31:31 +00:00
Nick O'Leary
f5fd6e3a36 Rework palette state management 2024-03-28 14:23:39 +00:00
Gauthier Dandele
a173e8e70f Apply suggestions from code review
Co-authored-by: Nick O'Leary <nick.oleary@gmail.com>
2024-03-28 12:57:04 +01:00
Nick O'Leary
19dcc3a683 Merge pull request #4632 from node-red/4625-sf-env-err-handling
Add validation and error handling on subflow instance properties
2024-03-28 11:10:28 +00:00
Nick O'Leary
20d067c1ea Merge pull request #4633 from node-red/4617-hide-library-context-options
Hide import/export context menu if disabled in theme
2024-03-28 11:10:14 +00:00
Nick O'Leary
9526566799 Hide import/export context menu if disabled in theme 2024-03-28 11:00:10 +00:00
Nick O'Leary
0b9dd82c91 Merge pull request #4631 from node-red/4626-subflow-change-notification
Show change indicator on subflow tabs
2024-03-27 19:10:39 +00:00
Nick O'Leary
19213434f9 Add validation to subflow instance env properties 2024-03-27 19:08:25 +00:00
Nick O'Leary
014691346a Handle malformed env var values and log errors 2024-03-27 18:23:12 +00:00
Nick O'Leary
6738b95c29 Merge pull request #4630 from node-red/bump-express
Bump dependencies
2024-03-27 18:11:54 +00:00
Nick O'Leary
6a8230ec1e Show change icon on subflow tabs
Fixes #4626
2024-03-27 18:10:04 +00:00
Nick O'Leary
5679d264b6 Bump dependencies 2024-03-27 18:00:06 +00:00
Nick O'Leary
b20c5f3a8d Merge pull request #4621 from GogoVega/addfrench-v4
Add French translations for 4.0.0-beta.1
2024-03-27 17:48:07 +00:00
Nick O'Leary
014f206e9c Initial multiplayer feature 2024-03-27 17:30:44 +00:00
Nick O'Leary
068b93befa CComms API updates 2024-03-27 17:21:12 +00:00
GogoVega
65d8872cea Separate the "add new config-node" option into a button 2024-03-27 14:59:49 +01:00
GogoVega
bffd1d61b2 Improve with error handling, storage cleanup and centralization in one object 2024-03-26 16:58:45 +01:00
GogoVega
4788b81220 Replace setTimeout with a listener 2024-03-26 13:05:38 +01:00
GogoVega
9a07fc03c6 Retain palette collapse and filter to localStorage 2024-03-25 20:47:55 +01:00
GogoVega
954f518030 Add French translations for 4.0.0-beta.1 2024-03-25 12:02:38 +01:00
GogoVega
9f8ff71757 Add French translations for TCP and CSV nodes 2024-03-25 11:58:35 +01:00
GogoVega
06dd59dc81 Add missing global-config translation 2024-03-25 10:51:12 +01:00
Nick O'Leary
37265cf4ef Merge pull request #4619 from node-red/4600-reset-workspace-index
Reset workspace index when clearing nodes
2024-03-21 17:38:39 +00:00
Nick O'Leary
8a63275989 Merge pull request #4613 from kazuhitoyokoi/master-fixglobalconfig
Remove typo in global config
2024-03-21 16:54:01 +00:00
Nick O'Leary
7fc64a84e8 Bump test helper 2024-03-21 15:16:49 +00:00
Nick O'Leary
02f7cdd5aa Ensure all httpRequest test servers are ready before tests run 2024-03-21 15:03:37 +00:00
Nick O'Leary
d7dcceef60 Add debug for http tests 2024-03-21 11:32:29 +00:00
Nick O'Leary
ae5e1570ae Reset workspace index when clearing nodes
Fixes #4600
2024-03-21 11:14:34 +00:00
Kazuhito Yokoi
3ca045394a Remove typo in global config 2024-03-16 18:51:13 +09:00
79 changed files with 1857 additions and 450 deletions

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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",

View File

@@ -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>",

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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
}
})();

View 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)
}
}
})();

View File

@@ -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",

View File

@@ -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();

View File

@@ -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;

View File

@@ -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") }
)
}

View File

@@ -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);
}
}

View File

@@ -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"),

View File

@@ -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',

View File

@@ -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,

View File

@@ -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);

View File

@@ -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() {

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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();
}

View File

@@ -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: []
});

View File

@@ -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;
}
}

View 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;
}
}

View File

@@ -73,3 +73,5 @@
@import "radialMenu";
@import "tourGuide";
@import "multiplayer";

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -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>`
}
}

View File

@@ -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 }
}
})

View File

@@ -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') {

View File

@@ -1,7 +1,11 @@
<script type="text/html" data-template-name="range">
<div class="form-row">
<label for="node-input-property"><i class="fa fa-ellipsis-h"></i> <span data-i18n="common.label.property"></span></label>
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row">
<label for="node-input-property"><i class="fa fa-sign-in"></i> <span data-i18n="common.label.propertyIn"></span></label>
<input type="text" id="node-input-property" style="width:calc(70% - 1px)"/>
</div>
<div class="form-row">
@@ -31,8 +35,8 @@
</div>
<br/>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
<label for="node-input-propertyOut"><i class="fa fa-sign-out"></i> <span data-i18n="common.label.propertyOut"></span></label>
<input type="text" id="node-input-propertyOut" style="width:70%;"/>
</div>
<div class="form-tips" id="node-tip"><span data-i18n="range.tip"></span></div>
</script>
@@ -57,9 +61,13 @@
action: {value:"scale"},
round: {value:false},
property: {value:"payload",required:true,
label:RED._("node-red:common.label.property"),
label:RED._("node-red:common.label.propertyIn"),
validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })
},
propertyOut: {value:"payload",required:true,
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true}),
label:RED._("node-red:common.label.propertyOut")},
name: {value:""}
},
inputs: 1,
@@ -77,6 +85,10 @@
$("#node-input-property").val("payload");
}
$("#node-input-property").typedInput({default:'msg',types:['msg']});
if (this.propertyOut === undefined) {
$("#node-input-propertyOut").val("payload");
}
$("#node-input-propertyOut").typedInput({default:'msg',types:['msg']});
}
});
</script>

View File

@@ -16,6 +16,7 @@
module.exports = function(RED) {
"use strict";
const { getMessagePropertySafe } = require('../utils.js')(RED)
function RangeNode(n) {
RED.nodes.createNode(this, n);
this.action = n.action;
@@ -25,10 +26,11 @@ module.exports = function(RED) {
this.minout = Number(n.minout);
this.maxout = Number(n.maxout);
this.property = n.property||"payload";
this.propertyOut = n.propertyOut||this.property;
var node = this;
this.on('input', function (msg, send, done) {
var value = RED.util.getMessageProperty(msg,node.property);
var value = getMessagePropertySafe(msg, node.property);
if (value !== undefined) {
var n = Number(value);
if (!isNaN(n)) {
@@ -46,7 +48,7 @@ module.exports = function(RED) {
}
value = ((n - node.minin) / (node.maxin - node.minin) * (node.maxout - node.minout)) + node.minout;
if (node.round) { value = Math.round(value); }
RED.util.setMessageProperty(msg,node.property,value);
RED.util.setMessageProperty(msg,node.propertyOut,value);
send(msg);
}
else { node.log(RED._("range.errors.notnumber")+": "+value); }

View File

@@ -4,11 +4,6 @@
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"></div>
</div>
<div class="form-row">
<label for="node-input-field"><i class="fa fa-ellipsis-h"></i> <span data-i18n="template.label.property"></span></label>
<input type="text" id="node-input-field" placeholder="payload" style="width:250px;">
<input type="hidden" id="node-input-fieldType">
</div>
<div class="form-row" style="position: relative; margin-bottom: 0px;">
<label for="node-input-template"><i class="fa fa-file-code-o"></i> <span data-i18n="template.label.template"></span></label>
<input type="hidden" id="node-input-template" autofocus="autofocus">
@@ -41,14 +36,18 @@
</select>
</div>
<div class="form-row">
<label for="node-input-output"><i class="fa fa-long-arrow-right"></i> <span data-i18n="template.label.output"></span></label>
<label for="node-input-output"><i class="fa fa-sign-out"></i> <span data-i18n="template.label.output"></span></label>
<select id="node-input-output" style="width:180px;">
<option value="str" data-i18n="template.label.plain"></option>
<option value="json" data-i18n="template.label.json"></option>
<option value="yaml" data-i18n="template.label.yaml"></option>
</select>
</div>
<div class="form-row">
<label for="node-input-field"><i class="fa fa-sign-out"></i> <span data-i18n="common.label.propertyOut"></span></label>
<input type="text" id="node-input-field" placeholder="payload" style="width:250px;">
<input type="hidden" id="node-input-fieldType">
</div>
</script>
<script type="text/javascript">

View File

@@ -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) {

View File

@@ -15,6 +15,16 @@
-->
<script type="text/html" data-template-name="http request">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row">
<label for="node-input-property"><i class="fa fa-sign-in"></i> <span data-i18n="common.label.propertyIn"></span></label>
<input type="text" id="node-input-property" style="width:70%;"/>
</div>
<div class="form-row">
<label for="node-input-method"><i class="fa fa-tasks"></i> <span data-i18n="httpin.label.method"></span></label>
<select type="text" id="node-input-method" style="width:70%;">
@@ -98,7 +108,7 @@
<div class="form-row">
<label for="node-input-ret"><i class="fa fa-arrow-left"></i> <span data-i18n="httpin.label.return"></span></label>
<label for="node-input-ret"><i class="fa fa-sign-out"></i> <span data-i18n="httpin.label.return"></span></label>
<select type="text" id="node-input-ret" style="width:70%;">
<option value="txt" data-i18n="httpin.utf8"></option>
<option value="bin" data-i18n="httpin.binary"></option>
@@ -107,7 +117,10 @@
</div>
<div class="form-row form-tips" id="tip-json" hidden><span data-i18n="httpin.tip.req"></span></div>
<div class="form-row">
<label for="node-input-propertyOut"><i class="fa fa-sign-out"></i> <span data-i18n="common.label.propertyOut"></span></label>
<input type="text" id="node-input-propertyOut" style="width:70%;"/>
</div>
<div class="form-row" style="margin-bottom:0;">
<label><i class="fa fa-list"></i> <span data-i18n="httpin.label.headers"></span></label>
</div>
@@ -115,10 +128,7 @@
<ol id="node-input-headers-container"></ol>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
</script>
<script type="text/javascript">
@@ -235,7 +245,13 @@
insecureHTTPParser: {value: false},
authType: {value: ""},
senderr: {value: false},
headers: { value: [] }
headers: { value: [] },
property: {value:"payload",required:true,
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true }),
label:RED._("node-red:common.label.propertyIn")},
propertyOut: {value:"payload",required:true,
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true}),
label:RED._("node-red:common.label.propertyOut")}
},
credentials: {
user: {type:"text"},
@@ -426,6 +442,15 @@
headerList.editableList('addItem', node.headers[index]);
}
}
if (this.property === undefined) {
$("#node-input-property").val("payload");
}
$("#node-input-property").typedInput({default:'msg',types:['msg']});
if (this.propertyOut === undefined) {
$("#node-input-propertyOut").val("payload");
}
$("#node-input-propertyOut").typedInput({default:'msg',types:['msg']});
},
oneditsave: function() {
if (!$("#node-input-usetls").is(':checked')) {

View File

@@ -29,7 +29,7 @@ module.exports = async function(RED) {
var querystring = require("querystring");
var cookie = require("cookie");
var hashSum = require("hash-sum");
const { getMessagePropertySafe } = require('../utils.js')(RED)
// Cache a reference to the existing https.request function
// so we can compare later to see if an old agent-base instance
@@ -68,6 +68,8 @@ in your Node-RED user directory (${RED.settings.userDir}).
RED.nodes.createNode(this,n);
checkNodeAgentPatch();
const node = this;
node.property = n.property||"payload";
node.propertyOut = n.propertyOut||node.property;
const nodeUrl = n.url;
const isTemplatedUrl = (nodeUrl||"").indexOf("{{") != -1;
const nodeMethod = n.method || "GET";
@@ -431,14 +433,14 @@ in your Node-RED user directory (${RED.settings.userDir}).
}
}
var payload = null;
const value = getMessagePropertySafe(msg, node.property)
if (method !== 'GET' && method !== 'HEAD' && typeof msg.payload !== "undefined") {
if (opts.headers['content-type'] == 'multipart/form-data' && typeof msg.payload === "object") {
if (method !== 'GET' && method !== 'HEAD' && typeof value !== "undefined") {
if (opts.headers['content-type'] == 'multipart/form-data' && typeof value === "object") {
let formData = new FormData();
for (var opt in msg.payload) {
if (msg.payload.hasOwnProperty(opt)) {
var val = msg.payload[opt];
for (var opt in value) {
if (value.hasOwnProperty(opt)) {
var val = value[opt];
if (val !== undefined && val !== null) {
if (typeof val === 'string' || Buffer.isBuffer(val)) {
formData.append(opt, val);
@@ -455,15 +457,15 @@ in your Node-RED user directory (${RED.settings.userDir}).
delete opts.headers['content-type'];
opts.body = formData;
} else {
if (typeof msg.payload === "string" || Buffer.isBuffer(msg.payload)) {
payload = msg.payload;
} else if (typeof msg.payload == "number") {
payload = msg.payload+"";
if (typeof value === "string" || Buffer.isBuffer(value)) {
payload = value;
} else if (typeof value == "number") {
payload = value + "";
} else {
if (opts.headers['content-type'] == 'application/x-www-form-urlencoded') {
payload = querystring.stringify(msg.payload);
payload = querystring.stringify(value);
} else {
payload = JSON.stringify(msg.payload);
payload = JSON.stringify(value);
if (opts.headers['content-type'] == null) {
opts.headers[ctSet] = "application/json";
}
@@ -481,13 +483,13 @@ in your Node-RED user directory (${RED.settings.userDir}).
}
if (method == 'GET' && typeof msg.payload !== "undefined" && paytoqs) {
if (typeof msg.payload === "object") {
if (method == 'GET' && typeof value !== "undefined" && paytoqs) {
if (typeof value === "object") {
try {
if (url.indexOf("?") !== -1) {
url += (url.endsWith("?")?"":"&") + querystring.stringify(msg.payload);
url += (url.endsWith("?")?"":"&") + querystring.stringify(value);
} else {
url += "?" + querystring.stringify(msg.payload);
url += "?" + querystring.stringify(value);
}
} catch(err) {
@@ -501,14 +503,14 @@ in your Node-RED user directory (${RED.settings.userDir}).
nodeDone();
return;
}
} else if ( method == "GET" && typeof msg.payload !== "undefined" && paytobody) {
} else if ( method == "GET" && typeof value !== "undefined" && paytobody) {
opts.allowGetBody = true;
if (typeof msg.payload === "object") {
opts.body = JSON.stringify(msg.payload);
} else if (typeof msg.payload == "number") {
opts.body = msg.payload+"";
} else if (typeof msg.payload === "string" || Buffer.isBuffer(msg.payload)) {
opts.body = msg.payload;
if (typeof value === "object") {
opts.body = JSON.stringify(value);
} else if (typeof value == "number") {
opts.body = value + "";
} else if (typeof value === "string" || Buffer.isBuffer(value)) {
opts.body = value;
}
}
@@ -597,7 +599,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
msg.statusCode = res.statusCode;
msg.headers = res.headers;
msg.responseUrl = res.url;
msg.payload = res.body;
let result = res.body;
msg.redirectList = redirectList;
msg.retry = 0;
@@ -622,14 +624,15 @@ in your Node-RED user directory (${RED.settings.userDir}).
// Convert the payload to the required return type
if (node.ret !== "bin") {
msg.payload = msg.payload.toString('utf8'); // txt
result = result.toString('utf8'); // txt
if (node.ret === "obj") {
if (msg.statusCode == 204){msg.payload= "{}"};
try { msg.payload = JSON.parse(msg.payload); } // obj
if (msg.statusCode == 204){result= "{}"};
try { result = JSON.parse(result); } // obj
catch(e) { node.warn(RED._("httpin.errors.json-error")); }
}
}
RED.util.setMessageProperty(msg, node.propertyOut, result)
node.status({});
nodeSend(msg);
nodeDone();
@@ -647,7 +650,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
node.error(err,msg);
node.status({fill:"red", shape:"ring", text:err.code});
}
msg.payload = err.toString() + " : " + url;
RED.util.setMessageProperty(msg, node.propertyOut, err.toString() + " : " + url)
msg.statusCode = err.code || (err.response?err.response.statusCode:undefined);
if (node.metric() && timingLog) {
emitTimingMetricLog(err.timings, msg);

View File

@@ -1,5 +1,13 @@
<script type="text/html" data-template-name="csv">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row">
<label for="node-input-property"><i class="fa fa-sign-in"></i> <span data-i18n="common.label.propertyIn"></span></label>
<input type="text" id="node-input-property" style="width:70%;"/>
</div>
<div class="form-row">
<label for="node-input-temp"><i class="fa fa-list"></i> <span data-i18n="csv.label.columns"></span></label>
<input type="text" id="node-input-temp" data-i18n="[placeholder]csv.placeholder.columns">
@@ -32,8 +40,8 @@
</div>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
<label for="node-input-propertyOut"><i class="fa fa-sign-out"></i> <span data-i18n="common.label.propertyOut"></span></label>
<input type="text" id="node-input-propertyOut" style="width:70%;"/>
</div>
<hr align="middle"/>
<div class="form-row">
@@ -102,7 +110,13 @@
skip: {value:"0"},
strings: {value:true},
include_empty_strings: {value:""},
include_null_values: {value:""}
include_null_values: {value:""},
property: {value:"payload",required:true,
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true }),
label:RED._("node-red:common.label.propertyIn")},
propertyOut: {value:"payload",required:true,
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true}),
label:RED._("node-red:common.label.propertyOut")}
},
inputs:1,
outputs:1,
@@ -114,6 +128,15 @@
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
if (this.property === undefined) {
$("#node-input-property").val("payload");
}
if (this.property === undefined) {
$("#node-input-propertyOut").val("payload");
}
$("#node-input-property").typedInput({default:'msg',types:['msg']});
$("#node-input-propertyOut").typedInput({default:'msg',types:['msg']});
if (this.hdrout === false) { this.hdrout = "none"; $("#node-input-hdrout").val("none"); }
if (this.hdrout === true) { this.hdrout = "all"; $("#node-input-hdrout").val("all");}
if (this.strings === undefined) { this.strings = true; $("#node-input-strings").prop('checked', true); }

View File

@@ -15,9 +15,11 @@
**/
module.exports = function(RED) {
const csv = require('./lib/csv')
"use strict";
const csv = require('./lib/csv')
const { getMessagePropertySafe } = require('../utils.js')(RED)
function CSVNode(n) {
RED.nodes.createNode(this,n)
const node = this
@@ -26,6 +28,9 @@ module.exports = function(RED) {
node.status({}) // clear status
node.property = n.property||"payload";
node.propertyOut = n.propertyOut||node.property;
if (legacyMode) {
this.template = (n.temp || "");
this.sep = (n.sep || ',').replace(/\\t/g,"\t").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
@@ -66,43 +71,44 @@ module.exports = function(RED) {
if (msg.hasOwnProperty("reset")) {
node.hdrSent = false;
}
if (msg.hasOwnProperty("payload")) {
if (typeof msg.payload == "object") { // convert object to CSV string
let inputData = getMessagePropertySafe(msg, node.property)
if (typeof inputData !== "undefined") {
if (typeof inputData == "object") { // convert object to CSV string
try {
if (!(notemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
template = clean(node.template);
}
const ou = [];
if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; }
if (!Array.isArray(inputData)) { inputData = [ inputData ]; }
if (node.hdrout !== "none" && node.hdrSent === false) {
if ((template.length === 1) && (template[0] === '')) {
if (msg.hasOwnProperty("columns")) {
template = clean(msg.columns || "",",");
}
else {
template = Object.keys(msg.payload[0]);
template = Object.keys(inputData[0]);
}
}
ou.push(template.map(v => v.indexOf(node.sep)!==-1 ? '"'+v+'"' : v).join(node.sep));
if (node.hdrout === "once") { node.hdrSent = true; }
}
for (var s = 0; s < msg.payload.length; s++) {
if ((Array.isArray(msg.payload[s])) || (typeof msg.payload[s] !== "object")) {
if (typeof msg.payload[s] !== "object") { msg.payload = [ msg.payload ]; }
for (var t = 0; t < msg.payload[s].length; t++) {
if (msg.payload[s][t] === undefined) { msg.payload[s][t] = ""; }
if (msg.payload[s][t].toString().indexOf(node.quo) !== -1) { // add double quotes if any quotes
msg.payload[s][t] = msg.payload[s][t].toString().replace(/"/g, '""');
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
for (var s = 0; s < inputData.length; s++) {
if ((Array.isArray(inputData[s])) || (typeof inputData[s] !== "object")) {
if (typeof inputData[s] !== "object") { inputData = [ inputData ]; }
for (var t = 0; t < inputData[s].length; t++) {
if (inputData[s][t] === undefined) { inputData[s][t] = ""; }
if (inputData[s][t].toString().indexOf(node.quo) !== -1) { // add double quotes if any quotes
inputData[s][t] = inputData[s][t].toString().replace(/"/g, '""');
inputData[s][t] = node.quo + inputData[s][t].toString() + node.quo;
}
else if (msg.payload[s][t].toString().indexOf(node.sep) !== -1) { // add quotes if any "commas"
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
else if (inputData[s][t].toString().indexOf(node.sep) !== -1) { // add quotes if any "commas"
inputData[s][t] = node.quo + inputData[s][t].toString() + node.quo;
}
else if (msg.payload[s][t].toString().indexOf("\n") !== -1) { // add quotes if any "\n"
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
else if (inputData[s][t].toString().indexOf("\n") !== -1) { // add quotes if any "\n"
inputData[s][t] = node.quo + inputData[s][t].toString() + node.quo;
}
}
ou.push(msg.payload[s].join(node.sep));
ou.push(inputData[s].join(node.sep));
}
else {
if ((template.length === 1) && (template[0] === '') && (msg.hasOwnProperty("columns"))) {
@@ -115,16 +121,16 @@ module.exports = function(RED) {
tmpwarn = false;
}
const row = [];
for (var p in msg.payload[0]) {
for (var p in inputData[0]) {
/* istanbul ignore else */
if (msg.payload[s].hasOwnProperty(p)) {
if (inputData[s].hasOwnProperty(p)) {
/* istanbul ignore else */
if (typeof msg.payload[s][p] !== "object") {
if (typeof inputData[s][p] !== "object") {
// Fix to honour include null values flag
//if (typeof msg.payload[s][p] !== "object" || (node.include_null_values === true && msg.payload[s][p] === null)) {
//if (typeof inputData[s][p] !== "object" || (node.include_null_values === true && inputData[s][p] === null)) {
var q = "";
if (msg.payload[s][p] !== undefined) {
q += msg.payload[s][p];
if (inputData[s][p] !== undefined) {
q += inputData[s][p];
}
if (q.indexOf(node.quo) !== -1) { // add double quotes if any quotes
q = q.replace(/"/g, '""');
@@ -149,7 +155,7 @@ module.exports = function(RED) {
var tt = template[t];
if (template[t].indexOf('"') >=0 ) { tt = "'"+tt+"'"; }
else { tt = '"'+tt+'"'; }
var p = RED.util.getMessageProperty(msg,'payload["'+s+'"]['+tt+']');
var p = RED.util.getMessageProperty(inputData[s] || {}, tt);
/* istanbul ignore else */
if (p === undefined) { p = ""; }
// fix to honour include null values flag
@@ -170,16 +176,17 @@ module.exports = function(RED) {
}
}
// join lines, don't forget to add the last new line
msg.payload = ou.join(node.ret) + node.ret;
inputData = ou.join(node.ret) + node.ret;
RED.util.setMessageProperty(msg, node.propertyOut, inputData)
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).join(',');
if (msg.payload !== '') {
if (inputData !== '') {
send(msg);
}
done();
}
catch(e) { done(e); }
}
else if (typeof msg.payload == "string") { // convert CSV string to object
else if (typeof inputData == "string") { // convert CSV string to object
try {
var f = true; // flag to indicate if inside or outside a pair of quotes true = outside.
var j = 0; // pointer into array of template items
@@ -188,7 +195,7 @@ module.exports = function(RED) {
var a = []; // output array is needed for multiline option
var first = true; // is this the first line
var last = false;
var line = msg.payload;
var line = inputData;
var linecount = 0;
var tmp = "";
var has_parts = msg.hasOwnProperty("parts");
@@ -282,13 +289,13 @@ module.exports = function(RED) {
}
if (node.multi !== "one") {
msg.payload = a;
RED.util.setMessageProperty(msg, node.propertyOut, a);
if (has_parts && nocr <= 1) {
if (JSON.stringify(o) !== "{}") {
node.store.push(o);
}
if (msg.parts.index + 1 === msg.parts.count) {
msg.payload = node.store;
RED.util.setMessageProperty(msg, node.propertyOut, node.store);
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
delete msg.parts;
send(msg);
@@ -305,7 +312,7 @@ module.exports = function(RED) {
for (var i = 0; i < len; i++) {
var newMessage = RED.util.cloneMessage(msg);
newMessage.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
newMessage.payload = a[i];
RED.util.setMessageProperty(newMessage, node.propertyOut, a[i])
if (!has_parts) {
newMessage.parts = {
id: msg._msgid,
@@ -411,8 +418,8 @@ module.exports = function(RED) {
if (msg.hasOwnProperty("reset")) {
node.hdrSent = false
}
if (msg.hasOwnProperty("payload")) {
let inputData = msg.payload
let inputData = getMessagePropertySafe(msg, node.property)
if (typeof inputData !== "undefined") {
if (typeof inputData == "object") { // convert object to CSV string
try {
// first determine the payload kind. Array or objects? Array of primitives? Array of arrays? Just an object?
@@ -517,9 +524,10 @@ module.exports = function(RED) {
}
// join lines, don't forget to add the last new line
msg.payload = stringBuilder.join(node.ret) + node.ret
const result = stringBuilder.join(node.ret) + node.ret
RED.util.setMessageProperty(msg, node.propertyOut, result)
msg.columns = templateArrayToColumnString(template)
if (msg.payload !== '') { send(msg) }
if (result !== '') { send(msg) }
done()
}
catch (e) {
@@ -614,7 +622,7 @@ module.exports = function(RED) {
node.store.push(...data)
}
if (msg.parts.index + 1 === msg.parts.count) {
msg.payload = node.store
RED.util.setMessageProperty(msg, node.propertyOut, node.store)
msg.columns = csvParseResult.header
// msg._mode = 'RFC4180 mode'
delete msg.parts
@@ -625,7 +633,7 @@ module.exports = function(RED) {
else {
msg.columns = csvParseResult.header
// msg._mode = 'RFC4180 mode'
msg.payload = data
RED.util.setMessageProperty(msg, node.propertyOut, data)
send(msg); // finally send the array
}
}
@@ -634,7 +642,7 @@ module.exports = function(RED) {
for (let row = 0; row < len; row++) {
const newMessage = RED.util.cloneMessage(msg)
newMessage.columns = csvParseResult.header
newMessage.payload = data[row]
RED.util.setMessageProperty(newMessage, node.propertyOut, data[row])
if (!has_parts) {
newMessage.parts = {
id: msg._msgid,

View File

@@ -1,5 +1,13 @@
<script type="text/html" data-template-name="json">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row">
<label for="node-input-property"><i class="fa fa-sign-in"></i> <span data-i18n="common.label.propertyIn"></span></label>
<input type="text" id="node-input-property" style="width:70%;"/>
</div>
<div class="form-row">
<label for="node-input-action"><i class="fa fa-dot-circle-o"></i> <span data-i18n="json.label.action"></span></label>
<select style="width:70%" id="node-input-action">
@@ -9,12 +17,8 @@
</select>
</div>
<div class="form-row">
<label for="node-input-property"><i class="fa fa-ellipsis-h"></i> <span data-i18n="json.label.property"></span></label>
<input type="text" id="node-input-property" style="width:70%;"/>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
<label for="node-input-propertyOut"><i class="fa fa-sign-out"></i> <span data-i18n="common.label.propertyOut"></span></label>
<input type="text" id="node-input-propertyOut" style="width:70%;"/>
</div>
<hr align="middle"/>
<div class="form-row node-json-to-json-options">
@@ -33,7 +37,10 @@
name: {value:""},
property: {value:"payload",required:true,
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true}),
label:RED._("node-red:json.label.property")},
label:RED._("node-red:common.label.propertyIn")},
propertyOut: {value:"payload",required:true,
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true}),
label:RED._("node-red:common.label.propertyOut")},
action: {value:""},
pretty: {value:false}
},
@@ -50,7 +57,11 @@
if (this.property === undefined) {
$("#node-input-property").val("payload");
}
if (this.propertyOut === undefined) {
$("#node-input-propertyOut").val("payload");
}
$("#node-input-property").typedInput({default:'msg',types:['msg']});
$("#node-input-propertyOut").typedInput({default:'msg',types:['msg']});
$("#node-input-action").on("change", function() {
if (this.value === "" || this.value === "str") {
$(".node-json-to-json-options").show();

View File

@@ -19,12 +19,14 @@ module.exports = function(RED) {
const Ajv = require('ajv');
const ajv = new Ajv({allErrors: true});
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'));
const { getMessagePropertySafe } = require('../utils.js')(RED)
function JSONNode(n) {
RED.nodes.createNode(this,n);
this.indent = n.pretty ? 4 : 0;
this.action = n.action||"";
this.property = n.property||"payload";
this.propertyOut = n.propertyOut||this.property;
this.schema = null;
this.compiledSchema = null;
@@ -47,7 +49,7 @@ module.exports = function(RED) {
}
validate = true;
}
var value = RED.util.getMessageProperty(msg,node.property);
var value = getMessagePropertySafe(msg,node.property);
if (value !== undefined) {
if (typeof value === "string" || Buffer.isBuffer(value)) {
// if (Buffer.isBuffer(value) && node.action !== "obj") {
@@ -56,7 +58,7 @@ module.exports = function(RED) {
// else
if (node.action === "" || node.action === "obj") {
try {
RED.util.setMessageProperty(msg,node.property,JSON.parse(value));
RED.util.setMessageProperty(msg,node.propertyOut,JSON.parse(value));
if (validate) {
if (this.compiledSchema(msg[node.property])) {
delete msg.schema;
@@ -95,7 +97,7 @@ module.exports = function(RED) {
try {
if (validate) {
if (this.compiledSchema(value)) {
RED.util.setMessageProperty(msg,node.property,JSON.stringify(value,null,node.indent));
RED.util.setMessageProperty(msg,node.propertyOut,JSON.stringify(value,null,node.indent));
delete msg.schema;
send(msg);
done();
@@ -104,7 +106,7 @@ module.exports = function(RED) {
done(`${RED._("json.errors.schema-error")}: ${ajv.errorsText(this.compiledSchema.errors)}`);
}
} else {
RED.util.setMessageProperty(msg,node.property,JSON.stringify(value,null,node.indent));
RED.util.setMessageProperty(msg,node.propertyOut,JSON.stringify(value,null,node.indent));
send(msg);
done();
}

View File

@@ -1,12 +1,16 @@
<script type="text/html" data-template-name="xml">
<div class="form-row">
<label for="node-input-property"><i class="fa fa-ellipsis-h"></i> <span data-i18n="common.label.property"></span></label>
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row">
<label for="node-input-property"><i class="fa fa-sign-in"></i> <span data-i18n="common.label.propertyIn"></span></label>
<input type="text" id="node-input-property" style="width:70%;"/>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
<label for="node-input-propertyOut"><i class="fa fa-sign-out"></i> <span data-i18n="common.label.propertyOut"></span></label>
<input type="text" id="node-input-propertyOut" style="width:70%;"/>
</div>
<hr align="middle"/>
<div class="form-row">
@@ -27,7 +31,11 @@
defaults: {
name: {value:""},
property: {value:"payload",required:true,
label:RED._("node-red:common.label.property"),
label:RED._("node-red:common.label.propertyIn"),
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true })},
attr: {value:""},
propertyOut: {value:"payload",required:true,
label:RED._("node-red:common.label.propertyOut"),
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true })},
attr: {value:""},
chr: {value:""}
@@ -45,7 +53,11 @@
if (this.property === undefined) {
$("#node-input-property").val("payload");
}
if (this.propertyOut === undefined) {
$("#node-input-propertyOut").val("payload");
}
$("#node-input-property").typedInput({default:'msg',types:['msg']});
$("#node-input-propertyOut").typedInput({default:'msg',types:['msg']});
}
});
</script>

View File

@@ -3,15 +3,17 @@ module.exports = function(RED) {
"use strict";
var xml2js = require('xml2js');
var parseString = xml2js.parseString;
const { getMessagePropertySafe } = require('../utils.js')(RED)
function XMLNode(n) {
RED.nodes.createNode(this,n);
this.attrkey = n.attr;
this.charkey = n.chr;
this.property = n.property||"payload";
this.propertyOut = n.propertyOut||this.property;
var node = this;
this.on("input", function(msg,send,done) {
var value = RED.util.getMessageProperty(msg,node.property);
var value = getMessagePropertySafe(msg,node.property);
if (value !== undefined) {
var options;
if (typeof value === "object") {
@@ -20,7 +22,7 @@ module.exports = function(RED) {
options.async = false;
var builder = new xml2js.Builder(options);
value = builder.buildObject(value, options);
RED.util.setMessageProperty(msg,node.property,value);
RED.util.setMessageProperty(msg,node.propertyOut,value);
send(msg);
done();
}
@@ -33,7 +35,7 @@ module.exports = function(RED) {
parseString(value, options, function (err, result) {
if (err) { done(err); }
else {
RED.util.setMessageProperty(msg,node.property,result);
RED.util.setMessageProperty(msg,node.propertyOut,result);
send(msg);
done();
}

View File

@@ -1,12 +1,16 @@
<script type="text/html" data-template-name="yaml">
<div class="form-row">
<label for="node-input-property"><i class="fa fa-ellipsis-h"></i> <span data-i18n="common.label.property"></span></label>
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row">
<label for="node-input-property"><i class="fa fa-sign-in"></i> <span data-i18n="common.label.propertyIn"></span></label>
<input type="text" id="node-input-property" style="width:70%;"/>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
<label for="node-input-propertyOut"><i class="fa fa-sign-out"></i> <span data-i18n="common.label.propertyOut"></span></label>
<input type="text" id="node-input-propertyOut" style="width:70%;"/>
</div>
</script>
@@ -17,7 +21,10 @@
defaults: {
property: {value:"payload",required:true,
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true }),
label:RED._("node-red:common.label.property")},
label:RED._("node-red:common.label.propertyIn")},
propertyOut: {value:"payload",required:true,
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true}),
label:RED._("node-red:common.label.propertyOut")},
name: {value:""}
},
inputs:1,
@@ -34,6 +41,10 @@
$("#node-input-property").val("payload");
}
$("#node-input-property").typedInput({default:'msg',types:['msg']});
if (this.propertyOut === undefined) {
$("#node-input-propertyOut").val("payload");
}
$("#node-input-propertyOut").typedInput({default:'msg',types:['msg']});
}
});
</script>

View File

@@ -2,17 +2,19 @@
module.exports = function(RED) {
"use strict";
var yaml = require('js-yaml');
const { getMessagePropertySafe } = require('../utils.js')(RED)
function YAMLNode(n) {
RED.nodes.createNode(this,n);
this.property = n.property||"payload";
this.propertyOut = n.propertyOut||this.property;
var node = this;
this.on("input", function(msg,send,done) {
var value = RED.util.getMessageProperty(msg,node.property);
var value = getMessagePropertySafe(msg,node.property);
if (value !== undefined) {
if (typeof value === "string") {
try {
value = yaml.load(value);
RED.util.setMessageProperty(msg,node.property,value);
RED.util.setMessageProperty(msg,node.propertyOut,value);
send(msg);
done();
}
@@ -22,7 +24,7 @@ module.exports = function(RED) {
if (!Buffer.isBuffer(value)) {
try {
value = yaml.dump(value);
RED.util.setMessageProperty(msg,node.property,value);
RED.util.setMessageProperty(msg,node.propertyOut,value);
send(msg);
done();
}

View File

@@ -1,5 +1,9 @@
<script type="text/html" data-template-name="file">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name">
</div>
<div class="form-row node-input-filename">
<label for="node-input-filename"><i class="fa fa-file"></i> <span data-i18n="file.label.filename"></span></label>
<input id="node-input-filename" type="text">
@@ -28,14 +32,14 @@
<select type="text" id="node-input-encoding" style="width: 250px;">
</select>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name">
</div>
<div class="form-tips"><span data-i18n="file.tip"></span></div>
</script>
<script type="text/html" data-template-name="file in">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name">
</div>
<div class="form-row">
<label for="node-input-filename"><i class="fa fa-file"></i> <span data-i18n="file.label.filename"></span></label>
<input id="node-input-filename" type="text">
@@ -61,8 +65,8 @@
</select>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name">
<label for="node-input-propertyOut"><i class="fa fa-sign-out"></i> <span data-i18n="common.label.propertyOut"></span></label>
<input type="text" id="node-input-propertyOut" style="width:70%;"/>
</div>
<div class="form-tips"><span data-i18n="file.tip"></span></div>
</script>
@@ -299,6 +303,9 @@
name: {value:""},
filename: {value:"", validate: RED.validators.typedInput({ typeField: 'filenameType' }) },
filenameType: {value:"str"},
propertyOut: {value:"payload",required:true,
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true}),
label:RED._("node-red:common.label.propertyOut")},
format: {value:"utf8"},
chunk: {value:false},
sendError: {value: false},
@@ -345,6 +352,10 @@
types: [{label:RED._("node-red:file.label.path"), value:"str", icon:""}, "msg", "jsonata", "env"],
typeField: $("#node-input-filenameType")
});
if (this.propertyOut === undefined) {
$("#node-input-propertyOut").val("payload");
}
$("#node-input-propertyOut").typedInput({default:'msg',types:['msg']});
if(typeof node.filenameType == 'undefined') {
//existing node AND filenameType is not set - inplace (compatible) upgrade to new typedInput
if(node.filename == "") { //was using empty value to denote msg.filename - set typedInput to match

View File

@@ -286,6 +286,7 @@ module.exports = function(RED) {
RED.nodes.createNode(this,n);
this.filename = n.filename;
this.filenameType = n.filenameType;
this.propertyOut = n.propertyOut || "payload";
this.format = n.format;
this.chunk = false;
this.encoding = n.encoding || "none";
@@ -370,7 +371,7 @@ module.exports = function(RED) {
m.topic = msg.topic;
m.filename = msg.filename;
}
m.payload = bits[i];
RED.util.setMessageProperty(m,node.propertyOut,bits[i]);
m.parts= {index:count, ch:ch, type:type, id:msg._msgid}
count += 1;
nodeSend(m);
@@ -386,7 +387,7 @@ module.exports = function(RED) {
m.topic = msg.topic;
m.filename = msg.filename;
}
m.payload = chunk;
RED.util.setMessageProperty(m,node.propertyOut,chunk);
m.parts = {index:count, ch:ch, type:type, id:msg._msgid}
count += 1;
if (chunk.length < hwm) { // last chunk is smaller that high water mark = eof
@@ -405,7 +406,7 @@ module.exports = function(RED) {
node.error(err, msg);
if (node.sendError) {
var sendMessage = RED.util.cloneMessage(msg);
delete sendMessage.payload;
delete sendMessage[node.propertyOut];
sendMessage.error = err;
nodeSend(sendMessage);
}
@@ -414,9 +415,10 @@ module.exports = function(RED) {
.on('end', function() {
if (node.chunk === false) {
if (node.format === "utf8") {
msg.payload = decode(lines, node.encoding);
RED.util.setMessageProperty(msg,node.propertyOut,decode(lines, node.encoding));
} else {
RED.util.setMessageProperty(msg,node.propertyOut,lines);
}
else { msg.payload = lines; }
nodeSend(msg);
}
else if (node.format === "lines") {
@@ -428,7 +430,7 @@ module.exports = function(RED) {
m.topic = msg.topic;
m.filename = msg.filename;
}
m.payload = spare;
RED.util.setMessageProperty(m,node.propertyOut,spare);
m.parts = {
index: count,
count: count + 1,

View File

@@ -0,0 +1,21 @@
function utils(RED) {
/**
* Returns the value of a property in a message object using a path. If not found, returns undefined.
* @param {Object} msg - The message object.
* @param {string} path - The path to the property.
*/
function getMessagePropertySafe (msg, path) {
try {
return RED.util.getMessageProperty(msg, path)
} catch (_e) {
return undefined
}
}
return {
getMessagePropertySafe
}
}
module.exports = utils

View File

@@ -7,6 +7,8 @@
"username": "Username",
"password": "Password",
"property": "Property",
"propertyIn": "Property In",
"propertyOut": "Property Out",
"selectNodes": "Select nodes...",
"expand": "Expand"
},

View 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>

View File

@@ -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",

View File

@@ -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>

View File

@@ -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

View File

@@ -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",

View File

@@ -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");
}

View File

@@ -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;

View File

@@ -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:

View File

@@ -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"
}
}

View File

@@ -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
})
}
}
};

View File

@@ -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

View File

@@ -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) {

View 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
})
})
}
}

View File

@@ -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"
}

View File

@@ -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",

View File

@@ -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"
},

View File

@@ -437,6 +437,10 @@ module.exports = {
}
},
multiplayer: {
/** To enable the Multiplayer feature, set this value to true */
enabled: false
},
},
/*******************************************************************************

View File

@@ -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' } });
});
});

View File

@@ -30,13 +30,19 @@ describe('range Node', function() {
helper.stopServer(done);
});
it('should load some defaults', function(done) {
it('should load with defaults', function(done) {
var flow = [{"id":"rangeNode1","type":"range","name":"rangeNode"}];
helper.load(rangeNode, flow, function() {
var rangeNode1 = helper.getNode("rangeNode1");
rangeNode1.should.have.property('name', 'rangeNode');
rangeNode1.should.have.property('round', false);
done();
try {
var rangeNode1 = helper.getNode("rangeNode1");
rangeNode1.should.have.property('name', 'rangeNode');
rangeNode1.should.have.property('round', false);
rangeNode1.should.have.property('property', 'payload')
rangeNode1.should.have.property('propertyOut', 'payload')
done();
} catch (error) {
done(error);
}
});
});
@@ -170,4 +176,23 @@ describe('range Node', function() {
rangeNode1.receive({payload:"NOT A NUMBER"});
});
});
it('uses configured property to get input value and propertyOut to set output value', function (done) {
var flow = [{ "id": "rangeNode1", "type": "range", "minin": 0, "maxin": 10, "minout": 0, "maxout": 100, "action": "scale", "round": true, "name": "rangeNode", "property": "payload.sub.prop", "propertyOut": "result", "wires": [["helperNode1"]] },
{ id: "helperNode1", type: "helper", wires: [] }]
helper.load(rangeNode, flow, function () {
var rangeNode1 = helper.getNode("rangeNode1")
var helperNode1 = helper.getNode("helperNode1")
helperNode1.on("input", function (msg) {
try {
msg.should.have.property("result")
msg.result.should.equal(50)
done()
} catch (err) {
done(err)
}
})
rangeNode1.receive({ payload: { sub: { prop: 5 } } })
});
})
});

View File

@@ -60,6 +60,21 @@ describe('template node', function() {
});
});
it('should load with defaults', function (done) {
const flow = [{ id: "n1", type: "template", template: "payload={{payload}}" }]
helper.load(templateNode, flow, function () {
try {
const n1 = helper.getNode("n1")
n1.should.have.property('syntax', 'mustache')
n1.should.have.property('field', 'payload') // `propertyOut` on this node is `field`
n1.should.have.property('fieldType', 'msg')
n1.should.have.property('outputFormat', 'str')
done()
} catch (error) {
done(error)
}
})
})
it('should modify payload using node-configured template', function(done) {
var flow = [{id:"n1", type:"template", field:"payload", template:"payload={{payload}}",wires:[["n2"]]},{id:"n2",type:"helper"}];

View File

@@ -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)
});
});
});
@@ -479,6 +515,45 @@ describe('HTTP Request Node', function() {
});
describe('request', function() {
it('should load with defaults', function(done) {
const flow = [{id:"n1",type:"http request", name: "my http request",wires:[["n2"]]}]
helper.load(httpRequestNode, flow, function() {
try {
const n1 = helper.getNode("n1")
n1.should.have.property('name', 'my http request')
n1.should.not.have.property('method')
n1.should.have.property('property', 'payload')
n1.should.have.property('propertyOut', 'payload')
n1.should.have.property('ret', 'txt')
n1.should.have.property('reqTimeout', 120000)
n1.should.have.property('headers').and.be.an.Array().and.have.length(0)
n1.should.have.property('authType', 'basic')
done()
} catch (error) {
done(error)
}
})
})
it('should get using message properties specified for `property in` and `property out`', function (done) {
const flow = [{ id: "n1", type: "http request", wires: [["n2"]], method: "GET", ret: "txt", property: 'body.data.in', propertyOut: 'result', url: getTestURL('/text') },
{ id: "n2", type: "helper" }]
helper.load(httpRequestNode, flow, function () {
const n1 = helper.getNode("n1")
const n2 = helper.getNode("n2")
n2.on("input", function (msg) {
try {
msg.should.have.property('result', 'hello')
msg.should.have.property('statusCode', 200)
done()
} catch (err) {
done(err)
}
})
n1.receive({ body: { data: { in: 'foo' } } })
})
})
it('should get plain text content', function(done) {
var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getTestURL('/text')},
{id:"n2", type:"helper"}];

View File

@@ -23,7 +23,7 @@ const delayNode = require("nr-test-utils").require("@node-red/nodes/core/functio
const helper = require("node-red-node-test-helper");
// const { neq } = require("semver");
describe('CSV node (Legacy Mode)', function() {
describe.only('CSV node (Legacy Mode)', function() {
before(function(done) {
helper.startServer(done);
@@ -51,6 +51,8 @@ describe('CSV node (Legacy Mode)', function() {
// n1.should.have.property('lineend', '\n');
n1.should.have.property('multi', 'one');
n1.should.have.property('hdrin', false);
n1.should.have.property('property', 'payload');
n1.should.have.property('propertyOut', 'payload');
done();
} catch (error) {
done(error);
@@ -788,6 +790,26 @@ describe('CSV node (Legacy Mode)', function() {
});
});
it ('should use message properties specified for `property in` and `property out`', function(done) {
const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", property: 'payload.sub.prop', propertyOut: 'result.sub_prop', wires: [["n2"]] },
{ id: "n2", type: "helper" }];
helper.load(csvNode, flow, function () {
const n1 = helper.getNode("n1");
const n2 = helper.getNode("n2");
n2.on("input", function (msg) {
try {
msg.should.have.property('result').and.be.an.Object();
msg.result.should.have.property('sub_prop', { a: 1, b: 2, c: 3, d: 4 });
msg.should.have.property('columns', "a,b,c,d");
check_parts(msg, 0, 1);
done();
}
catch (e) { done(e); }
});
const testString = "1,2,3,4" + String.fromCharCode(10);
n1.emit("input", { payload: { sub: { prop: testString } } });
});
})
});
describe('json object to csv', function() {
@@ -1101,6 +1123,23 @@ describe('CSV node (Legacy Mode)', function() {
});
});
it ('should use message properties specified for `property in` and `property out`', function(done) {
const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", ret: '\n', property: 'payload.sub.prop', propertyOut: 'result', wires: [["n2"]] }, // RFC-vs-Legacy difference - use line separator \n to satisfy original test
{ id: "n2", type: "helper" }];
helper.load(csvNode, flow, function() {
const n1 = helper.getNode("n1");
const n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property('result', '4,3,2,1\n');
done();
} catch(e) { done(e); }
});
const testJson = { d: 1, b: 3, c: 2, a: 4 };
n1.emit("input", { payload: { sub: { prop: testJson } } });
});
})
});
it('should just pass through if no payload provided', function(done) {
@@ -1190,7 +1229,7 @@ describe('CSV node (Legacy Mode)', function() {
});
});
describe('CSV node (RFC Mode)', function () {
describe.only('CSV node (RFC Mode)', function () {
before(function (done) {
helper.startServer(done);
@@ -1219,6 +1258,8 @@ describe('CSV node (RFC Mode)', function () {
n1.should.have.property('ret', '\r\n'); // RFC-Legacy difference
n1.should.have.property('multi', 'one');
n1.should.have.property('hdrin', false);
n1.should.have.property('property', 'payload');
n1.should.have.property('propertyOut', 'payload');
done();
} catch (error) {
done(error);
@@ -1997,6 +2038,26 @@ describe('CSV node (RFC Mode)', function () {
});
});
it ('should use message properties specified for `property in` and `property out`', function(done) {
const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", property: 'payload.sub.prop', propertyOut: 'result.sub_prop', wires: [["n2"]] },
{ id: "n2", type: "helper" }];
helper.load(csvNode, flow, function () {
const n1 = helper.getNode("n1");
const n2 = helper.getNode("n2");
n2.on("input", function (msg) {
try {
msg.should.have.property('result').and.be.an.Object();
msg.result.should.have.property('sub_prop', { a: 1, b: 2, c: 3, d: 4 });
msg.should.have.property('columns', "a,b,c,d");
check_parts(msg, 0, 1);
done();
}
catch (e) { done(e); }
});
const testString = "1,2,3,4" + String.fromCharCode(10);
n1.emit("input", { payload: { sub: { prop: testString } } });
});
})
});
describe('json object to csv', function () {
@@ -2336,6 +2397,23 @@ describe('CSV node (RFC Mode)', function () {
});
});
it ('should use message properties specified for `property in` and `property out`', function(done) {
const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", ret: '\n', property: 'payload.sub.prop', propertyOut: 'result', wires: [["n2"]] }, // RFC-vs-Legacy difference - use line separator \n to satisfy original test
{ id: "n2", type: "helper" }];
helper.load(csvNode, flow, function() {
const n1 = helper.getNode("n1");
const n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property('result', '4,3,2,1\n');
done();
} catch(e) { done(e); }
});
const testJson = { d: 1, b: 3, c: 2, a: 4 };
n1.emit("input", { payload: { sub: { prop: testJson } } });
});
})
});
it('should just pass through if no payload provided', function (done) {

View File

@@ -32,6 +32,25 @@ describe('JSON node', function() {
helper.unload();
});
it('should be loaded with defaults', function(done) {
const flow = [{id:"jn1",type:"json", name: 'json node',wires:[["jn2"]]}]
helper.load(jsonNode, flow, function() {
const n1 = helper.getNode("jn1")
try {
n1.should.have.property('name', 'json node')
n1.should.have.property('property','payload')
n1.should.have.property('propertyOut','payload')
n1.should.have.property('schema', null)
n1.should.have.property('compiledSchema', null)
n1.should.have.property('action', '')
n1.should.have.property('indent', 0)
done();
} catch (error) {
done(error)
}
})
})
it('should convert a valid json string to a javascript object', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
@@ -587,4 +606,56 @@ describe('JSON node', function() {
jn1.receive({payload:jsonObject, schema:schema});
});
});
it('should convert a valid json string to a javascript object using message properties specified for `property in` and `property out`', function (done) {
const flow = [{ id: "jn1", type: "json", property: "payload.sub.prop", propertyOut: "result", wires: [["jn2"]] },
{ id: "jn2", type: "helper" }]
helper.load(jsonNode, flow, function () {
const jn1 = helper.getNode("jn1")
const jn2 = helper.getNode("jn2")
jn2.on("input", function (msg) {
msg.should.have.property('topic', 'bar')
msg.should.have.property('result').and.be.an.Object()
msg.result.should.have.property('employees')
msg.result.employees[0].should.have.property('firstName', 'John')
msg.result.employees[0].should.have.property('lastName', 'Smith')
done()
})
const jsonString = ' {"employees":[{"firstName":"John", "lastName":"Smith"}]}\r\n '
jn1.receive({ topic: "bar", payload: { sub: { prop: jsonString } } })
})
})
it('should convert a javascript object to a json string using message properties specified for `property in` and `property out`', function (done) {
const flow = [{ id: "jn1", type: "json", property: "payload.sub.prop", propertyOut: "result", wires: [["jn2"]] },
{ id: "jn2", type: "helper" }]
helper.load(jsonNode, flow, function () {
const jn1 = helper.getNode("jn1")
const jn2 = helper.getNode("jn2")
jn2.on("input", function (msg) {
msg.should.have.property('result').and.be.a.String()
should.equal(msg.result, '{"employees":[{"firstName":"John","lastName":"Smith"}]}')
done()
})
const obj = { employees: [{ firstName: "John", lastName: "Smith" }] }
jn1.receive({ payload: { sub: { prop: obj } } })
})
})
it('should pass through if property specified for `property in` is missing', function (done) {
const flow = [{ id: "jn1", type: "json", property: "payload.sub.prop", propertyOut: "result", wires: [["jn2"]] },
{ id: "jn2", type: "helper" }]
helper.load(jsonNode, flow, function () {
const jn1 = helper.getNode("jn1")
const jn2 = helper.getNode("jn2")
const obj = { employees: [{ firstName: "John", lastName: "Smith" }] }
jn2.on("input", function (msg) {
msg.should.not.have.property('result') // never set
msg.should.have.propertyByPath('payload', 'sub', 'propBAD').and.be.an.Object() // unchanged
msg.payload.sub.propBAD.should.deepEqual(obj)
done()
})
jn1.receive({ payload: { sub: { propBAD: obj } } })
})
})
});

View File

@@ -32,12 +32,18 @@ describe('XML node', function() {
helper.unload();
});
it('should be loaded', function(done) {
it('should load with defaults', function(done) {
var flow = [{id:"xmlNode1", type:"xml", name: "xmlNode" }];
helper.load(xmlNode, flow, function() {
var xmlNode1 = helper.getNode("xmlNode1");
xmlNode1.should.have.property('name', 'xmlNode');
done();
try {
var xmlNode1 = helper.getNode("xmlNode1");
xmlNode1.should.have.property('name', 'xmlNode');
xmlNode1.should.have.property('property', 'payload')
xmlNode1.should.have.property('propertyOut', 'payload')
done();
} catch (error) {
done(error);
}
});
});
@@ -195,4 +201,41 @@ describe('XML node', function() {
});
});
it('should convert a valid xml string to a javascript object using message properties specified for `property in` and `property out`', function(done) {
const flow = [{ id: "n1", type: "xml", property: "payload.sub.prop", propertyOut: 'result', wires: [["n2"]], func: "return msg;" },
{ id: "n2", type: "helper" }]
helper.load(xmlNode, flow, function () {
const n1 = helper.getNode("n1")
const n2 = helper.getNode("n2")
n2.on("input", function (msg) {
msg.should.have.property('topic', 'bar')
msg.should.have.property('result').and.be.an.Object()
msg.result.should.have.property('employees')
msg.result.employees.should.have.property('firstName')
should.equal(msg.result.employees.firstName[0], 'John')
msg.result.employees.should.have.property('lastName')
should.equal(msg.result.employees.lastName[0], 'Smith')
done()
})
const string = ' <employees><firstName>John</firstName><lastName>Smith</lastName></employees>\r\n '
n1.receive({ topic: "bar", payload: { sub: { prop: string } } })
})
})
it('should convert a javascript object to an xml string using message properties specified for `property in` and `property out`', function (done) {
const flow = [{ id: "n1", type: "xml", property: "payload.sub.prop", propertyOut: 'result', wires: [["n2"]], func: "return msg" },
{ id: "n2", type: "helper" }]
helper.load(xmlNode, flow, function () {
const n1 = helper.getNode("n1")
const n2 = helper.getNode("n2")
n2.on("input", function (msg) {
msg.should.have.property('topic', 'bar')
const index = msg.result.indexOf('<employees><firstName>John</firstName><lastName>Smith</lastName></employees>')
index.should.be.above(-1)
done()
})
const obj = { "employees": { "firstName": ["John"], "lastName": ["Smith"] } }
n1.receive({ topic: "bar", payload: { sub: { prop: obj } } })
})
})
});

View File

@@ -32,12 +32,18 @@ describe('YAML node', function() {
helper.unload();
});
it('should be loaded', function(done) {
it('should load with defaults', function(done) {
var flow = [{id:"yamlNode1", type:"yaml", name: "yamlNode" }];
helper.load(yamlNode, flow, function() {
var yamlNode1 = helper.getNode("yamlNode1");
yamlNode1.should.have.property('name', 'yamlNode');
done();
try {
var yamlNode1 = helper.getNode("yamlNode1");
yamlNode1.should.have.property('name', 'yamlNode');
yamlNode1.should.have.property('property', 'payload')
yamlNode1.should.have.property('propertyOut', 'payload')
done();
} catch (error) {
done(error);
}
});
});
@@ -192,4 +198,36 @@ describe('YAML node', function() {
});
});
it('should convert a valid yaml string to a javascript object using message properties specified for `property in` and `property out`', function (done) {
const flow = [{ id: "yn1", type: "yaml", property: "payload.sub.prop", propertyOut: "result", wires: [["yn2"]], func: "return msg;" },
{ id: "yn2", type: "helper" }]
helper.load(yamlNode, flow, function () {
const yn1 = helper.getNode("yn1")
const yn2 = helper.getNode("yn2")
yn2.on("input", function (msg) {
msg.should.have.property('topic', 'bar')
msg.result.should.have.property('employees')
msg.result.employees[0].should.have.property('firstName', 'John')
msg.result.employees[0].should.have.property('lastName', 'Smith')
done()
})
const yamlString = "employees:\n - firstName: John\n lastName: Smith\n"
yn1.receive({ topic: "bar", payload: { sub: { prop: yamlString } } })
})
})
it('should convert a javascript object to a yaml string using message properties specified for `property in` and `property out`', function (done) {
const flow = [{ id: "yn1", type: "yaml", property: "payload.sub.prop", propertyOut: "result", wires: [["yn2"]], func: "return msg;" },
{ id: "yn2", type: "helper" }]
helper.load(yamlNode, flow, function () {
const yn1 = helper.getNode("yn1")
const yn2 = helper.getNode("yn2")
yn2.on("input", function (msg) {
should.equal(msg.result, "employees:\n - firstName: John\n lastName: Smith\n")
done()
})
const obj = { employees: [{ firstName: "John", lastName: "Smith" }] }
yn1.receive({ payload: { sub: { prop: obj } } })
})
})
});

View File

@@ -43,6 +43,7 @@ describe('file Nodes', function() {
var relativePathToFile = "50-file-test-file.txt";
var resourcesDir = path.join(__dirname,"..","..","..","resources");
resourcesDir = resourcesDir.replace(/\\/g, '/'); // Windows
var fileToTest = path.join(resourcesDir,relativePathToFile);
var wait = 250;
@@ -240,7 +241,7 @@ describe('file Nodes', function() {
f.should.equal("Line1\nLine2\nLine3\nLine4");
}
else {
f.should.have.length(23);
f.should.have.length(26);
f.should.equal("Line1\r\nLine2\r\nLine3\r\nLine4");
}
done();
@@ -1220,6 +1221,7 @@ describe('file Nodes', function() {
var relativePathToFile = "50-file-test-file.txt";
var resourcesDir = path.join(__dirname,"..","..","..","resources");
resourcesDir = resourcesDir.replace(/\\/g, '/'); // Windows
var fileToTest = path.join(resourcesDir,relativePathToFile);
var fileToTest2 = "\t"+path.join(resourcesDir,relativePathToFile)+"\r\n";
var wait = 150;
@@ -1237,11 +1239,12 @@ describe('file Nodes', function() {
});
});
it('should be loaded', function(done) {
it('should load with defaults', function(done) {
var flow = [{id:"fileInNode1", type:"file in", name: "fileInNode", "filename":fileToTest, "format":"utf8"}];
helper.load(fileNode, flow, function() {
var n1 = helper.getNode("fileInNode1");
n1.should.have.property('name', 'fileInNode');
n1.should.have.property('propertyOut', 'payload')
done();
});
});
@@ -1528,6 +1531,23 @@ describe('file Nodes', function() {
});
});
it('should read in a file and output to the message property specified in `propertyOut`', function (done) {
const flow = [{ id: "fileInNode1", type: "file in", name: "fileInNode", "filename": fileToTest, "format": "", propertyOut: "file-data", wires: [["n2"]] },
{ id: "n2", type: "helper" }]
helper.load(fileNode, flow, function () {
const n1 = helper.getNode("fileInNode1")
const n2 = helper.getNode("n2")
n2.on("input", function (msg) {
msg.should.have.property('file-data')
Buffer.isBuffer(msg['file-data']).should.be.true()
msg['file-data'].should.have.length(40)
msg['file-data'].toString().should.equal('File message line 1\nFile message line 2\n')
done()
})
n1.receive({ payload: "" })
})
})
describe('encodings', function() {
function checkReadWithEncoding(enc, data, done) {

View File

@@ -0,0 +1,2 @@
describe('multiplayer', function() {
})