Compare commits
99 Commits
3.1.8
...
4.0.0-beta
Author | SHA1 | Date | |
---|---|---|---|
|
712d78ca39 | ||
|
93f2910bd2 | ||
|
d0ef12c486 | ||
|
241fd09053 | ||
|
208dd2a457 | ||
|
e34ee44b21 | ||
|
d5f59307b7 | ||
|
64136cc565 | ||
|
3e2508c740 | ||
|
0853cd65b2 | ||
|
01802c817b | ||
|
7e10093bb8 | ||
|
54c17c3175 | ||
|
80e60538e2 | ||
|
84a76909e2 | ||
|
033405fdbc | ||
|
9444009a9b | ||
|
29e9def314 | ||
|
8832a1aa20 | ||
|
5beb6dbeee | ||
|
1261d26b23 | ||
|
0b9dd11fff | ||
|
08a607aa6a | ||
|
e12efc320b | ||
|
3ded9de803 | ||
|
d5b424910f | ||
|
d94d13737f | ||
|
b1fa4918e3 | ||
|
742aa2fa0d | ||
|
ce133c1c04 | ||
|
e4dc1779c3 | ||
|
22b4ab6bb2 | ||
|
2dcff51125 | ||
|
b50e0533eb | ||
|
711545539f | ||
|
a6cbceed28 | ||
|
837d17ab65 | ||
|
eff31c4bdc | ||
|
6a8f653b73 | ||
|
0cdb36f73d | ||
|
db249356e6 | ||
|
d509c1a57c | ||
|
6802539ccc | ||
|
74efaa3c2d | ||
|
a5223709ba | ||
|
2291dc6132 | ||
|
b2548c158d | ||
|
5a48d6d4cd | ||
|
7ee2b93b10 | ||
|
cc611a7a02 | ||
|
1a9c34fe40 | ||
|
ff8eb0ec2b | ||
|
f66b48e586 | ||
|
931a2344b4 | ||
|
dd3c75d298 | ||
|
4a4a15de93 | ||
|
a007ab7f2e | ||
|
7b01457038 | ||
|
54e6d60fe5 | ||
|
c2710f4f6f | ||
|
20187b51b1 | ||
|
4be6d57d98 | ||
|
a77f8cc3e9 | ||
|
ea4c0cdbee | ||
|
7197153fd5 | ||
|
b9c1dedab3 | ||
|
918943816f | ||
|
33cf34f7c7 | ||
|
febc769df5 | ||
|
ea483218ea | ||
|
c8f3ad8ac7 | ||
|
7916dc9c05 | ||
|
3123a5ee51 | ||
|
5b5b06cc06 | ||
|
f49f692ffa | ||
|
10ce681d46 | ||
|
08c6ea94cb | ||
|
fea1da5542 | ||
|
32e8f4eac6 | ||
|
bfe5a8a986 | ||
|
f2cb5ea44e | ||
|
c7335ed25b | ||
|
5fda57c730 | ||
|
9fd929ac1e | ||
|
c48a15c915 | ||
|
eb940d6d57 | ||
|
9091935d77 | ||
|
34e8d2b051 | ||
|
0c2ab13c48 | ||
|
9489953a8f | ||
|
b0136d03ea | ||
|
9fe73645ad | ||
|
54d4079457 | ||
|
8e1a21e682 | ||
|
d84cdca43e | ||
|
1c6dcd373d | ||
|
4410ce1486 | ||
|
cef3a01042 | ||
|
0c042abcab |
8
.github/workflows/tests.yml
vendored
@@ -12,12 +12,11 @@ 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: [16, 18, 20]
|
||||
node-version: [18, 20]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
@@ -29,8 +28,3 @@ 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 }}
|
||||
|
31
CHANGELOG.md
@@ -1,3 +1,34 @@
|
||||
#### 4.0.0-beta.1: Beta Release
|
||||
|
||||
Editor
|
||||
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
#### 3.1.7: Maintenance Release
|
||||
|
||||
- Add Japanese translation for v3.1.6 (#4603) @kazuhitoyokoi
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "node-red",
|
||||
"version": "3.1.7",
|
||||
"version": "4.0.0-beta.1",
|
||||
"description": "Low-code programming for event-driven applications",
|
||||
"homepage": "https://nodered.org",
|
||||
"license": "Apache-2.0",
|
||||
@@ -54,7 +54,7 @@
|
||||
"is-utf8": "0.2.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"json-stringify-safe": "5.0.1",
|
||||
"jsonata": "1.8.7",
|
||||
"jsonata": "2.0.4",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"media-typer": "1.1.0",
|
||||
"memorystore": "1.6.7",
|
||||
@@ -122,6 +122,6 @@
|
||||
"supertest": "6.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
@@ -33,6 +33,9 @@ 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) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/editor-api",
|
||||
"version": "3.1.7",
|
||||
"version": "4.0.0-beta.1",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
"repository": {
|
||||
@@ -16,8 +16,8 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@node-red/util": "3.1.7",
|
||||
"@node-red/editor-client": "3.1.7",
|
||||
"@node-red/util": "4.0.0-beta.1",
|
||||
"@node-red/editor-client": "4.0.0-beta.1",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.20.2",
|
||||
"clone": "2.1.2",
|
||||
|
@@ -924,7 +924,14 @@
|
||||
"date": "timestamp",
|
||||
"jsonata": "expression",
|
||||
"env": "env variable",
|
||||
"cred": "credential"
|
||||
"cred": "credential",
|
||||
"conf-types": "config node"
|
||||
},
|
||||
"date": {
|
||||
"format": {
|
||||
"timestamp": "milliseconds since epoch",
|
||||
"object": "JavaScript Date Object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"editableList": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/editor-client",
|
||||
"version": "3.1.7",
|
||||
"version": "4.0.0-beta.1",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@@ -91,6 +91,31 @@ 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++) {
|
||||
|
@@ -174,12 +174,24 @@
|
||||
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());
|
||||
}
|
||||
@@ -189,8 +201,32 @@
|
||||
tolerance: "pointer",
|
||||
forcePlaceholderSize:true,
|
||||
placeholder: "red-ui-editabelList-item-placeholder",
|
||||
start: function(e, ui){
|
||||
ui.placeholder.height(ui.item.height()-4);
|
||||
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?
|
||||
}
|
||||
}
|
||||
};
|
||||
if (this.options.connectWith) {
|
||||
|
@@ -54,25 +54,26 @@
|
||||
return icon;
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
const msgAutoComplete = function(options) {
|
||||
return function(val) {
|
||||
var matches = [];
|
||||
options.forEach(opt => {
|
||||
@@ -102,6 +103,197 @@
|
||||
}
|
||||
}
|
||||
|
||||
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" },
|
||||
@@ -166,20 +358,22 @@
|
||||
{ value: "_session", source: ["websocket out","tcp out"] },
|
||||
]
|
||||
var allOptions = {
|
||||
msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: autoComplete(msgCompletions)},
|
||||
msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: msgAutoComplete(msgCompletions)},
|
||||
flow: {value:"flow",label:"flow.",hasValue:true,
|
||||
options:[],
|
||||
validate:RED.utils.validatePropertyExpression,
|
||||
parse: contextParse,
|
||||
export: contextExport,
|
||||
valueLabel: contextLabel
|
||||
valueLabel: contextLabel,
|
||||
autoComplete: contextAutoComplete
|
||||
},
|
||||
global: {value:"global",label:"global.",hasValue:true,
|
||||
options:[],
|
||||
validate:RED.utils.validatePropertyExpression,
|
||||
parse: contextParse,
|
||||
export: contextExport,
|
||||
valueLabel: contextLabel
|
||||
valueLabel: contextLabel,
|
||||
autoComplete: contextAutoComplete
|
||||
},
|
||||
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) {
|
||||
@@ -214,7 +408,25 @@
|
||||
}
|
||||
},
|
||||
re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.svg"},
|
||||
date: {value:"date",label:"timestamp",icon:"fa fa-clock-o",hasValue:false},
|
||||
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'
|
||||
}
|
||||
]
|
||||
},
|
||||
jsonata: {
|
||||
value: "jsonata",
|
||||
label: "expression",
|
||||
@@ -251,7 +463,8 @@
|
||||
env: {
|
||||
value: "env",
|
||||
label: "env variable",
|
||||
icon: "red/images/typedInput/env.svg"
|
||||
icon: "red/images/typedInput/env.svg",
|
||||
autoComplete: envAutoComplete
|
||||
},
|
||||
node: {
|
||||
value: "node",
|
||||
@@ -383,18 +596,75 @@
|
||||
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<opt.options.length;i++) {
|
||||
op = opt.options[i];
|
||||
for (var i=0;i<_options.length;i++) {
|
||||
op = _options[i];
|
||||
if (typeof op === "string" && op === currentVal) {
|
||||
return {value:currentVal}
|
||||
} else if (op.value === currentVal) {
|
||||
@@ -411,8 +681,8 @@
|
||||
currentValues[v] = true;
|
||||
}
|
||||
});
|
||||
for (var i=0;i<opt.options.length;i++) {
|
||||
op = opt.options[i];
|
||||
for (var i=0;i<_options.length;i++) {
|
||||
op = _options[i];
|
||||
var val = typeof op === "string" ? op : op.value;
|
||||
if (currentValues.hasOwnProperty(val)) {
|
||||
delete currentValues[val];
|
||||
@@ -427,6 +697,7 @@
|
||||
}
|
||||
|
||||
var nlsd = false;
|
||||
let contextStoreOptions;
|
||||
|
||||
$.widget( "nodered.typedInput", {
|
||||
_create: function() {
|
||||
@@ -438,7 +709,7 @@
|
||||
}
|
||||
}
|
||||
var contextStores = RED.settings.context.stores;
|
||||
var contextOptions = contextStores.map(function(store) {
|
||||
contextStoreOptions = 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) {
|
||||
@@ -449,13 +720,17 @@
|
||||
return A.value.localeCompare(B.value);
|
||||
}
|
||||
})
|
||||
if (contextOptions.length < 2) {
|
||||
if (contextStoreOptions.length < 2) {
|
||||
allOptions.flow.options = [];
|
||||
allOptions.global.options = [];
|
||||
} else {
|
||||
allOptions.flow.options = contextOptions;
|
||||
allOptions.global.options = contextOptions;
|
||||
allOptions.flow.options = contextStoreOptions;
|
||||
allOptions.global.options = contextStoreOptions;
|
||||
}
|
||||
// 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;
|
||||
@@ -544,7 +819,7 @@
|
||||
that.element.trigger('paste',evt);
|
||||
});
|
||||
this.input.on('keydown', function(evt) {
|
||||
if (that.typeMap[that.propertyType].autoComplete) {
|
||||
if (that.typeMap[that.propertyType].autoComplete || that.input.hasClass('red-ui-autoComplete')) {
|
||||
return
|
||||
}
|
||||
if (evt.keyCode >= 37 && evt.keyCode <= 40) {
|
||||
@@ -838,7 +1113,9 @@
|
||||
if (this.optionMenu) {
|
||||
this.optionMenu.remove();
|
||||
}
|
||||
this.menu.remove();
|
||||
if (this.menu) {
|
||||
this.menu.remove();
|
||||
}
|
||||
this.uiSelect.remove();
|
||||
},
|
||||
types: function(types) {
|
||||
@@ -871,7 +1148,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);
|
||||
this.type(this.typeList[0]?.value || ""); // permit empty typeList
|
||||
}
|
||||
} else {
|
||||
this.propertyType = null;
|
||||
@@ -908,6 +1185,11 @@
|
||||
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) }
|
||||
@@ -921,8 +1203,8 @@
|
||||
checkValues = valueToCheck.split(",");
|
||||
}
|
||||
checkValues.forEach(function(valueToCheck) {
|
||||
for (var i=0;i<opt.options.length;i++) {
|
||||
var op = opt.options[i];
|
||||
for (var i=0;i<_options.length;i++) {
|
||||
var op = _options[i];
|
||||
if (typeof op === "string") {
|
||||
if (op === valueToCheck || op === ""+valueToCheck) {
|
||||
selectedOption.push(that.activeOptions[op]);
|
||||
@@ -957,7 +1239,7 @@
|
||||
},
|
||||
type: function(type) {
|
||||
if (!arguments.length) {
|
||||
return this.propertyType;
|
||||
return this.propertyType || this.options?.default || '';
|
||||
} else {
|
||||
var that = this;
|
||||
if (this.options.debug) { console.log(this.identifier,"----- SET TYPE -----",type) }
|
||||
@@ -967,6 +1249,9 @@
|
||||
// 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}) }
|
||||
@@ -1013,7 +1298,9 @@
|
||||
this.input.val(this.oldValues.hasOwnProperty("_")?this.oldValues["_"]:(opt.default||""))
|
||||
}
|
||||
if (previousType.autoComplete) {
|
||||
this.input.autoComplete("destroy");
|
||||
if (this.input.hasClass('red-ui-autoComplete')) {
|
||||
this.input.autoComplete("destroy");
|
||||
}
|
||||
}
|
||||
}
|
||||
this.propertyType = type;
|
||||
@@ -1053,6 +1340,10 @@
|
||||
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;
|
||||
@@ -1069,7 +1360,7 @@
|
||||
this.valueLabelContainer.hide();
|
||||
}
|
||||
this.activeOptions = {};
|
||||
opt.options.forEach(function(o) {
|
||||
_options.forEach(function(o) {
|
||||
if (typeof o === 'string') {
|
||||
that.activeOptions[o] = {label:o,value:o};
|
||||
} else {
|
||||
@@ -1089,7 +1380,7 @@
|
||||
if (validValues) {
|
||||
that._updateOptionSelectLabel(validValues)
|
||||
} else {
|
||||
op = opt.options[0];
|
||||
op = _options[0] || {value:""}; // permit zero options
|
||||
if (typeof op === "string") {
|
||||
this.value(op);
|
||||
that._updateOptionSelectLabel({value:op});
|
||||
@@ -1108,7 +1399,7 @@
|
||||
that._updateOptionSelectLabel(validValues);
|
||||
}
|
||||
} else {
|
||||
var selectedOption = this.optionValue||opt.options[0];
|
||||
var selectedOption = this.optionValue||_options[0];
|
||||
if (opt.parse) {
|
||||
var selectedOptionObj = typeof selectedOption === "string"?{value:selectedOption}:selectedOption
|
||||
var parts = opt.parse(this.input.val(),selectedOptionObj);
|
||||
@@ -1141,8 +1432,18 @@
|
||||
} 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(opt.options,opt,function(v){
|
||||
this.optionMenu = this._createMenu(_options,opt,function(v){
|
||||
if (!opt.multiple) {
|
||||
that._updateOptionSelectLabel(that.activeOptions[v]);
|
||||
if (!opt.hasValue) {
|
||||
@@ -1183,8 +1484,12 @@
|
||||
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: opt.autoComplete,
|
||||
search: searchFunction,
|
||||
minLength: 0
|
||||
})
|
||||
}
|
||||
|
@@ -326,47 +326,78 @@ RED.editor = (function() {
|
||||
|
||||
/**
|
||||
* Create a config-node select box for this property
|
||||
* @param node - the node being edited
|
||||
* @param property - the name of the field
|
||||
* @param type - the type of the config-node
|
||||
* @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)
|
||||
*/
|
||||
function prepareConfigNodeSelect(node,property,type,prefix,filter) {
|
||||
var input = $("#"+prefix+"-"+property);
|
||||
if (input.length === 0 ) {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
var newWidth = input.width();
|
||||
var attrStyle = input.attr('style');
|
||||
var m;
|
||||
const attrStyle = input.attr('style');
|
||||
let newWidth;
|
||||
let m;
|
||||
if ((m = /(^|\s|;)width\s*:\s*([^;]+)/i.exec(attrStyle)) !== null) {
|
||||
newWidth = m[2].trim();
|
||||
} else {
|
||||
newWidth = "70%";
|
||||
}
|
||||
var outerWrap = $("<div></div>").css({
|
||||
const outerWrap = $("<div></div>").css({
|
||||
width: newWidth,
|
||||
display:'inline-flex'
|
||||
display: 'inline-flex'
|
||||
});
|
||||
var select = $('<select id="'+prefix+'-'+property+'"></select>').appendTo(outerWrap);
|
||||
const select = $('<select id="' + selectId + '"></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,node[property],prefix,filter);
|
||||
$('<a id="'+prefix+'-lookup-'+property+'" class="red-ui-button"><i class="fa fa-pencil"></i></a>')
|
||||
.css({"margin-left":"10px"})
|
||||
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" })
|
||||
.appendTo(outerWrap);
|
||||
$('#'+prefix+'-lookup-'+property).on("click", function(e) {
|
||||
showEditConfigNodeDialog(property,type,select.find(":selected").val(),prefix,node);
|
||||
|
||||
// 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);
|
||||
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(node[property]);
|
||||
var node_def = RED.nodes.getType(type);
|
||||
var configNode = RED.nodes.node(nodeValue);
|
||||
|
||||
if (configNode) {
|
||||
label = RED.utils.getNodeLabel(configNode,configNode.id);
|
||||
label = RED.utils.getNodeLabel(configNode, configNode.id);
|
||||
}
|
||||
input.val(label);
|
||||
}
|
||||
@@ -768,12 +799,9 @@ RED.editor = (function() {
|
||||
}
|
||||
|
||||
function defaultConfigNodeSort(A,B) {
|
||||
if (A.__label__ < B.__label__) {
|
||||
return -1;
|
||||
} else if (A.__label__ > B.__label__) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
// 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'})
|
||||
}
|
||||
|
||||
function updateConfigNodeSelect(name,type,value,prefix,filter) {
|
||||
@@ -788,7 +816,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();
|
||||
@@ -796,6 +824,7 @@ RED.editor = (function() {
|
||||
var activeWorkspace = RED.nodes.workspace(RED.workspaces.active());
|
||||
if (!activeWorkspace) {
|
||||
activeWorkspace = RED.nodes.subflow(RED.workspaces.active());
|
||||
inclSubflowEnvvars = true
|
||||
}
|
||||
|
||||
var configNodes = [];
|
||||
@@ -811,6 +840,31 @@ 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;
|
||||
@@ -822,7 +876,10 @@ RED.editor = (function() {
|
||||
}
|
||||
|
||||
configNodes.forEach(function(cn) {
|
||||
$('<option value="'+cn.id+'"'+(value==cn.id?" selected":"")+'></option>').text(RED.text.bidi.enforceTextDirectionWithUCC(cn.__label__)).appendTo(select);
|
||||
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)
|
||||
}
|
||||
delete cn.__label__;
|
||||
});
|
||||
|
||||
@@ -1483,9 +1540,16 @@ RED.editor = (function() {
|
||||
}
|
||||
RED.tray.close(function() {
|
||||
var filter = null;
|
||||
if (editContext && typeof editContext._def.defaults[configProperty].filter === 'function') {
|
||||
filter = function(n) {
|
||||
return editContext._def.defaults[configProperty].filter.call(editContext,n);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
updateConfigNodeSelect(configProperty,configType,editing_config_node.id,prefix,filter);
|
||||
@@ -1546,7 +1610,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);
|
||||
}
|
||||
@@ -2087,6 +2151,7 @@ RED.editor = (function() {
|
||||
}
|
||||
},
|
||||
editBuffer: function(options) { showTypeEditor("_buffer", options) },
|
||||
getEditStack: function () { return [...editStack] },
|
||||
buildEditForm: buildEditForm,
|
||||
validateNode: validateNode,
|
||||
updateNodeProperties: updateNodeProperties,
|
||||
@@ -2131,6 +2196,7 @@ RED.editor = (function() {
|
||||
filteredEditPanes[type] = filter
|
||||
}
|
||||
editPanes[type] = definition;
|
||||
}
|
||||
},
|
||||
prepareConfigNodeSelect: prepareConfigNodeSelect,
|
||||
}
|
||||
})();
|
||||
|
@@ -1,8 +1,9 @@
|
||||
RED.editor.envVarList = (function() {
|
||||
|
||||
var currentLocale = 'en-US';
|
||||
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'];
|
||||
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'];
|
||||
|
||||
/**
|
||||
* Create env var edit interface
|
||||
@@ -10,8 +11,8 @@ RED.editor.envVarList = (function() {
|
||||
* @param node - subflow node
|
||||
*/
|
||||
function buildPropertiesList(envContainer, node) {
|
||||
|
||||
var isTemplateNode = (node.type === "subflow");
|
||||
if(RED.editor.envVarList.debug) { console.log('envVarList: buildPropertiesList', envContainer, node) }
|
||||
const isTemplateNode = (node.type === "subflow");
|
||||
|
||||
envContainer
|
||||
.css({
|
||||
@@ -83,7 +84,14 @@ 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 === 'cred') {
|
||||
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') {
|
||||
opt.ui = opt.ui || {
|
||||
icon: "",
|
||||
type: "cred"
|
||||
@@ -119,7 +127,7 @@ RED.editor.envVarList = (function() {
|
||||
}
|
||||
});
|
||||
|
||||
buildEnvEditRow(uiRow, opt.ui, nameField, valueField);
|
||||
buildEnvEditRow(uiRow, opt, nameField, valueField);
|
||||
nameField.trigger('change');
|
||||
}
|
||||
},
|
||||
@@ -181,21 +189,23 @@ RED.editor.envVarList = (function() {
|
||||
* @param nameField - name field of env var
|
||||
* @param valueField - value field of env var
|
||||
*/
|
||||
function buildEnvEditRow(container, ui, nameField, valueField) {
|
||||
function buildEnvEditRow(container, opt, nameField, valueField) {
|
||||
const ui = opt.ui
|
||||
if(RED.editor.envVarList.debug) { console.log('envVarList: 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>').text(RED._("editor.inputType")).appendTo(topRow);
|
||||
$('<div class="red-env-ui-input-type-col">').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},
|
||||
'select': {opts:[]},
|
||||
'spinner': {},
|
||||
'cred': {}
|
||||
'input': {types:DEFAULT_ENV_TYPE_LIST_INC_CONFTYPES},
|
||||
'select': {opts:[]},
|
||||
'spinner': {},
|
||||
'cred': {}
|
||||
};
|
||||
if (ui.opts) {
|
||||
typeOptions[ui.type] = ui.opts;
|
||||
@@ -260,15 +270,16 @@ RED.editor.envVarList = (function() {
|
||||
labelInput.attr("placeholder",$(this).val())
|
||||
});
|
||||
|
||||
var inputCell = $('<div></div>').appendTo(row);
|
||||
var inputCellInput = $('<input type="text">').css("width","100%").appendTo(inputCell);
|
||||
var inputCell = $('<div class="red-env-ui-input-type-col"></div>').appendTo(row);
|
||||
var uiInputTypeInput = $('<input type="text">').css("width","100%").appendTo(inputCell);
|
||||
if (ui.type === "input") {
|
||||
inputCellInput.val(ui.opts.types.join(","));
|
||||
uiInputTypeInput.val(ui.opts.types.join(","));
|
||||
}
|
||||
var checkbox;
|
||||
var selectBox;
|
||||
|
||||
inputCellInput.typedInput({
|
||||
// the options presented in the UI section for an "input" type selection
|
||||
uiInputTypeInput.typedInput({
|
||||
types: [
|
||||
{
|
||||
value:"input",
|
||||
@@ -429,7 +440,7 @@ RED.editor.envVarList = (function() {
|
||||
}
|
||||
});
|
||||
ui.opts.opts = vals;
|
||||
inputCellInput.typedInput('value',Date.now())
|
||||
uiInputTypeInput.typedInput('value',Date.now())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -496,12 +507,13 @@ RED.editor.envVarList = (function() {
|
||||
} else {
|
||||
delete ui.opts.max;
|
||||
}
|
||||
inputCellInput.typedInput('value',Date.now())
|
||||
uiInputTypeInput.typedInput('value',Date.now())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'conf-types',
|
||||
{
|
||||
value:"none",
|
||||
label:RED._("editor.inputs.none"), icon:"fa fa-times",hasValue:false
|
||||
@@ -519,14 +531,20 @@ 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.
|
||||
inputCellInput.typedInput('value',ui.opts.types.join(","))
|
||||
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)
|
||||
} else {
|
||||
// No other type cares about `value`, but doing this will
|
||||
// force a refresh of the label now that `ui.opts` has
|
||||
// been updated.
|
||||
inputCellInput.typedInput('value',Date.now())
|
||||
uiInputTypeInput.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);
|
||||
@@ -544,7 +562,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');
|
||||
@@ -556,8 +574,46 @@ RED.editor.envVarList = (function() {
|
||||
}
|
||||
|
||||
}).on("change", function(evt,type) {
|
||||
if (ui.type === 'input') {
|
||||
var types = inputCellInput.typedInput('value');
|
||||
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');
|
||||
ui.opts.types = (types === "") ? ["str"] : types.split(",");
|
||||
valueField.typedInput('types',ui.opts.types);
|
||||
}
|
||||
@@ -569,7 +625,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
|
||||
inputCellInput.typedInput('type',ui.type)
|
||||
uiInputTypeInput.typedInput('type',ui.type)
|
||||
}
|
||||
|
||||
function setLocale(l, list) {
|
||||
|
@@ -909,17 +909,19 @@ RED.subflow = (function() {
|
||||
|
||||
|
||||
/**
|
||||
* Create interface for controlling env var UI definition
|
||||
* 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
|
||||
*/
|
||||
function buildEnvControl(envList,node) {
|
||||
function buildEnvControl(uiContainer,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 = envList.editableList("items");
|
||||
var list = uiContainer.editableList("items");
|
||||
var exportedEnv = exportEnvList(list, true);
|
||||
buildEnvUI(inputContainer, exportedEnv,node);
|
||||
buildEnvUI(inputContainer, exportedEnv, node);
|
||||
}
|
||||
$("#subflow-env-tabs-content").children().hide();
|
||||
$("#" + tab.id).show();
|
||||
@@ -957,12 +959,33 @@ RED.subflow = (function() {
|
||||
RED.editor.envVarList.setLocale(locale);
|
||||
}
|
||||
|
||||
|
||||
function buildEnvUIRow(row, tenv, ui, node) {
|
||||
/**
|
||||
* 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 || {}
|
||||
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 }
|
||||
@@ -1006,9 +1029,10 @@ 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%').appendTo(row);
|
||||
input = $('<input type="text">').css('width','70%').attr('id', elId).appendTo(row);
|
||||
if (ui.opts.types && ui.opts.types.length > 0) {
|
||||
var inputType = val.type;
|
||||
if (ui.opts.types.indexOf(inputType) === -1) {
|
||||
@@ -1035,7 +1059,7 @@ RED.subflow = (function() {
|
||||
}
|
||||
break;
|
||||
case "select":
|
||||
input = $('<select>').css('width','70%').appendTo(row);
|
||||
input = $('<select>').css('width','70%').attr('id', elId).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);
|
||||
@@ -1046,7 +1070,7 @@ RED.subflow = (function() {
|
||||
case "checkbox":
|
||||
label.css("cursor","default");
|
||||
var cblabel = $('<label>').css('width','70%').appendTo(row);
|
||||
input = $('<input type="checkbox">').css({
|
||||
input = $('<input type="checkbox">').attr('id', elId).css({
|
||||
marginTop: 0,
|
||||
width: 'auto',
|
||||
height: '34px'
|
||||
@@ -1064,7 +1088,7 @@ RED.subflow = (function() {
|
||||
input.prop("checked",boolVal);
|
||||
break;
|
||||
case "spinner":
|
||||
input = $('<input>').css('width','70%').appendTo(row);
|
||||
input = $('<input>').css('width','70%').attr('id', elId).appendTo(row);
|
||||
var spinnerOpts = {};
|
||||
if (ui.opts.hasOwnProperty('min')) {
|
||||
spinnerOpts.min = ui.opts.min;
|
||||
@@ -1093,18 +1117,25 @@ RED.subflow = (function() {
|
||||
default: 'cred'
|
||||
})
|
||||
break;
|
||||
}
|
||||
if (input) {
|
||||
input.attr('id',getSubflowEnvPropertyName(tenv.name))
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create environment variable input UI
|
||||
* Build the edit form for a subflow instance
|
||||
* Also used to build the preview form in the subflow template edit dialog
|
||||
* @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];
|
||||
@@ -1112,7 +1143,7 @@ RED.subflow = (function() {
|
||||
continue;
|
||||
}
|
||||
var row = $("<div/>", { class: "form-row" }).appendTo(uiContainer);
|
||||
buildEnvUIRow(row,tenv, tenv.ui || {}, node);
|
||||
buildEnvUIRow(row, tenv, node);
|
||||
}
|
||||
}
|
||||
// buildEnvUI
|
||||
@@ -1185,6 +1216,9 @@ RED.subflow = (function() {
|
||||
delete ui.opts
|
||||
}
|
||||
break;
|
||||
case "conf-types":
|
||||
delete ui.opts;
|
||||
break;
|
||||
default:
|
||||
delete ui.opts;
|
||||
}
|
||||
@@ -1207,8 +1241,9 @@ 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) {
|
||||
subflowDef.env.forEach(function(env, i) {
|
||||
var item = {
|
||||
index: i,
|
||||
name:env.name,
|
||||
parent: {
|
||||
type: env.type,
|
||||
@@ -1273,6 +1308,7 @@ 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
|
||||
@@ -1319,6 +1355,9 @@ 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);
|
||||
@@ -1332,8 +1371,15 @@ RED.subflow = (function() {
|
||||
return 'node-input-subflow-env-'+name.replace(/[^a-z0-9-_]/ig,"_");
|
||||
}
|
||||
|
||||
// Called by subflow.oneditprepare for both instances and templates
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
|
@@ -435,10 +435,15 @@ RED.tourGuide = (function() {
|
||||
|
||||
function listTour() {
|
||||
return [
|
||||
{
|
||||
id: "4_0",
|
||||
label: "4.0",
|
||||
path: "./tours/welcome.js"
|
||||
},
|
||||
{
|
||||
id: "3_1",
|
||||
label: "3.1",
|
||||
path: "./tours/welcome.js"
|
||||
path: "./tours/3.1/welcome.js"
|
||||
},
|
||||
{
|
||||
id: "3_0",
|
||||
|
@@ -483,6 +483,16 @@ 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);
|
||||
|
||||
@@ -589,6 +599,7 @@ 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);
|
||||
}
|
||||
@@ -619,6 +630,7 @@ 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);
|
||||
}
|
||||
@@ -675,6 +687,7 @@ 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);
|
||||
}
|
||||
|
@@ -6252,6 +6252,10 @@ RED.view = (function() {
|
||||
}
|
||||
})
|
||||
}
|
||||
if (selection.links) {
|
||||
selectedLinks.clear();
|
||||
selection.links.forEach(selectedLinks.add);
|
||||
}
|
||||
}
|
||||
}
|
||||
updateSelection();
|
||||
|
@@ -38,7 +38,7 @@ body {
|
||||
}
|
||||
#red-ui-main-container {
|
||||
position: absolute;
|
||||
top:40px; left:0; bottom: 0; right:0;
|
||||
top: var(--red-ui-header-height); left:0; bottom: 0; right:0;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
|
@@ -259,7 +259,8 @@ $deploy-button-background-disabled-hover: #555;
|
||||
|
||||
$header-background: #000;
|
||||
$header-button-background-active: #121212;
|
||||
$header-menu-color: #C7C7C7;
|
||||
$header-accent: #d41313;
|
||||
$header-menu-color: #eee;
|
||||
$header-menu-color-disabled: #666;
|
||||
$header-menu-heading-color: #fff;
|
||||
$header-menu-sublabel-color: #aeaeae;
|
||||
|
@@ -23,16 +23,20 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
height: var(--red-ui-header-height);
|
||||
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;
|
||||
@@ -42,7 +46,7 @@
|
||||
vertical-align: middle;
|
||||
font-size: 16px !important;
|
||||
&:not(:first-child) {
|
||||
margin-left: 5px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
img {
|
||||
|
17
packages/node_modules/@node-red/editor-client/src/sass/sizes.scss
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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;
|
@@ -15,4 +15,5 @@
|
||||
**/
|
||||
|
||||
@import "colors";
|
||||
@import "sizes";
|
||||
@import "variables";
|
@@ -15,6 +15,7 @@
|
||||
**/
|
||||
|
||||
@import "colors";
|
||||
@import "sizes";
|
||||
@import "variables";
|
||||
@import "mixins";
|
||||
|
||||
|
@@ -16,6 +16,9 @@
|
||||
|
||||
--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
|
||||
@@ -240,6 +243,7 @@
|
||||
|
||||
|
||||
--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};
|
||||
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 189 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
231
packages/node_modules/@node-red/editor-client/src/tours/3.1/welcome.js
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
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>`
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
BIN
packages/node_modules/@node-red/editor-client/src/tours/images/nr4-auto-complete.png
vendored
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
packages/node_modules/@node-red/editor-client/src/tours/images/nr4-sf-config.png
vendored
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
packages/node_modules/@node-red/editor-client/src/tours/images/nr4-timestamp-formatting.png
vendored
Normal file
After Width: | Height: | Size: 15 KiB |
@@ -1,12 +1,12 @@
|
||||
export default {
|
||||
version: "3.1.0",
|
||||
version: "4.0.0-beta.1",
|
||||
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!"
|
||||
"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!"
|
||||
},
|
||||
description: {
|
||||
"en-US": "<p>Let's take a moment to discover the new features in this release.</p>",
|
||||
@@ -16,202 +16,49 @@ export default {
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "New ways to work with groups",
|
||||
"ja": "グループの新たな操作方法",
|
||||
"fr": "De nouvelles façons de travailler avec les groupes"
|
||||
"en-US": "Timestamp formatting options",
|
||||
// "ja": ""
|
||||
},
|
||||
image: 'images/nr4-timestamp-formatting.png',
|
||||
description: {
|
||||
"en-US": `<p>We have changed how you interact with groups in the editor.</p>
|
||||
"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>
|
||||
<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>
|
||||
<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>
|
||||
</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>`
|
||||
// "ja": ``
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Change notification on tabs",
|
||||
"ja": "タブ上の変更通知",
|
||||
"fr": "Notification de changement sur les onglets"
|
||||
"en-US": "Auto-complete of flow/global and env types",
|
||||
// "ja": ""
|
||||
},
|
||||
image: 'images/tab-changes.png',
|
||||
image: 'images/nr4-auto-complete.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>`
|
||||
"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": ``
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "A bigger canvas to work with",
|
||||
"ja": "より広くなった作業キャンバス",
|
||||
"fr": "Un canevas plus grand pour travailler"
|
||||
"en-US": "Config node customisation in Subflows",
|
||||
// "ja": ""
|
||||
},
|
||||
image: 'images/nr4-sf-config.png',
|
||||
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>`
|
||||
"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": ``
|
||||
}
|
||||
},
|
||||
{
|
||||
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",
|
||||
@@ -221,10 +68,13 @@ 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>`,
|
||||
"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>`
|
||||
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>`
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -164,7 +165,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
}
|
||||
|
||||
if (st.length > 32) { st = st.substr(0,32) + "..."; }
|
||||
if (st.length > statuslength) { st = st.substr(0,statuslength) + "..."; }
|
||||
|
||||
var newStatus = {fill:fill, shape:shape, text:st};
|
||||
if (JSON.stringify(newStatus) !== node.oldState) { // only send if we have to
|
||||
|
@@ -512,7 +512,8 @@ RED.debug = (function() {
|
||||
hideKey: false,
|
||||
path: path,
|
||||
sourceId: sourceNode&&sourceNode.id,
|
||||
rootPath: path
|
||||
rootPath: path,
|
||||
nodeSelector: config.messageSourceClick,
|
||||
});
|
||||
// Do this in a separate step so the element functions aren't stripped
|
||||
debugMessage.appendTo(el);
|
||||
|
@@ -117,7 +117,7 @@ module.exports = function(RED) {
|
||||
});
|
||||
return
|
||||
} else if (rule.tot === 'date') {
|
||||
value = Date.now();
|
||||
value = RED.util.evaluateNodeProperty(rule.to, rule.tot, node)
|
||||
} else if (rule.tot === 'jsonata') {
|
||||
RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => {
|
||||
if (err) {
|
||||
@@ -233,7 +233,9 @@ module.exports = function(RED) {
|
||||
// only replace if they match exactly
|
||||
RED.util.setMessageProperty(msg,property,value);
|
||||
} else {
|
||||
current = current.replace(fromRE,value);
|
||||
// if target is boolean then just replace it
|
||||
if (rule.tot === "bool") { current = value; }
|
||||
else { current = current.replace(fromRE,value); }
|
||||
RED.util.setMessageProperty(msg,property,current);
|
||||
}
|
||||
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {
|
||||
|
@@ -40,6 +40,99 @@
|
||||
|
||||
(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() {
|
||||
@@ -192,7 +285,8 @@
|
||||
value: "",
|
||||
label:RED._("node-red:websocket.sendheartbeat"),
|
||||
validate: RED.validators.number(/*blank allowed*/true) },
|
||||
subprotocol: {value:"",required: false}
|
||||
subprotocol: {value:"",required: false},
|
||||
headers: { value: [] }
|
||||
},
|
||||
inputs:0,
|
||||
outputs:0,
|
||||
@@ -200,6 +294,9 @@
|
||||
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()))
|
||||
});
|
||||
@@ -214,14 +311,114 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -299,8 +496,15 @@
|
||||
<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>
|
||||
<span data-i18n="[html]websocket.tip.url2"></span>
|
||||
<p><span data-i18n="[html]websocket.tip.url2"></span></p>
|
||||
<span data-i18n="[html]websocket.tip.headers"></span>
|
||||
</div>
|
||||
</script>
|
||||
|
@@ -58,6 +58,7 @@ 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);
|
||||
@@ -96,6 +97,42 @@ 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
|
||||
|
@@ -411,23 +411,33 @@ module.exports = function(RED) {
|
||||
if (msg._session && msg._session.type == "tcp") {
|
||||
var client = connectionPool[msg._session.id];
|
||||
if (client) {
|
||||
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 (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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (var i in connectionPool) {
|
||||
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 (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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -547,13 +557,34 @@ module.exports = function(RED) {
|
||||
|
||||
this.on("input", function(msg, nodeSend, nodeDone) {
|
||||
var i = 0;
|
||||
if ((!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) {
|
||||
if (msg.payload !== undefined && (!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
|
||||
@@ -621,13 +652,16 @@ 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:"common.status.connected"});
|
||||
node.status({fill:"green",shape:"dot",text:RED._("tcpin.status.connections",{count:Object.keys(clients).length})});
|
||||
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)) {
|
||||
clients[connection_id].client.write(event.msg.payload);
|
||||
if (event.msg.payload !== undefined) {
|
||||
clients[connection_id].client.write(event.msg.payload);
|
||||
}
|
||||
event.nodeDone();
|
||||
}
|
||||
if (node.out === "time" && node.splitc < 0) {
|
||||
@@ -823,7 +857,9 @@ 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)
|
||||
clients[connection_id].client.write(event.msg.payload);
|
||||
if (event.msg.payload !== undefined ) {
|
||||
clients[connection_id].client.write(event.msg.payload);
|
||||
}
|
||||
event.nodeDone();
|
||||
}
|
||||
}
|
||||
|
@@ -17,7 +17,20 @@
|
||||
</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">
|
||||
@@ -60,10 +73,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:150px;" id="node-input-ret">
|
||||
<select style="width:calc(70% - 108px);" id="node-input-ret">
|
||||
<option value='\r\n' data-i18n="csv.newline.windows"></option>
|
||||
<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>
|
||||
@@ -75,6 +88,7 @@
|
||||
color:"#DEBD5C",
|
||||
defaults: {
|
||||
name: {value:""},
|
||||
spec: {value:"rfc"},
|
||||
sep: {
|
||||
value:',', required:true,
|
||||
label:RED._("node-red:csv.label.separator"),
|
||||
@@ -83,7 +97,7 @@
|
||||
hdrin: {value:""},
|
||||
hdrout: {value:"none"},
|
||||
multi: {value:"one",required:true},
|
||||
ret: {value:'\\n'},
|
||||
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)")
|
||||
temp: {value:""},
|
||||
skip: {value:"0"},
|
||||
strings: {value:true},
|
||||
@@ -123,6 +137,27 @@
|
||||
$("#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>
|
||||
|
@@ -15,322 +15,674 @@
|
||||
**/
|
||||
|
||||
module.exports = function(RED) {
|
||||
const csv = require('./lib/csv')
|
||||
|
||||
"use strict";
|
||||
function CSVNode(n) {
|
||||
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');
|
||||
RED.nodes.createNode(this,n)
|
||||
const node = this
|
||||
const RFC4180Mode = n.spec === 'rfc'
|
||||
const legacyMode = !RFC4180Mode
|
||||
|
||||
// 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;
|
||||
node.status({}) // clear status
|
||||
|
||||
this.on("input", function(msg, send, done) {
|
||||
if (msg.hasOwnProperty("reset")) {
|
||||
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;
|
||||
}
|
||||
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]);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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 || "",",");
|
||||
}
|
||||
const ou = [];
|
||||
if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; }
|
||||
if (node.hdrout !== "none" && node.hdrSent === false) {
|
||||
if ((template.length === 1) && (template[0] === '')) {
|
||||
/* istanbul ignore else */
|
||||
if (tmpwarn === true) { // just warn about missing template once
|
||||
node.warn(RED._("csv.errors.obj_csv"));
|
||||
tmpwarn = false;
|
||||
if (msg.hasOwnProperty("columns")) {
|
||||
template = clean(msg.columns || "",",");
|
||||
}
|
||||
const row = [];
|
||||
for (var p in msg.payload[0]) {
|
||||
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] === '')) {
|
||||
/* istanbul ignore else */
|
||||
if (msg.payload[s].hasOwnProperty(p)) {
|
||||
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]) {
|
||||
/* 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 (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 (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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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); }
|
||||
}
|
||||
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;
|
||||
}
|
||||
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 {
|
||||
const row = [];
|
||||
for (var t=0; t < template.length; t++) {
|
||||
if (template[t] === '') {
|
||||
row.push('');
|
||||
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 {
|
||||
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
|
||||
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];
|
||||
}
|
||||
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];
|
||||
}
|
||||
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); }
|
||||
}
|
||||
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; }
|
||||
}
|
||||
// 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); }
|
||||
|
||||
// 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 ( 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 ((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]; }
|
||||
|
||||
if (JSON.stringify(o) !== "{}") { // don't send empty objects
|
||||
a.push(o); // add to the array
|
||||
}
|
||||
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];
|
||||
}
|
||||
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];
|
||||
}
|
||||
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); }
|
||||
|
||||
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 (node.multi !== "one") {
|
||||
msg.payload = a;
|
||||
if (has_parts && nocr <= 1) {
|
||||
if (JSON.stringify(o) !== "{}") {
|
||||
node.store.push(o);
|
||||
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 (msg.parts.index + 1 === msg.parts.count) {
|
||||
msg.payload = node.store;
|
||||
else {
|
||||
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
|
||||
delete msg.parts;
|
||||
send(msg);
|
||||
node.store = [];
|
||||
send(msg); // finally send the array
|
||||
}
|
||||
}
|
||||
else {
|
||||
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
|
||||
send(msg); // finally send the array
|
||||
}
|
||||
}
|
||||
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
|
||||
};
|
||||
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);
|
||||
}
|
||||
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 (has_parts && last && len === 0) {
|
||||
send({complete:true});
|
||||
}
|
||||
}
|
||||
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]) || ['']
|
||||
}
|
||||
}
|
||||
if (last) { newMessage.complete = true; }
|
||||
send(newMessage);
|
||||
stringBuilder.push(templateArrayToColumnString(template, true))
|
||||
if (sendHeadersOnce) { node.hdrSent = true }
|
||||
}
|
||||
if (has_parts && last && len === 0) {
|
||||
send({complete: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)
|
||||
}
|
||||
node.linecount = 0;
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
node.linecount = 0
|
||||
done()
|
||||
}
|
||||
catch (e) {
|
||||
done(e)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
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.
|
||||
else {
|
||||
if (!msg.hasOwnProperty("reset")) {
|
||||
node.send(msg); // If no payload and not reset - just pass it on.
|
||||
}
|
||||
done()
|
||||
}
|
||||
done();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType("csv",CSVNode);
|
||||
|
||||
RED.nodes.registerType("csv",CSVNode)
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@
|
||||
<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>
|
||||
@@ -28,6 +29,10 @@
|
||||
<label for="node-input-outproperty"> </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>
|
||||
@@ -45,7 +50,8 @@
|
||||
outproperty: {value:"payload", validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true }) },
|
||||
tag: {value:""},
|
||||
ret: {value:"html"},
|
||||
as: {value:"single"}
|
||||
as: {value:"single"},
|
||||
chr: { value: "_" }
|
||||
},
|
||||
inputs:1,
|
||||
outputs:1,
|
||||
@@ -59,6 +65,13 @@
|
||||
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>
|
||||
|
@@ -25,6 +25,7 @@ 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);
|
||||
@@ -47,6 +48,11 @@ 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) {
|
||||
@@ -69,6 +75,11 @@ 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++;
|
||||
|
324
packages/node_modules/@node-red/nodes/core/parsers/lib/csv/index.js
vendored
Normal file
@@ -0,0 +1,324 @@
|
||||
|
||||
/**
|
||||
* @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
|
@@ -15,7 +15,11 @@
|
||||
-->
|
||||
|
||||
<script type="text/html" data-template-name="split">
|
||||
<div class="form-row"><span data-i18n="[html]split.intro"></span></div>
|
||||
<!-- <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.strBuff"></span></div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-splt" style="padding-left:10px; margin-right:-10px;" data-i18n="split.splitUsing"></label>
|
||||
@@ -39,10 +43,9 @@
|
||||
<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="common.label.name"></span></label>
|
||||
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
|
||||
<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">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
@@ -57,7 +60,8 @@
|
||||
arraySplt: {value:1},
|
||||
arraySpltType: {value:"len"},
|
||||
stream: {value:false},
|
||||
addname: {value:"", validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })}
|
||||
addname: {value:"", validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })},
|
||||
property: {value:"payload",required:true}
|
||||
},
|
||||
inputs:1,
|
||||
outputs:1,
|
||||
@@ -69,6 +73,10 @@
|
||||
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"),
|
||||
|
@@ -19,13 +19,13 @@ module.exports = function(RED) {
|
||||
|
||||
function sendArray(node,msg,array,send) {
|
||||
for (var i = 0; i < array.length-1; i++) {
|
||||
msg.payload = array[i];
|
||||
RED.util.setMessageProperty(msg,node.property,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) {
|
||||
msg.payload = array[i];
|
||||
RED.util.setMessageProperty(msg,node.property,array[i]);
|
||||
msg.parts.index = node.c++;
|
||||
msg.parts.count = array.length;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
@@ -40,10 +40,12 @@ 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);
|
||||
@@ -51,7 +53,8 @@ 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);
|
||||
@@ -69,18 +72,22 @@ module.exports = function(RED) {
|
||||
node.buffer = Buffer.from([]);
|
||||
node.pendingDones = [];
|
||||
this.on("input", function(msg, send, done) {
|
||||
if (msg.hasOwnProperty("payload")) {
|
||||
var value = RED.util.getMessageProperty(msg,node.property);
|
||||
if (value !== undefined) {
|
||||
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 msg.payload === "string") { // Split String into array
|
||||
msg.payload = (node.remainder || "") + msg.payload;
|
||||
if (typeof value === "string") { // Split String into array
|
||||
value = (node.remainder || "") + value;
|
||||
msg.parts.type = "string";
|
||||
if (node.spltType === "len") {
|
||||
msg.parts.ch = "";
|
||||
msg.parts.len = node.splt;
|
||||
var count = msg.payload.length/node.splt;
|
||||
var count = value.length/node.splt;
|
||||
if (Math.floor(count) !== count) {
|
||||
count = Math.ceil(count);
|
||||
}
|
||||
@@ -89,9 +96,9 @@ module.exports = function(RED) {
|
||||
node.c = 0;
|
||||
}
|
||||
var pos = 0;
|
||||
var data = msg.payload;
|
||||
var data = value;
|
||||
for (var i=0; i<count-1; i++) {
|
||||
msg.payload = data.substring(pos,pos+node.splt);
|
||||
RED.util.setMessageProperty(msg,node.property,data.substring(pos,pos+node.splt));
|
||||
msg.parts.index = node.c++;
|
||||
pos += node.splt;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
@@ -102,7 +109,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
node.remainder = data.substring(pos);
|
||||
if ((node.stream !== true) || (node.remainder.length === node.splt)) {
|
||||
msg.payload = node.remainder;
|
||||
RED.util.setMessageProperty(msg,node.property,node.remainder);
|
||||
msg.parts.index = node.c++;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
node.pendingDones.forEach(d => d());
|
||||
@@ -119,47 +126,48 @@ module.exports = function(RED) {
|
||||
if (!node.spltBufferString) {
|
||||
node.spltBufferString = node.splt.toString();
|
||||
}
|
||||
a = msg.payload.split(node.spltBufferString);
|
||||
a = value.split(node.spltBufferString);
|
||||
msg.parts.ch = node.spltBuffer; // pass the split char to other end for rejoin
|
||||
} else if (node.spltType === "str") {
|
||||
a = msg.payload.split(node.splt);
|
||||
a = value.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(msg.payload)) { // then split array into messages
|
||||
else if (Array.isArray(value)) { // then split array into messages
|
||||
msg.parts.type = "array";
|
||||
var count = msg.payload.length/node.arraySplt;
|
||||
var count = value.length/node.arraySplt;
|
||||
if (Math.floor(count) !== count) {
|
||||
count = Math.ceil(count);
|
||||
}
|
||||
msg.parts.count = count;
|
||||
var pos = 0;
|
||||
var data = msg.payload;
|
||||
var data = value;
|
||||
msg.parts.len = node.arraySplt;
|
||||
for (var i=0; i<count; i++) {
|
||||
msg.payload = data.slice(pos,pos+node.arraySplt);
|
||||
var m = data.slice(pos,pos+node.arraySplt);
|
||||
if (node.arraySplt === 1) {
|
||||
msg.payload = msg.payload[0];
|
||||
m = m[0];
|
||||
}
|
||||
RED.util.setMessageProperty(msg,node.property,m);
|
||||
msg.parts.index = i;
|
||||
pos += node.arraySplt;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
}
|
||||
done();
|
||||
}
|
||||
else if ((typeof msg.payload === "object") && !Buffer.isBuffer(msg.payload)) {
|
||||
else if ((typeof value === "object") && !Buffer.isBuffer(value)) {
|
||||
var j = 0;
|
||||
var l = Object.keys(msg.payload).length;
|
||||
var pay = msg.payload;
|
||||
var l = Object.keys(value).length;
|
||||
var pay = value;
|
||||
msg.parts.type = "object";
|
||||
for (var p in pay) {
|
||||
if (pay.hasOwnProperty(p)) {
|
||||
msg.payload = pay[p];
|
||||
RED.util.setMessageProperty(msg,node.property,pay[p]);
|
||||
if (node.addname !== "") {
|
||||
msg[node.addname] = p;
|
||||
RED.util.setMessageProperty(msg,node.addname,p);
|
||||
}
|
||||
msg.parts.key = p;
|
||||
msg.parts.index = j;
|
||||
@@ -170,9 +178,9 @@ module.exports = function(RED) {
|
||||
}
|
||||
done();
|
||||
}
|
||||
else if (Buffer.isBuffer(msg.payload)) {
|
||||
var len = node.buffer.length + msg.payload.length;
|
||||
var buff = Buffer.concat([node.buffer, msg.payload], len);
|
||||
else if (Buffer.isBuffer(value)) {
|
||||
var len = node.buffer.length + value.length;
|
||||
var buff = Buffer.concat([node.buffer, value], len);
|
||||
msg.parts.type = "buffer";
|
||||
if (node.spltType === "len") {
|
||||
var count = buff.length/node.splt;
|
||||
@@ -186,7 +194,7 @@ module.exports = function(RED) {
|
||||
var pos = 0;
|
||||
msg.parts.len = node.splt;
|
||||
for (var i=0; i<count-1; i++) {
|
||||
msg.payload = buff.slice(pos,pos+node.splt);
|
||||
RED.util.setMessageProperty(msg,node.property,buff.slice(pos,pos+node.splt));
|
||||
msg.parts.index = node.c++;
|
||||
pos += node.splt;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
@@ -197,7 +205,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
node.buffer = buff.slice(pos);
|
||||
if ((node.stream !== true) || (node.buffer.length === node.splt)) {
|
||||
msg.payload = node.buffer;
|
||||
RED.util.setMessageProperty(msg,node.property,node.buffer);
|
||||
msg.parts.index = node.c++;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
node.pendingDones.forEach(d => d());
|
||||
@@ -230,7 +238,7 @@ module.exports = function(RED) {
|
||||
var i = 0, p = 0;
|
||||
pos = buff.indexOf(node.splt);
|
||||
while (pos > -1) {
|
||||
msg.payload = buff.slice(p,pos);
|
||||
RED.util.setMessageProperty(msg,node.property,buff.slice(p,pos));
|
||||
msg.parts.index = node.c++;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
i++;
|
||||
@@ -242,7 +250,7 @@ module.exports = function(RED) {
|
||||
node.pendingDones = [];
|
||||
}
|
||||
if ((node.stream !== true) && (p < buff.length)) {
|
||||
msg.payload = buff.slice(p,buff.length);
|
||||
RED.util.setMessageProperty(msg,node.property,buff.slice(p,buff.length));
|
||||
msg.parts.index = node.c++;
|
||||
msg.parts.count = node.c++;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
@@ -298,7 +306,6 @@ 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);
|
||||
@@ -330,6 +337,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reduceAndSendGroup(node, group, done) {
|
||||
var is_right = node.reduce_right;
|
||||
var flag = is_right ? -1 : 1;
|
||||
@@ -515,13 +523,13 @@ module.exports = function(RED) {
|
||||
if (typeof group.joinChar !== 'string') {
|
||||
groupJoinChar = group.joinChar.toString();
|
||||
}
|
||||
RED.util.setMessageProperty(group.msg,node.property,group.payload.join(groupJoinChar));
|
||||
RED.util.setMessageProperty(group.msg,group?.prop||"payload",group.payload.join(groupJoinChar));
|
||||
}
|
||||
else {
|
||||
if (node.propertyType === 'full') {
|
||||
group.msg = RED.util.cloneMessage(group.msg);
|
||||
}
|
||||
RED.util.setMessageProperty(group.msg,node.property,group.payload);
|
||||
RED.util.setMessageProperty(group.msg,group?.prop||"payload",group.payload);
|
||||
}
|
||||
if (group.msg.hasOwnProperty('parts') && group.msg.parts.hasOwnProperty('parts')) {
|
||||
group.msg.parts = group.msg.parts.parts;
|
||||
@@ -589,7 +597,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
|
||||
if (node.mode === 'auto' && (!msg.hasOwnProperty("parts")||!msg.parts.hasOwnProperty("id"))) {
|
||||
// if a blank reset messag erest it all.
|
||||
// if a blank reset message reset it all.
|
||||
if (msg.hasOwnProperty("reset")) {
|
||||
if (inflight && inflight.hasOwnProperty("partId") && inflight[partId].timeout) {
|
||||
clearTimeout(inflight[partId].timeout);
|
||||
@@ -603,6 +611,15 @@ 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;
|
||||
@@ -618,6 +635,7 @@ 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});
|
||||
@@ -719,6 +737,8 @@ 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);
|
||||
|
||||
|
@@ -516,7 +516,8 @@
|
||||
"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:// oder wss:// 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."
|
||||
"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."
|
||||
},
|
||||
"status": {
|
||||
"connected": "Verbunden __count__",
|
||||
|
@@ -586,7 +586,8 @@
|
||||
"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:// or wss:// 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."
|
||||
"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."
|
||||
},
|
||||
"status": {
|
||||
"connected": "connected __count__",
|
||||
@@ -849,7 +850,13 @@
|
||||
"newline": "Newline",
|
||||
"usestrings": "parse numerical values",
|
||||
"include_empty_strings": "include empty strings",
|
||||
"include_null_values": "include null values"
|
||||
"include_null_values": "include null values",
|
||||
"spec": "Parser"
|
||||
},
|
||||
"spec": {
|
||||
"rfc": "RFC4180",
|
||||
"legacy": "Legacy",
|
||||
"legacy_warning": "Legacy mode will be removed in a future release."
|
||||
},
|
||||
"placeholder": {
|
||||
"columns": "comma-separated column names"
|
||||
@@ -878,6 +885,7 @@
|
||||
"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."
|
||||
@@ -887,12 +895,14 @@
|
||||
"label": {
|
||||
"select": "Selector",
|
||||
"output": "Output",
|
||||
"in": "in"
|
||||
"in": "in",
|
||||
"prefix": "Property name for HTML content"
|
||||
},
|
||||
"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"
|
||||
"attr": "an object of any attributes of the elements",
|
||||
"compl": "an object of any attributes of the elements and html contents"
|
||||
},
|
||||
"format": {
|
||||
"single": "as a single message containing an array",
|
||||
@@ -1001,7 +1011,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",
|
||||
|
@@ -30,6 +30,8 @@
|
||||
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>
|
||||
@@ -40,6 +42,8 @@
|
||||
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>
|
||||
|
@@ -36,7 +36,9 @@
|
||||
</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>
|
||||
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>
|
||||
<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
|
||||
@@ -49,4 +51,5 @@
|
||||
<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>
|
||||
|
@@ -583,7 +583,8 @@
|
||||
"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:// ou wss:// 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."
|
||||
"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."
|
||||
},
|
||||
"status": {
|
||||
"connected": "__count__ connecté",
|
||||
|
@@ -586,7 +586,8 @@
|
||||
"path1": "標準では <code>payload</code> がwebsocketから送信、受信されるデータを持ちます。クライアントはJSON形式の文字列としてメッセージ全体を送信、受信するよう設定できます。",
|
||||
"path2": "このパスは <code>__path__</code> の相対パスになります。",
|
||||
"url1": "URLには ws:// または wss:// スキーマを使用して、存在するwebsocketリスナを設定してください。",
|
||||
"url2": "標準では <code>payload</code> がwebsocketから送信、受信されるデータを持ちます。クライアントはJSON形式の文字列としてメッセージ全体を送信、受信するよう設定できます。"
|
||||
"url2": "標準では <code>payload</code> がwebsocketから送信、受信されるデータを持ちます。クライアントはJSON形式の文字列としてメッセージ全体を送信、受信するよう設定できます。",
|
||||
"headers": "ヘッダーは、HTTP から WS/WSS プロトコルへのプロトコル アップグレード メカニズム中にのみ送信されます。"
|
||||
},
|
||||
"status": {
|
||||
"connected": "接続数 __count__",
|
||||
|
@@ -451,7 +451,8 @@
|
||||
"path1": "표준으로는 <code>payload</code> 가 websocket에서 송신, 수신된 데이터를 기다립니다. 클라이언트는 JSON형식의 문자열로 메세지전체를 송신, 수신하도록 설정할 수 있습니다.",
|
||||
"path2": "This path will be relative to <code>__path__</code>.",
|
||||
"url1": "URL에는 ws:// 또는 wss:// 스키마를 사용하여, 존재하는 websocket리스너를 설정해 주세요.",
|
||||
"url2": "표준으로는 <code>payload</code> 가 websocket에서 송신,수신될 데이터를 기다립니다.클라이언트는 JSON형식의 문자열로 메세지전체를 송신, 수신하도록 설정할 수 있습니다."
|
||||
"url2": "표준으로는 <code>payload</code> 가 websocket에서 송신,수신될 데이터를 기다립니다.클라이언트는 JSON형식의 문자열로 메세지전체를 송신, 수신하도록 설정할 수 있습니다.",
|
||||
"headers": "헤더는 HTTP에서 WS/WSS 프로토콜로 프로토콜 업그레이드 메커니즘 중에만 제출됩니다."
|
||||
},
|
||||
"status": {
|
||||
"connected": "접속 수 __count__",
|
||||
|
@@ -573,7 +573,8 @@
|
||||
"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:// ou wss:// 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."
|
||||
"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."
|
||||
},
|
||||
"status": {
|
||||
"connected": "conectado __count__",
|
||||
|
@@ -475,7 +475,8 @@
|
||||
"path1": "По умолчанию <code>payload</code> будет содержать данные, которые будут отправлены или получены из websocket. Слушатель может быть настроен на отправку или получение всего объекта сообщения в виде строки в формате JSON.",
|
||||
"path2": "Путь будет относительно <code>__path__</code>.",
|
||||
"url1": "URL должен использовать схему ws:// или wss:// и указывать на существующего слушателя websocket.",
|
||||
"url2": "По умолчанию <code>payload</code> будет содержать данные, которые будут отправлены или получены из websocket. Клиент может быть настроен на отправку или получение всего объекта сообщения в виде строки в формате JSON."
|
||||
"url2": "По умолчанию <code>payload</code> будет содержать данные, которые будут отправлены или получены из websocket. Клиент может быть настроен на отправку или получение всего объекта сообщения в виде строки в формате JSON.",
|
||||
"headers": "Заголовки передаются только во время механизма обновления протокола с HTTP на протокол WS/WSS."
|
||||
},
|
||||
"status": {
|
||||
"connected": "подключен __count__",
|
||||
|
@@ -576,7 +576,8 @@
|
||||
"path1": "默认情况下,<code>payload</code>将包含要发送或从Websocket接收的数据。侦听器可以配置为以JSON格式的字符串发送或接收整个消息对象.",
|
||||
"path2": "这条路径将相对于 <code>__path__</code>.",
|
||||
"url1": "URL 应该使用ws://或者wss://方案并指向现有的websocket侦听器.",
|
||||
"url2": "默认情况下,<code>payload</code> 将包含要发送或从Websocket接收的数据。可以将客户端配置为以JSON格式的字符串发送或接收整个消息对象."
|
||||
"url2": "默认情况下,<code>payload</code> 将包含要发送或从Websocket接收的数据。可以将客户端配置为以JSON格式的字符串发送或接收整个消息对象.",
|
||||
"headers": "标头仅在协议升级机制期间提交,从 HTTP 到 WS/WSS 协议."
|
||||
},
|
||||
"status": {
|
||||
"connected": "已连接数量 __count__",
|
||||
|
@@ -471,7 +471,8 @@
|
||||
"path1": "預設情況下,<code>payload</code>將包含要發送或從Websocket接收的資料。偵聽器可以配置為以JSON格式的字串發送或接收整個消息物件.",
|
||||
"path2": "這條路徑將相對於 <code>__path__</code>.",
|
||||
"url1": "URL 應該使用ws://或者wss://方案並指向現有的websocket監聽器.",
|
||||
"url2": "預設情況下,<code>payload</code> 將包含要發送或從Websocket接收的資料。可以將使用者端配置為以JSON格式的字串發送或接收整個消息物件."
|
||||
"url2": "預設情況下,<code>payload</code> 將包含要發送或從Websocket接收的資料。可以將使用者端配置為以JSON格式的字串發送或接收整個消息物件.",
|
||||
"headers": "標頭僅在協定升級機制期間提交,從 HTTP 到 WS/WSS 協定."
|
||||
},
|
||||
"status": {
|
||||
"connected": "連接數 __count__",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/nodes",
|
||||
"version": "3.1.7",
|
||||
"version": "4.0.0-beta.1",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@@ -264,7 +264,7 @@ async function installModule(moduleDetails) {
|
||||
"module": moduleDetails.module,
|
||||
"version": moduleDetails.version,
|
||||
"dir": installDir,
|
||||
"args": ["--production","--engine-strict"]
|
||||
"args": ["--omit=dev","--engine-strict"]
|
||||
}
|
||||
return hooks.trigger("preInstall", triggerPayload).then((result) => {
|
||||
// preInstall passed
|
||||
|
@@ -215,7 +215,7 @@ async function installModule(module,version,url) {
|
||||
"dir": installDir,
|
||||
"isExisting": isExisting,
|
||||
"isUpgrade": isUpgrade,
|
||||
"args": ['--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix=~','--production','--engine-strict']
|
||||
"args": ['--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix=~','--omit=dev','--engine-strict']
|
||||
}
|
||||
|
||||
return hooks.trigger("preInstall", triggerPayload).then((result) => {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/registry",
|
||||
"version": "3.1.7",
|
||||
"version": "4.0.0-beta.1",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
"repository": {
|
||||
@@ -16,7 +16,7 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@node-red/util": "3.1.7",
|
||||
"@node-red/util": "4.0.0-beta.1",
|
||||
"clone": "2.1.2",
|
||||
"fs-extra": "11.1.1",
|
||||
"semver": "7.5.4",
|
||||
|
@@ -68,6 +68,7 @@ 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
|
||||
*/
|
||||
@@ -102,6 +103,15 @@ 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;
|
||||
@@ -118,32 +128,58 @@ var api = module.exports = {
|
||||
stores = [store];
|
||||
}
|
||||
|
||||
|
||||
var result = {};
|
||||
var c = stores.length;
|
||||
var errorReported = false;
|
||||
stores.forEach(function(store) {
|
||||
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);
|
||||
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
|
||||
}
|
||||
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;
|
||||
}
|
||||
c--;
|
||||
if (c === 0) {
|
||||
if (!errorReported) {
|
||||
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key},opts.req);
|
||||
resolve(result);
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
@@ -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: ">=8.9.0"})+" *");
|
||||
log.error("* "+log._("runtime.unsupported_version",{component:"Node.js",version:process.version,requires: ">=18"})+" *");
|
||||
log.error("*****************************************************************");
|
||||
events.emit("runtime-event",{id:"runtime-unsupported-version",payload:{type:"error",text:"notification.errors.unsupportedVersion"},retain:true});
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/runtime",
|
||||
"version": "3.1.7",
|
||||
"version": "4.0.0-beta.1",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
"repository": {
|
||||
@@ -16,8 +16,8 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@node-red/registry": "3.1.7",
|
||||
"@node-red/util": "3.1.7",
|
||||
"@node-red/registry": "4.0.0-beta.1",
|
||||
"@node-red/util": "4.0.0-beta.1",
|
||||
"async-mutex": "0.4.0",
|
||||
"clone": "2.1.2",
|
||||
"express": "4.18.2",
|
||||
|
23
packages/node_modules/@node-red/util/lib/util.js
vendored
@@ -636,7 +636,15 @@ function evaluateNodeProperty(value, type, node, msg, callback) {
|
||||
} else if (type === 're') {
|
||||
result = new RegExp(value);
|
||||
} else if (type === 'date') {
|
||||
result = Date.now();
|
||||
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)
|
||||
}
|
||||
} else if (type === 'bin') {
|
||||
var data = JSON.parse(value);
|
||||
if (Array.isArray(data) || (typeof(data) === "string")) {
|
||||
@@ -769,12 +777,15 @@ function evaluateJSONataExpression(expr,msg,callback) {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
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)
|
||||
const error = new Error('Calls to RED.util.evaluateJSONataExpression must include a callback.')
|
||||
throw error
|
||||
}
|
||||
return expr.evaluate(context, bindings, callback);
|
||||
|
||||
expr.evaluate(context, bindings).then(result => {
|
||||
callback(null, result)
|
||||
}).catch(err => {
|
||||
callback(err)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/util",
|
||||
"version": "3.1.7",
|
||||
"version": "4.0.0-beta.1",
|
||||
"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": "1.8.7",
|
||||
"jsonata": "2.0.4",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"moment": "2.29.4",
|
||||
"moment-timezone": "0.5.43"
|
||||
|
3
packages/node_modules/node-red/lib/red.js
vendored
@@ -33,8 +33,7 @@ if (NODE_MAJOR_VERSION >= 16) {
|
||||
|
||||
function checkVersion(userSettings) {
|
||||
var semver = require('semver');
|
||||
if (!semver.satisfies(process.version,">=14.0.0")) {
|
||||
// TODO: in the future, make this a hard error.
|
||||
if (!semver.satisfies(process.version,">=18.0.0")) {
|
||||
// var e = new Error("Unsupported version of Node.js");
|
||||
// e.code = "unsupported_version";
|
||||
// throw e;
|
||||
|
12
packages/node_modules/node-red/package.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "node-red",
|
||||
"version": "3.1.7",
|
||||
"version": "4.0.0-beta.1",
|
||||
"description": "Low-code programming for event-driven applications",
|
||||
"homepage": "https://nodered.org",
|
||||
"license": "Apache-2.0",
|
||||
@@ -31,10 +31,10 @@
|
||||
"flow"
|
||||
],
|
||||
"dependencies": {
|
||||
"@node-red/editor-api": "3.1.7",
|
||||
"@node-red/runtime": "3.1.7",
|
||||
"@node-red/util": "3.1.7",
|
||||
"@node-red/nodes": "3.1.7",
|
||||
"@node-red/editor-api": "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",
|
||||
"basic-auth": "2.0.1",
|
||||
"bcryptjs": "2.4.3",
|
||||
"express": "4.18.2",
|
||||
@@ -47,6 +47,6 @@
|
||||
"bcrypt": "5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
17
packages/node_modules/node-red/red.js
vendored
@@ -26,6 +26,13 @@ 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");
|
||||
@@ -346,7 +353,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 v8.9.0 or later");
|
||||
console.log("Node-RED requires Node.js v18 or later");
|
||||
} else {
|
||||
console.log("Failed to start server:");
|
||||
if (err.stack) {
|
||||
@@ -408,9 +415,15 @@ httpsPromise.then(function(startupHttps) {
|
||||
if (settings.httpAdminRoot !== false) {
|
||||
app.use(settings.httpAdminRoot,RED.httpAdmin);
|
||||
}
|
||||
|
||||
if (settings.httpNodeRoot !== false && settings.httpNodeAuth) {
|
||||
app.use(settings.httpNodeRoot,basicAuthMiddleware(settings.httpNodeAuth.user,settings.httpNodeAuth.pass));
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.httpNodeRoot !== false) {
|
||||
app.use(settings.httpNodeRoot,RED.httpNode);
|
||||
}
|
||||
|
4
packages/node_modules/node-red/settings.js
vendored
@@ -449,6 +449,7 @@ module.exports = {
|
||||
* - ui (for use with Node-RED Dashboard)
|
||||
* - debugUseColors
|
||||
* - debugMaxLength
|
||||
* - debugStatusLength
|
||||
* - execMaxBufferSize
|
||||
* - httpRequestTimeout
|
||||
* - mqttReconnectTime
|
||||
@@ -504,6 +505,9 @@ 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,
|
||||
|
||||
|
@@ -1718,9 +1718,13 @@ 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,200)
|
||||
setTimeout(resolve, ${timeoutMS});
|
||||
})`,
|
||||
func:"return msg;"
|
||||
},
|
||||
@@ -1733,9 +1737,10 @@ describe('function node', function() {
|
||||
msg.delta = Date.now() - msg.payload;
|
||||
receivedMsgs.push(msg)
|
||||
if (receivedMsgs.length === 5) {
|
||||
var errors = receivedMsgs.filter(msg => msg.delta < 200)
|
||||
let deltas = receivedMsgs.map(msg => msg.delta);
|
||||
var errors = deltas.filter(delta => delta < (timeoutMS - timeoutCheckMargin))
|
||||
if (errors.length > 0) {
|
||||
done(new Error(`Message received before init completed - was ${msg.delta} expected >300`))
|
||||
done(new Error(`Message received before init completed - delta values ${JSON.stringify(deltas)} expected to be > ${timeoutMS - timeoutCheckMargin}`))
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
|
@@ -66,6 +66,27 @@ 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"}];
|
||||
@@ -108,6 +129,31 @@ 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"}];
|
||||
@@ -516,6 +562,7 @@ 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"}];
|
||||
@@ -562,6 +609,32 @@ 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"}];
|
||||
@@ -639,6 +712,35 @@ 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"}];
|
||||
|
@@ -379,10 +379,17 @@ describe("@node-red/util/util", function() {
|
||||
result = util.evaluateNodeProperty('','bool');
|
||||
result.should.be.false();
|
||||
});
|
||||
it('returns date',function() {
|
||||
it('returns date - default format',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);
|
||||
@@ -441,9 +448,16 @@ describe("@node-red/util/util", function() {
|
||||
},{});
|
||||
result.should.eql("123");
|
||||
});
|
||||
it('returns jsonata result', function () {
|
||||
var result = util.evaluateNodeProperty('$abs(-1)','jsonata',{},{});
|
||||
result.should.eql(1);
|
||||
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 null', function() {
|
||||
var result = util.evaluateNodeProperty(null,'null');
|
||||
@@ -601,51 +615,105 @@ describe("@node-red/util/util", function() {
|
||||
});
|
||||
});
|
||||
describe('evaluateJSONataExpression', function() {
|
||||
it('evaluates an expression', function() {
|
||||
it('evaluates an expression', function(done) {
|
||||
var expr = util.prepareJSONataExpression('payload',{});
|
||||
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
|
||||
result.should.eql("hello");
|
||||
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
|
||||
try {
|
||||
result.should.eql("hello");
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
it('evaluates a legacyMode expression', function() {
|
||||
var expr = util.prepareJSONataExpression('msg.payload',{});
|
||||
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
|
||||
result.should.eql("hello");
|
||||
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
|
||||
try {
|
||||
result.should.eql("hello");
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
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]}}}}});
|
||||
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
|
||||
result.should.eql("bar");
|
||||
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
|
||||
try {
|
||||
result.should.eql("bar");
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
it('accesses undefined environment variable from an expression', function() {
|
||||
var expr = util.prepareJSONataExpression('$env("UTIL_ENV")',{});
|
||||
var result = util.evaluateJSONataExpression(expr,{});
|
||||
result.should.eql('');
|
||||
});
|
||||
util.evaluateJSONataExpression(expr,{}, (err, result) => {
|
||||
try {
|
||||
result.should.eql("");
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
it('accesses environment variable from an expression', function() {
|
||||
process.env.UTIL_ENV = 'foo';
|
||||
var expr = util.prepareJSONataExpression('$env("UTIL_ENV")',{});
|
||||
var result = util.evaluateJSONataExpression(expr,{});
|
||||
result.should.eql('foo');
|
||||
});
|
||||
util.evaluateJSONataExpression(expr,{}, (err, result) => {
|
||||
try {
|
||||
result.should.eql("foo");
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
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")',{});
|
||||
var result = util.evaluateJSONataExpression(expr,{});
|
||||
result.should.eql('2020-07-03');
|
||||
util.evaluateJSONataExpression(expr,{}, (err, result) => {
|
||||
try {
|
||||
result.should.eql("2020-07-03");
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
it('accesses moment-timezone from an expression', function() {
|
||||
var expr = util.prepareJSONataExpression('$moment("2013-11-18 11:55Z").tz("Asia/Taipei").format()',{});
|
||||
var result = util.evaluateJSONataExpression(expr,{});
|
||||
result.should.eql('2013-11-18T19:55:00+08:00');
|
||||
util.evaluateJSONataExpression(expr,{}, (err, result) => {
|
||||
try {
|
||||
result.should.eql("2013-11-18T19:55:00+08:00");
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
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]}}}}});
|
||||
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
|
||||
should.not.exist(result);
|
||||
});
|
||||
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
|
||||
try {
|
||||
should.not.exist(result);
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
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]}}}}});
|
||||
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
|
||||
should.not.exist(result);
|
||||
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
|
||||
try {
|
||||
should.not.exist(result);
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
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)}}}}});
|
||||
|