From deb9c4ecc098d2cdbecdd777be5a5819ea54dad2 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 9 Jun 2022 15:47:16 -0500 Subject: [PATCH 01/17] Reset mouse state when switching tabs Fixes #3639 --- .../@node-red/editor-client/src/js/ui/view.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 adab87e38..7d4d950f3 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 @@ -385,6 +385,9 @@ RED.view = (function() { drag_lines = []; RED.events.on("workspace:change",function(event) { + // Just in case the mouse left the workspace whilst doing an action, + // put us back into default mode so the refresh works + mouse_mode = 0 if (event.old !== 0) { workspaceScrollPositions[event.old] = { left:chart.scrollLeft(), @@ -955,7 +958,7 @@ RED.view = (function() { } function canvasMouseDown() { - if (RED.view.DEBUG) { + if (RED.view.DEBUG) { console.warn("canvasMouseDown", { mouse_mode, point: d3.mouse(this), event: d3.event }); } if (mouse_mode === RED.state.SELECTING_NODE) { @@ -1719,7 +1722,7 @@ RED.view = (function() { function canvasMouseUp() { lastClickPosition = [d3.event.offsetX/scaleFactor,d3.event.offsetY/scaleFactor]; - if (RED.view.DEBUG) { + if (RED.view.DEBUG) { console.warn("canvasMouseUp", { mouse_mode, point: d3.mouse(this), event: d3.event }); } var i; @@ -3719,7 +3722,7 @@ RED.view = (function() { function junctionMouseOutProxy(e) { junctionMouseOut(d3.select(this), this.__data__) } function linkMouseDown(d) { - if (RED.view.DEBUG) { + if (RED.view.DEBUG) { console.warn("linkMouseDown", { mouse_mode, point: d3.mouse(this), event: d3.event }); } if (mouse_mode === RED.state.SELECTING_NODE) { From a16032a8ede59fa838ea235a9bce62ac13d879fa Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 13 Jun 2022 21:01:34 +0100 Subject: [PATCH 02/17] Track mouse release outside workspace so current action completes --- .../@node-red/editor-client/src/js/ui/view.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 7d4d950f3..a3bc16e9d 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 @@ -230,6 +230,7 @@ RED.view = (function() { .on("mousedown", canvasMouseDown) .on("mouseup", canvasMouseUp) .on("mouseenter", function() { + d3.select(document).on('mouseup.red-ui-workspace-tracker', null) if (lasso) { if (d3.event.buttons !== 1) { lasso.remove(); @@ -245,6 +246,7 @@ RED.view = (function() { } } }) + .on("mouseleave", canvasMouseLeave) .on("touchend", function() { d3.event.preventDefault(); clearTimeout(touchStartTime); @@ -1719,7 +1721,14 @@ RED.view = (function() { redraw(); } } - + function canvasMouseLeave() { + if (mouse_mode !== 0 && d3.event.buttons !== 0) { + d3.select(document).on('mouseup.red-ui-workspace-tracker', function() { + d3.select(document).on('mouseup.red-ui-workspace-tracker', null) + canvasMouseUp.call(this) + }) + } + } function canvasMouseUp() { lastClickPosition = [d3.event.offsetX/scaleFactor,d3.event.offsetY/scaleFactor]; if (RED.view.DEBUG) { From 3ab93ecdd4845303a69146901aa3f84668da8a1d Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Thu, 16 Jun 2022 00:00:02 +0900 Subject: [PATCH 03/17] fix disable junction --- .../node_modules/@node-red/editor-client/src/js/ui/view.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 a15ec32f4..7b42c7e21 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 @@ -5736,7 +5736,12 @@ RED.view = (function() { node.dirty = true; node.dirtyStatus = true; node.changed = true; - RED.events.emit("nodes:change",node); + if (node.type === "junction") { + RED.events.emit("junctions:change",node); + } + else { + RED.events.emit("nodes:change",node); + } } } } From 8d99a42307c08cb7a3a3082e2e2b0bb75fd9d47d Mon Sep 17 00:00:00 2001 From: Kazuhito Yokoi Date: Thu, 16 Jun 2022 00:32:20 +0900 Subject: [PATCH 04/17] Add Japanese translations (Backport #3576 to v2.x) --- .../node_modules/@node-red/runtime/locales/ja/runtime.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/runtime/locales/ja/runtime.json b/packages/node_modules/@node-red/runtime/locales/ja/runtime.json index fdf8b2249..df5f9fa08 100644 --- a/packages/node_modules/@node-red/runtime/locales/ja/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/ja/runtime.json @@ -100,7 +100,9 @@ "error": "クレデンシャルの読み込みエラー: __message__", "error-saving": "クレデンシャルの保存エラー: __message__", "not-registered": "クレデンシャル '__type__' は登録されていません", - "system-key-warning": "\n\n---------------------------------------------------------------------\nフローのクレデンシャルファイルはシステム生成キーで暗号化されています。\n\nシステム生成キーを何らかの理由で失った場合、クレデンシャルファイルを\n復元することはできません。その場合、ファイルを削除してクレデンシャルを\n再入力しなければなりません。\n\n設定ファイル内で 'credentialSecret' オプションを使って独自キーを設定\nします。変更を次にデプロイする際、Node-REDは選択したキーを用いてクレ\nデンシャルを再暗号化します。 \n\n---------------------------------------------------------------------\n" + "system-key-warning": "\n\n---------------------------------------------------------------------\nフローのクレデンシャルファイルはシステム生成キーで暗号化されています。\n\nシステム生成キーを何らかの理由で失った場合、クレデンシャルファイルを\n復元することはできません。その場合、ファイルを削除してクレデンシャルを\n再入力しなければなりません。\n\n設定ファイル内で 'credentialSecret' オプションを使って独自キーを設定\nします。変更を次にデプロイする際、Node-REDは選択したキーを用いてクレ\nデンシャルを再暗号化します。 \n\n---------------------------------------------------------------------\n", + "unencrypted": "暗号化されていないクレデンシャルを使用", + "encryptedNotFound": "暗号化されたクレデンシャルが存在しません" }, "flows": { "safe-mode": "セーフモードでフローを停止しました。開始するためにはデプロイしてください", From d1312703c5d8d46269a8a4893cb8247f680259f7 Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Thu, 16 Jun 2022 14:16:40 +0900 Subject: [PATCH 05/17] fix initial cursor position of init/finalize --- .../@node-red/nodes/core/function/10-function.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.html b/packages/node_modules/@node-red/nodes/core/function/10-function.html index 6309ae9ad..156abd29d 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.html +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.html @@ -460,7 +460,7 @@ } }); - var buildEditor = function(id, stateId, focus, value, defaultValue, extraLibs) { + var buildEditor = function(id, stateId, focus, value, defaultValue, extraLibs, offset) { var editor = RED.editor.createEditor({ id: id, mode: 'ace/mode/nrjavascript', @@ -484,14 +484,14 @@ extraLibs: extraLibs }); if (defaultValue && value === "") { - editor.moveCursorTo(defaultValue.split("\n").length - 1, 0); + editor.moveCursorTo(defaultValue.split("\n").length +offset, 0); } editor.__stateId = stateId; return editor; } - this.initEditor = buildEditor('node-input-init-editor', this.id + "/" + "initEditor", false, $("#node-input-initialize").val(), RED._("node-red:function.text.initialize")) - this.editor = buildEditor('node-input-func-editor', this.id + "/" + "editor", true, $("#node-input-func").val(), undefined, that.libs || []) - this.finalizeEditor = buildEditor('node-input-finalize-editor', this.id + "/" + "finalizeEditor", false, $("#node-input-finalize").val(), RED._("node-red:function.text.finalize")) + this.initEditor = buildEditor('node-input-init-editor', this.id + "/" + "initEditor", false, $("#node-input-initialize").val(), RED._("node-red:function.text.initialize"), undefined, 0); + this.editor = buildEditor('node-input-func-editor', this.id + "/" + "editor", true, $("#node-input-func").val(), undefined, that.libs || [], undefined, -1); + this.finalizeEditor = buildEditor('node-input-finalize-editor', this.id + "/" + "finalizeEditor", false, $("#node-input-finalize").val(), RED._("node-red:function.text.finalize"), undefined, 0); RED.library.create({ url:"functions", // where to get the data from From ba22b07dcea0bf5bdbd93e25e58632733597925e Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Thu, 16 Jun 2022 10:57:29 +0100 Subject: [PATCH 06/17] align functionality of `nodesDir` with coreNodesDir and userDir --- .../@node-red/registry/lib/localfilesystem.js | 195 +++++++++++++----- 1 file changed, 146 insertions(+), 49 deletions(-) diff --git a/packages/node_modules/@node-red/registry/lib/localfilesystem.js b/packages/node_modules/@node-red/registry/lib/localfilesystem.js index 62080b4a8..da4006ecc 100644 --- a/packages/node_modules/@node-red/registry/lib/localfilesystem.js +++ b/packages/node_modules/@node-red/registry/lib/localfilesystem.js @@ -88,9 +88,10 @@ function getLocalFile(file) { /** * Synchronously walks the directory looking for node files. * @param dir the directory to search + * @param skipValidNodeRedModules a flag to skip lading icons & files if the directory a valid node-red module * @return an array of fully-qualified paths to .js files */ -function getLocalNodeFiles(dir) { +function getLocalNodeFiles(dir, skipValidNodeRedModules) { dir = path.resolve(dir); var result = []; @@ -102,6 +103,14 @@ function getLocalNodeFiles(dir) { return {files: [], icons: []}; } files.sort(); + // when loading local files, if the path is a valid node-red module + // dont include it (will be picked up in scanTreeForNodesModules) + if(skipValidNodeRedModules && files.indexOf("package.json") >= 0) { + const package = getPackageDetails(dir) + if(package.isNodeRedModule) { + return {files: [], icons: []}; + } + } files.forEach(function(fn) { var stats = fs.statSync(path.join(dir,fn)); if (stats.isFile()) { @@ -114,7 +123,7 @@ function getLocalNodeFiles(dir) { } else if (stats.isDirectory()) { // Ignore /.dirs/, /lib/ /node_modules/ if (!/^(\..*|lib|icons|node_modules|test|locales)$/.test(fn)) { - var subDirResults = getLocalNodeFiles(path.join(dir,fn)); + var subDirResults = getLocalNodeFiles(path.join(dir,fn), skipValidNodeRedModules); result = result.concat(subDirResults.files); icons = icons.concat(subDirResults.icons); } else if (fn === "icons") { @@ -126,21 +135,30 @@ function getLocalNodeFiles(dir) { return {files: result, icons: icons} } -function scanDirForNodesModules(dir,moduleName) { - var results = []; - var scopeName; +function scanDirForNodesModules(dir,moduleName,package) { + let results = []; + let scopeName; + let files try { - var files = fs.readdirSync(dir); - if (moduleName) { - var m = /^(?:(@[^/]+)[/])?([^@/]+)/.exec(moduleName); - if (m) { - scopeName = m[1]; - moduleName = m[2]; + let isNodeRedModule = false + if(package) { + dir = path.join(package.moduleDir,'..') + files = [path.basename(package.moduleDir)] + moduleName = (package.package ? package.package.name : null) || moduleName + isNodeRedModule = package.isNodeRedModule + } else { + files = fs.readdirSync(dir); + if (moduleName) { + var m = /^(?:(@[^/]+)[/])?([^@/]+)/.exec(moduleName); + if (m) { + scopeName = m[1]; + moduleName = m[2]; + } } } - for (var i=0;i look for node_modules + 2. exist(package.json) && package.json.has(node-red) => load this only + 3. in original scan of nodesDir, ignore if:(exist(package.json) && package.json.has(node-red)) + */ + if (nodesDir) { + for (let dirIndex = 0; dirIndex < nodesDir.length; dirIndex++) { + const nodeDir = nodesDir[dirIndex]; + const packageDetails = getPackageDetails(nodeDir) + if(packageDetails.isNodeRedModule) { + //we have found a node-red module, scan it + const nrModules = scanDirForNodesModules(nodeDir, packageDetails.package.name, packageDetails); + results = results.concat(nrModules); + + } else if (packageDetails.has_node_modules) { + //If this dir has a `node_modues` dir, scan it + const nodeModulesDir = path.join(nodeDir, 'node_modules') + const nrModules = scanDirForNodesModules(nodeModulesDir, moduleName ); + results = results.concat(nrModules); + + } else { + //If this is not a node-red module AND it does NOT have a node_modules dir, + //it may be a directory of project directories or a node_modules dir? + //scan this instead + const nrModules = scanDirForNodesModules(nodeDir, moduleName); + results = results.concat(nrModules); + } } } return results; @@ -274,24 +328,26 @@ function getModuleNodeFiles(module) { } function getNodeFiles(disableNodePathScan) { - var dir; // Find all of the nodes to load - var nodeFiles = []; - var results; - - var dir; - var iconList = []; + let results; + let nodesDir; + if(settings.nodesDir) { + nodesDir = Array.isArray(settings.nodesDir) ? settings.nodesDir : [settings.nodesDir] + } + let dir; + let nodeFiles = []; + let iconList = []; if (settings.coreNodesDir) { results = getLocalNodeFiles(path.resolve(settings.coreNodesDir)); nodeFiles = nodeFiles.concat(results.files); iconList = iconList.concat(results.icons); - var defaultLocalesPath = path.join(settings.coreNodesDir,"locales"); + let defaultLocalesPath = path.join(settings.coreNodesDir,"locales"); i18n.registerMessageCatalog("node-red",defaultLocalesPath,"messages.json"); } if (settings.userDir) { dir = path.join(settings.userDir,"lib","icons"); - var icons = scanIconDir(dir); + let icons = scanIconDir(dir); if (icons.length > 0) { iconList.push({path:dir,icons:icons}); } @@ -301,13 +357,9 @@ function getNodeFiles(disableNodePathScan) { nodeFiles = nodeFiles.concat(results.files); iconList = iconList.concat(results.icons); } - if (settings.nodesDir) { - dir = settings.nodesDir; - if (typeof settings.nodesDir == "string") { - dir = [dir]; - } - for (var i=0;i Date: Thu, 16 Jun 2022 11:00:31 +0100 Subject: [PATCH 07/17] =?UTF-8?q?improve=20tests=20for=20nodeDir=20Adds=20?= =?UTF-8?q?new=20resources=20(loose=20files,=20non=20NR=20pkgs,=20NR=20mod?= =?UTF-8?q?ules,=20NR=20Plugins)=20Adds=20new=20tests=20#getNodeFiles=20-?= =?UTF-8?q?=20new=20tests=20below=20=20=20=E2=88=9A=20Finds=20nodes=20and?= =?UTF-8?q?=20icons=20only=20in=20nodesDir=20with=20files,=20icons=20and?= =?UTF-8?q?=20valid=20node-red=20packages=20=20=20=E2=88=9A=20Should=20not?= =?UTF-8?q?=20find=20node-red=20node=20in=20nodesDir=20with=20files,=20ico?= =?UTF-8?q?ns=20and=20valid=20node-red=20packages=20=20=20=E2=88=9A=20Shou?= =?UTF-8?q?ld=20not=20find=20node-red=20node=20in=20nodesDir=20when=20regu?= =?UTF-8?q?lar=20package=20and=20valid=20node-red=20packages=20#getModuleF?= =?UTF-8?q?iles=20-=20new=20tests=20below=20=20=20=E2=88=9A=20gets=20a=20n?= =?UTF-8?q?odes=20module=20files=20=20=20=E2=88=9A=20Finds=20only=201=20no?= =?UTF-8?q?de-red=20node=20in=20nodesDir=20amongst=20legacy=20nodes=20and?= =?UTF-8?q?=20regular=20nodes=20=20=20=E2=88=9A=20Finds=20a=20node-red=20n?= =?UTF-8?q?ode=20in=20nodesDir=20with=20a=20sub=20dir=20containing=20valid?= =?UTF-8?q?=20node-red=20package=20=20=20=E2=88=9A=20Finds=202=20node-red?= =?UTF-8?q?=20node=20and=201=20plugin=20in=20nodesDir=20(in=20root=20of=20?= =?UTF-8?q?dir)=20=20=20=E2=88=9A=20Finds=202=20node-red=20node=20and=201?= =?UTF-8?q?=20plugin=20in=20nodesDir=20pointing=20to=20a=20node=5Fmodules?= =?UTF-8?q?=20dir?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../registry/lib/localfilesystem_spec.js | 226 ++++++++++++++---- .../lib/resources/nodesDir1/icons/loose1.svg | 1 + .../lib/resources/nodesDir1/loose1.html | 5 + .../lib/resources/nodesDir1/loose1.js | 4 + .../nodesDir1/loose2/icons/loose2.svg | 1 + .../nodesDir1/loose2/icons/loose2b.svg | 1 + .../resources/nodesDir1/loose2/loose2.html | 5 + .../lib/resources/nodesDir1/loose2/loose2.js | 4 + .../node-red-node-testnode/icons/test.svg | 1 + .../nodesDir1/node-red-node-testnode/main.js | 4 + .../node-red-node-testnode/package.json | 19 ++ .../nodesDir1/regular_module/icons/test.svg | 1 + .../nodesDir1/regular_module/main.js | 4 + .../nodesDir1/regular_module/package.json | 14 ++ .../nodesDir2/@test/testnode/index.html | 5 + .../nodesDir2/@test/testnode/index.js | 4 + .../nodesDir2/@test/testnode/package.json | 20 ++ .../resources/nodesDir2/testnode2/index.html | 5 + .../resources/nodesDir2/testnode2/index.js | 4 + .../nodesDir2/testnode2/package.json | 20 ++ .../theme-plugin2/files/clientside.js | 3 + .../nodesDir2/theme-plugin2/files/plugin.js | 14 ++ .../nodesDir2/theme-plugin2/files/theme.css | 1 + .../nodesDir2/theme-plugin2/package.json | 24 ++ .../node_modules/@test/testnode/index.html | 5 + .../node_modules/@test/testnode/index.js | 4 + .../node_modules/@test/testnode/package.json | 20 ++ .../@test/theme-plugin3/files/clientside.js | 3 + .../@test/theme-plugin3/files/plugin.js | 14 ++ .../@test/theme-plugin3/files/theme.css | 1 + .../@test/theme-plugin3/package.json | 24 ++ .../node_modules/testnode3/index.html | 5 + .../nodesDir3/node_modules/testnode3/index.js | 4 + .../node_modules/testnode3/package.json | 20 ++ 34 files changed, 441 insertions(+), 49 deletions(-) create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir1/icons/loose1.svg create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir1/loose1.html create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir1/loose1.js create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir1/loose2/icons/loose2.svg create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir1/loose2/icons/loose2b.svg create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir1/loose2/loose2.html create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir1/loose2/loose2.js create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir1/node-red-node-testnode/icons/test.svg create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir1/node-red-node-testnode/main.js create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir1/node-red-node-testnode/package.json create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir1/regular_module/icons/test.svg create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir1/regular_module/main.js create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir1/regular_module/package.json create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir2/@test/testnode/index.html create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir2/@test/testnode/index.js create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir2/@test/testnode/package.json create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir2/testnode2/index.html create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir2/testnode2/index.js create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir2/testnode2/package.json create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir2/theme-plugin2/files/clientside.js create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir2/theme-plugin2/files/plugin.js create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir2/theme-plugin2/files/theme.css create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir2/theme-plugin2/package.json create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/testnode/index.html create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/testnode/index.js create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/testnode/package.json create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/theme-plugin3/files/clientside.js create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/theme-plugin3/files/plugin.js create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/theme-plugin3/files/theme.css create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/theme-plugin3/package.json create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/testnode3/index.html create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/testnode3/index.js create mode 100644 test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/testnode3/package.json diff --git a/test/unit/@node-red/registry/lib/localfilesystem_spec.js b/test/unit/@node-red/registry/lib/localfilesystem_spec.js index e84761657..3b1bb63f3 100644 --- a/test/unit/@node-red/registry/lib/localfilesystem_spec.js +++ b/test/unit/@node-red/registry/lib/localfilesystem_spec.js @@ -14,26 +14,41 @@ * limitations under the License. **/ -var should = require("should"); -var sinon = require("sinon"); -var path = require("path"); +const should = require("should"); +const sinon = require("sinon"); +const path = require("path"); -var NR_TEST_UTILS = require("nr-test-utils"); +const NR_TEST_UTILS = require("nr-test-utils"); -var localfilesystem = NR_TEST_UTILS.require("@node-red/registry/lib/localfilesystem"); +const localfilesystem = NR_TEST_UTILS.require("@node-red/registry/lib/localfilesystem"); -var resourcesDir = path.resolve(path.join(__dirname,"resources","local")); -var userDir = path.resolve(path.join(__dirname,"resources","userDir")); -var moduleDir = path.resolve(path.join(__dirname,"resources","local","TestNodeModule")); +const resourcesDir = path.resolve(path.join(__dirname,"resources","local")); +const userDir = path.resolve(path.join(__dirname,"resources","userDir")); -var i18n = NR_TEST_UTILS.require("@node-red/util").i18n; +const nodesDir1 = path.resolve(path.join(__dirname,"resources","nodesDir1")) +const nodesDir2 = path.resolve(path.join(__dirname,"resources","nodesDir2")) +const nodesDir3 =path.resolve(path.join(__dirname,"resources","nodesDir3")) + +const moduleDir = path.resolve(path.join(__dirname,"resources","local","TestNodeModule")); + +const i18n = NR_TEST_UTILS.require("@node-red/util").i18n; describe("red/nodes/registry/localfilesystem",function() { + var stubs = []; + function stubPathJoin() { + var _join = path.join; + stubs.push(sinon.stub(path,"join").callsFake(function() { + if (arguments[0] == resourcesDir) { + // This stops the module tree scan from going any higher + // up the tree than resourcesDir. + return arguments[0]; + } + return _join.apply(null,arguments); + })); + } beforeEach(function() { stubs.push(sinon.stub(i18n,"registerMessageCatalog").callsFake(function() { return Promise.resolve(); })); }) - - var stubs = []; afterEach(function() { while(stubs.length) { stubs.pop().restore(); @@ -129,16 +144,76 @@ describe("red/nodes/registry/localfilesystem",function() { checkNodes(nm.nodes,['TestNode5'],['TestNode1']); done(); }); + it("Finds nodes and icons only in nodesDir with files, icons and valid node-red packages",function(done) { + localfilesystem.init({nodesDir:nodesDir1}); + const nodeList = localfilesystem.getNodeFiles(true); + nodeList.should.have.a.property("node-red"); + const nm = nodeList['node-red']; + nm.should.have.a.property('name','node-red'); + nm.should.have.a.property("nodes"); + nm.should.have.a.property("icons"); + checkNodes(nm.nodes,['loose1', 'loose2'], []); + //1 icon in nodesDir1/icons/ - should be found + //2 icons in nodesDir1/loose2/icons/ - should be found + //1 icons in nodesDir1/node-red-node-testnode/icons/ - should be found + //1 icons in nodesDir1/regular_module/icons/ - should NOT be found + //total icon sets 3, total icons 4 + nm.icons.should.have.a.property("length", 3); + nm.icons[0].should.have.a.property("path") + nm.icons[0].should.have.a.property("icons", ['loose1.svg']) + nm.icons[1].should.have.a.property("path") + nm.icons[1].should.have.a.property("icons", ['loose2.svg', 'loose2b.svg']) + nm.icons[2].should.have.a.property("path") + nm.icons[2].should.have.a.property("icons", ['test.svg']) + done(); + }); + it("Should not find node-red node in nodesDir with files, icons and valid node-red packages",function(done) { + // path contains a regular node module and a node-red node module + localfilesystem.init({nodesDir:path.join(nodesDir1)}); + const nodeList = localfilesystem.getNodeFiles(true); + nodeList.should.have.a.property("node-red"); + const nm = nodeList['node-red']; + nm.should.have.a.property('name','node-red'); + nm.should.have.a.property("nodes"); + nm.nodes.should.have.a.property("loose1"); + nm.nodes.should.have.a.property("loose2"); + nm.nodes.should.not.have.a.property("regular_module"); + nm.nodes.should.not.have.a.property("node-red-node-testnode"); + for (let key of Object.keys(nm.nodes)) { + const n = nm.nodes[key]; + n.file.indexOf("regular_module").should.eql(-1, `found icons in a node-red module`) + n.file.indexOf("node-red-node-testnode").should.eql(-1, `found icons in a node-red module`) + } + //1 icon in nodesDir1/icons/ - should be found + //2 icons in nodesDir1/loose2/icons/ - should be found + //1 icons in nodesDir1/node-red-node-testnode/icons/ - should be found + //1 icons in nodesDir1/regular_module/icons/ - should NOT be found + //total icon sets 3, total icons 4 + nm.should.have.a.property("icons"); + nm.icons.should.have.a.property("length", 3); + let iconCount = 0; + for (let index = 0; index < nm.icons.length; index++) { + const iconDir = nm.icons[index]; + iconCount += iconDir.icons.length + iconDir.path.indexOf("node-red-node-testnode").should.eql(-1, `should not find icons in a node-red module`) + } + should(iconCount).eql(4, "Should find only 4 icons") + done(); + }); + it("Should not find node-red node in nodesDir when regular package and valid node-red packages",function(done) { + localfilesystem.init({nodesDir:path.join(nodesDir1,"regular_module")}); + const nodeList = localfilesystem.getNodeFiles(true); + nodeList.should.have.a.property("node-red"); + const nm = nodeList['node-red']; + nm.should.have.a.property('name','node-red'); + nm.should.have.a.property("nodes", {}); + nm.should.have.a.property("icons"); + nm.icons.should.have.a.property("length", 1); //should find 1 icons folder + nm.icons[0].should.have.a.property("icons", [ 'test.svg' ]); //should find 1 icon in regular package + done(); + }); it("Finds nodes module path",function(done) { - var _join = path.join; - stubs.push(sinon.stub(path,"join").callsFake(function() { - if (arguments[0] == resourcesDir) { - // This stops the module tree scan from going any higher - // up the tree than resourcesDir. - return arguments[0]; - } - return _join.apply(null,arguments); - })); + stubPathJoin() localfilesystem.init({coreNodesDir:moduleDir}); var nodeList = localfilesystem.getNodeFiles(); nodeList.should.have.a.property("node-red"); @@ -166,8 +241,6 @@ describe("red/nodes/registry/localfilesystem",function() { i18n.registerMessageCatalog.lastCall.args[1].should.eql(path.resolve(path.join(moduleDir,"locales"))); i18n.registerMessageCatalog.lastCall.args[2].should.eql('messages.json'); - - done(); }); it.skip("finds locales directory"); @@ -205,15 +278,7 @@ describe("red/nodes/registry/localfilesystem",function() { }); describe("#getModuleFiles",function() { it("gets a nodes module files",function(done) { - var _join = path.join; - stubs.push(sinon.stub(path,"join").callsFake(function() { - if (arguments[0] == resourcesDir) { - // This stops the module tree scan from going any higher - // up the tree than resourcesDir. - return arguments[0]; - } - return _join.apply(null,arguments); - })); + stubPathJoin() localfilesystem.init({coreNodesDir:moduleDir}); var nodeModule = localfilesystem.getModuleFiles('TestNodeModule'); nodeModule.should.have.a.property('TestNodeModule'); @@ -230,16 +295,87 @@ describe("red/nodes/registry/localfilesystem",function() { done(); }); + it("Finds only 1 node-red node in nodesDir amongst legacy nodes and regular nodes",function(done) { + stubPathJoin() + localfilesystem.init({nodesDir:[path.join(nodesDir1,"node-red-node-testnode")]}); + const nodeModule = localfilesystem.getModuleFiles(); + const loaded = Object.keys(nodeModule) + loaded.should.have.a.property("length", 1) + loaded.indexOf('node-red-node-testnode').should.greaterThan(-1, "Should load node-red-node-testnode") + + nodeModule['node-red-node-testnode'].should.have.a.property('name','node-red-node-testnode'); + nodeModule['node-red-node-testnode'].should.have.a.property('version','1.0.0'); + nodeModule['node-red-node-testnode'].should.have.a.property('nodes'); + nodeModule['node-red-node-testnode'].should.have.a.property('path'); + nodeModule['node-red-node-testnode'].should.have.a.property('user', false); + checkNodes(nodeModule['node-red-node-testnode'].nodes,['testnode'],[],'node-red-node-testnode'); + done(); + }); + it("Finds a node-red node in nodesDir with a sub dir containing valid node-red package",function(done) { + stubPathJoin() + localfilesystem.init({nodesDir:[path.join(nodesDir1,"node-red-node-testnode")]}); + const nodeModule = localfilesystem.getModuleFiles(); + const loaded = Object.keys(nodeModule) + nodeModule['node-red-node-testnode'].should.have.a.property('name','node-red-node-testnode'); + nodeModule['node-red-node-testnode'].should.have.a.property('version','1.0.0'); + nodeModule['node-red-node-testnode'].should.have.a.property('nodes'); + nodeModule['node-red-node-testnode'].should.have.a.property('path'); + nodeModule['node-red-node-testnode'].should.have.a.property('user', false); + checkNodes(nodeModule['node-red-node-testnode'].nodes,['testnode'],[],'node-red-node-testnode'); + done(); + }); + it("Finds 2 node-red modules and 1 plugin in nodesDir (in root of dir)",function(done) { + stubPathJoin() + localfilesystem.init({nodesDir:[nodesDir2]}); + const nodeModule = localfilesystem.getModuleFiles(); + const loaded = Object.keys(nodeModule) + loaded.should.have.a.property("length", 3) + loaded.indexOf('@test/testnode').should.greaterThan(-1, "Should load @test/testnode") + loaded.indexOf('testnode2').should.greaterThan(-1, "Should load testnode2") + loaded.indexOf('test-theme2').should.greaterThan(-1, "Should load test-theme2") + + nodeModule['@test/testnode'].should.have.a.property('name','@test/testnode'); + nodeModule['@test/testnode'].should.have.a.property('version','1.0.0'); + nodeModule['@test/testnode'].should.have.a.property('nodes'); + nodeModule['@test/testnode'].should.have.a.property('path'); + nodeModule['@test/testnode'].should.have.a.property('user', false); + + nodeModule['testnode2'].should.have.a.property('name','testnode2'); + nodeModule['testnode2'].should.have.a.property('version','1.0.0'); + nodeModule['testnode2'].should.have.a.property('nodes'); + nodeModule['testnode2'].should.have.a.property('path'); + nodeModule['testnode2'].should.have.a.property('user', false); + + nodeModule['test-theme2'].should.have.a.property('name','test-theme2'); + + nodeModule['test-theme2'].should.have.a.property('version','0.0.1'); + nodeModule['test-theme2'].should.have.a.property('nodes', {}); + nodeModule['test-theme2'].should.have.a.property('path'); + nodeModule['test-theme2'].should.have.a.property('user', false); + nodeModule['test-theme2'].should.have.a.property('plugins'); + nodeModule['test-theme2'].plugins.should.have.a.property('test-theme2'); + nodeModule['test-theme2'].plugins['test-theme2'].should.have.a.property('name','test-theme2'); + nodeModule['test-theme2'].plugins['test-theme2'].should.have.a.property('module','test-theme2'); + nodeModule['test-theme2'].plugins['test-theme2'].should.have.a.property('version', '0.0.1'); + nodeModule['test-theme2'].plugins['test-theme2'].should.have.a.property('file'); + nodeModule['test-theme2'].plugins['test-theme2'].should.have.a.property('local', false); + + done(); + }); + it("Finds 2 node-red modules and 1 plugin in nodesDir pointing to a node_modules dir",function(done) { + stubPathJoin() + localfilesystem.init({nodesDir:[path.join(nodesDir3, "node_modules")]}); + const nodeModule = localfilesystem.getModuleFiles(); + const loaded = Object.keys(nodeModule) + loaded.should.have.a.property("length", 3) + + loaded.indexOf('@test/testnode').should.greaterThan(-1, "Should load @test/testnode") + loaded.indexOf('@test/test-theme3').should.greaterThan(-1, "Should load test-theme3") + loaded.indexOf('testnode3').should.greaterThan(-1, "Should load testnode3") + done(); + }); it("throws an error if a node isn't found",function(done) { - var _join = path.join; - stubs.push(sinon.stub(path,"join").callsFake(function() { - if (arguments[0] == resourcesDir) { - // This stops the module tree scan from going any higher - // up the tree than resourcesDir. - return arguments[0]; - } - return _join.apply(null,arguments); - })); + stubPathJoin() localfilesystem.init({coreNodesDir:moduleDir}); /*jshint immed: false */ (function(){ @@ -250,15 +386,7 @@ describe("red/nodes/registry/localfilesystem",function() { it.skip("finds locales directory"); it.skip("finds icon path directory"); it("scans icon files with a module file",function(done) { - var _join = path.join; - stubs.push(sinon.stub(path,"join").callsFake(function() { - if (arguments[0] == resourcesDir) { - // This stops the module tree scan from going any higher - // up the tree than resourcesDir. - return arguments[0]; - } - return _join.apply(null,arguments); - })); + stubPathJoin() localfilesystem.init({ coreNodesDir: moduleDir }); diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir1/icons/loose1.svg b/test/unit/@node-red/registry/lib/resources/nodesDir1/icons/loose1.svg new file mode 100644 index 000000000..927680ee3 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir1/icons/loose1.svg @@ -0,0 +1 @@ + diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir1/loose1.html b/test/unit/@node-red/registry/lib/resources/nodesDir1/loose1.html new file mode 100644 index 000000000..8f392056c --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir1/loose1.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir1/loose1.js b/test/unit/@node-red/registry/lib/resources/nodesDir1/loose1.js new file mode 100644 index 000000000..624cfe6d8 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir1/loose1.js @@ -0,0 +1,4 @@ + + (function() { + console.log("hello from loose1.js") + })() diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir1/loose2/icons/loose2.svg b/test/unit/@node-red/registry/lib/resources/nodesDir1/loose2/icons/loose2.svg new file mode 100644 index 000000000..04bebc370 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir1/loose2/icons/loose2.svg @@ -0,0 +1 @@ + diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir1/loose2/icons/loose2b.svg b/test/unit/@node-red/registry/lib/resources/nodesDir1/loose2/icons/loose2b.svg new file mode 100644 index 000000000..250348861 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir1/loose2/icons/loose2b.svg @@ -0,0 +1 @@ + diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir1/loose2/loose2.html b/test/unit/@node-red/registry/lib/resources/nodesDir1/loose2/loose2.html new file mode 100644 index 000000000..e23e2880a --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir1/loose2/loose2.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir1/loose2/loose2.js b/test/unit/@node-red/registry/lib/resources/nodesDir1/loose2/loose2.js new file mode 100644 index 000000000..e1cde2a82 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir1/loose2/loose2.js @@ -0,0 +1,4 @@ + + (function() { + console.log("hello from loose2.js") + })() diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir1/node-red-node-testnode/icons/test.svg b/test/unit/@node-red/registry/lib/resources/nodesDir1/node-red-node-testnode/icons/test.svg new file mode 100644 index 000000000..04bebc370 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir1/node-red-node-testnode/icons/test.svg @@ -0,0 +1 @@ + diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir1/node-red-node-testnode/main.js b/test/unit/@node-red/registry/lib/resources/nodesDir1/node-red-node-testnode/main.js new file mode 100644 index 000000000..e2e0781d1 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir1/node-red-node-testnode/main.js @@ -0,0 +1,4 @@ + + (function() { + console.log("hello from regular module main.js") + })() diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir1/node-red-node-testnode/package.json b/test/unit/@node-red/registry/lib/resources/nodesDir1/node-red-node-testnode/package.json new file mode 100644 index 000000000..26d4151fb --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir1/node-red-node-testnode/package.json @@ -0,0 +1,19 @@ +{ + "name": "node-red-node-testnode", + "version": "1.0.0", + "description": "A node-red node that does nothing other than exist", + "main": "main.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "node-red" + ], + "node-red": { + "nodes": { + "testnode": "index.js" + } + }, + "author": "@testyMcTersterson", + "license": "MIT" +} diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir1/regular_module/icons/test.svg b/test/unit/@node-red/registry/lib/resources/nodesDir1/regular_module/icons/test.svg new file mode 100644 index 000000000..04bebc370 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir1/regular_module/icons/test.svg @@ -0,0 +1 @@ + diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir1/regular_module/main.js b/test/unit/@node-red/registry/lib/resources/nodesDir1/regular_module/main.js new file mode 100644 index 000000000..e2e0781d1 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir1/regular_module/main.js @@ -0,0 +1,4 @@ + + (function() { + console.log("hello from regular module main.js") + })() diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir1/regular_module/package.json b/test/unit/@node-red/registry/lib/resources/nodesDir1/regular_module/package.json new file mode 100644 index 000000000..23cfd7e86 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir1/regular_module/package.json @@ -0,0 +1,14 @@ +{ + "name": "regular_node", + "version": "1.0.0", + "description": "A regular node that does nothing other than exist", + "main": "main.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "test" + ], + "author": "@testyMcTersterson", + "license": "MIT" +} diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir2/@test/testnode/index.html b/test/unit/@node-red/registry/lib/resources/nodesDir2/@test/testnode/index.html new file mode 100644 index 000000000..0e45b3007 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir2/@test/testnode/index.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir2/@test/testnode/index.js b/test/unit/@node-red/registry/lib/resources/nodesDir2/@test/testnode/index.js new file mode 100644 index 000000000..1fe9d89a3 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir2/@test/testnode/index.js @@ -0,0 +1,4 @@ + + (function() { + console.log("hello from @test/testnode index.js") + })() diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir2/@test/testnode/package.json b/test/unit/@node-red/registry/lib/resources/nodesDir2/@test/testnode/package.json new file mode 100644 index 000000000..a32462631 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir2/@test/testnode/package.json @@ -0,0 +1,20 @@ +{ + "name": "@test/testnode", + "version": "1.0.0", + "description": "A test node that does nothing other than exist", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "node-red", + "test" + ], + "node-red": { + "nodes": { + "testnode": "index.js" + } + }, + "author": "@testyMcTersterson", + "license": "MIT" +} diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir2/testnode2/index.html b/test/unit/@node-red/registry/lib/resources/nodesDir2/testnode2/index.html new file mode 100644 index 000000000..5d9f2b0ec --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir2/testnode2/index.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir2/testnode2/index.js b/test/unit/@node-red/registry/lib/resources/nodesDir2/testnode2/index.js new file mode 100644 index 000000000..0f0eba392 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir2/testnode2/index.js @@ -0,0 +1,4 @@ + + (function() { + console.log("hello from testnode2 index.js") + })() diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir2/testnode2/package.json b/test/unit/@node-red/registry/lib/resources/nodesDir2/testnode2/package.json new file mode 100644 index 000000000..f9e8aecbe --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir2/testnode2/package.json @@ -0,0 +1,20 @@ +{ + "name": "testnode2", + "version": "1.0.0", + "description": "A test node that does nothing other than exist", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "node-red", + "test" + ], + "node-red": { + "nodes": { + "testnode2": "index.js" + } + }, + "author": "@testyMcTersterson", + "license": "MIT" +} diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir2/theme-plugin2/files/clientside.js b/test/unit/@node-red/registry/lib/resources/nodesDir2/theme-plugin2/files/clientside.js new file mode 100644 index 000000000..fb2e22289 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir2/theme-plugin2/files/clientside.js @@ -0,0 +1,3 @@ +(function() { + console.log("Hi from test plugin client side") +})() diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir2/theme-plugin2/files/plugin.js b/test/unit/@node-red/registry/lib/resources/nodesDir2/theme-plugin2/files/plugin.js new file mode 100644 index 000000000..3202aeb8b --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir2/theme-plugin2/files/plugin.js @@ -0,0 +1,14 @@ +module.exports = function (RED) { + RED.plugins.registerPlugin('test-theme', { + type: 'node-red-theme', + scripts: [ + 'files/clientside.js' + ], + css: [ + 'files/theme.css', + ], + monacoOptions: { + theme: "vs" + } + }) +} diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir2/theme-plugin2/files/theme.css b/test/unit/@node-red/registry/lib/resources/nodesDir2/theme-plugin2/files/theme.css new file mode 100644 index 000000000..872c93e10 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir2/theme-plugin2/files/theme.css @@ -0,0 +1 @@ +:root{--red-ui-primary-background: #f2f3fb;} \ No newline at end of file diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir2/theme-plugin2/package.json b/test/unit/@node-red/registry/lib/resources/nodesDir2/theme-plugin2/package.json new file mode 100644 index 000000000..b8608c175 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir2/theme-plugin2/package.json @@ -0,0 +1,24 @@ +{ + "name": "test-theme2", + "version": "0.0.1", + "description": "test theme for Node-RED", + + "keywords": [ + "node-red", + "plugin", + "theme" + ], + "author": { + "name": "testy-McTesterson" + }, + "license": "MIT", + "node-red": { + "version": ">=2.2.0", + "plugins": { + "test-theme2": "files/plugin.js" + } + }, + "engines": { + "node": ">=12.x" + } +} diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/testnode/index.html b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/testnode/index.html new file mode 100644 index 000000000..0e45b3007 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/testnode/index.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/testnode/index.js b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/testnode/index.js new file mode 100644 index 000000000..1fe9d89a3 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/testnode/index.js @@ -0,0 +1,4 @@ + + (function() { + console.log("hello from @test/testnode index.js") + })() diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/testnode/package.json b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/testnode/package.json new file mode 100644 index 000000000..a32462631 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/testnode/package.json @@ -0,0 +1,20 @@ +{ + "name": "@test/testnode", + "version": "1.0.0", + "description": "A test node that does nothing other than exist", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "node-red", + "test" + ], + "node-red": { + "nodes": { + "testnode": "index.js" + } + }, + "author": "@testyMcTersterson", + "license": "MIT" +} diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/theme-plugin3/files/clientside.js b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/theme-plugin3/files/clientside.js new file mode 100644 index 000000000..fb2e22289 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/theme-plugin3/files/clientside.js @@ -0,0 +1,3 @@ +(function() { + console.log("Hi from test plugin client side") +})() diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/theme-plugin3/files/plugin.js b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/theme-plugin3/files/plugin.js new file mode 100644 index 000000000..3202aeb8b --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/theme-plugin3/files/plugin.js @@ -0,0 +1,14 @@ +module.exports = function (RED) { + RED.plugins.registerPlugin('test-theme', { + type: 'node-red-theme', + scripts: [ + 'files/clientside.js' + ], + css: [ + 'files/theme.css', + ], + monacoOptions: { + theme: "vs" + } + }) +} diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/theme-plugin3/files/theme.css b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/theme-plugin3/files/theme.css new file mode 100644 index 000000000..872c93e10 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/theme-plugin3/files/theme.css @@ -0,0 +1 @@ +:root{--red-ui-primary-background: #f2f3fb;} \ No newline at end of file diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/theme-plugin3/package.json b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/theme-plugin3/package.json new file mode 100644 index 000000000..56b1bfb6d --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/@test/theme-plugin3/package.json @@ -0,0 +1,24 @@ +{ + "name": "@test/test-theme3", + "version": "0.0.1", + "description": "test theme for Node-RED", + + "keywords": [ + "node-red", + "plugin", + "theme" + ], + "author": { + "name": "testy-McTesterson" + }, + "license": "MIT", + "node-red": { + "version": ">=2.2.0", + "plugins": { + "test-theme3": "files/plugin.js" + } + }, + "engines": { + "node": ">=12.x" + } +} diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/testnode3/index.html b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/testnode3/index.html new file mode 100644 index 000000000..a4034f340 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/testnode3/index.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/testnode3/index.js b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/testnode3/index.js new file mode 100644 index 000000000..855768ad8 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/testnode3/index.js @@ -0,0 +1,4 @@ + + (function() { + console.log("hello from testnode3 index.js") + })() diff --git a/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/testnode3/package.json b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/testnode3/package.json new file mode 100644 index 000000000..41e9bb8f8 --- /dev/null +++ b/test/unit/@node-red/registry/lib/resources/nodesDir3/node_modules/testnode3/package.json @@ -0,0 +1,20 @@ +{ + "name": "testnode3", + "version": "1.0.0", + "description": "A test node that does nothing other than exist", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "node-red", + "test" + ], + "node-red": { + "nodes": { + "testnode3": "index.js" + } + }, + "author": "@testyMcTersterson", + "license": "MIT" +} From 62a2a4a9f5b90b660d790eaf626ac20d16e91db4 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Thu, 16 Jun 2022 11:38:19 +0100 Subject: [PATCH 08/17] Further simplify file node filename entry UX (v3) fixes #3668 --- .../@node-red/nodes/core/storage/10-file.html | 16 ++++++++-------- .../@node-red/nodes/locales/en-US/messages.json | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/storage/10-file.html b/packages/node_modules/@node-red/nodes/core/storage/10-file.html index b726b537d..6b19ebaa8 100755 --- a/packages/node_modules/@node-red/nodes/core/storage/10-file.html +++ b/packages/node_modules/@node-red/nodes/core/storage/10-file.html @@ -198,8 +198,8 @@ category: 'storage', defaults: { name: {value:""}, - filename: {value:"filename"}, - filenameType: {value:"msg"}, + filename: {value:""}, + filenameType: {value:"str"}, appendNewline: {value:true}, createDir: {value:false}, overwriteFile: {value:"false"}, @@ -236,8 +236,8 @@ label: node._("file.encoding.setbymsg") }).text(label).appendTo(encSel); $("#node-input-filename").typedInput({ - default: "msg", - types: ["str", "msg", "jsonata", "env"], + default: "str", + types: [{label:RED._("node-red:file.label.path"), value:"str", icon:""}, "msg", "jsonata", "env"], typeField: $("#node-input-filenameType") }); if(typeof node.filenameType == 'undefined') { @@ -297,8 +297,8 @@ category: 'storage', defaults: { name: {value:""}, - filename: {value:"filename"}, - filenameType: {value:"msg"}, + filename: {value:""}, + filenameType: {value:"str"}, format: {value:"utf8"}, chunk: {value:false}, sendError: {value: false}, @@ -341,8 +341,8 @@ label: label }).text(label).appendTo(encSel); $("#node-input-filename").typedInput({ - default: "msg", - types: ["str", "msg", "jsonata", "env"], + default: "str", + types: [{label:RED._("node-red:file.label.path"), value:"str", icon:""}, "msg", "jsonata", "env"], typeField: $("#node-input-filenameType") }); if(typeof node.filenameType == 'undefined') { diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json index 77d832abd..62d5f351f 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json @@ -928,6 +928,7 @@ "write": "write file", "read": "read file", "filename": "Filename", + "path": "path", "action": "Action", "addnewline": "Add newline (\\n) to each payload?", "createdir": "Create directory if it doesn't exist?", From ea469470541de61bcb13dc8dc4a120a9c6905673 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 16 Jun 2022 13:48:36 +0100 Subject: [PATCH 09/17] Add RED.actions.getLabel to retrieve action i18n label --- .../editor-client/src/js/ui/actions.js | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/actions.js b/packages/node_modules/@node-red/editor-client/src/js/ui/actions.js index 2273ae9ab..5bd9ea034 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/actions.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/actions.js @@ -21,6 +21,34 @@ RED.actions = (function() { function getAction(name) { return actions[name].handler; } + function getActionLabel(name) { + let def = actions[name] + if (!def) { + return '' + } + if (!def.label) { + var options = def.options; + var key = options ? options.label : undefined; + if (!key) { + key = "action-list." +name.replace(/^.*:/,""); + } + var label = RED._(key); + if (label === key) { + // no translation. convert `name` to description + label = name.replace(/(^.+:([a-z]))|(-([a-z]))/g, function() { + if (arguments[5] === 0) { + return arguments[2].toUpperCase(); + } else { + return " "+arguments[4].toUpperCase(); + } + }); + } + def.label = label; + } + return def.label + } + + function invokeAction() { var args = Array.prototype.slice.call(arguments); var name = args.shift(); @@ -31,7 +59,7 @@ RED.actions = (function() { } function listActions() { var result = []; - var missing = []; + Object.keys(actions).forEach(function(action) { var def = actions[action]; var shortcut = RED.keyboard.getShortcut(action); @@ -42,28 +70,8 @@ RED.actions = (function() { isUser = !!RED.keyboard.getUserShortcut(action); } if (!def.label) { - var name = action; - var options = def.options; - var key = options ? options.label : undefined; - if (!key) { - key = "action-list." +name.replace(/^.*:/,""); - } - var label = RED._(key); - if (label === key) { - // no translation. convert `name` to description - label = name.replace(/(^.+:([a-z]))|(-([a-z]))/g, function() { - if (arguments[5] === 0) { - return arguments[2].toUpperCase(); - } else { - return " "+arguments[4].toUpperCase(); - } - }); - missing.push(key); - } - def.label = label; + def.label = getActionLabel(action) } - //console.log("; missing:", missing); - result.push({ id:action, scope:shortcut?shortcut.scope:undefined, @@ -79,6 +87,7 @@ RED.actions = (function() { add: addAction, remove: removeAction, get: getAction, + getLabel: getActionLabel, invoke: invokeAction, list: listActions } From 6bea3dabbb4a999a4c3fb77926ec0b0b154664b6 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 16 Jun 2022 13:49:25 +0100 Subject: [PATCH 10/17] Allow popover position to be set via absolute pos rather than relative --- .../@node-red/editor-client/src/js/ui/common/popover.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js index f1728ed83..9ddd3d866 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js @@ -610,10 +610,13 @@ RED.popover = (function() { var target = options.target; var align = options.align || "right"; var offset = options.offset || [0,0]; + var xPos = options.x; + var yPos = options.y; + var isAbsolutePosition = (xPos !== undefined && yPos !== undefined) - var pos = target.offset(); - var targetWidth = target.width(); - var targetHeight = target.outerHeight(); + var pos = isAbsolutePosition?{left:xPos, top: yPos}:target.offset(); + var targetWidth = isAbsolutePosition?0:target.width(); + var targetHeight = isAbsolutePosition?0:target.outerHeight(); var panelHeight = panel.height(); var panelWidth = panel.width(); From cce4f6f7f764675d6c9eecd611fde310f657f054 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 16 Jun 2022 13:50:05 +0100 Subject: [PATCH 11/17] Add onpre/postselect, direction opts to menu and make id optional --- .../editor-client/src/js/ui/common/menu.js | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/menu.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/menu.js index 417189b33..6327f5268 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/menu.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/menu.js @@ -16,6 +16,7 @@ RED.menu = (function() { var menuItems = {}; + let menuItemCount = 0 function createMenuItem(opt) { var item; @@ -59,15 +60,16 @@ RED.menu = (function() { item = $('
  • '); } else { item = $('
  • '); - + if (!opt.id) { + opt.id = 'red-ui-menu-item-'+(menuItemCount++) + } if (opt.group) { item.addClass("red-ui-menu-group-"+opt.group); - } var linkContent = ''; if (opt.toggle) { - linkContent += ''; - linkContent += ''; + linkContent += ''; + linkContent += ''; } if (opt.icon !== undefined) { @@ -77,12 +79,15 @@ RED.menu = (function() { linkContent += ' '; } } - + let label = opt.label + if (!opt.label && typeof opt.onselect === 'string') { + label = RED.actions.getLabel(opt.onselect) + } if (opt.sublabel) { - linkContent += ''+opt.label+''+ + linkContent += ''+label+''+ ''+opt.sublabel+'' } else { - linkContent += ''+opt.label+'' + linkContent += ''+label+'' } linkContent += ''; @@ -126,10 +131,21 @@ RED.menu = (function() { }); } if (opt.options) { - item.addClass("red-ui-menu-dropdown-submenu pull-left"); + item.addClass("red-ui-menu-dropdown-submenu"+(opt.direction!=='right'?" pull-left":"")); var submenu = $('
      ').appendTo(item); for (var i=0;i",{class:"red-ui-menu red-ui-menu-dropdown pull-right"}); - + if (options.direction) { + topMenu.addClass("red-ui-menu-dropdown-direction-"+options.direction) + } if (options.id) { topMenu.attr({id:options.id+"-submenu"}); var menuParent = $("#"+options.id); @@ -175,6 +193,15 @@ RED.menu = (function() { var lastAddedSeparator = false; for (var i=0;i Date: Thu, 16 Jun 2022 13:57:50 +0100 Subject: [PATCH 12/17] Allow typeSearch to list actions A proof-of-concept which needs a bit more UI polish, but capturing the current code for the future. We do not add actions to the list, so the code is unused. --- .../editor-client/src/js/ui/typeSearch.js | 33 +++++++++++++++---- .../editor-client/src/js/ui/utils.js | 2 ++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js b/packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js index 0fc633071..fc5b8e99e 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js @@ -104,7 +104,9 @@ RED.typeSearch = (function() { var index = Math.max(0,selected); if (index < children.length) { var n = $(children[index]).find(".red-ui-editableList-item-content").data('data'); - typesUsed[n.type] = Date.now(); + if (!/^_action_:/.test(n.type)) { + typesUsed[n.type] = Date.now(); + } if (n.def.outputs === 0) { confirm(n); } else { @@ -173,6 +175,8 @@ RED.typeSearch = (function() { var nodeDiv = $('
      ',{class:"red-ui-search-result-node"}).appendTo(div); if (object.type === "junction") { nodeDiv.addClass("red-ui-palette-icon-junction"); + } else if (/^_action_:/.test(object.type)) { + nodeDiv.addClass("red-ui-palette-icon-junction") } else { var colour = RED.utils.getNodeColor(object.type,def); nodeDiv.css('backgroundColor',colour); @@ -182,11 +186,14 @@ RED.typeSearch = (function() { var iconContainer = $('
      ',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv); RED.utils.createIconElement(icon_url, iconContainer, false); - if (object.type !== "junction" && def.inputs > 0) { - $('
      ',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv); - } - if (object.type !== "junction" && def.outputs > 0) { - $('
      ',{class:"red-ui-search-result-node-port red-ui-search-result-node-output"}).appendTo(nodeDiv); + + if (!/^_action_:/.test(object.type) && object.type !== "junction") { + if (def.inputs > 0) { + $('
      ',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv); + } + if (def.outputs > 0) { + $('
      ',{class:"red-ui-search-result-node-port red-ui-search-result-node-output"}).appendTo(nodeDiv); + } } var contentDiv = $('
      ',{class:"red-ui-search-result-description"}).appendTo(div); @@ -207,7 +214,9 @@ RED.typeSearch = (function() { } function confirm(def) { hide(); - typesUsed[def.type] = Date.now(); + if (!/^_action_:/.test(def.type)) { + typesUsed[def.type] = Date.now(); + } addCallback(def.type); } @@ -316,6 +325,7 @@ RED.typeSearch = (function() { function applyFilter(filter,type,def) { return !filter || ( + (!filter.spliceMultiple) && (!filter.type || type === filter.type) && (!filter.input || type === 'junction' || def.inputs > 0) && (!filter.output || type === 'junction' || def.outputs > 0) @@ -330,6 +340,13 @@ RED.typeSearch = (function() { 'inject','debug','function','change','switch','junction' ].filter(function(t) { return applyFilter(opts.filter,t,RED.nodes.getType(t)); }); + // if (opts.filter && opts.filter.input && opts.filter.output && !opts.filter.type) { + // if (opts.filter.spliceMultiple) { + // common.push('_action_:core:split-wires-with-junctions') + // } + // common.push('_action_:core:split-wire-with-link-nodes') + // } + var recentlyUsed = Object.keys(typesUsed); recentlyUsed.sort(function(a,b) { return typesUsed[b]-typesUsed[a]; @@ -354,6 +371,8 @@ RED.typeSearch = (function() { var itemDef = RED.nodes.getType(common[i]); if (common[i] === 'junction') { itemDef = { inputs:1, outputs: 1, label: 'junction', type: 'junction'} + } else if (/^_action_:/.test(common[i]) ) { + itemDef = { inputs:1, outputs: 1, label: common[i], type: common[i]} } if (itemDef) { item = { 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 c3ea0ddd1..2c4cdca6b 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 @@ -1032,6 +1032,8 @@ RED.utils = (function() { return "font-awesome/fa-circle-o" } else if (def.category === 'config') { return RED.settings.apiRootUrl+"icons/node-red/cog.svg" + } else if ((node && /^_action_:/.test(node.type)) || /^_action_:/.test(def.type)) { + return "font-awesome/fa-cogs" } else if (node && node.type === 'tab') { return "red-ui-icons/red-ui-icons-flow" // return RED.settings.apiRootUrl+"images/subflow_tab.svg" From 0eba4bdd61e196b446981b786d30edb5ccbd3665 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 16 Jun 2022 13:59:14 +0100 Subject: [PATCH 13/17] Add right-click context menu to workspace --- Gruntfile.js | 1 + .../editor-client/src/js/ui/contextMenu.js | 175 +++++++++++ .../editor-client/src/js/ui/view-tools.js | 155 +++++++++- .../@node-red/editor-client/src/js/ui/view.js | 272 ++++++++++-------- .../editor-client/src/sass/dropdownMenu.scss | 35 ++- 5 files changed, 505 insertions(+), 133 deletions(-) create mode 100644 packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js diff --git a/Gruntfile.js b/Gruntfile.js index 979b38051..a168d7ce4 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -192,6 +192,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/ui/library.js", "packages/node_modules/@node-red/editor-client/src/js/ui/notifications.js", "packages/node_modules/@node-red/editor-client/src/js/ui/search.js", + "packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js", "packages/node_modules/@node-red/editor-client/src/js/ui/actionList.js", "packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js", "packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js", diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js b/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js new file mode 100644 index 000000000..1a0dee2bb --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js @@ -0,0 +1,175 @@ +RED.contextMenu = (function() { + + let menu; + function createMenu() { + // menu = RED.popover.menu({ + // options: [ + // { + // label: 'delete selection', + // onselect: function() { + // RED.actions.invoke('core:delete-selection') + // RED.view.focus() + // } + // }, + // { label: 'world' } + // ], + // width: 200, + // }) + + + + + } + + function disposeMenu() { + $(document).off("mousedown.red-ui-workspace-context-menu"); + if (menu) { + menu.remove(); + } + menu = null; + } + function show(options) { + if (menu) { + menu.remove() + } + + const selection = RED.view.selection() + const hasSelection = (selection.nodes && selection.nodes.length > 0); + const hasMultipleSelection = hasSelection && selection.nodes.length > 1; + const hasLinks = selection.links && selection.links.length > 0; + const isSingleLink = !hasSelection && hasLinks && selection.links.length === 1 + const isMultipleLinks = !hasSelection && hasLinks && selection.links.length > 1 + const canDelete = hasSelection || hasLinks + const isGroup = hasSelection && selection.nodes.length === 1 && selection.nodes[0].type === 'group' + + const canRemoveFromGroup = hasSelection && !!selection.nodes[0].g + + + const menuItems = [ + { onselect: 'core:show-action-list', onpostselect: function() {} }, + { + label: 'Insert', + options: [ + { + label: 'Node', + onselect: function() { + RED.view.showQuickAddDialog({ + position: [ options.x - offset.left, options.y - offset.top ], + touchTrigger: true, + splice: isSingleLink?selection.links[0]:undefined, + // spliceMultiple: isMultipleLinks + }) + } + }, + { + label: 'Junction', + onselect: 'core:split-wires-with-junctions', + disabled: hasSelection || !hasLinks + }, + { + label: 'Link Nodes', + onselect: 'core:split-wire-with-link-nodes', + disabled: hasSelection || !hasLinks + } + ] + + + + } + ] + // menuItems.push( + // { + // label: (isSingleLink || isMultipleLinks)?'Insert into wire...':'Add node...', + // onselect: function() { + // RED.view.showQuickAddDialog({ + // position: [ options.x - offset.left, options.y - offset.top ], + // touchTrigger: true, + // splice: isSingleLink?selection.links[0]:undefined, + // spliceMultiple: isMultipleLinks + // }) + // } + // }, + // ) + // if (hasLinks && !hasSelection) { + // menuItems.push({ onselect: 'core:split-wires-with-junctions', label: 'Insert junction'}) + // } + menuItems.push( + null, + { onselect: 'core:undo', disabled: RED.history.list().length === 0 }, + { onselect: 'core:redo', disabled: RED.history.listRedo().length === 0 }, + null, + { onselect: 'core:cut-selection-to-internal-clipboard', label: RED._("keyboard.cutNode"), disabled: !hasSelection}, + { onselect: 'core:copy-selection-to-internal-clipboard', label: RED._("keyboard.copyNode"), disabled: !hasSelection }, + { onselect: 'core:paste-from-internal-clipboard', label: RED._("keyboard.pasteNode"), disabled: !RED.view.clipboard() }, + { onselect: 'core:delete-selection', disabled: !canDelete }, + { onselect: 'core:show-export-dialog', label: RED._("menu.label.export") }, + { onselect: 'core:select-all-nodes' }, + ) + + if (hasSelection) { + menuItems.push( + null, + isGroup + ? { onselect: 'core:ungroup-selection', disabled: !isGroup } + : { onselect: 'core:group-selection', disabled: !hasSelection } + ) + if (canRemoveFromGroup) { + menuItems.push({ onselect: 'core:remove-selection-from-group', label: RED._("menu.label.groupRemoveSelection") }) + } + + } + const offset = $("#red-ui-workspace-chart").offset() + menu = RED.menu.init({ + direction: 'right', + onpreselect: function() { + disposeMenu() + }, + onpostselect: function() { + RED.view.focus() + }, + options: menuItems + }); + + menu.attr("id","red-ui-workspace-context-menu"); + menu.css({ + position: "absolute" + }) + menu.appendTo("body"); + + // TODO: prevent the menu from overflowing the window. + + var top = options.y + var left = options.x + + if (top+menu.height()-$(document).scrollTop() > $(window).height()) { + top -= (top+menu.height())-$(window).height() + 22; + } + if (left+menu.width()-$(document).scrollLeft() > $(window).width()) { + left -= (left+menu.width())-$(window).width() + 18; + } + menu.css({ + top: top+"px", + left: left+"px" + }) + $(".red-ui-menu.red-ui-menu-dropdown").hide(); + $(document).on("mousedown.red-ui-workspace-context-menu", function(evt) { + if (menu && menu[0].contains(evt.target)) { + return + } + disposeMenu() + }); + menu.show(); + + // menu.show({ + // target: $('#red-ui-main-container'), + // x: options.x, + // y: options.y + // }) + + + } + + return { + show: show + } +})() diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js index 699e5f222..888fb4d7f 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js @@ -336,17 +336,17 @@ RED.view.tools = (function() { } - function addNode() { - var selection = RED.view.selection(); - if (selection.nodes && selection.nodes.length === 1 && selection.nodes[0].outputs > 0) { - var selectedNode = selection.nodes[0]; - RED.view.showQuickAddDialog([ - selectedNode.x + selectedNode.w + 50,selectedNode.y - ]) - } else { - RED.view.showQuickAddDialog(); - } - } + // function addNode() { + // var selection = RED.view.selection(); + // if (selection.nodes && selection.nodes.length === 1 && selection.nodes[0].outputs > 0) { + // var selectedNode = selection.nodes[0]; + // RED.view.showQuickAddDialog([ + // selectedNode.x + selectedNode.w + 50,selectedNode.y + // ]) + // } else { + // RED.view.showQuickAddDialog(); + // } + // } function gotoNearestNode(direction) { @@ -815,6 +815,9 @@ RED.view.tools = (function() { */ function splitWiresWithLinkNodes(wires) { let wiresToSplit = wires || RED.view.selection().links; + if (!wiresToSplit) { + return + } if (!Array.isArray(wiresToSplit)) { wiresToSplit = [wiresToSplit]; } @@ -1047,6 +1050,135 @@ RED.view.tools = (function() { } } + function addJunctionsToWires(wires) { + let wiresToSplit = wires || RED.view.selection().links; + if (!wiresToSplit) { + return + } + if (!Array.isArray(wiresToSplit)) { + wiresToSplit = [wiresToSplit]; + } + if (wiresToSplit.length === 0) { + return; + } + + var removedLinks = new Set() + var addedLinks = [] + var addedJunctions = [] + + var groupedLinks = {} + wiresToSplit.forEach(function(l) { + var sourceId = l.source.id+":"+l.sourcePort + groupedLinks[sourceId] = groupedLinks[sourceId] || [] + groupedLinks[sourceId].push(l) + + groupedLinks[l.target.id] = groupedLinks[l.target.id] || [] + groupedLinks[l.target.id].push(l) + }); + var linkGroups = Object.keys(groupedLinks) + linkGroups.sort(function(A,B) { + return groupedLinks[B].length - groupedLinks[A].length + }) + linkGroups.forEach(function(gid) { + var links = groupedLinks[gid] + var junction = { + _def: {defaults:{}}, + type: 'junction', + z: RED.workspaces.active(), + id: RED.nodes.id(), + x: 0, + y: 0, + w: 0, h: 0, + outputs: 1, + inputs: 1, + dirty: true + } + links = links.filter(function(l) { return !removedLinks.has(l) }) + if (links.length === 0) { + return + } + let pointCount = 0 + links.forEach(function(l) { + if (l._sliceLocation) { + junction.x += l._sliceLocation.x + junction.y += l._sliceLocation.y + delete l._sliceLocation + pointCount++ + } else { + junction.x += l.source.x + l.source.w/2 + l.target.x - l.target.w/2 + junction.y += l.source.y + l.target.y + pointCount += 2 + } + }) + junction.x = Math.round(junction.x/pointCount) + junction.y = Math.round(junction.y/pointCount) + if (RED.view.snapGrid) { + let gridSize = RED.view.gridSize() + junction.x = (gridSize*Math.round(junction.x/gridSize)); + junction.y = (gridSize*Math.round(junction.y/gridSize)); + } + + var nodeGroups = new Set() + + RED.nodes.addJunction(junction) + addedJunctions.push(junction) + let newLink + if (gid === links[0].source.id+":"+links[0].sourcePort) { + newLink = { + source: links[0].source, + sourcePort: links[0].sourcePort, + target: junction + } + } else { + newLink = { + source: junction, + sourcePort: 0, + target: links[0].target + } + } + addedLinks.push(newLink) + RED.nodes.addLink(newLink) + links.forEach(function(l) { + removedLinks.add(l) + RED.nodes.removeLink(l) + let newLink + if (gid === l.target.id) { + newLink = { + source: l.source, + sourcePort: l.sourcePort, + target: junction + } + } else { + newLink = { + source: junction, + sourcePort: 0, + target: l.target + } + } + addedLinks.push(newLink) + RED.nodes.addLink(newLink) + nodeGroups.add(l.source.g || "__NONE__") + nodeGroups.add(l.target.g || "__NONE__") + }) + if (nodeGroups.size === 1) { + var group = nodeGroups.values().next().value + if (group !== "__NONE__") { + RED.group.addToGroup(RED.nodes.group(group), junction) + } + } + }) + if (addedJunctions.length > 0) { + RED.history.push({ + t: 'add', + links: addedLinks, + junctions: addedJunctions, + removedLinks: Array.from(removedLinks) + }) + RED.nodes.dirty(true) + } + RED.view.redraw(true); + } + return { init: function() { RED.actions.add("core:show-selected-node-labels", function() { setSelectedNodeLabelState(true); }) @@ -1109,6 +1241,7 @@ RED.view.tools = (function() { RED.actions.add("core:wire-node-to-multiple", function() { wireNodeToMultiple() }) RED.actions.add("core:split-wire-with-link-nodes", function () { splitWiresWithLinkNodes() }); + RED.actions.add("core:split-wires-with-junctions", function () { addJunctionsToWires() }); RED.actions.add("core:generate-node-names", generateNodeNames ) 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 5ac9525ff..9f28d7191 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 @@ -206,7 +206,15 @@ RED.view = (function() { function init() { chart = $("#red-ui-workspace-chart"); - + chart.on('contextmenu', function(evt) { + evt.preventDefault() + evt.stopPropagation() + RED.contextMenu.show({ + x:evt.clientX-5, + y:evt.clientY-5 + }) + return false + }) outer = d3.select("#red-ui-workspace-chart") .append("svg:svg") .attr("width", space_width) @@ -992,7 +1000,10 @@ RED.view = (function() { scroll_position = [chart.scrollLeft(),chart.scrollTop()]; return; } - if (!mousedown_node && !mousedown_link && !mousedown_group) { + if (d3.event.button === 2) { + return + } + if (!mousedown_node && !mousedown_link && !mousedown_group && !d3.event.shiftKey) { selectedLinks.clear(); updateSelection(); } @@ -1046,6 +1057,7 @@ RED.view = (function() { options = options || {}; var point = options.position || lastClickPosition; var spliceLink = options.splice; + var spliceMultipleLinks = options.spliceMultiple var targetGroup = options.group; var touchTrigger = options.touchTrigger; @@ -1058,6 +1070,10 @@ RED.view = (function() { var ox = point[0]; var oy = point[1]; + const offset = $("#red-ui-workspace-chart").offset() + var clientX = ox + offset.left + var clientY = oy + offset.top + if (RED.settings.get("editor").view['view-snap-grid']) { // eventLayer.append("circle").attr("cx",point[0]).attr("cy",point[1]).attr("r","2").attr('fill','red') point[0] = Math.round(point[0] / gridSize) * gridSize; @@ -1109,8 +1125,12 @@ RED.view = (function() { } hideDragLines(); } - if (spliceLink) { - filter = {input:true, output:true} + if (spliceLink || spliceMultipleLinks) { + filter = { + input:true, + output:true, + spliceMultiple: spliceMultipleLinks + } } var rebuildQuickAddLink = function() { @@ -1135,8 +1155,8 @@ RED.view = (function() { var lastAddedWidth; RED.typeSearch.show({ - x:d3.event.clientX-mainPos.left-node_width/2 - (ox-point[0]), - y:d3.event.clientY-mainPos.top+ node_height/2 + 5 - (oy-point[1]), + x:clientX-mainPos.left-node_width/2 - (ox-point[0]), + y:clientY-mainPos.top+ node_height/2 + 5 - (oy-point[1]), disableFocus: touchTrigger, filter: filter, move: function(dx,dy) { @@ -1164,7 +1184,7 @@ RED.view = (function() { hideDragLines(); redraw(); }, - add: function(type,keepAdding) { + add: function(type, keepAdding) { if (touchTrigger) { keepAdding = false; resetMouseVars(); @@ -1172,7 +1192,13 @@ RED.view = (function() { var nn; var historyEvent; - if (type === 'junction') { + if (/^_action_:/.test(type)) { + const actionName = type.substring(9) + quickAddActive = false; + ghostNode.remove(); + RED.actions.invoke(actionName) + return + } else if (type === 'junction') { nn = { _def: {defaults:{}}, type: 'junction', @@ -1844,8 +1870,20 @@ RED.view = (function() { } } }) - - + activeLinks.forEach(function(link) { + if (!link.selected) { + var sourceY = link.source.y + var targetY = link.target.y + var sourceX = link.source.x+(link.source.w/2) + 10 + var targetX = link.target.x-(link.target.w/2) - 10 + if ( + sourceX > x && sourceX < x2 && sourceY > y && sourceY < y2 && + targetX > x && targetX < x2 && targetY > y && targetY < y2 + ) { + selectedLinks.add(link); + } + } + }) // var selectionChanged = false; // do { @@ -1893,114 +1931,118 @@ RED.view = (function() { slicePath = null; RED.view.redraw(true); } else if (mouse_mode == RED.state.SLICING_JUNCTION) { - var removedLinks = new Set() - var addedLinks = [] - var addedJunctions = [] - - var groupedLinks = {} - selectedLinks.forEach(function(l) { - var sourceId = l.source.id+":"+l.sourcePort - groupedLinks[sourceId] = groupedLinks[sourceId] || [] - groupedLinks[sourceId].push(l) - - groupedLinks[l.target.id] = groupedLinks[l.target.id] || [] - groupedLinks[l.target.id].push(l) - }); - var linkGroups = Object.keys(groupedLinks) - linkGroups.sort(function(A,B) { - return groupedLinks[B].length - groupedLinks[A].length - }) - linkGroups.forEach(function(gid) { - var links = groupedLinks[gid] - var junction = { - _def: {defaults:{}}, - type: 'junction', - z: RED.workspaces.active(), - id: RED.nodes.id(), - x: 0, - y: 0, - w: 0, h: 0, - outputs: 1, - inputs: 1, - dirty: true - } - links = links.filter(function(l) { return !removedLinks.has(l) }) - if (links.length === 0) { - return - } - links.forEach(function(l) { - junction.x += l._sliceLocation.x - junction.y += l._sliceLocation.y - }) - junction.x = Math.round(junction.x/links.length) - junction.y = Math.round(junction.y/links.length) - if (snapGrid) { - junction.x = (gridSize*Math.round(junction.x/gridSize)); - junction.y = (gridSize*Math.round(junction.y/gridSize)); - } - - var nodeGroups = new Set() - - RED.nodes.addJunction(junction) - addedJunctions.push(junction) - let newLink - if (gid === links[0].source.id+":"+links[0].sourcePort) { - newLink = { - source: links[0].source, - sourcePort: links[0].sourcePort, - target: junction - } - } else { - newLink = { - source: junction, - sourcePort: 0, - target: links[0].target - } - } - addedLinks.push(newLink) - RED.nodes.addLink(newLink) - links.forEach(function(l) { - removedLinks.add(l) - RED.nodes.removeLink(l) - let newLink - if (gid === l.target.id) { - newLink = { - source: l.source, - sourcePort: l.sourcePort, - target: junction - } - } else { - newLink = { - source: junction, - sourcePort: 0, - target: l.target - } - } - addedLinks.push(newLink) - RED.nodes.addLink(newLink) - nodeGroups.add(l.source.g || "__NONE__") - nodeGroups.add(l.target.g || "__NONE__") - }) - if (nodeGroups.size === 1) { - var group = nodeGroups.values().next().value - if (group !== "__NONE__") { - RED.group.addToGroup(RED.nodes.group(group), junction) - } - } - }) + RED.actions.invoke("core:split-wires-with-junctions") slicePath.remove(); slicePath = null; - if (addedJunctions.length > 0) { - RED.history.push({ - t: 'add', - links: addedLinks, - junctions: addedJunctions, - removedLinks: Array.from(removedLinks) - }) - RED.nodes.dirty(true) - } - RED.view.redraw(true); + // var removedLinks = new Set() + // var addedLinks = [] + // var addedJunctions = [] + // + // var groupedLinks = {} + // selectedLinks.forEach(function(l) { + // var sourceId = l.source.id+":"+l.sourcePort + // groupedLinks[sourceId] = groupedLinks[sourceId] || [] + // groupedLinks[sourceId].push(l) + // + // groupedLinks[l.target.id] = groupedLinks[l.target.id] || [] + // groupedLinks[l.target.id].push(l) + // }); + // var linkGroups = Object.keys(groupedLinks) + // linkGroups.sort(function(A,B) { + // return groupedLinks[B].length - groupedLinks[A].length + // }) + // linkGroups.forEach(function(gid) { + // var links = groupedLinks[gid] + // var junction = { + // _def: {defaults:{}}, + // type: 'junction', + // z: RED.workspaces.active(), + // id: RED.nodes.id(), + // x: 0, + // y: 0, + // w: 0, h: 0, + // outputs: 1, + // inputs: 1, + // dirty: true + // } + // links = links.filter(function(l) { return !removedLinks.has(l) }) + // if (links.length === 0) { + // return + // } + // links.forEach(function(l) { + // junction.x += l._sliceLocation.x + // junction.y += l._sliceLocation.y + // }) + // junction.x = Math.round(junction.x/links.length) + // junction.y = Math.round(junction.y/links.length) + // if (snapGrid) { + // junction.x = (gridSize*Math.round(junction.x/gridSize)); + // junction.y = (gridSize*Math.round(junction.y/gridSize)); + // } + // + // var nodeGroups = new Set() + // + // RED.nodes.addJunction(junction) + // addedJunctions.push(junction) + // let newLink + // if (gid === links[0].source.id+":"+links[0].sourcePort) { + // newLink = { + // source: links[0].source, + // sourcePort: links[0].sourcePort, + // target: junction + // } + // } else { + // newLink = { + // source: junction, + // sourcePort: 0, + // target: links[0].target + // } + // } + // addedLinks.push(newLink) + // RED.nodes.addLink(newLink) + // links.forEach(function(l) { + // removedLinks.add(l) + // RED.nodes.removeLink(l) + // let newLink + // if (gid === l.target.id) { + // newLink = { + // source: l.source, + // sourcePort: l.sourcePort, + // target: junction + // } + // } else { + // newLink = { + // source: junction, + // sourcePort: 0, + // target: l.target + // } + // } + // addedLinks.push(newLink) + // RED.nodes.addLink(newLink) + // nodeGroups.add(l.source.g || "__NONE__") + // nodeGroups.add(l.target.g || "__NONE__") + // }) + // if (nodeGroups.size === 1) { + // var group = nodeGroups.values().next().value + // if (group !== "__NONE__") { + // RED.group.addToGroup(RED.nodes.group(group), junction) + // } + // } + // }) + // slicePath.remove(); + // slicePath = null; + // + // if (addedJunctions.length > 0) { + // RED.history.push({ + // t: 'add', + // links: addedLinks, + // junctions: addedJunctions, + // removedLinks: Array.from(removedLinks) + // }) + // RED.nodes.dirty(true) + // } + // RED.view.redraw(true); } if (mouse_mode == RED.state.MOVING_ACTIVE) { if (movingSet.length() > 0) { diff --git a/packages/node_modules/@node-red/editor-client/src/sass/dropdownMenu.scss b/packages/node_modules/@node-red/editor-client/src/sass/dropdownMenu.scss index 98ab3bd3b..4104fd83e 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/dropdownMenu.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/dropdownMenu.scss @@ -46,7 +46,7 @@ & > li > a, & > li > a:focus { display: block; - padding: 4px 12px 4px 32px; + padding: 4px 20px 4px 12px; clear: both; font-weight: normal; line-height: 20px; @@ -54,7 +54,10 @@ white-space: normal !important; outline: none; } - + & > li.pull-left > a, + & > li.pull-left > a:focus { + padding: 4px 12px 4px 32px; + } & > .active > a, & > .active > a:hover, & > .active > a:focus { @@ -145,8 +148,8 @@ position: relative; & > .red-ui-menu-dropdown { top: 0; - left: 100%; - margin-top: -6px; + left: calc(100% - 5px); + margin-top: 0; margin-left: -1px; } &.open > .red-ui-menu-dropdown, @@ -175,10 +178,10 @@ } } -.red-ui-menu-dropdown-submenu>a:after { +.red-ui-menu-dropdown-submenu.pull-left>a:after { display: none; } -.red-ui-menu-dropdown-submenu>a:before { +.red-ui-menu-dropdown-submenu.pull-left>a:before { display: block; float: left; width: 0; @@ -192,7 +195,25 @@ border-width: 5px 5px 5px 0; content: " "; } - +.red-ui-menu-dropdown-direction-right { + .red-ui-menu-dropdown-submenu>a:after { + display: none; + } + .red-ui-menu-dropdown-submenu>a:before { + display: block; + float: right; + width: 0; + height: 0; + margin-top: 5px; + margin-right: -15px; + /* Caret Arrow */ + border-color: transparent; + border-left-color: $menuCaret; + border-style: solid; + border-width: 5px 0 5px 5px; + content: " "; + } +} .red-ui-menu-dropdown-submenu.disabled > a:before { border-right-color: $menuCaret; } From ad32677263c3219161f0382973df9be550f52f13 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 16 Jun 2022 14:37:48 +0100 Subject: [PATCH 14/17] Fix lint errors in contextMenu --- .../@node-red/editor-client/src/js/ui/contextMenu.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js b/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js index 1a0dee2bb..379ed5433 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js @@ -103,14 +103,14 @@ RED.contextMenu = (function() { { onselect: 'core:paste-from-internal-clipboard', label: RED._("keyboard.pasteNode"), disabled: !RED.view.clipboard() }, { onselect: 'core:delete-selection', disabled: !canDelete }, { onselect: 'core:show-export-dialog', label: RED._("menu.label.export") }, - { onselect: 'core:select-all-nodes' }, + { onselect: 'core:select-all-nodes' } ) if (hasSelection) { menuItems.push( null, - isGroup - ? { onselect: 'core:ungroup-selection', disabled: !isGroup } + isGroup ? + { onselect: 'core:ungroup-selection', disabled: !isGroup } : { onselect: 'core:group-selection', disabled: !hasSelection } ) if (canRemoveFromGroup) { From 1780cb9b91a3ac0d954cf2f5ddb821f82f5535a4 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 16 Jun 2022 14:47:11 +0100 Subject: [PATCH 15/17] Update dependencies --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index d22cf89dd..87518820e 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "hash-sum": "2.0.0", "hpagent": "1.0.0", "https-proxy-agent": "5.0.1", - "i18next": "21.8.2", + "i18next": "21.8.10", "iconv-lite": "0.6.3", "is-utf8": "0.2.1", "js-yaml": "4.1.0", @@ -62,7 +62,7 @@ "moment": "2.29.3", "moment-timezone": "0.5.34", "mqtt": "4.3.7", - "multer": "1.4.4", + "multer": "1.4.5-lts.1", "mustache": "4.2.0", "node-red-admin": "^3.0.0", "node-watch": "0.7.3", @@ -76,7 +76,7 @@ "semver": "7.3.7", "tar": "6.1.11", "tough-cookie": "4.0.0", - "uglify-js": "3.15.5", + "uglify-js": "3.16.0", "uuid": "8.3.2", "ws": "7.5.6", "xml2js": "0.4.23" @@ -105,16 +105,16 @@ "grunt-sass": "~3.1.0", "grunt-simple-mocha": "~0.4.1", "grunt-simple-nyc": "^3.0.1", - "i18next-http-backend": "1.4.0", + "i18next-http-backend": "1.4.1", "jquery-i18next": "1.2.1", "jsdoc-nr-template": "github:node-red/jsdoc-nr-template", - "marked": "4.0.15", + "marked": "4.0.17", "minami": "1.2.3", "mocha": "9.2.2", "node-red-node-test-helper": "^0.2.7", "nodemon": "2.0.16", "proxy": "^1.0.2", - "sass": "1.51.0", + "sass": "1.52.3", "should": "13.2.3", "sinon": "11.1.2", "stoppable": "^1.1.0", From 16644284296faf6aea7108533cf430bde41c42f2 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 16 Jun 2022 15:37:07 +0100 Subject: [PATCH 16/17] Update tour for 3.0-beta.3 --- .../src/tours/images/context-menu.png | Bin 0 -> 67976 bytes .../editor-client/src/tours/welcome.js | 35 ++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 packages/node_modules/@node-red/editor-client/src/tours/images/context-menu.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/context-menu.png b/packages/node_modules/@node-red/editor-client/src/tours/images/context-menu.png new file mode 100644 index 0000000000000000000000000000000000000000..1acaab48b867480c8927079552f7e9431374573d GIT binary patch literal 67976 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≧h`;Ao2cW=E&fS`~X;_=(6n8!UyaT&;l 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 literal 0 HcmV?d00001 diff --git a/packages/node_modules/@node-red/editor-client/src/tours/welcome.js b/packages/node_modules/@node-red/editor-client/src/tours/welcome.js index 219aaec7b..4b6f5f66d 100644 --- a/packages/node_modules/@node-red/editor-client/src/tours/welcome.js +++ b/packages/node_modules/@node-red/editor-client/src/tours/welcome.js @@ -1,15 +1,27 @@ export default { - version: "3.0.0-beta.1", + version: "3.0.0-beta.3", steps: [ { titleIcon: "fa fa-map-o", title: { - "en-US": "Welcome to Node-RED 3.0 Beta 1!", - "ja": "Node-RED 3.0 ベータ1へようこそ!" + "en-US": "Welcome to Node-RED 3.0 Beta 3!", + "ja": "Node-RED 3.0 ベータ3へようこそ!" }, description: { - "en-US": "

      This is the first Beta release of Node-RED 3.0. It contains just about everything we have planned for the final release.

      Let's take a moment to discover the new features in this release.

      ", - "ja": "

      これはNode-RED 3.0の最初のベータリリースです。これには、最終リリースで計画しているほぼ全ての機能が含まれています。

      本リリースの新機能を見つけてみましょう。

      " + "en-US": "

      This is the final beta release of Node-RED 3.0.

      Let's take a moment to discover the new features in this release.

      ", + // "ja": "

      これはNode-RED 3.0の最初のベータリリースです。これには、最終リリースで計画しているほぼ全ての機能が含まれています。

      本リリースの新機能を見つけてみましょう。

      " + } + }, + { + title: { + "en-US": "Context Menu" + }, + image: 'images/context-menu.png', + description: { + "en-US": `

      The editor now has its own context menu when you + right-click in the workspace.

      +

      This makes many of the built-in actions much easier + to access.

      ` } }, { @@ -19,12 +31,13 @@ export default { }, image: 'images/junction-slice.gif', description: { - "en-US": `

      To make it easier to route wires around your flows, it is now possible to - add junction nodes that give you more control.

      -

      Junctions can be added to wires by holding the Shift key, then click and drag with - the right-hand mouse button across the wires.

      `, - "ja": `

      フローのワイヤーの経路をより制御しやすくするために、分岐点ノードを追加できるようになりました。

      -

      シフトキーを押しながら、マウスの右ボタンをクリックし、ワイヤーを横切るようにドラッグすることで、分岐点を追加できます。

      ` + "en-US": `

      To make it easier to route wires around your flows, + it is now possible to add junction nodes that give + you more control.

      +

      Junctions can be added to wires by holding the Alt key + then click and drag the mouse across the wires.

      `, + // "ja": `

      フローのワイヤーの経路をより制御しやすくするために、分岐点ノードを追加できるようになりました。

      + //

      シフトキーを押しながら、マウスの右ボタンをクリックし、ワイヤーを横切るようにドラッグすることで、分岐点を追加できます。

      ` }, }, { From ce529a9b9f3bbd9c8c71a2450191525813ea94ca Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 16 Jun 2022 15:39:42 +0100 Subject: [PATCH 17/17] Update module dependencies --- packages/node_modules/@node-red/editor-api/package.json | 2 +- packages/node_modules/@node-red/nodes/package.json | 2 +- packages/node_modules/@node-red/registry/package.json | 2 +- packages/node_modules/@node-red/util/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/node_modules/@node-red/editor-api/package.json b/packages/node_modules/@node-red/editor-api/package.json index 793c5e516..769eb8f5a 100644 --- a/packages/node_modules/@node-red/editor-api/package.json +++ b/packages/node_modules/@node-red/editor-api/package.json @@ -26,7 +26,7 @@ "express": "4.18.1", "memorystore": "1.6.7", "mime": "3.0.0", - "multer": "1.4.4", + "multer": "1.4.5-lts.1", "mustache": "4.2.0", "oauth2orize": "1.11.1", "passport-http-bearer": "1.0.1", diff --git a/packages/node_modules/@node-red/nodes/package.json b/packages/node_modules/@node-red/nodes/package.json index 2a0d7e7c1..8c9d5adf7 100644 --- a/packages/node_modules/@node-red/nodes/package.json +++ b/packages/node_modules/@node-red/nodes/package.json @@ -36,7 +36,7 @@ "js-yaml": "4.1.0", "media-typer": "1.1.0", "mqtt": "4.3.7", - "multer": "1.4.4", + "multer": "1.4.5-lts.1", "mustache": "4.2.0", "node-watch": "0.7.3", "on-headers": "1.0.2", diff --git a/packages/node_modules/@node-red/registry/package.json b/packages/node_modules/@node-red/registry/package.json index 29e1b8dd6..b13fefc9f 100644 --- a/packages/node_modules/@node-red/registry/package.json +++ b/packages/node_modules/@node-red/registry/package.json @@ -21,6 +21,6 @@ "fs-extra": "10.1.0", "semver": "7.3.7", "tar": "6.1.11", - "uglify-js": "3.15.5" + "uglify-js": "3.16.0" } } diff --git a/packages/node_modules/@node-red/util/package.json b/packages/node_modules/@node-red/util/package.json index 302e6f84e..a75ecd7ff 100644 --- a/packages/node_modules/@node-red/util/package.json +++ b/packages/node_modules/@node-red/util/package.json @@ -16,7 +16,7 @@ ], "dependencies": { "fs-extra": "10.1.0", - "i18next": "21.8.2", + "i18next": "21.8.10", "json-stringify-safe": "5.0.1", "jsonata": "1.8.6", "lodash.clonedeep": "^4.5.0",