1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Merge branch 'dev' into dev

This commit is contained in:
Stefan Kleeschulte 2019-11-26 21:34:27 +01:00 committed by GitHub
commit 422ed371f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 841 additions and 360 deletions

View File

@ -1,3 +1,39 @@
#### 1.0.3: Maintenance Release
Runtime
- Increase timeouts in Subflow tests to minimise false positives
- Update grunt-sass and add node-sass for node12 support
- Fix timings of Delay node tests
- #2340 Update JSONata to 1.7.0
- Bump https-proxy-agent version
- #2332 Fix error handling of nodes with multiple input handlers
- Add script to generate npm publish script
- #2371 Ensure folder is present before write (e.g. flows file not in user folder)
- #2371 Handle windows UNC '\\' paths
- #2366 Handle logging of non-JSON encodable objects
Editor
- #2328 Fix language handling in subflow node
- Use default language if lng param not set in i18n req
- #2326 Fix palette editor search visualization
- #2375 Subflow status not showing i18n version of contained core nodes status
- Fix inverse of 'replace' editor event
- #2376 Fallback to base language files if present
- #2373 Support UI testing on the latest Google Chrome
- #2364 Add tooltip to expand button in markdown editor
- #2363 Support ctrl key to select tabs for Windows
- #2356 Make JSONata help initially shown in expression editor
- #2355 Prohibit line break in type menu of typedInput
Nodes
- Delay: Fix delay to not pass through .reset and .flush props consistently
- #2352 File: Using the a msg per line the last line does not get msg.topic passed
- #2339 HTTP Request: Check auth type on opening
- HTTP Request: add units info
- #2372 MQTT/WS: Improved proxy support for MQTT and WebSocket nodes
- #2370 MQTT: Add clarification that MQTT Out requires payload to send msg
#### 1.0.2: Maintenance Release
Runtime

View File

