Compare commits

..

39 Commits

Author SHA1 Message Date
Nick O'Leary
03648dc7e8 Update tests for changed function node low-level output 2024-05-09 17:25:47 +01:00
Nick O'Leary
66a667fe58 Pass full error object in Function node and copy over cause property
Fixes #4683
2024-05-09 16:48:51 +01:00
Nick O'Leary
08927dfb55 Merge pull request #4684 from node-red/4363-autoLogin-redirect-loop
Avoid login loops when autoLogin enabled but login fails
2024-05-08 15:48:21 +01:00
Nick O'Leary
b27483de9c Avoid login loops when autoLogin enabled but login fails
Fixes #4363
2024-05-08 15:09:51 +01:00
Nick O'Leary
211d420fb2 Merge pull request #4667 from node-red/fix-subflow-property-undo
Fix undo of subflow env property edits
2024-04-23 23:45:46 +02:00
Nick O'Leary
b8ca4665c1 Merge pull request #4660 from JoshuaCWebDeveloper/patch-1
Fix three error typos in monaco.js
2024-04-23 23:45:16 +02:00
Nick O'Leary
960af87fb0 Ensure subflow change state is cleared after deploy 2024-04-23 21:17:35 +02:00
Nick O'Leary
de7339ae97 Fix undo of subflow env property edits 2024-04-23 20:39:14 +02:00
Stephen McLaughlin
0995af62b6 Merge pull request #4664 from ZJvandeWeg/patch-3
docs: Add closing paragraph tag
2024-04-20 13:54:37 +01:00
Zeger-Jan van de Weg
c2e03a40b4 docs: Add closing paragraph tag
Minor change that only improves xpath parsing.
2024-04-20 14:20:59 +02:00
Joshua Carter
c855050bcf Fix three error typos in monaco.js 2024-04-15 08:09:26 -07: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
Nick O'Leary
bd58431603 Fix use of spawn on windows with cmd files 2024-04-11 14:40:29 +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
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
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
410b938442 Bump for 3.1.8 2024-03-28 15:02:02 +00: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
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
90 changed files with 1229 additions and 4459 deletions

View File

@@ -12,11 +12,12 @@ permissions:
jobs:
build:
permissions:
checks: write # for coverallsapp/github-action to create new checks
contents: read # for actions/checkout to fetch code
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20]
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
@@ -28,3 +29,8 @@ jobs:
- name: Run tests
run: |
npm run test
# - name: Publish to coveralls.io
# if: ${{ matrix.node-version == 16 }}
# uses: coverallsapp/github-action@v1.1.2
# with:
# github-token: ${{ github.token }}

View File

