diff --git a/CHANGELOG.md b/CHANGELOG.md index fdbb4ada9..7af2084c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +### 1.2.8: Maintenance Release + +Editor + + - Ensure subflow help is picked up for palette tooltip Fixes #2834 + - Improve Ru locale (#2826) @alexk111 + - Fix scrollbars (#2825) @alexk111 + +Runtime + + - Restrict project file access to inside the project directory + - Validate user-provided language parameter before passing to i18n + - Fix grunt release mkdir issue on Node.js 14 (#2827) @alexk111 + - Prevent crash when coreNodesDir is empty (#2831) @hardillb + +Nodes + + - Batch node: Fixing minor typo in node's documentation (#2848) @matthiasradde + - Split node: Handle out of order messages as long as one of the messages has msg.parts.count set to the proper value (#2748) @s4ke + ### 1.2.7: Maintenance Release Editor diff --git a/Gruntfile.js b/Gruntfile.js index 2bdc2e3aa..23481f793 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -142,6 +142,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/text/bidi.js", "packages/node_modules/@node-red/editor-client/src/js/text/format.js", "packages/node_modules/@node-red/editor-client/src/js/ui/state.js", + "packages/node_modules/@node-red/editor-client/src/js/plugins.js", "packages/node_modules/@node-red/editor-client/src/js/nodes.js", "packages/node_modules/@node-red/editor-client/src/js/font-awesome.js", "packages/node_modules/@node-red/editor-client/src/js/history.js", diff --git a/package.json b/package.json index 82688f4e7..f7e4ea8bb 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "grunt-jsdoc": "2.4.1", "grunt-jsdoc-to-markdown": "5.0.0", "grunt-jsonlint": "2.1.3", - "grunt-mkdir": "~1.0.0", + "grunt-mkdir": "~1.1.0", "grunt-npm-command": "~0.1.2", "grunt-sass": "~3.1.0", "grunt-simple-mocha": "~0.4.1", diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/index.js b/packages/node_modules/@node-red/editor-api/lib/admin/index.js index 34c47b2cb..cf3505439 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/index.js @@ -22,6 +22,7 @@ var flow = require("./flow"); var context = require("./context"); var auth = require("../auth"); var info = require("./settings"); +var plugins = require("./plugins"); var apiUtil = require("../util"); @@ -32,6 +33,7 @@ module.exports = { nodes.init(runtimeAPI); context.init(runtimeAPI); info.init(settings,runtimeAPI); + plugins.init(runtimeAPI); var needsPermission = auth.needsPermission; @@ -80,6 +82,10 @@ module.exports = { adminApp.get("/settings",needsPermission("settings.read"),info.runtimeSettings,apiUtil.errorHandler); + // Plugins + adminApp.get("/plugins", needsPermission("plugins.read"), plugins.getAll, apiUtil.errorHandler); + adminApp.get("/plugins/messages", needsPermission("plugins.read"), plugins.getCatalogs, apiUtil.errorHandler); + return adminApp; } } diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/plugins.js b/packages/node_modules/@node-red/editor-api/lib/admin/plugins.js new file mode 100644 index 000000000..3d49a7e8a --- /dev/null +++ b/packages/node_modules/@node-red/editor-api/lib/admin/plugins.js @@ -0,0 +1,46 @@ +var apiUtils = require("../util"); + +var runtimeAPI; + +module.exports = { + init: function(_runtimeAPI) { + runtimeAPI = _runtimeAPI; + }, + getAll: function(req,res) { + var opts = { + user: req.user, + req: apiUtils.getRequestLogObject(req) + } + if (req.get("accept") == "application/json") { + runtimeAPI.plugins.getPluginList(opts).then(function(list) { + res.json(list); + }) + } else { + opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages()); + if (/[^a-z\-\*]/i.test(opts.lang)) { + res.json({}); + return; + } + runtimeAPI.plugins.getPluginConfigs(opts).then(function(configs) { + res.send(configs); + }) + } + }, + getCatalogs: function(req,res) { + var opts = { + user: req.user, + lang: req.query.lng, + req: apiUtils.getRequestLogObject(req) + } + if (/[^a-z\-\*]/i.test(opts.lang)) { + res.json({}); + return; + } + runtimeAPI.plugins.getPluginCatalogs(opts).then(function(result) { + res.json(result); + }).catch(function(err) { + console.log(err.stack); + apiUtils.rejectHandler(req,res,err); + }) + } +}; diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/index.js b/packages/node_modules/@node-red/editor-api/lib/editor/index.js index 71876eaa6..2e8333f3f 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/index.js @@ -76,7 +76,7 @@ module.exports = { editorApp.get("/icons/:scope/:module/:icon",ui.icon); var theme = require("./theme"); - theme.init(settings); + theme.init(settings, runtimeAPI); editorApp.use("/theme",theme.app()); editorApp.use("/",ui.editorResources); diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/locales.js b/packages/node_modules/@node-red/editor-api/lib/editor/locales.js index a7f300cd3..f9453f55b 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/locales.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/locales.js @@ -39,9 +39,12 @@ module.exports = { }, get: function(req,res) { var namespace = req.params[0]; - var lngs = req.query.lng; namespace = namespace.replace(/\.json$/,""); var lang = req.query.lng || i18n.defaultLang; //apiUtil.determineLangFromHeaders(req.acceptsLanguages() || []); + if (/[^a-z\-\*]/i.test(lang)) { + res.json({}); + return; + } var prevLang = i18n.i.language; // Trigger a load from disk of the language if it is not the default i18n.i.changeLanguage(lang, function(){ diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js index 17dbbafea..52f7974ec 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js @@ -41,6 +41,10 @@ var theme = null; var themeContext = clone(defaultContext); var themeSettings = null; +var activeTheme = null; +var activeThemeInitialised = false; + +var runtimeAPI; var themeApp; function serveFile(app,baseUrl,file) { @@ -58,7 +62,7 @@ function serveFile(app,baseUrl,file) { } } -function serveFilesFromTheme(themeValue, themeApp, directory) { +function serveFilesFromTheme(themeValue, themeApp, directory, baseDirectory) { var result = []; if (themeValue) { var array = themeValue; @@ -67,7 +71,14 @@ function serveFilesFromTheme(themeValue, themeApp, directory) { } for (var i=0;i theme.id); res.json(themeContext); }) @@ -185,10 +200,38 @@ module.exports = { themeSettings.projects = theme.projects; } - + if (theme.theme) { + themeSettings.theme = theme.theme; + } return themeApp; }, - context: function() { + context: async function() { + if (activeTheme && !activeThemeInitialised) { + const themePlugin = await runtimeAPI.plugins.getPlugin({ + id:activeTheme + }); + if (themePlugin) { + if (themePlugin.css) { + const cssFiles = serveFilesFromTheme( + themePlugin.css, + themeApp, + "/css/", + themePlugin.path + ); + themeContext.page.css = cssFiles.concat(themeContext.page.css || []) + } + if (themePlugin.scripts) { + const scriptFiles = serveFilesFromTheme( + themePlugin.scripts, + themeApp, + "/scripts/", + themePlugin.path + ) + themeContext.page.scripts = scriptFiles.concat(themeContext.page.scripts || []) + } + } + activeThemeInitialised = true; + } return themeContext; }, settings: function() { diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/ui.js b/packages/node_modules/@node-red/editor-api/lib/editor/ui.js index 24b819fec..a4812a668 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/ui.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/ui.js @@ -68,8 +68,8 @@ module.exports = { apiUtils.rejectHandler(req,res,err); }) }, - editor: function(req,res) { - res.send(Mustache.render(editorTemplate,theme.context())); + editor: async function(req,res) { + res.send(Mustache.render(editorTemplate,await theme.context())); }, editorResources: express.static(path.join(editorClientDir,'public')) }; diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index e4c585811..f90e30669 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -38,6 +38,7 @@ } }, "event": { + "loadPlugins": "Loading Plugins", "loadPalette": "Loading Palette", "loadNodeCatalogs": "Loading Node catalogs", "loadNodes": "Loading Nodes __count__", @@ -338,6 +339,7 @@ "output": "outputs:", "status": "status node", "deleteSubflow": "delete subflow", + "confirmDelete": "Are you sure you want to delete this subflow?", "info": "Description", "category": "Category", "module": "Module", @@ -397,6 +399,7 @@ "icon": "Icon", "inputType": "Input type", "selectType": "select types...", + "loadCredentials": "Loading node credentials", "inputs" : { "input": "input", "select": "select", @@ -431,7 +434,8 @@ }, "errors": { "scopeChange": "Changing the scope will make it unavailable to nodes in other flows that use it", - "invalidProperties": "Invalid properties:" + "invalidProperties": "Invalid properties:", + "credentialLoadFailed": "Failed to load node credentials" } }, "keyboard": { diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/jsonata.json b/packages/node_modules/@node-red/editor-client/locales/en-US/jsonata.json index 57ebb00a7..282dfe551 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/jsonata.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/jsonata.json @@ -243,19 +243,19 @@ "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": { + "$encodeUrlComponent": { "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": { + "$encodeUrl": { "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": { + "$decodeUrlComponent": { "args": "str", "desc": "Decodes a Uniform Resource Locator (URL) component previously created by encodeUrlComponent. \n\nExample: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`" }, - "$decodeUrlComponent": { + "$decodeUrl": { "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=шеллы\"`" }, diff --git a/packages/node_modules/@node-red/editor-client/locales/ja/jsonata.json b/packages/node_modules/@node-red/editor-client/locales/ja/jsonata.json index 02973a69a..7f96c747a 100644 --- a/packages/node_modules/@node-red/editor-client/locales/ja/jsonata.json +++ b/packages/node_modules/@node-red/editor-client/locales/ja/jsonata.json @@ -243,19 +243,19 @@ "args": "array, function", "desc": "`array`の要素のうち、条件判定関数`function`を満たす(`function`に与えた場合に真偽値`true`を返す)要素が1つのみである場合、それを返します。マッチする要素が1つのみでない場合、例外を送出します。\n\n指定する関数は`function(value [, index [, array]])`というシグネチャでなければなりません。ここで、`value`は`array`の要素値、`index`は要素の添字、第三引数には配列全体を渡します。" }, - "$encodeUrl": { + "$encodeUrlComponent": { "args": "str", "desc": "Uniform Resource Locator (URL)を構成する文字を1、2、3、もしくは、4文字エスケープシーケンスのUTF-8文字エンコーディングで置換します。\n\n例: `$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`" }, - "$encodeUrlComponent": { + "$encodeUrl": { "args": "str", "desc": "Uniform Resource Locator (URL)要素を構成する文字を1、2、3、もしくは、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": { + "$decodeUrlComponent": { "args": "str", "desc": "encodeUrlComponentで置換したUniform Resource Locator (URL)をデコードします。\n\n例: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`" }, - "$decodeUrlComponent": { + "$decodeUrl": { "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=шеллы\"`" }, diff --git a/packages/node_modules/@node-red/editor-client/locales/ru/editor.json b/packages/node_modules/@node-red/editor-client/locales/ru/editor.json index 1f91d4b0b..04ff361ba 100755 --- a/packages/node_modules/@node-red/editor-client/locales/ru/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/ru/editor.json @@ -246,8 +246,8 @@ "import": { "import": "Импортировать в", "importSelected": "Импортировать выбранные", - "importCopy": "Импортировать копию", - "viewNodes": "Посмотреть узлы...", + "importCopy": "Импортировать копии", + "viewNodes": "Показать узлы...", "newFlow": "новый поток", "replace": "заменить", "errors": { @@ -257,7 +257,7 @@ "missingType": "Недопустимый поток - у элемента __index__ отсутствует свойство 'type'" }, "conflictNotification1": "Некоторые импортируемые Вами узлы уже существуют в рабочей области.", - "conflictNotification2": "Выберите, какие узлы импортировать и следует ли заменить существующие узлы или импортировать их копию." + "conflictNotification2": "Выберите, какие узлы импортировать и следует ли заменить ими существующие узлы или импортировать их копии." }, "copyMessagePath": "Путь скопирован", "copyMessageValue": "Значение скопировано", @@ -373,12 +373,12 @@ "configAdd": "Добавить", "configUpdate": "Обновить", "configDelete": "Удалить", - "nodesUse": "__count__ узел использует эту конфигурацию", - "nodesUse_plural_2": "__count__ узла используют эту конфигурацию", - "nodesUse_plural_5": "__count__ узлов используют эту конфигурацию", - "addNewConfig": "Добавить новый конфигурационный узел __type__", + "nodesUse": "__count__ узел использует этот конфиг", + "nodesUse_plural_2": "__count__ узла используют этот конфиг", + "nodesUse_plural_5": "__count__ узлов используют этот конфиг", + "addNewConfig": "Добавить новый конфиг узел __type__", "editNode": "Изменить узел __type__", - "editConfig": "Изменить конфигурационный узел __type__", + "editConfig": "Изменить конфиг узел __type__", "addNewType": "Добавить новый __type__...", "nodeProperties": "свойства узла", "label": "Метка", @@ -615,7 +615,7 @@ "info": { "name": "Информация", "tabName": "Имя", - "label": "сведения", + "label": "инфо", "node": "Узел", "type": "Тип", "group": "Группа", @@ -648,8 +648,8 @@ "showTips":"Вы можете открыть советы из панели настроек", "outline": "Структура", "empty": "пусто", - "globalConfig": "Узлы глобальной конфигурации", - "triggerAction": "Запустить действие", + "globalConfig": "Глобальные конфиг узлы", + "triggerAction": "Вызвать действие", "find": "Найти в рабочей области", "search": { "configNodes": "Узлы конфигурации", @@ -671,8 +671,8 @@ }, "config": { "name": "Узлы конфигураций", - "label": "конфигурация", - "global": "На всех потока", + "label": "конфиг", + "global": "На всех потоках", "none": "нет", "subflows": "подпотоки", "flows": "потоки", @@ -690,8 +690,8 @@ "none": "ничего не выбрано", "refresh": "обновите, чтобы загрузить", "empty": "пусто", - "node": "Узел", - "flow": "Поток", + "node": "Узловой", + "flow": "Потоковый", "global": "Глобальный", "deleteConfirm": "Вы уверены, что хотите удалить этот элемент?", "autoRefresh": "Обновить при изменении выбора", @@ -877,7 +877,7 @@ "bool": "логический тип", "json": "JSON", "bin": "буфер", - "date": "отметка времени", + "date": "метка времени", "jsonata": "выражение", "env": "переменная среды", "cred": "учетные данные" diff --git a/packages/node_modules/@node-red/editor-client/locales/ru/jsonata.json b/packages/node_modules/@node-red/editor-client/locales/ru/jsonata.json index fd0ecf24c..920042c25 100755 --- a/packages/node_modules/@node-red/editor-client/locales/ru/jsonata.json +++ b/packages/node_modules/@node-red/editor-client/locales/ru/jsonata.json @@ -243,19 +243,19 @@ "args": "array, function", "desc": "Возвращает одно-единственное значение из массива `array`, которое удовлетворяет предикату `function` (то есть когда `function` возвращает логическое `true` при передаче значения). Выдает исключение, если число подходящих значений не одно.\n\nФункция должна соответствовать следующей сигнатуре: `function(value [, index [, array]])` где value - элемент массива, index - позиция этого значения, а весь массив передается в качестве третьего аргумента" }, - "$encodeUrl": { + "$encodeUrlComponent": { "args": "str", "desc": "Кодирует компонент Uniform Resource Locator (URL), заменяя каждый экземпляр определенных символов одной, двумя, тремя или четырьмя escape-последовательностями, представляющими кодировку UTF-8 символа.\n\nПример: `$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`" }, - "$encodeUrlComponent": { + "$encodeUrl": { "args": "str", "desc": "Кодирует Uniform Resource Locator (URL), заменяя каждый экземпляр определенных символов одной, двумя, тремя или четырьмя escape-последовательностями, представляющими кодировку UTF-8 символа.\n\nПример: `$encodeUrl(\"https://mozilla.org/?x=шеллы\")` => `\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\"`" }, - "$decodeUrl": { + "$decodeUrlComponent": { "args": "str", "desc": "Декодирует компонент Uniform Resource Locator (URL), ранее созданный с помощью encodeUrlComponent.\n\nПример: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`" }, - "$decodeUrlComponent": { + "$decodeUrl": { "args": "str", "desc": "Декодирует компонент Uniform Resource Locator (URL), ранее созданный с помощью encodeUrl. \n\nПример: `$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")` => `\"https://mozilla.org/?x=шеллы\"`" }, diff --git a/packages/node_modules/@node-red/editor-client/locales/zh-CN/jsonata.json b/packages/node_modules/@node-red/editor-client/locales/zh-CN/jsonata.json index a9e6a7b1f..b4403e318 100644 --- a/packages/node_modules/@node-red/editor-client/locales/zh-CN/jsonata.json +++ b/packages/node_modules/@node-red/editor-client/locales/zh-CN/jsonata.json @@ -243,19 +243,19 @@ "args": "array, function", "desc": "返回满足参数function谓语的array参数中的唯一值 (比如:传递值时,函数返回布尔值“true”)。如果匹配值的数量不唯一时,则抛出异常。\n\n应在以下签名中提供函数: `function(value [,index [,array []]])` 其中value是数组的每个输入,index是该值的位置,整个数组作为第三个参数传递。" }, - "$encodeUrl": { + "$encodeUrlComponent": { "args": "str", "desc": "通过用表示字符的UTF-8编码的一个,两个,三个或四个转义序列替换某些字符的每个实例,对统一资源定位符(URL)组件进行编码。\n\n示例: `$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`" }, - "$encodeUrlComponent": { + "$encodeUrl": { "args": "str", "desc": "通过用表示字符的UTF-8编码的一个,两个,三个或四个转义序列替换某些字符的每个实例,对统一资源定位符(URL)进行编码。\n\n示例: `$encodeUrl(\"https://mozilla.org/?x=шеллы\")` => `\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\"`" }, - "$decodeUrl": { + "$decodeUrlComponent": { "args": "str", "desc": "解码以前由encodeUrlComponent创建的统一资源定位器(URL)组件。 \n\n示例: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`" }, - "$decodeUrlComponent": { + "$decodeUrl": { "args": "str", "desc": "解码先前由encodeUrl创建的统一资源定位符(URL)。 \n\n示例: `$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")` => `\"https://mozilla.org/?x=шеллы\"`" }, diff --git a/packages/node_modules/@node-red/editor-client/locales/zh-TW/jsonata.json b/packages/node_modules/@node-red/editor-client/locales/zh-TW/jsonata.json index 3765ab3dd..2b47c1af7 100644 --- a/packages/node_modules/@node-red/editor-client/locales/zh-TW/jsonata.json +++ b/packages/node_modules/@node-red/editor-client/locales/zh-TW/jsonata.json @@ -243,19 +243,19 @@ "args": "array, function", "desc": "返回滿足參數function謂語的array參數中的唯一值 (比如:傳遞值時,函數返回布林值“true”)。如果匹配值的數量不唯一時,則拋出異常。\n\n應在以下簽名中提供函數:`function(value [,index [,array []]])`其中value是數組的每個輸入,index是該值的位置,整個數組作為第三個參數傳遞。" }, - "$encodeUrl": { + "$encodeUrlComponent": { "args": "str", "desc": "通過用表示字符的UTF-8編碼的一個,兩個,三個或四個轉義序列替換某些字符的每個實例,對統一資源定位符(URL)組件進行編碼。\n\n示例:`$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`" }, - "$encodeUrlComponent": { + "$encodeUrl": { "args": "str", "desc": "通過用表示字符的UTF-8編碼的一個,兩個,三個或四個轉義序列替換某些字符的每個實例,對統一資源定位符(URL)進行編碼。\n\n示例: `$encodeUrl(\"https://mozilla.org/?x=шеллы\")` => `\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\"`" }, - "$decodeUrl": { + "$decodeUrlComponent": { "args": "str", "desc": "解碼以前由encodeUrlComponent創建的統一資源定位器(URL)組件。 \n\n示例: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`" }, - "$decodeUrlComponent": { + "$decodeUrl": { "args": "str", "desc": "解碼先前由encodeUrl創建的統一資源定位符(URL)。 \n\n示例: `$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")` => `\"https://mozilla.org/?x=шеллы\"`" }, diff --git a/packages/node_modules/@node-red/editor-client/src/js/history.js b/packages/node_modules/@node-red/editor-client/src/js/history.js index a3392fd26..7fedf84be 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/history.js +++ b/packages/node_modules/@node-red/editor-client/src/js/history.js @@ -343,17 +343,29 @@ RED.history = (function() { if (ev.changes.hasOwnProperty(i)) { inverseEv.changes[i] = ev.node[i]; if (ev.node._def.defaults && ev.node._def.defaults[i] && ev.node._def.defaults[i].type) { - // This is a config node property - var currentConfigNode = RED.nodes.node(ev.node[i]); - if (currentConfigNode) { - currentConfigNode.users.splice(currentConfigNode.users.indexOf(ev.node),1); - RED.events.emit("nodes:change",currentConfigNode); + // This property is a reference to another node or nodes. + var nodeList = ev.node[i]; + if (!Array.isArray(nodeList)) { + nodeList = [nodeList]; } - var newConfigNode = RED.nodes.node(ev.changes[i]); - if (newConfigNode) { - newConfigNode.users.push(ev.node); - RED.events.emit("nodes:change",newConfigNode); + nodeList.forEach(function(id) { + var currentConfigNode = RED.nodes.node(id); + if (currentConfigNode && currentConfigNode._def.category === "config") { + currentConfigNode.users.splice(currentConfigNode.users.indexOf(ev.node),1); + RED.events.emit("nodes:change",currentConfigNode); + } + }); + nodeList = ev.changes[i]; + if (!Array.isArray(nodeList)) { + nodeList = [nodeList]; } + nodeList.forEach(function(id) { + var newConfigNode = RED.nodes.node(id); + if (newConfigNode && newConfigNode._def.category === "config") { + newConfigNode.users.push(ev.node); + RED.events.emit("nodes:change",newConfigNode); + } + }); } ev.node[i] = ev.changes[i]; } diff --git a/packages/node_modules/@node-red/editor-client/src/js/i18n.js b/packages/node_modules/@node-red/editor-client/src/js/i18n.js index f1d0edfaf..ac439ecbc 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/i18n.js +++ b/packages/node_modules/@node-red/editor-client/src/js/i18n.js @@ -108,6 +108,31 @@ RED.i18n = (function() { } }); }) + }, + + loadPluginCatalogs: function(done) { + var languageList = i18n.functions.toLanguages(localStorage.getItem("editor-language")||i18n.detectLanguage()); + var toLoad = languageList.length; + + languageList.forEach(function(lang) { + $.ajax({ + headers: { + "Accept":"application/json" + }, + cache: false, + url: apiRootUrl+'plugins/messages?lng='+lang, + success: function(data) { + var namespaces = Object.keys(data); + namespaces.forEach(function(ns) { + i18n.addResourceBundle(lang,ns,data[ns]); + }); + toLoad--; + if (toLoad === 0) { + done(); + } + } + }); + }) } } })(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index 3e513de08..236f7fa57 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -164,6 +164,21 @@ RED.nodes = (function() { // TODO: too tightly coupled into palette UI } + if (def.defaults) { + for (var d in def.defaults) { + if (def.defaults.hasOwnProperty(d)) { + if (def.defaults[d].type) { + try { + def.defaults[d]._type = parseNodePropertyTypeString(def.defaults[d].type) + } catch(err) { + console.warn(err); + } + } + } + } + } + + RED.events.emit("registry:node-type-added",nt); }, removeNodeType: function(nt) { @@ -193,6 +208,59 @@ RED.nodes = (function() { return (1+Math.random()*4294967295).toString(16); } + function parseNodePropertyTypeString(typeString) { + typeString = typeString.trim(); + var c; + var pos = 0; + var isArray = /\[\]$/.test(typeString); + if (isArray) { + typeString = typeString.substring(0,typeString.length-2); + } + + var l = typeString.length; + var inBrackets = false; + var inToken = false; + var currentToken = ""; + var types = []; + while (pos < l) { + c = typeString[pos]; + if (inToken) { + if (c === "|") { + types.push(currentToken.trim()) + currentToken = ""; + inToken = false; + } else if (c === ")") { + types.push(currentToken.trim()) + currentToken = ""; + inBrackets = false; + inToken = false; + } else { + currentToken += c; + } + } else { + if (c === "(") { + if (inBrackets) { + throw new Error("Invalid character '"+c+"' at position "+pos) + } + inBrackets = true; + } else if (c !== " ") { + inToken = true; + currentToken = c; + } + } + pos++; + } + currentToken = currentToken.trim(); + if (currentToken.length > 0) { + types.push(currentToken) + } + return { + types: types, + array: isArray + } + } + + function addNode(n) { if (n.type.indexOf("subflow") !== 0) { n["_"] = n._def._; @@ -781,6 +849,12 @@ RED.nodes = (function() { subflowSet.push(n); } }); + RED.nodes.eachConfig(function(n) { + if (n.z == subflowId) { + subflowSet.push(n); + exportedConfigNodes[n.id] = true; + } + }); var exportableSubflow = createExportableNodeSet(subflowSet, exportedIds, exportedSubflows, exportedConfigNodes); nns = exportableSubflow.concat(nns); } @@ -788,16 +862,29 @@ RED.nodes = (function() { if (node.type !== "subflow") { var convertedNode = RED.nodes.convertNode(node); for (var d in node._def.defaults) { - if (node._def.defaults[d].type && node[d] in configNodes) { - var confNode = configNodes[node[d]]; - var exportable = registry.getNodeType(node._def.defaults[d].type).exportable; - if ((exportable == null || exportable)) { - if (!(node[d] in exportedConfigNodes)) { - exportedConfigNodes[node[d]] = true; - set.push(confNode); + if (node._def.defaults[d].type) { + var nodeList = node[d]; + if (!Array.isArray(nodeList)) { + nodeList = [nodeList]; + } + nodeList = nodeList.filter(function(id) { + if (id in configNodes) { + var confNode = configNodes[id]; + if (confNode._def.exportable !== false) { + if (!(id in exportedConfigNodes)) { + exportedConfigNodes[id] = true; + set.push(confNode); + return true; + } + } + return false; } + return true; + }) + if (nodeList.length === 0) { + convertedNode[d] = Array.isArray(node[d])?[]:"" } else { - convertedNode[d] = ""; + convertedNode[d] = Array.isArray(node[d])?nodeList:nodeList[0] } } } @@ -1588,15 +1675,6 @@ RED.nodes = (function() { } } } - // TODO: make this a part of the node definition so it doesn't have to - // be hardcoded here - var nodeTypeArrayReferences = { - "catch":"scope", - "status":"scope", - "complete": "scope", - "link in":"links", - "link out":"links" - } // Remap all wires and config node references for (i=0;i)/); + var totalCount = configs.length; + var stepConfig = function() { + // loader.reportProgress(RED._("event.loadNodes",{count:(totalCount-configs.length)+"/"+totalCount}), 30 + ((totalCount-configs.length)/totalCount)*40 ) + if (configs.length === 0) { + done(); + } else { + var config = configs.shift(); + appendPluginConfig(config,stepConfig); + } + } + stepConfig(); + } + }); + } + + function appendConfig(config, moduleIdMatch, targetContainer, done) { done = done || function(){}; - var m = //.exec(nodeConfig.trim()); var moduleId; - if (m) { - moduleId = m[1]; + if (moduleIdMatch) { + moduleId = moduleIdMatch[1]; + RED._loadingModule = moduleId; } else { moduleId = "unknown"; } try { var hasDeferred = false; - - var nodeConfigEls = $("
"+nodeConfig+"
"); + var nodeConfigEls = $("
"+config+"
"); var scripts = nodeConfigEls.find("script"); var scriptCount = scripts.length; scripts.each(function(i,el) { @@ -38,14 +84,15 @@ var RED = (function() { newScript.onload = function() { scriptCount--; if (scriptCount === 0) { - $("#red-ui-editor-node-configs").append(nodeConfigEls); + $(targetContainer).append(nodeConfigEls); + delete RED._loadingModule; done() } } if ($(el).attr('type') === "module") { newScript.type = "module"; } - $("#red-ui-editor-node-configs").append(newScript); + $(targetContainer).append(newScript); newScript.src = RED.settings.apiRootUrl+srcUrl; hasDeferred = true; } else { @@ -61,7 +108,8 @@ var RED = (function() { } }) if (!hasDeferred) { - $("#red-ui-editor-node-configs").append(nodeConfigEls); + $(targetContainer).append(nodeConfigEls); + delete RED._loadingModule; done(); } } catch(err) { @@ -70,9 +118,27 @@ var RED = (function() { timeout: 10000 }); console.log("["+moduleId+"] "+err.toString()); + delete RED._loadingModule; done(); } } + function appendPluginConfig(pluginConfig,done) { + appendConfig( + pluginConfig, + //.exec(pluginConfig.trim()), + "#red-ui-editor-plugin-configs", + done + ); + } + + function appendNodeConfig(nodeConfig,done) { + appendConfig( + nodeConfig, + //.exec(nodeConfig.trim()), + "#red-ui-editor-node-configs", + done + ); + } function loadNodeList() { loader.reportProgress(RED._("event.loadPalette"), 20) @@ -186,6 +252,7 @@ var RED = (function() { RED.workspaces.show(currentHash.substring(6)); } } catch(err) { + console.warn(err); RED.notify( RED._("event.importError", {message: err.message}), { @@ -593,7 +660,7 @@ var RED = (function() { RED.actions.add("core:show-about", showAbout); - loadNodeList(); + loadPluginList(); } @@ -609,6 +676,7 @@ var RED = (function() { '
'+ '
'+ '').appendTo(options.target); + $('
').appendTo(options.target); $('
').appendTo(options.target); $('
').appendTo(options.target); @@ -627,9 +695,12 @@ var RED = (function() { $('').html(theme.header.title).appendTo(logo); } } + if (theme.themes) { + knownThemes = theme.themes; + } }); } - + var knownThemes = null; var initialised = false; function init(options) { @@ -649,7 +720,13 @@ var RED = (function() { buildEditor(options); RED.i18n.init(options, function() { - RED.settings.init(options, loadEditor); + RED.settings.init(options, function() { + if (knownThemes) { + RED.settings.editorTheme = RED.settings.editorTheme || {}; + RED.settings.editorTheme.themes = knownThemes; + } + loadEditor(); + }); }) } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js index e46c88b4f..4ed618f6d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js @@ -72,9 +72,7 @@ RED.clipboard = (function() { text: RED._("clipboard.export.copy"), click: function() { if (activeTab === "red-ui-clipboard-dialog-export-tab-clipboard") { - $("#red-ui-clipboard-dialog-export-text").select(); - document.execCommand("copy"); - document.getSelection().removeAllRanges(); + copyText($("#red-ui-clipboard-dialog-export-text").val()); RED.notify(RED._("clipboard.nodesExported"),{id:"clipboard"}); $( this ).dialog( "close" ); } else { @@ -222,14 +220,22 @@ RED.clipboard = (function() { ''+ '
'+ '
'+ - '
'+ - ''+ + '
'+ + '
    '+ '
    '+ - '
    '+ - ''+ - ''+ - ''+ - ''+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + ''+ + '
    '+ + '
    '+ + ''+ + ''+ + ''+ + ''+ + '
    '+ '
    '+ '
    '+ '
    '+ @@ -592,6 +598,30 @@ RED.clipboard = (function() { }) loadFlowLibrary(libraryBrowser,"local",RED._("library.types.local")); + var clipboardTabs = RED.tabs.create({ + id: "red-ui-clipboard-dialog-export-tab-clipboard-tabs", + onchange: function(tab) { + $(".red-ui-clipboard-dialog-export-tab-clipboard-tab").hide(); + $("#" + tab.id).show(); + } + }); + + clipboardTabs.addTab({ + id: "red-ui-clipboard-dialog-export-tab-clipboard-preview", + label: RED._("clipboard.exportNodes") + }); + + clipboardTabs.addTab({ + id: "red-ui-clipboard-dialog-export-tab-clipboard-json", + label: RED._("editor.types.json") + }); + + + var previewList = $("#red-ui-clipboard-dialog-export-tab-clipboard-preview-list").css({position:"absolute",top:0,right:0,bottom:0,left:0}).treeList({ + data: [] + }) + refreshExportPreview(); + $("#red-ui-clipboard-dialog-tab-library-name").val("flows.json").select(); dialogContainer.i18n(); @@ -630,10 +660,10 @@ RED.clipboard = (function() { } $(this).parent().children().removeClass('selected'); $(this).addClass('selected'); - var type = $(this).attr('id'); + var type = $(this).attr('id').substring("red-ui-clipboard-dialog-export-rng-".length); var flow = ""; var nodes = null; - if (type === 'red-ui-clipboard-dialog-export-rng-selected') { + if (type === 'selected') { var selection = RED.workspaces.selection(); if (selection.length > 0) { nodes = []; @@ -647,14 +677,14 @@ RED.clipboard = (function() { } // Don't include the subflow meta-port nodes in the exported selection nodes = RED.nodes.createExportableNodeSet(nodes.filter(function(n) { return n.type !== 'subflow'})); - } else if (type === 'red-ui-clipboard-dialog-export-rng-flow') { + } else if (type === 'flow') { var activeWorkspace = RED.workspaces.active(); nodes = RED.nodes.groups(activeWorkspace); nodes = nodes.concat(RED.nodes.filterNodes({z:activeWorkspace})); var parentNode = RED.nodes.workspace(activeWorkspace)||RED.nodes.subflow(activeWorkspace); nodes.unshift(parentNode); nodes = RED.nodes.createExportableNodeSet(nodes); - } else if (type === 'red-ui-clipboard-dialog-export-rng-full') { + } else if (type === 'full') { nodes = RED.nodes.createCompleteNodeSet(false); } if (nodes !== null) { @@ -670,8 +700,10 @@ RED.clipboard = (function() { $("#red-ui-clipboard-dialog-export").addClass('disabled'); } $("#red-ui-clipboard-dialog-export-text").val(flow); - setTimeout(function() { $("#red-ui-clipboard-dialog-export-text").scrollTop(0); },50); - $("#red-ui-clipboard-dialog-export-text").trigger("focus"); + setTimeout(function() { + $("#red-ui-clipboard-dialog-export-text").scrollTop(0); + refreshExportPreview(type); + },50); }) $("#red-ui-clipboard-dialog-ok").hide(); @@ -717,6 +749,93 @@ RED.clipboard = (function() { } + function refreshExportPreview(type) { + + var flowData = $("#red-ui-clipboard-dialog-export-text").val() || "[]"; + var flow = JSON.parse(flowData); + var flows = {}; + var subflows = {}; + var nodes = []; + var nodesByZ = {}; + + var treeFlows = []; + var treeSubflows = []; + + flow.forEach(function(node) { + if (node.type === "tab") { + flows[node.id] = { + element: getFlowLabel(node,false), + deferBuild: type !== "flow", + expanded: type === "flow", + children: [] + }; + treeFlows.push(flows[node.id]) + } else if (node.type === "subflow") { + subflows[node.id] = { + element: getNodeLabel(node,false), + deferBuild: true, + children: [] + }; + treeSubflows.push(subflows[node.id]) + } else { + nodes.push(node); + } + }); + + var globalNodes = []; + var parentlessNodes = []; + + nodes.forEach(function(node) { + var treeNode = { + element: getNodeLabel(node, false, false) + }; + if (node.z) { + if (!flows[node.z] && !subflows[node.z]) { + parentlessNodes.push(treeNode) + } else if (flows[node.z]) { + flows[node.z].children.push(treeNode) + } else if (subflows[node.z]) { + subflows[node.z].children.push(treeNode) + } + } else { + globalNodes.push(treeNode); + } + }); + var treeData = []; + + if (parentlessNodes.length > 0) { + treeData = treeData.concat(parentlessNodes); + } + if (type === "flow") { + treeData = treeData.concat(treeFlows); + } else if (treeFlows.length > 0) { + treeData.push({ + label: RED._("menu.label.flows"), + deferBuild: treeFlows.length > 20, + expanded: treeFlows.length <= 20, + children: treeFlows + }) + } + if (treeSubflows.length > 0) { + treeData.push({ + label: RED._("menu.label.subflows"), + deferBuild: treeSubflows.length > 10, + expanded: treeSubflows.length <= 10, + children: treeSubflows + }) + } + if (globalNodes.length > 0) { + treeData.push({ + label: RED._("sidebar.info.globalConfig"), + deferBuild: globalNodes.length > 10, + expanded: globalNodes.length <= 10, + children: globalNodes + }) + } + + $("#red-ui-clipboard-dialog-export-tab-clipboard-preview-list").treeList('data',treeData); + } + function loadFlowLibrary(browser,library,label) { // if (includeExamples) { // listing.push({ @@ -756,6 +875,7 @@ RED.clipboard = (function() { } function copyText(value,element,msg) { var truncated = false; + var currentFocus = document.activeElement; if (typeof value !== "string" ) { value = JSON.stringify(value, function(key,value) { if (value !== null && typeof value === 'object') { @@ -787,7 +907,7 @@ RED.clipboard = (function() { if (truncated) { msg += "_truncated"; } - $("#red-ui-clipboard-hidden").val(value).select(); + $("#red-ui-clipboard-hidden").val(value).focus().select(); var result = document.execCommand("copy"); if (result && element) { var popover = RED.popover.create({ @@ -801,6 +921,10 @@ RED.clipboard = (function() { },1000); popover.open(); } + $("#red-ui-clipboard-hidden").val(""); + if (currentFocus) { + $(currentFocus).focus(); + } return result; } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js index e8c0daa39..82a81da95 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js @@ -444,16 +444,18 @@ RED.editor = (function() { for (var d in definition.defaults) { if (definition.defaults.hasOwnProperty(d)) { if (definition.defaults[d].type) { - var configTypeDef = RED.nodes.getType(definition.defaults[d].type); - if (configTypeDef) { - if (configTypeDef.exclusive) { - prepareConfigNodeButton(node,d,definition.defaults[d].type,prefix); + if (!definition.defaults[d]._type.array) { + var configTypeDef = RED.nodes.getType(definition.defaults[d].type); + if (configTypeDef && configTypeDef.category === 'config') { + if (configTypeDef.exclusive) { + prepareConfigNodeButton(node,d,definition.defaults[d].type,prefix); + } else { + prepareConfigNodeSelect(node,d,definition.defaults[d].type,prefix); + } } else { - prepareConfigNodeSelect(node,d,definition.defaults[d].type,prefix); + console.log("Unknown type:", definition.defaults[d].type); + preparePropertyEditor(node,d,prefix,definition.defaults); } - } else { - console.log("Unknown type:", definition.defaults[d].type); - preparePropertyEditor(node,d,prefix,definition.defaults); } } else { preparePropertyEditor(node,d,prefix,definition.defaults); @@ -494,11 +496,13 @@ RED.editor = (function() { populateCredentialsInputs(node, definition.credentials, node.credentials, prefix); completePrepare(); } else { - $.getJSON(getCredentialsURL(node.type, node.id), function (data) { - node.credentials = data; - node.credentials._ = $.extend(true,{},data); - if (!/^subflow:/.test(definition.type)) { - populateCredentialsInputs(node, definition.credentials, node.credentials, prefix); + getNodeCredentials(node.type, node.id, function(data) { + if (data) { + node.credentials = data; + node.credentials._ = $.extend(true,{},data); + if (!/^subflow:/.test(definition.type)) { + populateCredentialsInputs(node, definition.credentials, node.credentials, prefix); + } } completePrepare(); }); @@ -1086,8 +1090,11 @@ RED.editor = (function() { node.infoEditor = nodeInfoEditor; return nodeInfoEditor; } +var buildingEditDialog = false; function showEditDialog(node, defaultTab) { + if (buildingEditDialog) { return } + buildingEditDialog = true; var editing_node = node; var isDefaultIcon; var defaultIcon; @@ -1612,6 +1619,7 @@ RED.editor = (function() { if (defaultTab) { editorTabs.activateTab(defaultTab); } + buildingEditDialog = false; done(); }); }, @@ -1663,6 +1671,8 @@ RED.editor = (function() { * prefix - the input prefix of the parent property */ function showEditConfigNodeDialog(name,type,id,prefix) { + if (buildingEditDialog) { return } + buildingEditDialog = true; var adding = (id == "_ADD_"); var node_def = RED.nodes.getType(type); var editing_config_node = RED.nodes.node(id); @@ -1826,6 +1836,7 @@ RED.editor = (function() { trayBody.i18n(); trayFooter.i18n(); finishedBuilding = true; + buildingEditDialog = false; done(); }); }, @@ -2149,6 +2160,8 @@ RED.editor = (function() { } function showEditSubflowDialog(subflow) { + if (buildingEditDialog) { return } + buildingEditDialog = true; var editing_node = subflow; editStack.push(subflow); RED.view.state(RED.state.EDITING); @@ -2405,15 +2418,17 @@ RED.editor = (function() { buildEditForm(nodePropertiesTab.content,"dialog-form","subflow-template", undefined, editing_node); trayBody.i18n(); - - $.getJSON(getCredentialsURL("subflow", subflow.id), function (data) { - subflow.credentials = data; - subflow.credentials._ = $.extend(true,{},data); - + getNodeCredentials("subflow", subflow.id, function(data) { + if (data) { + subflow.credentials = data; + subflow.credentials._ = $.extend(true,{},data); + } $("#subflow-input-name").val(subflow.name); RED.text.bidi.prepareInput($("#subflow-input-name")); finishedBuilding = true; + buildingEditDialog = false; + done(); }); }, @@ -2434,7 +2449,39 @@ RED.editor = (function() { RED.tray.show(trayOptions); } + function getNodeCredentials(type, id, done) { + var timeoutNotification; + var intialTimeout = setTimeout(function() { + timeoutNotification = RED.notify($('

    ').i18n(),{fixed: true}) + },800); + + $.ajax({ + url: getCredentialsURL(type,id), + dataType: 'json', + success: function(data) { + if (timeoutNotification) { + timeoutNotification.close(); + timeoutNotification = null; + } + clearTimeout(intialTimeout); + done(data); + }, + error: function(jqXHR,status,error) { + if (timeoutNotification) { + timeoutNotification.close(); + timeoutNotification = null; + } + clearTimeout(intialTimeout); + RED.notify(RED._("editor.errors.credentialLoadFailed"),"error") + done(null); + }, + timeout: 30000, + }); + } + function showEditGroupDialog(group) { + if (buildingEditDialog) { return } + buildingEditDialog = true; var editing_node = group; editStack.push(group); RED.view.state(RED.state.EDITING); @@ -2658,6 +2705,7 @@ RED.editor = (function() { prepareEditDialog(group,group._def,"node-input", function() { trayBody.i18n(); finishedBuilding = true; + buildingEditDialog = false; done(); }); }, diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js index bd0b55cba..0cb202051 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js @@ -97,13 +97,18 @@ RED.palette = (function() { label = RED.utils.sanitize(label); - var words = label.split(/[ -]/); + var words = label.split(/([ -]|\\n )/); var displayLines = []; var currentLine = ""; for (var i=0;i').appendTo(popOverContent) + + if (/^subflow:/.test(type)) { + $('').appendTo(popOverContent) + } + + var safeType = type.replace(/'/g,"\\'"); + + $('').appendTo(popOverContent) + $('').appendTo(popOverContent) + $('

    ',{style:"font-size: 0.8em"}).text(metaData).appendTo(popOverContent); } } catch(err) { @@ -396,7 +410,8 @@ RED.palette = (function() { RED.workspaces.show(nt.substring(8)); e.preventDefault(); }); - nodeInfo = RED.utils.renderMarkdown(def.info||""); + var subflow = RED.nodes.subflow(nt.substring(8)); + nodeInfo = RED.utils.renderMarkdown(subflow.info||""); } setLabel(nt,d,label,nodeInfo); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js index b4cf4be08..6227ef37b 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js @@ -464,12 +464,43 @@ RED.subflow = (function() { $("#red-ui-subflow-delete").on("click", function(event) { event.preventDefault(); - var startDirty = RED.nodes.dirty(); - var historyEvent = removeSubflow(RED.workspaces.active()); - historyEvent.t = 'delete'; - historyEvent.dirty = startDirty; + var subflow = RED.nodes.subflow(RED.workspaces.active()); + if (subflow.instances.length > 0) { + var msg = $('

    ') + $('

    ').text(RED._("subflow.subflowInstances",{count: subflow.instances.length})).appendTo(msg); + $('

    ').text(RED._("subflow.confirmDelete")).appendTo(msg); + var confirmDeleteNotification = RED.notify(msg, { + modal: true, + fixed: true, + buttons: [ + { + text: RED._('common.label.cancel'), + click: function() { + confirmDeleteNotification.close(); + } + }, + { + text: RED._('workspace.confirmDelete'), + class: "primary", + click: function() { + confirmDeleteNotification.close(); + completeDelete(); + } + } + ] + }); - RED.history.push(historyEvent); + return; + } else { + completeDelete(); + } + function completeDelete() { + var startDirty = RED.nodes.dirty(); + var historyEvent = removeSubflow(RED.workspaces.active()); + historyEvent.t = 'delete'; + historyEvent.dirty = startDirty; + RED.history.push(historyEvent); + } }); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js index f0c6e631a..891f9f7dc 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js @@ -119,34 +119,17 @@ RED.sidebar.info.outliner = (function() { return div; } - function getSubflowLabel(n) { - - var div = $('

    ',{class:"red-ui-info-outline-item"}); - RED.utils.createNodeIcon(n).appendTo(div); - var contentDiv = $('
    ',{class:"red-ui-search-result-description"}).appendTo(div); - var labelText = getNodeLabelText(n); - var label = $('
    ',{class:"red-ui-search-result-node-label red-ui-info-outline-item-label"}).appendTo(contentDiv); - if (labelText) { - label.text(labelText) - } else { - label.html(" ") - } - - addControls(n, div); - - return div; - - - // var div = $('
    ',{class:"red-ui-info-outline-item red-ui-info-outline-item-flow"}); - // var contentDiv = $('
    ',{class:"red-ui-search-result-description red-ui-info-outline-item-label"}).appendTo(div); - // contentDiv.text(n.name || n.id); - // addControls(n, div); - // return div; - } - function addControls(n,div) { var controls = $('
    ',{class:"red-ui-info-outline-item-controls red-ui-info-outline-item-hover-controls"}).appendTo(div); + if (n.type === "subflow") { + var subflowInstanceBadge = $('').text(n.instances.length).appendTo(controls).on("click",function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + RED.search.show("type:subflow:"+n.id); + }) + // RED.popover.tooltip(userCountBadge,function() { return RED._('editor.nodesUse',{count:n.users.length})}); + } if (n._def.category === "config" && n.type !== "group") { var userCountBadge = $('').text(n.users.length).appendTo(controls).on("click",function(evt) { evt.preventDefault(); @@ -169,7 +152,7 @@ RED.sidebar.info.outliner = (function() { // evt.stopPropagation(); // RED.view.reveal(n.id); // }) - if (n.type !== 'group' && n.type !== 'subflow') { + if (n.type !== 'subflow') { var toggleButton = $('').appendTo(controls).on("click",function(evt) { evt.preventDefault(); evt.stopPropagation(); @@ -179,6 +162,46 @@ RED.sidebar.info.outliner = (function() { } else { RED.workspaces.disable(n.id) } + } else if (n.type === 'group') { + var groupNodes = RED.group.getNodes(n,true); + var groupHistoryEvent = { + t:'multi', + events:[], + dirty: RED.nodes.dirty() + } + var targetState; + groupNodes.forEach(function(n) { + if (n.type !== 'group') { + if (targetState === undefined) { + targetState = !n.d; + } + var state = !!n.d; + if (state !== targetState) { + var historyEvent = { + t: "edit", + node: n, + changed: n.changed, + changes: { + d: n.d + } + } + if (n.d) { + delete n.d; + } else { + n.d = true; + } + n.dirty = true; + n.changed = true; + RED.events.emit("nodes:change",n); + groupHistoryEvent.events.push(historyEvent); + } + } + if (groupHistoryEvent.events.length > 0) { + RED.history.push(groupHistoryEvent); + RED.nodes.dirty(true) + RED.view.redraw(); + } + }) } else { // TODO: this ought to be a utility function in RED.nodes var historyEvent = { @@ -198,11 +221,15 @@ RED.sidebar.info.outliner = (function() { n.dirty = true; n.changed = true; RED.events.emit("nodes:change",n); + RED.history.push(historyEvent); RED.nodes.dirty(true) RED.view.redraw(); } }); RED.popover.tooltip(toggleButton,function() { + if (n.type === "group") { + return RED._("common.label.enable")+" / "+RED._("common.label.disable") + } return RED._("common.label."+(((n.type==='tab' && n.disabled) || (n.type!=='tab' && n.d))?"enable":"disable")); }); } else { @@ -486,6 +513,13 @@ RED.sidebar.info.outliner = (function() { existingObject.treeList.remove(); delete objects[n.id] + if (/^subflow:/.test(n.type)) { + var sfType = n.type.substring(8); + if (objects[sfType]) { + objects[sfType].element.find(".red-ui-info-outline-item-control-users").text(RED.nodes.subflow(sfType).instances.length); + } + } + // If this is a group being removed, it may have an empty item if (empties[n.id]) { delete empties[n.id]; @@ -587,6 +621,12 @@ RED.sidebar.info.outliner = (function() { configNodeTypes[parent].types[n.type].treeList.addChild(objects[n.id]); } objects[n.id].element.toggleClass("red-ui-info-outline-item-disabled", !!n.d) + if (/^subflow:/.test(n.type)) { + var sfType = n.type.substring(8); + if (objects[sfType]) { + objects[sfType].element.find(".red-ui-info-outline-item-control-users").text(RED.nodes.subflow(sfType).instances.length); + } + } updateSearch(); } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js index a8e5583cb..bf03d2146 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js @@ -338,7 +338,7 @@ RED.sidebar.info = (function() { count++; propRow = $('').appendTo(tableBody); $(propRow.children()[0]).text(n); - if (defaults[n].type) { + if (defaults[n].type && !defaults[n]._type.array) { var configNode = RED.nodes.node(val); if (!configNode) { RED.utils.createObjectElement(undefined).appendTo(propRow.children()[1]); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js b/packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js index 07ab12180..2bdae4b13 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js @@ -109,13 +109,19 @@ RED.userSettings = (function() { function compText(a, b) { return a.text.localeCompare(b.text); } - + var viewSettings = [ { options: [ {setting:"editor-language",local: true, label:"menu.label.view.language",options:function(done){ done([{val:'',text:RED._('menu.label.view.browserDefault')}].concat(RED.settings.theme("languages").map(localeToName).sort(compText))) }}, ] - },{ + }, + // { + // options: [ + // {setting:"theme", label:"Theme",options:function(done){ done([{val:'',text:'default'}].concat(RED.settings.theme("themes"))) }}, + // ] + // }, + { title: "menu.label.view.grid", options: [ {setting:"view-show-grid",oldSetting:"menu-menu-item-view-show-grid",label:"menu.label.view.showGrid", default: true, toggle:true,onchange:"core:toggle-show-grid"}, diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js index 5be2d6c40..fd84ea70d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js @@ -615,18 +615,25 @@ RED.utils = (function() { return element; } - function normalisePropertyExpression(str) { + function createError(code, message) { + var e = new Error(message); + e.code = code; + return e; + } + + function normalisePropertyExpression(str,msg) { // This must be kept in sync with validatePropertyExpression // in editor/js/ui/utils.js var length = str.length; if (length === 0) { - throw new Error("Invalid property expression: zero-length"); + throw createError("INVALID_EXPR","Invalid property expression: zero-length"); } var parts = []; var start = 0; var inString = false; var inBox = false; + var boxExpression = false; var quoteChar; var v; for (var i=0;i 0) { + throw createError("INVALID_EXPR","Invalid property expression: unmatched '[' at position "+i); + } + continue; + } else if (!/["'\d]/.test(str[i+1])) { + // Next char is either a quote or a number + throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1)); } start = i+1; inBox = true; } else if (c === ']') { if (!inBox) { - throw new Error("Invalid property expression: unexpected "+c+" at position "+i); + throw createError("INVALID_EXPR","Invalid property expression: unexpected "+c+" at position "+i); } if (start != i) { v = str.substring(start,i); if (/^\d+$/.test(v)) { parts.push(parseInt(v)); } else { - throw new Error("Invalid property expression: unexpected array expression at position "+start); + throw createError("INVALID_EXPR","Invalid property expression: unexpected array expression at position "+start); } } start = i+1; inBox = false; } else if (c === ' ') { - throw new Error("Invalid property expression: unexpected ' ' at position "+i); + throw createError("INVALID_EXPR","Invalid property expression: unexpected ' ' at position "+i); } } else { if (c === quoteChar) { if (i-start === 0) { - throw new Error("Invalid property expression: zero-length string at position "+start); + throw createError("INVALID_EXPR","Invalid property expression: zero-length string at position "+start); } parts.push(str.substring(start,i)); // If inBox, next char must be a ]. Otherwise it may be [ or . if (inBox && !/\]/.test(str[i+1])) { - throw new Error("Invalid property expression: unexpected array expression at position "+start); + throw createError("INVALID_EXPR","Invalid property expression: unexpected array expression at position "+start); } else if (!inBox && i+1!==length && !/[\[\.]/.test(str[i+1])) { - throw new Error("Invalid property expression: unexpected "+str[i+1]+" expression at position "+(i+1)); + throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" expression at position "+(i+1)); } start = i+1; inString = false; @@ -711,7 +760,7 @@ RED.utils = (function() { } if (inBox || inString) { - throw new Error("Invalid property expression: unterminated expression"); + throw new createError("INVALID_EXPR","Invalid property expression: unterminated expression"); } if (start < length) { parts.push(str.substring(start)); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 888871f4b..e6bf6476c 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -2276,7 +2276,7 @@ RED.view = (function() { } function calculateTextWidth(str, className) { - var result=convertLineBreakCharacter(str); + var result = convertLineBreakCharacter(str); var width = 0; for (var i=0;i { if (err) { done(err) @@ -243,6 +247,10 @@ module.exports = function(RED) { return done(undefined,msg); } else if (rule.pt === 'flow' || rule.pt === 'global') { var contextKey = RED.util.parseContextStore(property); + if (/\[msg/.test(contextKey.key)) { + // The key has a nest msg. reference to evaluate first + contextKey.key = RED.util.normalisePropertyExpression(contextKey.key, msg, true) + } var target = node.context()[rule.pt]; var callback = err => { if (err) { diff --git a/packages/node_modules/@node-red/nodes/core/function/90-exec.html b/packages/node_modules/@node-red/nodes/core/function/90-exec.html index 636117d71..789e15f75 100644 --- a/packages/node_modules/@node-red/nodes/core/function/90-exec.html +++ b/packages/node_modules/@node-red/nodes/core/function/90-exec.html @@ -52,7 +52,7 @@ color:"darksalmon", defaults: { command: {value:""}, - addpay: {value:true}, + addpay: {value:false}, append: {value:""}, useSpawn: {value:"false"}, timer: {value:""}, diff --git a/packages/node_modules/@node-red/nodes/core/function/90-exec.js b/packages/node_modules/@node-red/nodes/core/function/90-exec.js index 0df3e6012..132286cb4 100644 --- a/packages/node_modules/@node-red/nodes/core/function/90-exec.js +++ b/packages/node_modules/@node-red/nodes/core/function/90-exec.js @@ -31,12 +31,12 @@ module.exports = function(RED) { this.timer = Number(n.timer || 0)*1000; this.activeProcesses = {}; this.oldrc = (n.oldrc || false).toString(); - this.execOpt = {encoding:'binary', maxBuffer:10000000}; + this.execOpt = {encoding:'binary', maxBuffer:RED.settings.execMaxBufferSize||10000000}; var node = this; - if (process.platform === 'linux' && fs.existsSync('/bin/bash')) { node.execOpt.shell = '/bin/bash'; } - - var cleanup = function(p) { + if (process.platform === 'linux' && fs.existsSync('/bin/bash')) { node.execOpt.shell = '/bin/bash'; } + + var cleanup = function(p) { node.activeProcesses[p].kill(); //node.status({fill:"red",shape:"dot",text:"timeout"}); //node.error("Exec node timeout"); diff --git a/packages/node_modules/@node-red/nodes/core/network/31-tcpin.html b/packages/node_modules/@node-red/nodes/core/network/31-tcpin.html index ae0eaedda..2aab13437 100644 --- a/packages/node_modules/@node-red/nodes/core/network/31-tcpin.html +++ b/packages/node_modules/@node-red/nodes/core/network/31-tcpin.html @@ -235,6 +235,7 @@ oneditprepare: function() { var previous = null; $("#node-input-out").on('focus', function () { previous = this.value; }).on("change", function() { + $("#node-input-splitc").show(); if (previous === null) { previous = $("#node-input-out").val(); } if ($("#node-input-out").val() == "char") { if (previous != "char") { $("#node-input-splitc").val("\\n"); } @@ -247,6 +248,7 @@ else if ($("#node-input-out").val() == "immed") { if (previous != "immed") { $("#node-input-splitc").val(" "); } $("#node-units").text(""); + $("#node-input-splitc").hide(); } else if ($("#node-input-out").val() == "count") { if (previous != "count") { $("#node-input-splitc").val("12"); } @@ -255,6 +257,7 @@ else { if (previous != "sit") { $("#node-input-splitc").val(" "); } $("#node-units").text(""); + $("#node-input-splitc").hide(); } }); } diff --git a/packages/node_modules/@node-red/nodes/core/sequence/17-split.js b/packages/node_modules/@node-red/nodes/core/sequence/17-split.js index dc422f3da..318319f3d 100644 --- a/packages/node_modules/@node-red/nodes/core/sequence/17-split.js +++ b/packages/node_modules/@node-red/nodes/core/sequence/17-split.js @@ -737,7 +737,10 @@ module.exports = function(RED) { group.msg = Object.assign(group.msg, msg); group.send = send; var tcnt = group.targetCount; - if (msg.hasOwnProperty("parts")) { tcnt = group.targetCount || msg.parts.count; } + if (msg.hasOwnProperty("parts")) { + tcnt = group.targetCount || msg.parts.count; + group.targetCount = tcnt; + } if ((tcnt > 0 && group.currentCount >= tcnt) || msg.hasOwnProperty('complete')) { completeSend(partId); } @@ -759,3 +762,4 @@ module.exports = function(RED) { } RED.nodes.registerType("join",JoinNode); } + diff --git a/packages/node_modules/@node-red/nodes/examples/storage/file-in/01 - Read string from a file.json b/packages/node_modules/@node-red/nodes/examples/storage/file-in/01 - Read string from a file.json new file mode 100644 index 000000000..14d7152c7 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/file-in/01 - Read string from a file.json @@ -0,0 +1,113 @@ +[ + { + "id": "84222b92.d65d18", + "type": "inject", + "z": "194a3e4f.a92772", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "Hello, World!", + "payloadType": "str", + "x": 230, + "y": 220, + "wires": [ + [ + "b4b9f603.739598" + ] + ] + }, + { + "id": "7b014430.dfd94c", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "Write string to a file, then read from the file", + "info": "File-in node can read string from a file.", + "x": 260, + "y": 140, + "wires": [] + }, + { + "id": "b4b9f603.739598", + "type": "file", + "z": "194a3e4f.a92772", + "name": "", + "filename": "/tmp/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 420, + "y": 220, + "wires": [ + [ + "6dc01cac.5c4bf4" + ] + ] + }, + { + "id": "2587adb9.7e60f2", + "type": "debug", + "z": "194a3e4f.a92772", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 810, + "y": 220, + "wires": [] + }, + { + "id": "6dc01cac.5c4bf4", + "type": "file in", + "z": "194a3e4f.a92772", + "name": "", + "filename": "/tmp/hello.txt", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "x": 620, + "y": 220, + "wires": [ + [ + "2587adb9.7e60f2" + ] + ] + }, + { + "id": "f4b4309a.3b78a", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↑read result from file", + "info": "", + "x": 630, + "y": 260, + "wires": [] + }, + { + "id": "672d3693.3cabd8", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↓write to /tmp/hello.txt", + "info": "", + "x": 440, + "y": 180, + "wires": [] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/file-in/02 - Read data in specified encoding.json b/packages/node_modules/@node-red/nodes/examples/storage/file-in/02 - Read data in specified encoding.json new file mode 100644 index 000000000..d96623fb1 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/file-in/02 - Read data in specified encoding.json @@ -0,0 +1,113 @@ +[ + { + "id": "8997398f.c5d628", + "type": "inject", + "z": "194a3e4f.a92772", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "😀", + "payloadType": "str", + "x": 210, + "y": 480, + "wires": [ + [ + "56e32d23.050f44" + ] + ] + }, + { + "id": "4e598e65.1799d", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "Read data in specified encoding", + "info": "File-in node can specify encoding of data read from a file.", + "x": 230, + "y": 400, + "wires": [] + }, + { + "id": "56e32d23.050f44", + "type": "file", + "z": "194a3e4f.a92772", + "name": "", + "filename": "/tmp/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 380, + "y": 480, + "wires": [ + [ + "38fa0579.f2cd8a" + ] + ] + }, + { + "id": "d28c8994.99c0a8", + "type": "debug", + "z": "194a3e4f.a92772", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 770, + "y": 480, + "wires": [] + }, + { + "id": "38fa0579.f2cd8a", + "type": "file in", + "z": "194a3e4f.a92772", + "name": "", + "filename": "/tmp/hello.txt", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "base64", + "x": 580, + "y": 480, + "wires": [ + [ + "d28c8994.99c0a8" + ] + ] + }, + { + "id": "fa22ca20.ae4528", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↑read data from file as base64 string", + "info": "", + "x": 640, + "y": 520, + "wires": [] + }, + { + "id": "148e25ad.98891a", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↓write to /tmp/hello.txt", + "info": "", + "x": 400, + "y": 440, + "wires": [] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/file-in/03 - Read data breaking lines into messages.json b/packages/node_modules/@node-red/nodes/examples/storage/file-in/03 - Read data breaking lines into messages.json new file mode 100644 index 000000000..b3d35a4da --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/file-in/03 - Read data breaking lines into messages.json @@ -0,0 +1,132 @@ +[ + { + "id": "6a0b1d03.d4cee4", + "type": "inject", + "z": "194a3e4f.a92772", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 220, + "y": 740, + "wires": [ + [ + "d4b00cb7.a5a23" + ] + ] + }, + { + "id": "f17ea1d1.8ecc3", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "Read data breaking lines into individual messages", + "info": "File-in node can break read text into messages with individual lines", + "x": 290, + "y": 660, + "wires": [] + }, + { + "id": "99ae7806.1d6428", + "type": "file", + "z": "194a3e4f.a92772", + "name": "", + "filename": "/tmp/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 540, + "y": 740, + "wires": [ + [ + "70d7892f.d27db8" + ] + ] + }, + { + "id": "7ed8282c.92b338", + "type": "debug", + "z": "194a3e4f.a92772", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 810, + "y": 800, + "wires": [] + }, + { + "id": "70d7892f.d27db8", + "type": "file in", + "z": "194a3e4f.a92772", + "name": "", + "filename": "/tmp/hello.txt", + "format": "lines", + "chunk": false, + "sendError": false, + "encoding": "none", + "x": 620, + "y": 800, + "wires": [ + [ + "7ed8282c.92b338" + ] + ] + }, + { + "id": "c1b7e05.1d94b2", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↑read data from file breaking lines into messages", + "info": "", + "x": 720, + "y": 840, + "wires": [] + }, + { + "id": "a5f647b2.cf27a8", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↓write to /tmp/hello.txt", + "info": "", + "x": 560, + "y": 700, + "wires": [] + }, + { + "id": "d4b00cb7.a5a23", + "type": "template", + "z": "194a3e4f.a92772", + "name": "data", + "field": "payload", + "fieldType": "msg", + "format": "handlebars", + "syntax": "plain", + "template": "one\ntwo\nthree!", + "output": "str", + "x": 370, + "y": 740, + "wires": [ + [ + "99ae7806.1d6428" + ] + ] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/file-in/04 - Create a message stream.json b/packages/node_modules/@node-red/nodes/examples/storage/file-in/04 - Create a message stream.json new file mode 100644 index 000000000..18b462755 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/file-in/04 - Create a message stream.json @@ -0,0 +1,201 @@ +[ + { + "id": "bdd57acc.2edc48", + "type": "inject", + "z": "194a3e4f.a92772", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 220, + "y": 1040, + "wires": [ + [ + "7a069b01.0c2324" + ] + ] + }, + { + "id": "1fd12220.33953e", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "Creating a message stream from lines of data", + "info": "File-in node can break read text into messages with individual lines. The messages creates a stream of messages.", + "x": 270, + "y": 960, + "wires": [] + }, + { + "id": "ab6eb213.2a08d", + "type": "file", + "z": "194a3e4f.a92772", + "name": "", + "filename": "/tmp/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 540, + "y": 1040, + "wires": [ + [ + "b7ed49b0.649fb8" + ] + ] + }, + { + "id": "c48d8ae0.9ff3a8", + "type": "debug", + "z": "194a3e4f.a92772", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 810, + "y": 1140, + "wires": [] + }, + { + "id": "b7ed49b0.649fb8", + "type": "file in", + "z": "194a3e4f.a92772", + "name": "", + "filename": "/tmp/hello.txt", + "format": "lines", + "chunk": false, + "sendError": false, + "encoding": "none", + "x": 280, + "y": 1140, + "wires": [ + [ + "83073ebe.fcce4" + ] + ] + }, + { + "id": "3c33e69f.6a04ba", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↑read data from file breaking lines into messages", + "info": "", + "x": 380, + "y": 1180, + "wires": [] + }, + { + "id": "3598bf7d.5712a", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↓write to /tmp/hello.txt", + "info": "", + "x": 560, + "y": 1000, + "wires": [] + }, + { + "id": "7a069b01.0c2324", + "type": "template", + "z": "194a3e4f.a92772", + "name": "data", + "field": "payload", + "fieldType": "msg", + "format": "handlebars", + "syntax": "plain", + "template": "Apple\nBanana\nGrape\nOrange", + "output": "str", + "x": 370, + "y": 1040, + "wires": [ + [ + "ab6eb213.2a08d" + ] + ] + }, + { + "id": "8d4ed1d0.821fe", + "type": "join", + "z": "194a3e4f.a92772", + "name": "", + "mode": "auto", + "build": "string", + "property": "payload", + "propertyType": "msg", + "key": "topic", + "joiner": "\\n", + "joinerType": "str", + "accumulate": "false", + "timeout": "", + "count": "", + "reduceRight": false, + "x": 630, + "y": 1140, + "wires": [ + [ + "c48d8ae0.9ff3a8" + ] + ] + }, + { + "id": "83073ebe.fcce4", + "type": "switch", + "z": "194a3e4f.a92772", + "name": "< D", + "property": "payload", + "propertyType": "msg", + "rules": [ + { + "t": "lt", + "v": "D", + "vt": "str" + } + ], + "checkall": "true", + "repair": true, + "outputs": 1, + "x": 470, + "y": 1140, + "wires": [ + [ + "8d4ed1d0.821fe" + ] + ] + }, + { + "id": "2088e195.f7aebe", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↓filter data before \"D\"", + "info": "", + "x": 520, + "y": 1100, + "wires": [] + }, + { + "id": "b848cdc7.61e06", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↑join to single string", + "info": "", + "x": 670, + "y": 1180, + "wires": [] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/file/01 - Write string to a file.json b/packages/node_modules/@node-red/nodes/examples/storage/file/01 - Write string to a file.json new file mode 100644 index 000000000..2be033021 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/file/01 - Write string to a file.json @@ -0,0 +1,113 @@ +[ + { + "id": "84222b92.d65d18", + "type": "inject", + "z": "4b63452d.672afc", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "Hello, World!", + "payloadType": "str", + "x": 230, + "y": 200, + "wires": [ + [ + "b4b9f603.739598" + ] + ] + }, + { + "id": "7b014430.dfd94c", + "type": "comment", + "z": "4b63452d.672afc", + "name": "Write string to a file, then read from the file", + "info": "File node can write string to a file.", + "x": 260, + "y": 120, + "wires": [] + }, + { + "id": "b4b9f603.739598", + "type": "file", + "z": "4b63452d.672afc", + "name": "", + "filename": "/tmp/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 420, + "y": 200, + "wires": [ + [ + "6dc01cac.5c4bf4" + ] + ] + }, + { + "id": "2587adb9.7e60f2", + "type": "debug", + "z": "4b63452d.672afc", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 810, + "y": 200, + "wires": [] + }, + { + "id": "6dc01cac.5c4bf4", + "type": "file in", + "z": "4b63452d.672afc", + "name": "", + "filename": "/tmp/hello.txt", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "x": 620, + "y": 200, + "wires": [ + [ + "2587adb9.7e60f2" + ] + ] + }, + { + "id": "f4b4309a.3b78a", + "type": "comment", + "z": "4b63452d.672afc", + "name": "↑read result from file", + "info": "", + "x": 630, + "y": 240, + "wires": [] + }, + { + "id": "672d3693.3cabd8", + "type": "comment", + "z": "4b63452d.672afc", + "name": "↓write to /tmp/hello.txt", + "info": "", + "x": 440, + "y": 160, + "wires": [] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/file/02 - Write string to a file specified by property.json b/packages/node_modules/@node-red/nodes/examples/storage/file/02 - Write string to a file specified by property.json new file mode 100644 index 000000000..6aaff500a --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/file/02 - Write string to a file specified by property.json @@ -0,0 +1,118 @@ +[ + { + "id": "704479e1.399388", + "type": "inject", + "z": "4b63452d.672afc", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "filename", + "v": "/tmp/hello.txt", + "vt": "str" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "Hello, World!", + "payloadType": "str", + "x": 230, + "y": 400, + "wires": [ + [ + "402f3b7e.988014" + ] + ] + }, + { + "id": "8e876a75.e9beb8", + "type": "comment", + "z": "4b63452d.672afc", + "name": "Write string to a file specied by filename property, the read from the file", + "info": "File node can target file using `filename` property.", + "x": 350, + "y": 320, + "wires": [] + }, + { + "id": "402f3b7e.988014", + "type": "file", + "z": "4b63452d.672afc", + "name": "", + "filename": "", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 390, + "y": 400, + "wires": [ + [ + "26e077d6.bbcd98" + ] + ] + }, + { + "id": "97b6b6b2.a54b38", + "type": "debug", + "z": "4b63452d.672afc", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 770, + "y": 400, + "wires": [] + }, + { + "id": "26e077d6.bbcd98", + "type": "file in", + "z": "4b63452d.672afc", + "name": "", + "filename": "/tmp/hello.txt", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "x": 580, + "y": 400, + "wires": [ + [ + "97b6b6b2.a54b38" + ] + ] + }, + { + "id": "85062297.da79", + "type": "comment", + "z": "4b63452d.672afc", + "name": "↑read result from file", + "info": "", + "x": 590, + "y": 440, + "wires": [] + }, + { + "id": "7316c4fc.b1dcdc", + "type": "comment", + "z": "4b63452d.672afc", + "name": "↓write to file specified by filename property", + "info": "", + "x": 500, + "y": 360, + "wires": [] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/file/03 - Delete a file.json b/packages/node_modules/@node-red/nodes/examples/storage/file/03 - Delete a file.json new file mode 100644 index 000000000..fe6ac166b --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/file/03 - Delete a file.json @@ -0,0 +1,85 @@ +[ + { + "id": "4ac00fb0.d5f52", + "type": "inject", + "z": "4b63452d.672afc", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 220, + "y": 600, + "wires": [ + [ + "542cc2f4.92857c" + ] + ] + }, + { + "id": "671f8295.0e6f6c", + "type": "comment", + "z": "4b63452d.672afc", + "name": "Delete a file", + "info": "File node can delete a file.", + "x": 170, + "y": 540, + "wires": [] + }, + { + "id": "542cc2f4.92857c", + "type": "file", + "z": "4b63452d.672afc", + "name": "", + "filename": "/tmp/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "delete", + "encoding": "none", + "x": 420, + "y": 600, + "wires": [ + [ + "a24da523.5babe8" + ] + ] + }, + { + "id": "a24da523.5babe8", + "type": "debug", + "z": "4b63452d.672afc", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 630, + "y": 600, + "wires": [] + }, + { + "id": "51157051.2f62", + "type": "comment", + "z": "4b63452d.672afc", + "name": "↓delete a file", + "info": "", + "x": 390, + "y": 560, + "wires": [] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/file/04 - Specify encoding of written data.json b/packages/node_modules/@node-red/nodes/examples/storage/file/04 - Specify encoding of written data.json new file mode 100644 index 000000000..4dabbd670 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/file/04 - Specify encoding of written data.json @@ -0,0 +1,113 @@ +[ + { + "id": "e4ef1f5e.7cd82", + "type": "inject", + "z": "4b63452d.672afc", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "8J+YgA==", + "payloadType": "str", + "x": 220, + "y": 820, + "wires": [ + [ + "72b37cc8.177054" + ] + ] + }, + { + "id": "f5997af4.5a9298", + "type": "comment", + "z": "4b63452d.672afc", + "name": "Specify encoding of written data", + "info": "File node can specify encoding of data.", + "x": 230, + "y": 740, + "wires": [] + }, + { + "id": "72b37cc8.177054", + "type": "file", + "z": "4b63452d.672afc", + "name": "", + "filename": "/tmp/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "base64", + "x": 400, + "y": 820, + "wires": [ + [ + "2da33ec.f45cac2" + ] + ] + }, + { + "id": "2e814354.278c8c", + "type": "debug", + "z": "4b63452d.672afc", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 790, + "y": 820, + "wires": [] + }, + { + "id": "2da33ec.f45cac2", + "type": "file in", + "z": "4b63452d.672afc", + "name": "", + "filename": "/tmp/hello.txt", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "x": 600, + "y": 820, + "wires": [ + [ + "2e814354.278c8c" + ] + ] + }, + { + "id": "ec754c99.84bfd", + "type": "comment", + "z": "4b63452d.672afc", + "name": "↓write string with base64 encoding", + "info": "", + "x": 460, + "y": 780, + "wires": [] + }, + { + "id": "3e6704ff.4ce25c", + "type": "comment", + "z": "4b63452d.672afc", + "name": "↑read result from file", + "info": "", + "x": 610, + "y": 860, + "wires": [] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/watch/01 - Watch change of a file.json b/packages/node_modules/@node-red/nodes/examples/storage/watch/01 - Watch change of a file.json new file mode 100644 index 000000000..873e99b89 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/watch/01 - Watch change of a file.json @@ -0,0 +1,108 @@ +[ + { + "id": "84222b92.d65d18", + "type": "inject", + "z": "a7ac8a68.0f7218", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "Hello, World!", + "payloadType": "str", + "x": 230, + "y": 160, + "wires": [ + [ + "b4b9f603.739598" + ] + ] + }, + { + "id": "7b014430.dfd94c", + "type": "comment", + "z": "a7ac8a68.0f7218", + "name": "Watch changes of a file", + "info": "Watch node can watch and report changes of a file.", + "x": 200, + "y": 80, + "wires": [] + }, + { + "id": "b4b9f603.739598", + "type": "file", + "z": "a7ac8a68.0f7218", + "name": "", + "filename": "/tmp/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 420, + "y": 160, + "wires": [ + [] + ] + }, + { + "id": "672d3693.3cabd8", + "type": "comment", + "z": "a7ac8a68.0f7218", + "name": "↓write to /tmp/hello.txt", + "info": "", + "x": 440, + "y": 120, + "wires": [] + }, + { + "id": "15f1f5aa.506ffa", + "type": "watch", + "z": "a7ac8a68.0f7218", + "name": "", + "files": "/tmp/hello.txt", + "recursive": "", + "x": 410, + "y": 200, + "wires": [ + [ + "a91562b9.ca805" + ] + ] + }, + { + "id": "a91562b9.ca805", + "type": "debug", + "z": "a7ac8a68.0f7218", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 610, + "y": 200, + "wires": [] + }, + { + "id": "2ab4eba8.267d64", + "type": "comment", + "z": "a7ac8a68.0f7218", + "name": "↑watch changes of /tmp/hello.txt", + "info": "", + "x": 470, + "y": 240, + "wires": [] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/watch/02 - Watch change in a directory.json b/packages/node_modules/@node-red/nodes/examples/storage/watch/02 - Watch change in a directory.json new file mode 100644 index 000000000..6b7fd0b2e --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/watch/02 - Watch change in a directory.json @@ -0,0 +1,163 @@ +[ + { + "id": "acec9dcc.eca82", + "type": "inject", + "z": "a7ac8a68.0f7218", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "Hello, World!", + "payloadType": "str", + "x": 230, + "y": 460, + "wires": [ + [ + "137d60e2.0e267f" + ] + ] + }, + { + "id": "cf011d1e.8afa6", + "type": "comment", + "z": "a7ac8a68.0f7218", + "name": "Watch changes in a directory", + "info": "Watch node can watch and report changes in a directory", + "x": 220, + "y": 340, + "wires": [] + }, + { + "id": "137d60e2.0e267f", + "type": "file", + "z": "a7ac8a68.0f7218", + "name": "", + "filename": "/tmp/HG/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 430, + "y": 460, + "wires": [ + [] + ] + }, + { + "id": "9dfce283.63e5a", + "type": "comment", + "z": "a7ac8a68.0f7218", + "name": "↓write to /tmp/HG/*", + "info": "", + "x": 430, + "y": 420, + "wires": [] + }, + { + "id": "cbfb788b.297f98", + "type": "watch", + "z": "a7ac8a68.0f7218", + "name": "", + "files": "/tmp/HG", + "recursive": false, + "x": 400, + "y": 540, + "wires": [ + [ + "3c691cd5.a0f2b4" + ] + ] + }, + { + "id": "3c691cd5.a0f2b4", + "type": "debug", + "z": "a7ac8a68.0f7218", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 550, + "y": 540, + "wires": [] + }, + { + "id": "c0d7ca6e.cad418", + "type": "comment", + "z": "a7ac8a68.0f7218", + "name": "↑watch changes in /tmp/HG", + "info": "", + "x": 460, + "y": 580, + "wires": [] + }, + { + "id": "a04e5231.5a2e1", + "type": "inject", + "z": "a7ac8a68.0f7218", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "Goodbye, World!", + "payloadType": "str", + "x": 240, + "y": 500, + "wires": [ + [ + "655d7bab.cda6f4" + ] + ] + }, + { + "id": "655d7bab.cda6f4", + "type": "file", + "z": "a7ac8a68.0f7218", + "name": "", + "filename": "/tmp/HG/goodbye.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 440, + "y": 500, + "wires": [ + [] + ] + }, + { + "id": "f3b42209.a7673", + "type": "comment", + "z": "a7ac8a68.0f7218", + "name": "Notice: Create /tmp/HG directory before deploying this example", + "info": "", + "x": 330, + "y": 380, + "wires": [] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/locales/de/sequence/19-batch.html b/packages/node_modules/@node-red/nodes/locales/de/sequence/19-batch.html index 68df80963..aeab9c15e 100755 --- a/packages/node_modules/@node-red/nodes/locales/de/sequence/19-batch.html +++ b/packages/node_modules/@node-red/nodes/locales/de/sequence/19-batch.html @@ -20,19 +20,18 @@

    Es gibt drei Modi für die Erstellung von Nachrichtensequenzen:

    Anzahl der Nachrichten
    -
    Die Nachrichten werden zu Sequenzen einer bestimmten Länge gruppiert. Die Option Überlappung> +
    Die Nachrichten werden zu Sequenzen einer bestimmten Länge gruppiert. Die Option Überlappung gibt an, wie viele Nachrichten vom Ende einer Sequenz am Anfang der nächsten Sequenz wiederholt werden sollen.
    Zeitintervall
    Gruppiert Nachrichten, die innerhalb des angegebenen Intervalls eingehen. Wenn keine Nachrichten innerhalb des Intervalls ankommen, kann der Node optional eine leere Nachricht senden.
    -
    Sequenzesn verketten/dt> +
    Sequenzen verketten/dt>
    Erzeugt eine Nachrichtensequenz durch die Verkettung eingehender Sequenzen. Jede Nachricht muss eine msg.topic und eine msg.parts Eigenschaft haben, um die Sequenz identifizieren zu können. Der Node ist mit einer Liste von topic konfiguriert, - mit der die Reihnmefolge der Verkettung der Sequenzen definiert wird. -
    + mit der die Reihenfolge der Verkettung der Sequenzen definiert wird.

    Speichern der Nachrichten

    diff --git a/packages/node_modules/@node-red/nodes/locales/ru/common/20-inject.html b/packages/node_modules/@node-red/nodes/locales/ru/common/20-inject.html index e265fccc6..5b762bc7d 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/common/20-inject.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/common/20-inject.html @@ -16,7 +16,7 @@ diff --git a/packages/node_modules/@node-red/nodes/locales/ru/common/21-debug.html b/packages/node_modules/@node-red/nodes/locales/ru/common/21-debug.html index 66edb72cc..7804e6af5 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/common/21-debug.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/common/21-debug.html @@ -16,24 +16,24 @@ diff --git a/packages/node_modules/@node-red/nodes/locales/ru/common/24-complete.html b/packages/node_modules/@node-red/nodes/locales/ru/common/24-complete.html index 3709f82dc..672310d6f 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/common/24-complete.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/common/24-complete.html @@ -16,7 +16,7 @@ diff --git a/packages/node_modules/@node-red/nodes/locales/ru/common/25-catch.html b/packages/node_modules/@node-red/nodes/locales/ru/common/25-catch.html index 867e9b203..af1627c22 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/common/25-catch.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/common/25-catch.html @@ -16,7 +16,7 @@ diff --git a/packages/node_modules/@node-red/nodes/locales/ru/common/60-link.html b/packages/node_modules/@node-red/nodes/locales/ru/common/60-link.html index 1ef3079af..f2894da97 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/common/60-link.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/common/60-link.html @@ -21,13 +21,13 @@

    Подробности

    - Этот узел может быть подключен к любому узлу link out на любой вкладке. После подключения они ведут себя так, как если бы они были соединены вместе обычным проводом. + Этот узел может быть подключен к любому link out узлу на любой вкладке. После подключения они ведут себя так, как если бы они были соединены вместе обычным проводом.

    - Связи между link-узлами отображаются, только когда выбран link-узел. Если есть какие-либо провода к другим вкладкам, отображается виртуальный узел, по которому можно кликнуть, чтобы перейти на соответствующую вкладку. + Связи между link-узлами отображаются, только когда выбран один из соединенных link узлов. Если есть какие-либо провода, ведущие на другие вкладки, они отображаются в виде виртуального узла, по которому можно кликнуть, чтобы перейти на соответствующую вкладку.

    - Примечание: Связи не могут идти внутрь подпотока или изнутри подпотока наружу. + Примечание: Провод не может вести внутрь подпотока или изнутри подпотока наружу.

    @@ -38,12 +38,12 @@

    Подробности

    - Узел может быть подключен к любому узлу link in на любой вкладке. После подключения они ведут себя так, как если бы они были соединены вместе обычным проводом. + Узел может быть подключен к любому link in узлу на любой вкладке. После подключения они ведут себя так, как если бы они были соединены вместе обычным проводом.

    - Связи между link-узлами отображаются, только когда выбран link-узел. Если есть какие-либо провода к другим вкладкам, отображается виртуальный узел, по которому можно кликнуть, чтобы перейти на соответствующую вкладку. + Связи между link-узлами отображаются, только когда выбран один из соединенных link узлов. Если есть какие-либо провода, ведущие на другие вкладки, они отображаются в виде виртуального узла, по которому можно кликнуть, чтобы перейти на соответствующую вкладку.

    - Примечание: Связи не могут идти внутрь подпотока или изнутри подпотока наружу. + Примечание: Провод не может вести внутрь подпотока или изнутри подпотока наружу.

    diff --git a/packages/node_modules/@node-red/nodes/locales/ru/common/90-comment.html b/packages/node_modules/@node-red/nodes/locales/ru/common/90-comment.html index f8cb254bd..69ba9ba12 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/common/90-comment.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/common/90-comment.html @@ -16,11 +16,11 @@ diff --git a/packages/node_modules/@node-red/nodes/locales/ru/common/98-unknown.html b/packages/node_modules/@node-red/nodes/locales/ru/common/98-unknown.html index 6ffb5a03a..f9fd62285 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/common/98-unknown.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/common/98-unknown.html @@ -16,22 +16,22 @@ diff --git a/packages/node_modules/@node-red/nodes/locales/ru/function/10-function.html b/packages/node_modules/@node-red/nodes/locales/ru/function/10-function.html index f99408605..9ba16d4be 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/function/10-function.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/function/10-function.html @@ -16,22 +16,22 @@ diff --git a/packages/node_modules/@node-red/nodes/locales/ru/function/89-delay.html b/packages/node_modules/@node-red/nodes/locales/ru/function/89-delay.html index 2a86fd64c..4489155ac 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/function/89-delay.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/function/89-delay.html @@ -21,20 +21,20 @@

    Принимает

    delay число
    -
    Устанавливает задержку в миллисекундах, которая будет применена к сообщению. Этот параметр применяется только в том случае, если узел настроен на то, чтобы разрешать сообщению переопределять установленный интервал задержки по умолчанию.
    +
    Устанавливает задержку в миллисекундах, которая будет применена к сообщению. Этот параметр применяется только в том случае, если узел настроен так, чтобы разрешать сообщению переопределять установленный интервал задержки по умолчанию.
    reset
    -
    Если в полученном сообщении этому свойству присвоено какое-либо значение, все ожидающие сообщения, удерживаемые узлом, очищаются без дальнейшей отправки.
    +
    Если в полученном сообщении этому свойству присвоено какое-либо значение, все ожидающие сообщения, удерживаемые узлом, сбрасываются без дальнейшей отправки.
    flush
    Если в полученном сообщении этому свойству присвоено какое-либо значение, все ожидающие сообщения, удерживаемые узлом, немедленно отправляются далее.

    Подробности

    - Когда настроено задерживать сообщения, интервал задержки может быть фиксированным значением, случайным значением в пределах диапазона или динамически установленным для каждого сообщения. Каждое сообщение задерживается независимо от любых других сообщений, основываясь на времени его прибытия. + Когда узел настроен на задержку сообщений, интервал задержки может быть фиксированным значением, случайным значением в пределах диапазона или динамически установленным для каждого сообщения. Каждое сообщение задерживается независимо от любых других сообщений, основываясь на времени его прибытия в узел.

    - Когда настроено ограничивать скорость сообщений, их доставка распределяется по настроенному периоду времени. Статус показывает количество сообщений, находящихся в данный момент в очереди. Если выбрано он может отбрасывать промежуточные сообщений по мере их поступления. + Когда узел настроен на ограничение скорости сообщений, их доставка распределяется по установленному периоду времени. Статус показывает количество сообщений, находящихся в данный момент в очереди. При необходимости узел может быть настроен на отбрасывание промежуточных сообщений по мере их поступления.

    - Ограничение скорости может применяться ко всем сообщениям или группировать их в соответствии со значением msg.topic. При группировании промежуточные сообщения автоматически отбрасываются. В каждом интервале времени узел может либо выпустить самое последнее сообщение для всех тем, либо выпустить самое последнее сообщение для следующей темы. + Ограничение скорости может применяться ко всем сообщениям или группам сообщений в соответствии с их значением темы (msg.topic). При группировании промежуточные сообщения автоматически отбрасываются. В каждом интервале времени узел может либо выпустить самое последнее сообщение для всех тем, либо выпустить самое последнее сообщение для следующей темы.

    diff --git a/packages/node_modules/@node-red/nodes/locales/ru/function/89-trigger.html b/packages/node_modules/@node-red/nodes/locales/ru/function/89-trigger.html index c53058235..9a8d687cf 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/function/89-trigger.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/function/89-trigger.html @@ -16,7 +16,7 @@ diff --git a/packages/node_modules/@node-red/nodes/locales/ru/function/90-exec.html b/packages/node_modules/@node-red/nodes/locales/ru/function/90-exec.html index b654673fe..708ddfbc9 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/function/90-exec.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/function/90-exec.html @@ -19,16 +19,16 @@ Запускает системную команду и возвращает ее вывод.

    - Узел может быть настроен либо на ожидание завершения команды, либо на отправку своих выходных данных, пока команда их генерирует. + Узел может быть настроен либо на ожидание завершения выполнения команды, либо на отправку выходных данных по мере их генерации в ходе выполнения.

    - Выполняемая команда может быть настроена в узле или предоставлена полученным сообщением. + Выполняемая команда может быть установлена в настройках узла или полученным сообщением.

    Принимает

    payload строка
    -
    будет добавлено к выполненной команде, если настроено так делать.
    +
    будет добавлено к выполняемой команде, если узел настроен так делать.
    kill строка
    тип сигнала уничтожения для отправки существующему процессу узла exec.
    pid число|строка
    @@ -44,7 +44,7 @@
    rc объект
    -
    только в exec режиме, копия объекта кода возврата (также доступна на порту 3)
    +
    только в exec режиме, копия объекта кода возврата (также доступна по 3-му порту)
  • Стандартный вывод ошибок @@ -54,7 +54,7 @@
    rc объект
    -
    только в exec режиме, копия объекта кода возврата (также доступна на порту 3)
    +
    только в exec режиме, копия объекта кода возврата (также доступна по 3-му порту)
  • Код возврата @@ -73,7 +73,7 @@ При желании вместо этого можно использовать spawn, который возвращает выходные данные из stdout и stderr по ходу выполнения команды, обычно по одной строке за раз. После завершения он возвращает объект на 3-й порт. Например, успешная команда должна вернуть {code: 0}.

    - Ошибки могут возвращать дополнительную информацию на 3-м порту в msg.payload, такую как строка message, строка signal. + Ошибки могут возвращать на 3-й порт дополнительную информацию в msg.payload, такую как строка message, строка signal.

    Выполняемая команда настраивается в узле, с возможностью добавления к ней msg.payload и дополнительного набора параметров. @@ -82,25 +82,25 @@ Команды или параметры с пробелами должны быть заключены в кавычки - "Это один параметр"

    - Возвращаемый payload обычно представляет собой строку, пока не обнаружены символы, отличные от UTF8, в этом случае это буфер. + Возвращаемый payload обычно представляет собой строку, пока не обнаружены символы, отличные от UTF8, в этом случае возвращаемое значение будет иметь тип буфер.

    - Значок состояния узла и PID будут видны, пока узел активен. Изменения в статусе могут быть прочитаны узлом Status. + Значок статуса узла и PID будут видны, когда узел активен. Изменения в статусе можно отслеживать узлом Status.

    Уничтожения процессов

    - Отправка msg.kill уничтожит один активный процесс. msg.kill должен быть строкой, содержащей тип передаваемого сигнала, например, SIGINT, SIGQUIT или SIGHUP. По умолчанию SIGTERM, если задана пустая строка. + Отправка msg.kill уничтожит один активный процесс. msg.kill должен быть строкой, содержащей тип передаваемого сигнала, например, SIGINT, SIGQUIT или SIGHUP. По умолчанию будет передан SIGTERM, если задана пустая строка.

    - Если узлом запущено более одного процесса, тогда в msg.pid также должно быть установлено значение PID для уничтожения. + Если узлом запущено более одного процесса, тогда в msg.pid также должно быть установлено значение PID процесса для уничтожения.

    - Если в поле Тайм-аут указано значение, то, если процесс не завершится по истечении указанного количества секунд, он будет автоматически уничтожен + Если в поле Тайм-аут введено значение, тогда, если процесс не завершится по истечении указанного количества секунд, он будет автоматически уничтожен.

    - Совет: если Вы запускаете Python приложение, Вам может потребоваться использовать параметр -u, чтобы остановить буферизацию вывода. + Совет: если вы запускаете Python приложение, вам может потребоваться использование параметра -u, чтобы остановить буферизацию вывода.

    diff --git a/packages/node_modules/@node-red/nodes/locales/ru/messages.json b/packages/node_modules/@node-red/nodes/locales/ru/messages.json index 63e597526..08e2b6b73 100755 --- a/packages/node_modules/@node-red/nodes/locales/ru/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/ru/messages.json @@ -46,7 +46,7 @@ "bool": "логический тип", "json": "объект", "bin": "буфер", - "date": "отметка времени", + "date": "метка времени", "env": "переменная среды", "object": "объект", "string": "строка", @@ -55,7 +55,7 @@ "Array": "массив", "invalid": "Неверный объект JSON" }, - "timestamp": "отметка времени", + "timestamp": "метка времени", "none": "нет", "interval": "с интервалом", "interval-time": "с интервалом в промежутке", diff --git a/packages/node_modules/@node-red/nodes/locales/ru/network/05-tls.html b/packages/node_modules/@node-red/nodes/locales/ru/network/05-tls.html index 33c790ed0..4818af0fb 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/network/05-tls.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/network/05-tls.html @@ -15,5 +15,5 @@ --> diff --git a/packages/node_modules/@node-red/nodes/locales/ru/network/06-httpproxy.html b/packages/node_modules/@node-red/nodes/locales/ru/network/06-httpproxy.html index 85fe4eb81..fb7b5ddc9 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/network/06-httpproxy.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/network/06-httpproxy.html @@ -21,6 +21,6 @@

    Подробности

    - При доступе к хосту, находящемуся в списке игнорируемых хостов, прокси не будет использоваться. + При обращении к хосту, находящемуся в списке игнорируемых хостов, прокси не будет использоваться.

    diff --git a/packages/node_modules/@node-red/nodes/locales/ru/network/10-mqtt.html b/packages/node_modules/@node-red/nodes/locales/ru/network/10-mqtt.html index ce10a9515..fab155c5a 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/network/10-mqtt.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/network/10-mqtt.html @@ -13,7 +13,7 @@ diff --git a/packages/node_modules/@node-red/nodes/locales/ru/network/31-tcpin.html b/packages/node_modules/@node-red/nodes/locales/ru/network/31-tcpin.html index f23bd974a..7a3dcf2f3 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/network/31-tcpin.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/network/31-tcpin.html @@ -16,16 +16,16 @@ diff --git a/packages/node_modules/@node-red/nodes/locales/ru/network/32-udp.html b/packages/node_modules/@node-red/nodes/locales/ru/network/32-udp.html index 2a1026e8b..d014c4279 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/network/32-udp.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/network/32-udp.html @@ -16,13 +16,13 @@ @@ -37,6 +37,6 @@ Если Вы выберете широковещательную рассылку, то либо задайте в качестве адреса локальный широковещательный IP-адрес, либо попробуйте 255.255.255.255, который является глобальным широковещательным адресом.

    - Примечание. В некоторых системах Вам могут потребоваться права root или администратора для доступа к портам ниже 1024 и/или широковещательной рассылки. + Примечание. В некоторых системах вам могут потребоваться права root или администратора для доступа к портам ниже 1024 и/или широковещательной рассылки.

    diff --git a/packages/node_modules/@node-red/nodes/locales/ru/parsers/70-CSV.html b/packages/node_modules/@node-red/nodes/locales/ru/parsers/70-CSV.html index de94a1fd8..66186eb7e 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/parsers/70-CSV.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/parsers/70-CSV.html @@ -30,10 +30,10 @@
    payloadобъект | массив | строка
      -
    • Если вход является значением строкового типа, узел пытается проанализировать ее как CSV и создает объект JavaScript из пар ключ/значение для каждой строки. Затем узел либо отправит сообщение для каждой строки или одно сообщение, содержащее массив объектов.
    • -
    • Если вход является JavaScript объектом, узел пытается построить CSV-строку.
    • -
    • Если вход является массивом простых значений, узел построит однострочную CSV-строку.
    • -
    • Если вход является массивом массивов или массивом объектов, создается многострочная CSV-строка.
    • +
    • Если на входе значение строкового типа, узел попытается проанализировать его как CSV и создает объект JavaScript из пар ключ/значение для каждой строки. Затем узел либо отправит сообщение для каждой строки или одно сообщение, содержащее массив объектов.
    • +
    • Если на входе JavaScript объект, узел попробует построить CSV-строку.
    • +
    • Если на входе массив простых значений, узел построит однострочную CSV-строку.
    • +
    • Если на входе массив массивов или массив объектов, создается многострочная CSV-строка.
    @@ -46,7 +46,7 @@ При преобразовании в CSV шаблон столбцов используется для определения того, какие свойства извлекать из объекта и в каком порядке.

    - Если шаблон пуст, то узел может использовать простой список свойств, разделенных запятыми, предоставленных в msg.columns, чтобы определить, что извлечь. Если этого нет, то все свойства объекта выводятся в том порядке, в котором они были найдены в первой строке. + Если шаблон пуст, то узел может использовать простой список свойств, разделенных запятыми, предоставленных в msg.columns, чтобы определить, что извлечь. Если его нет, то все свойства объекта выводятся в том порядке, в котором они были найдены в первой строке.

    Если входные данные являются массивом, то шаблон столбцов используется только для необязательного генерирования строки с заголовками столбцов. diff --git a/packages/node_modules/@node-red/nodes/locales/ru/parsers/70-JSON.html b/packages/node_modules/@node-red/nodes/locales/ru/parsers/70-JSON.html index 0b663ed05..a2dc2da78 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/parsers/70-JSON.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/parsers/70-JSON.html @@ -37,7 +37,7 @@

    schemaErrorмассив
    -
    Если проверка JSON-схемы завершится неудачно, узел catch будет иметь свойство schemaError, содержащее массив ошибок.
    +
    Если проверка JSON-схемы завершится неудачно, узлом catch можно получить свойство schemaError, содержащее массив ошибок.

    Подробности

    @@ -45,10 +45,10 @@ По умолчанию узел работает с msg.payload, но его можно настроить для преобразования любого свойства сообщения.

    - Узел также может быть сконфигурирован для обеспечения конкретной кодировки вместо переключения между ними. Это можно использовать, например, с узлом HTTP In, чтобы гарантировать, что данные payload являются объектом, даже если входящий запрос неправильно установил свой тип содержимого для узла HTTP In, чтобы выполнить преобразование. + Узел также может быть сконфигурирован для обеспечения конкретной кодировки вместо переключения между ними. Это можно использовать, например, при работе с узлом HTTP In, чтобы гарантировать, что данные payload всегда будут являться объектом, даже если входящий запрос неправильно установил свой тип содержимого для узла HTTP In, чтобы выполнить преобразование.

    - Если узел настроен на то, чтобы свойство кодировалось как строка, и он получает строку, дальнейшие проверки этого свойства выполняться не будут. Он не будет проверять, является ли строка допустимым JSON, и не будет переформатировать ее, если выбрана опция форматирования. + Если узел настроен так, чтобы свойство кодировалось как строка, и он получает строку, дальнейшие проверки этого свойства выполняться не будут. Он не будет проверять, является ли строка допустимым JSON, и не будет переформатировать ее, если выбрана опция форматирования.

    Подробнее о JSON-схеме Вы можете узнать в спецификации здесь. diff --git a/packages/node_modules/@node-red/nodes/locales/ru/parsers/70-XML.html b/packages/node_modules/@node-red/nodes/locales/ru/parsers/70-XML.html index ed6fe6c44..d600826bd 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/parsers/70-XML.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/parsers/70-XML.html @@ -32,8 +32,8 @@

    payloadобъект | строка
      -
    • Если вход является значением строкового типа, узел пытается проанализировать ее как XML и создает объект JavaScript.
    • -
    • Если вход является JavaScript объектом, узел пытается построить XML-строку.
    • +
    • Если на входе значение строкового типа, узел пытается проанализировать его как XML и создает объект JavaScript.
    • +
    • Если на входе JavaScript объект, узел пытается построить XML-строку.
    diff --git a/packages/node_modules/@node-red/nodes/locales/ru/parsers/70-YAML.html b/packages/node_modules/@node-red/nodes/locales/ru/parsers/70-YAML.html index b0049330b..8d8ac5f9b 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/parsers/70-YAML.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/parsers/70-YAML.html @@ -30,8 +30,8 @@
    payloadобъект | строка
      -
    • Если вход является YAML-строкой, узел пытается проанализировать ее как JavaScript объект.
    • -
    • Если вход является JavaScript объектом, узел создает YAML-строку.
    • +
    • Если на входе YAML-строка, узел пытается проанализировать ее как JavaScript объект.
    • +
    • Если на входе JavaScript объект, узел создает YAML-строку.
    diff --git a/packages/node_modules/@node-red/nodes/locales/ru/sequence/17-split.html b/packages/node_modules/@node-red/nodes/locales/ru/sequence/17-split.html index cf7397157..717c6de75 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/sequence/17-split.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/sequence/17-split.html @@ -24,7 +24,7 @@
    payloadобъект | строка | массив | буфер
    Поведение узла определяется типом msg.payload:
      -
    • строка/буфер - сообщение разделяется с помощью указанного символа (по умолчанию: \n), последовательности буфера или фиксированной длины.
    • +
    • строка/буфер - сообщение разделяется с помощью указанного символа (по умолчанию: \n), последовательности буфера или по фиксированной длине.
    • массив - сообщение разбивается на отдельные элементы массива или массивы фиксированной длины.
    • объект - сообщение отправляется для каждой пары ключ/значение объекта.
    @@ -34,7 +34,7 @@

    Выводит

    partsобъект
    -
    Это свойство содержит информацию о том, как сообщение было отделено от исходного сообщения. При передаче на узел join последовательность может быть собрана в одно сообщение. Свойство имеет следующие свойства: +
    Это свойство содержит информацию о том, как сообщение было отделено от исходного сообщения. При передаче на узел join последовательность может быть собрана обратно в одно сообщение. Объект содержит следующие свойства:
    • id - идентификатор группы сообщений
    • index - позиция в группе
    • @@ -49,7 +49,7 @@

      Подробности

      - Этот узел облегчает создание потока, который выполняет общие действия над последовательностью сообщений, перед тем как с помощью узла join объединить последовательность в одно сообщение. + Этот узел облегчает создание потока, который выполняет общие действия над последовательностью сообщений, перед тем как с помощью узла join объединить последовательность обратно в одно сообщение.

      Он использует свойство msg.parts для отслеживания отдельных частей последовательности. @@ -60,7 +60,7 @@ Узел также может использоваться для переформатирования потока сообщений. Например, последовательное устройство, которое отправляет завершенные новой строкой команды, может доставлять одно сообщение с частичной командой в конце. В 'потоковом режиме' этот узел будет разбивать сообщение и отправлять каждый завершенный сегмент. Если в конце есть частичный сегмент, узел удержит его и добавит к следующему полученному сообщению.

      - При работе в этом режиме узел не будет устанавливать свойство msg.parts.count, так как он не знает, сколько сообщений ожидать в потоке. Это означает, что его нельзя использовать с узлом join в его автоматическом режиме. + При работе в этом режиме узел не будет устанавливать свойство msg.parts.count, так как он не знает, сколько сообщений ожидать в потоке. Это означает, что его нельзя использовать вместе с узлом join в его автоматическом режиме.

      @@ -73,7 +73,7 @@

      автоматический
      -
      При использовании с узлом split он автоматически объединит сообщения, чтобы отменить выполненное разделение.
      +
      При использовании с узлом split он автоматически объединит сообщения, чтобы восстановить структуру сообщения, которая была до разделения.
      ручной
      Объединяет последовательности сообщений различными способами.
      агрегация последовательности
      @@ -103,7 +103,7 @@

      Подробности

      Автоматический режим

      - В автоматическом режиме используется свойство parts входящих сообщений, чтобы определить способ объединения последовательности. Это позволяет автоматически отменять действие узла split. + В автоматическом режиме используется свойство parts входящих сообщений, чтобы определить способ объединения последовательности. Это позволяет автоматически выполнять обратное действие для узла split.

      Ручной режим

      @@ -123,7 +123,7 @@ Свойство кол-во может быть установлено для количества сообщений, которое должно быть получено перед генерацией выходного сообщения. Для выходных данных объекта, когда это число достигнуто, узел может быть настроен на отправку сообщения для каждого последующего полученного сообщения.

      - Свойство время (сек) может быть установлено, чтобы инициировать отправку нового сообщения с использованием того, что было получено до сих пор. + Свойство время (сек) может быть установлено, чтобы инициировать отправку нового сообщения с использованием всего, что было получено до сих пор.

      Если сообщение получено с установленным свойством msg.complete, выходное сообщение завершается и отправляется. Это сбрасывает любой подсчет частей. diff --git a/packages/node_modules/@node-red/nodes/locales/ru/sequence/18-sort.html b/packages/node_modules/@node-red/nodes/locales/ru/sequence/18-sort.html index efd183854..64efa3967 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/sequence/18-sort.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/sequence/18-sort.html @@ -48,7 +48,7 @@

    - Примечание. Этот узел внутренне хранит сообщения для своей работы. Чтобы предотвратить непредвиденное использование памяти, можно указать максимальное количество хранимых сообщений. По умолчанию количество сообщений не ограничено. + Примечание. Этот узел буферизирует сообщения внутри для своей работы. Чтобы предотвратить непредвиденное использование памяти, можно указать максимальное количество хранимых сообщений. По умолчанию количество сообщений не ограничено.

    • Свойство nodeMessageBufferMaxLength устанавливается в settings.js.
    diff --git a/packages/node_modules/@node-red/nodes/locales/ru/sequence/19-batch.html b/packages/node_modules/@node-red/nodes/locales/ru/sequence/19-batch.html index ecda13154..2589233be 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/sequence/19-batch.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/sequence/19-batch.html @@ -37,7 +37,7 @@

    Хранение сообщений

    - Этот узел будет буферизировать сообщения внутри, чтобы работать между последовательностями. Параметр nodeMessageBufferMaxLength можно использовать для ограничения количества сообщений, которые узел будут буферизовать. + Этот узел будет буферизировать сообщения внутри, чтобы работать с последовательностями. Параметр nodeMessageBufferMaxLength можно использовать для ограничения количества сообщений, которые узел будут буферизовать.

    Если сообщение получено с установленным свойством msg.reset, буферизованные сообщения удаляются и не отправляются. diff --git a/packages/node_modules/@node-red/nodes/locales/ru/storage/10-file.html b/packages/node_modules/@node-red/nodes/locales/ru/storage/10-file.html index e1c8bf456..8a4a64f24 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/storage/10-file.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/storage/10-file.html @@ -16,7 +16,7 @@ diff --git a/packages/node_modules/@node-red/registry/lib/index.js b/packages/node_modules/@node-red/registry/lib/index.js index ef091992a..a2d0bb1f7 100644 --- a/packages/node_modules/@node-red/registry/lib/index.js +++ b/packages/node_modules/@node-red/registry/lib/index.js @@ -29,6 +29,7 @@ var loader = require("./loader"); var installer = require("./installer"); var library = require("./library"); const externalModules = require("./externalModules") +var plugins = require("./plugins"); /** * Initialise the registry with a reference to a runtime object @@ -41,6 +42,7 @@ function init(runtime) { // the util module it. The Util module is responsible for constructing the // RED object passed to node modules when they are loaded. loader.init(runtime); + plugins.init(runtime.settings); registry.init(runtime.settings,loader); library.init(); externalModules.init(runtime.settings); @@ -301,6 +303,12 @@ module.exports = { checkFlowDependencies: externalModules.checkFlowDependencies, + registerPlugin: plugins.registerPlugin, + getPlugin: plugins.getPlugin, + getPluginsByType: plugins.getPluginsByType, + getPluginList: plugins.getPluginList, + getPluginConfigs: plugins.getPluginConfigs, + deprecated: require("./deprecated") }; diff --git a/packages/node_modules/@node-red/registry/lib/library.js b/packages/node_modules/@node-red/registry/lib/library.js index 90b0fda82..b305456a5 100644 --- a/packages/node_modules/@node-red/registry/lib/library.js +++ b/packages/node_modules/@node-red/registry/lib/library.js @@ -27,17 +27,19 @@ async function getFlowsFromPath(path) { var validFiles = []; return fs.readdir(path).then(files => { var promises = []; - files.forEach(function(file) { - var fullPath = fspath.join(path,file); - var stats = fs.lstatSync(fullPath); - if (stats.isDirectory()) { - validFiles.push(file); - promises.push(getFlowsFromPath(fullPath)); - } else if (/\.json$/.test(file)){ - validFiles.push(file); - promises.push(Promise.resolve(file.split(".")[0])) - } - }) + if (files) { + files.forEach(function(file) { + var fullPath = fspath.join(path,file); + var stats = fs.lstatSync(fullPath); + if (stats.isDirectory()) { + validFiles.push(file); + promises.push(getFlowsFromPath(fullPath)); + } else if (/\.json$/.test(file)){ + validFiles.push(file); + promises.push(Promise.resolve(file.split(".")[0])) + } + }) + } return Promise.all(promises) }).then(results => { results.forEach(function(r,i) { diff --git a/packages/node_modules/@node-red/registry/lib/loader.js b/packages/node_modules/@node-red/registry/lib/loader.js index f35987732..bd5a7aae4 100644 --- a/packages/node_modules/@node-red/registry/lib/loader.js +++ b/packages/node_modules/@node-red/registry/lib/loader.js @@ -36,78 +36,140 @@ function load(disableNodePathScan) { // To skip node scan, the following line will use the stored node list. // We should expose that as an option at some point, although the // performance gains are minimal. - //return loadNodeFiles(registry.getModuleList()); + //return loadModuleFiles(registry.getModuleList()); log.info(log._("server.loading")); - var nodeFiles = localfilesystem.getNodeFiles(disableNodePathScan); - return loadNodeFiles(nodeFiles); + var modules = localfilesystem.getNodeFiles(disableNodePathScan); + return loadModuleFiles(modules); } -function loadNodeFiles(nodeFiles) { + +function loadModuleTypeFiles(module, type) { + const things = module[type]; + var first = true; var promises = []; - var nodes = []; - for (var module in nodeFiles) { + for (var thingName in things) { /* istanbul ignore else */ - if (nodeFiles.hasOwnProperty(module)) { - if (nodeFiles[module].redVersion && - !semver.satisfies((settings.version||"0.0.0").replace(/(\-[1-9A-Za-z-][0-9A-Za-z-\.]*)?(\+[0-9A-Za-z-\.]+)?$/,""), nodeFiles[module].redVersion)) { + if (things.hasOwnProperty(thingName)) { + if (module.name != "node-red" && first) { + // Check the module directory exists + first = false; + var fn = things[thingName].file; + var parts = fn.split("/"); + var i = parts.length-1; + for (;i>=0;i--) { + if (parts[i] == "node_modules") { + break; + } + } + var moduleFn = parts.slice(0,i+2).join("/"); + + try { + var stat = fs.statSync(moduleFn); + } catch(err) { + // Module not found, don't attempt to load its nodes + break; + } + } + + try { + var promise; + if (type === "nodes") { + promise = loadNodeConfig(things[thingName]); + } else if (type === "plugins") { + promise = loadPluginConfig(things[thingName]); + } + promises.push( + promise.then( + (function() { + var m = module.name; + var n = thingName; + return function(nodeSet) { + things[n] = nodeSet; + return nodeSet; + } + })() + ).catch(err => {console.log(err)}) + ); + } catch(err) { + console.log(err) + // + } + } + } + return promises; +} + +function loadModuleFiles(modules) { + var pluginPromises = []; + var nodePromises = []; + for (var module in modules) { + /* istanbul ignore else */ + if (modules.hasOwnProperty(module)) { + if (modules[module].redVersion && + !semver.satisfies((settings.version||"0.0.0").replace(/(\-[1-9A-Za-z-][0-9A-Za-z-\.]*)?(\+[0-9A-Za-z-\.]+)?$/,""), modules[module].redVersion)) { //TODO: log it - log.warn("["+module+"] "+log._("server.node-version-mismatch",{version:nodeFiles[module].redVersion})); - nodeFiles[module].err = "version_mismatch"; + log.warn("["+module+"] "+log._("server.node-version-mismatch",{version:modules[module].redVersion})); + modules[module].err = "version_mismatch"; continue; } if (module == "node-red" || !registry.getModuleInfo(module)) { - var first = true; - for (var node in nodeFiles[module].nodes) { - /* istanbul ignore else */ - if (nodeFiles[module].nodes.hasOwnProperty(node)) { - if (module != "node-red" && first) { - // Check the module directory exists - first = false; - var fn = nodeFiles[module].nodes[node].file; - var parts = fn.split("/"); - var i = parts.length-1; - for (;i>=0;i--) { - if (parts[i] == "node_modules") { - break; - } - } - var moduleFn = parts.slice(0,i+2).join("/"); - - try { - var stat = fs.statSync(moduleFn); - } catch(err) { - // Module not found, don't attempt to load its nodes - break; - } - } - - try { - promises.push(loadNodeConfig(nodeFiles[module].nodes[node]).then((function() { - var m = module; - var n = node; - return function(nodeSet) { - nodeFiles[m].nodes[n] = nodeSet; - nodes.push(nodeSet); - } - })()).catch(err => {})); - } catch(err) { - // - } - } + if (modules[module].nodes) { + nodePromises = nodePromises.concat(loadModuleTypeFiles(modules[module], "nodes")); + } + if (modules[module].plugins) { + pluginPromises = pluginPromises.concat(loadModuleTypeFiles(modules[module], "plugins")); } } } } - return Promise.all(promises).then(function(results) { - for (var module in nodeFiles) { - if (nodeFiles.hasOwnProperty(module)) { - if (!nodeFiles[module].err) { - registry.addModule(nodeFiles[module]); + var pluginList; + var nodeList; + + return Promise.all(pluginPromises).then(function(results) { + pluginList = results.filter(r => !!r); + // Initial plugin load has happened. Ensure modules that provide + // plugins are in the registry now. + for (var module in modules) { + if (modules.hasOwnProperty(module)) { + if (modules[module].plugins && Object.keys(modules[module].plugins).length > 0) { + // Add the modules for plugins + if (!modules[module].err) { + registry.addModule(modules[module]); + } } } } - return loadNodeSetList(nodes); + return loadNodeSetList(pluginList); + }).then(function() { + return Promise.all(nodePromises); + }).then(function(results) { + nodeList = results.filter(r => !!r); + // Initial node load has happened. Ensure remaining modules are in the registry + for (var module in modules) { + if (modules.hasOwnProperty(module)) { + if (!modules[module].plugins || Object.keys(modules[module].plugins).length === 0) { + if (!modules[module].err) { + registry.addModule(modules[module]); + } + } + } + } + return loadNodeSetList(nodeList); + }); +} + +async function loadPluginTemplate(plugin) { + return fs.readFile(plugin.template,'utf8').then(content => { + plugin.config = content; + return plugin; + }).catch(err => { + if (err.code === 'ENOENT') { + plugin.err = "Error: "+plugin.template+" does not exist"; + } else { + plugin.err = err.toString(); + } + return plugin; }); } @@ -175,11 +237,12 @@ async function loadNodeLocales(node) { node.namespace = node.module; return node } - return fs.stat(path.join(path.dirname(node.file),"locales")).then(stat => { + const baseFile = node.file||node.template; + return fs.stat(path.join(path.dirname(baseFile),"locales")).then(stat => { node.namespace = node.id; return i18n.registerMessageCatalog(node.id, - path.join(path.dirname(node.file),"locales"), - path.basename(node.file,".js")+".json") + path.join(path.dirname(baseFile),"locales"), + path.basename(baseFile).replace(/\.[^.]+$/,".json")) .then(() => node); }).catch(err => { node.namespace = node.module; @@ -204,6 +267,7 @@ async function loadNodeConfig(fileInfo) { } var node = { + type: "node", id: id, module: module, name: name, @@ -227,6 +291,58 @@ async function loadNodeConfig(fileInfo) { return node; } +async function loadPluginConfig(fileInfo) { + var file = fileInfo.file; + var module = fileInfo.module; + var name = fileInfo.name; + var version = fileInfo.version; + + var id = module + "/" + name; + var isEnabled = true; + + // TODO: registry.getPluginInfo + + // var info = registry.getPluginInfo(id); + // if (info) { + // if (info.hasOwnProperty("loaded")) { + // throw new Error(file+" already loaded"); + // } + // isEnabled = info.enabled; + // } + + + if (!fs.existsSync(jsFile)) { + } + + var plugin = { + type: "plugin", + id: id, + module: module, + name: name, + enabled: isEnabled, + loaded:false, + version: version, + local: fileInfo.local, + plugins: [], + config: "", + help: {} + }; + var jsFile = file.replace(/\.[^.]+$/,".js"); + var htmlFile = file.replace(/\.[^.]+$/,".html"); + if (fs.existsSync(jsFile)) { + plugin.file = jsFile; + } + if (fs.existsSync(htmlFile)) { + plugin.template = htmlFile; + } + await loadNodeLocales(plugin) + + if (plugin.template && !settings.disableEditor) { + return loadPluginTemplate(plugin); + } + return plugin +} + /** * Loads the specified node into the runtime * @param node a node info object - see loadNodeConfig @@ -236,8 +352,6 @@ async function loadNodeConfig(fileInfo) { * */ function loadNodeSet(node) { - var nodeDir = path.dirname(node.file); - var nodeFn = path.basename(node.file); if (!node.enabled) { return Promise.resolve(node); } else { @@ -284,11 +398,59 @@ function loadNodeSet(node) { } } +async function loadPlugin(plugin) { + if (!plugin.file) { + // No runtime component - nothing to load + return plugin; + } + try { + var r = require(plugin.file); + if (typeof r === "function") { + + var red = registryUtil.createNodeApi(plugin); + var promise = r(red); + if (promise != null && typeof promise.then === "function") { + return promise.then(function() { + plugin.enabled = true; + plugin.loaded = true; + return plugin; + }).catch(function(err) { + plugin.err = err; + return plugin; + }); + } + } + plugin.enabled = true; + plugin.loaded = true; + return plugin; + } catch(err) { + console.log(err); + plugin.err = err; + var stack = err.stack; + var message; + if (stack) { + var i = stack.indexOf(plugin.file); + if (i > -1) { + var excerpt = stack.substring(i+node.file.length+1,i+plugin.file.length+20); + var m = /^(\d+):(\d+)/.exec(excerpt); + if (m) { + plugin.err = err+" (line:"+m[1]+")"; + } + } + } + return plugin; + } +} + function loadNodeSetList(nodes) { var promises = []; nodes.forEach(function(node) { if (!node.err) { - promises.push(loadNodeSet(node).catch(err => {})); + if (node.type === "plugin") { + promises.push(loadPlugin(node).catch(err => {})); + } else { + promises.push(loadNodeSet(node).catch(err => {})); + } } else { promises.push(node); } @@ -338,7 +500,7 @@ function addModule(module) { } } } - return loadNodeFiles(moduleFiles).then(() => module) + return loadModuleFiles(moduleFiles).then(() => module) } catch(err) { return Promise.reject(err); } diff --git a/packages/node_modules/@node-red/registry/lib/localfilesystem.js b/packages/node_modules/@node-red/registry/lib/localfilesystem.js index ed4b81d8e..92e03fd62 100644 --- a/packages/node_modules/@node-red/registry/lib/localfilesystem.js +++ b/packages/node_modules/@node-red/registry/lib/localfilesystem.js @@ -220,33 +220,41 @@ function getModuleNodeFiles(module) { var moduleDir = module.dir; var pkg = module.package; - var nodes = pkg['node-red'].nodes||{}; - var results = []; var iconDirs = []; var iconList = []; - for (var n in nodes) { - /* istanbul ignore else */ - if (nodes.hasOwnProperty(n)) { - var file = path.join(moduleDir,nodes[n]); - results.push({ - file: file, - module: pkg.name, - name: n, - version: pkg.version - }); - var iconDir = path.join(moduleDir,path.dirname(nodes[n]),"icons"); - if (iconDirs.indexOf(iconDir) == -1) { - try { - fs.statSync(iconDir); - var icons = scanIconDir(iconDir); - iconList.push({path:iconDir,icons:icons}); - iconDirs.push(iconDir); - } catch(err) { + + function scanTypes(types) { + const files = []; + for (var n in types) { + /* istanbul ignore else */ + if (types.hasOwnProperty(n)) { + var file = path.join(moduleDir,types[n]); + files.push({ + file: file, + module: pkg.name, + name: n, + version: pkg.version + }); + var iconDir = path.join(moduleDir,path.dirname(types[n]),"icons"); + if (iconDirs.indexOf(iconDir) == -1) { + try { + fs.statSync(iconDir); + var icons = scanIconDir(iconDir); + iconList.push({path:iconDir,icons:icons}); + iconDirs.push(iconDir); + } catch(err) { + } } } } + return files; } - var result = {files:results,icons:iconList}; + + var result = { + nodeFiles:scanTypes(pkg['node-red'].nodes||{}), + pluginFiles:scanTypes(pkg['node-red'].plugins||{}), + icons:iconList + }; var examplesDir = path.join(moduleDir,"examples"); try { @@ -396,6 +404,7 @@ function convertModuleFileListToObject(moduleFiles) { local: moduleFile.local||false, user: moduleFile.user||false, nodes: {}, + plugins: {}, icons: nodeModuleFiles.icons, examples: nodeModuleFiles.examples }; @@ -408,11 +417,14 @@ function convertModuleFileListToObject(moduleFiles) { if (moduleFile.usedBy) { nodeList[moduleFile.package.name].usedBy = moduleFile.usedBy; } - nodeModuleFiles.files.forEach(function(node) { - node.local = moduleFile.local||false; + nodeModuleFiles.nodeFiles.forEach(function(node) { nodeList[moduleFile.package.name].nodes[node.name] = node; nodeList[moduleFile.package.name].nodes[node.name].local = moduleFile.local || false; }); + nodeModuleFiles.pluginFiles.forEach(function(plugin) { + nodeList[moduleFile.package.name].plugins[plugin.name] = plugin; + nodeList[moduleFile.package.name].plugins[plugin.name].local = moduleFile.local || false; + }); }); return nodeList; } diff --git a/packages/node_modules/@node-red/registry/lib/plugins.js b/packages/node_modules/@node-red/registry/lib/plugins.js new file mode 100644 index 000000000..82b230011 --- /dev/null +++ b/packages/node_modules/@node-red/registry/lib/plugins.js @@ -0,0 +1,103 @@ +const registry = require("./registry"); +const {events} = require("@node-red/util") + +var pluginConfigCache = {}; +var pluginToId = {}; +var plugins = {}; +var pluginsByType = {}; +var settings; + +function init(_settings) { + settings = _settings; + plugins = {}; + pluginConfigCache = {}; + pluginToId = {}; + pluginsByType = {}; +} + +function registerPlugin(nodeSetId,id,definition) { + var moduleId = registry.getModuleFromSetId(nodeSetId); + var pluginId = registry.getNodeFromSetId(nodeSetId); + + definition.id = id; + definition.module = moduleId; + pluginToId[id] = nodeSetId; + plugins[id] = definition; + var module = registry.getModule(moduleId); + + definition.path = module.path; + + module.plugins[pluginId].plugins.push(definition); + if (definition.type) { + pluginsByType[definition.type] = pluginsByType[definition.type] || []; + pluginsByType[definition.type].push(definition); + } + if (definition.onadd && typeof definition.onadd === 'function') { + definition.onadd(); + } + events.emit("registry:plugin-added",id); +} + +function getPlugin(id) { + return plugins[id] +} + +function getPluginsByType(type) { + return pluginsByType[type] || []; +} + +function getPluginConfigs(lang) { + if (!pluginConfigCache[lang]) { + var result = ""; + var script = ""; + var moduleConfigs = registry.getModuleList(); + for (var module in moduleConfigs) { + /* istanbul ignore else */ + if (moduleConfigs.hasOwnProperty(module)) { + var plugins = moduleConfigs[module].plugins; + for (var plugin in plugins) { + if (plugins.hasOwnProperty(plugin)) { + var config = plugins[plugin]; + if (config.enabled && !config.err && config.config) { + result += "\n\n"; + result += config.config; + } + } + } + } + } + pluginConfigCache[lang] = result; + } + return pluginConfigCache[lang]; +} +function getPluginList() { + var list = []; + var moduleConfigs = registry.getModuleList(); + for (var module in moduleConfigs) { + /* istanbul ignore else */ + if (moduleConfigs.hasOwnProperty(module)) { + var plugins = moduleConfigs[module].plugins; + for (var plugin in plugins) { + /* istanbul ignore else */ + if (plugins.hasOwnProperty(plugin)) { + var pluginInfo = registry.filterNodeInfo(plugins[plugin]); + pluginInfo.version = moduleConfigs[module].version; + // if (moduleConfigs[module].pending_version) { + // nodeInfo.pending_version = moduleConfigs[module].pending_version; + // } + list.push(pluginInfo); + } + } + } + } + return list; +} + +module.exports = { + init, + registerPlugin, + getPlugin, + getPluginsByType, + getPluginConfigs, + getPluginList +} \ No newline at end of file diff --git a/packages/node_modules/@node-red/registry/lib/registry.js b/packages/node_modules/@node-red/registry/lib/registry.js index f2ea23b2f..a4d504fac 100644 --- a/packages/node_modules/@node-red/registry/lib/registry.js +++ b/packages/node_modules/@node-red/registry/lib/registry.js @@ -64,17 +64,24 @@ function filterNodeInfo(n) { if (n.hasOwnProperty("err")) { r.err = n.err; } + if (n.hasOwnProperty("plugins")) { + r.plugins = n.plugins; + } + if (n.type === "plugin") { + r.editor = !!n.template; + r.runtime = !!n.file; + } return r; } -function getModule(id) { +function getModuleFromSetId(id) { var parts = id.split("/"); return parts.slice(0,parts.length-1).join("/"); } -function getNode(id) { +function getNodeFromSetId(id) { var parts = id.split("/"); return parts[parts.length-1]; } @@ -217,11 +224,11 @@ function addModule(module) { function removeNode(id) { - var config = moduleConfigs[getModule(id)].nodes[getNode(id)]; + var config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; if (!config) { throw new Error("Unrecognised id: "+id); } - delete moduleConfigs[getModule(id)].nodes[getNode(id)]; + delete moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; var i = nodeList.indexOf(id); if (i > -1) { nodeList.splice(i,1); @@ -292,9 +299,9 @@ function getNodeInfo(typeOrId) { } /* istanbul ignore else */ if (id) { - var module = moduleConfigs[getModule(id)]; + var module = moduleConfigs[getModuleFromSetId(id)]; if (module) { - var config = module.nodes[getNode(id)]; + var config = module.nodes[getNodeFromSetId(id)]; if (config) { var info = filterNodeInfo(config); if (config.hasOwnProperty("loaded")) { @@ -321,9 +328,9 @@ function getFullNodeInfo(typeOrId) { } /* istanbul ignore else */ if (id) { - var module = moduleConfigs[getModule(id)]; + var module = moduleConfigs[getModuleFromSetId(id)]; if (module) { - return module.nodes[getNode(id)]; + return module.nodes[getNodeFromSetId(id)]; } } return null; @@ -357,16 +364,10 @@ function getNodeList(filter) { } function getModuleList() { - //var list = []; - //for (var module in moduleNodes) { - // /* istanbul ignore else */ - // if (moduleNodes.hasOwnProperty(module)) { - // list.push(registry.getModuleInfo(module)); - // } - //} - //return list; return moduleConfigs; - +} +function getModule(id) { + return moduleConfigs[id]; } function getModuleInfo(module) { @@ -465,13 +466,11 @@ function getAllNodeConfigs(lang) { var script = ""; for (var i=0;i 0)) { continue; } - - var config = module.nodes[getNode(id)]; + var config = module.nodes[getNodeFromSetId(id)]; if (config.enabled && !config.err) { result += "\n\n"; result += config.config; @@ -490,11 +489,11 @@ function getAllNodeConfigs(lang) { } function getNodeConfig(id,lang) { - var config = moduleConfigs[getModule(id)]; + var config = moduleConfigs[getModuleFromSetId(id)]; if (!config) { return null; } - config = config.nodes[getNode(id)]; + config = config.nodes[getNodeFromSetId(id)]; if (config) { var result = "\n"+config.config; result += loader.getNodeHelp(config,lang||"en-US") @@ -515,7 +514,7 @@ function getNodeConstructor(type) { if (typeof id === "undefined") { config = undefined; } else { - config = moduleConfigs[getModule(id)].nodes[getNode(id)]; + config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; } if (!config || (config.enabled && !config.err)) { @@ -553,7 +552,7 @@ function enableNodeSet(typeOrId) { } var config; try { - config = moduleConfigs[getModule(id)].nodes[getNode(id)]; + config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; delete config.err; config.enabled = true; nodeConfigCache = {}; @@ -576,7 +575,7 @@ function disableNodeSet(typeOrId) { } var config; try { - config = moduleConfigs[getModule(id)].nodes[getNode(id)]; + config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; // TODO: persist setting config.enabled = false; nodeConfigCache = {}; @@ -715,6 +714,7 @@ var registry = module.exports = { getFullNodeInfo: getFullNodeInfo, getNodeList: getNodeList, getModuleList: getModuleList, + getModule: getModule, getModuleInfo: getModuleInfo, getNodeIconPath: getNodeIconPath, @@ -730,5 +730,8 @@ var registry = module.exports = { saveNodeList: saveNodeList, - cleanModuleList: cleanModuleList + cleanModuleList: cleanModuleList, + getModuleFromSetId: getModuleFromSetId, + getNodeFromSetId: getNodeFromSetId, + filterNodeInfo: filterNodeInfo }; diff --git a/packages/node_modules/@node-red/registry/lib/util.js b/packages/node_modules/@node-red/registry/lib/util.js index d6433f6f4..0a5d579ab 100644 --- a/packages/node_modules/@node-red/registry/lib/util.js +++ b/packages/node_modules/@node-red/registry/lib/util.js @@ -70,6 +70,17 @@ function createNodeApi(node) { }) } }, + plugins: { + registerPlugin: function(id,definition) { + return runtime.plugins.registerPlugin(node.id,id,definition); + }, + get: function(id) { + return runtime.plugins.getPlugin(id); + }, + getByType: function(type) { + return runtime.plugins.getPluginsByType(type); + } + }, library: { register: function(type) { return runtime.library.register(node.id,type); diff --git a/packages/node_modules/@node-red/runtime/lib/api/index.js b/packages/node_modules/@node-red/runtime/lib/api/index.js index b131470b0..46d15d1e7 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/index.js +++ b/packages/node_modules/@node-red/runtime/lib/api/index.js @@ -28,6 +28,7 @@ var api = module.exports = { api.library.init(runtime); api.projects.init(runtime); api.context.init(runtime); + api.plugins.init(runtime); }, comms: require("./comms"), @@ -37,6 +38,7 @@ var api = module.exports = { settings: require("./settings"), projects: require("./projects"), context: require("./context"), + plugins: require("./plugins"), isStarted: async function(opts) { return runtime.isStarted(); diff --git a/packages/node_modules/@node-red/runtime/lib/api/nodes.js b/packages/node_modules/@node-red/runtime/lib/api/nodes.js index 556e57df9..db4ed32a5 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/nodes.js +++ b/packages/node_modules/@node-red/runtime/lib/api/nodes.js @@ -94,6 +94,10 @@ var api = module.exports = { getNodeConfig: async function(opts) { var id = opts.id; var lang = opts.lang; + if (/[^a-z\-\*]/i.test(opts.lang)) { + reject(new Error("Invalid language: "+opts.lang)); + return + } var result = runtime.nodes.getNodeConfig(id,lang); if (result) { runtime.log.audit({event: "nodes.config.get",id:id}, opts.req); @@ -116,6 +120,10 @@ var api = module.exports = { * @memberof @node-red/runtime_nodes */ getNodeConfigs: async function(opts) { + if (/[^a-z\-\*]/i.test(opts.lang)) { + reject(new Error("Invalid language: "+opts.lang)); + return + } runtime.log.audit({event: "nodes.configs.get"}, opts.req); return runtime.nodes.getNodeConfigs(opts.lang); }, @@ -374,6 +382,10 @@ var api = module.exports = { getModuleCatalogs: async function(opts) { var namespace = opts.module; var lang = opts.lang; + if (/[^a-z\-\*]/i.test(lang)) { + reject(new Error("Invalid language: "+lang)); + return + } var prevLang = runtime.i18n.i.language; // Trigger a load from disk of the language if it is not the default return new Promise( (resolve,reject) => { @@ -404,6 +416,10 @@ var api = module.exports = { getModuleCatalog: async function(opts) { var namespace = opts.module; var lang = opts.lang; + if (/[^a-z\-\*]/i.test(lang)) { + reject(new Error("Invalid language: "+lang)); + return + } var prevLang = runtime.i18n.i.language; // Trigger a load from disk of the language if it is not the default return new Promise(resolve => { diff --git a/packages/node_modules/@node-red/runtime/lib/api/plugins.js b/packages/node_modules/@node-red/runtime/lib/api/plugins.js new file mode 100644 index 000000000..076638640 --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/api/plugins.js @@ -0,0 +1,95 @@ +/** + * @mixin @node-red/runtime_plugins + */ + +var runtime; + +var api = module.exports = { + init: function(_runtime) { + runtime = _runtime; + }, + + /** + * Gets a plugin definition from the registry + * @param {Object} opts + * @param {String} opts.id - the id of the plugin to get + * @param {User} opts.user - the user calling the api + * @param {Object} opts.req - the request to log (optional) + * @return {Promise} - the plugin definition + * @memberof @node-red/runtime_plugins + */ + getPlugin: async function(opts) { + return runtime.plugins.getPlugin(opts.id); + }, + + /** + * Gets all plugin definitions of a given type + * @param {Object} opts + * @param {String} opts.type - the type of the plugins to get + * @param {User} opts.user - the user calling the api + * @param {Object} opts.req - the request to log (optional) + * @return {Promise} - the plugin definitions + * @memberof @node-red/runtime_plugins + */ + getPluginsByType: async function(opts) { + return runtime.plugins.getPluginsByType(opts.type); + }, + + /** + * Gets the editor content for an individual plugin + * @param {String} opts.lang - the locale language to return + * @param {User} opts.user - the user calling the api + * @param {Object} opts.req - the request to log (optional) + * @return {Promise} - the node information + * @memberof @node-red/runtime_plugins + */ + getPluginList: async function(opts) { + runtime.log.audit({event: "plugins.list.get"}, opts.req); + return runtime.plugins.getPluginList(); + }, + + /** + * Gets the editor content for all registered plugins + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {User} opts.user - the user calling the api + * @param {Object} opts.req - the request to log (optional) + * @return {Promise} - the node information + * @memberof @node-red/runtime_plugins + */ + getPluginConfigs: async function(opts) { + if (/[^a-z\-]/i.test(opts.lang)) { + throw new Error("Invalid language: "+opts.lang) + return; + } + runtime.log.audit({event: "plugins.configs.get"}, opts.req); + return runtime.plugins.getPluginConfigs(opts.lang); + }, + /** + * Gets all registered module message catalogs + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {User} opts.lang - the i18n language to return. If not set, uses runtime default (en-US) + * @param {Object} opts.req - the request to log (optional) + * @return {Promise} - the message catalogs + * @memberof @node-red/runtime_plugins + */ + getPluginCatalogs: async function(opts) { + var lang = opts.lang; + var prevLang = runtime.i18n.i.language; + // Trigger a load from disk of the language if it is not the default + return new Promise( (resolve,reject) => { + runtime.i18n.i.changeLanguage(lang, function(){ + var nodeList = runtime.plugins.getPluginList(); + var result = {}; + nodeList.forEach(function(n) { + if (n.module !== "node-red") { + result[n.id] = runtime.i18n.i.getResourceBundle(lang, n.id)||{}; + } + }); + runtime.i18n.i.changeLanguage(prevLang); + resolve(result); + }); + }); + }, +} diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js index 86c9156fa..1df63d7ac 100644 --- a/packages/node_modules/@node-red/runtime/lib/index.js +++ b/packages/node_modules/@node-red/runtime/lib/index.js @@ -21,6 +21,7 @@ var flows = require("./flows"); var storage = require("./storage"); var library = require("./library"); var hooks = require("./hooks"); +var plugins = require("./plugins"); var settings = require("./settings"); var express = require("express"); @@ -281,6 +282,7 @@ var runtime = { storage: storage, hooks: hooks, nodes: redNodes, + plugins: plugins, flows: flows, library: library, exec: exec, @@ -342,6 +344,12 @@ module.exports = { */ context: externalAPI.context, + /** + * @memberof @node-red/runtime + * @mixes @node-red/runtime_plugins + */ + plugins: externalAPI.plugins, + /** * Returns whether the runtime is started * @param {Object} opts diff --git a/packages/node_modules/@node-red/runtime/lib/plugins.js b/packages/node_modules/@node-red/runtime/lib/plugins.js new file mode 100644 index 000000000..738a64fa7 --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/plugins.js @@ -0,0 +1,10 @@ +const registry = require("@node-red/registry"); + +module.exports = { + init: function() {}, + registerPlugin: registry.registerPlugin, + getPlugin: registry.getPlugin, + getPluginsByType: registry.getPluginsByType, + getPluginList: registry.getPluginList, + getPluginConfigs: registry.getPluginConfigs, +} \ No newline at end of file diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js index 060a71227..4057f83b3 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js @@ -305,6 +305,9 @@ Project.prototype.update = async function (user, data) { return new Error("Invalid package file: "+data.files.package) } var root = data.files.package.substring(0,data.files.package.length-12); + if (/^\.\./.test(fspath.relative(this.path,fspath.join(this.path,data.files.package)))) { + return Promise.reject("Invalid package file: "+data.files.package) + } this.paths.root = root; this.paths['package.json'] = data.files.package; globalProjectSettings.projects[this.name].rootPath = root; @@ -322,12 +325,18 @@ Project.prototype.update = async function (user, data) { } if (data.files.hasOwnProperty('flow') && this.package['node-red'].settings.flowFile !== data.files.flow.substring(this.paths.root.length)) { + if (/^\.\./.test(fspath.relative(this.path,fspath.join(this.path,data.files.flow)))) { + return Promise.reject("Invalid flow file: "+data.files.flow) + } this.paths.flowFile = data.files.flow; this.package['node-red'].settings.flowFile = data.files.flow.substring(this.paths.root.length); savePackage = true; flowFilesChanged = true; } if (data.files.hasOwnProperty('credentials') && this.package['node-red'].settings.credentialsFile !== data.files.credentials.substring(this.paths.root.length)) { + if (/^\.\./.test(fspath.relative(this.path,fspath.join(this.path,data.files.credentials)))) { + return Promise.reject("Invalid credentials file: "+data.files.credentials) + } this.paths.credentialsFile = data.files.credentials; this.package['node-red'].settings.credentialsFile = data.files.credentials.substring(this.paths.root.length); // Don't know if the credSecret is invalid or not so clear the flag @@ -490,6 +499,10 @@ Project.prototype.getFile = function (filePath,treeish) { if (treeish !== "_") { return gitTools.getFile(this.path, filePath, treeish); } else { + let fullPath = fspath.join(this.path,filePath); + if (/^\.\./.test(fspath.relative(this.path,fullPath))) { + throw new Error("Invalid file name") + } return fs.readFile(fspath.join(this.path,filePath),"utf8"); } }; @@ -639,6 +652,11 @@ Project.prototype.pull = function (user,remoteBranchName,setRemote,allowUnrelate Project.prototype.resolveMerge = function (file,resolutions) { var filePath = fspath.join(this.path,file); + + if (/^\.\./.test(fspath.relative(this.path,filePath))) { + throw new Error("Invalid file name") + } + var self = this; if (typeof resolutions === 'string') { return util.writeFile(filePath, resolutions).then(function() { @@ -1047,7 +1065,10 @@ function loadProject(projectPath) { function init(_settings, _runtime) { settings = _settings; runtime = _runtime; - projectsDir = fspath.join(settings.userDir,"projects"); + projectsDir = fspath.resolve(fspath.join(settings.userDir,"projects")); + if(settings.editorTheme.projects.path) { + projectsDir = settings.editorTheme.projects.path; + } authCache.init(); } diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js index 8ddd61232..0533fe5e6 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js @@ -113,7 +113,13 @@ function init(_settings, _runtime) { globalGitUser = gitConfig.user; Projects.init(settings,runtime); sshTools.init(settings); - projectsDir = fspath.join(settings.userDir,"projects"); + + projectsDir = fspath.resolve(fspath.join(settings.userDir,"projects")); + + if(settings.editorTheme.projects.path) { + projectsDir = settings.editorTheme.projects.path; + } + if (!settings.readOnly) { return fs.ensureDir(projectsDir) //TODO: this is accessing settings from storage directly as settings @@ -210,9 +216,16 @@ function getBackupFilename(filename) { } function loadProject(name) { + let fullPath = fspath.resolve(fspath.join(projectsDir,name)); var projectPath = name; if (projectPath.indexOf(fspath.sep) === -1) { - projectPath = fspath.join(projectsDir,name); + projectPath = fullPath; + } else { + // Ensure this project dir is under projectsDir; + let relativePath = fspath.relative(projectsDir,fullPath); + if (/^\.\./.test(relativePath)) { + throw new Error("Invalid project name") + } } return Projects.load(projectPath).then(function(project) { activeProject = project; @@ -236,6 +249,10 @@ function deleteProject(user, name) { throw e; } var projectPath = fspath.join(projectsDir,name); + let relativePath = fspath.relative(projectsDir,projectPath); + if (/^\.\./.test(relativePath)) { + throw new Error("Invalid project name") + } return Projects.delete(user, projectPath); } @@ -394,6 +411,10 @@ function createProject(user, metadata) { metadata.files.credentialSecret = currentEncryptionKey; } metadata.path = fspath.join(projectsDir,metadata.name); + if (/^\.\./.test(fspath.relative(projectsDir,metadata.path))) { + throw new Error("Invalid project name") + } + return Projects.create(user, metadata).then(function(p) { return setActiveProject(user, p.name); }).then(function() { @@ -501,6 +522,11 @@ async function getFlows() { if (!initialFlowLoadComplete) { initialFlowLoadComplete = true; log.info(log._("storage.localfilesystem.user-dir",{path:settings.userDir})); + + if (projectsEnabled) { + log.info(log._("storage.localfilesystem.projects.projects-directory", {projectsDirectory: projectsDir})); + } + if (activeProject) { // At this point activeProject will be a string, so go load it and // swap in an instance of Project diff --git a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json index 58eb7308d..02e15aa88 100644 --- a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json @@ -156,6 +156,7 @@ "projects": { "changing-project": "Setting active project : __project__", "active-project": "Active project : __project__", + "projects-directory": "Projects directory: __projectsDirectory__", "project-not-found": "Project not found : __project__", "no-active-project": "No active project : using default flows file", "disabled": "Projects disabled : editorTheme.projects.enabled=false", diff --git a/packages/node_modules/@node-red/util/lib/i18n.js b/packages/node_modules/@node-red/util/lib/i18n.js index e9dbedace..96a66ba1d 100644 --- a/packages/node_modules/@node-red/util/lib/i18n.js +++ b/packages/node_modules/@node-red/util/lib/i18n.js @@ -23,7 +23,7 @@ var i18n = require("i18next"); var path = require("path"); -var fs = require("fs"); +var fs = require("fs-extra"); var defaultLang = "en-US"; @@ -81,36 +81,28 @@ function mergeCatalog(fallback,catalog) { } -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); - fs.readFile(file, "utf8", function (err, content) { - if (err) { - reject(err); - } else { - try { - resourceCache[ns] = resourceCache[ns] || {}; - resourceCache[ns][lng] = JSON.parse(content.replace(/^\uFEFF/, '')); - 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]); - } - resolve(resourceCache[ns][lng]); - } catch (e) { - reject(e); - } - } - }); - } else { - reject(new Error("Unrecognised namespace")); +async function readFile(lng, ns) { + if (/[^a-z\-]/i.test(lng)) { + throw new Error("Invalid language: "+lng) + } + if (resourceCache[ns] && resourceCache[ns][lng]) { + return resourceCache[ns][lng]; + } else if (resourceMap[ns]) { + const file = path.join(resourceMap[ns].basedir, lng, resourceMap[ns].file); + const content = await fs.readFile(file, "utf8"); + resourceCache[ns] = resourceCache[ns] || {}; + resourceCache[ns][lng] = JSON.parse(content.replace(/^\uFEFF/, '')); + 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]); + } + return resourceCache[ns][lng]; + } else { + throw new Error("Unrecognised namespace"); + } } var MessageFileLoader = { @@ -181,6 +173,10 @@ function init(settings) { function getCatalog(namespace,lang) { var result = null; lang = lang || defaultLang; + if (/[^a-z\-]/i.test(lang)) { + throw new Error("Invalid language: "+lng) + } + if (resourceCache.hasOwnProperty(namespace)) { result = resourceCache[namespace][lang]; if (!result) { diff --git a/packages/node_modules/@node-red/util/lib/util.js b/packages/node_modules/@node-red/util/lib/util.js index 07f506007..54da6b1f9 100644 --- a/packages/node_modules/@node-red/util/lib/util.js +++ b/packages/node_modules/@node-red/util/lib/util.js @@ -189,11 +189,17 @@ function createError(code, message) { * * For example, `a["b"].c` returns `['a','b','c']` * + * If `msg` is provided, any internal cross-references will be evaluated against that + * object. Otherwise, it will return a nested set of properties + * + * For example, without msg set, 'a[msg.foo]' returns `['a', [ 'msg', 'foo'] ]` + * But if msg is set to '{"foo": "bar"}', 'a[msg.foo]' returns `['a', 'bar' ]` + * * @param {String} str - the property expression * @return {Array} the normalised expression * @memberof @node-red/util_util */ -function normalisePropertyExpression(str) { +function normalisePropertyExpression(str, msg, toString) { // This must be kept in sync with validatePropertyExpression // in editor/js/ui/utils.js @@ -205,6 +211,7 @@ function normalisePropertyExpression(str) { var start = 0; var inString = false; var inBox = false; + var boxExpression = false; var quoteChar; var v; for (var i=0;i 0) { + throw createError("INVALID_EXPR","Invalid property expression: unmatched '[' at position "+i); + } + continue; + } else if (!/["'\d]/.test(str[i+1])) { + // Next char is either a quote or a number throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1)); } start = i+1; @@ -294,6 +347,23 @@ function normalisePropertyExpression(str) { if (start < length) { parts.push(str.substring(start)); } + + if (toString) { + var result = parts.shift(); + while(parts.length > 0) { + var p = parts.shift(); + if (typeof p === 'string') { + if (/"/.test(p)) { + p = "'"+p+"'"; + } else { + p = '"'+p+'"'; + } + } + result = result+"["+p+"]"; + } + return result; + } + return parts; } @@ -340,8 +410,7 @@ function getMessageProperty(msg,expr) { */ function getObjectProperty(msg,expr) { var result = null; - var msgPropParts = normalisePropertyExpression(expr); - var m; + var msgPropParts = normalisePropertyExpression(expr,msg); msgPropParts.reduce(function(obj, key) { result = (typeof obj[key] !== "undefined" ? obj[key] : undefined); return result; @@ -381,7 +450,7 @@ function setObjectProperty(msg,prop,value,createMissing) { if (typeof createMissing === 'undefined') { createMissing = (typeof value !== 'undefined'); } - var msgPropParts = normalisePropertyExpression(prop); + var msgPropParts = normalisePropertyExpression(prop, msg); var depth = 0; var length = msgPropParts.length; var obj = msg; @@ -553,6 +622,10 @@ function evaluateNodeProperty(value, type, node, msg, callback) { } } else if ((type === 'flow' || type === 'global') && node) { var contextKey = parseContextStore(value); + if (/\[msg/.test(contextKey.key)) { + // The key has a nest msg. reference to evaluate first + contextKey.key = normalisePropertyExpression(contextKey.key, msg, true) + } result = node.context()[type].get(contextKey.key,contextKey.store,callback); if (callback) { return; diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 7d5643ecd..cac6a2ce7 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -41,6 +41,10 @@ module.exports = { // Timeout in milliseconds for HTTP request connections // defaults to 120 seconds //httpRequestTimeout: 120000, + + // Maximum buffer size for the exec node + // defaults to 10Mb + //execMaxBufferSize: 10000000, // The maximum length, in characters, of any message sent to the debug sidebar tab debugMaxLength: 1000, diff --git a/test/nodes/core/function/10-function_spec.js b/test/nodes/core/function/10-function_spec.js index 63e1031b6..d737875cc 100644 --- a/test/nodes/core/function/10-function_spec.js +++ b/test/nodes/core/function/10-function_spec.js @@ -53,6 +53,49 @@ describe('function node', function() { }); }); + it('should send returned message using send()', function(done) { + var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"node.send(msg);"}, + {id:"n2", type:"helper"}]; + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'foo'); + done(); + }); + n1.receive({payload:"foo",topic: "bar"}); + }); + }); + + it('should do something with the catch node', function(done) { + var flow = [{"id":"funcNode","type":"function","wires":[["goodNode"]],"func":"node.error('This is an error', msg);"},{"id":"goodNode","type":"helper"},{"id":"badNode","type":"helper"},{"id":"catchNode","type":"catch","scope":null,"uncaught":false,"wires":[["badNode"]]}]; + var catchNodeModule = require("nr-test-utils").require("@node-red/nodes/core/common/25-catch.js") + helper.load([catchNodeModule, functionNode], flow, function() { + var funcNode = helper.getNode("funcNode"); + var catchNode = helper.getNode("catchNode"); + var goodNode = helper.getNode("goodNode"); + var badNode = helper.getNode("badNode"); + + badNode.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'foo'); + msg.should.have.property('error'); + msg.error.should.have.property('message',"This is an error"); + msg.error.should.have.property('source'); + msg.error.source.should.have.property('id', "funcNode"); + done(); + }); + funcNode.receive({payload:"foo",topic: "bar"}); + }); + }); + + + + + +/* + it('should be loaded', function(done) { var flow = [{id:"n1", type:"function", name: "function" }]; helper.load(functionNode, flow, function() { @@ -1560,4 +1603,5 @@ describe('function node', function() { }); }) + */ }); diff --git a/test/nodes/core/function/10-switch_spec.js b/test/nodes/core/function/10-switch_spec.js index b431fcaaf..dcb7dfd45 100644 --- a/test/nodes/core/function/10-switch_spec.js +++ b/test/nodes/core/function/10-switch_spec.js @@ -119,13 +119,17 @@ describe('switch Node', function() { * @param done - callback when done */ function customFlowSwitchTest(flow, shouldReceive, sendPayload, done) { + customFlowMessageSwitchTest(flow,shouldReceive,{payload: sendPayload}, done); + } + + function customFlowMessageSwitchTest(flow, shouldReceive, message, done) { helper.load(switchNode, flow, function() { var switchNode1 = helper.getNode("switchNode1"); var helperNode1 = helper.getNode("helperNode1"); helperNode1.on("input", function(msg) { try { if (shouldReceive === true) { - should.equal(msg.payload,sendPayload); + should.equal(msg,message); done(); } else { should.fail(null, null, "We should never get an input!"); @@ -134,7 +138,7 @@ describe('switch Node', function() { done(err); } }); - switchNode1.receive({payload:sendPayload}); + switchNode1.receive(message); if (shouldReceive === false) { setTimeout(function() { done(); @@ -425,6 +429,29 @@ describe('switch Node', function() { }); }); + it('should use a nested message property to compare value - matches', function(done) { + var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload[msg.topic]",rules:[{"t":"eq","v":"bar"}],checkall:true,outputs:1,wires:[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + customFlowMessageSwitchTest(flow, true, {topic:"foo",payload:{"foo":"bar"}}, done); + }) + it('should use a nested message property to compare value - no match', function(done) { + var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload[msg.topic]",rules:[{"t":"eq","v":"bar"}],checkall:true,outputs:1,wires:[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + customFlowMessageSwitchTest(flow, false, {topic:"foo",payload:{"foo":"none"}}, done); + + }) + + it('should use a nested message property to compare nested message property - matches', function(done) { + var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload[msg.topic]",rules:[{"t":"eq","v":"payload[msg.topic2]",vt:"msg"}],checkall:true,outputs:1,wires:[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + customFlowMessageSwitchTest(flow, true, {topic:"foo",topic2:"foo2",payload:{"foo":"bar","foo2":"bar"}}, done); + }) + it('should use a nested message property to compare nested message property - no match', function(done) { + var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload[msg.topic]",rules:[{"t":"eq","v":"payload[msg.topic2]",vt:"msg"}],checkall:true,outputs:1,wires:[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + customFlowMessageSwitchTest(flow, false, {topic:"foo",topic2:"foo2",payload:{"foo":"bar","foo2":"none"}}, done); + }) + it('should match regex with ignore-case flag set true', function(done) { var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"regex","v":"onetwothree","case":true}],checkall:true,outputs:1,wires:[["helperNode1"]]}, {id:"helperNode1", type:"helper", wires:[]}]; diff --git a/test/nodes/core/function/15-change_spec.js b/test/nodes/core/function/15-change_spec.js index 1809d91d9..fc0c14600 100644 --- a/test/nodes/core/function/15-change_spec.js +++ b/test/nodes/core/function/15-change_spec.js @@ -98,7 +98,7 @@ describe('change Node', function() { }); describe('#set' , function() { - + it('sets the value of the message property', function(done) { var flow = [{"id":"changeNode1","type":"change","action":"replace","property":"payload","from":"","to":"changed","reg":false,"name":"changeNode","wires":[["helperNode1"]]}, {id:"helperNode1", type:"helper", wires:[]}]; @@ -672,6 +672,111 @@ describe('change Node', function() { }); }); + it('sets the value of a message property using a nested property', function(done) { + var flow = [{"id":"changeNode1","type":"change","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"lookup[msg.topic]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.equal(2); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.receive({payload:"",lookup:{a:1,b:2},topic:"b"}); + }); + }); + + it('sets the value of a nested message property using a message property', function(done) { + var flow = [{"id":"changeNode1","type":"change","name":"","rules":[{"t":"set","p":"lookup[msg.topic]","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.lookup.b.should.equal("newValue"); + done(); + } catch(err) { + done(err); + } + }); + var msg = { + payload: "newValue", + lookup:{a:1,b:2}, + topic:"b" + } + changeNode1.receive(msg); + }); + }); + + it('sets the value of a message property using a nested property in flow context', function(done) { + var flow = [{"id":"changeNode1","type":"change","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"lookup[msg.topic]","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"wires":[["helperNode1"]],"z":"flow"}, + {id:"helperNode1", type:"helper", wires:[]}]; + + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.eql(2); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.context().flow.set("lookup",{a:1, b:2}); + changeNode1.receive({payload: "", topic: "b"}); + }); + }) + + it('sets the value of a message property using a nested property in flow context', function(done) { + var flow = [{"id":"changeNode1","type":"change","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"lookup[msg.topic]","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"wires":[["helperNode1"]],"z":"flow"}, + {id:"helperNode1", type:"helper", wires:[]}]; + + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.eql(2); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.context().flow.set("lookup",{a:1, b:2}); + changeNode1.receive({payload: "", topic: "b"}); + }); + }) + + it('sets the value of a nested flow context property using a message property', function(done) { + var flow = [{"id":"changeNode1","type":"change","name":"","rules":[{"t":"set","p":"lookup[msg.topic]","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"wires":[["helperNode1"]],"z":"flow"}, + {id:"helperNode1", type:"helper", wires:[]}]; + + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.eql("newValue"); + changeNode1.context().flow.get("lookup.b").should.eql("newValue"); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.context().flow.set("lookup",{a:1, b:2}); + changeNode1.receive({payload: "newValue", topic: "b"}); + }); + }) + + }); describe('#change', function() { it('changes the value of the message property', function(done) { diff --git a/test/nodes/core/sequence/17-split_spec.js b/test/nodes/core/sequence/17-split_spec.js index 3ffa424b6..a216be90b 100644 --- a/test/nodes/core/sequence/17-split_spec.js +++ b/test/nodes/core/sequence/17-split_spec.js @@ -1759,13 +1759,13 @@ describe('JOIN node', function() { { msg: {seq:0, payload:"A", parts:{id:1, type:"string", ch:",", index:0, count:3}}, delay:0, avr:500, var:100}, { msg: {seq:1, payload:"B", parts:{id:1, type:"string", ch:",", index:1, count:3}}, delay:200, avr:500, var:100}, { msg: {seq:2, payload:"dummy", reset: true, parts:{id:1}}, delay:500, avr:500, var:100} - ]); + ]); }); it('should call done() when timed out', function (done) { mapiDoneJoinTestHelper(done, {mode:"custom", joiner:",", build:"string", timeout:0.5}, [ { msg: {seq:0, payload:"A"}, delay:0, avr:500, var:100}, { msg: {seq:1, payload:"B"}, delay:200, avr:500, var:100}, - ]); + ]); }); it('should call done() when all messages are reduced', function (done) { mapiDoneJoinTestHelper(done, {mode:"reduce", reduceRight:false, reduceExp:"$A+payload", reduceInit:"0", @@ -1785,4 +1785,40 @@ describe('JOIN node', function() { ]); }); }); + + it('should handle msg.parts even if messages are out of order in auto mode if exactly one message has count set', function (done) { + var flow = [{ id: "n1", type: "join", wires: [["n2"]], 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) { + msg.payload.length.should.be.eql(5); + msg.payload.should.be.eql([0,1,2,3,4]); + done(); + }); + + var msg = {}; + msg.parts = { + id: RED.util.generateId() + }; + for(var elem = 1; elem < 5; ++elem) { + var _msg = RED.util.cloneMessage(msg); + _msg.parts.index = elem; + if(elem == 4) { + _msg.parts.count = 5; + } + _msg.payload = elem; + n1.receive(_msg); + } + + var _msg = RED.util.cloneMessage(msg); + delete _msg.parts.count; + _msg.parts.index = 0; + _msg.payload = 0; + n1.receive(_msg); + }); + + }) }); diff --git a/test/resources/plugin/test-plugin/locales/en-US/test-editor-plugin.json b/test/resources/plugin/test-plugin/locales/en-US/test-editor-plugin.json new file mode 100644 index 000000000..e990e2ec1 --- /dev/null +++ b/test/resources/plugin/test-plugin/locales/en-US/test-editor-plugin.json @@ -0,0 +1,3 @@ +{ + "plugin": "winning" +} \ No newline at end of file diff --git a/test/resources/plugin/test-plugin/package.json b/test/resources/plugin/test-plugin/package.json new file mode 100644 index 000000000..1077042de --- /dev/null +++ b/test/resources/plugin/test-plugin/package.json @@ -0,0 +1,12 @@ +{ + "name": "test-plugin", + "version": "1.0.0", + "description": "", + "node-red": { + "plugins": { + "test": "test.js", + "test-editor-plugin": "test-editor-plugin.html", + "test-runtime-plugin": "test-runtime-plugin.js" + } + } +} diff --git a/test/resources/plugin/test-plugin/test-editor-plugin.html b/test/resources/plugin/test-plugin/test-editor-plugin.html new file mode 100644 index 000000000..177813526 --- /dev/null +++ b/test/resources/plugin/test-plugin/test-editor-plugin.html @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/test/resources/plugin/test-plugin/test-runtime-plugin.js b/test/resources/plugin/test-plugin/test-runtime-plugin.js new file mode 100644 index 000000000..d9e3ff3c7 --- /dev/null +++ b/test/resources/plugin/test-plugin/test-runtime-plugin.js @@ -0,0 +1,10 @@ +module.exports = function(RED) { + console.log("Loaded test-plugin/test-runtime-plugin") + + RED.plugins.registerPlugin("my-test-runtime-only-plugin", { + type: "bar", + onadd: function() { + console.log("my-test-runtime-only-plugin.onadd called") + } + }) +} \ No newline at end of file diff --git a/test/resources/plugin/test-plugin/test.html b/test/resources/plugin/test-plugin/test.html new file mode 100644 index 000000000..72d2979df --- /dev/null +++ b/test/resources/plugin/test-plugin/test.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/test/resources/plugin/test-plugin/test.js b/test/resources/plugin/test-plugin/test.js new file mode 100644 index 000000000..3a14fe9af --- /dev/null +++ b/test/resources/plugin/test-plugin/test.js @@ -0,0 +1,13 @@ +module.exports = function(RED) { + console.log("Loaded test-plugin/test") + + RED.plugins.registerPlugin("my-test-plugin", { + type: "foo", + onadd: function() { + console.log("my-test-plugin.onadd called") + RED.events.on("registry:plugin-added", function(id) { + console.log(`my-test-plugin: plugin-added event "${id}"`) + }); + } + }) +} \ No newline at end of file diff --git a/test/unit/@node-red/editor-api/lib/admin/plugins_spec.js b/test/unit/@node-red/editor-api/lib/admin/plugins_spec.js new file mode 100644 index 000000000..74584a1d8 --- /dev/null +++ b/test/unit/@node-red/editor-api/lib/admin/plugins_spec.js @@ -0,0 +1,111 @@ +const should = require("should"); +const request = require('supertest'); +const express = require('express'); +const bodyParser = require("body-parser"); + +var app; + +var NR_TEST_UTILS = require("nr-test-utils"); + +var plugins = NR_TEST_UTILS.require("@node-red/editor-api/lib/admin/plugins"); + +describe("api/editor/plugins", function() { + const pluginList = [ + { + "id": "test-module/test-set", + "enabled": true, + "local": false, + "plugins": [ + { + "type": "foo", + "id": "a-plugin", + "module": "test-module" + }, + { + "type": "bar", + "id": "a-plugin2", + "module": "test-module" + }, + { + "type": "foo", + "id": "a-plugin3", + "module": "test-module" + } + ] + }, + { + "id": "test-module/test-disabled-set", + "enabled": false, + "local": false, + "plugins": [] + } + ]; + const pluginConfigs = ` + +test-module-config`; + + const pluginCatalogs = { "test-module": {"foo": "bar"}}; + + before(function() { + app = express(); + app.use(bodyParser.json()); + app.get("/plugins",plugins.getAll); + app.get("/plugins/messages",plugins.getCatalogs); + + plugins.init({ + plugins: { + getPluginList: async function() { return pluginList }, + getPluginConfigs: async function() { return pluginConfigs }, + getPluginCatalogs: async function() { return pluginCatalogs } + } + }) + }); + + it('returns the list of plugins', function(done) { + request(app) + .get("/plugins") + .set('Accept', 'application/json') + .expect(200) + .end(function(err,res) { + if (err) { + return done(err); + } + try { + JSON.stringify(res.body).should.eql(JSON.stringify(pluginList)); + done(); + } catch(err) { + done(err) + } + }); + }); + it('returns the plugin configs', function(done) { + request(app) + .get("/plugins") + .set('Accept', 'text/html') + .expect(200) + .expect(pluginConfigs) + .end(function(err,res) { + if (err) { + return done(err); + } + done(); + }); + }); + it('returns the plugin catalogs', function(done) { + request(app) + .get("/plugins/messages") + .set('Accept', 'application/json') + .expect(200) + .end(function(err,res) { + if (err) { + return done(err); + } + try { + JSON.stringify(res.body).should.eql(JSON.stringify(pluginCatalogs)); + done(); + } catch(err) { + done(err) + } + }); + }); +}); diff --git a/test/unit/@node-red/editor-api/lib/editor/theme_spec.js b/test/unit/@node-red/editor-api/lib/editor/theme_spec.js index 5c8123d61..2fa43e01a 100644 --- a/test/unit/@node-red/editor-api/lib/editor/theme_spec.js +++ b/test/unit/@node-red/editor-api/lib/editor/theme_spec.js @@ -33,11 +33,11 @@ describe("api/editor/theme", function () { theme.init({settings: {}}); fs.statSync.restore(); }); - it("applies the default theme", function () { + it("applies the default theme", async function () { var result = theme.init({}); should.not.exist(result); - var context = theme.context(); + var context = await theme.context(); context.should.have.a.property("page"); context.page.should.have.a.property("title", "Node-RED"); context.page.should.have.a.property("favicon", "favicon.ico"); @@ -52,7 +52,7 @@ describe("api/editor/theme", function () { should.not.exist(theme.settings()); }); - it("picks up custom theme", function () { + it("picks up custom theme", async function () { theme.init({ editorTheme: { page: { @@ -104,7 +104,7 @@ describe("api/editor/theme", function () { theme.app(); - var context = theme.context(); + var context = await theme.context(); context.should.have.a.property("page"); context.page.should.have.a.property("title", "Test Page Title"); context.page.should.have.a.property("favicon", "theme/favicon/favicon"); diff --git a/test/unit/@node-red/registry/lib/plugins_spec.js b/test/unit/@node-red/registry/lib/plugins_spec.js new file mode 100644 index 000000000..8f32d3802 --- /dev/null +++ b/test/unit/@node-red/registry/lib/plugins_spec.js @@ -0,0 +1,155 @@ + +const should = require("should"); +const sinon = require("sinon"); +const path = require("path"); + +const NR_TEST_UTILS = require("nr-test-utils"); + +const plugins = NR_TEST_UTILS.require("@node-red/registry/lib/plugins"); +const registry = NR_TEST_UTILS.require("@node-red/registry/lib/registry"); +const { events } = NR_TEST_UTILS.require("@node-red/util"); + +describe("red/nodes/registry/plugins",function() { + let receivedEvents = []; + let modules; + function handleEvent(evnt) { + receivedEvents.push(evnt); + } + beforeEach(function() { + plugins.init({}); + receivedEvents = []; + modules = { + "test-module": { + plugins: { + "test-set": { + id: "test-module/test-set", + enabled: true, + config: "test-module-config", + plugins: [] + }, + "test-disabled-set": { + id: "test-module/test-disabled-set", + enabled: false, + config: "disabled-plugin-config", + plugins: [] + } + } + } + } + events.on("registry:plugin-added",handleEvent); + sinon.stub(registry,"getModule", moduleId => modules[moduleId]); + sinon.stub(registry,"getModuleList", () => modules) + }); + afterEach(function() { + events.removeListener("registry:plugin-added",handleEvent); + registry.getModule.restore(); + registry.getModuleList.restore(); + }) + + describe("registerPlugin", function() { + it("registers a plugin", function() { + let pluginDef = {} + plugins.registerPlugin("test-module/test-set","a-plugin",pluginDef); + receivedEvents.length.should.eql(1); + receivedEvents[0].should.eql("a-plugin"); + should.exist(modules['test-module'].plugins['test-set'].plugins[0]) + modules['test-module'].plugins['test-set'].plugins[0].should.equal(pluginDef) + }) + it("calls a plugins onadd function", function() { + let pluginDef = { onadd: sinon.stub() } + plugins.registerPlugin("test-module/test-set","a-plugin",pluginDef); + pluginDef.onadd.called.should.be.true(); + }) + }) + + describe("getPlugin", function() { + it("returns a registered plugin", function() { + let pluginDef = {} + plugins.registerPlugin("test-module/test-set","a-plugin",pluginDef); + pluginDef.should.equal(plugins.getPlugin("a-plugin")); + }) + }) + describe("getPluginsByType", function() { + it("returns a plugins of a given type", function() { + let pluginDef = {type: "foo"} + let pluginDef2 = {type: "bar"} + let pluginDef3 = {type: "foo"} + plugins.registerPlugin("test-module/test-set","a-plugin",pluginDef); + plugins.registerPlugin("test-module/test-set","a-plugin2",pluginDef2); + plugins.registerPlugin("test-module/test-set","a-plugin3",pluginDef3); + + let fooPlugins = plugins.getPluginsByType("foo"); + let barPlugins = plugins.getPluginsByType("bar"); + let noPlugins = plugins.getPluginsByType("none"); + + noPlugins.should.be.of.length(0); + + fooPlugins.should.be.of.length(2); + fooPlugins.should.containEql(pluginDef); + fooPlugins.should.containEql(pluginDef3); + + barPlugins.should.be.of.length(1); + barPlugins.should.containEql(pluginDef2); + + }) + }) + + describe("getPluginConfigs", function() { + it("gets all plugin configs", function() { + let configs = plugins.getPluginConfigs("en-US"); + configs.should.eql(` + +test-module-config`) + }) + }) + + + describe("getPluginList", function() { + it("returns a plugins of a given type", function() { + let pluginDef = {type: "foo"} + let pluginDef2 = {type: "bar"} + let pluginDef3 = {type: "foo"} + plugins.registerPlugin("test-module/test-set","a-plugin",pluginDef); + plugins.registerPlugin("test-module/test-set","a-plugin2",pluginDef2); + plugins.registerPlugin("test-module/test-set","a-plugin3",pluginDef3); + + let pluginList = plugins.getPluginList(); + JSON.stringify(pluginList).should.eql(JSON.stringify( + [ + { + "id": "test-module/test-set", + "enabled": true, + "local": false, + "user": false, + "plugins": [ + { + "type": "foo", + "id": "a-plugin", + "module": "test-module" + }, + { + "type": "bar", + "id": "a-plugin2", + "module": "test-module" + }, + { + "type": "foo", + "id": "a-plugin3", + "module": "test-module" + } + ] + }, + { + "id": "test-module/test-disabled-set", + "enabled": false, + "local": false, + "user": false, + "plugins": [] + } + ] + )) + }) + }) + + +}); \ No newline at end of file diff --git a/test/unit/@node-red/runtime/lib/api/plugins_spec.js b/test/unit/@node-red/runtime/lib/api/plugins_spec.js new file mode 100644 index 000000000..7ae6d8286 --- /dev/null +++ b/test/unit/@node-red/runtime/lib/api/plugins_spec.js @@ -0,0 +1,68 @@ +const should = require("should"); +const sinon = require("sinon"); + +const NR_TEST_UTILS = require("nr-test-utils"); +const plugins = NR_TEST_UTILS.require("@node-red/runtime/lib/api/plugins") + +const mockLog = () => ({ + log: sinon.stub(), + debug: sinon.stub(), + trace: sinon.stub(), + warn: sinon.stub(), + info: sinon.stub(), + metric: sinon.stub(), + audit: sinon.stub(), + _: function() { return "abc"} +}) + +describe("runtime-api/plugins", function() { + const pluginList = [{id:"one",module:'test-module'},{id:"two",module:"node-red"}]; + const pluginConfigs = "123"; + + describe("getPluginList", function() { + it("gets the plugin list", function() { + plugins.init({ + log: mockLog(), + plugins: { + getPluginList: function() { return pluginList} + } + }); + return plugins.getPluginList({}).then(function(result) { + result.should.eql(pluginList); + }) + }); + }); + describe("getPluginConfigs", function() { + it("gets the plugin configs", function() { + plugins.init({ + log: mockLog(), + plugins: { + getPluginConfigs: function() { return pluginConfigs} + } + }); + return plugins.getPluginConfigs({}).then(function(result) { + result.should.eql(pluginConfigs); + }) + }); + }); + describe("getPluginCatalogs", function() { + it("gets the plugin catalogs", function() { + plugins.init({ + log: mockLog(), + plugins: { + getPluginList: function() { return pluginList} + }, + i18n: { + i: { + changeLanguage: function(lang,done) { done && done() }, + getResourceBundle: function(lang, id) { return {lang,id}} + } + } + }); + return plugins.getPluginCatalogs({lang: "en-US"}).then(function(result) { + JSON.stringify(result).should.eql(JSON.stringify({ one: { lang: "en-US", id: "one" } })) + }) + }); + }); + +}); \ No newline at end of file diff --git a/test/unit/@node-red/runtime/lib/plugins_spec.js b/test/unit/@node-red/runtime/lib/plugins_spec.js new file mode 100644 index 000000000..a78de643c --- /dev/null +++ b/test/unit/@node-red/runtime/lib/plugins_spec.js @@ -0,0 +1,13 @@ +const should = require("should"); +const sinon = require("sinon"); +const NR_TEST_UTILS = require("nr-test-utils"); + +const plugins = NR_TEST_UTILS.require("@node-red/runtime/lib/plugins"); + +describe("runtime/plugins",function() { + + it.skip("delegates all functions to registry module", function() { + // There's no easy way to test this as we can't stub the registry functions + // before the plugin module gets a reference to them + }) +}); diff --git a/test/unit/@node-red/util/lib/util_spec.js b/test/unit/@node-red/util/lib/util_spec.js index c4a64d53f..6db93f311 100644 --- a/test/unit/@node-red/util/lib/util_spec.js +++ b/test/unit/@node-red/util/lib/util_spec.js @@ -164,6 +164,13 @@ describe("@node-red/util/util", function() { var v2 = util.getMessageProperty({a:"foo"},"a"); v2.should.eql("foo"); }); + it('retrieves a nested property', function() { + var v = util.getMessageProperty({a:"foo",b:{foo:1,bar:2}},"msg.b[msg.a]"); + v.should.eql(1); + var v2 = util.getMessageProperty({a:"bar",b:{foo:1,bar:2}},"b[msg.a]"); + v2.should.eql(2); + }); + it('should return undefined if property does not exist', function() { var v = util.getMessageProperty({a:"foo"},"msg.b"); should.not.exist(v); @@ -331,7 +338,18 @@ describe("@node-red/util/util", function() { msg.a[0].should.eql(1); msg.a[1].should.eql(3); }) - + it('handles nested message property references', function() { + var obj = {a:"foo",b:{}}; + var result = util.setObjectProperty(obj,"b[msg.a]","bar"); + result.should.be.true(); + obj.b.should.have.property("foo","bar"); + }); + it('handles nested message property references', function() { + var obj = {a:"foo",b:{"foo":[0,0,0]}}; + var result = util.setObjectProperty(obj,"b[msg.a][2]","bar"); + result.should.be.true(); + obj.b.foo.should.eql([0,0,"bar"]) + }); }); describe('evaluateNodeProperty', function() { @@ -459,13 +477,24 @@ describe("@node-red/util/util", function() { // console.log(result); result.should.eql(expected); } - - function testInvalid(input) { + function testABCWithMessage(input,msg,expected) { + var result = util.normalisePropertyExpression(input,msg); + // console.log("+",input); + // console.log(result); + result.should.eql(expected); + } + function testInvalid(input,msg) { /*jshint immed: false */ (function() { - util.normalisePropertyExpression(input); + util.normalisePropertyExpression(input,msg); }).should.throw(); } + function testToString(input,msg,expected) { + var result = util.normalisePropertyExpression(input,msg,true); + console.log("+",input); + console.log(result); + result.should.eql(expected); + } it('pass a.b.c',function() { testABC('a.b.c',['a','b','c']); }) it('pass a["b"]["c"]',function() { testABC('a["b"]["c"]',['a','b','c']); }) it('pass a["b"].c',function() { testABC('a["b"].c',['a','b','c']); }) @@ -479,12 +508,25 @@ describe("@node-red/util/util", function() { it("pass 'a.b'[1]",function() { testABC("'a.b'[1]",['a.b',1]); }) it("pass 'a.b'.c",function() { testABC("'a.b'.c",['a.b','c']); }) + it("pass a[msg.b]",function() { testABC("a[msg.b]",["a",["msg","b"]]); }) + it("pass a[msg[msg.b]]",function() { testABC("a[msg[msg.b]]",["a",["msg",["msg","b"]]]); }) + it("pass a[msg.b]",function() { testABC("a[msg.b]",["a",["msg","b"]]); }) + it("pass a[msg.b]",function() { testABC("a[msg.b]",["a",["msg","b"]]); }) + it("pass a[msg['b]\"[']]",function() { testABC("a[msg['b]\"[']]",["a",["msg","b]\"["]]); }) + it("pass a[msg['b][']]",function() { testABC("a[msg['b][']]",["a",["msg","b]["]]); }) + it("pass b[msg.a][2]",function() { testABC("b[msg.a][2]",["b",["msg","a"],2])}) + + it("pass b[msg.a][2] (with message)",function() { testABCWithMessage("b[msg.a][2]",{a: "foo"},["b","foo",2])}) it('pass a.$b.c',function() { testABC('a.$b.c',['a','$b','c']); }) it('pass a["$b"].c',function() { testABC('a["$b"].c',['a','$b','c']); }) it('pass a._b.c',function() { testABC('a._b.c',['a','_b','c']); }) it('pass a["_b"].c',function() { testABC('a["_b"].c',['a','_b','c']); }) + it("pass a['a.b[0]'].c",function() { testToString("a['a.b[0]'].c",null,'a["a.b[0]"]["c"]'); }) + it("pass a.b.c",function() { testToString("a.b.c",null,'a["b"]["c"]'); }) + it('pass a[msg.c][0]["fred"]',function() { testToString('a[msg.c][0]["fred"]',{c:"123"},'a["123"][0]["fred"]'); }) + it("fail a'b'.c",function() { testInvalid("a'b'.c"); }) it("fail a['b'.c",function() { testInvalid("a['b'.c"); }) it("fail a[]",function() { testInvalid("a[]"); }) @@ -505,6 +547,12 @@ describe("@node-red/util/util", function() { it("fail a['']",function() { testInvalid("a['']"); }) it("fail 'a.b'c",function() { testInvalid("'a.b'c"); }) it("fail ",function() { testInvalid("");}) + it("fail a[b]",function() { testInvalid("a[b]"); }) + it("fail a[msg.]",function() { testInvalid("a[msg.]"); }) + it("fail a[msg[]",function() { testInvalid("a[msg[]"); }) + it("fail a[msg.[]]",function() { testInvalid("a[msg.[]]"); }) + it("fail a[msg['af]]",function() { testInvalid("a[msg['af]]"); }) + it("fail b[msg.undefined][2] (with message)",function() { testInvalid("b[msg.undefined][2]",{})}) }); @@ -983,4 +1031,5 @@ describe("@node-red/util/util", function() { }); }); + });