@ -16,6 +16,7 @@
var path = require("path");
var fs = require("fs-extra");
var sass = require("node-sass");
module.exports = function(grunt) {
@ -220,6 +221,7 @@ module.exports = function(grunt) {
sass: {
build: {
options: {
implementation: sass,
outputStyle: 'compressed'
},
files: [{

View File

@ -1,6 +1,6 @@
{
"name": "node-red",
"version": "1.0.2",
"version": "1.0.3",
"description": "Low-code programming for event-driven applications",
"homepage": "http://nodered.org",
"license": "Apache-2.0",
@ -34,20 +34,20 @@
"cookie": "0.4.0",
"cookie-parser": "1.4.4",
"cors": "2.8.5",
"cron": "1.7.1",
"cron": "1.7.2",
"denque": "1.4.1",
"express": "4.17.1",
"express-session": "1.16.2",
"express-session": "1.17.0",
"fs-extra": "8.1.0",
"fs.notify": "0.0.4",
"hash-sum": "2.0.0",
"https-proxy-agent": "2.2.2",
"https-proxy-agent": "2.2.4",
"i18next": "15.1.2",
"iconv-lite": "0.5.0",
"is-utf8": "0.2.1",
"js-yaml": "3.13.1",
"json-stringify-safe": "5.0.1",
"jsonata": "1.6.5",
"jsonata": "1.7.0",
"media-typer": "1.1.0",
"memorystore": "1.6.1",
"mime": "2.4.4",
@ -66,10 +66,10 @@
"raw-body": "2.4.1",
"request": "2.88.0",
"semver": "6.3.0",
"uglify-js": "3.6.0",
"uglify-js": "3.6.9",
"when": "3.7.8",
"ws": "6.2.1",
"xml2js": "0.4.19"
"xml2js": "0.4.22"
},
"optionalDependencies": {
"bcrypt": "3.0.6"
@ -93,19 +93,20 @@
"grunt-mocha-istanbul": "5.0.2",
"grunt-nodemon": "~0.4.2",
"grunt-npm-command": "~0.1.2",
"grunt-sass": "~2.0.0",
"grunt-sass": "~3.1.0",
"grunt-simple-mocha": "~0.4.1",
"http-proxy": "^1.16.2",
"http-proxy": "1.18.0",
"istanbul": "0.4.5",
"jsdoc-nr-template": "github:node-red/jsdoc-nr-template",
"minami": "1.2.3",
"mocha": "^5.2.0",
"mosca": "^2.8.3",
"node-red-node-test-helper": "^0.2.3",
"node-sass": "^4.13.0",
"should": "^8.4.0",
"sinon": "1.17.7",
"stoppable": "^1.1.0",
"supertest": "3.4.2",
"node-red-node-test-helper": "^0.2.3",
"jsdoc-nr-template": "node-red/jsdoc-nr-template"
"supertest": "3.4.2"
},
"engines": {
"node": ">=8"

View File

@ -44,6 +44,7 @@ module.exports = {
user: req.user,
module: req.body.module,
version: req.body.version,
url: req.body.url,
req: apiUtils.getRequestLogObject(req)
}
runtimeAPI.nodes.addModule(opts).then(function(info) {

View File

@ -41,7 +41,7 @@ module.exports = {
var namespace = req.params[0];
var lngs = req.query.lng;
namespace = namespace.replace(/\.json$/,"");
var lang = req.query.lng; //apiUtil.determineLangFromHeaders(req.acceptsLanguages() || []);
var lang = req.query.lng || i18n.defaultLang; //apiUtil.determineLangFromHeaders(req.acceptsLanguages() || []);
var prevLang = i18n.i.language;
// Trigger a load from disk of the language if it is not the default
i18n.i.changeLanguage(lang, function(){

View File

@ -42,7 +42,7 @@ var editor;
/**
* Initialise the module.
* @param {Object} settings The runtime settings
* @param {HTTPServer} server An instance of HTTP Server
* @param {HTTPServer} _server An instance of HTTP Server
* @param {Storage} storage An instance of Node-RED Storage
* @param {Runtime} runtimeAPI An instance of Node-RED Runtime
* @memberof @node-red/editor-api

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/editor-api",
"version": "1.0.2",
"version": "1.0.3",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@ -16,13 +16,13 @@
}
],
"dependencies": {
"@node-red/util": "1.0.2",
"@node-red/editor-client": "1.0.2",
"@node-red/util": "1.0.3",
"@node-red/editor-client": "1.0.3",
"bcryptjs": "2.4.3",
"body-parser": "1.19.0",
"clone": "2.1.2",
"cors": "2.8.5",
"express-session": "1.16.2",
"express-session": "1.17.0",
"express": "4.17.1",
"memorystore": "1.6.1",
"mime": "2.4.4",

View File

@ -15,6 +15,17 @@
"next": "Next",
"clone": "Clone project",
"cont": "Continue"
},
"type": {
"string": "string",
"number": "number",
"boolean": "boolean",
"array": "array",
"buffer": "buffer",
"object": "object",
"jsonString": "JSON string",
"undefined": "undefined",
"null": "null"
}
},
"workspace": {
@ -792,10 +803,14 @@
"copyPath": "Copy path to item",
"expandItems": "Expand items",
"collapseItems": "Collapse items",
"duplicate": "Duplicate"
"duplicate": "Duplicate",
"error": {
"invalidJSON": "Invalid JSON: "
}
},
"markdownEditor": {
"title": "Markdown editor",
"expand": "Expand",
"format": "Formatted with markdown",
"heading1": "Heading 1",
"heading2": "Heading 2",

View File

@ -1,7 +1,7 @@
{
"$string": {
"args": "arg",
"desc": "Casts the *arg* parameter to a string using the following casting rules:\n\n - Strings are unchanged\n - Functions are converted to an empty string\n - Numeric infinity and NaN throw an error because they cannot be represented as a JSON number\n - All other values are converted to a JSON string using the `JSON.stringify` function"
"args": "arg[, prettify]",
"desc": "Casts the `arg` parameter to a string using the following casting rules:\n\n - Strings are unchanged\n - Functions are converted to an empty string\n - Numeric infinity and NaN throw an error because they cannot be represented as a JSON number\n - All other values are converted to a JSON string using the `JSON.stringify` function. If `prettify` is true, then \"prettified\" JSON is produced. i.e One line per field and lines will be indented based on the field depth."
},
"$length": {
"args": "str",
@ -185,7 +185,7 @@
},
"$reduce": {
"args":"array, function [, init]",
"desc":"Returns an aggregated value derived from applying the `function` parameter successively to each value in `array` in combination with the result of the previous application of the function.\n\nThe function must accept two arguments, and behaves like an infix operator between each value within the `array`.\n\nThe optional `init` parameter is used as the initial value in the aggregation."
"desc":"Returns an aggregated value derived from applying the `function` parameter successively to each value in `array` in combination with the result of the previous application of the function.\n\nThe function must accept two arguments, and behaves like an infix operator between each value within the `array`. The signature of `function` must be of the form: `myfunc($accumulator, $value[, $index[, $array]])`\n\nThe optional `init` parameter is used as the initial value in the aggregation."
},
"$flowContext": {
"args": "string[, string]",
@ -230,6 +230,37 @@
"$parseInteger": {
"args": "string, picture",
"desc": "Parses the contents of the `string` parameter to an integer (as a JSON number) using the format specified by the `picture` string. The `picture` string parameter has the same format as `$formatInteger`."
},
"$error": {
"args": "[str]",
"desc": "Throws an error with a message. The optional `str` will replace the default message of `$error() function evaluated`"
},
"$assert": {
"args": "arg, str",
"desc": "If `arg` is true the function returns undefined. If `arg` is false an exception is thrown with `str` as the message of the exception."
},
"$single": {
"args": "array, function",
"desc": "Returns the one and only value in the `array` parameter that satisfies the `function` predicate (i.e. the `function` returns Boolean `true` when passed the value). Throws an exception if the number of matching values is not exactly one.\n\nThe function should be supplied in the following signature: `function(value [, index [, array]])` where value is each input of the array, index is the position of that value and the whole array is passed as the third argument"
},
"$encodeUrl": {
"args": "str",
"desc": "Encodes a Uniform Resource Locator (URL) component by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character.\n\nExample: `$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`"
},
"$encodeUrlComponent": {
"args": "str",
"desc": "Encodes a Uniform Resource Locator (URL) by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character. \n\nExample: `$encodeUrl(\"https://mozilla.org/?x=шеллы\")` => `\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\"`"
},
"$decodeUrl": {
"args": "str",
"desc": "Decodes a Uniform Resource Locator (URL) component previously created by encodeUrlComponent. \n\nExample: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`"
},
"$decodeUrlComponent": {
"args": "str",
"desc": "Decodes a Uniform Resource Locator (URL) previously created by encodeUrl. \n\nExample: `$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")` => `\"https://mozilla.org/?x=шеллы\"`"
},
"$distinct": {
"args": "array",
"desc": "Returns an array with duplicate values removed from `array`"
}
}

View File

@ -15,6 +15,17 @@
"next": "進む",
"clone": "プロジェクトをクローン",
"cont": "続ける"
},
"type": {
"string": "文字列",
"number": "数値",
"boolean": "真偽値",
"array": "配列",
"buffer": "バッファ",
"object": "オブジェクト",
"jsonString": "JSON文字列",
"undefined": "undefined",
"null": "null"
}
},
"workspace": {
@ -791,10 +802,14 @@
"copyPath": "要素のパスをコピー",
"expandItems": "要素を展開",
"collapseItems": "要素を折り畳む",
"duplicate": "複製"
"duplicate": "複製",
"error": {
"invalidJSON": "不正なJSON: "
}
},
"markdownEditor": {
"title": "マークダウンエディタ",
"expand": "拡大",
"format": "マークダウン形式で記述",
"heading1": "見出しレベル1",
"heading2": "見出しレベル2",

View File

@ -1,7 +1,7 @@
{
"$string": {
"args": "arg",
"desc": "以下の型変換ルールを用いて、引数 *arg* を文字列へ型変換します。:\n\n - 文字列は変換しません。\n - 関数は空の文字列に変換します。\n - JSONの数値として表現できないため、無限大やNaNはエラーになります。\n - 他の値は `JSON.stringify` 関数を用いて、JSONの文字列へ変換します。"
"args": "arg[, prettify]",
"desc": "以下の型変換ルールを用いて、引数 *arg* を文字列へ型変換します。:\n\n - 文字列は変換しません。\n - 関数は空の文字列に変換します。\n - JSONの数値として表現できないため、無限大やNaNはエラーになります。\n - 他の値は `JSON.stringify` 関数を用いて、JSONの文字列へ変換します。`prettify`が真の場合、JSONを整形出力します。フィールドを1行毎に出力。フィールドのネスト深さによってインデントを行います。"
},
"$length": {
"args": "str",
@ -185,7 +185,7 @@
},
"$reduce": {
"args": "array, function [, init]",
"desc": "配列の各要素値に関数 `function` を連続的に適用して得られる集約値を返します。 `function` の適用の際には、直前の `function` の適用結果と要素値が引数として与えられます。\n\n関数 `function` は引数を2つ取り、配列の各要素の間に配置する中置演算子のように作用しなくてはなりません。\n\n任意の引数 `init` には、集約時の初期値を設定します。"
"desc": "配列の各要素値に関数 `function` を連続的に適用して得られる集約値を返します。 `function` の適用の際には、直前の `function` の適用結果と要素値が引数として与えられます。\n\n関数 `function` は引数を2つ取り、配列の各要素の間に配置する中置演算子のように作用しなくてはなりません。関数`function`のシグネチャは`myfunc($accumulator, $value[, $index[, $array]])`という形式でなければなりません。\n\n任意の引数 `init` には、集約時の初期値を設定します。"
},
"$flowContext": {
"args": "string",
@ -230,5 +230,37 @@
"$parseInteger": {
"args": "string, picture",
"desc": "`picture`文字列の指定に従って、`string`パラメータを整数(JSON数値)に変換します。`picture`文字列は`$formatInteger`と同じ形式です。"
},
"$error": {
"args": "[str]",
"desc": "メッセージを指定して例外を送出します。メッセージ`str`を省略した場合は`$error() function evaluated`をメッセージとします。"
},
"$assert": {
"args": "arg, str",
"desc": "`arg`が真の場合、undefinedを返します。偽の場合、`str`をメッセージとする例外を送出します。"
},
"$single": {
"args": "array, function",
"desc": "`array`の要素のうち、条件判定関数`function`を満たす(`function`に与えた場合に真偽値`true`を返す)要素が1つのみである場合、それを返します。マッチする要素が1つのみでない場合、例外を送出します。\n\n指定する関数は`function(value [, index [, array]])`というシグネチャでなければなりません。ここで、`value`は`array`の要素値、`index`は要素の添字、第三引数には配列全体を渡します。"
},
"$encodeUrl": {
"args": "str",
"desc": "Uniform Resource Locator (URL)を構成する文字を1、、3、もしくは、4文字エスケープシーケンスのUTF-8文字エンコーディングで置換します。\n\n例: `$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`"
},
"$encodeUrlComponent": {
"args": "str",
"desc": "Uniform Resource Locator (URL)要素を構成する文字を1、、もしくは、4文字エスケープシーケンスのUTF-8文字エンコーディングで置換します。\n\n例: `$encodeUrl(\"https://mozilla.org/?x=шеллы\")` => `\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\"`"
},
"$decodeUrl": {
"args": "str",
"desc": "encodeUrlComponentで置換したUniform Resource Locator (URL)をデコードします。\n\n例: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`"
},
"$decodeUrlComponent": {
"args": "str",
"desc": "encodeUrlで置換したUniform Resource Locator (URL)要素をデコードします。 \n\n例: `$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")` => `\"https://mozilla.org/?x=шеллы\"`"
},
"$distinct": {
"args": "array",
"desc": "配列`array`から重複要素を削除した配列を返します。"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/editor-client",
"version": "1.0.2",
"version": "1.0.3",
"license": "Apache-2.0",
"repository": {
"type": "git",

View File

@ -39,7 +39,7 @@ RED.history = (function() {
inverseEv = {
t: 'replace',
config: RED.nodes.createCompleteNodeSet(),
changed: [],
changed: {},
rev: RED.nodes.version()
};
RED.nodes.clear();

View File

@ -845,7 +845,7 @@ RED.nodes = (function() {
var m = /^subflow:(.+)$/.exec(newNodes[i].type);
if (m) {
var subflowId = m[1];
var parent = getSubflow(newNodes[i].z || activeWorkspace);
var parent = getSubflow(activeWorkspace);
if (parent) {
var err;
if (subflowId === parent.id) {

View File

@ -218,7 +218,7 @@ RED.tabs = (function() {
var thisTab = $(this).parent();
var fireSelectionChanged = false;
if (options.onselect) {
if (evt.metaKey) {
if (evt.metaKey || evt.ctrlKey) {
if (thisTab.hasClass("selected")) {
thisTab.removeClass("selected");
if (thisTab[0] !== currentTab[0]) {

View File

@ -2484,7 +2484,7 @@ RED.editor = (function() {
editor.toolbar = customEditTypes['_markdown'].buildToolbar(toolbarRow,editor);
if (options.expandable !== false) {
var expandButton = $('<button type="button" class="red-ui-button" style="float: right;"><i class="fa fa-expand"></i></button>').appendTo(editor.toolbar);
RED.popover.tooltip(expandButton, RED._("markdownEditor.expand"));
expandButton.on("click", function(e) {
e.preventDefault();
var value = editor.getValue();
@ -2558,7 +2558,7 @@ RED.editor = (function() {
/**
* Register a type editor.
* @param {string} type - the type name
* @param {object} options - the editor definition
* @param {object} definition - the editor definition
* @function
* @memberof RED.editor
*/

View File

@ -207,6 +207,7 @@
}
expressionEditor.getSession().setValue(v||"",-1);
});
funcSelect.change();
var tabs = RED.tabs.create({
element: $("#red-ui-editor-type-expression-tabs"),

View File

@ -302,8 +302,8 @@
types:[
'str','num','bool',
{value:"null",label:"null",hasValue:false},
{value:"array",label:"array",hasValue:false},
{value:"object",label:"object",hasValue:false}
{value:"array",label:RED._("common.type.array"),hasValue:false},
{value:"object",label:RED._("common.type.object"),hasValue:false}
],
default: valType
});
@ -577,7 +577,7 @@
} catch(err) {
rootNode = null;
list.treeList('data',[{
label: "Invalid JSON: "+err.toString()
label: RED._("jsonEditor.error.invalidJSON")+err.toString()
}]);
}
}

View File

@ -75,13 +75,16 @@ RED.palette.editor = (function() {
});
})
}
function installNodeModule(id,version,callback) {
function installNodeModule(id,version,url,callback) {
var requestBody = {
module: id
};
if (version) {
requestBody.version = version;
}
if (url) {
requestBody.url = url;
}
$.ajax({
url:"nodes",
type: "POST",
@ -622,7 +625,7 @@ RED.palette.editor = (function() {
if ($(this).hasClass('disabled')) {
return;
}
update(entry,loadedIndex[entry.name].version,container,function(err){});
update(entry,loadedIndex[entry.name].version,loadedIndex[entry.name].pkg_url,container,function(err){});
})
@ -788,7 +791,7 @@ RED.palette.editor = (function() {
initInstallTab();
})
packageList = $('<ol>',{style:"position: absolute;top: 78px;bottom: 0;left: 0;right: 0px;"}).appendTo(installTab).editableList({
packageList = $('<ol>',{style:"position: absolute;top: 79px;bottom: 0;left: 0;right: 0px;"}).appendTo(installTab).editableList({
addButton: false,
scrollOnAdd: false,
addItem: function(container,i,object) {
@ -872,7 +875,7 @@ RED.palette.editor = (function() {
$('<div id="red-ui-palette-module-install-shade" class="red-ui-palette-module-shade hide"><div class="red-ui-palette-module-shade-status"></div><img src="red/images/spin.svg" class="red-ui-palette-spinner"/></div>').appendTo(installTab);
}
function update(entry,version,container,done) {
function update(entry,version,url,container,done) {
if (RED.settings.theme('palette.editable') === false) {
done(new Error('Palette not editable'));
return;
@ -898,7 +901,7 @@ RED.palette.editor = (function() {
RED.actions.invoke("core:show-event-log");
});
RED.eventLog.startEvent(RED._("palette.editor.confirm.button.install")+" : "+entry.name+" "+version);
installNodeModule(entry.name,version,function(xhr) {
installNodeModule(entry.name,version,url,function(xhr) {
spinner.remove();
if (xhr) {
if (xhr.responseJSON) {
@ -1023,7 +1026,7 @@ RED.palette.editor = (function() {
RED.actions.invoke("core:show-event-log");
});
RED.eventLog.startEvent(RED._("palette.editor.confirm.button.install")+" : "+entry.id+" "+entry.version);
installNodeModule(entry.id,entry.version,function(xhr) {
installNodeModule(entry.id,entry.version,entry.pkg_url,function(xhr) {
spinner.remove();
if (xhr) {
if (xhr.responseJSON) {

View File

@ -19,12 +19,18 @@ RED.subflow = (function() {
var currentLocale = "en-US";
var _subflowEditTemplate = '<script type="text/x-red" data-template-name="subflow">'+
'<div class="form-row"><label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label><input type="text" id="node-input-name"></div>'+
'<div class="form-row">'+
'<label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label>'+
'<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">'+
'</div>'+
'<div id="subflow-input-ui"></div>'+
'</script>';
var _subflowTemplateEditTemplate = '<script type="text/x-red" data-template-name="subflow-template">'+
'<div class="form-row"><label for="subflow-input-name" data-i18n="[append]common.label.name"><i class="fa fa-tag"></i> </label><input type="text" id="subflow-input-name"></div>'+
'<div class="form-row">'+
'<label for="subflow-input-name" data-i18n="[append]common.label.name"><i class="fa fa-tag"></i> </label>'+
'<input type="text" id="subflow-input-name" data-i18n="[placeholder]common.label.name">'+
'</div>'+
'<div class="form-row">'+
'<ul style="margin-bottom: 20px;" id="subflow-env-tabs"></ul>'+
'</div>'+
@ -802,8 +808,8 @@ RED.subflow = (function() {
}
$("<option/>", opt).text(item.text).appendTo(locales);
});
currentLocale = RED.i18n.lang();
locales.val(currentLocale);
var locale = RED.i18n.lang();
locales.val(locale);
locales.on("change", function() {
currentLocale = $(this).val();
@ -1048,7 +1054,7 @@ RED.subflow = (function() {
}
langs.forEach(function(l) {
var row = $('<div>').appendTo(content);
$('<span>').css({display:"inline-block",width:"50px"}).text(l+(l===currentLocale?"*":"")).appendTo(row);
$('<span>').css({display:"inline-block",width:"120px"}).text(RED._("languages."+l)+(l===currentLocale?"*":"")).appendTo(row);
$('<span>').text(ui.label[l]||"").appendTo(row);
});
return content;
@ -1371,7 +1377,8 @@ RED.subflow = (function() {
}
var labels = ui.label || {};
var labelText = lookupLabel(labels, labels["en-US"]||tenv.name, currentLocale);
var locale = RED.i18n.lang();
var labelText = lookupLabel(labels, labels["en-US"]||tenv.name, locale);
var label = $('<label>').appendTo(row);
var labelContainer = $('<span></span>').appendTo(label);
if (ui.icon) {
@ -1423,7 +1430,7 @@ RED.subflow = (function() {
input = $('<select>').css('width','70%').appendTo(row);
if (ui.opts.opts) {
ui.opts.opts.forEach(function(o) {
$('<option>').val(o.v).text(lookupLabel(o.l, o.l['en-US']||o.v, currentLocale)).appendTo(input);
$('<option>').val(o.v).text(lookupLabel(o.l, o.l['en-US']||o.v, locale)).appendTo(input);
})
}
input.val(val.value);

View File

@ -164,7 +164,7 @@ RED.workspaces = (function() {
var dialogForm = $('<form id="dialog-form" class="form-horizontal"></form>').appendTo(trayBody);
$('<div class="form-row">'+
'<label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label>'+
'<input type="text" id="node-input-name">'+
'<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">'+
'</div>').appendTo(dialogForm);

View File

@ -70,6 +70,7 @@
border: 1px solid $primary-border-color;
box-sizing: border-box;
background: $secondary-background;
white-space: nowrap;
z-index: 2000;
a {
padding: 6px 18px 6px 6px;

View File

@ -109,6 +109,7 @@
{
'$abs':{ args:[ 'number' ]},
'$append':{ args:[ 'array1', 'array2' ]},
'$assert':{ args: [ 'arg', 'str' ]},
'$average':{ args:[ 'array' ]},
'$base64decode':{ args:[ ]},
'$base64encode':{ args:[ ]},
@ -116,8 +117,14 @@
'$ceil':{ args:[ 'number' ]},
'$contains':{ args:[ 'str', 'pattern' ]},
'$count':{ args:[ 'array' ]},
'$decodeUrl':{ args:[ 'str' ]},
'$decodeUrlComponent':{ args:[ 'str' ]},
'$distinct':{ args:[ 'array' ]},
'$each':{ args:[ 'object', 'function' ]},
'$encodeUrl':{ args: ['str'] },
'$encodeUrlComponent':{ args:[ 'str' ]},
'$env': { args:[ 'arg' ]},
'$error':{ args:[ 'str' ]},
'$eval': { args: ['expr', 'context']},
'$exists':{ args:[ 'arg' ]},
'$filter':{ args:[ 'array', 'function' ]},
@ -152,11 +159,12 @@
'$round':{ args:[ 'number', 'precision' ]},
'$shuffle':{ args:[ 'array' ]},
'$sift':{ args: ['object', 'function'] },
'$single':{ args: ['array', 'function'] },
'$sort':{ args:[ 'array', 'function' ]},
'$split':{ args:[ 'str', 'separator', 'limit' ]},
'$spread':{ args:[ 'object' ]},
'$sqrt':{ args:[ 'number' ]},
'$string':{ args:[ 'arg' ]},
'$string':{ args:[ 'arg', 'prettify' ]},
'$substring':{ args:[ 'str', 'start', 'length' ]},
'$substringAfter':{ args:[ 'str', 'chars' ]},
'$substringBefore':{ args:[ 'str', 'chars' ]},

View File

@ -30,16 +30,16 @@
<!-- (with the 'node-input-' prefix). -->
<!-- The available icon classes are defined Font Awesome Icons (FA Icons) -->
<div class="form-row">
<label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label>
<input type="text" id="node-input-topic" placeholder="Topic">
<label for="node-input-topic"><i class="fa fa-tasks"></i> <span data-i18n="common.label.topic"></span></label>
<input type="text" id="node-input-topic" data-i18n="[placeholder]common.label.topic">
</div>
<br/>
<!-- By convention, most nodes have a 'name' property. The following div -->
<!-- provides the necessary field. Should always be the last option -->
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
</script>

View File

@ -2,7 +2,7 @@
<script type="text/x-red" data-template-name="comment">
<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">
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row node-text-editor-row">
<input type="hidden" id="node-input-info" autofocus="autofocus">

View File

@ -127,7 +127,7 @@
},
oneditprepare: function() {
var node = this;
var previousValueType = {value:"prev",label:this._("inject.previous"),hasValue:false};
var previousValueType = {value:"prev",label:this._("switch.previous"),hasValue:false};
$("#node-input-property").typedInput({default:this.propertyType||'msg',types:['msg','flow','global','jsonata','env']});
var outputCount = $("#node-input-outputs").val("{}");
@ -237,15 +237,15 @@
function createTypeValueField(){
return $('<input/>',{class:"node-input-rule-type-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'string',types:[
{value:"string",label:"string",hasValue:false},
{value:"number",label:"number",hasValue:false},
{value:"boolean",label:"boolean",hasValue:false},
{value:"array",label:"array",hasValue:false},
{value:"buffer",label:"buffer",hasValue:false},
{value:"object",label:"object",hasValue:false},
{value:"json",label:"JSON string",hasValue:false},
{value:"undefined",label:"undefined",hasValue:false},
{value:"null",label:"null",hasValue:false}
{value:"string",label:RED._("common.type.string"),hasValue:false,icon:"red/images/typedInput/az.png"},
{value:"number",label:RED._("common.type.number"),hasValue:false,icon:"red/images/typedInput/09.png"},
{value:"boolean",label:RED._("common.type.boolean"),hasValue:false,icon:"red/images/typedInput/bool.png"},
{value:"array",label:RED._("common.type.array"),hasValue:false,icon:"red/images/typedInput/json.png"},
{value:"buffer",label:RED._("common.type.buffer"),hasValue:false,icon:"red/images/typedInput/bin.png"},
{value:"object",label:RED._("common.type.object"),hasValue:false,icon:"red/images/typedInput/json.png"},
{value:"json",label:RED._("common.type.jsonString"),hasValue:false,icon:"red/images/typedInput/json.png"},
{value:"undefined",label:RED._("common.type.undefined"),hasValue:false},
{value:"null",label:RED._("common.type.null"),hasValue:false}
]});
}

View File

@ -153,15 +153,24 @@ module.exports = function(RED) {
}
else if (node.pauseType === "rate") {
node.on("input", function(msg) {
if (!node.drop) {
if (msg.hasOwnProperty("reset")) {
if (node.intervalID !== -1 ) {
if (!msg.hasOwnProperty("flush")) {
node.buffer.push(msg);
clearInterval(node.intervalID);
node.intervalID = -1;
}
node.buffer = [];
node.status({text:"reset"});
return;
}
if (!node.drop) {
var m = RED.util.cloneMessage(msg);
delete m.flush;
if (node.intervalID !== -1) {
node.buffer.push(m);
node.reportDepth();
}
}
else {
node.send(msg);
node.send(m);
node.reportDepth();
node.intervalID = setInterval(function() {
if (node.buffer.length === 0) {
@ -174,6 +183,12 @@ module.exports = function(RED) {
node.reportDepth();
}, node.rate);
}
if (msg.hasOwnProperty("flush")) {
while (node.buffer.length > 0) {
node.send(node.buffer.shift());
}
node.status({});
}
}
else {
var timeSinceLast;
@ -189,18 +204,6 @@ module.exports = function(RED) {
node.send(msg);
}
}
if (msg.hasOwnProperty("reset")) {
clearInterval(node.intervalID);
node.intervalID = -1;
node.buffer = [];
node.status({text:"reset"});
}
if (msg.hasOwnProperty("flush")) {
while (node.buffer.length > 0) {
node.send(node.buffer.shift());
}
node.status({});
}
});
node.on("close", function() {
clearInterval(node.intervalID);

View File

@ -111,9 +111,13 @@ module.exports = function(RED) {
if (typeof this.cleansession === 'undefined') {
this.cleansession = true;
}
var prox;
if (process.env.http_proxy != null) { prox = process.env.http_proxy; }
if (process.env.HTTP_PROXY != null) { prox = process.env.HTTP_PROXY; }
var prox, noprox;
if (process.env.http_proxy) { prox = process.env.http_proxy; }
if (process.env.HTTP_PROXY) { prox = process.env.HTTP_PROXY; }
if (process.env.no_proxy) { noprox = process.env.no_proxy.split(","); }
if (process.env.NO_PROXY) { noprox = process.env.NO_PROXY.split(","); }
// Create the URL to pass in to the MQTT.js library
if (this.brokerurl === "") {
@ -121,9 +125,15 @@ module.exports = function(RED) {
if (this.broker.indexOf("://") > -1) {
this.brokerurl = this.broker;
// Only for ws or wss, check if proxy env var for additional configuration
if (this.brokerurl.indexOf("wss://") > -1 || this.brokerurl.indexOf("ws://") > -1 )
if (this.brokerurl.indexOf("wss://") > -1 || this.brokerurl.indexOf("ws://") > -1 ) {
// check if proxy is set in env
if (prox) {
var noproxy;
if (noprox) {
for (var i = 0; i < noprox.length; i += 1) {
if (this.brokerurl.indexOf(noprox[i].trim()) !== -1) { noproxy=true; }
}
}
if (prox && !noproxy) {
var parsedUrl = url.parse(this.brokerurl);
var proxyOpts = url.parse(prox);
// true for wss
@ -134,9 +144,11 @@ module.exports = function(RED) {
agent: agent
}
}
if (this.brokerurl.indexOf("mqtts://") > -1 && (!this.usetls || !n.tls))
}
if (this.brokerurl.indexOf("mqtts://") > -1 && (!this.usetls || !n.tls)) {
// Default to validating the server cert
this.verifyservercert = true;
}
} else {
// construct the std mqtt:// url
if (this.usetls) {

View File

@ -170,6 +170,7 @@
if (this.authType) {
$('#node-input-useAuth').prop('checked', true);
$("#node-input-authType-select").val(this.authType);
$("#node-input-authType-select").change();
} else {
$('#node-input-useAuth').prop('checked', false);
}

View File

@ -39,10 +39,10 @@ module.exports = function(RED) {
else { this.reqTimeout = 120000; }
var prox, noprox;
if (process.env.http_proxy != null) { prox = process.env.http_proxy; }
if (process.env.HTTP_PROXY != null) { prox = process.env.HTTP_PROXY; }
if (process.env.no_proxy != null) { noprox = process.env.no_proxy.split(","); }
if (process.env.NO_PROXY != null) { noprox = process.env.NO_PROXY.split(","); }
if (process.env.http_proxy) { prox = process.env.http_proxy; }
if (process.env.HTTP_PROXY) { prox = process.env.HTTP_PROXY; }
if (process.env.no_proxy) { noprox = process.env.no_proxy.split(","); }
if (process.env.NO_PROXY) { noprox = process.env.NO_PROXY.split(","); }
var proxyConfig = null;
if (n.proxy) {
@ -88,8 +88,13 @@ module.exports = function(RED) {
if (msg.method && n.method && (n.method === "use")) {
method = msg.method.toUpperCase(); // use the msg parameter
}
var isHttps = (/^https/i.test(url));
var opts = {};
opts.url = url;
// set defaultport, else when using HttpsProxyAgent, it's defaultPort of 443 will be used :(.
opts.defaultPort = isHttps?443:80;
opts.timeout = node.reqTimeout;
opts.method = method;
opts.headers = {};
@ -284,9 +289,10 @@ module.exports = function(RED) {
opts.headers[clSet] = opts.headers['content-length'];
delete opts.headers['content-length'];
}
var noproxy;
if (noprox) {
for (var i in noprox) {
for (var i = 0; i < noprox.length; i += 1) {
if (url.indexOf(noprox[i]) !== -1) { noproxy=true; }
}
}

View File

@ -19,6 +19,8 @@ module.exports = function(RED) {
var ws = require("ws");
var inspect = require("util").inspect;
var url = require("url");
var HttpsProxyAgent = require('https-proxy-agent');
var serverUpgradeAdded = false;
function handleServerUpgrade(request, socket, head) {
@ -55,7 +57,28 @@ module.exports = function(RED) {
function startconn() { // Connect to remote endpoint
node.tout = null;
var prox, noprox;
if (process.env.http_proxy) { prox = process.env.http_proxy; }
if (process.env.HTTP_PROXY) { prox = process.env.HTTP_PROXY; }
if (process.env.no_proxy) { noprox = process.env.no_proxy.split(","); }
if (process.env.NO_PROXY) { noprox = process.env.NO_PROXY.split(","); }
var noproxy = false;
if (noprox) {
for (var i in noprox) {
if (node.path.indexOf(noprox[i].trim()) !== -1) { noproxy=true; }
}
}
var agent = undefined;
if (prox && !noproxy) {
agent = new HttpsProxyAgent(prox);
}
var options = {};
if (agent) {
options.agent = agent;
}
if (node.tls) {
var tlsNode = RED.nodes.getNode(node.tls);
if (tlsNode) {

View File

@ -371,7 +371,6 @@ module.exports = function(RED) {
var server = net.createServer(function (socket) {
socket.setKeepAlive(true,120000);
if (socketTimeout !== null) { socket.setTimeout(socketTimeout); }
var remoteDetails = socket.remoteAddress+":"+socket.remotePort;
node.log(RED._("tcpin.status.connection-from",{host:socket.remoteAddress, port:socket.remotePort}));
connectedSockets.push(socket);
node.status({text:RED._("tcpin.status.connections",{count:connectedSockets.length})});

View File

@ -285,7 +285,11 @@
$("#node-input-property").typedInput('types',['msg']);
$("#node-input-joiner").typedInput("show");
} else {
$("#node-input-property").typedInput('types',['msg', {value:"full",label:"complete message",hasValue:false}]);
$("#node-input-property").typedInput('types', ['msg', {
value: "full",
label: RED._("node-red:join.completeMessage"),
hasValue: false
}]);
}
});
@ -297,7 +301,11 @@
$("#node-input-property").typedInput({
typeField: $("#node-input-propertyType"),
types:['msg', {value:"full", label:"complete message", hasValue:false}]
types: ['msg', {
value: "full",
label: RED._("node-red:join.completeMessage"),
hasValue: false
}]
});
$("#node-input-key").typedInput({

View File

@ -341,6 +341,7 @@ module.exports = function(RED) {
}
else if (node.format === "lines") {
var m = { payload: spare,
topic:msg.topic,
parts: {
index: count,
count: count+1,

View File

@ -606,6 +606,7 @@
"rule": "rule",
"repair": "recreate message sequences"
},
"previous": "previous value",
"and": "and",
"checkall": "checking all rules",
"stopfirst": "stopping after first match",
@ -858,6 +859,7 @@
"custom": "manual"
},
"combine": "Combine each",
"completeMessage": "complete message",
"create": "to create",
"type": {
"string": "a String",

View File

@ -36,7 +36,7 @@
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload <span class="property-type">string | buffer</span></dt>
<dd> most users prefer simple text payloads, but binary buffers can also be published.</dd>
<dd> the payload to publish. If this property is not set, no message will be sent. To send a blank message, set this property to an empty String.</dd>
<dt class="optional">topic <span class="property-type">string</span></dt>
<dd> the MQTT topic to publish to.</dd>

View File

@ -36,7 +36,7 @@
<dt class="optional">followRedirects</dt>
<dd>If set to <code>false</code> prevent following Redirect (HTTP 301).<code>true</code> by default</dd>
<dt class="optional">requestTimeout</dt>
<dd>If set to a positive number, will override the globally set <code>httpRequestTimeout</code> parameter.</dd>
<dd>If set to a positive number of milliseconds, will override the globally set <code>httpRequestTimeout</code> parameter.</dd>
</dl>
<h3>Outputs</h3>
<dl class="message-properties">

View File

@ -604,6 +604,7 @@
"rule": "条件",
"repair": "メッセージ列の補正"
},
"previous": "前回の値",
"and": "",
"checkall": "全ての条件を適用",
"stopfirst": "最初に合致した条件で終了",
@ -856,6 +857,7 @@
"custom": "手動"
},
"combine": "結合",
"completeMessage": "メッセージ全体",
"create": "出力",
"type": {
"string": "文字列",

View File

@ -33,6 +33,8 @@
<dd><code>false</code>をセットすると、自己署名証明書を使用するhttpsサイトへのリクエストを許可します。</dd>
<dt class="optional">followRedirects</dt>
<dd><code>false</code>をセットすると、リダイレクトを行いません。デフォルトは<code>true</code>です。</dd>
<dt class="optional">requestTimeout</dt>
<dd>正のミリ秒数をセットすると、 グローバルに設定された<code>httpRequestTimeout</code>パラメータを上書きします。</dd>
</dl>
<h3>出力</h3>
<dl class="message-properties">

View File

@ -98,7 +98,7 @@
<p><i>合計値</i>」には出力メッセージを送信する前に受信すべきメッセージ数を指定します。オブジェクト出力の場合、この合計値に達すると後続メッセージの到着毎にメッセージを出力するように設定することもできます。</p>
<p><i></i>」には新規メッセージを送信するまでの経過時間を設定します。</p>
<p><code>msg.complete</code>プロパティを設定したメッセージを受信すると、出力メッセージを送信します。この時、メッセージ列の数をリセットします。</p>
<p><code>msg.reset</code>プロパティを設定したメッセージを受すると、部分的に受信済みのメッセージを破棄します。これらのメッセージは送信されません。この時、メッセージ列の数をリセットします。</p>
<p><code>msg.reset</code>プロパティを設定したメッセージを受すると、部分的に受信済みのメッセージを破棄します。これらのメッセージは送信されません。この時、メッセージ列の数をリセットします。</p>
<h4>列の集約モード</h4>
<p>列の集約モードを選択すると、メッセージ列を構成する各々のメッセージに対して式を適用し、集約した値を用いて一つのメッセージを構成します。</p>

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/nodes",
"version": "1.0.2",
"version": "1.0.3",
"license": "Apache-2.0",
"repository": {
"type": "git",
@ -22,12 +22,12 @@
"cookie-parser": "1.4.4",
"cookie": "0.4.0",
"cors": "2.8.5",
"cron": "1.7.1",
"cron": "1.7.2",
"denque": "1.4.1",
"fs-extra": "8.1.0",
"fs.notify": "0.0.4",
"hash-sum": "2.0.0",
"https-proxy-agent": "2.2.2",
"https-proxy-agent": "2.2.4",
"is-utf8": "0.2.1",
"js-yaml": "3.13.1",
"media-typer": "1.1.0",
@ -38,7 +38,7 @@
"raw-body": "2.4.1",
"request": "2.88.0",
"ws": "6.2.1",
"xml2js": "0.4.19",
"xml2js": "0.4.22",
"iconv-lite": "0.5.0"
}
}

View File

@ -32,6 +32,7 @@ var paletteEditorEnabled = false;
var settings;
var moduleRe = /^(@[^/]+?[/])?[^/]+?$/;
var slashRe = process.platform === "win32" ? /\\|[/]/ : /[/]/;
var pkgurlRe = /^(https?|git(|\+https?|\+ssh|\+file)):\/\//;
function init(runtime) {
events = runtime.events;
@ -76,14 +77,17 @@ function checkExistingModule(module,version) {
}
return false;
}
function installModule(module,version) {
function installModule(module,version,url) {
activePromise = activePromise.then(() => {
//TODO: ensure module is 'safe'
return new Promise((resolve,reject) => {
var installName = module;
var isUpgrade = false;
try {
if (moduleRe.test(module)) {
if (url && pkgurlRe.test(url)) {
// Git remote url or Tarball url - check the valid package url
installName = url;
} else if (moduleRe.test(module)) {
// Simple module name - assume it can be npm installed
if (version) {
installName += "@"+version;

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/registry",
"version": "1.0.2",
"version": "1.0.3",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@ -16,9 +16,9 @@
}
],
"dependencies": {
"@node-red/util": "1.0.2",
"@node-red/util": "1.0.3",
"semver": "6.3.0",
"uglify-js": "3.6.0",
"uglify-js": "3.6.9",
"when": "3.7.8"
}
}

View File

@ -159,6 +159,7 @@ var api = module.exports = {
* @param {User} opts.user - the user calling the api
* @param {String} opts.module - the id of the module to install
* @param {String} opts.version - (optional) the version of the module to install
* @param {String} opts.url - (optional) url to install
* @param {Object} opts.req - the request to log (optional)
* @return {Promise<ModuleInfo>} - the node module info
* @memberof @node-red/runtime_nodes
@ -183,20 +184,20 @@ var api = module.exports = {
return reject(err);
}
}
runtime.nodes.installModule(opts.module,opts.version).then(function(info) {
runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version}, opts.req);
runtime.nodes.installModule(opts.module,opts.version,opts.url).then(function(info) {
runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,url:opts.url}, opts.req);
return resolve(info);
}).catch(function(err) {
if (err.code === 404) {
runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,error:"not_found"}, opts.req);
runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,url:opts.url,error:"not_found"}, opts.req);
// TODO: code/status
err.status = 404;
} else if (err.code) {
err.status = 400;
runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,error:err.code}, opts.req);
runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,url:opts.url,error:err.code}, opts.req);
} else {
err.status = 400;
runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,error:err.code||"unexpected_error",message:err.toString()}, opts.req);
runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,url:opts.url,error:err.code||"unexpected_error",message:err.toString()}, opts.req);
}
return reject(err);
})

View File

@ -224,7 +224,7 @@ Node.prototype._emitInput = function(arg) {
}
);
} catch(err) {
node.error(err,msg);
node.error(err,arg);
}
}
}

View File

@ -200,6 +200,9 @@ class Subflow extends Flow {
self.node.status({text:text});
} else if (msg.status !== undefined) {
// if msg.status exists
if (msg.status.hasOwnProperty("text") && msg.status.text.indexOf("common.") === 0) {
msg.status.text = "node-red:"+msg.status.text;
}
self.node.status(msg.status)
}
})
@ -310,7 +313,6 @@ class Subflow extends Flow {
}
}
}
super.start(diff);
}
@ -435,7 +437,6 @@ class Subflow extends Flow {
}
return handled;
}
}

View File

@ -150,10 +150,10 @@ function reportNodeStateChange(info,enabled) {
}
}
function installModule(module,version) {
function installModule(module,version,url) {
var existingModule = registry.getModuleInfo(module);
var isUpgrade = !!existingModule;
return registry.installModule(module,version).then(function(info) {
return registry.installModule(module,version,url).then(function(info) {
if (isUpgrade) {
events.emit("runtime-event",{id:"node/upgraded",retain:false,payload:{module:module,version:version}});
} else {

View File

@ -56,8 +56,9 @@ function init(_settings, _runtime) {
if (settings.flowFile) {
flowsFile = settings.flowFile;
// handle Unix and Windows "C:\"
if ((flowsFile[0] == "/") || (flowsFile[1] == ":")) {
// handle Unix and Windows "C:\" and Windows "\\" for UNC.
if (fspath.isAbsolute(flowsFile)) {
//if (((flowsFile[0] == "\\") && (flowsFile[1] == "\\")) || (flowsFile[0] == "/") || (flowsFile[1] == ":")) {
// Absolute path
flowsFullPath = flowsFile;
} else if (flowsFile.substring(0,2) === "./") {

View File

@ -15,6 +15,7 @@
**/
var fs = require('fs-extra');
var fspath = require('path');
var when = require('when');
var nodeFn = require('when/node/function');
@ -85,6 +86,11 @@ module.exports = {
}
}
return when.promise(function(resolve,reject) {
fs.ensureDir(fspath.dirname(path), (err)=>{
if (err) {
reject(err);
return;
}
var stream = fs.createWriteStream(path);
stream.on('open',function(fd) {
stream.write(content,'utf8',function() {
@ -100,6 +106,7 @@ module.exports = {
reject(err);
});
});
});
},
readFile: readFile,

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/runtime",
"version": "1.0.2",
"version": "1.0.3",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@ -16,8 +16,8 @@
}
],
"dependencies": {
"@node-red/registry": "1.0.2",
"@node-red/util": "1.0.2",
"@node-red/registry": "1.0.3",
"@node-red/util": "1.0.3",
"clone": "2.1.2",
"express": "4.17.1",
"fs-extra": "8.1.0",

View File

@ -81,35 +81,55 @@ function mergeCatalog(fallback,catalog) {
}
}
var MessageFileLoader = {
type: "backend",
init: function(services, backendOptions, i18nextOptions) {},
read: function(lng, ns, callback) {
if (resourceMap[ns]) {
function readFile(lng, ns) {
return new Promise((resolve, reject) => {
if (resourceCache[ns] && resourceCache[ns][lng]) {
resolve(resourceCache[ns][lng]);
} else if (resourceMap[ns]) {
var file = path.join(resourceMap[ns].basedir, lng, resourceMap[ns].file);
//console.log(file);
fs.readFile(file, "utf8", function (err, content) {
if (err) {
callback(err);
reject(err);
} else {
try {
resourceCache[ns] = resourceCache[ns] || {};
resourceCache[ns][lng] = JSON.parse(content.replace(/^\uFEFF/, ''));
//console.log(resourceCache[ns][lng]);
var baseLng = lng.split('-')[0];
if (baseLng !== lng && resourceCache[ns][baseLng]) {
mergeCatalog(resourceCache[ns][baseLng], resourceCache[ns][lng]);
}
if (lng !== defaultLang) {
mergeCatalog(resourceCache[ns][defaultLang], resourceCache[ns][lng]);
}
callback(null, resourceCache[ns][lng]);
resolve(resourceCache[ns][lng]);
} catch (e) {
callback(e);
reject(e);
}
}
});
} else {
callback(new Error("Unrecognised namespace"));
reject(new Error("Unrecognised namespace"));
}
});
}
var MessageFileLoader = {
type: "backend",
init: function (services, backendOptions, i18nextOptions) { },
read: function (lng, ns, callback) {
readFile(lng, ns)
.then(data => callback(null, data))
.catch(err => {
if (/-/.test(lng)) {
// if reading language file fails -> try reading base language (e. g. 'fr' instead of 'fr-FR' or 'de' for 'de-DE')
var baseLng = lng.split('-')[0];
readFile(baseLng, ns).then(baseData => callback(null, baseData)).catch(err => callback(err));
} else {
callback(err);
}
});
}
}
function getCurrentLocale() {

View File

@ -84,9 +84,14 @@ var consoleLogger = function(msg) {
util.log("["+levelNames[msg.level]+"] "+(msg.type?"["+msg.type+":"+(msg.name||msg.id)+"] ":"")+msg.msg.stack);
} else {
var message = msg.msg;
try {
if (typeof message === 'object' && message !== null && message.toString() === '[object Object]' && message.message) {
message = message.message;
}
} catch(e){
message = 'Exception trying to log: '+util.inspect(message);
}
util.log("["+levelNames[msg.level]+"] "+(msg.type?"["+msg.type+":"+(msg.name||msg.id)+"] ":"")+message);
}
}

View File

@ -652,6 +652,7 @@ function normaliseNodeTypeName(name) {
* @memberof @node-red/util_util
*/
function encodeObject(msg,opts) {
try {
var debuglength = 1000;
if (opts && opts.hasOwnProperty('maxLength')) {
debuglength = opts.maxLength;
@ -754,7 +755,7 @@ function encodeObject(msg,opts) {
}," ");
} else {
try { msg.msg = msg.msg.toString(); }
catch(e) { msg.msg = "[Type not printable]"; }
catch(e) { msg.msg = "[Type not printable]" + util.inspect(msg.msg); }
}
}
} else if (msgType === "function") {
@ -776,6 +777,23 @@ function encodeObject(msg,opts) {
}
}
return msg;
} catch(e) {
msg.format = "error";
var errorMsg = {};
if (e.name) {
errorMsg.name = e.name;
}
if (e.hasOwnProperty('message')) {
errorMsg.message = 'encodeObject Error: ['+e.message + '] Value: '+util.inspect(msg.msg);
} else {
errorMsg.message = 'encodeObject Error: ['+e.toString() + '] Value: '+util.inspect(msg.msg);
}
if (errorMsg.message.length > debuglength) {
errorMsg.message = errorMsg.message.substring(0,debuglength);
}
msg.msg = JSON.stringify(errorMsg);
return msg;
}
}
module.exports = {

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/util",
"version": "1.0.2",
"version": "1.0.3",
"license": "Apache-2.0",
"repository": {
"type": "git",
@ -18,7 +18,7 @@
"clone": "2.1.2",
"i18next": "15.1.2",
"json-stringify-safe": "5.0.1",
"jsonata": "1.6.5",
"jsonata": "1.7.0",
"when": "3.7.8"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "node-red",
"version": "1.0.2",
"version": "1.0.3",
"description": "Low-code programming for event-driven applications",
"homepage": "http://nodered.org",
"license": "Apache-2.0",
@ -31,10 +31,10 @@
"flow"
],
"dependencies": {
"@node-red/editor-api": "1.0.2",
"@node-red/runtime": "1.0.2",
"@node-red/util": "1.0.2",
"@node-red/nodes": "1.0.2",
"@node-red/editor-api": "1.0.3",
"@node-red/runtime": "1.0.3",
"@node-red/util": "1.0.3",
"@node-red/nodes": "1.0.3",
"basic-auth": "2.0.1",
"bcryptjs": "2.4.3",
"express": "4.17.1",

View File

@ -4,4 +4,4 @@ npm install --no-save \
wdio-mocha-framework@^0.6.4 \
wdio-spec-reporter@^0.1.5 \
webdriverio@^4.14.1 \
chromedriver@2
chromedriver@^78.0.1

View File

@ -61,7 +61,7 @@ exports.config = {
maxInstances: 2,
//
browserName: 'chrome',
chromeOptions: {
'goog:chromeOptions': {
args: process.env.NODE_RED_NON_HEADLESS
// Runs tests with opening a browser.
? ['--disable-gpu']
@ -134,7 +134,7 @@ exports.config = {
// Services take over a specific job you don't want to take care of. They enhance
// your test setup with almost no effort. Unlike plugins, they don't add new
// commands. Instead, they hook themselves up into the test process.
port: '9515',
port: 9515,
path: '/',
services: ['chromedriver'],
//

View File

@ -251,7 +251,8 @@ describe('delay Node', function() {
var helperNode1 = helper.getNode("helperNode1");
var receivedMessagesStack = [];
var rate = 1000/aLimit;
// Add a small grace to the calculated delay
var rate = 1000/aLimit + 10;
var receiveTimestamp;

View File

@ -227,7 +227,7 @@ describe("api/admin/nodes", function() {
});
request(app)
.post('/nodes')
.send({module: 'foo',version:"1.2.3"})
.send({module: 'foo',version:"1.2.3",url:"https://example/foo-1.2.3.tgz"})
.expect(200)
.end(function(err,res) {
if (err) {
@ -238,6 +238,7 @@ describe("api/admin/nodes", function() {
res.body.nodes[0].should.have.property("id","123");
opts.should.have.property("module","foo");
opts.should.have.property("version","1.2.3");
opts.should.have.property("url","https://example/foo-1.2.3.tgz");
done();
});
});
@ -256,7 +257,7 @@ describe("api/admin/nodes", function() {
});
request(app)
.post('/nodes')
.send({module: 'foo',version:"1.2.3"})
.send({module: 'foo',version:"1.2.3",url:"https://example/foo-1.2.3.tgz"})
.expect(400)
.end(function(err,res) {
if (err) {

View File

@ -121,6 +121,17 @@ describe('nodes/registry/installer', function() {
done();
});
});
it("rejects when update requested to existing version and url", function(done) {
sinon.stub(typeRegistry,"getModuleInfo", function() {
return {
version: "0.1.1"
}
});
installer.installModule("this_wont_exist","0.1.1","https://example/foo-0.1.1.tgz").catch(function(err) {
err.code.should.be.eql('module_already_loaded');
done();
});
});
it("rejects with generic error", function(done) {
var res = {
code: 1,
@ -201,6 +212,29 @@ describe('nodes/registry/installer', function() {
done(err);
});
});
it("succeeds when url is valid node-red module", function(done) {
var nodeInfo = {nodes:{module:"foo",types:["a"]}};
var res = {
code: 0,
stdout:"",
stderr:""
}
var p = Promise.resolve(res);
p.catch((err)=>{});
initInstaller(p)
var addModule = sinon.stub(registry,"addModule",function(md) {
return when.resolve(nodeInfo);
});
installer.installModule("this_wont_exist",null,"https://example/foo-0.1.1.tgz").then(function(info) {
info.should.eql(nodeInfo);
done();
}).catch(function(err) {
done(err);
});
});
});
describe("uninstalls module", function() {

View File

@ -298,7 +298,7 @@ describe('Subflow', function() {
// // stoppedNodes.should.have.a.property(sfConfigId);
done();
});
},50);
},150);
});
it("instantiates a subflow inside a subflow and stops it",function(done) {
var config = flowUtils.parseConfig([
@ -331,7 +331,7 @@ describe('Subflow', function() {
Object.keys(currentNodes).should.have.length(0);
done();
});
},50);
},150);
});
it("rewires a subflow node on update/start",function(done){
@ -391,8 +391,8 @@ describe('Subflow', function() {
flow.stop().then(function() {
done();
});
},50);
},50);
},150);
},150);
});
});
describe('#stop', function() {
@ -452,7 +452,7 @@ describe('Subflow', function() {
flow.stop().then(function() {
done();
});
},50);
},150);
});
it("passes a status event to the subflow's parent tab status node - targetted scope",function(done) {
var config = flowUtils.parseConfig([
@ -491,7 +491,7 @@ describe('Subflow', function() {
done();
});
},50);
},150);
});
});
@ -536,7 +536,7 @@ describe('Subflow', function() {
done();
});
},50);
},150);
});
it("emits a status event when a message is passed to a subflow-status node - msg.payload as status obj", function(done) {
var config = flowUtils.parseConfig([
@ -578,7 +578,7 @@ describe('Subflow', function() {
done();
});
},50);
},150);
});
it("emits a status event when a message is passed to a subflow-status node - msg.status", function(done) {
var config = flowUtils.parseConfig([
@ -620,7 +620,7 @@ describe('Subflow', function() {
done();
});
},50);
},150);
});
it("does not emit a regular status event if it contains a subflow-status node", function(done) {
var config = flowUtils.parseConfig([
@ -690,7 +690,7 @@ describe('Subflow', function() {
flow.stop().then(function() {
done();
});
},50);
},150);
});
it("passes an error event to the subflow's parent tab catch node - targetted scope",function(done) {
var config = flowUtils.parseConfig([
@ -727,7 +727,7 @@ describe('Subflow', function() {
flow.stop().then(function() {
done();
});
},50);
},150);
});
});
@ -777,7 +777,7 @@ describe('Subflow', function() {
flow.stop().then(function() {
done();
});
},50);
},150);
});
it("can access subflow env var", function(done) {
@ -817,7 +817,7 @@ describe('Subflow', function() {
flow.stop().then(function() {
done();
});
},50);
},150);
});
it("can access nested subflow env var", function(done) {
@ -875,9 +875,9 @@ describe('Subflow', function() {
flow.stop().then(function() {
done();
});
},50);
},50);
},50);
},150);
},150);
},150);
});
});

View File

@ -19,6 +19,7 @@ var fs = require('fs-extra');
var path = require('path');
var sinon = require('sinon');
var NR_TEST_UTILS = require("nr-test-utils");
var process = require("process");
var localfilesystem = NR_TEST_UTILS.require("@node-red/runtime/lib/storage/localfilesystem");
var log = NR_TEST_UTILS.require("@node-red/util").log;
@ -474,4 +475,44 @@ describe('storage/localfilesystem', function() {
done(err);
});
});
it('should handle flow file in random unc path and non-existent subfolder',function(done) {
// only test on win32
if (process.platform !== 'win32') {
console.log('skipped test as not win32');
done();
return;
}
// get a real windows path
var flowFile = path.win32.resolve(userDir+'/some/random/path');
var rootdir = path.win32.resolve(userDir+'/some');
// make it into a local UNC path
flowFile = flowFile.replace('C:\\', '\\\\localhost\\c$\\');
localfilesystem.init({userDir:userDir, flowFile:flowFile}, mockRuntime).then(function() {
fs.existsSync(flowFile).should.be.false();
localfilesystem.saveFlows(testFlow).then(function() {
fs.existsSync(flowFile).should.be.true();
localfilesystem.getFlows().then(function(flows) {
flows.should.eql(testFlow);
// cleanup
fs.removeSync(rootdir);
done();
}).catch(function(err) {
// cleanup
fs.removeSync(rootdir);
done(err);
});
}).catch(function(err) {
// cleanup
fs.removeSync(rootdir);
done(err);
});
}).catch(function(err) {
// cleanup
fs.removeSync(rootdir);
done(err);
});
});
});

View File

@ -224,5 +224,29 @@ describe("@node-red/util/log", function() {
});
it('it can log without exception', function() {
var msg = {
msg: {
mystrangeobj:"hello",
},
};
msg.msg.toString = function(){
throw new Error('Exception in toString - should have been caught');
}
msg.msg.constructor = { name: "strangeobj" };
var ret = log.info(msg.msg);
});
it('it can log an object but use .message', function() {
var msg = {
msg: {
message: "my special message",
mystrangeobj:"hello",
},
};
var ret = log.info(msg.msg);
sinon.assert.calledWithMatch(util.log,"my special message");
});
});

View File

@ -772,6 +772,117 @@ describe("@node-red/util/util", function() {
var resultJson = JSON.parse(result.msg);
resultJson.socket.should.eql('[internal]');
});
it('object which fails to serialise', function(done) {
var msg = {
msg: {
obj:{
cantserialise:{
message:'this will not be displayed',
toJSON: function(val) {
throw 'this exception should have been caught';
return 'should not display because we threw first';
},
},
canserialise:{
message:'this should be displayed',
}
},
}
};
var result = util.encodeObject(msg);
result.format.should.eql("error");
var success = (result.msg.indexOf('cantserialise') > 0);
success &= (result.msg.indexOf('this exception should have been caught') > 0);
success &= (result.msg.indexOf('canserialise') > 0);
success.should.eql(1);
done();
});
it('object which fails to serialise - different error type', function(done) {
var msg = {
msg: {
obj:{
cantserialise:{
message:'this will not be displayed',
toJSON: function(val) {
throw new Error('this exception should have been caught');
return 'should not display because we threw first';
},
},
canserialise:{
message:'this should be displayed',
}
},
}
};
var result = util.encodeObject(msg);
result.format.should.eql("error");
var success = (result.msg.indexOf('cantserialise') > 0);
success &= (result.msg.indexOf('this exception should have been caught') > 0);
success &= (result.msg.indexOf('canserialise') > 0);
success.should.eql(1);
done();
});
it('very large object which fails to serialise should be truncated', function(done) {
var msg = {
msg: {
obj:{
big:"",
cantserialise:{
message:'this will not be displayed',
toJSON: function(val) {
throw new Error('this exception should have been caught');
return 'should not display because we threw first';
},
},
canserialise:{
message:'this should be displayed',
}
},
}
};
for (var i = 0; i < 1000; i++) {
msg.msg.obj.big += 'some more string ';
}
var result = util.encodeObject(msg);
result.format.should.eql("error");
var resultJson = JSON.parse(result.msg);
var success = (resultJson.message.length <= 1000);
success.should.eql(true);
done();
});
it('test bad toString', function(done) {
var msg = {
msg: {
mystrangeobj:"hello",
},
};
msg.msg.toString = function(){
throw new Error('Exception in toString - should have been caught');
}
msg.msg.constructor = { name: "strangeobj" };
var result = util.encodeObject(msg);
var success = (result.msg.indexOf('[Type not printable]') >= 0);
success.should.eql(true);
done();
});
it('test bad object constructor', function(done) {
var msg = {
msg: {
mystrangeobj:"hello",
constructor: {
get name(){
throw new Error('Exception in constructor name');
}
}
},
};
var result = util.encodeObject(msg);
done();
});
});
});
});