@@ -1,33 +1,18 @@
#### 4.0.0-beta.1: Beta Release
#### 3.1.9: Maintenance Release
Editor
- 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
- Click on id in debug panel highlights node or flow (#4439) @ralphwetzel
- Support config selection in a subflow env var (#4587) @Steve-Mcl
- Add timestamp formatting options to TypedInput (#4468) @knolleary
- Allow RED.view.select to select links (#4553) @lgrkvst
- Add auto-complete to flow/global/env typedInput types (#4480) @knolleary
- Improve the appearance of the Node-RED primary header (#4598) @joepavitt
#### 3.1.8: Maintenance Release
Runtime
- let settings.httpNodeAuth accept single middleware or array of middlewares (#4572) @kevinGodell
- Upgrade to JSONata 2.x (#4590) @knolleary
- Bump minimum version to node 18 (#4571) @knolleary
- npm: Remove production flag on npm invocation (#4347) @ZJvandeWeg
- Timer testing fix (#4367) @hlovdal
- Bump to 4.0.0-dev (#4322) @knolleary
Nodes
- TCP node - when resetting, if no payload, stay disconnected @dceejay
- HTML node: add option for collecting attributes and content (#4513) @gorenje
- let split node specify property to split on, and join auto join correctly (#4386) @dceejay
- Add RFC4180 compliant mode to CSV node (#4540) @Steve-Mcl
- Fix change node to return boolean if asked (#4525) @dceejay
- Let msg.reset reset Tcp request node connection when in stay connected mode (#4406) @dceejay
- 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
- 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

View File

@@ -1,6 +1,6 @@
{
"name": "node-red",
"version": "4.0.0-beta.1",
"version": "3.1.9",
"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",
@@ -54,7 +54,7 @@
"is-utf8": "0.2.1",
"js-yaml": "4.1.0",
"json-stringify-safe": "5.0.1",
"jsonata": "2.0.4",
"jsonata": "1.8.7",
"lodash.clonedeep": "^4.5.0",
"media-typer": "1.1.0",
"memorystore": "1.6.7",
@@ -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",
@@ -122,6 +122,6 @@
"supertest": "6.3.3"
},
"engines": {
"node": ">=18"
"node": ">=14"
}
}

View File

@@ -33,9 +33,6 @@ module.exports = {
store: req.query['store'],
req: apiUtils.getRequestLogObject(req)
}
if (req.query['keysOnly'] !== undefined) {
opts.keysOnly = true
}
runtimeAPI.context.getValue(opts).then(function(result) {
res.json(result);
}).catch(function(err) {

View File

@@ -205,9 +205,10 @@ function genericStrategy(adminApp,strategy) {
passport.use(new strategy.strategy(options, verify));
adminApp.get('/auth/strategy',
passport.authenticate(strategy.name, {session:false,
passport.authenticate(strategy.name, {
session:false,
failureMessage: true,
failureRedirect: settings.httpAdminRoot
failureRedirect: settings.httpAdminRoot + '?session_message=Login Failed'
}),
completeGenerateStrategyAuth,
handleStrategyError
@@ -221,7 +222,7 @@ function genericStrategy(adminApp,strategy) {
passport.authenticate(strategy.name, {
session:false,
failureMessage: true,
failureRedirect: settings.httpAdminRoot
failureRedirect: settings.httpAdminRoot + '?session_message=Login Failed'
}),
completeGenerateStrategyAuth,
handleStrategyError

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/editor-api",
"version": "4.0.0-beta.1",
"version": "3.1.9",
"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": "3.1.9",
"@node-red/editor-client": "3.1.9",
"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

@@ -924,14 +924,7 @@
"date": "timestamp",
"jsonata": "expression",
"env": "env variable",
"cred": "credential",
"conf-types": "config node"
},
"date": {
"format": {
"timestamp": "milliseconds since epoch",
"object": "JavaScript Date Object"
}
"cred": "credential"
}
},
"editableList": {

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/editor-client",
"version": "4.0.0-beta.1",
"version": "3.1.9",
"license": "Apache-2.0",
"repository": {
"type": "git",

View File

@@ -91,31 +91,6 @@ RED.nodes = (function() {
getNodeTypes: function() {
return Object.keys(nodeDefinitions);
},
/**
* Get an array of node definitions
* @param {Object} options - options object
* @param {boolean} [options.configOnly] - if true, only return config nodes
* @param {function} [options.filter] - a filter function to apply to the list of nodes
* @returns array of node definitions
*/
getNodeDefinitions: function(options) {
const result = []
const configOnly = (options && options.configOnly)
const filter = (options && options.filter)
const keys = Object.keys(nodeDefinitions)
for (const key of keys) {
const def = nodeDefinitions[key]
if(!def) { continue }
if (configOnly && def.category !== "config") {
continue
}
if (filter && !filter(nodeDefinitions[key])) {
continue
}
result.push(nodeDefinitions[key])
}
return result
},
setNodeList: function(list) {
nodeList = [];
for(var i=0;i<list.length;i++) {
@@ -572,12 +547,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);
}
}
}
}
@@ -1050,7 +1029,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

@@ -174,24 +174,12 @@
this.uiContainer.width(m[1]);
}
if (this.options.sortable) {
var isCanceled = false; // Flag to track if an item has been canceled from being dropped into a different list
var noDrop = false; // Flag to track if an item is being dragged into a different list
var handle = (typeof this.options.sortable === 'string')?
this.options.sortable :
".red-ui-editableList-item-handle";
var sortOptions = {
axis: "y",
update: function( event, ui ) {
// dont trigger update if the item is being canceled
const targetList = $(event.target);
const draggedItem = ui.item;
const draggedItemParent = draggedItem.parent();
if (!targetList.is(draggedItemParent) && draggedItem.hasClass("red-ui-editableList-item-constrained")) {
noDrop = true;
}
if (isCanceled || noDrop) {
return;
}
if (that.options.sortItems) {
that.options.sortItems(that.items());
}
@@ -201,32 +189,8 @@
tolerance: "pointer",
forcePlaceholderSize:true,
placeholder: "red-ui-editabelList-item-placeholder",
start: function (event, ui) {
isCanceled = false;
ui.placeholder.height(ui.item.height() - 4);
ui.item.css('cursor', 'grabbing'); // TODO: this doesn't seem to work, use a class instead?
},
stop: function (event, ui) {
ui.item.css('cursor', 'auto');
},
receive: function (event, ui) {
if (ui.item.hasClass("red-ui-editableList-item-constrained")) {
isCanceled = true;
$(ui.sender).sortable('cancel');
}
},
over: function (event, ui) {
// if the dragged item is constrained, prevent it from being dropped into a different list
const targetList = $(event.target);
const draggedItem = ui.item;
const draggedItemParent = draggedItem.parent();
if (!targetList.is(draggedItemParent) && draggedItem.hasClass("red-ui-editableList-item-constrained")) {
noDrop = true;
draggedItem.css('cursor', 'no-drop'); // TODO: this doesn't seem to work, use a class instead?
} else {
noDrop = false;
draggedItem.css('cursor', 'grabbing'); // TODO: this doesn't seem to work, use a class instead?
}
start: function(e, ui){
ui.placeholder.height(ui.item.height()-4);
}
};
if (this.options.connectWith) {

View File

@@ -54,26 +54,25 @@
return icon;
}
function getMatch(value, searchValue) {
const idx = value.toLowerCase().indexOf(searchValue.toLowerCase());
const len = idx > -1 ? searchValue.length : 0;
return {
index: idx,
found: idx > -1,
pre: value.substring(0,idx),
match: value.substring(idx,idx+len),
post: value.substring(idx+len),
var autoComplete = function(options) {
function getMatch(value, searchValue) {
const idx = value.toLowerCase().indexOf(searchValue.toLowerCase());
const len = idx > -1 ? searchValue.length : 0;
return {
index: idx,
found: idx > -1,
pre: value.substring(0,idx),
match: value.substring(idx,idx+len),
post: value.substring(idx+len),
}
}
function generateSpans(match) {
const els = [];
if(match.pre) { els.push($('<span/>').text(match.pre)); }
if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); }
if(match.post) { els.push($('<span/>').text(match.post)); }
return els;
}
}
function generateSpans(match) {
const els = [];
if(match.pre) { els.push($('<span/>').text(match.pre)); }
if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); }
if(match.post) { els.push($('<span/>').text(match.post)); }
return els;
}
const msgAutoComplete = function(options) {
return function(val) {
var matches = [];
options.forEach(opt => {
@@ -103,197 +102,6 @@
}
}
function getEnvVars (obj, envVars = {}) {
contextKnownKeys.env = contextKnownKeys.env || {}
if (contextKnownKeys.env[obj.id]) {
return contextKnownKeys.env[obj.id]
}
let parent
if (obj.type === 'tab' || obj.type === 'subflow') {
RED.nodes.eachConfig(function (conf) {
if (conf.type === "global-config") {
parent = conf;
}
})
} else if (obj.g) {
parent = RED.nodes.group(obj.g)
} else if (obj.z) {
parent = RED.nodes.workspace(obj.z) || RED.nodes.subflow(obj.z)
}
if (parent) {
getEnvVars(parent, envVars)
}
if (obj.env) {
obj.env.forEach(env => {
envVars[env.name] = obj
})
}
contextKnownKeys.env[obj.id] = envVars
return envVars
}
const envAutoComplete = function (val) {
const editStack = RED.editor.getEditStack()
if (editStack.length === 0) {
done([])
return
}
const editingNode = editStack.pop()
if (!editingNode) {
return []
}
const envVarsMap = getEnvVars(editingNode)
const envVars = Object.keys(envVarsMap)
const matches = []
const i = val.lastIndexOf('${')
let searchKey = val
let isSubkey = false
if (i > -1) {
if (val.lastIndexOf('}') < i) {
searchKey = val.substring(i+2)
isSubkey = true
}
}
envVars.forEach(v => {
let valMatch = getMatch(v, searchKey);
if (valMatch.found) {
const optSrc = envVarsMap[v]
const element = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
valEl.append(generateSpans(valMatch))
valEl.appendTo(element)
if (optSrc) {
const optEl = $('<div>').css({ "font-size": "0.8em" });
let label
if (optSrc.type === 'global-config') {
label = RED._('sidebar.context.global')
} else if (optSrc.type === 'group') {
label = RED.utils.getNodeLabel(optSrc) || (RED._('sidebar.info.group') + ': '+optSrc.id)
} else {
label = RED.utils.getNodeLabel(optSrc) || optSrc.id
}
optEl.append(generateSpans({ match: label }));
optEl.appendTo(element);
}
matches.push({
value: isSubkey ? val + v + '}' : v,
label: element,
i: valMatch.index
});
}
})
matches.sort(function(A,B){return A.i-B.i})
return matches
}
let contextKnownKeys = {}
let contextCache = {}
if (RED.events) {
RED.events.on("editor:close", function () {
contextCache = {}
contextKnownKeys = {}
});
}
const contextAutoComplete = function() {
const that = this
const getContextKeysFromRuntime = function(scope, store, searchKey, done) {
contextKnownKeys[scope] = contextKnownKeys[scope] || {}
contextKnownKeys[scope][store] = contextKnownKeys[scope][store] || new Set()
if (searchKey.length > 0) {
try {
RED.utils.normalisePropertyExpression(searchKey)
} catch (err) {
// Not a valid context key, so don't try looking up
done()
return
}
}
const url = `context/${scope}/${encodeURIComponent(searchKey)}?store=${store}&keysOnly`
if (contextCache[url]) {
// console.log('CACHED', url)
done()
} else {
// console.log('GET', url)
$.getJSON(url, function(data) {
// console.log(data)
contextCache[url] = true
const result = data[store] || {}
const keys = result.keys || []
const keyPrefix = searchKey + (searchKey.length > 0 ? '.' : '')
keys.forEach(key => {
if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(key)) {
contextKnownKeys[scope][store].add(keyPrefix + key)
} else {
contextKnownKeys[scope][store].add(searchKey + "[\""+key.replace(/"/,"\\\"")+"\"]")
}
})
done()
})
}
}
const getContextKeys = function(key, done) {
const keyParts = key.split('.')
const partialKey = keyParts.pop()
let scope = that.propertyType
if (scope === 'flow') {
// Get the flow id of the node we're editing
const editStack = RED.editor.getEditStack()
if (editStack.length === 0) {
done([])
return
}
const editingNode = editStack.pop()
if (editingNode.z) {
scope = `${scope}/${editingNode.z}`
} else {
done([])
return
}
}
const store = (contextStoreOptions.length === 1) ? contextStoreOptions[0].value : that.optionValue
const searchKey = keyParts.join('.')
getContextKeysFromRuntime(scope, store, searchKey, function() {
if (contextKnownKeys[scope][store].has(key) || key.endsWith(']')) {
getContextKeysFromRuntime(scope, store, key, function() {
done(contextKnownKeys[scope][store])
})
}
done(contextKnownKeys[scope][store])
})
}
return function(val, done) {
getContextKeys(val, function (keys) {
const matches = []
keys.forEach(v => {
let optVal = v
let valMatch = getMatch(optVal, val);
if (!valMatch.found && val.length > 0 && val.endsWith('.')) {
// Search key ends in '.' - but doesn't match. Check again
// with [" at the end instead so we match bracket notation
valMatch = getMatch(optVal, val.substring(0, val.length - 1) + '["')
}
if (valMatch.found) {
const element = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
valEl.append(generateSpans(valMatch))
valEl.appendTo(element)
matches.push({
value: optVal,
label: element,
});
}
})
matches.sort(function(a, b) { return a.value.localeCompare(b.value) });
done(matches);
})
}
}
// This is a hand-generated list of completions for the core nodes (based on the node help html).
var msgCompletions = [
{ value: "payload" },
@@ -358,22 +166,20 @@
{ value: "_session", source: ["websocket out","tcp out"] },
]
var allOptions = {
msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: msgAutoComplete(msgCompletions)},
msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: autoComplete(msgCompletions)},
flow: {value:"flow",label:"flow.",hasValue:true,
options:[],
validate:RED.utils.validatePropertyExpression,
parse: contextParse,
export: contextExport,
valueLabel: contextLabel,
autoComplete: contextAutoComplete
valueLabel: contextLabel
},
global: {value:"global",label:"global.",hasValue:true,
options:[],
validate:RED.utils.validatePropertyExpression,
parse: contextParse,
export: contextExport,
valueLabel: contextLabel,
autoComplete: contextAutoComplete
valueLabel: contextLabel
},
str: {value:"str",label:"string",icon:"red/images/typedInput/az.svg"},
num: {value:"num",label:"number",icon:"red/images/typedInput/09.svg",validate: function(v) {
@@ -408,25 +214,7 @@
}
},
re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.svg"},
date: {
value:"date",
label:"timestamp",
icon:"fa fa-clock-o",
options:[
{
label: 'milliseconds since epoch',
value: ''
},
{
label: 'YYYY-MM-DDTHH:mm:ss.sssZ',
value: 'iso'
},
{
label: 'JavaScript Date Object',
value: 'object'
}
]
},
date: {value:"date",label:"timestamp",icon:"fa fa-clock-o",hasValue:false},
jsonata: {
value: "jsonata",
label: "expression",
@@ -463,8 +251,7 @@
env: {
value: "env",
label: "env variable",
icon: "red/images/typedInput/env.svg",
autoComplete: envAutoComplete
icon: "red/images/typedInput/env.svg"
},
node: {
value: "node",
@@ -596,75 +383,18 @@
eyeButton.show();
}
}
},
'conf-types': {
value: "conf-types",
label: "config",
icon: "fa fa-cog",
// hasValue: false,
valueLabel: function (container, value) {
// get the selected option (for access to the "name" and "module" properties)
const _options = this._optionsCache || this.typeList.find(opt => opt.value === value)?.options || []
const selectedOption = _options.find(opt => opt.value === value) || {
title: '',
name: '',
module: ''
}
container.attr("title", selectedOption.title) // set tooltip to the full path/id of the module/node
container.text(selectedOption.name) // apply the "name" of the selected option
// set "line-height" such as to make the "name" appear further up, giving room for the "module" to be displayed below the value
container.css("line-height", "1.4em")
// add the module name in smaller, lighter font below the value
$('<div></div>').text(selectedOption.module).css({
// "font-family": "var(--red-ui-monospace-font)",
color: "var(--red-ui-tertiary-text-color)",
"font-size": "0.8em",
"line-height": "1em",
opacity: 0.8
}).appendTo(container);
},
// hasValue: false,
options: function () {
if (this._optionsCache) {
return this._optionsCache
}
const configNodes = RED.nodes.registry.getNodeDefinitions({configOnly: true, filter: (def) => def.type !== "global-config"}).map((def) => {
// create a container with with 2 rows (row 1 for the name, row 2 for the module name in smaller, lighter font)
const container = $('<div style="display: flex; flex-direction: column; justify-content: space-between; row-gap: 1px;">')
const row1Name = $('<div>').text(def.type)
const row2Module = $('<div style="font-size: 0.8em; color: var(--red-ui-tertiary-text-color);">').text(def.set.module)
container.append(row1Name, row2Module)
return {
value: def.type,
name: def.type,
enabled: def.set.enabled ?? true,
local: def.set.local,
title: def.set.id, // tooltip e.g. "node-red-contrib-foo/bar"
module: def.set.module,
icon: container[0].outerHTML.trim(), // the typeInput will interpret this as html text and render it in the anchor
}
})
this._optionsCache = configNodes
return configNodes
}
}
};
// For a type with options, check value is a valid selection
// If !opt.multiple, returns the valid option object
// if opt.multiple, returns an array of valid option objects
// If not valid, returns null;
function isOptionValueValid(opt, currentVal) {
let _options = opt.options
if (typeof _options === "function") {
_options = _options.call(this)
}
if (!opt.multiple) {
for (var i=0;i<_options.length;i++) {
op = _options[i];
for (var i=0;i<opt.options.length;i++) {
op = opt.options[i];
if (typeof op === "string" && op === currentVal) {
return {value:currentVal}
} else if (op.value === currentVal) {
@@ -681,8 +411,8 @@
currentValues[v] = true;
}
});
for (var i=0;i<_options.length;i++) {
op = _options[i];
for (var i=0;i<opt.options.length;i++) {
op = opt.options[i];
var val = typeof op === "string" ? op : op.value;
if (currentValues.hasOwnProperty(val)) {
delete currentValues[val];
@@ -697,7 +427,6 @@
}
var nlsd = false;
let contextStoreOptions;
$.widget( "nodered.typedInput", {
_create: function() {
@@ -709,7 +438,7 @@
}
}
var contextStores = RED.settings.context.stores;
contextStoreOptions = contextStores.map(function(store) {
var contextOptions = contextStores.map(function(store) {
return {value:store,label: store, icon:'<i class="red-ui-typedInput-icon fa fa-database"></i>'}
}).sort(function(A,B) {
if (A.value === RED.settings.context.default) {
@@ -720,17 +449,13 @@
return A.value.localeCompare(B.value);
}
})
if (contextStoreOptions.length < 2) {
if (contextOptions.length < 2) {
allOptions.flow.options = [];
allOptions.global.options = [];
} else {
allOptions.flow.options = contextStoreOptions;
allOptions.global.options = contextStoreOptions;
allOptions.flow.options = contextOptions;
allOptions.global.options = contextOptions;
}
// Translate timestamp options
allOptions.date.options.forEach(opt => {
opt.label = RED._("typedInput.date.format." + (opt.value || 'timestamp'), {defaultValue: opt.label})
})
}
nlsd = true;
var that = this;
@@ -819,7 +544,7 @@
that.element.trigger('paste',evt);
});
this.input.on('keydown', function(evt) {
if (that.typeMap[that.propertyType].autoComplete || that.input.hasClass('red-ui-autoComplete')) {
if (that.typeMap[that.propertyType].autoComplete) {
return
}
if (evt.keyCode >= 37 && evt.keyCode <= 40) {
@@ -1113,9 +838,7 @@
if (this.optionMenu) {
this.optionMenu.remove();
}
if (this.menu) {
this.menu.remove();
}
this.menu.remove();
this.uiSelect.remove();
},
types: function(types) {
@@ -1148,7 +871,7 @@
this.menu = this._createMenu(this.typeList,{},function(v) { that.type(v) });
if (currentType && !this.typeMap.hasOwnProperty(currentType)) {
if (!firstCall) {
this.type(this.typeList[0]?.value || ""); // permit empty typeList
this.type(this.typeList[0].value);
}
} else {
this.propertyType = null;
@@ -1185,11 +908,6 @@
var selectedOption = [];
var valueToCheck = value;
if (opt.options) {
let _options = opt.options
if (typeof opt.options === "function") {
_options = opt.options.call(this)
}
if (opt.hasValue && opt.parse) {
var parts = opt.parse(value);
if (this.options.debug) { console.log(this.identifier,"new parse",parts) }
@@ -1203,8 +921,8 @@
checkValues = valueToCheck.split(",");
}
checkValues.forEach(function(valueToCheck) {
for (var i=0;i<_options.length;i++) {
var op = _options[i];
for (var i=0;i<opt.options.length;i++) {
var op = opt.options[i];
if (typeof op === "string") {
if (op === valueToCheck || op === ""+valueToCheck) {
selectedOption.push(that.activeOptions[op]);
@@ -1239,7 +957,7 @@
},
type: function(type) {
if (!arguments.length) {
return this.propertyType || this.options?.default || '';
return this.propertyType;
} else {
var that = this;
if (this.options.debug) { console.log(this.identifier,"----- SET TYPE -----",type) }
@@ -1249,9 +967,6 @@
// If previousType is !null, then this is a change of the type, rather than the initialisation
var previousType = this.typeMap[this.propertyType];
previousValue = this.input.val();
if (this.input.hasClass('red-ui-autoComplete')) {
this.input.autoComplete("destroy");
}
if (previousType && this.typeChanged) {
if (this.options.debug) { console.log(this.identifier,"typeChanged",{previousType,previousValue}) }
@@ -1298,9 +1013,7 @@
this.input.val(this.oldValues.hasOwnProperty("_")?this.oldValues["_"]:(opt.default||""))
}
if (previousType.autoComplete) {
if (this.input.hasClass('red-ui-autoComplete')) {
this.input.autoComplete("destroy");
}
this.input.autoComplete("destroy");
}
}
this.propertyType = type;
@@ -1340,10 +1053,6 @@
this.optionMenu = null;
}
if (opt.options) {
let _options = opt.options
if (typeof _options === "function") {
_options = opt.options.call(this);
}
if (this.optionExpandButton) {
this.optionExpandButton.hide();
this.optionExpandButton.shown = false;
@@ -1360,7 +1069,7 @@
this.valueLabelContainer.hide();
}
this.activeOptions = {};
_options.forEach(function(o) {
opt.options.forEach(function(o) {
if (typeof o === 'string') {
that.activeOptions[o] = {label:o,value:o};
} else {
@@ -1380,7 +1089,7 @@
if (validValues) {
that._updateOptionSelectLabel(validValues)
} else {
op = _options[0] || {value:""}; // permit zero options
op = opt.options[0];
if (typeof op === "string") {
this.value(op);
that._updateOptionSelectLabel({value:op});
@@ -1399,7 +1108,7 @@
that._updateOptionSelectLabel(validValues);
}
} else {
var selectedOption = this.optionValue||_options[0];
var selectedOption = this.optionValue||opt.options[0];
if (opt.parse) {
var selectedOptionObj = typeof selectedOption === "string"?{value:selectedOption}:selectedOption
var parts = opt.parse(this.input.val(),selectedOptionObj);
@@ -1432,18 +1141,8 @@
} else {
this.optionSelectTrigger.hide();
}
if (opt.autoComplete) {
let searchFunction = opt.autoComplete
if (searchFunction.length === 0) {
searchFunction = opt.autoComplete.call(this)
}
this.input.autoComplete({
search: searchFunction,
minLength: 0
})
}
}
this.optionMenu = this._createMenu(_options,opt,function(v){
this.optionMenu = this._createMenu(opt.options,opt,function(v){
if (!opt.multiple) {
that._updateOptionSelectLabel(that.activeOptions[v]);
if (!opt.hasValue) {
@@ -1484,12 +1183,8 @@
this.valueLabelContainer.hide();
this.elementDiv.show();
if (opt.autoComplete) {
let searchFunction = opt.autoComplete
if (searchFunction.length === 0) {
searchFunction = opt.autoComplete.call(this)
}
this.input.autoComplete({
search: searchFunction,
search: opt.autoComplete,
minLength: 0
})
}

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

@@ -612,7 +612,10 @@ RED.deploy = (function() {
}
});
RED.nodes.eachSubflow(function (subflow) {
subflow.changed = false;
if (subflow.changed) {
subflow.changed = false;
RED.events.emit("subflows:change", subflow);
}
});
RED.nodes.eachWorkspace(function (ws) {
if (ws.changed || ws.added) {

View File

@@ -326,78 +326,47 @@ RED.editor = (function() {
/**
* Create a config-node select box for this property
* @param {Object} node - the node being edited
* @param {String} property - the name of the node property
* @param {String} type - the type of the config-node
* @param {"node-config-input"|"node-input"|"node-input-subflow-env"} prefix - the prefix to use in the input element ids
* @param {Function} [filter] - a function to filter the list of config nodes
* @param {Object} [env] - the environment variable object (only used for subflow env vars)
* @param node - the node being edited
* @param property - the name of the field
* @param type - the type of the config-node
*/
function prepareConfigNodeSelect(node, property, type, prefix, filter, env) {
let nodeValue
if (prefix === 'node-input-subflow-env') {
nodeValue = env?.value
} else {
nodeValue = node[property]
}
const buttonId = `${prefix}-lookup-${property}`
const selectId = prefix + '-' + property
const input = $(`#${selectId}`);
if (input.length === 0) {
function prepareConfigNodeSelect(node,property,type,prefix,filter) {
var input = $("#"+prefix+"-"+property);
if (input.length === 0 ) {
return;
}
const attrStyle = input.attr('style');
let newWidth;
let m;
var newWidth = input.width();
var attrStyle = input.attr('style');
var m;
if ((m = /(^|\s|;)width\s*:\s*([^;]+)/i.exec(attrStyle)) !== null) {
newWidth = m[2].trim();
} else {
newWidth = "70%";
}
const outerWrap = $("<div></div>").css({
var outerWrap = $("<div></div>").css({
width: newWidth,
display: 'inline-flex'
display:'inline-flex'
});
const select = $('<select id="' + selectId + '"></select>').appendTo(outerWrap);
var select = $('<select id="'+prefix+'-'+property+'"></select>').appendTo(outerWrap);
input.replaceWith(outerWrap);
// set the style attr directly - using width() on FF causes a value of 114%...
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>')
.css({ "margin-left": "10px" })
updateConfigNodeSelect(property,type,node[property],prefix,filter);
$('<a id="'+prefix+'-lookup-'+property+'" class="red-ui-button"><i class="fa fa-pencil"></i></a>')
.css({"margin-left":"10px"})
.appendTo(outerWrap);
// add the click handler
btn.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 }
showEditConfigNodeDialog(property, type, selectedOpt.val(), prefix, node);
$('#'+prefix+'-lookup-'+property).on("click", function(e) {
showEditConfigNodeDialog(property,type,select.find(":selected").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")
if (selectedOpt?.data('env')) {
disableButton(true)
} else {
disableButton(false)
}
});
var label = "";
var configNode = RED.nodes.node(nodeValue);
var configNode = RED.nodes.node(node[property]);
var node_def = RED.nodes.getType(type);
if (configNode) {
label = RED.utils.getNodeLabel(configNode, configNode.id);
label = RED.utils.getNodeLabel(configNode,configNode.id);
}
input.val(label);
}
@@ -799,9 +768,12 @@ RED.editor = (function() {
}
function defaultConfigNodeSort(A,B) {
// sort case insensitive so that `[env] node-name` items are at the top and
// not mixed inbetween the the lower and upper case items
return (A.__label__ || '').localeCompare((B.__label__ || ''), undefined, {sensitivity: 'base'})
if (A.__label__ < B.__label__) {
return -1;
} else if (A.__label__ > B.__label__) {
return 1;
}
return 0;
}
function updateConfigNodeSelect(name,type,value,prefix,filter) {
@@ -816,7 +788,7 @@ RED.editor = (function() {
}
$("#"+prefix+"-"+name).val(value);
} else {
let inclSubflowEnvvars = false
var select = $("#"+prefix+"-"+name);
var node_def = RED.nodes.getType(type);
select.children().remove();
@@ -824,7 +796,6 @@ RED.editor = (function() {
var activeWorkspace = RED.nodes.workspace(RED.workspaces.active());
if (!activeWorkspace) {
activeWorkspace = RED.nodes.subflow(RED.workspaces.active());
inclSubflowEnvvars = true
}
var configNodes = [];
@@ -840,31 +811,6 @@ RED.editor = (function() {
}
}
});
// as includeSubflowEnvvars is true, this is a subflow.
// include any 'conf-types' env vars as a list of avaiable configs
// in the config dropdown as `[env] node-name`
if (inclSubflowEnvvars && activeWorkspace.env) {
const parentEnv = activeWorkspace.env.filter(env => env.ui?.type === 'conf-types' && env.type === type)
if (parentEnv && parentEnv.length > 0) {
const locale = RED.i18n.lang()
for (let i = 0; i < parentEnv.length; i++) {
const tenv = parentEnv[i]
const ui = tenv.ui || {}
const labels = ui.label || {}
const labelText = RED.editor.envVarList.lookupLabel(labels, labels["en-US"] || tenv.name, locale)
const config = {
env: tenv,
id: '${' + parentEnv[0].name + '}',
type: type,
label: labelText,
__label__: `[env] ${labelText}`
}
configNodes.push(config)
}
}
}
var configSortFn = defaultConfigNodeSort;
if (typeof node_def.sort == "function") {
configSortFn = node_def.sort;
@@ -876,10 +822,7 @@ RED.editor = (function() {
}
configNodes.forEach(function(cn) {
const option = $('<option value="'+cn.id+'"'+(value==cn.id?" selected":"")+'></option>').text(RED.text.bidi.enforceTextDirectionWithUCC(cn.__label__)).appendTo(select);
if (cn.env) {
option.data('env', cn.env) // set a data attribute to indicate this is an env var (to inhibit the edit button)
}
$('<option value="'+cn.id+'"'+(value==cn.id?" selected":"")+'></option>').text(RED.text.bidi.enforceTextDirectionWithUCC(cn.__label__)).appendTo(select);
delete cn.__label__;
});
@@ -1540,16 +1483,9 @@ RED.editor = (function() {
}
RED.tray.close(function() {
var filter = null;
// when editing a config via subflow edit panel, the `configProperty` will not
// necessarily be a property of the editContext._def.defaults object
// Also, when editing via dashboard sidebar, editContext can be null
// so we need to guard both scenarios
if (editContext?._def) {
const isSubflow = (editContext._def.type === 'subflow' || /subflow:.*/.test(editContext._def.type))
if (editContext && !isSubflow && typeof editContext._def.defaults?.[configProperty]?.filter === 'function') {
filter = function(n) {
return editContext._def.defaults[configProperty].filter.call(editContext,n);
}
if (editContext && typeof editContext._def.defaults[configProperty].filter === 'function') {
filter = function(n) {
return editContext._def.defaults[configProperty].filter.call(editContext,n);
}
}
updateConfigNodeSelect(configProperty,configType,editing_config_node.id,prefix,filter);
@@ -1610,7 +1546,7 @@ RED.editor = (function() {
RED.history.push(historyEvent);
RED.tray.close(function() {
var filter = null;
if (editContext && typeof editContext._def.defaults[configProperty]?.filter === 'function') {
if (editContext && typeof editContext._def.defaults[configProperty].filter === 'function') {
filter = function(n) {
return editContext._def.defaults[configProperty].filter.call(editContext,n);
}
@@ -1687,8 +1623,8 @@ RED.editor = (function() {
}
if (!isSameObj(old_env, new_env)) {
editing_node.env = new_env;
editState.changes.env = editing_node.env;
editing_node.env = new_env;
editState.changed = true;
}
@@ -2151,7 +2087,6 @@ RED.editor = (function() {
}
},
editBuffer: function(options) { showTypeEditor("_buffer", options) },
getEditStack: function () { return [...editStack] },
buildEditForm: buildEditForm,
validateNode: validateNode,
updateNodeProperties: updateNodeProperties,
@@ -2196,7 +2131,6 @@ RED.editor = (function() {
filteredEditPanes[type] = filter
}
editPanes[type] = definition;
},
prepareConfigNodeSelect: prepareConfigNodeSelect,
}
}
})();

View File

@@ -514,7 +514,7 @@ RED.editor.codeEditor.monaco = (function() {
_monaco.languages.json.jsonDefaults.setDiagnosticsOptions(diagnosticOptions);
if(modeConfiguration) { _monaco.languages.json.jsonDefaults.setModeConfiguration(modeConfiguration); }
} catch (error) {
console.warn("monaco - Error setting up json options", err)
console.warn("monaco - Error setting up json options", error)
}
}
@@ -526,7 +526,7 @@ RED.editor.codeEditor.monaco = (function() {
if(htmlDefaults) { _monaco.languages.html.htmlDefaults.setOptions(htmlDefaults); }
if(handlebarDefaults) { _monaco.languages.html.handlebarDefaults.setOptions(handlebarDefaults); }
} catch (error) {
console.warn("monaco - Error setting up html options", err)
console.warn("monaco - Error setting up html options", error)
}
}
@@ -546,7 +546,7 @@ RED.editor.codeEditor.monaco = (function() {
if(lessDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(lessDefaults_modeConfiguration); }
if(scssDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(scssDefaults_modeConfiguration); }
} catch (error) {
console.warn("monaco - Error setting up CSS/SCSS/LESS options", err)
console.warn("monaco - Error setting up CSS/SCSS/LESS options", error)
}
}

View File

@@ -1,9 +1,8 @@
RED.editor.envVarList = (function() {
var currentLocale = 'en-US';
const DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env'];
const DEFAULT_ENV_TYPE_LIST_INC_CONFTYPES = ['str','num','bool','json','bin','env','conf-types'];
const DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred','jsonata'];
var DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env'];
var DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred','jsonata'];
/**
* Create env var edit interface
@@ -11,8 +10,8 @@ RED.editor.envVarList = (function() {
* @param node - subflow node
*/
function buildPropertiesList(envContainer, node) {
if(RED.editor.envVarList.debug) { console.log('envVarList: buildPropertiesList', envContainer, node) }
const isTemplateNode = (node.type === "subflow");
var isTemplateNode = (node.type === "subflow");
envContainer
.css({
@@ -84,14 +83,7 @@ RED.editor.envVarList = (function() {
// if `opt.ui` does not exist, then apply defaults. If these
// defaults do not change then they will get stripped off
// before saving.
if (opt.type === 'conf-types') {
opt.ui = opt.ui || {
icon: "fa fa-cog",
type: "conf-types",
opts: {opts:[]}
}
opt.ui.type = "conf-types";
} else if (opt.type === 'cred') {
if (opt.type === 'cred') {
opt.ui = opt.ui || {
icon: "",
type: "cred"
@@ -127,7 +119,7 @@ RED.editor.envVarList = (function() {
}
});
buildEnvEditRow(uiRow, opt, nameField, valueField);
buildEnvEditRow(uiRow, opt.ui, nameField, valueField);
nameField.trigger('change');
}
},
@@ -189,23 +181,21 @@ RED.editor.envVarList = (function() {
* @param nameField - name field of env var
* @param valueField - value field of env var
*/
function buildEnvEditRow(container, opt, nameField, valueField) {
const ui = opt.ui
if(RED.editor.envVarList.debug) { console.log('envVarList: buildEnvEditRow', container, ui, nameField, valueField) }
function buildEnvEditRow(container, ui, nameField, valueField) {
container.addClass("red-ui-editor-subflow-env-ui-row")
var topRow = $('<div></div>').appendTo(container);
$('<div></div>').appendTo(topRow);
$('<div>').text(RED._("editor.icon")).appendTo(topRow);
$('<div>').text(RED._("editor.label")).appendTo(topRow);
$('<div class="red-env-ui-input-type-col">').text(RED._("editor.inputType")).appendTo(topRow);
$('<div>').text(RED._("editor.inputType")).appendTo(topRow);
var row = $('<div></div>').appendTo(container);
$('<div><i class="red-ui-editableList-item-handle fa fa-bars"></i></div>').appendTo(row);
var typeOptions = {
'input': {types:DEFAULT_ENV_TYPE_LIST_INC_CONFTYPES},
'select': {opts:[]},
'spinner': {},
'cred': {}
'input': {types:DEFAULT_ENV_TYPE_LIST},
'select': {opts:[]},
'spinner': {},
'cred': {}
};
if (ui.opts) {
typeOptions[ui.type] = ui.opts;
@@ -270,16 +260,15 @@ RED.editor.envVarList = (function() {
labelInput.attr("placeholder",$(this).val())
});
var inputCell = $('<div class="red-env-ui-input-type-col"></div>').appendTo(row);
var uiInputTypeInput = $('<input type="text">').css("width","100%").appendTo(inputCell);
var inputCell = $('<div></div>').appendTo(row);
var inputCellInput = $('<input type="text">').css("width","100%").appendTo(inputCell);
if (ui.type === "input") {
uiInputTypeInput.val(ui.opts.types.join(","));
inputCellInput.val(ui.opts.types.join(","));
}
var checkbox;
var selectBox;
// the options presented in the UI section for an "input" type selection
uiInputTypeInput.typedInput({
inputCellInput.typedInput({
types: [
{
value:"input",
@@ -440,7 +429,7 @@ RED.editor.envVarList = (function() {
}
});
ui.opts.opts = vals;
uiInputTypeInput.typedInput('value',Date.now())
inputCellInput.typedInput('value',Date.now())
}
}
}
@@ -507,13 +496,12 @@ RED.editor.envVarList = (function() {
} else {
delete ui.opts.max;
}
uiInputTypeInput.typedInput('value',Date.now())
inputCellInput.typedInput('value',Date.now())
}
}
}
}
},
'conf-types',
{
value:"none",
label:RED._("editor.inputs.none"), icon:"fa fa-times",hasValue:false
@@ -531,20 +519,14 @@ RED.editor.envVarList = (function() {
// In the case of 'input' type, the typedInput uses the multiple-option
// mode. Its value needs to be set to a comma-separately list of the
// selected options.
uiInputTypeInput.typedInput('value',ui.opts.types.join(","))
} else if (ui.type === 'conf-types') {
// In the case of 'conf-types' type, the typedInput will be populated
// with a list of all config nodes types installed.
// Restore the value to the last selected type
uiInputTypeInput.typedInput('value', opt.type)
inputCellInput.typedInput('value',ui.opts.types.join(","))
} else {
// No other type cares about `value`, but doing this will
// force a refresh of the label now that `ui.opts` has
// been updated.
uiInputTypeInput.typedInput('value',Date.now())
inputCellInput.typedInput('value',Date.now())
}
if(RED.editor.envVarList.debug) { console.log('envVarList: inputCellInput on:typedinputtypechange. ui.type = ' + ui.type) }
switch (ui.type) {
case 'input':
valueField.typedInput('types',ui.opts.types);
@@ -562,7 +544,7 @@ RED.editor.envVarList = (function() {
valueField.typedInput('types',['cred']);
break;
default:
valueField.typedInput('types', DEFAULT_ENV_TYPE_LIST);
valueField.typedInput('types',DEFAULT_ENV_TYPE_LIST)
}
if (ui.type === 'checkbox') {
valueField.typedInput('type','bool');
@@ -574,46 +556,8 @@ RED.editor.envVarList = (function() {
}
}).on("change", function(evt,type) {
const selectedType = $(this).typedInput('type') // the UI typedInput type
if(RED.editor.envVarList.debug) { console.log('envVarList: inputCellInput on:change. selectedType = ' + selectedType) }
if (selectedType === 'conf-types') {
const selectedConfigType = $(this).typedInput('value') || opt.type
let activeWorkspace = RED.nodes.workspace(RED.workspaces.active());
if (!activeWorkspace) {
activeWorkspace = RED.nodes.subflow(RED.workspaces.active());
}
// get a list of all config nodes matching the selectedValue
const configNodes = [];
RED.nodes.eachConfig(function(config) {
if (config.type == selectedConfigType && (!config.z || config.z === activeWorkspace.id)) {
const modulePath = config._def?.set?.id || ''
let label = RED.utils.getNodeLabel(config, config.id) || config.id;
label += config.d ? ' ['+RED._('workspace.disabled')+']' : '';
const _config = {
_type: selectedConfigType,
value: config.id,
label: label,
title: modulePath ? modulePath + ' - ' + label : label,
enabled: config.d !== true,
disabled: config.d === true,
}
configNodes.push(_config);
}
});
const tiTypes = {
value: selectedConfigType,
label: "config",
icon: "fa fa-cog",
options: configNodes,
}
valueField.typedInput('types', [tiTypes]);
valueField.typedInput('type', selectedConfigType);
valueField.typedInput('value', opt.value);
} else if (ui.type === 'input') {
var types = uiInputTypeInput.typedInput('value');
if (ui.type === 'input') {
var types = inputCellInput.typedInput('value');
ui.opts.types = (types === "") ? ["str"] : types.split(",");
valueField.typedInput('types',ui.opts.types);
}
@@ -625,7 +569,7 @@ RED.editor.envVarList = (function() {
})
// Set the input to the right type. This will trigger the 'typedinputtypechange'
// event handler (just above ^^) to update the value if needed
uiInputTypeInput.typedInput('type',ui.type)
inputCellInput.typedInput('type',ui.type)
}
function setLocale(l, list) {

View File

@@ -909,19 +909,17 @@ RED.subflow = (function() {
/**
* Build the edit dialog for a subflow template (creating/modifying a subflow template)
* @param {Object} uiContainer - the jQuery container for the environment variable list
* @param {Object} node - the subflow template node
* Create interface for controlling env var UI definition
*/
function buildEnvControl(uiContainer,node) {
function buildEnvControl(envList,node) {
var tabs = RED.tabs.create({
id: "subflow-env-tabs",
onchange: function(tab) {
if (tab.id === "subflow-env-tab-preview") {
var inputContainer = $("#subflow-input-ui");
var list = uiContainer.editableList("items");
var list = envList.editableList("items");
var exportedEnv = exportEnvList(list, true);
buildEnvUI(inputContainer, exportedEnv, node);
buildEnvUI(inputContainer, exportedEnv,node);
}
$("#subflow-env-tabs-content").children().hide();
$("#" + tab.id).show();
@@ -959,33 +957,12 @@ RED.subflow = (function() {
RED.editor.envVarList.setLocale(locale);
}
/**
* Build a UI row for a subflow instance environment variable
* Also used to build the UI row for subflow template preview
* @param {JQuery} row - A form row element
* @param {Object} tenv - A template environment variable
* @param {String} tenv.name - The name of the environment variable
* @param {String} tenv.type - The type of the environment variable
* @param {String} tenv.value - The value set for this environment variable
* @param {Object} tenv.parent - The parent environment variable
* @param {String} tenv.parent.value - The value set for the parent environment variable
* @param {String} tenv.parent.type - The type of the parent environment variable
* @param {Object} tenv.ui - The UI configuration for the environment variable
* @param {String} tenv.ui.icon - The icon for the environment variable
* @param {Object} tenv.ui.label - The label for the environment variable
* @param {String} tenv.ui.type - The type of the UI control for the environment variable
* @param {Object} node - The subflow instance node
*/
function buildEnvUIRow(row, tenv, node) {
if(RED.subflow.debug) { console.log("buildEnvUIRow", tenv) }
const ui = tenv.ui || {}
function buildEnvUIRow(row, tenv, ui, node) {
ui.label = ui.label||{};
if ((tenv.type === "cred" || (tenv.parent && tenv.parent.type === "cred")) && !ui.type) {
ui.type = "cred";
ui.opts = {};
} else if (tenv.type === "conf-types") {
ui.type = "conf-types"
ui.opts = { types: ['conf-types'] }
} else if (!ui.type) {
ui.type = "input";
ui.opts = { types: RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST }
@@ -1029,10 +1006,9 @@ RED.subflow = (function() {
if (tenv.hasOwnProperty('type')) {
val.type = tenv.type;
}
const elId = getSubflowEnvPropertyName(tenv.name)
switch(ui.type) {
case "input":
input = $('<input type="text">').css('width','70%').attr('id', elId).appendTo(row);
input = $('<input type="text">').css('width','70%').appendTo(row);
if (ui.opts.types && ui.opts.types.length > 0) {
var inputType = val.type;
if (ui.opts.types.indexOf(inputType) === -1) {
@@ -1059,7 +1035,7 @@ RED.subflow = (function() {
}
break;
case "select":
input = $('<select>').css('width','70%').attr('id', elId).appendTo(row);
input = $('<select>').css('width','70%').appendTo(row);
if (ui.opts.opts) {
ui.opts.opts.forEach(function(o) {
$('<option>').val(o.v).text(RED.editor.envVarList.lookupLabel(o.l, o.l['en-US']||o.v, locale)).appendTo(input);
@@ -1070,7 +1046,7 @@ RED.subflow = (function() {
case "checkbox":
label.css("cursor","default");
var cblabel = $('<label>').css('width','70%').appendTo(row);
input = $('<input type="checkbox">').attr('id', elId).css({
input = $('<input type="checkbox">').css({
marginTop: 0,
width: 'auto',
height: '34px'
@@ -1088,7 +1064,7 @@ RED.subflow = (function() {
input.prop("checked",boolVal);
break;
case "spinner":
input = $('<input>').css('width','70%').attr('id', elId).appendTo(row);
input = $('<input>').css('width','70%').appendTo(row);
var spinnerOpts = {};
if (ui.opts.hasOwnProperty('min')) {
spinnerOpts.min = ui.opts.min;
@@ -1117,25 +1093,18 @@ RED.subflow = (function() {
default: 'cred'
})
break;
case "conf-types":
// let clsId = 'config-node-input-' + val.type + '-' + val.value + '-' + Math.floor(Math.random() * 100000);
// clsId = clsId.replace(/\W/g, '-');
// input = $('<input>').css('width','70%').addClass(clsId).attr('id', elId).appendTo(row);
input = $('<input>').css('width','70%').attr('id', elId).appendTo(row);
const _type = tenv.parent?.type || tenv.type;
RED.editor.prepareConfigNodeSelect(node, tenv.name, _type, 'node-input-subflow-env', null, tenv);
break;
}
if (input) {
input.attr('id',getSubflowEnvPropertyName(tenv.name))
}
}
/**
* Build the edit form for a subflow instance
* Also used to build the preview form in the subflow template edit dialog
* Create environment variable input UI
* @param uiContainer - container for UI
* @param envList - env var definitions of template
*/
function buildEnvUI(uiContainer, envList, node) {
if(RED.subflow.debug) { console.log("buildEnvUI",envList) }
uiContainer.empty();
for (var i = 0; i < envList.length; i++) {
var tenv = envList[i];
@@ -1143,7 +1112,7 @@ RED.subflow = (function() {
continue;
}
var row = $("<div/>", { class: "form-row" }).appendTo(uiContainer);
buildEnvUIRow(row, tenv, node);
buildEnvUIRow(row,tenv, tenv.ui || {}, node);
}
}
// buildEnvUI
@@ -1216,9 +1185,6 @@ RED.subflow = (function() {
delete ui.opts
}
break;
case "conf-types":
delete ui.opts;
break;
default:
delete ui.opts;
}
@@ -1241,9 +1207,8 @@ RED.subflow = (function() {
if (/^subflow:/.test(node.type)) {
var subflowDef = RED.nodes.subflow(node.type.substring(8));
if (subflowDef.env) {
subflowDef.env.forEach(function(env, i) {
subflowDef.env.forEach(function(env) {
var item = {
index: i,
name:env.name,
parent: {
type: env.type,
@@ -1308,7 +1273,6 @@ RED.subflow = (function() {
}
function exportSubflowInstanceEnv(node) {
if(RED.subflow.debug) { console.log("exportSubflowInstanceEnv",node) }
var env = [];
// First, get the values for the SubflowTemplate defined properties
// - these are the ones with custom UI elements
@@ -1355,9 +1319,6 @@ RED.subflow = (function() {
item.type = 'bool';
item.value = ""+input.prop("checked");
break;
case "conf-types":
item.value = input.val()
item.type = data.parent.value;
}
if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) {
env.push(item);
@@ -1371,15 +1332,8 @@ RED.subflow = (function() {
return 'node-input-subflow-env-'+name.replace(/[^a-z0-9-_]/ig,"_");
}
/**
* Build the subflow edit form
* Called by subflow.oneditprepare for both instances and templates
* @param {"subflow"|"subflow-template"} type - the type of subflow being edited
* @param {Object} node - the node being edited
*/
// Called by subflow.oneditprepare for both instances and templates
function buildEditForm(type,node) {
if(RED.subflow.debug) { console.log("buildEditForm",type,node) }
if (type === "subflow-template") {
// This is the tabbed UI that offers the env list - with UI options
// plus the preview tab

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

@@ -435,15 +435,10 @@ RED.tourGuide = (function() {
function listTour() {
return [
{
id: "4_0",
label: "4.0",
path: "./tours/welcome.js"
},
{
id: "3_1",
label: "3.1",
path: "./tours/3.1/welcome.js"
path: "./tours/welcome.js"
},
{
id: "3_0",

View File

@@ -483,16 +483,6 @@ RED.utils = (function() {
$('<span class="red-ui-debug-msg-type-string-swatch"></span>').css('backgroundColor',obj).appendTo(e);
}
let n = RED.nodes.node(obj) ?? RED.nodes.workspace(obj);
if (n) {
if (options.nodeSelector && "function" == typeof options.nodeSelector) {
e.css('cursor', 'pointer').on("click", function(evt) {
evt.preventDefault();
options.nodeSelector(n.id);
})
}
}
} else if (typeof obj === 'number') {
e = $('<span class="red-ui-debug-msg-type-number"></span>').appendTo(entryObj);
@@ -599,7 +589,6 @@ RED.utils = (function() {
exposeApi: exposeApi,
// tools: tools // Do not pass tools down as we
// keep them attached to the top-level header
nodeSelector: options.nodeSelector,
}
).appendTo(row);
}
@@ -630,7 +619,6 @@ RED.utils = (function() {
exposeApi: exposeApi,
// tools: tools // Do not pass tools down as we
// keep them attached to the top-level header
nodeSelector: options.nodeSelector,
}
).appendTo(row);
}
@@ -687,7 +675,6 @@ RED.utils = (function() {
exposeApi: exposeApi,
// tools: tools // Do not pass tools down as we
// keep them attached to the top-level header
nodeSelector: options.nodeSelector,
}
).appendTo(row);
}

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
}
}
@@ -6252,10 +6265,6 @@ RED.view = (function() {
}
})
}
if (selection.links) {
selectedLinks.clear();
selection.links.forEach(selectedLinks.add);
}
}
}
updateSelection();

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

@@ -38,7 +38,7 @@ body {
}
#red-ui-main-container {
position: absolute;
top: var(--red-ui-header-height); left:0; bottom: 0; right:0;
top:40px; left:0; bottom: 0; right:0;
overflow:hidden;
}

View File

@@ -259,8 +259,7 @@ $deploy-button-background-disabled-hover: #555;
$header-background: #000;
$header-button-background-active: #121212;
$header-accent: #d41313;
$header-menu-color: #eee;
$header-menu-color: #C7C7C7;
$header-menu-color-disabled: #666;
$header-menu-heading-color: #fff;
$header-menu-sublabel-color: #aeaeae;

View File

@@ -23,20 +23,16 @@
top: 0;
left: 0;
width: 100%;
height: var(--red-ui-header-height);
height: 40px;
background: var(--red-ui-header-background);
box-sizing: border-box;
padding: 0px 0px 0px 20px;
color: var(--red-ui-header-menu-color);
font-size: 14px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid var(--red-ui-header-accent);
padding-top: 2px;
span.red-ui-header-logo {
float: left;
margin-top: 5px;
font-size: 30px;
line-height: 30px;
text-decoration: none;
@@ -46,7 +42,7 @@
vertical-align: middle;
font-size: 16px !important;
&:not(:first-child) {
margin-left: 8px;
margin-left: 5px;
}
}
img {

View File

@@ -1,17 +0,0 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
$header-height: 48px;

View File

@@ -15,5 +15,4 @@
**/
@import "colors";
@import "sizes";
@import "variables";

View File

@@ -15,7 +15,6 @@
**/
@import "colors";
@import "sizes";
@import "variables";
@import "mixins";

View File

@@ -16,9 +16,6 @@
--red-ui-shadow: #{$shadow};
// Header Height
--red-ui-header-height: #{$header-height};
// Main body text
--red-ui-primary-text-color: #{$primary-text-color};
// UI control label text
@@ -243,7 +240,6 @@
--red-ui-header-background: #{$header-background};
--red-ui-header-accent: #{$header-accent};
--red-ui-header-button-background-active: #{$header-button-background-active};
--red-ui-header-menu-color: #{$header-menu-color};
--red-ui-header-menu-color-disabled: #{$header-menu-color-disabled};

View File

@@ -1,231 +0,0 @@
export default {
version: "3.1.0",
steps: [
{
titleIcon: "fa fa-map-o",
title: {
"en-US": "Welcome to Node-RED 3.1!",
"ja": "Node-RED 3.1へようこそ!",
"fr": "Bienvenue dans Node-RED 3.1!"
},
description: {
"en-US": "<p>Let's take a moment to discover the new features in this release.</p>",
"ja": "<p>本リリースの新機能を見つけてみましょう。</p>",
"fr": "<p>Prenons un moment pour découvrir les nouvelles fonctionnalités de cette version.</p>"
}
},
{
title: {
"en-US": "New ways to work with groups",
"ja": "グループの新たな操作方法",
"fr": "De nouvelles façons de travailler avec les groupes"
},
description: {
"en-US": `<p>We have changed how you interact with groups in the editor.</p>
<ul>
<li>They don't get in the way when clicking on a node</li>
<li>They can be reordered using the Moving Forwards and Move Backwards actions</li>
<li>Multiple nodes can be dragged into a group in one go</li>
<li>Holding <code>Alt</code> when dragging a node will *remove* it from its group</li>
</ul>`,
"ja": `<p>エディタ上のグループの操作が変更されました。</p>
<ul>
<li>グループ内のノードをクリックする時に、グループが邪魔をすることが無くなりました。</li>
<li>「前面へ移動」と「背面へ移動」の動作を用いて、複数のグループの表示順序を変えることができます。</li>
<li>グループ内へ一度に複数のノードをドラッグできるようになりました。</li>
<li><code>Alt</code> を押したまま、グループ内のノードをドラッグすると、そのグループから *除く* ことができます。</li>
</ul>`,
"fr": `<p>Nous avons modifié la façon dont vous interagissez avec les groupes dans l'éditeur.</p>
<ul>
<li>Ils ne gênent plus lorsque vous cliquez sur un noeud</li>
<li>Ils peuvent être réorganisés à l'aide des actions Avancer et Reculer</li>
<li>Plusieurs noeuds peuvent être glissés dans un groupe en une seule fois</li>
<li>Maintenir <code>Alt</code> lors du déplacement d'un noeud le *supprimera* de son groupe</li>
</ul>`
}
},
{
title: {
"en-US": "Change notification on tabs",
"ja": "タブ上の変更通知",
"fr": "Notification de changement sur les onglets"
},
image: 'images/tab-changes.png',
description: {
"en-US": `<p>When a tab contains undeployed changes it now shows the
same style of change icon used by nodes.</p>
<p>This will make it much easier to track down changes when you're
working across multiple flows.</p>`,
"ja": `<p>タブ内にデプロイされていない変更が存在する時は、ノードと同じスタイルで変更の印が表示されるようになりました。</p>
<p>これによって複数のフローを編集している時に、変更を見つけるのが簡単になりました。</p>`,
"fr": `<p>Lorsqu'un onglet contient des modifications non déployées, il affiche désormais le
même style d'icône de changement utilisé par les noeuds.</p>
<p>Cela facilitera grandement le suivi des modifications lorsque vous
travaillez sur plusieurs flux.</p>`
}
},
{
title: {
"en-US": "A bigger canvas to work with",
"ja": "より広くなった作業キャンバス",
"fr": "Un canevas plus grand pour travailler"
},
description: {
"en-US": `<p>The default canvas size has been increased so you can fit more
into one flow.</p>
<p>We still recommend using tools such as subflows and Link Nodes to help
keep things organised, but now you have more room to work in.</p>`,
"ja": `<p>標準のキャンバスが広くなったため、1つのフローに沢山のものを含めることができるようになりました。</p>
<p>引き続き、サブフローやリンクノードなどの方法を用いて整理することをお勧めしますが、作業できる場所が増えました。</p>`,
"fr": `<p>La taille par défaut du canevas a été augmentée pour que vous puissiez en mettre plus
sur un seul flux.</p>
<p>Nous recommandons toujours d'utiliser des outils tels que les sous-flux et les noeuds de lien pour vous aider
à garder les choses organisées, mais vous avez maintenant plus d'espace pour travailler.</p>`
}
},
{
title: {
"en-US": "Finding help",
"ja": "ヘルプを見つける",
"fr": "Trouver de l'aide"
},
image: 'images/node-help.png',
description: {
"en-US": `<p>All node edit dialogs now include a link to that node's help
in the footer.</p>
<p>Clicking it will open up the Help sidebar showing the help for that node.</p>`,
"ja": `<p>全てのノードの編集ダイアログの下に、ノードのヘルプへのリンクが追加されました。</p>
<p>これをクリックすると、ノードのヘルプサイドバーが表示されます。</p>`,
"fr": `<p>Toutes les boîtes de dialogue d'édition de noeud incluent désormais un lien vers l'aide de ce noeud
dans le pied de page.</p>
<p>Cliquer dessus ouvrira la barre latérale d'aide affichant l'aide pour ce noeud.</p>`
}
},
{
title: {
"en-US": "Improved Context Menu",
"ja": "コンテキストメニューの改善",
"fr": "Menu contextuel amélioré"
},
image: 'images/context-menu.png',
description: {
"en-US": `<p>The editor's context menu has been expanded to make lots more of
the built-in actions available.</p>
<p>Adding nodes, working with groups and plenty
of other useful tools are now just a click away.</p>
<p>The flow tab bar also has its own context menu to make working
with your flows much easier.</p>`,
"ja": `<p>より多くの組み込み動作を利用できるように、エディタのコンテキストメニューが拡張されました。</p>
<p>ノードの追加、グループの操作、その他の便利なツールをクリックするだけで実行できるようになりました。</p>
<p>フローのタブバーには、フローの操作をより簡単にする独自のコンテキストメニューもあります。</p>`,
"fr": `<p>Le menu contextuel de l'éditeur a été étendu pour faire beaucoup plus d'actions intégrées disponibles.</p>
<p>Ajouter des noeuds, travailler avec des groupes et beaucoup d'autres outils utiles sont désormais à portée de clic.</p>
<p>La barre d'onglets de flux possède également son propre menu contextuel pour faciliter l'utilisation de vos flux.</p>`
}
},
{
title: {
"en-US": "Hiding Flows",
"ja": "フローを非表示",
"fr": "Masquage de flux"
},
image: 'images/hiding-flows.png',
description: {
"en-US": `<p>Hiding flows is now done through the flow context menu.</p>
<p>The 'hide' button in previous releases has been removed from the tabs
as they were being clicked accidentally too often.</p>`,
"ja": `<p>フローを非表示にする機能は、フローのコンテキストメニューから実行するようになりました。</p>
<p>これまでのリリースでタブに存在していた「非表示」ボタンは、よく誤ってクリックされていたため、削除されました。</p>`,
"fr": `<p>Le masquage des flux s'effectue désormais via le menu contextuel du flux.</p>
<p>Le bouton "Masquer" des versions précédentes a été supprimé des onglets
car il était cliqué accidentellement trop souvent.</p>`
},
},
{
title: {
"en-US": "Locking Flows",
"ja": "フローを固定",
"fr": "Verrouillage de flux"
},
image: 'images/locking-flows.png',
description: {
"en-US": `<p>Flows can now be locked to prevent accidental changes being made.</p>
<p>When locked you cannot modify the nodes in any way.</p>
<p>The flow context menu provides the options to lock and unlock flows,
as well as in the Info sidebar explorer.</p>`,
"ja": `<p>誤ってフローに変更が加えられてしまうのを防ぐために、フローを固定できるようになりました。</p>
<p>固定されている時は、ノードを修正することはできません。</p>
<p>フローのコンテキストメニューと、情報サイドバーのエクスプローラには、フローの固定や解除をするためのオプションが用意されています。</p>`,
"fr": `<p>Les flux peuvent désormais être verrouillés pour éviter toute modification accidentelle.</p>
<p>Lorsqu'il est verrouillé, vous ne pouvez en aucun cas modifier les noeuds.</p>
<p>Le menu contextuel du flux fournit les options pour verrouiller et déverrouiller les flux,
ainsi que dans l'explorateur de la barre latérale d'informations.</p>`
},
},
{
title: {
"en-US": "Adding Images to node/flow descriptions",
"ja": "ノードやフローの説明へ画像を追加",
"fr": "Ajout d'images aux descriptions de noeud/flux"
},
// image: 'images/debug-path-tooltip.png',
description: {
"en-US": `<p>You can now add images to a node's or flows's description.</p>
<p>Simply drag the image into the text editor and it will get added inline.</p>
<p>When the description is shown in the Info sidebar, the image will be displayed.</p>`,
"ja": `<p>ノードまたはフローの説明に、画像を追加できるようになりました。</p>
<p>画像をテキストエディタにドラッグするだけで、行内に埋め込まれます。</p>
<p>情報サイドバーの説明を開くと、その画像が表示されます。</p>`,
"fr": `<p>Vous pouvez désormais ajouter des images à la description d'un noeud ou d'un flux.</p>
<p>Faites simplement glisser l'image dans l'éditeur de texte et elle sera ajoutée en ligne.</p>
<p>Lorsque la description s'affiche dans la barre latérale d'informations, l'image s'affiche.</p>`
},
},
{
title: {
"en-US": "Adding Mermaid Diagrams",
"ja": "Mermaid図を追加",
"fr": "Ajout de diagrammes Mermaid"
},
image: 'images/mermaid.png',
description: {
"en-US": `<p>You can also add <a href="https://github.com/mermaid-js/mermaid">Mermaid</a> diagrams directly into your node or flow descriptions.</p>
<p>This gives you much richer options for documenting your flows.</p>`,
"ja": `<p>ノードやフローの説明に、<a href="https://github.com/mermaid-js/mermaid">Mermaid</a>図を直接追加することもできます。</p>
<p>これによって、フローを説明する文書作成の選択肢がより多くなります。</p>`,
"fr": `<p>Vous pouvez également ajouter des diagrammes <a href="https://github.com/mermaid-js/mermaid">Mermaid</a> directement dans vos descriptions de noeud ou de flux.</p>
<p>Cela vous offre des options beaucoup plus riches pour documenter vos flux.</p>`
},
},
{
title: {
"en-US": "Managing Global Environment Variables",
"ja": "グローバル環境変数の管理",
"fr": "Gestion des variables d'environnement globales"
},
image: 'images/global-env-vars.png',
description: {
"en-US": `<p>You can set environment variables that apply to all nodes and flows in the new
'Global Environment Variables' section of User Settings.</p>`,
"ja": `<p>ユーザ設定に新しく追加された「大域環境変数」のセクションで、全てのノードとフローに適用される環境変数を登録できます。</p>`,
"fr": `<p>Vous pouvez définir des variables d'environnement qui s'appliquent à tous les noeuds et flux dans la nouvelle
section "Global Environment Variables" des paramètres utilisateur.</p>`
},
},
{
title: {
"en-US": "Node Updates",
"ja": "ノードの更新",
"fr": "Mises à jour des noeuds"
},
// image: "images/",
description: {
"en-US": `<p>The core nodes have received lots of minor fixes, documentation updates and
small enhancements. Check the full changelog in the Help sidebar for a full list.</p>`,
"ja": `<p>コアノードにマイナーな修正、ドキュメント更新、小規模な拡張が数多く追加されています。全ての一覧は、ヘルプサイドバーの全ての更新履歴を確認してください。</p>`,
"fr": `<p>Les noeuds principaux ont reçu de nombreux correctifs mineurs, mises à jour de la documentation et
petites améliorations. Consulter le journal des modifications complet dans la barre latérale d'aide.</p>`
}
}
]
}

View File

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,12 +1,12 @@
export default {
version: "4.0.0-beta.1",
version: "3.1.0",
steps: [
{
titleIcon: "fa fa-map-o",
title: {
"en-US": "Welcome to Node-RED 4.0 Beta 1!",
"ja": "Node-RED 4.0 Beta 0へようこそ!",
"fr": "Bienvenue dans Node-RED 4.0 Beta 1!"
"en-US": "Welcome to Node-RED 3.1!",
"ja": "Node-RED 3.1へようこそ!",
"fr": "Bienvenue dans Node-RED 3.1!"
},
description: {
"en-US": "<p>Let's take a moment to discover the new features in this release.</p>",
@@ -16,49 +16,202 @@ export default {
},
{
title: {
"en-US": "Timestamp formatting options",
// "ja": ""
"en-US": "New ways to work with groups",
"ja": "グループの新たな操作方法",
"fr": "De nouvelles façons de travailler avec les groupes"
},
image: 'images/nr4-timestamp-formatting.png',
description: {
"en-US": `<p>Nodes that let you set a timestamp now have options on what format that timestamp should be in.</p>
<p>We're keeping it simple to begin with by providing three options:<p>
"en-US": `<p>We have changed how you interact with groups in the editor.</p>
<ul>
<li>Milliseconds since epoch - this is existing behaviour of the timestamp option</li>
<li>ISO 8601 - a common format used by many systems</li>
<li>JavaScript Data Object</li>
<li>They don't get in the way when clicking on a node</li>
<li>They can be reordered using the Moving Forwards and Move Backwards actions</li>
<li>Multiple nodes can be dragged into a group in one go</li>
<li>Holding <code>Alt</code> when dragging a node will *remove* it from its group</li>
</ul>`,
// "ja": ``
"ja": `<p>エディタ上のグループの操作が変更されました。</p>
<ul>
<li>グループ内のノードをクリックする時に、グループが邪魔をすることが無くなりました。</li>
<li>「前面へ移動」と「背面へ移動」の動作を用いて、複数のグループの表示順序を変えることができます。</li>
<li>グループ内へ一度に複数のノードをドラッグできるようになりました。</li>
<li><code>Alt</code> を押したまま、グループ内のノードをドラッグすると、そのグループから *除く* ことができます。</li>
</ul>`,
"fr": `<p>Nous avons modifié la façon dont vous interagissez avec les groupes dans l'éditeur.</p>
<ul>
<li>Ils ne gênent plus lorsque vous cliquez sur un noeud</li>
<li>Ils peuvent être réorganisés à l'aide des actions Avancer et Reculer</li>
<li>Plusieurs noeuds peuvent être glissés dans un groupe en une seule fois</li>
<li>Maintenir <code>Alt</code> lors du déplacement d'un noeud le *supprimera* de son groupe</li>
</ul>`
}
},
{
title: {
"en-US": "Auto-complete of flow/global and env types",
// "ja": ""
"en-US": "Change notification on tabs",
"ja": "タブ上の変更通知",
"fr": "Notification de changement sur les onglets"
},
image: 'images/nr4-auto-complete.png',
image: 'images/tab-changes.png',
description: {
"en-US": `<p>The <code>flow</code>/<code>global</code> context inputs and the <code>env</code> input
now all include auto-complete suggestions based on the live state of your flows.</p>
`,
// "ja": ``
"en-US": `<p>When a tab contains undeployed changes it now shows the
same style of change icon used by nodes.</p>
<p>This will make it much easier to track down changes when you're
working across multiple flows.</p>`,
"ja": `<p>タブ内にデプロイされていない変更が存在する時は、ノードと同じスタイルで変更の印が表示されるようになりました。</p>
<p>これによって複数のフローを編集している時に、変更を見つけるのが簡単になりました。</p>`,
"fr": `<p>Lorsqu'un onglet contient des modifications non déployées, il affiche désormais le
même style d'icône de changement utilisé par les noeuds.</p>
<p>Cela facilitera grandement le suivi des modifications lorsque vous
travaillez sur plusieurs flux.</p>`
}
},
{
title: {
"en-US": "Config node customisation in Subflows",
// "ja": ""
"en-US": "A bigger canvas to work with",
"ja": "より広くなった作業キャンバス",
"fr": "Un canevas plus grand pour travailler"
},
image: 'images/nr4-sf-config.png',
description: {
"en-US": `<p>Subflows can now be customised to allow each instance to use a different
config node of a selected type.</p>
<p>For example, each instance of a subflow that connects to an MQTT Broker and does some post-processing
of the messages received can be pointed at a different broker.</p>
`,
// "ja": ``
"en-US": `<p>The default canvas size has been increased so you can fit more
into one flow.</p>
<p>We still recommend using tools such as subflows and Link Nodes to help
keep things organised, but now you have more room to work in.</p>`,
"ja": `<p>標準のキャンバスが広くなったため、1つのフローに沢山のものを含めることができるようになりました。</p>
<p>引き続き、サブフローやリンクノードなどの方法を用いて整理することをお勧めしますが、作業できる場所が増えました。</p>`,
"fr": `<p>La taille par défaut du canevas a été augmentée pour que vous puissiez en mettre plus
sur un seul flux.</p>
<p>Nous recommandons toujours d'utiliser des outils tels que les sous-flux et les noeuds de lien pour vous aider
à garder les choses organisées, mais vous avez maintenant plus d'espace pour travailler.</p>`
}
},
{
title: {
"en-US": "Finding help",
"ja": "ヘルプを見つける",
"fr": "Trouver de l'aide"
},
image: 'images/node-help.png',
description: {
"en-US": `<p>All node edit dialogs now include a link to that node's help
in the footer.</p>
<p>Clicking it will open up the Help sidebar showing the help for that node.</p>`,
"ja": `<p>全てのノードの編集ダイアログの下に、ノードのヘルプへのリンクが追加されました。</p>
<p>これをクリックすると、ノードのヘルプサイドバーが表示されます。</p>`,
"fr": `<p>Toutes les boîtes de dialogue d'édition de noeud incluent désormais un lien vers l'aide de ce noeud
dans le pied de page.</p>
<p>Cliquer dessus ouvrira la barre latérale d'aide affichant l'aide pour ce noeud.</p>`
}
},
{
title: {
"en-US": "Improved Context Menu",
"ja": "コンテキストメニューの改善",
"fr": "Menu contextuel amélioré"
},
image: 'images/context-menu.png',
description: {
"en-US": `<p>The editor's context menu has been expanded to make lots more of
the built-in actions available.</p>
<p>Adding nodes, working with groups and plenty
of other useful tools are now just a click away.</p>
<p>The flow tab bar also has its own context menu to make working
with your flows much easier.</p>`,
"ja": `<p>より多くの組み込み動作を利用できるように、エディタのコンテキストメニューが拡張されました。</p>
<p>ノードの追加、グループの操作、その他の便利なツールをクリックするだけで実行できるようになりました。</p>
<p>フローのタブバーには、フローの操作をより簡単にする独自のコンテキストメニューもあります。</p>`,
"fr": `<p>Le menu contextuel de l'éditeur a été étendu pour faire beaucoup plus d'actions intégrées disponibles.</p>
<p>Ajouter des noeuds, travailler avec des groupes et beaucoup d'autres outils utiles sont désormais à portée de clic.</p>
<p>La barre d'onglets de flux possède également son propre menu contextuel pour faciliter l'utilisation de vos flux.</p>`
}
},
{
title: {
"en-US": "Hiding Flows",
"ja": "フローを非表示",
"fr": "Masquage de flux"
},
image: 'images/hiding-flows.png',
description: {
"en-US": `<p>Hiding flows is now done through the flow context menu.</p>
<p>The 'hide' button in previous releases has been removed from the tabs
as they were being clicked accidentally too often.</p>`,
"ja": `<p>フローを非表示にする機能は、フローのコンテキストメニューから実行するようになりました。</p>
<p>これまでのリリースでタブに存在していた「非表示」ボタンは、よく誤ってクリックされていたため、削除されました。</p>`,
"fr": `<p>Le masquage des flux s'effectue désormais via le menu contextuel du flux.</p>
<p>Le bouton "Masquer" des versions précédentes a été supprimé des onglets
car il était cliqué accidentellement trop souvent.</p>`
},
},
{
title: {
"en-US": "Locking Flows",
"ja": "フローを固定",
"fr": "Verrouillage de flux"
},
image: 'images/locking-flows.png',
description: {
"en-US": `<p>Flows can now be locked to prevent accidental changes being made.</p>
<p>When locked you cannot modify the nodes in any way.</p>
<p>The flow context menu provides the options to lock and unlock flows,
as well as in the Info sidebar explorer.</p>`,
"ja": `<p>誤ってフローに変更が加えられてしまうのを防ぐために、フローを固定できるようになりました。</p>
<p>固定されている時は、ノードを修正することはできません。</p>
<p>フローのコンテキストメニューと、情報サイドバーのエクスプローラには、フローの固定や解除をするためのオプションが用意されています。</p>`,
"fr": `<p>Les flux peuvent désormais être verrouillés pour éviter toute modification accidentelle.</p>
<p>Lorsqu'il est verrouillé, vous ne pouvez en aucun cas modifier les noeuds.</p>
<p>Le menu contextuel du flux fournit les options pour verrouiller et déverrouiller les flux,
ainsi que dans l'explorateur de la barre latérale d'informations.</p>`
},
},
{
title: {
"en-US": "Adding Images to node/flow descriptions",
"ja": "ノードやフローの説明へ画像を追加",
"fr": "Ajout d'images aux descriptions de noeud/flux"
},
// image: 'images/debug-path-tooltip.png',
description: {
"en-US": `<p>You can now add images to a node's or flows's description.</p>
<p>Simply drag the image into the text editor and it will get added inline.</p>
<p>When the description is shown in the Info sidebar, the image will be displayed.</p>`,
"ja": `<p>ノードまたはフローの説明に、画像を追加できるようになりました。</p>
<p>画像をテキストエディタにドラッグするだけで、行内に埋め込まれます。</p>
<p>情報サイドバーの説明を開くと、その画像が表示されます。</p>`,
"fr": `<p>Vous pouvez désormais ajouter des images à la description d'un noeud ou d'un flux.</p>
<p>Faites simplement glisser l'image dans l'éditeur de texte et elle sera ajoutée en ligne.</p>
<p>Lorsque la description s'affiche dans la barre latérale d'informations, l'image s'affiche.</p>`
},
},
{
title: {
"en-US": "Adding Mermaid Diagrams",
"ja": "Mermaid図を追加",
"fr": "Ajout de diagrammes Mermaid"
},
image: 'images/mermaid.png',
description: {
"en-US": `<p>You can also add <a href="https://github.com/mermaid-js/mermaid">Mermaid</a> diagrams directly into your node or flow descriptions.</p>
<p>This gives you much richer options for documenting your flows.</p>`,
"ja": `<p>ノードやフローの説明に、<a href="https://github.com/mermaid-js/mermaid">Mermaid</a>図を直接追加することもできます。</p>
<p>これによって、フローを説明する文書作成の選択肢がより多くなります。</p>`,
"fr": `<p>Vous pouvez également ajouter des diagrammes <a href="https://github.com/mermaid-js/mermaid">Mermaid</a> directement dans vos descriptions de noeud ou de flux.</p>
<p>Cela vous offre des options beaucoup plus riches pour documenter vos flux.</p>`
},
},
{
title: {
"en-US": "Managing Global Environment Variables",
"ja": "グローバル環境変数の管理",
"fr": "Gestion des variables d'environnement globales"
},
image: 'images/global-env-vars.png',
description: {
"en-US": `<p>You can set environment variables that apply to all nodes and flows in the new
'Global Environment Variables' section of User Settings.</p>`,
"ja": `<p>ユーザ設定に新しく追加された「大域環境変数」のセクションで、全てのノードとフローに適用される環境変数を登録できます。</p>`,
"fr": `<p>Vous pouvez définir des variables d'environnement qui s'appliquent à tous les noeuds et flux dans la nouvelle
section "Global Environment Variables" des paramètres utilisateur.</p>`
},
},
{
title: {
"en-US": "Node Updates",
@@ -68,13 +221,10 @@ export default {
// image: "images/",
description: {
"en-US": `<p>The core nodes have received lots of minor fixes, documentation updates and
small enhancements. Check the full changelog in the Help sidebar for a full list.</p>
<ul>
<li>A fully RFC4180 compliant CSV mode</li>
<li>Customisable headers on the WebSocket node</li>
<li>Split node now can operate on any message property</li>
<li>and lots more...</li>
</ul>`
small enhancements. Check the full changelog in the Help sidebar for a full list.</p>`,
"ja": `<p>コアノードにマイナーな修正、ドキュメント更新、小規模な拡張が数多く追加されています。全ての一覧は、ヘルプサイドバーの全ての更新履歴を確認してください。</p>`,
"fr": `<p>Les noeuds principaux ont reçu de nombreux correctifs mineurs, mises à jour de la documentation et
petites améliorations. Consulter le journal des modifications complet dans la barre latérale d'aide.</p>`
}
}
]

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

@@ -5,7 +5,6 @@ module.exports = function(RED) {
const fs = require("fs-extra");
const path = require("path");
var debuglength = RED.settings.debugMaxLength || 1000;
var statuslength = RED.settings.debugStatusLength || 32;
var useColors = RED.settings.debugUseColors || false;
util.inspect.styles.boolean = "red";
const { hasOwnProperty } = Object.prototype;
@@ -165,7 +164,7 @@ module.exports = function(RED) {
}
}
if (st.length > statuslength) { st = st.substr(0,statuslength) + "..."; }
if (st.length > 32) { st = st.substr(0,32) + "..."; }
var newStatus = {fill:fill, shape:shape, text:st};
if (JSON.stringify(newStatus) !== node.oldState) { // only send if we have to

View File

@@ -512,8 +512,7 @@ RED.debug = (function() {
hideKey: false,
path: path,
sourceId: sourceNode&&sourceNode.id,
rootPath: path,
nodeSelector: config.messageSourceClick,
rootPath: path
});
// Do this in a separate step so the element functions aren't stripped
debugMessage.appendTo(el);

View File

@@ -438,10 +438,9 @@ module.exports = function(RED) {
//store the error in msg to be used in flows
msg.error = err;
var line = 0;
var errorMessage;
if (stack.length > 0) {
let line = 0;
let errorMessage;
while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) {
line++;
}
@@ -455,11 +454,13 @@ module.exports = function(RED) {
errorMessage += " (line "+lineno+", col "+cha+")";
}
}
if (errorMessage) {
err.message = errorMessage
}
}
if (!errorMessage) {
errorMessage = err.toString();
}
done(errorMessage);
// Pass the whole error object so any additional properties
// (such as cause) are preserved
done(err);
}
else if (typeof err === "string") {
done(err);

View File

@@ -117,7 +117,7 @@ module.exports = function(RED) {
});
return
} else if (rule.tot === 'date') {
value = RED.util.evaluateNodeProperty(rule.to, rule.tot, node)
value = Date.now();
} else if (rule.tot === 'jsonata') {
RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => {
if (err) {
@@ -233,9 +233,7 @@ 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);
RED.util.setMessageProperty(msg,property,current);
}
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {

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

@@ -40,99 +40,6 @@
(function() {
const headerTypes = [
/*
{ value: "Accept", label: "Accept", hasValue: false },
{ value: "Accept-Encoding", label: "Accept-Encoding", hasValue: false },
{ value: "Accept-Language", label: "Accept-Language", hasValue: false },
*/
{ value: "Authorization", label: "Authorization", hasValue: false },
/*
{ value: "Content-Type", label: "Content-Type", hasValue: false },
{ value: "Cache-Control", label: "Cache-Control", hasValue: false },
*/
{ value: "User-Agent", label: "User-Agent", hasValue: false },
/*
{ value: "Location", label: "Location", hasValue: false },
*/
{ value: "other", label: RED._("node-red:httpin.label.other"),
hasValue: true, icon: "red/images/typedInput/az.svg" },
]
const headerOptions = {};
const defaultOptions = [
{ value: "other", label: RED._("node-red:httpin.label.other"),
hasValue: true, icon: "red/images/typedInput/az.svg" },
"env",
];
/*
headerOptions["accept"] = [
{ value: "text/plain", label: "text/plain", hasValue: false },
{ value: "text/html", label: "text/html", hasValue: false },
{ value: "application/json", label: "application/json", hasValue: false },
{ value: "application/xml", label: "application/xml", hasValue: false },
...defaultOptions,
];
headerOptions["accept-encoding"] = [
{ value: "gzip", label: "gzip", hasValue: false },
{ value: "deflate", label: "deflate", hasValue: false },
{ value: "compress", label: "compress", hasValue: false },
{ value: "br", label: "br", hasValue: false },
{ value: "gzip, deflate", label: "gzip, deflate", hasValue: false },
{ value: "gzip, deflate, br", label: "gzip, deflate, br", hasValue: false },
...defaultOptions,
];
headerOptions["accept-language"] = [
{ value: "*", label: "*", hasValue: false },
{ value: "en-GB, en-US, en;q=0.9", label: "en-GB, en-US, en;q=0.9", hasValue: false },
{ value: "de-AT, de-DE;q=0.9, en;q=0.5", label: "de-AT, de-DE;q=0.9, en;q=0.5", hasValue: false },
{ value: "es-mx,es,en;q=0.5", label: "es-mx,es,en;q=0.5", hasValue: false },
{ value: "fr-CH, fr;q=0.9, en;q=0.8", label: "fr-CH, fr;q=0.9, en;q=0.8", hasValue: false },
{ value: "zh-CN, zh-TW; q = 0.9, zh-HK; q = 0.8, zh; q = 0.7, en; q = 0.6", label: "zh-CN, zh-TW; q = 0.9, zh-HK; q = 0.8, zh; q = 0.7, en; q = 0.6", hasValue: false },
{ value: "ja-JP, jp", label: "ja-JP, jp", hasValue: false },
...defaultOptions,
];
headerOptions["content-type"] = [
{ value: "text/css", label: "text/css", hasValue: false },
{ value: "text/plain", label: "text/plain", hasValue: false },
{ value: "text/html", label: "text/html", hasValue: false },
{ value: "application/json", label: "application/json", hasValue: false },
{ value: "application/octet-stream", label: "application/octet-stream", hasValue: false },
{ value: "application/pdf", label: "application/pdf", hasValue: false },
{ value: "application/xml", label: "application/xml", hasValue: false },
{ value: "application/zip", label: "application/zip", hasValue: false },
{ value: "multipart/form-data", label: "multipart/form-data", hasValue: false },
{ value: "audio/aac", label: "audio/aac", hasValue: false },
{ value: "audio/ac3", label: "audio/ac3", hasValue: false },
{ value: "audio/basic", label: "audio/basic", hasValue: false },
{ value: "audio/mp4", label: "audio/mp4", hasValue: false },
{ value: "audio/ogg", label: "audio/ogg", hasValue: false },
{ value: "image/bmp", label: "image/bmp", hasValue: false },
{ value: "image/gif", label: "image/gif", hasValue: false },
{ value: "image/jpeg", label: "image/jpeg", hasValue: false },
{ value: "image/png", label: "image/png", hasValue: false },
{ value: "image/tiff", label: "image/tiff", hasValue: false },
...defaultOptions,
];
headerOptions["cache-control"] = [
{ value: "max-age=0", label: "max-age=0", hasValue: false },
{ value: "max-age=86400", label: "max-age=86400", hasValue: false },
{ value: "no-cache", label: "no-cache", hasValue: false },
...defaultOptions,
];
*/
headerOptions["user-agent"] = [
{ value: "Mozilla/5.0", label: "Mozilla/5.0", hasValue: false },
...defaultOptions,
];
function getHeaderOptions(headerName) {
const lc = (headerName || "").toLowerCase();
let opts = headerOptions[lc];
return opts || defaultOptions;
}
function ws_oneditprepare() {
$("#websocket-client-row").hide();
$("#node-input-mode").on("change", function() {
@@ -285,8 +192,7 @@
value: "",
label:RED._("node-red:websocket.sendheartbeat"),
validate: RED.validators.number(/*blank allowed*/true) },
subprotocol: {value:"",required: false},
headers: { value: [] }
subprotocol: {value:"",required: false}
},
inputs:0,
outputs:0,
@@ -294,9 +200,6 @@
return this.path;
},
oneditprepare: function() {
const node = this;
$("#node-config-input-path").on("change keyup paste",function() {
$(".node-config-row-tls").toggle(/^wss:/i.test($(this).val()))
});
@@ -311,114 +214,14 @@
if (!heartbeatActive) {
$("#node-config-input-hb").val("");
}
const hasMatch = function (arr, value) {
return arr.some(function (ht) {
return ht.value === value
});
}
const headerList = $("#node-input-headers-container").css('min-height', '150px').css('min-width', '450px').editableList({
addItem: function (container, i, header) {
const row = $('<div/>').css({
overflow: 'hidden',
whiteSpace: 'nowrap',
display: 'flex'
}).appendTo(container);
const propertNameCell = $('<div/>').css({ 'flex-grow': 1 }).appendTo(row);
const propertyName = $('<input/>', { class: "node-input-header-name", type: "text", style: "width: 100%" })
.appendTo(propertNameCell)
.typedInput({ types: headerTypes });
const propertyValueCell = $('<div/>').css({ 'flex-grow': 1, 'margin-left': '10px' }).appendTo(row);
const propertyValue = $('<input/>', { class: "node-input-header-value", type: "text", style: "width: 100%" })
.appendTo(propertyValueCell)
.typedInput({
types: getHeaderOptions(header.keyType)
});
const setup = function(_header) {
const headerTypeIsAPreset = function(h) {return hasMatch(headerTypes, h) };
const headerValueIsAPreset = function(h, v) {return hasMatch(getHeaderOptions(h), v) };
const {keyType, keyValue, valueType, valueValue} = header;
if(keyType == "other") {
propertyName.typedInput('type', keyType);
propertyName.typedInput('value', keyValue);
} else if (headerTypeIsAPreset(keyType)) {
propertyName.typedInput('type', keyType);
} else {
propertyName.typedInput('type', "other");
propertyName.typedInput('value', keyValue);
}
if(valueType == "other" || valueType == "env" ) {
propertyValue.typedInput('type', valueType);
propertyValue.typedInput('value', valueValue);
} else if (headerValueIsAPreset(propertyName.typedInput('type'), valueType)) {
propertyValue.typedInput('type', valueType);
} else {
propertyValue.typedInput('type', "other");
propertyValue.typedInput('value', valueValue);
}
}
setup(header);
propertyName.on('change', function (event) {
propertyValue.typedInput('types', getHeaderOptions(propertyName.typedInput('type')));
});
},
sortable: true,
removable: true
});
if (node.headers) {
for (let index = 0; index < node.headers.length; index++) {
const element = node.headers[index];
headerList.editableList('addItem', node.headers[index]);
}
}
},
oneditsave: function() {
const node = this;
if (!/^wss:/i.test($("#node-config-input-path").val())) {
$("#node-config-input-tls").val("_ADD_");
}
if (!$("#node-config-input-hb-cb").prop("checked")) {
$("#node-config-input-hb").val("0");
}
const headers = $("#node-input-headers-container").editableList('items');
node.headers = [];
headers.each(function(i) {
const header = $(this);
const keyType = header.find(".node-input-header-name").typedInput('type');
const keyValue = header.find(".node-input-header-name").typedInput('value');
const valueType = header.find(".node-input-header-value").typedInput('type');
const valueValue = header.find(".node-input-header-value").typedInput('value');
node.headers.push({
keyType, keyValue, valueType, valueValue
})
});
},
oneditresize: function(size) {
const dlg = $("#dialog-form");
const expandRow = dlg.find('.node-input-headers-container-row');
let height = dlg.height() - 5;
if(expandRow && expandRow.length){
const siblingRows = dlg.find('> .form-row:not(.node-input-headers-container-row)');
for (let i = 0; i < siblingRows.size(); i++) {
const cr = $(siblingRows[i]);
if(cr.is(":visible"))
height -= cr.outerHeight(true);
}
$("#node-input-headers-container").editableList('height',height);
}
}
});
@@ -496,15 +299,8 @@
<span data-i18n="inject.seconds"></span>
</span>
</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>
<div class="form-row node-input-headers-container-row">
<ol id="node-input-headers-container"></ol>
</div>
<div class="form-tips">
<p><span data-i18n="[html]websocket.tip.url1"></span></p>
<p><span data-i18n="[html]websocket.tip.url2"></span></p>
<span data-i18n="[html]websocket.tip.headers"></span>
<span data-i18n="[html]websocket.tip.url2"></span>
</div>
</script>

View File

@@ -58,7 +58,6 @@ module.exports = function(RED) {
node.isServer = !/^ws{1,2}:\/\//i.test(node.path);
node.closing = false;
node.tls = n.tls;
node.upgradeHeaders = n.headers
if (n.hb) {
var heartbeat = parseInt(n.hb);
@@ -97,42 +96,6 @@ module.exports = function(RED) {
tlsNode.addTLSOptions(options);
}
}
// We need to check if undefined, to guard against previous installs, that will not have had this property set (applies to 3.1.x setups)
// Else this will be breaking potentially
if(node.upgradeHeaders !== undefined && node.upgradeHeaders.length > 0){
options.headers = {};
for(let i = 0;i<node.upgradeHeaders.length;i++){
const header = node.upgradeHeaders[i];
const keyType = header.keyType;
const keyValue = header.keyValue;
const valueType = header.valueType;
const valueValue = header.valueValue;
const headerName = keyType === 'other' ? keyValue : keyType;
let headerValue;
switch(valueType){
case 'other':
headerValue = valueValue;
break;
case 'env':
headerValue = RED.util.evaluateNodeProperty(valueValue,valueType,node);
break;
default:
headerValue = valueType;
break;
}
if(headerName && headerValue){
options.headers[headerName] = headerValue
}
}
}
var socket = new ws(node.path,node.subprotocol,options);
socket.setMaxListeners(0);
node.server = socket; // keep for closing

View File

@@ -411,33 +411,23 @@ module.exports = function(RED) {
if (msg._session && msg._session.type == "tcp") {
var client = connectionPool[msg._session.id];
if (client) {
if (msg?.reset === true) {
client.destroy();
}
else {
if (Buffer.isBuffer(msg.payload)) {
client.write(msg.payload);
} else if (typeof msg.payload === "string" && node.base64) {
client.write(Buffer.from(msg.payload,'base64'));
} else {
client.write(Buffer.from(""+msg.payload));
}
if (Buffer.isBuffer(msg.payload)) {
client.write(msg.payload);
} else if (typeof msg.payload === "string" && node.base64) {
client.write(Buffer.from(msg.payload,'base64'));
} else {
client.write(Buffer.from(""+msg.payload));
}
}
}
else {
for (var i in connectionPool) {
if (msg?.reset === true) {
connectionPool[i].destroy();
}
else {
if (Buffer.isBuffer(msg.payload)) {
connectionPool[i].write(msg.payload);
} else if (typeof msg.payload === "string" && node.base64) {
connectionPool[i].write(Buffer.from(msg.payload,'base64'));
} else {
connectionPool[i].write(Buffer.from(""+msg.payload));
}
if (Buffer.isBuffer(msg.payload)) {
connectionPool[i].write(msg.payload);
} else if (typeof msg.payload === "string" && node.base64) {
connectionPool[i].write(Buffer.from(msg.payload,'base64'));
} else {
connectionPool[i].write(Buffer.from(""+msg.payload));
}
}
}
@@ -557,34 +547,13 @@ module.exports = function(RED) {
this.on("input", function(msg, nodeSend, nodeDone) {
var i = 0;
if (msg.payload !== undefined && (!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) {
if ((!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) {
msg.payload = msg.payload.toString();
}
var host = node.server || msg.host;
var port = node.port || msg.port;
if (node.out === "sit" && msg?.reset) {
if (msg.reset === true) { // kill all connections
for (var cl in clients) {
if (clients[cl].hasOwnProperty("client")) {
clients[cl].client.destroy();
delete clients[cl];
}
}
}
if (typeof(msg.reset) === "string" && msg.reset.includes(":")) { // just kill connection host:port
if (clients.hasOwnProperty(msg.reset) && clients[msg.reset].hasOwnProperty("client")) {
clients[msg.reset].client.destroy();
delete clients[msg.reset];
}
}
const cc = Object.keys(clients).length;
node.status({fill:"green",shape:cc===0?"ring":"dot",text:RED._("tcpin.status.connections",{count:cc})});
if ((host === undefined || port === undefined) && !msg.hasOwnProperty("payload")) { return; }
if (!msg.hasOwnProperty("payload")) { return; }
}
// Store client information independently
// the clients object will have:
// clients[id].client, clients[id].msg, clients[id].timeout
@@ -652,16 +621,13 @@ module.exports = function(RED) {
clients[connection_id].connecting = true;
clients[connection_id].client.connect(connOpts, function() {
//node.log(RED._("tcpin.errors.client-connected"));
// node.status({fill:"green",shape:"dot",text:"common.status.connected"});
node.status({fill:"green",shape:"dot",text:RED._("tcpin.status.connections",{count:Object.keys(clients).length})});
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].connected = true;
clients[connection_id].connecting = false;
let event;
while (event = dequeue(clients[connection_id].msgQueue)) {
if (event.msg.payload !== undefined) {
clients[connection_id].client.write(event.msg.payload);
}
clients[connection_id].client.write(event.msg.payload);
event.nodeDone();
}
if (node.out === "time" && node.splitc < 0) {
@@ -857,9 +823,7 @@ module.exports = function(RED) {
else if (!clients[connection_id].connecting && clients[connection_id].connected) {
if (clients[connection_id] && clients[connection_id].client) {
let event = dequeue(clients[connection_id].msgQueue)
if (event.msg.payload !== undefined ) {
clients[connection_id].client.write(event.msg.payload);
}
clients[connection_id].client.write(event.msg.payload);
event.nodeDone();
}
}

View File

@@ -17,20 +17,7 @@
</select>
<input style="width:40px;" type="text" id="node-input-sep" pattern=".">
</div>
<div class="form-row">
<label><i class="fa fa-code"></i> <span data-i18n="csv.label.spec"></span></label>
<div style="display: inline-grid;width: 70%;">
<select style="width:100%" id="csv-option-spec">
<option value="rfc" data-i18n="csv.spec.rfc"></option>
<option value="" data-i18n="csv.spec.legacy"></option>
</select>
<div>
<div class="form-tips csv-lecacy-warning" data-i18n="node-red:csv.spec.legacy_warning"
style="width: calc(100% - 18px); margin-top: 4px; max-width: unset;">
</div>
</div>
</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">
@@ -73,10 +60,10 @@
<div class="form-row" style="padding-left:20px;">
<label></label>
<label style="width:auto; margin-right:10px;" for="node-input-ret"><span data-i18n="csv.label.newline"></span></label>
<select style="width:calc(70% - 108px);" id="node-input-ret">
<option value='\r\n' data-i18n="csv.newline.windows"></option>
<select style="width:150px;" id="node-input-ret">
<option value='\n' data-i18n="csv.newline.linux"></option>
<option value='\r' data-i18n="csv.newline.mac"></option>
<option value='\r\n' data-i18n="csv.newline.windows"></option>
</select>
</div>
</script>
@@ -88,7 +75,6 @@
color:"#DEBD5C",
defaults: {
name: {value:""},
spec: {value:"rfc"},
sep: {
value:',', required:true,
label:RED._("node-red:csv.label.separator"),
@@ -97,7 +83,7 @@
hdrin: {value:""},
hdrout: {value:"none"},
multi: {value:"one",required:true},
ret: {value:'\\r\\n'}, // default to CRLF (RFC4180 Sec 2.1: "Each record is located on a separate line, delimited by a line break (CRLF)")
ret: {value:'\\n'},
temp: {value:""},
skip: {value:"0"},
strings: {value:true},
@@ -137,27 +123,6 @@
$("#node-input-sep").hide();
}
});
$("#csv-option-spec").on("change", function() {
if ($("#csv-option-spec").val() == "rfc") {
$(".form-tips.csv-lecacy-warning").hide();
} else {
$(".form-tips.csv-lecacy-warning").show();
}
});
// new nodes will have `spec` set to "rfc" (default), but existing nodes will either not have
// a spec value or it will be empty - we need to maintain the legacy behaviour for existing
// flows but default to rfc for new nodes
let spec = !this.spec ? "" : "rfc"
$("#csv-option-spec").val(spec).trigger("change")
},
oneditsave: function() {
const specFormVal = $("#csv-option-spec").val() || '' // empty === legacy
const spectNodeVal = this.spec || '' // empty === legacy, null/undefined means in-place node upgrade (keep as is)
if (specFormVal !== spectNodeVal) {
// only update the flow value if changed (avoid marking the node dirty unnecessarily)
this.spec = specFormVal
}
}
});
</script>

View File

@@ -15,674 +15,322 @@
**/
module.exports = function(RED) {
const csv = require('./lib/csv')
"use strict";
function CSVNode(n) {
RED.nodes.createNode(this,n)
const node = this
const RFC4180Mode = n.spec === 'rfc'
const legacyMode = !RFC4180Mode
RED.nodes.createNode(this,n);
this.template = (n.temp || "");
this.sep = (n.sep || ',').replace(/\\t/g,"\t").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
this.quo = '"';
this.ret = (n.ret || "\n").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
this.winflag = (this.ret === "\r\n");
this.lineend = "\n";
this.multi = n.multi || "one";
this.hdrin = n.hdrin || false;
this.hdrout = n.hdrout || "none";
this.goodtmpl = true;
this.skip = parseInt(n.skip || 0);
this.store = [];
this.parsestrings = n.strings;
this.include_empty_strings = n.include_empty_strings || false;
this.include_null_values = n.include_null_values || false;
if (this.parsestrings === undefined) { this.parsestrings = true; }
if (this.hdrout === false) { this.hdrout = "none"; }
if (this.hdrout === true) { this.hdrout = "all"; }
var tmpwarn = true;
var node = this;
var re = new RegExp(node.sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') + '(?=(?:(?:[^"]*"){2})*[^"]*$)','g');
node.status({}) // clear status
// pass in an array of column names to be trimmed, de-quoted and retrimmed
var clean = function(col,sep) {
if (sep) { re = new RegExp(sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') +'(?=(?:(?:[^"]*"){2})*[^"]*$)','g'); }
col = col.trim().split(re) || [""];
col = col.map(x => x.replace(/"/g,'').trim());
if ((col.length === 1) && (col[0] === "")) { node.goodtmpl = false; }
else { node.goodtmpl = true; }
return col;
}
var template = clean(node.template,',');
var notemplate = template.length === 1 && template[0] === '';
node.hdrSent = false;
if (legacyMode) {
this.template = (n.temp || "");
this.sep = (n.sep || ',').replace(/\\t/g,"\t").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
this.quo = '"';
this.ret = (n.ret || "\n").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
this.winflag = (this.ret === "\r\n");
this.lineend = "\n";
this.multi = n.multi || "one";
this.hdrin = n.hdrin || false;
this.hdrout = n.hdrout || "none";
this.goodtmpl = true;
this.skip = parseInt(n.skip || 0);
this.store = [];
this.parsestrings = n.strings;
this.include_empty_strings = n.include_empty_strings || false;
this.include_null_values = n.include_null_values || false;
if (this.parsestrings === undefined) { this.parsestrings = true; }
if (this.hdrout === false) { this.hdrout = "none"; }
if (this.hdrout === true) { this.hdrout = "all"; }
var tmpwarn = true;
// var node = this;
var re = new RegExp(node.sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') + '(?=(?:(?:[^"]*"){2})*[^"]*$)','g');
// pass in an array of column names to be trimmed, de-quoted and retrimmed
var clean = function(col,sep) {
if (sep) { re = new RegExp(sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') +'(?=(?:(?:[^"]*"){2})*[^"]*$)','g'); }
col = col.trim().split(re) || [""];
col = col.map(x => x.replace(/"/g,'').trim());
if ((col.length === 1) && (col[0] === "")) { node.goodtmpl = false; }
else { node.goodtmpl = true; }
return col;
this.on("input", function(msg, send, done) {
if (msg.hasOwnProperty("reset")) {
node.hdrSent = false;
}
var template = clean(node.template,',');
var notemplate = template.length === 1 && template[0] === '';
node.hdrSent = false;
this.on("input", function(msg, send, done) {
if (msg.hasOwnProperty("reset")) {
node.hdrSent = false;
}
if (msg.hasOwnProperty("payload")) {
if (typeof msg.payload == "object") { // convert object to CSV string
try {
if (!(notemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
template = clean(node.template);
if (msg.hasOwnProperty("payload")) {
if (typeof msg.payload == "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 (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]);
}
}
const ou = [];
if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; }
if (node.hdrout !== "none" && node.hdrSent === false) {
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;
}
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 (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;
}
}
ou.push(msg.payload[s].join(node.sep));
}
else {
if ((template.length === 1) && (template[0] === '') && (msg.hasOwnProperty("columns"))) {
template = clean(msg.columns || "",",");
}
if ((template.length === 1) && (template[0] === '')) {
if (msg.hasOwnProperty("columns")) {
template = clean(msg.columns || "",",");
/* istanbul ignore else */
if (tmpwarn === true) { // just warn about missing template once
node.warn(RED._("csv.errors.obj_csv"));
tmpwarn = false;
}
else {
template = Object.keys(msg.payload[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;
}
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 (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;
}
}
ou.push(msg.payload[s].join(node.sep));
}
else {
if ((template.length === 1) && (template[0] === '') && (msg.hasOwnProperty("columns"))) {
template = clean(msg.columns || "",",");
}
if ((template.length === 1) && (template[0] === '')) {
const row = [];
for (var p in msg.payload[0]) {
/* istanbul ignore else */
if (tmpwarn === true) { // just warn about missing template once
node.warn(RED._("csv.errors.obj_csv"));
tmpwarn = false;
}
const row = [];
for (var p in msg.payload[0]) {
if (msg.payload[s].hasOwnProperty(p)) {
/* istanbul ignore else */
if (msg.payload[s].hasOwnProperty(p)) {
/* istanbul ignore else */
if (typeof msg.payload[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)) {
var q = "";
if (msg.payload[s][p] !== undefined) {
q += msg.payload[s][p];
}
if (q.indexOf(node.quo) !== -1) { // add double quotes if any quotes
q = q.replace(/"/g, '""');
row.push(node.quo + q + node.quo);
}
else if (q.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
row.push(node.quo + q + node.quo);
}
else { row.push(q); } // otherwise just add
if (typeof msg.payload[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)) {
var q = "";
if (msg.payload[s][p] !== undefined) {
q += msg.payload[s][p];
}
if (q.indexOf(node.quo) !== -1) { // add double quotes if any quotes
q = q.replace(/"/g, '""');
row.push(node.quo + q + node.quo);
}
else if (q.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
row.push(node.quo + q + node.quo);
}
else { row.push(q); } // otherwise just add
}
}
ou.push(row.join(node.sep)); // add separator
}
else {
const row = [];
for (var t=0; t < template.length; t++) {
if (template[t] === '') {
row.push('');
}
else {
var tt = template[t];
if (template[t].indexOf('"') >=0 ) { tt = "'"+tt+"'"; }
else { tt = '"'+tt+'"'; }
var p = RED.util.getMessageProperty(msg,'payload["'+s+'"]['+tt+']');
/* istanbul ignore else */
if (p === undefined) { p = ""; }
// fix to honour include null values flag
//if (p === null && node.include_null_values !== true) { p = "";}
p = RED.util.ensureString(p);
if (p.indexOf(node.quo) !== -1) { // add double quotes if any quotes
p = p.replace(/"/g, '""');
row.push(node.quo + p + node.quo);
}
else if (p.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
row.push(node.quo + p + node.quo);
}
else { row.push(p); } // otherwise just add
}
ou.push(row.join(node.sep)); // add separator
}
else {
const row = [];
for (var t=0; t < template.length; t++) {
if (template[t] === '') {
row.push('');
}
else {
var tt = template[t];
if (template[t].indexOf('"') >=0 ) { tt = "'"+tt+"'"; }
else { tt = '"'+tt+'"'; }
var p = RED.util.getMessageProperty(msg,'payload["'+s+'"]['+tt+']');
/* istanbul ignore else */
if (p === undefined) { p = ""; }
// fix to honour include null values flag
//if (p === null && node.include_null_values !== true) { p = "";}
p = RED.util.ensureString(p);
if (p.indexOf(node.quo) !== -1) { // add double quotes if any quotes
p = p.replace(/"/g, '""');
row.push(node.quo + p + node.quo);
}
else if (p.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
row.push(node.quo + p + node.quo);
}
else { row.push(p); } // otherwise just add
}
ou.push(row.join(node.sep)); // add separator
}
ou.push(row.join(node.sep)); // add separator
}
}
// join lines, don't forget to add the last new line
msg.payload = ou.join(node.ret) + node.ret;
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).join(',');
if (msg.payload !== '') {
send(msg);
}
done();
}
catch(e) { done(e); }
// join lines, don't forget to add the last new line
msg.payload = ou.join(node.ret) + node.ret;
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).join(',');
if (msg.payload !== '') { send(msg); }
done();
}
else if (typeof msg.payload == "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
var k = [""]; // array of data for each of the template items
var o = {}; // output object to build up
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 linecount = 0;
var tmp = "";
var has_parts = msg.hasOwnProperty("parts");
var reg = /^[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+$/i;
if (msg.hasOwnProperty("parts")) {
linecount = msg.parts.index;
if (msg.parts.index > node.skip) { first = false; }
if (msg.parts.hasOwnProperty("count") && (msg.parts.index+1 >= msg.parts.count)) { last = true; }
catch(e) { done(e); }
}
else if (typeof msg.payload == "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
var k = [""]; // array of data for each of the template items
var o = {}; // output object to build up
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 linecount = 0;
var tmp = "";
var has_parts = msg.hasOwnProperty("parts");
var reg = /^[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+$/i;
if (msg.hasOwnProperty("parts")) {
linecount = msg.parts.index;
if (msg.parts.index > node.skip) { first = false; }
if (msg.parts.hasOwnProperty("count") && (msg.parts.index+1 >= msg.parts.count)) { last = true; }
}
// For now we are just going to assume that any \r or \n means an end of line...
// got to be a weird csv that has singleton \r \n in it for another reason...
// Now process the whole file/line
var nocr = (line.match(/[\r\n]/g)||[]).length;
if (has_parts && node.multi === "mult" && nocr > 1) { tmp = ""; first = true; }
for (var i = 0; i < line.length; i++) {
if (first && (linecount < node.skip)) {
if (line[i] === "\n") { linecount += 1; }
continue;
}
// For now we are just going to assume that any \r or \n means an end of line...
// got to be a weird csv that has singleton \r \n in it for another reason...
// Now process the whole file/line
var nocr = (line.match(/[\r\n]/g)||[]).length;
if (has_parts && node.multi === "mult" && nocr > 1) { tmp = ""; first = true; }
for (var i = 0; i < line.length; i++) {
if (first && (linecount < node.skip)) {
if (line[i] === "\n") { linecount += 1; }
continue;
if ((node.hdrin === true) && first) { // if the template is in the first line
if ((line[i] === "\n")||(line[i] === "\r")||(line.length - i === 1)) { // look for first line break
if (line.length - i === 1) { tmp += line[i]; }
template = clean(tmp,node.sep);
first = false;
}
if ((node.hdrin === true) && first) { // if the template is in the first line
if ((line[i] === "\n")||(line[i] === "\r")||(line.length - i === 1)) { // look for first line break
if (line.length - i === 1) { tmp += line[i]; }
template = clean(tmp,node.sep);
first = false;
}
else { tmp += line[i]; }
else { tmp += line[i]; }
}
else {
if (line[i] === node.quo) { // if it's a quote toggle inside or outside
f = !f;
if (line[i-1] === node.quo) {
if (f === false) { k[j] += '\"'; }
} // if it's a quotequote then it's actually a quote
//if ((line[i-1] !== node.sep) && (line[i+1] !== node.sep)) { k[j] += line[i]; }
}
else {
if (line[i] === node.quo) { // if it's a quote toggle inside or outside
f = !f;
if (line[i-1] === node.quo) {
if (f === false) { k[j] += '\"'; }
} // if it's a quotequote then it's actually a quote
//if ((line[i-1] !== node.sep) && (line[i+1] !== node.sep)) { k[j] += line[i]; }
else if ((line[i] === node.sep) && f) { // if it is the end of the line then finish
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if ( template[j] && (template[j] !== "") ) {
// if no value between separators ('1,,"3"...') or if the line beings with separator (',1,"2"...') treat value as null
if (line[i-1] === node.sep || line[i-1].includes('\n','\r')) k[j] = null;
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
}
else if ((line[i] === node.sep) && f) { // if it is the end of the line then finish
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if ( template[j] && (template[j] !== "") ) {
// if no value between separators ('1,,"3"...') or if the line beings with separator (',1,"2"...') treat value as null
if (line[i-1] === node.sep || line[i-1].includes('\n','\r')) k[j] = null;
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
}
j += 1;
// if separator is last char in processing string line (without end of line), add null value at the end - example: '1,2,3\n3,"3",'
k[j] = line.length - 1 === i ? null : "";
j += 1;
// if separator is last char in processing string line (without end of line), add null value at the end - example: '1,2,3\n3,"3",'
k[j] = line.length - 1 === i ? null : "";
}
else if (((line[i] === "\n") || (line[i] === "\r")) && f) { // handle multiple lines
//console.log(j,k,o,k[j]);
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if ( template[j] && (template[j] !== "") ) {
// if separator before end of line, set null value ie. '1,2,"3"\n1,2,\n1,2,3'
if (line[i-1] === node.sep) k[j] = null;
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
}
else if (((line[i] === "\n") || (line[i] === "\r")) && f) { // handle multiple lines
//console.log(j,k,o,k[j]);
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if ( template[j] && (template[j] !== "") ) {
// if separator before end of line, set null value ie. '1,2,"3"\n1,2,\n1,2,3'
if (line[i-1] === node.sep) k[j] = null;
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
}
if (JSON.stringify(o) !== "{}") { // don't send empty objects
a.push(o); // add to the array
}
j = 0;
k = [""];
o = {};
f = true; // reset in/out flag ready for next line.
}
else { // just add to the part of the message
k[j] += line[i];
if (JSON.stringify(o) !== "{}") { // don't send empty objects
a.push(o); // add to the array
}
j = 0;
k = [""];
o = {};
f = true; // reset in/out flag ready for next line.
}
else { // just add to the part of the message
k[j] += line[i];
}
}
// Finished so finalize and send anything left
if (f === false) { node.warn(RED._("csv.errors.bad_csv")); }
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
}
// Finished so finalize and send anything left
if (f === false) { node.warn(RED._("csv.errors.bad_csv")); }
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if ( template[j] && (template[j] !== "") ) {
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
}
if ( template[j] && (template[j] !== "") ) {
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
}
if (JSON.stringify(o) !== "{}") { // don't send empty objects
a.push(o); // add to the array
}
if (JSON.stringify(o) !== "{}") { // don't send empty objects
a.push(o); // add to the array
}
if (node.multi !== "one") {
msg.payload = 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;
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
delete msg.parts;
send(msg);
node.store = [];
}
if (node.multi !== "one") {
msg.payload = a;
if (has_parts && nocr <= 1) {
if (JSON.stringify(o) !== "{}") {
node.store.push(o);
}
else {
if (msg.parts.index + 1 === msg.parts.count) {
msg.payload = node.store;
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
send(msg); // finally send the array
delete msg.parts;
send(msg);
node.store = [];
}
}
else {
var len = a.length;
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];
if (!has_parts) {
newMessage.parts = {
id: msg._msgid,
index: i,
count: len
};
}
else {
newMessage.parts.index -= node.skip;
newMessage.parts.count -= node.skip;
if (node.hdrin) { // if we removed the header line then shift the counts by 1
newMessage.parts.index -= 1;
newMessage.parts.count -= 1;
}
}
if (last) { newMessage.complete = true; }
send(newMessage);
}
if (has_parts && last && len === 0) {
send({complete:true});
}
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
send(msg); // finally send the array
}
node.linecount = 0;
done();
}
catch(e) { done(e); }
}
else { node.warn(RED._("csv.errors.csv_js")); done(); }
}
else {
if (!msg.hasOwnProperty("reset")) {
node.send(msg); // If no payload and not reset - just pass it on.
}
done();
}
});
}
if(RFC4180Mode) {
node.template = (n.temp || "")
node.sep = (n.sep || ',').replace(/\\t/g, "\t").replace(/\\n/g, "\n").replace(/\\r/g, "\r")
node.quo = '"'
// default to CRLF (RFC4180 Sec 2.1: "Each record is located on a separate line, delimited by a line break (CRLF)")
node.ret = (n.ret || "\r\n").replace(/\\n/g, "\n").replace(/\\r/g, "\r")
node.multi = n.multi || "one"
node.hdrin = n.hdrin || false
node.hdrout = n.hdrout || "none"
node.goodtmpl = true
node.skip = parseInt(n.skip || 0)
node.store = []
node.parsestrings = n.strings
node.include_empty_strings = n.include_empty_strings || false
node.include_null_values = n.include_null_values || false
if (node.parsestrings === undefined) { node.parsestrings = true }
if (node.hdrout === false) { node.hdrout = "none" }
if (node.hdrout === true) { node.hdrout = "all" }
const dontSendHeaders = node.hdrout === "none"
const sendHeadersOnce = node.hdrout === "once"
const sendHeadersAlways = node.hdrout === "all"
const sendHeaders = !dontSendHeaders && (sendHeadersOnce || sendHeadersAlways)
const quoteables = [node.sep, node.quo, "\n", "\r"]
const templateQuoteables = [',', '"', "\n", "\r"]
let badTemplateWarnOnce = true
const columnStringToTemplateArray = function (col, sep) {
// NOTE: enforce strict column template parsing in RFC4180 mode
const parsed = csv.parse(col, { separator: sep, quote: node.quo, outputStyle: 'array', strict: true })
if (parsed.headers.length > 0) { node.goodtmpl = true } else { node.goodtmpl = false }
return parsed.headers.length ? parsed.headers : null
}
const templateArrayToColumnString = function (template, keepEmptyColumns) {
// NOTE: enforce strict column template parsing in RFC4180 mode
const parsed = csv.parse('', {headers: template, headersOnly:true, separator: ',', quote: node.quo, outputStyle: 'array', strict: true })
return keepEmptyColumns
? parsed.headers.map(e => addQuotes(e || '', { separator: ',', quoteables: templateQuoteables}))
: parsed.header // exclues empty columns
// TODO: resolve inconsistency between CSV->JSON and JSON->CSV
// CSV->JSON: empty columns are excluded
// JSON->CSV: empty columns are kept in some cases
}
function addQuotes(cell, options) {
options = options || {}
return csv.quoteCell(cell, {
quote: options.quote || node.quo || '"',
separator: options.separator || node.sep || ',',
quoteables: options.quoteables || quoteables
})
}
const hasTemplate = (t) => t?.length > 0 && !(t.length === 1 && t[0] === '')
let template
try {
template = columnStringToTemplateArray(node.template, ',') || ['']
} catch (e) {
node.warn(RED._("csv.errors.bad_template")) // is warning really necessary now we have status?
node.status({ fill: "red", shape: "dot", text: RED._("csv.errors.bad_template") })
return // dont hook up the node
}
const noTemplate = hasTemplate(template) === false
node.hdrSent = false
node.on("input", function (msg, send, done) {
node.status({}) // clear status
if (msg.hasOwnProperty("reset")) {
node.hdrSent = false
}
if (msg.hasOwnProperty("payload")) {
let inputData = msg.payload
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?
// then, if necessary, convert to an array of objects/arrays
let isObject = !Array.isArray(inputData) && typeof inputData === 'object'
let isArrayOfObjects = Array.isArray(inputData) && inputData.length > 0 && typeof inputData[0] === 'object'
let isArrayOfArrays = Array.isArray(inputData) && inputData.length > 0 && Array.isArray(inputData[0])
let isArrayOfPrimitives = Array.isArray(inputData) && inputData.length > 0 && typeof inputData[0] !== 'object'
if (isObject) {
inputData = [inputData]
isArrayOfObjects = true
isObject = false
} else if (isArrayOfPrimitives) {
inputData = [inputData]
isArrayOfArrays = true
isArrayOfPrimitives = false
}
const stringBuilder = []
if (!(noTemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
template = columnStringToTemplateArray(node.template) || ['']
}
// build header line
if (sendHeaders && node.hdrSent === false) {
if (hasTemplate(template) === false) {
if (msg.hasOwnProperty("columns")) {
template = columnStringToTemplateArray(msg.columns || "", ",") || ['']
}
else {
template = Object.keys(inputData[0]) || ['']
}
}
stringBuilder.push(templateArrayToColumnString(template, true))
if (sendHeadersOnce) { node.hdrSent = true }
}
// build csv lines
for (let s = 0; s < inputData.length; s++) {
let row = inputData[s]
if (isArrayOfArrays) {
/*** row is an array of arrays ***/
const _hasTemplate = hasTemplate(template)
const len = _hasTemplate ? template.length : row.length
const result = []
for (let t = 0; t < len; t++) {
let cell = row[t]
if (cell === undefined) { cell = "" }
if(_hasTemplate) {
const header = template[t]
if (header) {
result[t] = addQuotes(RED.util.ensureString(cell))
}
} else {
result[t] = addQuotes(RED.util.ensureString(cell))
}
}
stringBuilder.push(result.join(node.sep))
} else {
/*** row is an object ***/
if (hasTemplate(template) === false && (msg.hasOwnProperty("columns"))) {
template = columnStringToTemplateArray(msg.columns || "", ",")
}
if (hasTemplate(template) === false) {
/*** row is an object but we still don't have a template ***/
if (badTemplateWarnOnce === true) {
node.warn(RED._("csv.errors.obj_csv"))
badTemplateWarnOnce = false
}
const rowData = []
for (let header in inputData[0]) {
if (row.hasOwnProperty(header)) {
const cell = row[header]
if (typeof cell !== "object") {
let cellValue = ""
if (cell !== undefined) {
cellValue += cell
}
rowData.push(addQuotes(cellValue))
}
}
}
stringBuilder.push(rowData.join(node.sep))
} else {
/*** row is an object and we have a template ***/
const rowData = []
for (let t = 0; t < template.length; t++) {
if (!template[t]) {
rowData.push('')
}
else {
let cellValue = inputData[s][template[t]]
if (cellValue === undefined) { cellValue = "" }
cellValue = RED.util.ensureString(cellValue)
rowData.push(addQuotes(cellValue))
}
}
stringBuilder.push(rowData.join(node.sep)); // add separator
}
}
}
// join lines, don't forget to add the last new line
msg.payload = stringBuilder.join(node.ret) + node.ret
msg.columns = templateArrayToColumnString(template)
if (msg.payload !== '') { send(msg) }
done()
}
catch (e) {
done(e)
}
}
else if (typeof inputData == "string") { // convert CSV string to object
try {
let firstLine = true; // is this the first line
let last = false
let linecount = 0
const has_parts = msg.hasOwnProperty("parts")
// determine if this is a multi part message and if so what part we are processing
if (msg.hasOwnProperty("parts")) {
linecount = msg.parts.index
if (msg.parts.index > node.skip) { firstLine = false }
if (msg.parts.hasOwnProperty("count") && (msg.parts.index + 1 >= msg.parts.count)) { last = true }
}
// If skip is set, compute the cursor position to start parsing from
let _cursor = 0
if (node.skip > 0 && linecount < node.skip) {
for (; _cursor < inputData.length; _cursor++) {
if (firstLine && (linecount < node.skip)) {
if (inputData[_cursor] === "\r" || inputData[_cursor] === "\n") {
linecount += 1
}
continue
}
break
}
if (_cursor >= inputData.length) {
return // skip this line
}
}
// count the number of line breaks in the string
const noofCR = ((_cursor ? inputData.slice(_cursor) : inputData).match(/[\r\n]/g) || []).length
// if we have `parts` and we are outputting multiple objects and we have more than one line
// then we need to set firstLine to true so that we process the header line
if (has_parts && node.multi === "mult" && noofCR > 1) {
firstLine = true
}
// if we are processing the first line and the node has been set to extract the header line
// update the template with the header line
if (firstLine && node.hdrin === true) {
/** @type {import('./lib/csv/index.js').CSVParseOptions} */
const csvOptionsForHeaderRow = {
cursor: _cursor,
separator: node.sep,
quote: node.quo,
dataHasHeaderRow: true,
headersOnly: true,
outputStyle: 'array',
strict: true // enforce strict parsing of the header row
}
try {
const csvHeader = csv.parse(inputData, csvOptionsForHeaderRow)
template = csvHeader.headers
_cursor = csvHeader.cursor
} catch (e) {
// node.warn(RED._("csv.errors.bad_template")) // add warning?
node.status({ fill: "red", shape: "dot", text: RED._("csv.errors.bad_template") })
throw e
}
}
// now we process the data lines
/** @type {import('./lib/csv/index.js').CSVParseOptions} */
const csvOptions = {
cursor: _cursor,
separator: node.sep,
quote: node.quo,
dataHasHeaderRow: false,
headers: hasTemplate(template) ? template : null,
outputStyle: 'object',
includeNullValues: node.include_null_values,
includeEmptyStrings: node.include_empty_strings,
parseNumeric: node.parsestrings,
strict: false // relax the strictness of the parser for data rows
}
const csvParseResult = csv.parse(inputData, csvOptions)
const data = csvParseResult.data
// output results
if (node.multi !== "one") {
if (has_parts && noofCR <= 1) {
if (data.length > 0) {
node.store.push(...data)
}
if (msg.parts.index + 1 === msg.parts.count) {
msg.payload = node.store
msg.columns = csvParseResult.header
// msg._mode = 'RFC4180 mode'
delete msg.parts
send(msg)
node.store = []
}
else {
var len = a.length;
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];
if (!has_parts) {
newMessage.parts = {
id: msg._msgid,
index: i,
count: len
};
}
else {
msg.columns = csvParseResult.header
// msg._mode = 'RFC4180 mode'
msg.payload = data
send(msg); // finally send the array
}
}
else {
const len = data.length
for (let row = 0; row < len; row++) {
const newMessage = RED.util.cloneMessage(msg)
newMessage.columns = csvParseResult.header
newMessage.payload = data[row]
if (!has_parts) {
newMessage.parts = {
id: msg._msgid,
index: row,
count: len
}
newMessage.parts.index -= node.skip;
newMessage.parts.count -= node.skip;
if (node.hdrin) { // if we removed the header line then shift the counts by 1
newMessage.parts.index -= 1;
newMessage.parts.count -= 1;
}
else {
newMessage.parts.index -= node.skip
newMessage.parts.count -= node.skip
if (node.hdrin) { // if we removed the header line then shift the counts by 1
newMessage.parts.index -= 1
newMessage.parts.count -= 1
}
}
if (last) { newMessage.complete = true }
// newMessage._mode = 'RFC4180 mode'
send(newMessage)
}
if (has_parts && last && len === 0) {
// send({complete:true, _mode: 'RFC4180 mode'})
send({ complete: true })
}
if (last) { newMessage.complete = true; }
send(newMessage);
}
if (has_parts && last && len === 0) {
send({complete:true});
}
node.linecount = 0
done()
}
catch (e) {
done(e)
}
node.linecount = 0;
done();
}
else {
// RFC-vs-legacy mode difference: In RFC mode, we throw catchable errors and provide a status message
const err = new Error(RED._("csv.errors.csv_js"))
node.status({ fill: "red", shape: "dot", text: err.message })
done(err)
}
catch(e) { done(e); }
}
else {
if (!msg.hasOwnProperty("reset")) {
node.send(msg); // If no payload and not reset - just pass it on.
}
done()
else { node.warn(RED._("csv.errors.csv_js")); done(); }
}
else {
if (!msg.hasOwnProperty("reset")) {
node.send(msg); // If no payload and not reset - just pass it on.
}
})
}
done();
}
});
}
RED.nodes.registerType("csv",CSVNode)
RED.nodes.registerType("csv",CSVNode);
}

View File

@@ -14,7 +14,6 @@
<option value="html" data-i18n="html.output.html"></option>
<option value="text" data-i18n="html.output.text"></option>
<option value="attr" data-i18n="html.output.attr"></option>
<option value="compl" data-i18n="html.output.compl"></option>
<!-- <option value="val">return the value from a form element</option> -->
</select>
</div>
@@ -29,10 +28,6 @@
<label for="node-input-outproperty">&nbsp;</label>
<span data-i18n="html.label.in" style="padding-left:8px; padding-right:2px; vertical-align:-1px;"></span> <input type="text" id="node-input-outproperty" style="width:64%">
</div>
<div id='html-prefix-row' class="form-row" style="display: none;">
<label for="node-input-chr" style="width: 230px;"><i class="fa fa-tag"></i> <span data-i18n="html.label.prefix"></span></label>
<input type="text" id="node-input-chr" style="text-align:center; width: 40px;" placeholder="_">
</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>
@@ -50,8 +45,7 @@
outproperty: {value:"payload", validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true }) },
tag: {value:""},
ret: {value:"html"},
as: {value:"single"},
chr: { value: "_" }
as: {value:"single"}
},
inputs:1,
outputs:1,
@@ -65,13 +59,6 @@
oneditprepare: function() {
$("#node-input-property").typedInput({default:'msg',types:['msg']});
$("#node-input-outproperty").typedInput({default:'msg',types:['msg']});
$('#node-input-ret').on( 'change', () => {
if ( $('#node-input-ret').val() == "compl" ) {
$('#html-prefix-row').show()
} else {
$('#html-prefix-row').hide()
}
});
}
});
</script>

View File

@@ -25,7 +25,6 @@ module.exports = function(RED) {
this.tag = n.tag;
this.ret = n.ret || "html";
this.as = n.as || "single";
this.chr = n.chr || "_";
var node = this;
this.on("input", function(msg,send,done) {
var value = RED.util.getMessageProperty(msg,node.property);
@@ -48,11 +47,6 @@ module.exports = function(RED) {
if (node.ret === "attr") {
pay2 = Object.assign({},this.attribs);
}
if (node.ret === "compl") {
var bse = {}
bse[node.chr] = $(this).html().trim()
pay2 = Object.assign(bse, this.attribs);
}
//if (node.ret === "val") { pay2 = $(this).val(); }
/* istanbul ignore else */
if (pay2) {
@@ -75,11 +69,6 @@ module.exports = function(RED) {
var attribs = Object.assign({},this.attribs);
pay.push( attribs );
}
if (node.ret === "compl") {
var bse = {}
bse[node.chr] = $(this).html().trim()
pay.push( Object.assign(bse, this.attribs) )
}
//if (node.ret === "val") { pay.push( $(this).val() ); }
}
index++;

View File

@@ -1,324 +0,0 @@
/**
* @typedef {Object} CSVParseOptions
* @property {number} [cursor=0] - an index into the CSV to start parsing from
* @property {string} [separator=','] - the separator character
* @property {string} [quote='"'] - the quote character
* @property {boolean} [headersOnly=false] - only parse the headers and return them
* @property {string[]} [headers=[]] - an array of headers to use instead of the first row of the CSV data
* @property {boolean} [dataHasHeaderRow=true] - whether the CSV data to parse has a header row
* @property {boolean} [outputHeader=true] - whether the output data should include a header row (only applies to array output)
* @property {boolean} [parseNumeric=false] - parse numeric values into numbers
* @property {boolean} [includeNullValues=false] - include null values in the output
* @property {boolean} [includeEmptyStrings=true] - include empty strings in the output
* @property {string} [outputStyle='object'] - output an array of arrays or an array of objects
* @property {boolean} [strict=false] - throw an error if the CSV is malformed
*/
/**
* Parses a CSV string into an array of arrays or an array of objects.
*
* NOTES:
* * Deviations from the RFC4180 spec (for the sake of user fiendliness, system implementations and flexibility), this parser will:
* * accept any separator character, not just `,`
* * accept any quote character, not just `"`
* * parse `\r`, `\n` or `\r\n` as line endings (RRFC4180 2.1 states lines are separated by CRLF)
* * Only single character `quote` is supported
* * `quote` is `"` by default
* * Any cell that contains a `quote` or `separator` will be quoted
* * Any `quote` characters inside a cell will be escaped as per RFC 4180 2.6
* * Only single character `separator` is supported
* * Only `array` and `object` output styles are supported
* * `array` output style is an array of arrays [[],[],[]]
* * `object` output style is an array of objects [{},{},{}]
* * Only `headers` or `dataHasHeaderRow` are supported, not both
* @param {string} csvIn - the CSV string to parse
* @param {CSVParseOptions} parseOptions - options
* @throws {Error}
*/
function parse(csvIn, parseOptions) {
/* Normalise options */
parseOptions = parseOptions || {};
const separator = parseOptions.separator ?? ',';
const quote = parseOptions.quote ?? '"';
const headersOnly = parseOptions.headersOnly ?? false;
const headers = Array.isArray(parseOptions.headers) ? parseOptions.headers : []
const dataHasHeaderRow = parseOptions.dataHasHeaderRow ?? true;
const outputHeader = parseOptions.outputHeader ?? true;
const parseNumeric = parseOptions.parseNumeric ?? false;
const includeNullValues = parseOptions.includeNullValues ?? false;
const includeEmptyStrings = parseOptions.includeEmptyStrings ?? true;
const outputStyle = ['array', 'object'].includes(parseOptions.outputStyle) ? parseOptions.outputStyle : 'object'; // 'array [[],[],[]]' or 'object [{},{},{}]
const strict = parseOptions.strict ?? false
/* Local variables */
const cursorMax = csvIn.length;
const ouputArrays = outputStyle === 'array';
const headersSupplied = headers.length > 0
// The original regex was an "is-a-number" positive logic test. /^ *[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+ *$/i;
// Below, is less strict and inverted logic but coupled with +cast it is 13%+ faster than original regex+parsefloat
// and has the benefit of understanding hexadecimals, binary and octal numbers.
const skipNumberConversion = /^ *(\+|-0\d|0\d)/
const cellBuilder = []
let rowBuilder = []
let cursor = typeof parseOptions.cursor === 'number' ? parseOptions.cursor : 0;
let newCell = true, inQuote = false, closed = false, output = [];
/* inline helper functions */
const finaliseCell = () => {
let cell = cellBuilder.join('')
cellBuilder.length = 0
// push the cell:
// NOTE: if cell is empty but newCell==true, then this cell had zero chars - push `null`
// otherwise push empty string
return rowBuilder.push(cell || (newCell ? null : ''))
}
const finaliseRow = () => {
if (cellBuilder.length) {
finaliseCell()
}
if (rowBuilder.length) {
output.push(rowBuilder)
rowBuilder = []
}
}
/* Main parsing loop */
while (cursor < cursorMax) {
const char = csvIn[cursor]
if (inQuote) {
if (char === quote && csvIn[cursor + 1] === quote) {
cellBuilder.push(quote)
cursor += 2;
newCell = false;
closed = false;
} else if (char === quote) {
inQuote = false;
cursor += 1;
newCell = false;
closed = true;
} else {
cellBuilder.push(char)
newCell = false;
closed = false;
cursor++;
}
} else {
if (char === separator) {
finaliseCell()
cursor += 1;
newCell = true;
closed = false;
} else if (char === quote) {
if (newCell) {
inQuote = true;
cursor += 1;
newCell = false;
closed = false;
}
else if (strict) {
throw new UnquotedQuoteError(cursor)
} else {
// not strict, keep 1 quote if the next char is not a cell/record separator
cursor++
if (csvIn[cursor] && csvIn[cursor] !== '\n' && csvIn[cursor] !== '\r' && csvIn[cursor] !== separator) {
cellBuilder.push(char)
if (csvIn[cursor] === quote) {
cursor++ // skip the next quote
}
}
}
} else {
if (char === '\n' || char === '\r') {
finaliseRow()
if (csvIn[cursor + 1] === '\n') {
cursor += 2;
} else {
cursor++
}
newCell = true;
closed = false;
if (headersOnly) {
break
}
} else {
if (closed) {
if (strict) {
throw new DataAfterCloseError(cursor)
} else {
cursor--; // move back to grab the previously discarded char
closed = false
}
} else {
cellBuilder.push(char)
newCell = false;
cursor++;
}
}
}
}
}
if (strict && inQuote) {
throw new ParseError(`Missing quote, unclosed cell`, cursor)
}
// finalise the last cell/row
finaliseRow()
let firstRowIsHeader = false
// if no headers supplied, generate them
if (output.length >= 1) {
if (headersSupplied) {
// headers already supplied
} else if (dataHasHeaderRow) {
// take the first row as the headers
headers.push(...output[0])
firstRowIsHeader = true
} else {
// generate headers col1, col2, col3, etc
for (let i = 0; i < output[0].length; i++) {
headers.push("col" + (i + 1))
}
}
}
const finalResult = {
/** @type {String[]} headers as an array of string */
headers: headers,
/** @type {String} headers as a comma-separated string */
header: null,
/** @type {Any[]} Result Data (may include header row: check `firstRowIsHeader` flag) */
data: [],
/** @type {Boolean|undefined} flag to indicate if the first row is a header row (only applies when `outputStyle` is 'array') */
firstRowIsHeader: undefined,
/** @type {'array'|'object'} flag to indicate the output style */
outputStyle: outputStyle,
/** @type {Number} The current cursor position */
cursor: cursor,
}
const quotedHeaders = []
for (let i = 0; i < headers.length; i++) {
if (!headers[i]) {
continue
}
quotedHeaders.push(quoteCell(headers[i], { quote, separator: ',' }))
}
finalResult.header = quotedHeaders.join(',') // always quote headers and join with comma
// output is an array of arrays [[],[],[]]
if (ouputArrays || headersOnly) {
if (!firstRowIsHeader && !headersOnly && outputHeader && headers.length > 0) {
if (output.length > 0) {
output.unshift(headers)
} else {
output = [headers]
}
firstRowIsHeader = true
}
if (headersOnly) {
delete finalResult.firstRowIsHeader
return finalResult
}
finalResult.firstRowIsHeader = firstRowIsHeader
finalResult.data = (firstRowIsHeader && !outputHeader) ? output.slice(1) : output
return finalResult
}
// output is an array of objects [{},{},{}]
const outputObjects = []
let i = firstRowIsHeader ? 1 : 0
for (; i < output.length; i++) {
const rowObject = {}
let isEmpty = true
for (let j = 0; j < headers.length; j++) {
if (!headers[j]) {
continue
}
let v = output[i][j] === undefined ? null : output[i][j]
if (v === null && !includeNullValues) {
continue
} else if (v === "" && !includeEmptyStrings) {
continue
} else if (parseNumeric === true && v && !skipNumberConversion.test(v)) {
const vTemp = +v
const isNumber = !isNaN(vTemp)
if(isNumber) {
v = vTemp
}
}
rowObject[headers[j]] = v
isEmpty = false
}
// determine if this row is empty
if (!isEmpty) {
outputObjects.push(rowObject)
}
}
finalResult.data = outputObjects
delete finalResult.firstRowIsHeader
return finalResult
}
/**
* Quotes a cell in a CSV string if necessary. Addiionally, any double quotes inside the cell will be escaped as per RFC 4180 2.6 (https://datatracker.ietf.org/doc/html/rfc4180#section-2).
* @param {string} cell - the string to quote
* @param {*} options - options
* @param {string} [options.quote='"'] - the quote character
* @param {string} [options.separator=','] - the separator character
* @param {string[]} [options.quoteables] - an array of characters that, when encountered, will trigger the application of outer quotes
* @returns
*/
function quoteCell(cell, { quote = '"', separator = ",", quoteables } = {
quote: '"',
separator: ",",
quoteables: [quote, separator, '\r', '\n']
}) {
quoteables = quoteables || [quote, separator, '\r', '\n'];
let doubleUp = false;
if (cell.indexOf(quote) !== -1) { // add double quotes if any quotes
doubleUp = true;
}
const quoteChar = quoteables.some(q => cell.includes(q)) ? quote : '';
return quoteChar + (doubleUp ? cell.replace(/"/g, '""') : cell) + quoteChar;
}
// #region Custom Error Classes
class ParseError extends Error {
/**
* @param {string} message - the error message
* @param {number} cursor - the cursor index where the error occurred
*/
constructor(message, cursor) {
super(message)
this.name = 'ParseError'
this.cursor = cursor
}
}
class UnquotedQuoteError extends ParseError {
/**
* @param {number} cursor - the cursor index where the error occurred
*/
constructor(cursor) {
super('Quote found in the middle of an unquoted field', cursor)
this.name = 'UnquotedQuoteError'
}
}
class DataAfterCloseError extends ParseError {
/**
* @param {number} cursor - the cursor index where the error occurred
*/
constructor(cursor) {
super('Data found after closing quote', cursor)
this.name = 'DataAfterCloseError'
}
}
// #endregion
exports.parse = parse
exports.quoteCell = quoteCell
exports.ParseError = ParseError
exports.UnquotedQuoteError = UnquotedQuoteError
exports.DataAfterCloseError = DataAfterCloseError

View File

@@ -15,11 +15,7 @@
-->
<script type="text/html" data-template-name="split">
<!-- <div class="form-row"><span data-i18n="[html]split.intro"></span></div> -->
<div class="form-row">
<label for="node-input-property"><i class="fa fa-forward"></i> <span data-i18n="split.split"></span></label>
<input type="text" id="node-input-property" style="width:70%;"/>
</div>
<div class="form-row"><span data-i18n="[html]split.intro"></span></div>
<div class="form-row"><span data-i18n="[html]split.strBuff"></span></div>
<div class="form-row">
<label for="node-input-splt" style="padding-left:10px; margin-right:-10px;" data-i18n="split.splitUsing"></label>
@@ -43,9 +39,10 @@
<label for="node-input-addname-cb" style="width:auto;" data-i18n="split.addname"></label>
<input type="text" id="node-input-addname" style="width:70%">
</div>
<hr/>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
<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>
@@ -60,8 +57,7 @@
arraySplt: {value:1},
arraySpltType: {value:"len"},
stream: {value:false},
addname: {value:"", validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })},
property: {value:"payload",required:true}
addname: {value:"", validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })}
},
inputs:1,
outputs:1,
@@ -73,10 +69,6 @@
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
if (this.property === undefined) {
$("#node-input-property").val("payload");
}
$("#node-input-property").typedInput({default:'msg',types:['msg']});
$("#node-input-splt").typedInput({
default: 'str',
typeField: $("#node-input-spltType"),

View File

@@ -19,13 +19,13 @@ module.exports = function(RED) {
function sendArray(node,msg,array,send) {
for (var i = 0; i < array.length-1; i++) {
RED.util.setMessageProperty(msg,node.property,array[i]);
msg.payload = array[i];
msg.parts.index = node.c++;
if (node.stream !== true) { msg.parts.count = array.length; }
send(RED.util.cloneMessage(msg));
}
if (node.stream !== true) {
RED.util.setMessageProperty(msg,node.property,array[i]);
msg.payload = array[i];
msg.parts.index = node.c++;
msg.parts.count = array.length;
send(RED.util.cloneMessage(msg));
@@ -40,12 +40,10 @@ module.exports = function(RED) {
node.stream = n.stream;
node.spltType = n.spltType || "str";
node.addname = n.addname || "";
node.property = n.property||"payload";
try {
if (node.spltType === "str") {
this.splt = (n.splt || "\\n").replace(/\\n/g,"\n").replace(/\\r/g,"\r").replace(/\\t/g,"\t").replace(/\\e/g,"\e").replace(/\\f/g,"\f").replace(/\\0/g,"\0");
}
else if (node.spltType === "bin") {
} else if (node.spltType === "bin") {
var spltArray = JSON.parse(n.splt);
if (Array.isArray(spltArray)) {
this.splt = Buffer.from(spltArray);
@@ -53,8 +51,7 @@ module.exports = function(RED) {
throw new Error("not an array");
}
this.spltBuffer = spltArray;
}
else if (node.spltType === "len") {
} else if (node.spltType === "len") {
this.splt = parseInt(n.splt);
if (isNaN(this.splt) || this.splt < 1) {
throw new Error("invalid split length: "+n.splt);
@@ -72,22 +69,18 @@ module.exports = function(RED) {
node.buffer = Buffer.from([]);
node.pendingDones = [];
this.on("input", function(msg, send, done) {
var value = RED.util.getMessageProperty(msg,node.property);
if (value !== undefined) {
if (msg.hasOwnProperty("payload")) {
if (msg.hasOwnProperty("parts")) { msg.parts = { parts:msg.parts }; } // push existing parts to a stack
else { msg.parts = {}; }
msg.parts.id = RED.util.generateId(); // generate a random id
if (node.property !== "payload") {
msg.parts.property = node.property;
}
delete msg._msgid;
if (typeof value === "string") { // Split String into array
value = (node.remainder || "") + value;
if (typeof msg.payload === "string") { // Split String into array
msg.payload = (node.remainder || "") + msg.payload;
msg.parts.type = "string";
if (node.spltType === "len") {
msg.parts.ch = "";
msg.parts.len = node.splt;
var count = value.length/node.splt;
var count = msg.payload.length/node.splt;
if (Math.floor(count) !== count) {
count = Math.ceil(count);
}
@@ -96,9 +89,9 @@ module.exports = function(RED) {
node.c = 0;
}
var pos = 0;
var data = value;
var data = msg.payload;
for (var i=0; i<count-1; i++) {
RED.util.setMessageProperty(msg,node.property,data.substring(pos,pos+node.splt));
msg.payload = data.substring(pos,pos+node.splt);
msg.parts.index = node.c++;
pos += node.splt;
send(RED.util.cloneMessage(msg));
@@ -109,7 +102,7 @@ module.exports = function(RED) {
}
node.remainder = data.substring(pos);
if ((node.stream !== true) || (node.remainder.length === node.splt)) {
RED.util.setMessageProperty(msg,node.property,node.remainder);
msg.payload = node.remainder;
msg.parts.index = node.c++;
send(RED.util.cloneMessage(msg));
node.pendingDones.forEach(d => d());
@@ -126,48 +119,47 @@ module.exports = function(RED) {
if (!node.spltBufferString) {
node.spltBufferString = node.splt.toString();
}
a = value.split(node.spltBufferString);
a = msg.payload.split(node.spltBufferString);
msg.parts.ch = node.spltBuffer; // pass the split char to other end for rejoin
} else if (node.spltType === "str") {
a = value.split(node.splt);
a = msg.payload.split(node.splt);
msg.parts.ch = node.splt; // pass the split char to other end for rejoin
}
sendArray(node,msg,a,send);
done();
}
}
else if (Array.isArray(value)) { // then split array into messages
else if (Array.isArray(msg.payload)) { // then split array into messages
msg.parts.type = "array";
var count = value.length/node.arraySplt;
var count = msg.payload.length/node.arraySplt;
if (Math.floor(count) !== count) {
count = Math.ceil(count);
}
msg.parts.count = count;
var pos = 0;
var data = value;
var data = msg.payload;
msg.parts.len = node.arraySplt;
for (var i=0; i<count; i++) {
var m = data.slice(pos,pos+node.arraySplt);
msg.payload = data.slice(pos,pos+node.arraySplt);
if (node.arraySplt === 1) {
m = m[0];
msg.payload = msg.payload[0];
}
RED.util.setMessageProperty(msg,node.property,m);
msg.parts.index = i;
pos += node.arraySplt;
send(RED.util.cloneMessage(msg));
}
done();
}
else if ((typeof value === "object") && !Buffer.isBuffer(value)) {
else if ((typeof msg.payload === "object") && !Buffer.isBuffer(msg.payload)) {
var j = 0;
var l = Object.keys(value).length;
var pay = value;
var l = Object.keys(msg.payload).length;
var pay = msg.payload;
msg.parts.type = "object";
for (var p in pay) {
if (pay.hasOwnProperty(p)) {
RED.util.setMessageProperty(msg,node.property,pay[p]);
msg.payload = pay[p];
if (node.addname !== "") {
RED.util.setMessageProperty(msg,node.addname,p);
msg[node.addname] = p;
}
msg.parts.key = p;
msg.parts.index = j;
@@ -178,9 +170,9 @@ module.exports = function(RED) {
}
done();
}
else if (Buffer.isBuffer(value)) {
var len = node.buffer.length + value.length;
var buff = Buffer.concat([node.buffer, value], len);
else if (Buffer.isBuffer(msg.payload)) {
var len = node.buffer.length + msg.payload.length;
var buff = Buffer.concat([node.buffer, msg.payload], len);
msg.parts.type = "buffer";
if (node.spltType === "len") {
var count = buff.length/node.splt;
@@ -194,7 +186,7 @@ module.exports = function(RED) {
var pos = 0;
msg.parts.len = node.splt;
for (var i=0; i<count-1; i++) {
RED.util.setMessageProperty(msg,node.property,buff.slice(pos,pos+node.splt));
msg.payload = buff.slice(pos,pos+node.splt);
msg.parts.index = node.c++;
pos += node.splt;
send(RED.util.cloneMessage(msg));
@@ -205,7 +197,7 @@ module.exports = function(RED) {
}
node.buffer = buff.slice(pos);
if ((node.stream !== true) || (node.buffer.length === node.splt)) {
RED.util.setMessageProperty(msg,node.property,node.buffer);
msg.payload = node.buffer;
msg.parts.index = node.c++;
send(RED.util.cloneMessage(msg));
node.pendingDones.forEach(d => d());
@@ -238,7 +230,7 @@ module.exports = function(RED) {
var i = 0, p = 0;
pos = buff.indexOf(node.splt);
while (pos > -1) {
RED.util.setMessageProperty(msg,node.property,buff.slice(p,pos));
msg.payload = buff.slice(p,pos);
msg.parts.index = node.c++;
send(RED.util.cloneMessage(msg));
i++;
@@ -250,7 +242,7 @@ module.exports = function(RED) {
node.pendingDones = [];
}
if ((node.stream !== true) && (p < buff.length)) {
RED.util.setMessageProperty(msg,node.property,buff.slice(p,buff.length));
msg.payload = buff.slice(p,buff.length);
msg.parts.index = node.c++;
msg.parts.count = node.c++;
send(RED.util.cloneMessage(msg));
@@ -306,6 +298,7 @@ module.exports = function(RED) {
return exp
}
function reduceMessageGroup(node,msgInfos,exp,fixup,count,accumulator,done) {
var msgInfo = msgInfos.shift();
exp.assign("I", msgInfo.msg.parts.index);
@@ -337,7 +330,6 @@ module.exports = function(RED) {
}
});
}
function reduceAndSendGroup(node, group, done) {
var is_right = node.reduce_right;
var flag = is_right ? -1 : 1;
@@ -523,13 +515,13 @@ module.exports = function(RED) {
if (typeof group.joinChar !== 'string') {
groupJoinChar = group.joinChar.toString();
}
RED.util.setMessageProperty(group.msg,group?.prop||"payload",group.payload.join(groupJoinChar));
RED.util.setMessageProperty(group.msg,node.property,group.payload.join(groupJoinChar));
}
else {
if (node.propertyType === 'full') {
group.msg = RED.util.cloneMessage(group.msg);
}
RED.util.setMessageProperty(group.msg,group?.prop||"payload",group.payload);
RED.util.setMessageProperty(group.msg,node.property,group.payload);
}
if (group.msg.hasOwnProperty('parts') && group.msg.parts.hasOwnProperty('parts')) {
group.msg.parts = group.msg.parts.parts;
@@ -597,7 +589,7 @@ module.exports = function(RED) {
}
if (node.mode === 'auto' && (!msg.hasOwnProperty("parts")||!msg.parts.hasOwnProperty("id"))) {
// if a blank reset message reset it all.
// if a blank reset messag erest it all.
if (msg.hasOwnProperty("reset")) {
if (inflight && inflight.hasOwnProperty("partId") && inflight[partId].timeout) {
clearTimeout(inflight[partId].timeout);
@@ -611,15 +603,6 @@ module.exports = function(RED) {
return;
}
if (node.mode === 'custom' && msg.hasOwnProperty('parts')) {
if (msg.parts.hasOwnProperty('parts')) {
msg.parts = { parts: msg.parts.parts };
}
else {
delete msg.parts;
}
}
var payloadType;
var propertyKey;
var targetCount;
@@ -635,7 +618,6 @@ module.exports = function(RED) {
propertyKey = msg.parts.key;
arrayLen = msg.parts.len;
propertyIndex = msg.parts.index;
property = RED.util.getMessageProperty(msg,msg.parts.property||"payload");
}
else if (node.mode === 'reduce') {
return processReduceMessageQueue({msg, send, done});
@@ -737,8 +719,6 @@ module.exports = function(RED) {
completeSend(partId)
}, node.timer)
}
if (node.mode === "auto") { inflight[partId].prop = msg.parts.property; }
else { inflight[partId].prop = node.property; }
}
inflight[partId].dones.push(done);

View File

@@ -516,8 +516,7 @@
"path1": "Standardmäßig enthält <code>payload</code> die Daten, die über einen WebSocket gesendet oder von einem WebSocket empfangen werden. Der Empfänger (Listener) kann so konfiguriert werden, dass er das gesamte Nachrichtenobjekt als eine JSON-formatierte Zeichenfolge (string) sendet oder empfängt.",
"path2": "Dieser Pfad ist relativ zu <code>__path__</code>.",
"url1": "URL sollte ws:&#47;&#47; oder wss:&#47;&#47; Schema verwenden und auf einen vorhandenen WebSocket-Listener verweisen.",
"url2": "Standardmäßig enthält <code>payload</code> die Daten, die über einen WebSocket gesendet oder von einem WebSocket empfangen werden. Der Client kann so konfiguriert werden, dass er das gesamte Nachrichtenobjekt als eine JSON-formatierte Zeichenfolge (string) sendet oder empfängt.",
"headers": "Header werden nur während des Protokollaktualisierungsmechanismus übermittelt, von HTTP auf das WS/WSS-Protokoll."
"url2": "Standardmäßig enthält <code>payload</code> die Daten, die über einen WebSocket gesendet oder von einem WebSocket empfangen werden. Der Client kann so konfiguriert werden, dass er das gesamte Nachrichtenobjekt als eine JSON-formatierte Zeichenfolge (string) sendet oder empfängt."
},
"status": {
"connected": "Verbunden __count__",

View File

@@ -586,8 +586,7 @@
"path1": "By default, <code>payload</code> will contain the data to be sent over, or received from a websocket. The listener can be configured to send or receive the entire message object as a JSON formatted string.",
"path2": "This path will be relative to <code>__path__</code>.",
"url1": "URL should use ws:&#47;&#47; or wss:&#47;&#47; scheme and point to an existing websocket listener.",
"url2": "By default, <code>payload</code> will contain the data to be sent over, or received from a websocket. The client can be configured to send or receive the entire message object as a JSON formatted string.",
"headers": "Headers are only submitted during the Protocol upgrade mechanism, from HTTP to the WS/WSS Protocol."
"url2": "By default, <code>payload</code> will contain the data to be sent over, or received from a websocket. The client can be configured to send or receive the entire message object as a JSON formatted string."
},
"status": {
"connected": "connected __count__",
@@ -850,13 +849,7 @@
"newline": "Newline",
"usestrings": "parse numerical values",
"include_empty_strings": "include empty strings",
"include_null_values": "include null values",
"spec": "Parser"
},
"spec": {
"rfc": "RFC4180",
"legacy": "Legacy",
"legacy_warning": "Legacy mode will be removed in a future release."
"include_null_values": "include null values"
},
"placeholder": {
"columns": "comma-separated column names"
@@ -885,7 +878,6 @@
"once": "send headers once, until msg.reset"
},
"errors": {
"bad_template": "Malformed columns template.",
"csv_js": "This node only handles CSV strings or js objects.",
"obj_csv": "No columns template specified for object -> CSV.",
"bad_csv": "Malformed CSV data - output probably corrupt."
@@ -895,14 +887,12 @@
"label": {
"select": "Selector",
"output": "Output",
"in": "in",
"prefix": "Property name for HTML content"
"in": "in"
},
"output": {
"html": "the html content of the elements",
"text": "only the text content of the elements",
"attr": "an object of any attributes of the elements",
"compl": "an object of any attributes of the elements and html contents"
"attr": "an object of any attributes of the elements"
},
"format": {
"single": "as a single message containing an array",
@@ -1011,7 +1001,7 @@
"tip": "Tip: The filename should be an absolute path, otherwise it will be relative to the working directory of the Node-RED process."
},
"split": {
"split": "Split",
"split": "split",
"intro": "Split <code>msg.payload</code> based on type:",
"object": "<b>Object</b>",
"objectSend": "Send a message for each key/value pair",

View File

@@ -30,8 +30,6 @@
before being sent.</p>
<p>If <code>msg._session</code> is not present the payload is
sent to <b>all</b> connected clients.</p>
<p>In Reply-to mode, setting <code>msg.reset = true</code> will reset the connection
specified by _session.id, or all connections if no _session.id is specified.</p>
<p><b>Note: </b>On some systems you may need root or administrator access
to access ports below 1024.</p>
</script>
@@ -42,8 +40,6 @@
returned characters into a fixed buffer, match a specified character before returning,
wait a fixed timeout from first reply and then return, sit and wait for data, or send then close the connection
immediately, without waiting for a reply.</p>
<p>If in sit and wait mode (remain connected) you can send <code>msg.reset = true</code> or <code>msg.reset = "host:port"</code> to force a break in
the connection and an automatic reconnection.</p>
<p>The response will be output in <code>msg.payload</code> as a buffer, so you may want to .toString() it.</p>
<p>If you leave tcp host or port blank they must be set by using the <code>msg.host</code> and <code>msg.port</code> properties in every message sent to the node.</p>
</script>

View File

@@ -36,9 +36,7 @@
</dl>
<h3>Details</h3>
<p>The column template can contain an ordered list of column names. When converting CSV to an object, the column names
will be used as the property names. Alternatively, the column names can be taken from the first row of the CSV.
<p>When the RFC parser is selected, the column template must be compliant with RFC4180.</p>
</p>
will be used as the property names. Alternatively, the column names can be taken from the first row of the CSV.</p>
<p>When converting to CSV, the columns template is used to identify which properties to extract from the object and in what order.</p>
<p>If the columns template is blank then you can use a simple comma separated list of properties supplied in <code>msg.columns</code> to
determine what to extract and in what order. If neither are present then all the object properties are output in the order
@@ -51,5 +49,4 @@
<p>If outputting multiple messages they will have their <code>parts</code> property set and form a complete message sequence.</p>
<p>If the node is set to only send column headers once, then setting <code>msg.reset</code> to any value will cause the node to resend the headers.</p>
<p><b>Note:</b> the column template must be comma separated - even if a different separator is chosen for the data.</p>
<p><b>Note:</b> in RFC mode, catchable errors will be thrown for malformed CSV headers and invalid input payload data</p>
</script>

View File

@@ -103,7 +103,7 @@
<h4>Automatic mode</h4>
<p>Automatic mode uses the <code>parts</code> property of incoming messages to
determine how the sequence should be joined. This allows it to automatically
reverse the action of a <b>split</b> node.
reverse the action of a <b>split</b> node.</p>
<h4>Manual mode</h4>
<p>When configured to join in manual mode, the node is able to join sequences

View File

@@ -583,8 +583,7 @@
"path1": "Par défaut, <code>payload</code> contiendra les données à envoyer ou à recevoir d'un websocket. L'écouteur peut être configuré pour envoyer ou recevoir l'intégralité de l'objet message sous forme de chaîne au format JSON.",
"path2": "Ce chemin sera relatif à <code>__path__</code>.",
"url1": "L'URL doit utiliser le schéma ws:&#47;&#47; ou wss:&#47;&#47; et pointer vers un écouteur websocket existant.",
"url2": "Par défaut, <code>payload</code> contiendra les données à envoyer ou à recevoir d'un websocket. Le client peut être configuré pour envoyer ou recevoir l'intégralité de l'objet message sous forme de chaîne au format JSON.",
"headers": "Les en-têtes ne sont soumis que lors du mécanisme de mise à niveau du protocole, de HTTP vers le protocole WS/WSS."
"url2": "Par défaut, <code>payload</code> contiendra les données à envoyer ou à recevoir d'un websocket. Le client peut être configuré pour envoyer ou recevoir l'intégralité de l'objet message sous forme de chaîne au format JSON."
},
"status": {
"connected": "__count__ connecté",

View File

@@ -1,3 +1,3 @@
<script type="text/html" data-help-name="global-config">
<p>大域的なフローの設定を保持するノード大域的な環境変数の定義を含みます</p>
</script>p
</script>

View File

@@ -586,8 +586,7 @@
"path1": "標準では <code>payload</code> がwebsocketから送信、受信されるデータを持ちます。クライアントはJSON形式の文字列としてメッセージ全体を送信、受信するよう設定できます。",
"path2": "このパスは <code>__path__</code> の相対パスになります。",
"url1": "URLには ws:&#47;&#47; または wss:&#47;&#47; スキーマを使用して、存在するwebsocketリスナを設定してください。",
"url2": "標準では <code>payload</code> がwebsocketから送信、受信されるデータを持ちます。クライアントはJSON形式の文字列としてメッセージ全体を送信、受信するよう設定できます。",
"headers": "ヘッダーは、HTTP から WS/WSS プロトコルへのプロトコル アップグレード メカニズム中にのみ送信されます。"
"url2": "標準では <code>payload</code> がwebsocketから送信、受信されるデータを持ちます。クライアントはJSON形式の文字列としてメッセージ全体を送信、受信するよう設定できます。"
},
"status": {
"connected": "接続数 __count__",

View File

@@ -451,8 +451,7 @@
"path1": "표준으로는 <code>payload</code> 가 websocket에서 송신, 수신된 데이터를 기다립니다. 클라이언트는 JSON형식의 문자열로 메세지전체를 송신, 수신하도록 설정할 수 있습니다.",
"path2": "This path will be relative to <code>__path__</code>.",
"url1": "URL에는 ws:&#47;&#47; 또는 wss:&#47;&#47; 스키마를 사용하여, 존재하는 websocket리스너를 설정해 주세요.",
"url2": "표준으로는 <code>payload</code> 가 websocket에서 송신,수신될 데이터를 기다립니다.클라이언트는 JSON형식의 문자열로 메세지전체를 송신, 수신하도록 설정할 수 있습니다.",
"headers": "헤더는 HTTP에서 WS/WSS 프로토콜로 프로토콜 업그레이드 메커니즘 중에만 제출됩니다."
"url2": "표준으로는 <code>payload</code> 가 websocket에서 송신,수신될 데이터를 기다립니다.클라이언트는 JSON형식의 문자열로 메세지전체를 송신, 수신하도록 설정할 수 있습니다."
},
"status": {
"connected": "접속 수 __count__",

View File

@@ -573,8 +573,7 @@
"path1": "Por padrão, a <code>carga útil</code> conterá os dados a serem enviados ou recebidos de um websocket. O ouvinte pode ser configurado para enviar ou receber todo o objeto de mensagem como uma cadeia de caracteres formatada em JSON.",
"path2": "Este caminho será relativo a <code>__path__</code>.",
"url1": "A URL deve usar o esquema ws:&#47;&#47; ou wss:&#47;&#47; e apontar para um ouvinte de websocket existente.",
"url2": "Por padrão, <code>carga útil</code> conterá os dados a serem enviados ou recebidos de um websocket. O cliente pode ser configurado para enviar ou receber todo o objeto de mensagem como uma cadeia de caracteres formatada em JSON.",
"headers": "Os cabeçalhos são enviados apenas durante o mecanismo de atualização do protocolo, do HTTP para o protocolo WS/WSS."
"url2": "Por padrão, <code>carga útil</code> conterá os dados a serem enviados ou recebidos de um websocket. O cliente pode ser configurado para enviar ou receber todo o objeto de mensagem como uma cadeia de caracteres formatada em JSON."
},
"status": {
"connected": "conectado __count__",

View File

@@ -475,8 +475,7 @@
"path1": "По умолчанию <code>payload</code> будет содержать данные, которые будут отправлены или получены из websocket. Слушатель может быть настроен на отправку или получение всего объекта сообщения в виде строки в формате JSON.",
"path2": "Путь будет относительно <code>__path__</code>.",
"url1": "URL должен использовать схему ws:&#47;&#47; или wss:&#47;&#47; и указывать на существующего слушателя websocket.",
"url2": "По умолчанию <code>payload</code> будет содержать данные, которые будут отправлены или получены из websocket. Клиент может быть настроен на отправку или получение всего объекта сообщения в виде строки в формате JSON.",
"headers": "Заголовки передаются только во время механизма обновления протокола с HTTP на протокол WS/WSS."
"url2": "По умолчанию <code>payload</code> будет содержать данные, которые будут отправлены или получены из websocket. Клиент может быть настроен на отправку или получение всего объекта сообщения в виде строки в формате JSON."
},
"status": {
"connected": "подключен __count__",

View File

@@ -576,8 +576,7 @@
"path1": "默认情况下,<code>payload</code>将包含要发送或从Websocket接收的数据。侦听器可以配置为以JSON格式的字符串发送或接收整个消息对象.",
"path2": "这条路径将相对于 <code>__path__</code>.",
"url1": "URL 应该使用ws:&#47;&#47;或者wss:&#47;&#47;方案并指向现有的websocket侦听器.",
"url2": "默认情况下,<code>payload</code> 将包含要发送或从Websocket接收的数据。可以将客户端配置为以JSON格式的字符串发送或接收整个消息对象.",
"headers": "标头仅在协议升级机制期间提交,从 HTTP 到 WS/WSS 协议."
"url2": "默认情况下,<code>payload</code> 将包含要发送或从Websocket接收的数据。可以将客户端配置为以JSON格式的字符串发送或接收整个消息对象."
},
"status": {
"connected": "已连接数量 __count__",

View File

@@ -471,8 +471,7 @@
"path1": "預設情況下,<code>payload</code>將包含要發送或從Websocket接收的資料。偵聽器可以配置為以JSON格式的字串發送或接收整個消息物件.",
"path2": "這條路徑將相對於 <code>__path__</code>.",
"url1": "URL 應該使用ws:&#47;&#47;或者wss:&#47;&#47;方案並指向現有的websocket監聽器.",
"url2": "預設情況下,<code>payload</code> 將包含要發送或從Websocket接收的資料。可以將使用者端配置為以JSON格式的字串發送或接收整個消息物件.",
"headers": "標頭僅在協定升級機制期間提交,從 HTTP 到 WS/WSS 協定."
"url2": "預設情況下,<code>payload</code> 將包含要發送或從Websocket接收的資料。可以將使用者端配置為以JSON格式的字串發送或接收整個消息物件."
},
"status": {
"connected": "連接數 __count__",

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/nodes",
"version": "4.0.0-beta.1",
"version": "3.1.9",
"license": "Apache-2.0",
"repository": {
"type": "git",

View File

@@ -264,7 +264,7 @@ async function installModule(moduleDetails) {
"module": moduleDetails.module,
"version": moduleDetails.version,
"dir": installDir,
"args": ["--omit=dev","--engine-strict"]
"args": ["--production","--engine-strict"]
}
return hooks.trigger("preInstall", triggerPayload).then((result) => {
// preInstall passed
@@ -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,12 +25,15 @@ const registryUtil = require("./util");
const library = require("./library");
const {exec,log,events,hooks} = require("@node-red/util");
const child_process = require('child_process');
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
let installerEnabled = false;
const isWindows = process.platform === 'win32'
const npmCommand = isWindows ? 'npm.cmd' : 'npm';
let installerEnabled = false;
let settings;
const moduleRe = /^(@[^/@]+?[/])?[^/@]+?$/;
const slashRe = process.platform === "win32" ? /\\|[/]/ : /[/]/;
const slashRe = isWindows ? /\\|[/]/ : /[/]/;
const pkgurlRe = /^(https?|git(|\+https?|\+ssh|\+file)):\/\//;
const localtgzRe = /^([a-zA-Z]:|\/).+tgz$/;
@@ -215,7 +218,7 @@ async function installModule(module,version,url) {
"dir": installDir,
"isExisting": isExisting,
"isUpgrade": isUpgrade,
"args": ['--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix=~','--omit=dev','--engine-strict']
"args": ['--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix=~','--production','--engine-strict']
}
return hooks.trigger("preInstall", triggerPayload).then((result) => {
@@ -225,7 +228,7 @@ async function installModule(module,version,url) {
let extraArgs = triggerPayload.args || [];
let args = ['install', ...extraArgs, installName]
log.trace(npmCommand + JSON.stringify(args));
return exec.run(npmCommand,args,{ cwd: installDir}, true)
return exec.run(npmCommand,args,{ cwd: installDir, shell: true }, true)
} else {
log.trace("skipping npm install");
}
@@ -260,7 +263,7 @@ async function installModule(module,version,url) {
log.warn("------------------------------------------");
e = new Error(log._("server.install.install-failed")+": "+err.toString());
if (err.hook === "postInstall") {
return exec.run(npmCommand,["remove",module],{ cwd: installDir}, false).finally(() => {
return exec.run(npmCommand,["remove",module],{ cwd: installDir, shell: true }, false).finally(() => {
throw e;
})
}
@@ -356,7 +359,7 @@ async function getModuleVersionFromNPM(module, version) {
}
return new Promise((resolve, reject) => {
child_process.execFile(npmCommand,['info','--json',installName],function(err,stdout,stderr) {
child_process.execFile(npmCommand,['info','--json',installName],{ shell: true },function(err,stdout,stderr) {
try {
if (!stdout) {
log.warn(log._("server.install.install-failed-not-found",{name:module}));
@@ -511,7 +514,7 @@ function uninstallModule(module) {
let extraArgs = triggerPayload.args || [];
let args = ['remove', ...extraArgs, module]
log.trace(npmCommand + JSON.stringify(args));
return exec.run(npmCommand,args,{ cwd: installDir}, true)
return exec.run(npmCommand,args,{ cwd: installDir, shell: true }, true)
} else {
log.trace("skipping npm uninstall");
}
@@ -578,7 +581,7 @@ async function checkPrereq() {
installerEnabled = false;
} else {
return new Promise(resolve => {
child_process.execFile(npmCommand,['-v'],function(err,stdout) {
child_process.execFile(npmCommand,['-v'],{ shell: true },function(err,stdout) {
if (err) {
log.info(log._("server.palette-editor.npm-not-found"));
installerEnabled = false;

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/registry",
"version": "4.0.0-beta.1",
"version": "3.1.9",
"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": "3.1.9",
"clone": "2.1.2",
"fs-extra": "11.1.1",
"semver": "7.5.4",
"tar": "6.1.13",
"tar": "6.2.1",
"uglify-js": "3.17.4"
}
}

View File

@@ -68,7 +68,6 @@ var api = module.exports = {
* @param {String} opts.store - the context store
* @param {String} opts.key - the context key
* @param {Object} opts.req - the request to log (optional)
* @param {Boolean} opts.keysOnly - whether to return keys only
* @return {Promise} - the node information
* @memberof @node-red/runtime_context
*/
@@ -103,15 +102,6 @@ var api = module.exports = {
if (key) {
store = store || availableStores.default;
ctx.get(key,store,function(err, v) {
if (opts.keysOnly) {
if (Array.isArray(v)) {
resolve({ [store]: { format: `array[${v.length}]`}})
} else if (typeof v === 'object') {
resolve({ [store]: { keys: Object.keys(v), format: 'Object' } })
} else {
resolve({ [store]: { keys: [] }})
}
}
var encoded = util.encodeObject({msg:v});
if (store !== availableStores.default) {
encoded.store = store;
@@ -128,58 +118,32 @@ var api = module.exports = {
stores = [store];
}
var result = {};
var c = stores.length;
var errorReported = false;
stores.forEach(function(store) {
if (opts.keysOnly) {
ctx.keys(store,function(err, keys) {
if (err) {
// TODO: proper error reporting
if (!errorReported) {
errorReported = true;
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"unexpected_error"}, opts.req);
var err = new Error();
err.code = "unexpected_error";
err.status = 400;
return reject(err);
}
return
exportContextStore(scope,ctx,store,result,function(err) {
if (err) {
// TODO: proper error reporting
if (!errorReported) {
errorReported = true;
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"unexpected_error"}, opts.req);
var err = new Error();
err.code = "unexpected_error";
err.status = 400;
return reject(err);
}
result[store] = { keys }
c--;
if (c === 0) {
if (!errorReported) {
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key},opts.req);
resolve(result);
}
}
})
} else {
exportContextStore(scope,ctx,store,result,function(err) {
if (err) {
// TODO: proper error reporting
if (!errorReported) {
errorReported = true;
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"unexpected_error"}, opts.req);
var err = new Error();
err.code = "unexpected_error";
err.status = 400;
return reject(err);
}
return;
return;
}
c--;
if (c === 0) {
if (!errorReported) {
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key},opts.req);
resolve(result);
}
c--;
if (c === 0) {
if (!errorReported) {
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key},opts.req);
resolve(result);
}
}
});
}
}
});
})
}
} else {

View File

@@ -678,6 +678,9 @@ class Flow {
if (logMessage.hasOwnProperty('stack')) {
errorMessage.error.stack = logMessage.stack;
}
if (logMessage.hasOwnProperty('cause')) {
errorMessage.error.cause = logMessage.cause;
}
targetCatchNode.receive(errorMessage);
handled = true;
});

View File

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

@@ -154,7 +154,7 @@ function start() {
log.info(log._("runtime.version",{component:"Node.js ",version:process.version}));
if (settings.UNSUPPORTED_VERSION) {
log.error("*****************************************************************");
log.error("* "+log._("runtime.unsupported_version",{component:"Node.js",version:process.version,requires: ">=18"})+" *");
log.error("* "+log._("runtime.unsupported_version",{component:"Node.js",version:process.version,requires: ">=8.9.0"})+" *");
log.error("*****************************************************************");
events.emit("runtime-event",{id:"runtime-unsupported-version",payload:{type:"error",text:"notification.errors.unsupportedVersion"},retain:true});
}

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/runtime",
"version": "4.0.0-beta.1",
"version": "3.1.9",
"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": "3.1.9",
"@node-red/util": "3.1.9",
"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

@@ -636,15 +636,7 @@ function evaluateNodeProperty(value, type, node, msg, callback) {
} else if (type === 're') {
result = new RegExp(value);
} else if (type === 'date') {
if (!value) {
result = Date.now();
} else if (value === 'object') {
result = new Date()
} else if (value === 'iso') {
result = (new Date()).toISOString()
} else {
result = moment().format(value)
}
result = Date.now();
} else if (type === 'bin') {
var data = JSON.parse(value);
if (Array.isArray(data) || (typeof(data) === "string")) {
@@ -777,15 +769,12 @@ function evaluateJSONataExpression(expr,msg,callback) {
});
}
} else {
const error = new Error('Calls to RED.util.evaluateJSONataExpression must include a callback.')
throw error
log.warn('Deprecated API warning: Calls to RED.util.evaluateJSONataExpression must include a callback. '+
'This will not be optional in Node-RED 4.0. Please identify the node from the following stack '+
'and check for an update on npm. If none is available, please notify the node author.')
log.warn(new Error().stack)
}
expr.evaluate(context, bindings).then(result => {
callback(null, result)
}).catch(err => {
callback(err)
})
return expr.evaluate(context, bindings, callback);
}
/**

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/util",
"version": "4.0.0-beta.1",
"version": "3.1.9",
"license": "Apache-2.0",
"repository": {
"type": "git",
@@ -18,7 +18,7 @@
"fs-extra": "11.1.1",
"i18next": "21.10.0",
"json-stringify-safe": "5.0.1",
"jsonata": "2.0.4",
"jsonata": "1.8.7",
"lodash.clonedeep": "^4.5.0",
"moment": "2.29.4",
"moment-timezone": "0.5.43"

View File

@@ -33,7 +33,8 @@ if (NODE_MAJOR_VERSION >= 16) {
function checkVersion(userSettings) {
var semver = require('semver');
if (!semver.satisfies(process.version,">=18.0.0")) {
if (!semver.satisfies(process.version,">=14.0.0")) {
// TODO: in the future, make this a hard error.
// var e = new Error("Unsupported version of Node.js");
// e.code = "unsupported_version";
// throw e;

View File

@@ -1,6 +1,6 @@
{
"name": "node-red",
"version": "4.0.0-beta.1",
"version": "3.1.9",
"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": "3.1.9",
"@node-red/runtime": "3.1.9",
"@node-red/util": "3.1.9",
"@node-red/nodes": "3.1.9",
"basic-auth": "2.0.1",
"bcryptjs": "2.4.3",
"express": "4.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"
},
@@ -47,6 +47,6 @@
"bcrypt": "5.1.0"
},
"engines": {
"node": ">=18"
"node": ">=14"
}
}

View File

@@ -26,13 +26,6 @@ if (process.argv[2] === 'admin') {
return;
}
var semver = require('semver');
if (!semver.satisfies(process.version, ">=18.0.0")) {
console.log("Unsupported version of Node.js:", process.version);
console.log("Node-RED requires Node.js v18 or later");
process.exit(1)
}
var http = require('http');
var https = require('https');
var util = require("util");
@@ -353,7 +346,7 @@ httpsPromise.then(function(startupHttps) {
} catch(err) {
if (err.code == "unsupported_version") {
console.log("Unsupported version of Node.js:",process.version);
console.log("Node-RED requires Node.js v18 or later");
console.log("Node-RED requires Node.js v8.9.0 or later");
} else {
console.log("Failed to start server:");
if (err.stack) {
@@ -415,15 +408,9 @@ httpsPromise.then(function(startupHttps) {
if (settings.httpAdminRoot !== false) {
app.use(settings.httpAdminRoot,RED.httpAdmin);
}
if (settings.httpNodeRoot !== false && settings.httpNodeAuth) {
if (typeof settings.httpNodeAuth === "function" || Array.isArray(settings.httpNodeAuth)) {
app.use(settings.httpNodeRoot, settings.httpNodeAuth);
} else {
app.use(settings.httpNodeRoot, basicAuthMiddleware(settings.httpNodeAuth.user, settings.httpNodeAuth.pass));
}
app.use(settings.httpNodeRoot,basicAuthMiddleware(settings.httpNodeAuth.user,settings.httpNodeAuth.pass));
}
if (settings.httpNodeRoot !== false) {
app.use(settings.httpNodeRoot,RED.httpNode);
}

View File

@@ -449,7 +449,6 @@ module.exports = {
* - ui (for use with Node-RED Dashboard)
* - debugUseColors
* - debugMaxLength
* - debugStatusLength
* - execMaxBufferSize
* - httpRequestTimeout
* - mqttReconnectTime
@@ -505,9 +504,6 @@ module.exports = {
/** The maximum length, in characters, of any message sent to the debug sidebar tab */
debugMaxLength: 1000,
/** The maximum length, in characters, of status messages under the debug node */
//debugStatusLength: 32,
/** Maximum buffer size for the exec node. Defaults to 10Mb */
//execMaxBufferSize: 10000000,

View File

@@ -390,7 +390,8 @@ describe('function node', function() {
msg.should.have.property('level', helper.log().ERROR);
msg.should.have.property('id', 'n1');
msg.should.have.property('type', 'function');
msg.should.have.property('msg', 'ReferenceError: retunr is not defined (line 2, col 1)');
msg.should.have.property('msg')
msg.msg.message.should.equal('ReferenceError: retunr is not defined (line 2, col 1)');
done();
} catch(err) {
done(err);
@@ -659,7 +660,8 @@ describe('function node', function() {
msg.should.have.property('level', helper.log().ERROR);
msg.should.have.property('id', name);
msg.should.have.property('type', 'function');
msg.should.have.property('msg', 'Error: Callback must be a function');
msg.should.have.property('msg')
msg.msg.message.should.equal('Callback must be a function');
done();
}
catch (e) {
@@ -1718,13 +1720,9 @@ describe('function node', function() {
describe("init function", function() {
it('should delay handling messages until init completes', function(done) {
const timeoutMS = 200;
// Since helper.load uses process.nextTick timers might occasionally finish
// a couple of milliseconds too early, so give some leeway to the check.
const timeoutCheckMargin = 5;
var flow = [{id:"n1",type:"function",wires:[["n2"]],initialize: `
return new Promise((resolve,reject) => {
setTimeout(resolve, ${timeoutMS});
setTimeout(resolve,200)
})`,
func:"return msg;"
},
@@ -1737,10 +1735,9 @@ describe('function node', function() {
msg.delta = Date.now() - msg.payload;
receivedMsgs.push(msg)
if (receivedMsgs.length === 5) {
let deltas = receivedMsgs.map(msg => msg.delta);
var errors = deltas.filter(delta => delta < (timeoutMS - timeoutCheckMargin))
var errors = receivedMsgs.filter(msg => msg.delta < 200)
if (errors.length > 0) {
done(new Error(`Message received before init completed - delta values ${JSON.stringify(deltas)} expected to be > ${timeoutMS - timeoutCheckMargin}`))
done(new Error(`Message received before init completed - was ${msg.delta} expected >300`))
} else {
done();
}

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

File diff suppressed because it is too large Load Diff

View File

@@ -66,27 +66,6 @@ describe('SPLIT node', function() {
});
});
it('should split an array on a sub-property into multiple messages', function(done) {
var flow = [{id:"sn1", type:"split", property:"foo", wires:[["sn2"]]},
{id:"sn2", type:"helper"}];
helper.load(splitNode, flow, function() {
var sn1 = helper.getNode("sn1");
var sn2 = helper.getNode("sn2");
sn2.on("input", function(msg) {
msg.should.have.property("parts");
msg.parts.should.have.property("count",4);
msg.parts.should.have.property("type","array");
msg.parts.should.have.property("index");
msg.parts.should.have.property("property","foo");
if (msg.parts.index === 0) { msg.foo.should.equal(1); }
if (msg.parts.index === 1) { msg.foo.should.equal(2); }
if (msg.parts.index === 2) { msg.foo.should.equal(3); }
if (msg.parts.index === 3) { msg.foo.should.equal(4); done(); }
});
sn1.receive({foo:[1,2,3,4]});
});
});
it('should split an array into multiple messages of a specified size', function(done) {
var flow = [{id:"sn1", type:"split", wires:[["sn2"]], arraySplt:3, arraySpltType:"len"},
{id:"sn2", type:"helper"}];
@@ -129,31 +108,6 @@ describe('SPLIT node', function() {
});
});
it('should split an object sub property into pieces', function(done) {
var flow = [{id:"sn1", type:"split", property:"foo.bar",wires:[["sn2"]]},
{id:"sn2", type:"helper"}];
helper.load(splitNode, flow, function() {
var sn1 = helper.getNode("sn1");
var sn2 = helper.getNode("sn2");
var count = 0;
sn2.on("input", function(msg) {
msg.should.have.property("foo");
msg.foo.should.have.property("bar");
msg.should.have.property("parts");
msg.parts.should.have.property("type","object");
msg.parts.should.have.property("key");
msg.parts.should.have.property("count");
msg.parts.should.have.property("index");
msg.parts.should.have.property("property","foo.bar");
msg.topic.should.equal("foo");
if (msg.parts.index === 0) { msg.foo.bar.should.equal(1); }
if (msg.parts.index === 1) { msg.foo.bar.should.equal("2"); }
if (msg.parts.index === 2) { msg.foo.bar.should.equal(true); done(); }
});
sn1.receive({topic:"foo",foo:{bar:{a:1,b:"2",c:true}}});
});
});
it('should split an object into pieces and overwrite their topics', function(done) {
var flow = [{id:"sn1", type:"split", addname:"topic", wires:[["sn2"]]},
{id:"sn2", type:"helper"}];
@@ -562,7 +516,6 @@ describe('JOIN node', function() {
n1.receive({payload:{a:1}});
});
});
it('should join things into an array ignoring msg.parts.index in manual mode', function(done) {
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:3, joiner:",",mode:"custom"},
{id:"n2", type:"helper"}];
@@ -609,32 +562,6 @@ describe('JOIN node', function() {
});
});
it('should join things into an array on a sub property in auto mode', function(done) {
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:3, joiner:",", mode:"auto"},
{id:"n2", type:"helper"}];
helper.load(joinNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("foo");
msg.foo.should.have.property("bar");
msg.foo.bar.should.be.an.Array();
msg.foo.bar[0].should.equal("A");
msg.foo.bar[1].should.equal("B");
//msg.payload[2].a.should.equal(1);
done();
}
catch(e) {done(e);}
});
n1.receive({foo:{bar:"A"}, parts:{id:1, type:"array", len:1, index:0, count:4, property:"foo.bar"}});
n1.receive({foo:{bar:"B"}, parts:{id:1, type:"array", len:1, index:1, count:4, property:"foo.bar"}});
n1.receive({foo:{bar:"C"}, parts:{id:1, type:"array", len:1, index:2, count:4, property:"foo.bar"}});
n1.receive({foo:{bar:"D"}, parts:{id:1, type:"array", len:1, index:3, count:4, property:"foo.bar"}});
});
});
it('should join strings into a buffer after a count', function(done) {
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:2, build:"buffer", joinerType:"bin", joiner:"", mode:"custom"},
{id:"n2", type:"helper"}];
@@ -712,35 +639,6 @@ describe('JOIN node', function() {
});
});
it('should merge sub property objects', function(done) {
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:5, property:"foo.bar", build:"merged", mode:"custom"},
{id:"n2", type:"helper"}];
helper.load(joinNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("foo");
msg.foo.should.have.property("bar");
msg.foo.bar.should.have.property("a",1);
msg.foo.bar.should.have.property("b",2);
msg.foo.bar.should.have.property("c",3);
msg.foo.bar.should.have.property("d",4);
msg.foo.bar.should.have.property("e",5);
done();
}
catch(e) { done(e)}
});
n1.receive({foo:{bar:{a:9}, topic:"f"}});
n1.receive({foo:{bar:{a:1}, topic:"a"}});
n1.receive({foo:{bar:{b:9}, topic:"b"}});
n1.receive({foo:{bar:{b:2}, topic:"b"}});
n1.receive({foo:{bar:{c:3}, topic:"c"}});
n1.receive({foo:{bar:{d:4}, topic:"d"}});
n1.receive({foo:{bar:{e:5}, topic:"e"}});
});
});
it('should merge full msg objects', function(done) {
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:6, build:"merged", mode:"custom", propertyType:"full", property:""},
{id:"n2", type:"helper"}];

View File

@@ -379,17 +379,10 @@ describe("@node-red/util/util", function() {
result = util.evaluateNodeProperty('','bool');
result.should.be.false();
});
it('returns date - default format',function() {
it('returns date',function() {
var result = util.evaluateNodeProperty('','date');
(Date.now() - result).should.be.approximately(0,50);
});
it('returns date - iso format',function() {
var result = util.evaluateNodeProperty('iso','date');
// 2023-12-04T16:51:04.429Z
/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d+Z$/.test(result).should.be.true()
});
it('returns bin', function () {
var result = util.evaluateNodeProperty('[1, 2]','bin');
result[0].should.eql(1);
@@ -448,16 +441,9 @@ describe("@node-red/util/util", function() {
},{});
result.should.eql("123");
});
it('returns jsonata result', function (done) {
util.evaluateNodeProperty('$abs(-1)','jsonata',{},{}, (err, result) => {
try {
result.should.eql(1);
done()
} catch (error) {
done(error)
}
});
it('returns jsonata result', function () {
var result = util.evaluateNodeProperty('$abs(-1)','jsonata',{},{});
result.should.eql(1);
});
it('returns null', function() {
var result = util.evaluateNodeProperty(null,'null');
@@ -615,105 +601,51 @@ describe("@node-red/util/util", function() {
});
});
describe('evaluateJSONataExpression', function() {
it('evaluates an expression', function(done) {
it('evaluates an expression', function() {
var expr = util.prepareJSONataExpression('payload',{});
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
try {
result.should.eql("hello");
done()
} catch (error) {
done(error)
}
});
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
result.should.eql("hello");
});
it('evaluates a legacyMode expression', function() {
var expr = util.prepareJSONataExpression('msg.payload',{});
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
try {
result.should.eql("hello");
done()
} catch (error) {
done(error)
}
});
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
result.should.eql("hello");
});
it('accesses flow context from an expression', function() {
var expr = util.prepareJSONataExpression('$flowContext("foo")',{context:function() { return {flow:{get: function(key) { return {'foo':'bar'}[key]}}}}});
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
try {
result.should.eql("bar");
done()
} catch (error) {
done(error)
}
});
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
result.should.eql("bar");
});
it('accesses undefined environment variable from an expression', function() {
var expr = util.prepareJSONataExpression('$env("UTIL_ENV")',{});
util.evaluateJSONataExpression(expr,{}, (err, result) => {
try {
result.should.eql("");
done()
} catch (error) {
done(error)
}
});
});
var result = util.evaluateJSONataExpression(expr,{});
result.should.eql('');
});
it('accesses environment variable from an expression', function() {
process.env.UTIL_ENV = 'foo';
var expr = util.prepareJSONataExpression('$env("UTIL_ENV")',{});
util.evaluateJSONataExpression(expr,{}, (err, result) => {
try {
result.should.eql("foo");
done()
} catch (error) {
done(error)
}
});
});
var result = util.evaluateJSONataExpression(expr,{});
result.should.eql('foo');
});
it('accesses moment from an expression', function() {
var expr = util.prepareJSONataExpression('$moment("2020-05-27", "YYYY-MM-DD").add(7, "days").add(1, "months").format("YYYY-MM-DD")',{});
util.evaluateJSONataExpression(expr,{}, (err, result) => {
try {
result.should.eql("2020-07-03");
done()
} catch (error) {
done(error)
}
});
var result = util.evaluateJSONataExpression(expr,{});
result.should.eql('2020-07-03');
});
it('accesses moment-timezone from an expression', function() {
var expr = util.prepareJSONataExpression('$moment("2013-11-18 11:55Z").tz("Asia/Taipei").format()',{});
util.evaluateJSONataExpression(expr,{}, (err, result) => {
try {
result.should.eql("2013-11-18T19:55:00+08:00");
done()
} catch (error) {
done(error)
}
});
var result = util.evaluateJSONataExpression(expr,{});
result.should.eql('2013-11-18T19:55:00+08:00');
});
it('handles non-existant flow context variable', function() {
var expr = util.prepareJSONataExpression('$flowContext("nonExistant")',{context:function() { return {flow:{get: function(key) { return {'foo':'bar'}[key]}}}}});
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
try {
should.not.exist(result);
done()
} catch (error) {
done(error)
}
});
});
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
should.not.exist(result);
});
it('handles non-existant global context variable', function() {
var expr = util.prepareJSONataExpression('$globalContext("nonExistant")',{context:function() { return {global:{get: function(key) { return {'foo':'bar'}[key]}}}}});
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
try {
should.not.exist(result);
done()
} catch (error) {
done(error)
}
});
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
should.not.exist(result);
});
it('handles async flow context access', function(done) {
var expr = util.prepareJSONataExpression('$flowContext("foo")',{context:function() { return {flow:{get: function(key,store,callback) { setTimeout(()=>{callback(null,{'foo':'bar'}[key])},10)}}}}});