From 468cfeffb6758d0397a3e6e5ca86f9eed27c115f Mon Sep 17 00:00:00 2001 From: martinb Date: Tue, 3 Nov 2020 09:35:21 +0100 Subject: [PATCH 01/39] make split node work with out of order messages as long as one of the messages has msg.parts.count set to the proper value --- .../node_modules/@node-red/nodes/core/sequence/17-split.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/nodes/core/sequence/17-split.js b/packages/node_modules/@node-red/nodes/core/sequence/17-split.js index 3f774d68c..fbbae8048 100644 --- a/packages/node_modules/@node-red/nodes/core/sequence/17-split.js +++ b/packages/node_modules/@node-red/nodes/core/sequence/17-split.js @@ -677,7 +677,10 @@ module.exports = function(RED) { } group.msg = Object.assign(group.msg, msg); var tcnt = group.targetCount; - if (msg.hasOwnProperty("parts")) { tcnt = group.targetCount || msg.parts.count; } + if (msg.hasOwnProperty("parts")) { + tcnt = group.targetCount || msg.parts.count; + group.targetCount = tcnt; + } if ((tcnt > 0 && group.currentCount >= tcnt) || msg.hasOwnProperty('complete')) { completeSend(partId); } @@ -697,3 +700,4 @@ module.exports = function(RED) { } RED.nodes.registerType("join",JoinNode); } + From 01b67c692b081921e51ed9d52fedce8314c15fd1 Mon Sep 17 00:00:00 2001 From: martinb Date: Thu, 12 Nov 2020 18:51:14 +0100 Subject: [PATCH 02/39] add test case for support of out of order messages support in auto mode of join node if exactly one message has count set --- test/nodes/core/sequence/17-split_spec.js | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/nodes/core/sequence/17-split_spec.js b/test/nodes/core/sequence/17-split_spec.js index 2c45f0e02..8369a70a6 100644 --- a/test/nodes/core/sequence/17-split_spec.js +++ b/test/nodes/core/sequence/17-split_spec.js @@ -1646,4 +1646,40 @@ describe('JOIN node', function() { }); }); + it('should handle msg.parts even if messages are out of order in auto mode if exactly one message has count set', function (done) { + var flow = [{ id: "n1", type: "join", wires: [["n2"]], joiner: "[44]", joinerType: "bin", build: "string", mode: "auto" }, + { id: "n2", type: "helper" }]; + helper.load(joinNode, flow, function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + + n2.on("input", function (msg) { + msg.payload.length.should.be.eql(5); + msg.payload.should.be.eql([0,1,2,3,4]); + done(); + }); + + var msg = {}; + msg.parts = { + id: RED.util.generateId() + }; + for(var elem = 1; elem < 5; ++elem) { + var _msg = RED.util.cloneMessage(msg); + _msg.parts.index = elem; + if(elem == 4) { + _msg.parts.count = 5; + } + _msg.payload = elem; + n1.receive(_msg); + } + + var _msg = RED.util.cloneMessage(msg); + delete _msg.parts.count; + _msg.parts.index = 0; + _msg.payload = 0; + n1.receive(_msg); + }); + + }) + }); From ccf4e737014c30134ea41d6d876edd8f72989b26 Mon Sep 17 00:00:00 2001 From: martinb Date: Thu, 12 Nov 2020 18:56:43 +0100 Subject: [PATCH 03/39] cleanup test case for support of out of order messages --- test/nodes/core/sequence/17-split_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/nodes/core/sequence/17-split_spec.js b/test/nodes/core/sequence/17-split_spec.js index 8369a70a6..62db4957d 100644 --- a/test/nodes/core/sequence/17-split_spec.js +++ b/test/nodes/core/sequence/17-split_spec.js @@ -1647,7 +1647,7 @@ describe('JOIN node', function() { }); it('should handle msg.parts even if messages are out of order in auto mode if exactly one message has count set', function (done) { - var flow = [{ id: "n1", type: "join", wires: [["n2"]], joiner: "[44]", joinerType: "bin", build: "string", mode: "auto" }, + var flow = [{ id: "n1", type: "join", wires: [["n2"]], mode: "auto" }, { id: "n2", type: "helper" }]; helper.load(joinNode, flow, function () { var n1 = helper.getNode("n1"); From ea2e3f25d8d505a939d97a1d65535e1db9153bd3 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 8 Jan 2021 14:19:12 +0000 Subject: [PATCH 04/39] Implement node property typing See https://github.com/node-red/designs/pull/37 --- .../@node-red/editor-client/src/js/history.js | 30 ++-- .../@node-red/editor-client/src/js/nodes.js | 133 ++++++++++++++---- .../@node-red/editor-client/src/js/red.js | 1 + .../editor-client/src/js/ui/editor.js | 18 +-- .../editor-client/src/js/ui/tab-info.js | 2 +- .../editor-client/src/sass/palette.scss | 4 +- .../nodes/core/common/24-complete.html | 2 +- .../@node-red/nodes/core/common/25-catch.html | 2 +- .../nodes/core/common/25-status.html | 2 +- .../@node-red/nodes/core/common/60-link.html | 4 +- 10 files changed, 145 insertions(+), 53 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/history.js b/packages/node_modules/@node-red/editor-client/src/js/history.js index a3392fd26..7fedf84be 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/history.js +++ b/packages/node_modules/@node-red/editor-client/src/js/history.js @@ -343,17 +343,29 @@ RED.history = (function() { if (ev.changes.hasOwnProperty(i)) { inverseEv.changes[i] = ev.node[i]; if (ev.node._def.defaults && ev.node._def.defaults[i] && ev.node._def.defaults[i].type) { - // This is a config node property - var currentConfigNode = RED.nodes.node(ev.node[i]); - if (currentConfigNode) { - currentConfigNode.users.splice(currentConfigNode.users.indexOf(ev.node),1); - RED.events.emit("nodes:change",currentConfigNode); + // This property is a reference to another node or nodes. + var nodeList = ev.node[i]; + if (!Array.isArray(nodeList)) { + nodeList = [nodeList]; } - var newConfigNode = RED.nodes.node(ev.changes[i]); - if (newConfigNode) { - newConfigNode.users.push(ev.node); - RED.events.emit("nodes:change",newConfigNode); + nodeList.forEach(function(id) { + var currentConfigNode = RED.nodes.node(id); + if (currentConfigNode && currentConfigNode._def.category === "config") { + currentConfigNode.users.splice(currentConfigNode.users.indexOf(ev.node),1); + RED.events.emit("nodes:change",currentConfigNode); + } + }); + nodeList = ev.changes[i]; + if (!Array.isArray(nodeList)) { + nodeList = [nodeList]; } + nodeList.forEach(function(id) { + var newConfigNode = RED.nodes.node(id); + if (newConfigNode && newConfigNode._def.category === "config") { + newConfigNode.users.push(ev.node); + RED.events.emit("nodes:change",newConfigNode); + } + }); } ev.node[i] = ev.changes[i]; } diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index 0d664ce3c..9e5464354 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -164,6 +164,21 @@ RED.nodes = (function() { // TODO: too tightly coupled into palette UI } + if (def.defaults) { + for (var d in def.defaults) { + if (def.defaults.hasOwnProperty(d)) { + if (def.defaults[d].type) { + try { + def.defaults[d]._type = parseNodePropertyTypeString(def.defaults[d].type) + } catch(err) { + console.warn(err); + } + } + } + } + } + + RED.events.emit("registry:node-type-added",nt); }, removeNodeType: function(nt) { @@ -193,6 +208,59 @@ RED.nodes = (function() { return (1+Math.random()*4294967295).toString(16); } + function parseNodePropertyTypeString(typeString) { + typeString = typeString.trim(); + var c; + var pos = 0; + var isArray = /\[\]$/.test(typeString); + if (isArray) { + typeString = typeString.substring(0,typeString.length-2); + } + + var l = typeString.length; + var inBrackets = false; + var inToken = false; + var currentToken = ""; + var types = []; + while (pos < l) { + c = typeString[pos]; + if (inToken) { + if (c === "|") { + types.push(currentToken.trim()) + currentToken = ""; + inToken = false; + } else if (c === ")") { + types.push(currentToken.trim()) + currentToken = ""; + inBrackets = false; + inToken = false; + } else { + currentToken += c; + } + } else { + if (c === "(") { + if (inBrackets) { + throw new Error("Invalid character '"+c+"' at position "+pos) + } + inBrackets = true; + } else if (c !== " ") { + inToken = true; + currentToken = c; + } + } + pos++; + } + currentToken = currentToken.trim(); + if (currentToken.length > 0) { + types.push(currentToken) + } + return { + types: types, + array: isArray + } + } + + function addNode(n) { if (n.type.indexOf("subflow") !== 0) { n["_"] = n._def._; @@ -787,16 +855,29 @@ RED.nodes = (function() { if (node.type !== "subflow") { var convertedNode = RED.nodes.convertNode(node); for (var d in node._def.defaults) { - if (node._def.defaults[d].type && node[d] in configNodes) { - var confNode = configNodes[node[d]]; - var exportable = registry.getNodeType(node._def.defaults[d].type).exportable; - if ((exportable == null || exportable)) { - if (!(node[d] in exportedConfigNodes)) { - exportedConfigNodes[node[d]] = true; - set.push(confNode); + if (node._def.defaults[d].type) { + var nodeList = node[d]; + if (!Array.isArray(nodeList)) { + nodeList = [nodeList]; + } + nodeList = nodeList.filter(function(id) { + if (id in configNodes) { + var confNode = configNodes[id]; + if (confNode._def.exportable !== false) { + if (!(id in exportedConfigNodes)) { + exportedConfigNodes[id] = true; + set.push(confNode); + return true; + } + } + return false; } + return true; + }) + if (nodeList.length === 0) { + convertedNode[d] = Array.isArray(node[d])?[]:"" } else { - convertedNode[d] = ""; + convertedNode[d] = Array.isArray(node[d])?nodeList:nodeList[0] } } } @@ -1587,15 +1668,6 @@ RED.nodes = (function() { } } } - // TODO: make this a part of the node definition so it doesn't have to - // be hardcoded here - var nodeTypeArrayReferences = { - "catch":"scope", - "status":"scope", - "complete": "scope", - "link in":"links", - "link out":"links" - } // Remap all wires and config node references for (i=0;i').appendTo(tableBody); $(propRow.children()[0]).text(n); - if (defaults[n].type) { + if (defaults[n].type && !defaults[n]._type.array) { var configNode = RED.nodes.node(val); if (!configNode) { RED.utils.createObjectElement(undefined).appendTo(propRow.children()[1]); diff --git a/packages/node_modules/@node-red/editor-client/src/sass/palette.scss b/packages/node_modules/@node-red/editor-client/src/sass/palette.scss index 7a2b53eb2..385ad761f 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/palette.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/palette.scss @@ -131,10 +131,10 @@ width: 120px; background-size: contain; position: relative; - &:not(.red-ui-palette-node-config):first-child { + &:not(.red-ui-palette-node-config):not(.red-ui-palette-node-small):first-child { margin-top: 15px; } - &:not(.red-ui-palette-node-config):last-child { + &:not(.red-ui-palette-node-config):not(.red-ui-palette-node-small):first-child { margin-bottom: 15px; } } diff --git a/packages/node_modules/@node-red/nodes/core/common/24-complete.html b/packages/node_modules/@node-red/nodes/core/common/24-complete.html index 9c49648e3..a6a7a2a45 100644 --- a/packages/node_modules/@node-red/nodes/core/common/24-complete.html +++ b/packages/node_modules/@node-red/nodes/core/common/24-complete.html @@ -18,7 +18,7 @@ color:"#c0edc0", defaults: { name: {value:""}, - scope: {value:[]}, + scope: {value:[], type:"*[]"}, uncaught: {value:false} }, inputs:0, diff --git a/packages/node_modules/@node-red/nodes/core/common/25-catch.html b/packages/node_modules/@node-red/nodes/core/common/25-catch.html index 1fa94258f..0b976ea78 100644 --- a/packages/node_modules/@node-red/nodes/core/common/25-catch.html +++ b/packages/node_modules/@node-red/nodes/core/common/25-catch.html @@ -30,7 +30,7 @@ color:"#e49191", defaults: { name: {value:""}, - scope: {value:null}, + scope: {value:null, type:"*[]"}, uncaught: {value:false} }, inputs:0, diff --git a/packages/node_modules/@node-red/nodes/core/common/25-status.html b/packages/node_modules/@node-red/nodes/core/common/25-status.html index dc2f8893d..47a3192e4 100644 --- a/packages/node_modules/@node-red/nodes/core/common/25-status.html +++ b/packages/node_modules/@node-red/nodes/core/common/25-status.html @@ -26,7 +26,7 @@ color:"#94c1d0", defaults: { name: {value:""}, - scope: {value:null} + scope: {value:null, type:"*[]"} }, inputs:0, outputs:1, diff --git a/packages/node_modules/@node-red/nodes/core/common/60-link.html b/packages/node_modules/@node-red/nodes/core/common/60-link.html index 7953c9979..c9207a345 100644 --- a/packages/node_modules/@node-red/nodes/core/common/60-link.html +++ b/packages/node_modules/@node-red/nodes/core/common/60-link.html @@ -187,7 +187,7 @@ color:"#ddd",//"#87D8CF", defaults: { name: {value:""}, - links: { value: [] } + links: { value: [], type:"link out[]" } }, inputs:0, outputs:1, @@ -216,7 +216,7 @@ color:"#ddd",//"#87D8CF", defaults: { name: {value:""}, - links: { value: []} + links: { value: [], type:"link in[]"} }, align:"right", inputs:1, From b1df6d5149a1ea824f5907badff5770623e930de Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 12 Jan 2021 18:23:15 +0000 Subject: [PATCH 05/39] Add preview of exported nodes to Export dialog --- .../editor-client/src/js/ui/clipboard.js | 158 ++++++++++++++++-- .../editor-client/src/sass/library.scss | 24 ++- .../editor-client/src/sass/tab-info.scss | 2 +- 3 files changed, 165 insertions(+), 19 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js index e46c88b4f..4ed618f6d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js @@ -72,9 +72,7 @@ RED.clipboard = (function() { text: RED._("clipboard.export.copy"), click: function() { if (activeTab === "red-ui-clipboard-dialog-export-tab-clipboard") { - $("#red-ui-clipboard-dialog-export-text").select(); - document.execCommand("copy"); - document.getSelection().removeAllRanges(); + copyText($("#red-ui-clipboard-dialog-export-text").val()); RED.notify(RED._("clipboard.nodesExported"),{id:"clipboard"}); $( this ).dialog( "close" ); } else { @@ -222,14 +220,22 @@ RED.clipboard = (function() { ''+ '
'+ '
'+ - '
'+ - ''+ + '
'+ + '
    '+ '
    '+ - '
    '+ - ''+ - ''+ - ''+ - ''+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + ''+ + '
    '+ + '
    '+ + ''+ + ''+ + ''+ + ''+ + '
    '+ '
    '+ '
    '+ '
    '+ @@ -592,6 +598,30 @@ RED.clipboard = (function() { }) loadFlowLibrary(libraryBrowser,"local",RED._("library.types.local")); + var clipboardTabs = RED.tabs.create({ + id: "red-ui-clipboard-dialog-export-tab-clipboard-tabs", + onchange: function(tab) { + $(".red-ui-clipboard-dialog-export-tab-clipboard-tab").hide(); + $("#" + tab.id).show(); + } + }); + + clipboardTabs.addTab({ + id: "red-ui-clipboard-dialog-export-tab-clipboard-preview", + label: RED._("clipboard.exportNodes") + }); + + clipboardTabs.addTab({ + id: "red-ui-clipboard-dialog-export-tab-clipboard-json", + label: RED._("editor.types.json") + }); + + + var previewList = $("#red-ui-clipboard-dialog-export-tab-clipboard-preview-list").css({position:"absolute",top:0,right:0,bottom:0,left:0}).treeList({ + data: [] + }) + refreshExportPreview(); + $("#red-ui-clipboard-dialog-tab-library-name").val("flows.json").select(); dialogContainer.i18n(); @@ -630,10 +660,10 @@ RED.clipboard = (function() { } $(this).parent().children().removeClass('selected'); $(this).addClass('selected'); - var type = $(this).attr('id'); + var type = $(this).attr('id').substring("red-ui-clipboard-dialog-export-rng-".length); var flow = ""; var nodes = null; - if (type === 'red-ui-clipboard-dialog-export-rng-selected') { + if (type === 'selected') { var selection = RED.workspaces.selection(); if (selection.length > 0) { nodes = []; @@ -647,14 +677,14 @@ RED.clipboard = (function() { } // Don't include the subflow meta-port nodes in the exported selection nodes = RED.nodes.createExportableNodeSet(nodes.filter(function(n) { return n.type !== 'subflow'})); - } else if (type === 'red-ui-clipboard-dialog-export-rng-flow') { + } else if (type === 'flow') { var activeWorkspace = RED.workspaces.active(); nodes = RED.nodes.groups(activeWorkspace); nodes = nodes.concat(RED.nodes.filterNodes({z:activeWorkspace})); var parentNode = RED.nodes.workspace(activeWorkspace)||RED.nodes.subflow(activeWorkspace); nodes.unshift(parentNode); nodes = RED.nodes.createExportableNodeSet(nodes); - } else if (type === 'red-ui-clipboard-dialog-export-rng-full') { + } else if (type === 'full') { nodes = RED.nodes.createCompleteNodeSet(false); } if (nodes !== null) { @@ -670,8 +700,10 @@ RED.clipboard = (function() { $("#red-ui-clipboard-dialog-export").addClass('disabled'); } $("#red-ui-clipboard-dialog-export-text").val(flow); - setTimeout(function() { $("#red-ui-clipboard-dialog-export-text").scrollTop(0); },50); - $("#red-ui-clipboard-dialog-export-text").trigger("focus"); + setTimeout(function() { + $("#red-ui-clipboard-dialog-export-text").scrollTop(0); + refreshExportPreview(type); + },50); }) $("#red-ui-clipboard-dialog-ok").hide(); @@ -717,6 +749,93 @@ RED.clipboard = (function() { } + function refreshExportPreview(type) { + + var flowData = $("#red-ui-clipboard-dialog-export-text").val() || "[]"; + var flow = JSON.parse(flowData); + var flows = {}; + var subflows = {}; + var nodes = []; + var nodesByZ = {}; + + var treeFlows = []; + var treeSubflows = []; + + flow.forEach(function(node) { + if (node.type === "tab") { + flows[node.id] = { + element: getFlowLabel(node,false), + deferBuild: type !== "flow", + expanded: type === "flow", + children: [] + }; + treeFlows.push(flows[node.id]) + } else if (node.type === "subflow") { + subflows[node.id] = { + element: getNodeLabel(node,false), + deferBuild: true, + children: [] + }; + treeSubflows.push(subflows[node.id]) + } else { + nodes.push(node); + } + }); + + var globalNodes = []; + var parentlessNodes = []; + + nodes.forEach(function(node) { + var treeNode = { + element: getNodeLabel(node, false, false) + }; + if (node.z) { + if (!flows[node.z] && !subflows[node.z]) { + parentlessNodes.push(treeNode) + } else if (flows[node.z]) { + flows[node.z].children.push(treeNode) + } else if (subflows[node.z]) { + subflows[node.z].children.push(treeNode) + } + } else { + globalNodes.push(treeNode); + } + }); + var treeData = []; + + if (parentlessNodes.length > 0) { + treeData = treeData.concat(parentlessNodes); + } + if (type === "flow") { + treeData = treeData.concat(treeFlows); + } else if (treeFlows.length > 0) { + treeData.push({ + label: RED._("menu.label.flows"), + deferBuild: treeFlows.length > 20, + expanded: treeFlows.length <= 20, + children: treeFlows + }) + } + if (treeSubflows.length > 0) { + treeData.push({ + label: RED._("menu.label.subflows"), + deferBuild: treeSubflows.length > 10, + expanded: treeSubflows.length <= 10, + children: treeSubflows + }) + } + if (globalNodes.length > 0) { + treeData.push({ + label: RED._("sidebar.info.globalConfig"), + deferBuild: globalNodes.length > 10, + expanded: globalNodes.length <= 10, + children: globalNodes + }) + } + + $("#red-ui-clipboard-dialog-export-tab-clipboard-preview-list").treeList('data',treeData); + } + function loadFlowLibrary(browser,library,label) { // if (includeExamples) { // listing.push({ @@ -756,6 +875,7 @@ RED.clipboard = (function() { } function copyText(value,element,msg) { var truncated = false; + var currentFocus = document.activeElement; if (typeof value !== "string" ) { value = JSON.stringify(value, function(key,value) { if (value !== null && typeof value === 'object') { @@ -787,7 +907,7 @@ RED.clipboard = (function() { if (truncated) { msg += "_truncated"; } - $("#red-ui-clipboard-hidden").val(value).select(); + $("#red-ui-clipboard-hidden").val(value).focus().select(); var result = document.execCommand("copy"); if (result && element) { var popover = RED.popover.create({ @@ -801,6 +921,10 @@ RED.clipboard = (function() { },1000); popover.open(); } + $("#red-ui-clipboard-hidden").val(""); + if (currentFocus) { + $(currentFocus).focus(); + } return result; } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/library.scss b/packages/node_modules/@node-red/editor-client/src/sass/library.scss index 26b17f38e..89109f357 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/library.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/library.scss @@ -29,8 +29,30 @@ } } } -.red-ui-clipboard-dialog-tab-clipboard { + +#red-ui-clipboard-dialog-export-tab-clipboard-preview { + .red-ui-treeList-container,.red-ui-editableList-border { + border: none; + border-radius: 0; + } +} +#red-ui-clipboard-dialog-export-tab-clipboard-json { + padding: 10px 10px 0; +} +#red-ui-clipboard-dialog-import-tab-clipboard { padding: 10px; +} +.red-ui-clipboard-dialog-export-tab-clipboard-tab { + position: absolute; + top: 40px; + right: 0; + left: 0; + bottom: 0; +} + +.red-ui-clipboard-dialog-tab-clipboard { + + textarea { resize: none; width: 100%; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/tab-info.scss b/packages/node_modules/@node-red/editor-client/src/sass/tab-info.scss index c28c0acfa..42d5462f7 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/tab-info.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/tab-info.scss @@ -326,7 +326,7 @@ div.red-ui-info-table { border-bottom: 1px solid $secondary-border-color; } } -.red-ui-info-outline,.red-ui-sidebar-help-toc, #red-ui-clipboard-dialog-import-conflicts-list { +.red-ui-info-outline,.red-ui-sidebar-help-toc, #red-ui-clipboard-dialog-import-conflicts-list, #red-ui-clipboard-dialog-export-tab-clipboard-preview { .red-ui-info-outline-item { display: inline-block; padding: 0; From aa47bae2adc347152e170f231d154613c97f65b1 Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Wed, 13 Jan 2021 10:12:19 +0000 Subject: [PATCH 06/39] Exec node - don't append msg.payload to command by default (#2818) * exec change default to not append payload --- .../@node-red/nodes/core/function/90-exec.html | 2 +- .../node_modules/@node-red/nodes/core/function/90-exec.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/function/90-exec.html b/packages/node_modules/@node-red/nodes/core/function/90-exec.html index 636117d71..789e15f75 100644 --- a/packages/node_modules/@node-red/nodes/core/function/90-exec.html +++ b/packages/node_modules/@node-red/nodes/core/function/90-exec.html @@ -52,7 +52,7 @@ color:"darksalmon", defaults: { command: {value:""}, - addpay: {value:true}, + addpay: {value:false}, append: {value:""}, useSpawn: {value:"false"}, timer: {value:""}, diff --git a/packages/node_modules/@node-red/nodes/core/function/90-exec.js b/packages/node_modules/@node-red/nodes/core/function/90-exec.js index 0df3e6012..a92bab9af 100644 --- a/packages/node_modules/@node-red/nodes/core/function/90-exec.js +++ b/packages/node_modules/@node-red/nodes/core/function/90-exec.js @@ -31,12 +31,12 @@ module.exports = function(RED) { this.timer = Number(n.timer || 0)*1000; this.activeProcesses = {}; this.oldrc = (n.oldrc || false).toString(); - this.execOpt = {encoding:'binary', maxBuffer:10000000}; + this.execOpt = {encoding:'binary', maxBuffer:10000000}; var node = this; - if (process.platform === 'linux' && fs.existsSync('/bin/bash')) { node.execOpt.shell = '/bin/bash'; } - - var cleanup = function(p) { + if (process.platform === 'linux' && fs.existsSync('/bin/bash')) { node.execOpt.shell = '/bin/bash'; } + + var cleanup = function(p) { node.activeProcesses[p].kill(); //node.status({fill:"red",shape:"dot",text:"timeout"}); //node.error("Exec node timeout"); From 2b28ae340283bbdda692ace2c2b1bbd1c497ff43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nathana=C3=ABl=20L=C3=A9caud=C3=A9?= Date: Thu, 14 Jan 2021 09:38:39 -0500 Subject: [PATCH 07/39] Add settings.execMaxBufferSize to control buffer size of exec node (#2819) Co-authored-by: Dave Conway-Jones closes #2817 --- .../node_modules/@node-red/nodes/core/function/90-exec.js | 2 +- packages/node_modules/node-red/settings.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/nodes/core/function/90-exec.js b/packages/node_modules/@node-red/nodes/core/function/90-exec.js index a92bab9af..132286cb4 100644 --- a/packages/node_modules/@node-red/nodes/core/function/90-exec.js +++ b/packages/node_modules/@node-red/nodes/core/function/90-exec.js @@ -31,7 +31,7 @@ module.exports = function(RED) { this.timer = Number(n.timer || 0)*1000; this.activeProcesses = {}; this.oldrc = (n.oldrc || false).toString(); - this.execOpt = {encoding:'binary', maxBuffer:10000000}; + this.execOpt = {encoding:'binary', maxBuffer:RED.settings.execMaxBufferSize||10000000}; var node = this; if (process.platform === 'linux' && fs.existsSync('/bin/bash')) { node.execOpt.shell = '/bin/bash'; } diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 7d5643ecd..cac6a2ce7 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -41,6 +41,10 @@ module.exports = { // Timeout in milliseconds for HTTP request connections // defaults to 120 seconds //httpRequestTimeout: 120000, + + // Maximum buffer size for the exec node + // defaults to 10Mb + //execMaxBufferSize: 10000000, // The maximum length, in characters, of any message sent to the debug sidebar tab debugMaxLength: 1000, From d8c8d7bc577ce37af237260b394b7699a4f2bdd4 Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Sun, 17 Jan 2021 01:58:13 +0900 Subject: [PATCH 08/39] hide unused input field (#2823) --- .../node_modules/@node-red/nodes/core/network/31-tcpin.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/node_modules/@node-red/nodes/core/network/31-tcpin.html b/packages/node_modules/@node-red/nodes/core/network/31-tcpin.html index ae0eaedda..2aab13437 100644 --- a/packages/node_modules/@node-red/nodes/core/network/31-tcpin.html +++ b/packages/node_modules/@node-red/nodes/core/network/31-tcpin.html @@ -235,6 +235,7 @@ oneditprepare: function() { var previous = null; $("#node-input-out").on('focus', function () { previous = this.value; }).on("change", function() { + $("#node-input-splitc").show(); if (previous === null) { previous = $("#node-input-out").val(); } if ($("#node-input-out").val() == "char") { if (previous != "char") { $("#node-input-splitc").val("\\n"); } @@ -247,6 +248,7 @@ else if ($("#node-input-out").val() == "immed") { if (previous != "immed") { $("#node-input-splitc").val(" "); } $("#node-units").text(""); + $("#node-input-splitc").hide(); } else if ($("#node-input-out").val() == "count") { if (previous != "count") { $("#node-input-splitc").val("12"); } @@ -255,6 +257,7 @@ else { if (previous != "sit") { $("#node-input-splitc").val(" "); } $("#node-units").text(""); + $("#node-input-splitc").hide(); } }); } From 55ff035fc93b183342320baf83dd6a8400eac576 Mon Sep 17 00:00:00 2001 From: Tiago Ferreira Date: Mon, 18 Jan 2021 13:18:07 +0000 Subject: [PATCH 09/39] Ability to add projects path to the settings file (#2816) * add the ability to set the projects path * Update packages/node_modules/@node-red/runtime/locales/en-US/runtime.json use directory to keep consistency with the project Co-authored-by: Nick O'Leary * Update packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js only show the projects directory is projects are enabled Co-authored-by: Nick O'Leary * use "directory" instead of "folder" to keep consistency with the Node-RED project Co-authored-by: Nick O'Leary --- .../lib/storage/localfilesystem/projects/Project.js | 5 +++++ .../lib/storage/localfilesystem/projects/index.js | 10 ++++++++++ .../@node-red/runtime/locales/en-US/runtime.json | 1 + 3 files changed, 16 insertions(+) diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js index 060a71227..e10325b58 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js @@ -1048,6 +1048,11 @@ function init(_settings, _runtime) { settings = _settings; runtime = _runtime; projectsDir = fspath.join(settings.userDir,"projects"); + + if(settings.editorTheme.projects.path) { + projectsDir = settings.editorTheme.projects.path; + } + authCache.init(); } diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js index 8ddd61232..6c1228c6c 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js @@ -114,6 +114,11 @@ function init(_settings, _runtime) { Projects.init(settings,runtime); sshTools.init(settings); projectsDir = fspath.join(settings.userDir,"projects"); + + if(settings.editorTheme.projects.path) { + projectsDir = settings.editorTheme.projects.path; + } + if (!settings.readOnly) { return fs.ensureDir(projectsDir) //TODO: this is accessing settings from storage directly as settings @@ -501,6 +506,11 @@ async function getFlows() { if (!initialFlowLoadComplete) { initialFlowLoadComplete = true; log.info(log._("storage.localfilesystem.user-dir",{path:settings.userDir})); + + if (projectsEnabled) { + log.info(log._("storage.localfilesystem.projects.projects-directory", {projectsDirectory: projectsDir})); + } + if (activeProject) { // At this point activeProject will be a string, so go load it and // swap in an instance of Project diff --git a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json index 58eb7308d..02e15aa88 100644 --- a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json @@ -156,6 +156,7 @@ "projects": { "changing-project": "Setting active project : __project__", "active-project": "Active project : __project__", + "projects-directory": "Projects directory: __projectsDirectory__", "project-not-found": "Project not found : __project__", "no-active-project": "No active project : using default flows file", "disabled": "Projects disabled : editorTheme.projects.enabled=false", From a006b5205234b076df2f3a9ff753ea3fb5d4c2cb Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 10 Dec 2020 16:01:55 +0000 Subject: [PATCH 10/39] Initial plugin runtime api implementation --- .../@node-red/editor-api/lib/admin/index.js | 6 + .../@node-red/editor-api/lib/admin/plugins.js | 38 +++ .../editor-client/locales/en-US/editor.json | 1 + .../@node-red/editor-client/src/js/i18n.js | 25 ++ .../@node-red/editor-client/src/js/red.js | 85 +++++- .../@node-red/registry/lib/index.js | 8 + .../@node-red/registry/lib/loader.js | 289 ++++++++++++++---- .../@node-red/registry/lib/localfilesystem.js | 58 ++-- .../@node-red/registry/lib/plugins.js | 100 ++++++ .../@node-red/registry/lib/registry.js | 57 ++-- .../@node-red/registry/lib/util.js | 11 + .../@node-red/runtime/lib/api/index.js | 2 + .../@node-red/runtime/lib/api/plugins.js | 57 ++++ .../@node-red/runtime/lib/index.js | 8 + .../@node-red/runtime/lib/plugins.js | 10 + .../locales/en-US/test-editor-plugin.json | 3 + .../resources/plugin/test-plugin/package.json | 12 + .../test-plugin/test-editor-plugin.html | 5 + .../plugin/test-plugin/test-runtime-plugin.js | 10 + test/resources/plugin/test-plugin/test.html | 4 + test/resources/plugin/test-plugin/test.js | 13 + .../editor-api/lib/admin/plugins_spec.js | 111 +++++++ .../@node-red/registry/lib/plugins_spec.js | 153 ++++++++++ .../@node-red/runtime/lib/api/plugins_spec.js | 68 +++++ .../@node-red/runtime/lib/plugins_spec.js | 13 + 25 files changed, 1026 insertions(+), 121 deletions(-) create mode 100644 packages/node_modules/@node-red/editor-api/lib/admin/plugins.js create mode 100644 packages/node_modules/@node-red/registry/lib/plugins.js create mode 100644 packages/node_modules/@node-red/runtime/lib/api/plugins.js create mode 100644 packages/node_modules/@node-red/runtime/lib/plugins.js create mode 100644 test/resources/plugin/test-plugin/locales/en-US/test-editor-plugin.json create mode 100644 test/resources/plugin/test-plugin/package.json create mode 100644 test/resources/plugin/test-plugin/test-editor-plugin.html create mode 100644 test/resources/plugin/test-plugin/test-runtime-plugin.js create mode 100644 test/resources/plugin/test-plugin/test.html create mode 100644 test/resources/plugin/test-plugin/test.js create mode 100644 test/unit/@node-red/editor-api/lib/admin/plugins_spec.js create mode 100644 test/unit/@node-red/registry/lib/plugins_spec.js create mode 100644 test/unit/@node-red/runtime/lib/api/plugins_spec.js create mode 100644 test/unit/@node-red/runtime/lib/plugins_spec.js diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/index.js b/packages/node_modules/@node-red/editor-api/lib/admin/index.js index 34c47b2cb..cf3505439 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/index.js @@ -22,6 +22,7 @@ var flow = require("./flow"); var context = require("./context"); var auth = require("../auth"); var info = require("./settings"); +var plugins = require("./plugins"); var apiUtil = require("../util"); @@ -32,6 +33,7 @@ module.exports = { nodes.init(runtimeAPI); context.init(runtimeAPI); info.init(settings,runtimeAPI); + plugins.init(runtimeAPI); var needsPermission = auth.needsPermission; @@ -80,6 +82,10 @@ module.exports = { adminApp.get("/settings",needsPermission("settings.read"),info.runtimeSettings,apiUtil.errorHandler); + // Plugins + adminApp.get("/plugins", needsPermission("plugins.read"), plugins.getAll, apiUtil.errorHandler); + adminApp.get("/plugins/messages", needsPermission("plugins.read"), plugins.getCatalogs, apiUtil.errorHandler); + return adminApp; } } diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/plugins.js b/packages/node_modules/@node-red/editor-api/lib/admin/plugins.js new file mode 100644 index 000000000..15428f86f --- /dev/null +++ b/packages/node_modules/@node-red/editor-api/lib/admin/plugins.js @@ -0,0 +1,38 @@ +var apiUtils = require("../util"); + +var runtimeAPI; + +module.exports = { + init: function(_runtimeAPI) { + runtimeAPI = _runtimeAPI; + }, + getAll: function(req,res) { + var opts = { + user: req.user, + req: apiUtils.getRequestLogObject(req) + } + if (req.get("accept") == "application/json") { + runtimeAPI.plugins.getPluginList(opts).then(function(list) { + res.json(list); + }) + } else { + opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages()); + runtimeAPI.plugins.getPluginConfigs(opts).then(function(configs) { + res.send(configs); + }) + } + }, + getCatalogs: function(req,res) { + var opts = { + user: req.user, + lang: req.query.lng, + req: apiUtils.getRequestLogObject(req) + } + runtimeAPI.plugins.getPluginCatalogs(opts).then(function(result) { + res.json(result); + }).catch(function(err) { + console.log(err.stack); + apiUtils.rejectHandler(req,res,err); + }) + } +}; diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 7894f1ed2..7cdfde99d 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -38,6 +38,7 @@ } }, "event": { + "loadPlugins": "Loading Plugins", "loadPalette": "Loading Palette", "loadNodeCatalogs": "Loading Node catalogs", "loadNodes": "Loading Nodes __count__", diff --git a/packages/node_modules/@node-red/editor-client/src/js/i18n.js b/packages/node_modules/@node-red/editor-client/src/js/i18n.js index f1d0edfaf..ac439ecbc 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/i18n.js +++ b/packages/node_modules/@node-red/editor-client/src/js/i18n.js @@ -108,6 +108,31 @@ RED.i18n = (function() { } }); }) + }, + + loadPluginCatalogs: function(done) { + var languageList = i18n.functions.toLanguages(localStorage.getItem("editor-language")||i18n.detectLanguage()); + var toLoad = languageList.length; + + languageList.forEach(function(lang) { + $.ajax({ + headers: { + "Accept":"application/json" + }, + cache: false, + url: apiRootUrl+'plugins/messages?lng='+lang, + success: function(data) { + var namespaces = Object.keys(data); + namespaces.forEach(function(ns) { + i18n.addResourceBundle(lang,ns,data[ns]); + }); + toLoad--; + if (toLoad === 0) { + done(); + } + } + }); + }) } } })(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index 3a4f4781f..e3439c435 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -15,19 +15,66 @@ **/ var RED = (function() { - function appendNodeConfig(nodeConfig,done) { + + function loadPluginList() { + loader.reportProgress(RED._("event.loadPlugins"), 10) + $.ajax({ + headers: { + "Accept":"application/json" + }, + cache: false, + url: 'plugins', + success: function(data) { + console.log(data); + loader.reportProgress(RED._("event.loadPlugins"), 13) + RED.i18n.loadPluginCatalogs(function() { + loadPlugins(function() { + loadNodeList(); + }); + }); + } + }); + } + function loadPlugins(done) { + loader.reportProgress(RED._("event.loadPlugins",{count:""}), 17) + var lang = localStorage.getItem("editor-language")||i18n.detectLanguage(); + + $.ajax({ + headers: { + "Accept":"text/html", + "Accept-Language": lang + }, + cache: false, + url: 'plugins', + success: function(data) { + var configs = data.trim().split(/(?=)/); + var totalCount = configs.length; + var stepConfig = function() { + // loader.reportProgress(RED._("event.loadNodes",{count:(totalCount-configs.length)+"/"+totalCount}), 30 + ((totalCount-configs.length)/totalCount)*40 ) + if (configs.length === 0) { + done(); + } else { + var config = configs.shift(); + appendPluginConfig(config,stepConfig); + } + } + stepConfig(); + } + }); + } + + function appendConfig(config, moduleIdMatch, targetContainer, done) { done = done || function(){}; - var m = //.exec(nodeConfig.trim()); var moduleId; - if (m) { - moduleId = m[1]; + if (moduleIdMatch) { + moduleId = moduleIdMatch[1]; } else { moduleId = "unknown"; } try { - var hasDeferred = false; - var nodeConfigEls = $("
    "+nodeConfig+"
    "); + var hasDeferred = false; + var nodeConfigEls = $("
    "+config+"
    "); var scripts = nodeConfigEls.find("script"); var scriptCount = scripts.length; scripts.each(function(i,el) { @@ -38,14 +85,14 @@ var RED = (function() { newScript.onload = function() { scriptCount--; if (scriptCount === 0) { - $("#red-ui-editor-node-configs").append(nodeConfigEls); + $(targetContainer).append(nodeConfigEls); done() } } if ($(el).attr('type') === "module") { newScript.type = "module"; } - $("#red-ui-editor-node-configs").append(newScript); + $(targetContainer).append(newScript); newScript.src = RED.settings.apiRootUrl+srcUrl; hasDeferred = true; } else { @@ -61,7 +108,7 @@ var RED = (function() { } }) if (!hasDeferred) { - $("#red-ui-editor-node-configs").append(nodeConfigEls); + $(targetContainer).append(nodeConfigEls); done(); } } catch(err) { @@ -73,6 +120,23 @@ var RED = (function() { done(); } } + function appendPluginConfig(pluginConfig,done) { + appendConfig( + pluginConfig, + //.exec(pluginConfig.trim()), + "#red-ui-editor-plugin-configs", + done + ); + } + + function appendNodeConfig(nodeConfig,done) { + appendConfig( + nodeConfig, + //.exec(nodeConfig.trim()), + "#red-ui-editor-node-configs", + done + ); + } function loadNodeList() { loader.reportProgress(RED._("event.loadPalette"), 20) @@ -580,7 +644,7 @@ var RED = (function() { RED.actions.add("core:show-about", showAbout); - loadNodeList(); + loadPluginList(); } @@ -596,6 +660,7 @@ var RED = (function() { '
    '+ '
    '+ '
    ').appendTo(options.target); + $('
    ').appendTo(options.target); $('
    ').appendTo(options.target); $('
    ').appendTo(options.target); diff --git a/packages/node_modules/@node-red/registry/lib/index.js b/packages/node_modules/@node-red/registry/lib/index.js index 03f979424..251b04971 100644 --- a/packages/node_modules/@node-red/registry/lib/index.js +++ b/packages/node_modules/@node-red/registry/lib/index.js @@ -28,6 +28,7 @@ var registry = require("./registry"); var loader = require("./loader"); var installer = require("./installer"); var library = require("./library"); +var plugins = require("./plugins"); /** * Initialise the registry with a reference to a runtime object @@ -40,6 +41,7 @@ function init(runtime) { // the util module it. The Util module is responsible for constructing the // RED object passed to node modules when they are loaded. loader.init(runtime); + plugins.init(runtime.settings); registry.init(runtime.settings,loader); library.init(); } @@ -297,6 +299,12 @@ module.exports = { */ getNodeExampleFlowPath: library.getExampleFlowPath, + registerPlugin: plugins.registerPlugin, + getPlugin: plugins.getPlugin, + getPluginsByType: plugins.getPluginsByType, + getPluginList: plugins.getPluginList, + getPluginConfigs: plugins.getPluginConfigs, + deprecated: require("./deprecated") }; diff --git a/packages/node_modules/@node-red/registry/lib/loader.js b/packages/node_modules/@node-red/registry/lib/loader.js index f35987732..d29eadc66 100644 --- a/packages/node_modules/@node-red/registry/lib/loader.js +++ b/packages/node_modules/@node-red/registry/lib/loader.js @@ -36,78 +36,140 @@ function load(disableNodePathScan) { // To skip node scan, the following line will use the stored node list. // We should expose that as an option at some point, although the // performance gains are minimal. - //return loadNodeFiles(registry.getModuleList()); + //return loadModuleFiles(registry.getModuleList()); log.info(log._("server.loading")); - var nodeFiles = localfilesystem.getNodeFiles(disableNodePathScan); - return loadNodeFiles(nodeFiles); + var modules = localfilesystem.getNodeFiles(disableNodePathScan); + return loadModuleFiles(modules); } -function loadNodeFiles(nodeFiles) { + +function loadModuleTypeFiles(module, type) { + const things = module[type]; + var first = true; var promises = []; - var nodes = []; - for (var module in nodeFiles) { + for (var thingName in things) { /* istanbul ignore else */ - if (nodeFiles.hasOwnProperty(module)) { - if (nodeFiles[module].redVersion && - !semver.satisfies((settings.version||"0.0.0").replace(/(\-[1-9A-Za-z-][0-9A-Za-z-\.]*)?(\+[0-9A-Za-z-\.]+)?$/,""), nodeFiles[module].redVersion)) { + if (things.hasOwnProperty(thingName)) { + if (module.name != "node-red" && first) { + // Check the module directory exists + first = false; + var fn = things[thingName].file; + var parts = fn.split("/"); + var i = parts.length-1; + for (;i>=0;i--) { + if (parts[i] == "node_modules") { + break; + } + } + var moduleFn = parts.slice(0,i+2).join("/"); + + try { + var stat = fs.statSync(moduleFn); + } catch(err) { + // Module not found, don't attempt to load its nodes + break; + } + } + + try { + var promise; + if (type === "nodes") { + promise = loadNodeConfig(things[thingName]); + } else if (type === "plugins") { + promise = loadPluginConfig(things[thingName]); + } + promises.push( + promise.then( + (function() { + var m = module.name; + var n = thingName; + return function(nodeSet) { + things[n] = nodeSet; + return nodeSet; + } + })() + ).catch(err => {console.log(err)}) + ); + } catch(err) { + console.log(err) + // + } + } + } + return promises; +} + +function loadModuleFiles(modules) { + var pluginPromises = []; + var nodePromises = []; + for (var module in modules) { + /* istanbul ignore else */ + if (modules.hasOwnProperty(module)) { + if (modules[module].redVersion && + !semver.satisfies((settings.version||"0.0.0").replace(/(\-[1-9A-Za-z-][0-9A-Za-z-\.]*)?(\+[0-9A-Za-z-\.]+)?$/,""), modules[module].redVersion)) { //TODO: log it - log.warn("["+module+"] "+log._("server.node-version-mismatch",{version:nodeFiles[module].redVersion})); - nodeFiles[module].err = "version_mismatch"; + log.warn("["+module+"] "+log._("server.node-version-mismatch",{version:modules[module].redVersion})); + modules[module].err = "version_mismatch"; continue; } if (module == "node-red" || !registry.getModuleInfo(module)) { - var first = true; - for (var node in nodeFiles[module].nodes) { - /* istanbul ignore else */ - if (nodeFiles[module].nodes.hasOwnProperty(node)) { - if (module != "node-red" && first) { - // Check the module directory exists - first = false; - var fn = nodeFiles[module].nodes[node].file; - var parts = fn.split("/"); - var i = parts.length-1; - for (;i>=0;i--) { - if (parts[i] == "node_modules") { - break; - } - } - var moduleFn = parts.slice(0,i+2).join("/"); - - try { - var stat = fs.statSync(moduleFn); - } catch(err) { - // Module not found, don't attempt to load its nodes - break; - } - } - - try { - promises.push(loadNodeConfig(nodeFiles[module].nodes[node]).then((function() { - var m = module; - var n = node; - return function(nodeSet) { - nodeFiles[m].nodes[n] = nodeSet; - nodes.push(nodeSet); - } - })()).catch(err => {})); - } catch(err) { - // - } - } + if (modules[module].nodes) { + nodePromises = nodePromises.concat(loadModuleTypeFiles(modules[module], "nodes")); + } + if (modules[module].plugins) { + pluginPromises = pluginPromises.concat(loadModuleTypeFiles(modules[module], "plugins")); } } } } - return Promise.all(promises).then(function(results) { - for (var module in nodeFiles) { - if (nodeFiles.hasOwnProperty(module)) { - if (!nodeFiles[module].err) { - registry.addModule(nodeFiles[module]); + var pluginList; + var nodeList; + + return Promise.all(pluginPromises).then(function(results) { + pluginList = results.filter(r => !!r); + // Initial plugin load has happened. Ensure modules that provide + // plugins are in the registry now. + for (var module in modules) { + if (modules.hasOwnProperty(module)) { + if (modules[module].plugins && Object.keys(modules[module].plugins).length > 0) { + // Add the modules for plugins + if (!modules[module].err) { + registry.addModule(modules[module]); + } } } } - return loadNodeSetList(nodes); + return loadNodeSetList(pluginList); + }).then(function() { + return Promise.all(nodePromises); + }).then(function(results) { + nodeList = results.filter(r => !!r); + // Initial node load has happened. Ensure remaining modules are in the registry + for (var module in modules) { + if (modules.hasOwnProperty(module)) { + if (!modules[module].plugins || Object.keys(modules[module].plugins).length === 0) { + if (!modules[module].err) { + registry.addModule(modules[module]); + } + } + } + } + return loadNodeSetList(nodeList); + }); +} + +async function loadPluginTemplate(plugin) { + return fs.readFile(plugin.template,'utf8').then(content => { + plugin.config = content; + return plugin; + }).catch(err => { + if (err.code === 'ENOENT') { + plugin.err = "Error: "+plugin.template+" does not exist"; + } else { + plugin.err = err.toString(); + } + return plugin; }); } @@ -175,11 +237,12 @@ async function loadNodeLocales(node) { node.namespace = node.module; return node } - return fs.stat(path.join(path.dirname(node.file),"locales")).then(stat => { + const baseFile = node.file||node.template; + return fs.stat(path.join(path.dirname(baseFile),"locales")).then(stat => { node.namespace = node.id; return i18n.registerMessageCatalog(node.id, - path.join(path.dirname(node.file),"locales"), - path.basename(node.file,".js")+".json") + path.join(path.dirname(baseFile),"locales"), + path.basename(baseFile).replace(/\.[^.]+$/,".json")) .then(() => node); }).catch(err => { node.namespace = node.module; @@ -204,6 +267,7 @@ async function loadNodeConfig(fileInfo) { } var node = { + type: "node", id: id, module: module, name: name, @@ -227,6 +291,58 @@ async function loadNodeConfig(fileInfo) { return node; } +async function loadPluginConfig(fileInfo) { + var file = fileInfo.file; + var module = fileInfo.module; + var name = fileInfo.name; + var version = fileInfo.version; + + var id = module + "/" + name; + var isEnabled = true; + + // TODO: registry.getPluginInfo + + // var info = registry.getPluginInfo(id); + // if (info) { + // if (info.hasOwnProperty("loaded")) { + // throw new Error(file+" already loaded"); + // } + // isEnabled = info.enabled; + // } + + + if (!fs.existsSync(jsFile)) { + } + + var plugin = { + type: "plugin", + id: id, + module: module, + name: name, + enabled: isEnabled, + loaded:false, + version: version, + local: fileInfo.local, + plugins: [], + config: "", + help: {} + }; + var jsFile = file.replace(/\.[^.]+$/,".js"); + var htmlFile = file.replace(/\.[^.]+$/,".html"); + if (fs.existsSync(jsFile)) { + plugin.file = jsFile; + } + if (fs.existsSync(htmlFile)) { + plugin.template = htmlFile; + } + await loadNodeLocales(plugin) + + if (plugin.template && !settings.disableEditor) { + return loadPluginTemplate(plugin); + } + return plugin +} + /** * Loads the specified node into the runtime * @param node a node info object - see loadNodeConfig @@ -236,8 +352,6 @@ async function loadNodeConfig(fileInfo) { * */ function loadNodeSet(node) { - var nodeDir = path.dirname(node.file); - var nodeFn = path.basename(node.file); if (!node.enabled) { return Promise.resolve(node); } else { @@ -284,11 +398,59 @@ function loadNodeSet(node) { } } +async function loadPlugin(plugin) { + if (!plugin.file) { + // No runtime component - nothing to load + return plugin; + } + try { + var r = require(plugin.file); + if (typeof r === "function") { + + var red = registryUtil.createNodeApi(plugin); + var promise = r(red); + if (promise != null && typeof promise.then === "function") { + return promise.then(function() { + plugin.enabled = true; + plugin.loaded = true; + return plugin; + }).catch(function(err) { + plugin.err = err; + return plugin; + }); + } + } + plugin.enabled = true; + plugin.loaded = true; + return plugin; + } catch(err) { + console.log(err); + plugin.err = err; + var stack = err.stack; + var message; + if (stack) { + var i = stack.indexOf(plugin.file); + if (i > -1) { + var excerpt = stack.substring(i+node.file.length+1,i+plugin.file.length+20); + var m = /^(\d+):(\d+)/.exec(excerpt); + if (m) { + plugin.err = err+" (line:"+m[1]+")"; + } + } + } + return plugin; + } +} + function loadNodeSetList(nodes) { var promises = []; nodes.forEach(function(node) { if (!node.err) { - promises.push(loadNodeSet(node).catch(err => {})); + if (node.type === "plugin") { + promises.push(loadPlugin(node).catch(err => {})); + } else { + promises.push(loadNodeSet(node).catch(err => {})); + } } else { promises.push(node); } @@ -316,6 +478,7 @@ function addModule(module) { return Promise.reject(e); } try { +<<<<<<< HEAD var moduleFiles = {}; var moduleStack = [module]; while(moduleStack.length > 0) { @@ -339,6 +502,10 @@ function addModule(module) { } } return loadNodeFiles(moduleFiles).then(() => module) +======= + var moduleFiles = localfilesystem.getModuleFiles(module); + return loadModuleFiles(moduleFiles); +>>>>>>> Initial plugin runtime api implementation } catch(err) { return Promise.reject(err); } diff --git a/packages/node_modules/@node-red/registry/lib/localfilesystem.js b/packages/node_modules/@node-red/registry/lib/localfilesystem.js index ed4b81d8e..92e03fd62 100644 --- a/packages/node_modules/@node-red/registry/lib/localfilesystem.js +++ b/packages/node_modules/@node-red/registry/lib/localfilesystem.js @@ -220,33 +220,41 @@ function getModuleNodeFiles(module) { var moduleDir = module.dir; var pkg = module.package; - var nodes = pkg['node-red'].nodes||{}; - var results = []; var iconDirs = []; var iconList = []; - for (var n in nodes) { - /* istanbul ignore else */ - if (nodes.hasOwnProperty(n)) { - var file = path.join(moduleDir,nodes[n]); - results.push({ - file: file, - module: pkg.name, - name: n, - version: pkg.version - }); - var iconDir = path.join(moduleDir,path.dirname(nodes[n]),"icons"); - if (iconDirs.indexOf(iconDir) == -1) { - try { - fs.statSync(iconDir); - var icons = scanIconDir(iconDir); - iconList.push({path:iconDir,icons:icons}); - iconDirs.push(iconDir); - } catch(err) { + + function scanTypes(types) { + const files = []; + for (var n in types) { + /* istanbul ignore else */ + if (types.hasOwnProperty(n)) { + var file = path.join(moduleDir,types[n]); + files.push({ + file: file, + module: pkg.name, + name: n, + version: pkg.version + }); + var iconDir = path.join(moduleDir,path.dirname(types[n]),"icons"); + if (iconDirs.indexOf(iconDir) == -1) { + try { + fs.statSync(iconDir); + var icons = scanIconDir(iconDir); + iconList.push({path:iconDir,icons:icons}); + iconDirs.push(iconDir); + } catch(err) { + } } } } + return files; } - var result = {files:results,icons:iconList}; + + var result = { + nodeFiles:scanTypes(pkg['node-red'].nodes||{}), + pluginFiles:scanTypes(pkg['node-red'].plugins||{}), + icons:iconList + }; var examplesDir = path.join(moduleDir,"examples"); try { @@ -396,6 +404,7 @@ function convertModuleFileListToObject(moduleFiles) { local: moduleFile.local||false, user: moduleFile.user||false, nodes: {}, + plugins: {}, icons: nodeModuleFiles.icons, examples: nodeModuleFiles.examples }; @@ -408,11 +417,14 @@ function convertModuleFileListToObject(moduleFiles) { if (moduleFile.usedBy) { nodeList[moduleFile.package.name].usedBy = moduleFile.usedBy; } - nodeModuleFiles.files.forEach(function(node) { - node.local = moduleFile.local||false; + nodeModuleFiles.nodeFiles.forEach(function(node) { nodeList[moduleFile.package.name].nodes[node.name] = node; nodeList[moduleFile.package.name].nodes[node.name].local = moduleFile.local || false; }); + nodeModuleFiles.pluginFiles.forEach(function(plugin) { + nodeList[moduleFile.package.name].plugins[plugin.name] = plugin; + nodeList[moduleFile.package.name].plugins[plugin.name].local = moduleFile.local || false; + }); }); return nodeList; } diff --git a/packages/node_modules/@node-red/registry/lib/plugins.js b/packages/node_modules/@node-red/registry/lib/plugins.js new file mode 100644 index 000000000..c2bb2298d --- /dev/null +++ b/packages/node_modules/@node-red/registry/lib/plugins.js @@ -0,0 +1,100 @@ +const registry = require("./registry"); +const {events} = require("@node-red/util") + +var pluginConfigCache = {}; +var pluginToId = {}; +var plugins = {}; +var pluginsByType = {}; +var settings; + +function init(_settings) { + settings = _settings; + plugins = {}; + pluginConfigCache = {}; + pluginToId = {}; + pluginsByType = {}; +} + +function registerPlugin(nodeSetId,id,definition) { + var moduleId = registry.getModuleFromSetId(nodeSetId); + var pluginId = registry.getNodeFromSetId(nodeSetId); + + definition.id = id; + definition.module = moduleId; + pluginToId[id] = nodeSetId; + plugins[id] = definition; + var module = registry.getModule(moduleId); + module.plugins[pluginId].plugins.push(definition); + if (definition.type) { + pluginsByType[definition.type] = pluginsByType[definition.type] || []; + pluginsByType[definition.type].push(definition); + } + if (definition.onadd && typeof definition.onadd === 'function') { + definition.onadd(); + } + events.emit("registry:plugin-added",id); +} + +function getPlugin(id) { + return plugins[id] +} + +function getPluginsByType(type) { + return pluginsByType[type] || []; +} + +function getPluginConfigs(lang) { + if (!pluginConfigCache[lang]) { + var result = ""; + var script = ""; + var moduleConfigs = registry.getModuleList(); + for (var module in moduleConfigs) { + /* istanbul ignore else */ + if (moduleConfigs.hasOwnProperty(module)) { + var plugins = moduleConfigs[module].plugins; + for (var plugin in plugins) { + if (plugins.hasOwnProperty(plugin)) { + var config = plugins[plugin]; + if (config.enabled && !config.err && config.config) { + result += "\n\n"; + result += config.config; + } + } + } + } + } + pluginConfigCache[lang] = result; + } + return pluginConfigCache[lang]; +} +function getPluginList() { + var list = []; + var moduleConfigs = registry.getModuleList(); + for (var module in moduleConfigs) { + /* istanbul ignore else */ + if (moduleConfigs.hasOwnProperty(module)) { + var plugins = moduleConfigs[module].plugins; + for (var plugin in plugins) { + /* istanbul ignore else */ + if (plugins.hasOwnProperty(plugin)) { + var pluginInfo = registry.filterNodeInfo(plugins[plugin]); + pluginInfo.version = moduleConfigs[module].version; + // if (moduleConfigs[module].pending_version) { + // nodeInfo.pending_version = moduleConfigs[module].pending_version; + // } + list.push(pluginInfo); + } + } + } + } + return list; +} + +module.exports = { + init, + registerPlugin, + getPlugin, + getPluginsByType, + getPluginConfigs, + getPluginList +} \ No newline at end of file diff --git a/packages/node_modules/@node-red/registry/lib/registry.js b/packages/node_modules/@node-red/registry/lib/registry.js index 730280d72..96594fbef 100644 --- a/packages/node_modules/@node-red/registry/lib/registry.js +++ b/packages/node_modules/@node-red/registry/lib/registry.js @@ -67,17 +67,24 @@ function filterNodeInfo(n) { if (n.hasOwnProperty("err")) { r.err = n.err; } + if (n.hasOwnProperty("plugins")) { + r.plugins = n.plugins; + } + if (n.type === "plugin") { + r.editor = !!n.template; + r.runtime = !!n.file; + } return r; } -function getModule(id) { +function getModuleFromSetId(id) { var parts = id.split("/"); return parts.slice(0,parts.length-1).join("/"); } -function getNode(id) { +function getNodeFromSetId(id) { var parts = id.split("/"); return parts[parts.length-1]; } @@ -220,11 +227,11 @@ function addModule(module) { function removeNode(id) { - var config = moduleConfigs[getModule(id)].nodes[getNode(id)]; + var config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; if (!config) { throw new Error("Unrecognised id: "+id); } - delete moduleConfigs[getModule(id)].nodes[getNode(id)]; + delete moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; var i = nodeList.indexOf(id); if (i > -1) { nodeList.splice(i,1); @@ -294,9 +301,9 @@ function getNodeInfo(typeOrId) { } /* istanbul ignore else */ if (id) { - var module = moduleConfigs[getModule(id)]; + var module = moduleConfigs[getModuleFromSetId(id)]; if (module) { - var config = module.nodes[getNode(id)]; + var config = module.nodes[getNodeFromSetId(id)]; if (config) { var info = filterNodeInfo(config); if (config.hasOwnProperty("loaded")) { @@ -323,9 +330,9 @@ function getFullNodeInfo(typeOrId) { } /* istanbul ignore else */ if (id) { - var module = moduleConfigs[getModule(id)]; + var module = moduleConfigs[getModuleFromSetId(id)]; if (module) { - return module.nodes[getNode(id)]; + return module.nodes[getNodeFromSetId(id)]; } } return null; @@ -359,16 +366,10 @@ function getNodeList(filter) { } function getModuleList() { - //var list = []; - //for (var module in moduleNodes) { - // /* istanbul ignore else */ - // if (moduleNodes.hasOwnProperty(module)) { - // list.push(registry.getModuleInfo(module)); - // } - //} - //return list; return moduleConfigs; - +} +function getModule(id) { + return moduleConfigs[id]; } function getModuleInfo(module) { @@ -461,13 +462,11 @@ function getAllNodeConfigs(lang) { var script = ""; for (var i=0;i 0)) { continue; } - - var config = module.nodes[getNode(id)]; + var config = module.nodes[getNodeFromSetId(id)]; if (config.enabled && !config.err) { result += "\n\n"; result += config.config; @@ -486,11 +485,11 @@ function getAllNodeConfigs(lang) { } function getNodeConfig(id,lang) { - var config = moduleConfigs[getModule(id)]; + var config = moduleConfigs[getModuleFromSetId(id)]; if (!config) { return null; } - config = config.nodes[getNode(id)]; + config = config.nodes[getNodeFromSetId(id)]; if (config) { var result = "\n"+config.config; result += loader.getNodeHelp(config,lang||"en-US") @@ -511,7 +510,7 @@ function getNodeConstructor(type) { if (typeof id === "undefined") { config = undefined; } else { - config = moduleConfigs[getModule(id)].nodes[getNode(id)]; + config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; } if (!config || (config.enabled && !config.err)) { @@ -548,7 +547,7 @@ function enableNodeSet(typeOrId) { } var config; try { - config = moduleConfigs[getModule(id)].nodes[getNode(id)]; + config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; delete config.err; config.enabled = true; nodeConfigCache = {}; @@ -571,7 +570,7 @@ function disableNodeSet(typeOrId) { } var config; try { - config = moduleConfigs[getModule(id)].nodes[getNode(id)]; + config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; // TODO: persist setting config.enabled = false; nodeConfigCache = {}; @@ -710,6 +709,7 @@ var registry = module.exports = { getFullNodeInfo: getFullNodeInfo, getNodeList: getNodeList, getModuleList: getModuleList, + getModule: getModule, getModuleInfo: getModuleInfo, getNodeIconPath: getNodeIconPath, @@ -725,5 +725,8 @@ var registry = module.exports = { saveNodeList: saveNodeList, - cleanModuleList: cleanModuleList + cleanModuleList: cleanModuleList, + getModuleFromSetId: getModuleFromSetId, + getNodeFromSetId: getNodeFromSetId, + filterNodeInfo: filterNodeInfo }; diff --git a/packages/node_modules/@node-red/registry/lib/util.js b/packages/node_modules/@node-red/registry/lib/util.js index 94690d6cb..e6caa5d67 100644 --- a/packages/node_modules/@node-red/registry/lib/util.js +++ b/packages/node_modules/@node-red/registry/lib/util.js @@ -70,6 +70,17 @@ function createNodeApi(node) { }) } }, + plugins: { + registerPlugin: function(id,definition) { + return runtime.plugins.registerPlugin(node.id,id,definition); + }, + get: function(id) { + return runtime.plugins.getPlugin(id); + }, + getByType: function(type) { + return runtime.plugins.getPluginsByType(type); + } + }, library: { register: function(type) { return runtime.library.register(node.id,type); diff --git a/packages/node_modules/@node-red/runtime/lib/api/index.js b/packages/node_modules/@node-red/runtime/lib/api/index.js index b131470b0..46d15d1e7 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/index.js +++ b/packages/node_modules/@node-red/runtime/lib/api/index.js @@ -28,6 +28,7 @@ var api = module.exports = { api.library.init(runtime); api.projects.init(runtime); api.context.init(runtime); + api.plugins.init(runtime); }, comms: require("./comms"), @@ -37,6 +38,7 @@ var api = module.exports = { settings: require("./settings"), projects: require("./projects"), context: require("./context"), + plugins: require("./plugins"), isStarted: async function(opts) { return runtime.isStarted(); diff --git a/packages/node_modules/@node-red/runtime/lib/api/plugins.js b/packages/node_modules/@node-red/runtime/lib/api/plugins.js new file mode 100644 index 000000000..ee35d445a --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/api/plugins.js @@ -0,0 +1,57 @@ +/** + * @mixin @node-red/runtime_plugins + */ + +var runtime; + +var api = module.exports = { + init: function(_runtime) { + runtime = _runtime; + }, + + + /** + * Gets the editor content for an individual plugin + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {Object} opts.req - the request to log (optional) + * @return {Promise} - the node information + * @memberof @node-red/runtime_nodes + */ + getPluginList: async function(opts) { + runtime.log.audit({event: "plugins.list.get"}, opts.req); + return runtime.plugins.getPluginList(); + }, + + getPluginConfigs: async function(opts) { + runtime.log.audit({event: "plugins.configs.get"}, opts.req); + return runtime.plugins.getPluginConfigs(opts.lang); + }, + /** + * Gets all registered module message catalogs + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {User} opts.lang - the i18n language to return. If not set, uses runtime default (en-US) + * @param {Object} opts.req - the request to log (optional) + * @return {Promise} - the message catalogs + * @memberof @node-red/runtime_nodes + */ + getPluginCatalogs: async function(opts) { + var lang = opts.lang; + var prevLang = runtime.i18n.i.language; + // Trigger a load from disk of the language if it is not the default + return new Promise( (resolve,reject) => { + runtime.i18n.i.changeLanguage(lang, function(){ + var nodeList = runtime.plugins.getPluginList(); + var result = {}; + nodeList.forEach(function(n) { + if (n.module !== "node-red") { + result[n.id] = runtime.i18n.i.getResourceBundle(lang, n.id)||{}; + } + }); + runtime.i18n.i.changeLanguage(prevLang); + resolve(result); + }); + }); + }, +} diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js index 40aea7e36..0be1c6511 100644 --- a/packages/node_modules/@node-red/runtime/lib/index.js +++ b/packages/node_modules/@node-red/runtime/lib/index.js @@ -21,6 +21,7 @@ var flows = require("./flows"); var storage = require("./storage"); var library = require("./library"); var hooks = require("./hooks"); +var plugins = require("./plugins"); var settings = require("./settings"); var express = require("express"); @@ -280,6 +281,7 @@ var runtime = { storage: storage, hooks: hooks, nodes: redNodes, + plugins: plugins, flows: flows, library: library, exec: exec, @@ -341,6 +343,12 @@ module.exports = { */ context: externalAPI.context, + /** + * @memberof @node-red/runtime + * @mixes @node-red/runtime_plugins + */ + plugins: externalAPI.plugins, + /** * Returns whether the runtime is started * @param {Object} opts diff --git a/packages/node_modules/@node-red/runtime/lib/plugins.js b/packages/node_modules/@node-red/runtime/lib/plugins.js new file mode 100644 index 000000000..738a64fa7 --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/plugins.js @@ -0,0 +1,10 @@ +const registry = require("@node-red/registry"); + +module.exports = { + init: function() {}, + registerPlugin: registry.registerPlugin, + getPlugin: registry.getPlugin, + getPluginsByType: registry.getPluginsByType, + getPluginList: registry.getPluginList, + getPluginConfigs: registry.getPluginConfigs, +} \ No newline at end of file diff --git a/test/resources/plugin/test-plugin/locales/en-US/test-editor-plugin.json b/test/resources/plugin/test-plugin/locales/en-US/test-editor-plugin.json new file mode 100644 index 000000000..e990e2ec1 --- /dev/null +++ b/test/resources/plugin/test-plugin/locales/en-US/test-editor-plugin.json @@ -0,0 +1,3 @@ +{ + "plugin": "winning" +} \ No newline at end of file diff --git a/test/resources/plugin/test-plugin/package.json b/test/resources/plugin/test-plugin/package.json new file mode 100644 index 000000000..1077042de --- /dev/null +++ b/test/resources/plugin/test-plugin/package.json @@ -0,0 +1,12 @@ +{ + "name": "test-plugin", + "version": "1.0.0", + "description": "", + "node-red": { + "plugins": { + "test": "test.js", + "test-editor-plugin": "test-editor-plugin.html", + "test-runtime-plugin": "test-runtime-plugin.js" + } + } +} diff --git a/test/resources/plugin/test-plugin/test-editor-plugin.html b/test/resources/plugin/test-plugin/test-editor-plugin.html new file mode 100644 index 000000000..067d09d3c --- /dev/null +++ b/test/resources/plugin/test-plugin/test-editor-plugin.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/test/resources/plugin/test-plugin/test-runtime-plugin.js b/test/resources/plugin/test-plugin/test-runtime-plugin.js new file mode 100644 index 000000000..d9e3ff3c7 --- /dev/null +++ b/test/resources/plugin/test-plugin/test-runtime-plugin.js @@ -0,0 +1,10 @@ +module.exports = function(RED) { + console.log("Loaded test-plugin/test-runtime-plugin") + + RED.plugins.registerPlugin("my-test-runtime-only-plugin", { + type: "bar", + onadd: function() { + console.log("my-test-runtime-only-plugin.onadd called") + } + }) +} \ No newline at end of file diff --git a/test/resources/plugin/test-plugin/test.html b/test/resources/plugin/test-plugin/test.html new file mode 100644 index 000000000..72d2979df --- /dev/null +++ b/test/resources/plugin/test-plugin/test.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/test/resources/plugin/test-plugin/test.js b/test/resources/plugin/test-plugin/test.js new file mode 100644 index 000000000..3a14fe9af --- /dev/null +++ b/test/resources/plugin/test-plugin/test.js @@ -0,0 +1,13 @@ +module.exports = function(RED) { + console.log("Loaded test-plugin/test") + + RED.plugins.registerPlugin("my-test-plugin", { + type: "foo", + onadd: function() { + console.log("my-test-plugin.onadd called") + RED.events.on("registry:plugin-added", function(id) { + console.log(`my-test-plugin: plugin-added event "${id}"`) + }); + } + }) +} \ No newline at end of file diff --git a/test/unit/@node-red/editor-api/lib/admin/plugins_spec.js b/test/unit/@node-red/editor-api/lib/admin/plugins_spec.js new file mode 100644 index 000000000..74584a1d8 --- /dev/null +++ b/test/unit/@node-red/editor-api/lib/admin/plugins_spec.js @@ -0,0 +1,111 @@ +const should = require("should"); +const request = require('supertest'); +const express = require('express'); +const bodyParser = require("body-parser"); + +var app; + +var NR_TEST_UTILS = require("nr-test-utils"); + +var plugins = NR_TEST_UTILS.require("@node-red/editor-api/lib/admin/plugins"); + +describe("api/editor/plugins", function() { + const pluginList = [ + { + "id": "test-module/test-set", + "enabled": true, + "local": false, + "plugins": [ + { + "type": "foo", + "id": "a-plugin", + "module": "test-module" + }, + { + "type": "bar", + "id": "a-plugin2", + "module": "test-module" + }, + { + "type": "foo", + "id": "a-plugin3", + "module": "test-module" + } + ] + }, + { + "id": "test-module/test-disabled-set", + "enabled": false, + "local": false, + "plugins": [] + } + ]; + const pluginConfigs = ` + +test-module-config`; + + const pluginCatalogs = { "test-module": {"foo": "bar"}}; + + before(function() { + app = express(); + app.use(bodyParser.json()); + app.get("/plugins",plugins.getAll); + app.get("/plugins/messages",plugins.getCatalogs); + + plugins.init({ + plugins: { + getPluginList: async function() { return pluginList }, + getPluginConfigs: async function() { return pluginConfigs }, + getPluginCatalogs: async function() { return pluginCatalogs } + } + }) + }); + + it('returns the list of plugins', function(done) { + request(app) + .get("/plugins") + .set('Accept', 'application/json') + .expect(200) + .end(function(err,res) { + if (err) { + return done(err); + } + try { + JSON.stringify(res.body).should.eql(JSON.stringify(pluginList)); + done(); + } catch(err) { + done(err) + } + }); + }); + it('returns the plugin configs', function(done) { + request(app) + .get("/plugins") + .set('Accept', 'text/html') + .expect(200) + .expect(pluginConfigs) + .end(function(err,res) { + if (err) { + return done(err); + } + done(); + }); + }); + it('returns the plugin catalogs', function(done) { + request(app) + .get("/plugins/messages") + .set('Accept', 'application/json') + .expect(200) + .end(function(err,res) { + if (err) { + return done(err); + } + try { + JSON.stringify(res.body).should.eql(JSON.stringify(pluginCatalogs)); + done(); + } catch(err) { + done(err) + } + }); + }); +}); diff --git a/test/unit/@node-red/registry/lib/plugins_spec.js b/test/unit/@node-red/registry/lib/plugins_spec.js new file mode 100644 index 000000000..430553fac --- /dev/null +++ b/test/unit/@node-red/registry/lib/plugins_spec.js @@ -0,0 +1,153 @@ + +const should = require("should"); +const sinon = require("sinon"); +const path = require("path"); + +const NR_TEST_UTILS = require("nr-test-utils"); + +const plugins = NR_TEST_UTILS.require("@node-red/registry/lib/plugins"); +const registry = NR_TEST_UTILS.require("@node-red/registry/lib/registry"); +const { events } = NR_TEST_UTILS.require("@node-red/util"); + +describe("red/nodes/registry/plugins",function() { + let receivedEvents = []; + let modules; + function handleEvent(evnt) { + receivedEvents.push(evnt); + } + beforeEach(function() { + plugins.init({}); + receivedEvents = []; + modules = { + "test-module": { + plugins: { + "test-set": { + id: "test-module/test-set", + enabled: true, + config: "test-module-config", + plugins: [] + }, + "test-disabled-set": { + id: "test-module/test-disabled-set", + enabled: false, + config: "disabled-plugin-config", + plugins: [] + } + } + } + } + events.on("registry:plugin-added",handleEvent); + sinon.stub(registry,"getModule", moduleId => modules[moduleId]); + sinon.stub(registry,"getModuleList", () => modules) + }); + afterEach(function() { + events.removeListener("registry:plugin-added",handleEvent); + registry.getModule.restore(); + registry.getModuleList.restore(); + }) + + describe("registerPlugin", function() { + it("registers a plugin", function() { + let pluginDef = {} + plugins.registerPlugin("test-module/test-set","a-plugin",pluginDef); + receivedEvents.length.should.eql(1); + receivedEvents[0].should.eql("a-plugin"); + should.exist(modules['test-module'].plugins['test-set'].plugins[0]) + modules['test-module'].plugins['test-set'].plugins[0].should.equal(pluginDef) + }) + it("calls a plugins onadd function", function() { + let pluginDef = { onadd: sinon.stub() } + plugins.registerPlugin("test-module/test-set","a-plugin",pluginDef); + pluginDef.onadd.called.should.be.true(); + }) + }) + + describe("getPlugin", function() { + it("returns a registered plugin", function() { + let pluginDef = {} + plugins.registerPlugin("test-module/test-set","a-plugin",pluginDef); + pluginDef.should.equal(plugins.getPlugin("a-plugin")); + }) + }) + describe("getPluginsByType", function() { + it("returns a plugins of a given type", function() { + let pluginDef = {type: "foo"} + let pluginDef2 = {type: "bar"} + let pluginDef3 = {type: "foo"} + plugins.registerPlugin("test-module/test-set","a-plugin",pluginDef); + plugins.registerPlugin("test-module/test-set","a-plugin2",pluginDef2); + plugins.registerPlugin("test-module/test-set","a-plugin3",pluginDef3); + + let fooPlugins = plugins.getPluginsByType("foo"); + let barPlugins = plugins.getPluginsByType("bar"); + let noPlugins = plugins.getPluginsByType("none"); + + noPlugins.should.be.of.length(0); + + fooPlugins.should.be.of.length(2); + fooPlugins.should.containEql(pluginDef); + fooPlugins.should.containEql(pluginDef3); + + barPlugins.should.be.of.length(1); + barPlugins.should.containEql(pluginDef2); + + }) + }) + + describe("getPluginConfigs", function() { + it("gets all plugin configs", function() { + let configs = plugins.getPluginConfigs("en-US"); + configs.should.eql(` + +test-module-config`) + }) + }) + + + describe("getPluginList", function() { + it("returns a plugins of a given type", function() { + let pluginDef = {type: "foo"} + let pluginDef2 = {type: "bar"} + let pluginDef3 = {type: "foo"} + plugins.registerPlugin("test-module/test-set","a-plugin",pluginDef); + plugins.registerPlugin("test-module/test-set","a-plugin2",pluginDef2); + plugins.registerPlugin("test-module/test-set","a-plugin3",pluginDef3); + + let pluginList = plugins.getPluginList(); + JSON.stringify(pluginList).should.eql(JSON.stringify( + [ + { + "id": "test-module/test-set", + "enabled": true, + "local": false, + "plugins": [ + { + "type": "foo", + "id": "a-plugin", + "module": "test-module" + }, + { + "type": "bar", + "id": "a-plugin2", + "module": "test-module" + }, + { + "type": "foo", + "id": "a-plugin3", + "module": "test-module" + } + ] + }, + { + "id": "test-module/test-disabled-set", + "enabled": false, + "local": false, + "plugins": [] + } + ] + )) + }) + }) + + +}); \ No newline at end of file diff --git a/test/unit/@node-red/runtime/lib/api/plugins_spec.js b/test/unit/@node-red/runtime/lib/api/plugins_spec.js new file mode 100644 index 000000000..7ae6d8286 --- /dev/null +++ b/test/unit/@node-red/runtime/lib/api/plugins_spec.js @@ -0,0 +1,68 @@ +const should = require("should"); +const sinon = require("sinon"); + +const NR_TEST_UTILS = require("nr-test-utils"); +const plugins = NR_TEST_UTILS.require("@node-red/runtime/lib/api/plugins") + +const mockLog = () => ({ + log: sinon.stub(), + debug: sinon.stub(), + trace: sinon.stub(), + warn: sinon.stub(), + info: sinon.stub(), + metric: sinon.stub(), + audit: sinon.stub(), + _: function() { return "abc"} +}) + +describe("runtime-api/plugins", function() { + const pluginList = [{id:"one",module:'test-module'},{id:"two",module:"node-red"}]; + const pluginConfigs = "123"; + + describe("getPluginList", function() { + it("gets the plugin list", function() { + plugins.init({ + log: mockLog(), + plugins: { + getPluginList: function() { return pluginList} + } + }); + return plugins.getPluginList({}).then(function(result) { + result.should.eql(pluginList); + }) + }); + }); + describe("getPluginConfigs", function() { + it("gets the plugin configs", function() { + plugins.init({ + log: mockLog(), + plugins: { + getPluginConfigs: function() { return pluginConfigs} + } + }); + return plugins.getPluginConfigs({}).then(function(result) { + result.should.eql(pluginConfigs); + }) + }); + }); + describe("getPluginCatalogs", function() { + it("gets the plugin catalogs", function() { + plugins.init({ + log: mockLog(), + plugins: { + getPluginList: function() { return pluginList} + }, + i18n: { + i: { + changeLanguage: function(lang,done) { done && done() }, + getResourceBundle: function(lang, id) { return {lang,id}} + } + } + }); + return plugins.getPluginCatalogs({lang: "en-US"}).then(function(result) { + JSON.stringify(result).should.eql(JSON.stringify({ one: { lang: "en-US", id: "one" } })) + }) + }); + }); + +}); \ No newline at end of file diff --git a/test/unit/@node-red/runtime/lib/plugins_spec.js b/test/unit/@node-red/runtime/lib/plugins_spec.js new file mode 100644 index 000000000..a78de643c --- /dev/null +++ b/test/unit/@node-red/runtime/lib/plugins_spec.js @@ -0,0 +1,13 @@ +const should = require("should"); +const sinon = require("sinon"); +const NR_TEST_UTILS = require("nr-test-utils"); + +const plugins = NR_TEST_UTILS.require("@node-red/runtime/lib/plugins"); + +describe("runtime/plugins",function() { + + it.skip("delegates all functions to registry module", function() { + // There's no easy way to test this as we can't stub the registry functions + // before the plugin module gets a reference to them + }) +}); From 7531314e3feed1fab2235527c4b7e72724806899 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 14 Dec 2020 10:40:06 +0000 Subject: [PATCH 11/39] Add RED.plugins module to editor --- Gruntfile.js | 1 + .../@node-red/editor-client/src/js/plugins.js | 29 +++++++++++++++++++ .../test-plugin/test-editor-plugin.html | 8 +++-- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 packages/node_modules/@node-red/editor-client/src/js/plugins.js diff --git a/Gruntfile.js b/Gruntfile.js index 2bdc2e3aa..23481f793 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -142,6 +142,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/text/bidi.js", "packages/node_modules/@node-red/editor-client/src/js/text/format.js", "packages/node_modules/@node-red/editor-client/src/js/ui/state.js", + "packages/node_modules/@node-red/editor-client/src/js/plugins.js", "packages/node_modules/@node-red/editor-client/src/js/nodes.js", "packages/node_modules/@node-red/editor-client/src/js/font-awesome.js", "packages/node_modules/@node-red/editor-client/src/js/history.js", diff --git a/packages/node_modules/@node-red/editor-client/src/js/plugins.js b/packages/node_modules/@node-red/editor-client/src/js/plugins.js new file mode 100644 index 000000000..25d6acf8a --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/plugins.js @@ -0,0 +1,29 @@ +RED.plugins = (function() { + var plugins = {}; + var pluginsByType = {}; + + function registerPlugin(id,definition) { + plugins[id] = definition; + if (definition.type) { + pluginsByType[definition.type] = pluginsByType[definition.type] || []; + pluginsByType[definition.type].push(definition); + } + if (definition.onadd && typeof definition.onadd === 'function') { + definition.onadd(); + } + RED.events.emit("registry:plugin-added",id); + } + + function getPlugin(id) { + return plugins[id] + } + + function getPluginsByType(type) { + return pluginsByType[type] || []; + } + return { + registerPlugin: registerPlugin, + getPlugin: getPlugin, + getPluginsByType: getPluginsByType + } +})(); diff --git a/test/resources/plugin/test-plugin/test-editor-plugin.html b/test/resources/plugin/test-plugin/test-editor-plugin.html index 067d09d3c..177813526 100644 --- a/test/resources/plugin/test-plugin/test-editor-plugin.html +++ b/test/resources/plugin/test-plugin/test-editor-plugin.html @@ -1,5 +1,9 @@ \ No newline at end of file From 9f71dbb0060c9070fc99f683cf71c5f0f8427679 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 18 Jan 2021 15:11:24 +0000 Subject: [PATCH 12/39] Fixup merge --- packages/node_modules/@node-red/editor-client/src/js/red.js | 1 - packages/node_modules/@node-red/registry/lib/loader.js | 5 ----- 2 files changed, 6 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index e3439c435..229147b10 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -25,7 +25,6 @@ var RED = (function() { cache: false, url: 'plugins', success: function(data) { - console.log(data); loader.reportProgress(RED._("event.loadPlugins"), 13) RED.i18n.loadPluginCatalogs(function() { loadPlugins(function() { diff --git a/packages/node_modules/@node-red/registry/lib/loader.js b/packages/node_modules/@node-red/registry/lib/loader.js index d29eadc66..74eaa05c1 100644 --- a/packages/node_modules/@node-red/registry/lib/loader.js +++ b/packages/node_modules/@node-red/registry/lib/loader.js @@ -478,7 +478,6 @@ function addModule(module) { return Promise.reject(e); } try { -<<<<<<< HEAD var moduleFiles = {}; var moduleStack = [module]; while(moduleStack.length > 0) { @@ -502,10 +501,6 @@ function addModule(module) { } } return loadNodeFiles(moduleFiles).then(() => module) -======= - var moduleFiles = localfilesystem.getModuleFiles(module); - return loadModuleFiles(moduleFiles); ->>>>>>> Initial plugin runtime api implementation } catch(err) { return Promise.reject(err); } From 9e179170ee082da3d3bb30085916e0dfebbaae78 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 20 Jan 2021 15:35:44 +0000 Subject: [PATCH 13/39] Add i18n function to editor plugins when they are registered Adds a `_` function to the plugin definition object that will automatically prepend the plugin's module namespace to any call. This saves the plugin from having to prepend its namespace all of the time. --- .../@node-red/editor-client/src/js/plugins.js | 17 +++++++++++++++++ .../@node-red/editor-client/src/js/red.js | 5 ++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/plugins.js b/packages/node_modules/@node-red/editor-client/src/js/plugins.js index 25d6acf8a..e6f41517d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/plugins.js +++ b/packages/node_modules/@node-red/editor-client/src/js/plugins.js @@ -8,6 +8,23 @@ RED.plugins = (function() { pluginsByType[definition.type] = pluginsByType[definition.type] || []; pluginsByType[definition.type].push(definition); } + if (RED._loadingModule) { + definition.module = RED._loadingModule; + definition["_"] = function() { + var args = Array.prototype.slice.call(arguments); + var originalKey = args[0]; + if (!/:/.test(args[0])) { + args[0] = definition.module+":"+args[0]; + } + var result = RED._.apply(null,args); + if (result === args[0]) { + return originalKey; + } + return result; + } + } else { + definition["_"] = RED["_"] + } if (definition.onadd && typeof definition.onadd === 'function') { definition.onadd(); } diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index 229147b10..a2bcb0e7d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -67,11 +67,11 @@ var RED = (function() { var moduleId; if (moduleIdMatch) { moduleId = moduleIdMatch[1]; + RED._loadingModule = moduleId; } else { moduleId = "unknown"; } try { - var hasDeferred = false; var nodeConfigEls = $("
    "+config+"
    "); var scripts = nodeConfigEls.find("script"); @@ -85,6 +85,7 @@ var RED = (function() { scriptCount--; if (scriptCount === 0) { $(targetContainer).append(nodeConfigEls); + delete RED._loadingModule; done() } } @@ -108,6 +109,7 @@ var RED = (function() { }) if (!hasDeferred) { $(targetContainer).append(nodeConfigEls); + delete RED._loadingModule; done(); } } catch(err) { @@ -116,6 +118,7 @@ var RED = (function() { timeout: 10000 }); console.log("["+moduleId+"] "+err.toString()); + delete RED._loadingModule; done(); } } From ca44af06258298a3f8fb5142838d577366c22358 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Mon, 25 Jan 2021 10:56:23 +0000 Subject: [PATCH 14/39] Prevent crash when coreNodesDir is empty (#2831) * Fix for HTTP-Request not sending body for GET Background in SO question: https://stackoverflow.com/q/60356824/504554 * Prevent crash when coreNodesDir points to empty dir This should prevent a crash when you point to an empty core nodes directory. * Matching upstream master --- .../@node-red/registry/lib/library.js | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/node_modules/@node-red/registry/lib/library.js b/packages/node_modules/@node-red/registry/lib/library.js index a8ca8a775..c45282b89 100644 --- a/packages/node_modules/@node-red/registry/lib/library.js +++ b/packages/node_modules/@node-red/registry/lib/library.js @@ -28,17 +28,19 @@ function getFlowsFromPath(path) { fs.readdir(path,function(err,files) { var promises = []; var validFiles = []; - files.forEach(function(file) { - var fullPath = fspath.join(path,file); - var stats = fs.lstatSync(fullPath); - if (stats.isDirectory()) { - validFiles.push(file); - promises.push(getFlowsFromPath(fullPath)); - } else if (/\.json$/.test(file)){ - validFiles.push(file); - promises.push(Promise.resolve(file.split(".")[0])) - } - }) + if (files) { + files.forEach(function(file) { + var fullPath = fspath.join(path,file); + var stats = fs.lstatSync(fullPath); + if (stats.isDirectory()) { + validFiles.push(file); + promises.push(getFlowsFromPath(fullPath)); + } else if (/\.json$/.test(file)){ + validFiles.push(file); + promises.push(Promise.resolve(file.split(".")[0])) + } + }) + } var i=0; Promise.all(promises).then(function(results) { results.forEach(function(r) { From 441eb3bb293c0fe219060e3783a8d64b23b28305 Mon Sep 17 00:00:00 2001 From: Alex Kaul Date: Mon, 25 Jan 2021 18:02:43 +0700 Subject: [PATCH 15/39] Fix scrollbars (#2825) * Update Russian Locale * Fix scrollbars --- .../node_modules/@node-red/editor-client/src/sass/base.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/node_modules/@node-red/editor-client/src/sass/base.scss b/packages/node_modules/@node-red/editor-client/src/sass/base.scss index 391ddf83f..ca1a5807f 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/base.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/base.scss @@ -15,6 +15,9 @@ **/ +body { + overflow: hidden; +} .red-ui-editor { font-size: $primary-font-size; From 79473c243d4a6d367f943d91be23fe18c7bb3479 Mon Sep 17 00:00:00 2001 From: Alex Kaul Date: Mon, 25 Jan 2021 18:03:51 +0700 Subject: [PATCH 16/39] Fix grunt release mkdir issue on Node.js 14 (#2827) * Update Russian Locale * Upd grunt-mkdir --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1050b4bff..a914a1498 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "grunt-jsdoc": "2.4.1", "grunt-jsdoc-to-markdown": "5.0.0", "grunt-jsonlint": "2.1.3", - "grunt-mkdir": "~1.0.0", + "grunt-mkdir": "~1.1.0", "grunt-npm-command": "~0.1.2", "grunt-sass": "~3.1.0", "grunt-simple-mocha": "~0.4.1", From a0f736bb888adf3fb11574ef4e1e835959afa383 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 25 Jan 2021 17:06:27 +0000 Subject: [PATCH 17/39] Validate user-provided language parameter before passing to i18n --- .../editor-api/lib/editor/locales.js | 5 +- .../@node-red/runtime/lib/api/nodes.js | 16 ++++++ .../node_modules/@node-red/util/lib/i18n.js | 56 +++++++++---------- 3 files changed, 46 insertions(+), 31 deletions(-) diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/locales.js b/packages/node_modules/@node-red/editor-api/lib/editor/locales.js index a7f300cd3..ebe3e3281 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/locales.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/locales.js @@ -39,9 +39,12 @@ module.exports = { }, get: function(req,res) { var namespace = req.params[0]; - var lngs = req.query.lng; namespace = namespace.replace(/\.json$/,""); var lang = req.query.lng || i18n.defaultLang; //apiUtil.determineLangFromHeaders(req.acceptsLanguages() || []); + if (/[^a-z\-]/i.test(lang)) { + res.json({}); + return; + } var prevLang = i18n.i.language; // Trigger a load from disk of the language if it is not the default i18n.i.changeLanguage(lang, function(){ diff --git a/packages/node_modules/@node-red/runtime/lib/api/nodes.js b/packages/node_modules/@node-red/runtime/lib/api/nodes.js index a06cbed96..aa5eca100 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/nodes.js +++ b/packages/node_modules/@node-red/runtime/lib/api/nodes.js @@ -99,6 +99,10 @@ var api = module.exports = { return new Promise(function(resolve,reject) { var id = opts.id; var lang = opts.lang; + if (/[^a-z\-]/i.test(opts.lang)) { + reject(new Error("Invalid language: "+opts.lang)); + return + } var result = runtime.nodes.getNodeConfig(id,lang); if (result) { runtime.log.audit({event: "nodes.config.get",id:id}, opts.req); @@ -124,6 +128,10 @@ var api = module.exports = { getNodeConfigs: function(opts) { return new Promise(function(resolve,reject) { runtime.log.audit({event: "nodes.configs.get"}, opts.req); + if (/[^a-z\-]/i.test(opts.lang)) { + reject(new Error("Invalid language: "+opts.lang)); + return + } return resolve(runtime.nodes.getNodeConfigs(opts.lang)); }); }, @@ -398,6 +406,10 @@ var api = module.exports = { var namespace = opts.module; var lang = opts.lang; var prevLang = runtime.i18n.i.language; + if (/[^a-z\-]/i.test(lang)) { + reject(new Error("Invalid language: "+lang)); + return + } // Trigger a load from disk of the language if it is not the default runtime.i18n.i.changeLanguage(lang, function(){ var nodeList = runtime.nodes.getNodeList(); @@ -427,6 +439,10 @@ var api = module.exports = { return new Promise(function(resolve,reject) { var namespace = opts.module; var lang = opts.lang; + if (/[^a-z\-]/i.test(lang)) { + reject(new Error("Invalid language: "+lang)); + return + } var prevLang = runtime.i18n.i.language; // Trigger a load from disk of the language if it is not the default runtime.i18n.i.changeLanguage(lang, function(){ diff --git a/packages/node_modules/@node-red/util/lib/i18n.js b/packages/node_modules/@node-red/util/lib/i18n.js index d95f4bec6..5d5a18887 100644 --- a/packages/node_modules/@node-red/util/lib/i18n.js +++ b/packages/node_modules/@node-red/util/lib/i18n.js @@ -24,7 +24,7 @@ var i18n = require("i18next"); var when = require("when"); var path = require("path"); -var fs = require("fs"); +var fs = require("fs-extra"); var defaultLang = "en-US"; @@ -82,36 +82,28 @@ function mergeCatalog(fallback,catalog) { } -function readFile(lng, ns) { - return new Promise((resolve, reject) => { - if (resourceCache[ns] && resourceCache[ns][lng]) { - resolve(resourceCache[ns][lng]); - } else if (resourceMap[ns]) { - var file = path.join(resourceMap[ns].basedir, lng, resourceMap[ns].file); - fs.readFile(file, "utf8", function (err, content) { - if (err) { - reject(err); - } else { - try { - resourceCache[ns] = resourceCache[ns] || {}; - resourceCache[ns][lng] = JSON.parse(content.replace(/^\uFEFF/, '')); - var baseLng = lng.split('-')[0]; - if (baseLng !== lng && resourceCache[ns][baseLng]) { - mergeCatalog(resourceCache[ns][baseLng], resourceCache[ns][lng]); - } - if (lng !== defaultLang) { - mergeCatalog(resourceCache[ns][defaultLang], resourceCache[ns][lng]); - } - resolve(resourceCache[ns][lng]); - } catch (e) { - reject(e); - } - } - }); - } else { - reject(new Error("Unrecognised namespace")); +async function readFile(lng, ns) { + if (/[^a-z\-]/i.test(lng)) { + throw new Error("Invalid language: "+lng) + } + if (resourceCache[ns] && resourceCache[ns][lng]) { + return resourceCache[ns][lng]; + } else if (resourceMap[ns]) { + const file = path.join(resourceMap[ns].basedir, lng, resourceMap[ns].file); + const content = await fs.readFile(file, "utf8"); + resourceCache[ns] = resourceCache[ns] || {}; + resourceCache[ns][lng] = JSON.parse(content.replace(/^\uFEFF/, '')); + var baseLng = lng.split('-')[0]; + if (baseLng !== lng && resourceCache[ns][baseLng]) { + mergeCatalog(resourceCache[ns][baseLng], resourceCache[ns][lng]); } - }); + if (lng !== defaultLang) { + mergeCatalog(resourceCache[ns][defaultLang], resourceCache[ns][lng]); + } + return resourceCache[ns][lng]; + } else { + throw new Error("Unrecognised namespace"); + } } var MessageFileLoader = { @@ -182,6 +174,10 @@ function init() { function getCatalog(namespace,lang) { var result = null; lang = lang || defaultLang; + if (/[^a-z\-]/i.test(lang)) { + throw new Error("Invalid language: "+lng) + } + if (resourceCache.hasOwnProperty(namespace)) { result = resourceCache[namespace][lang]; if (!result) { From 70554e24b107388d28f01096539f31d762d13624 Mon Sep 17 00:00:00 2001 From: Alex Kaul Date: Tue, 26 Jan 2021 00:25:06 +0700 Subject: [PATCH 18/39] Improve Ru locale (#2826) * Update Russian Locale * Upd ru translation for "timestamp" * Improve node help texts for ru locale * Improve editor texts for ru locale --- .../editor-client/locales/ru/editor.json | 32 +++++++++---------- .../nodes/locales/ru/common/20-inject.html | 16 ++++------ .../nodes/locales/ru/common/21-debug.html | 14 ++++---- .../nodes/locales/ru/common/24-complete.html | 8 ++--- .../nodes/locales/ru/common/25-catch.html | 10 +++--- .../nodes/locales/ru/common/25-status.html | 6 ++-- .../nodes/locales/ru/common/60-link.html | 12 +++---- .../nodes/locales/ru/common/90-comment.html | 4 +-- .../nodes/locales/ru/common/98-unknown.html | 10 +++--- .../locales/ru/function/10-function.html | 18 +++++------ .../nodes/locales/ru/function/10-switch.html | 14 ++++---- .../nodes/locales/ru/function/15-change.html | 10 +++--- .../nodes/locales/ru/function/16-range.html | 6 ++-- .../locales/ru/function/80-template.html | 16 +++++----- .../nodes/locales/ru/function/89-delay.html | 10 +++--- .../nodes/locales/ru/function/89-trigger.html | 12 +++---- .../nodes/locales/ru/function/90-exec.html | 24 +++++++------- .../@node-red/nodes/locales/ru/messages.json | 4 +-- .../nodes/locales/ru/network/05-tls.html | 2 +- .../locales/ru/network/06-httpproxy.html | 2 +- .../nodes/locales/ru/network/10-mqtt.html | 14 ++++---- .../nodes/locales/ru/network/21-httpin.html | 10 +++--- .../locales/ru/network/21-httprequest.html | 10 +++--- .../locales/ru/network/22-websocket.html | 8 ++--- .../nodes/locales/ru/network/31-tcpin.html | 14 ++++---- .../nodes/locales/ru/network/32-udp.html | 6 ++-- .../nodes/locales/ru/parsers/70-CSV.html | 10 +++--- .../nodes/locales/ru/parsers/70-JSON.html | 6 ++-- .../nodes/locales/ru/parsers/70-XML.html | 4 +-- .../nodes/locales/ru/parsers/70-YAML.html | 4 +-- .../nodes/locales/ru/sequence/17-split.html | 14 ++++---- .../nodes/locales/ru/sequence/18-sort.html | 2 +- .../nodes/locales/ru/sequence/19-batch.html | 2 +- .../nodes/locales/ru/storage/10-file.html | 12 +++---- .../nodes/locales/ru/storage/23-watch.html | 10 +++--- 35 files changed, 177 insertions(+), 179 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/ru/editor.json b/packages/node_modules/@node-red/editor-client/locales/ru/editor.json index 1f91d4b0b..04ff361ba 100755 --- a/packages/node_modules/@node-red/editor-client/locales/ru/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/ru/editor.json @@ -246,8 +246,8 @@ "import": { "import": "Импортировать в", "importSelected": "Импортировать выбранные", - "importCopy": "Импортировать копию", - "viewNodes": "Посмотреть узлы...", + "importCopy": "Импортировать копии", + "viewNodes": "Показать узлы...", "newFlow": "новый поток", "replace": "заменить", "errors": { @@ -257,7 +257,7 @@ "missingType": "Недопустимый поток - у элемента __index__ отсутствует свойство 'type'" }, "conflictNotification1": "Некоторые импортируемые Вами узлы уже существуют в рабочей области.", - "conflictNotification2": "Выберите, какие узлы импортировать и следует ли заменить существующие узлы или импортировать их копию." + "conflictNotification2": "Выберите, какие узлы импортировать и следует ли заменить ими существующие узлы или импортировать их копии." }, "copyMessagePath": "Путь скопирован", "copyMessageValue": "Значение скопировано", @@ -373,12 +373,12 @@ "configAdd": "Добавить", "configUpdate": "Обновить", "configDelete": "Удалить", - "nodesUse": "__count__ узел использует эту конфигурацию", - "nodesUse_plural_2": "__count__ узла используют эту конфигурацию", - "nodesUse_plural_5": "__count__ узлов используют эту конфигурацию", - "addNewConfig": "Добавить новый конфигурационный узел __type__", + "nodesUse": "__count__ узел использует этот конфиг", + "nodesUse_plural_2": "__count__ узла используют этот конфиг", + "nodesUse_plural_5": "__count__ узлов используют этот конфиг", + "addNewConfig": "Добавить новый конфиг узел __type__", "editNode": "Изменить узел __type__", - "editConfig": "Изменить конфигурационный узел __type__", + "editConfig": "Изменить конфиг узел __type__", "addNewType": "Добавить новый __type__...", "nodeProperties": "свойства узла", "label": "Метка", @@ -615,7 +615,7 @@ "info": { "name": "Информация", "tabName": "Имя", - "label": "сведения", + "label": "инфо", "node": "Узел", "type": "Тип", "group": "Группа", @@ -648,8 +648,8 @@ "showTips":"Вы можете открыть советы из панели настроек", "outline": "Структура", "empty": "пусто", - "globalConfig": "Узлы глобальной конфигурации", - "triggerAction": "Запустить действие", + "globalConfig": "Глобальные конфиг узлы", + "triggerAction": "Вызвать действие", "find": "Найти в рабочей области", "search": { "configNodes": "Узлы конфигурации", @@ -671,8 +671,8 @@ }, "config": { "name": "Узлы конфигураций", - "label": "конфигурация", - "global": "На всех потока", + "label": "конфиг", + "global": "На всех потоках", "none": "нет", "subflows": "подпотоки", "flows": "потоки", @@ -690,8 +690,8 @@ "none": "ничего не выбрано", "refresh": "обновите, чтобы загрузить", "empty": "пусто", - "node": "Узел", - "flow": "Поток", + "node": "Узловой", + "flow": "Потоковый", "global": "Глобальный", "deleteConfirm": "Вы уверены, что хотите удалить этот элемент?", "autoRefresh": "Обновить при изменении выбора", @@ -877,7 +877,7 @@ "bool": "логический тип", "json": "JSON", "bin": "буфер", - "date": "отметка времени", + "date": "метка времени", "jsonata": "выражение", "env": "переменная среды", "cred": "учетные данные" diff --git a/packages/node_modules/@node-red/nodes/locales/ru/common/20-inject.html b/packages/node_modules/@node-red/nodes/locales/ru/common/20-inject.html index e265fccc6..5b762bc7d 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/common/20-inject.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/common/20-inject.html @@ -16,7 +16,7 @@ diff --git a/packages/node_modules/@node-red/nodes/locales/ru/common/21-debug.html b/packages/node_modules/@node-red/nodes/locales/ru/common/21-debug.html index 66edb72cc..7804e6af5 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/common/21-debug.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/common/21-debug.html @@ -16,24 +16,24 @@ diff --git a/packages/node_modules/@node-red/nodes/locales/ru/common/24-complete.html b/packages/node_modules/@node-red/nodes/locales/ru/common/24-complete.html index 3709f82dc..672310d6f 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/common/24-complete.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/common/24-complete.html @@ -16,7 +16,7 @@ diff --git a/packages/node_modules/@node-red/nodes/locales/ru/common/25-catch.html b/packages/node_modules/@node-red/nodes/locales/ru/common/25-catch.html index 867e9b203..af1627c22 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/common/25-catch.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/common/25-catch.html @@ -16,7 +16,7 @@ diff --git a/packages/node_modules/@node-red/nodes/locales/ru/common/60-link.html b/packages/node_modules/@node-red/nodes/locales/ru/common/60-link.html index 1ef3079af..f2894da97 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/common/60-link.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/common/60-link.html @@ -21,13 +21,13 @@

    Подробности

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

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

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

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

    Подробности

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

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

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

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

    Принимает

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

    Подробности

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

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

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

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

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

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

    Принимает

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

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

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

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

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

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

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

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

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

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

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

    Подробности

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

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

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

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

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

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

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

    Подробности

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

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

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

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

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

    Выводит

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

      Подробности

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

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

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

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

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

      Подробности

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

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

      Ручной режим

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

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

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

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

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

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

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

    Если сообщение получено с установленным свойством msg.reset, буферизованные сообщения удаляются и не отправляются. diff --git a/packages/node_modules/@node-red/nodes/locales/ru/storage/10-file.html b/packages/node_modules/@node-red/nodes/locales/ru/storage/10-file.html index e1c8bf456..8a4a64f24 100644 --- a/packages/node_modules/@node-red/nodes/locales/ru/storage/10-file.html +++ b/packages/node_modules/@node-red/nodes/locales/ru/storage/10-file.html @@ -16,7 +16,7 @@ From 6e718ca77211221abdc31754c3638ca3ad60ca47 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 26 Jan 2021 13:44:38 +0000 Subject: [PATCH 19/39] Fix merge of dev --- packages/node_modules/@node-red/registry/lib/loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/registry/lib/loader.js b/packages/node_modules/@node-red/registry/lib/loader.js index 74eaa05c1..bd5a7aae4 100644 --- a/packages/node_modules/@node-red/registry/lib/loader.js +++ b/packages/node_modules/@node-red/registry/lib/loader.js @@ -500,7 +500,7 @@ function addModule(module) { } } } - return loadNodeFiles(moduleFiles).then(() => module) + return loadModuleFiles(moduleFiles).then(() => module) } catch(err) { return Promise.reject(err); } From 8e7a230dbc9078047aa725813355c3a0ee884891 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 26 Jan 2021 13:49:13 +0000 Subject: [PATCH 20/39] Fix plugin test to expect user flag --- test/unit/@node-red/registry/lib/plugins_spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit/@node-red/registry/lib/plugins_spec.js b/test/unit/@node-red/registry/lib/plugins_spec.js index 430553fac..8f32d3802 100644 --- a/test/unit/@node-red/registry/lib/plugins_spec.js +++ b/test/unit/@node-red/registry/lib/plugins_spec.js @@ -120,6 +120,7 @@ test-module-config`) "id": "test-module/test-set", "enabled": true, "local": false, + "user": false, "plugins": [ { "type": "foo", @@ -142,6 +143,7 @@ test-module-config`) "id": "test-module/test-disabled-set", "enabled": false, "local": false, + "user": false, "plugins": [] } ] From 1f6328bf4e9a4f12e914816782008376f6c4b3bd Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 26 Jan 2021 13:35:40 +0000 Subject: [PATCH 21/39] Add initial support for ThemePlugins --- .../@node-red/editor-api/lib/admin/plugins.js | 8 +++ .../@node-red/editor-api/lib/editor/index.js | 2 +- .../@node-red/editor-api/lib/editor/theme.js | 55 +++++++++++++++++-- .../@node-red/editor-api/lib/editor/ui.js | 4 +- .../@node-red/editor-client/src/js/red.js | 13 ++++- .../editor-client/src/js/ui/userSettings.js | 10 +++- .../@node-red/registry/lib/plugins.js | 3 + .../@node-red/runtime/lib/api/plugins.js | 44 ++++++++++++++- .../editor-api/lib/editor/theme_spec.js | 8 +-- 9 files changed, 127 insertions(+), 20 deletions(-) diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/plugins.js b/packages/node_modules/@node-red/editor-api/lib/admin/plugins.js index 15428f86f..3d49a7e8a 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/plugins.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/plugins.js @@ -17,6 +17,10 @@ module.exports = { }) } else { opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages()); + if (/[^a-z\-\*]/i.test(opts.lang)) { + res.json({}); + return; + } runtimeAPI.plugins.getPluginConfigs(opts).then(function(configs) { res.send(configs); }) @@ -28,6 +32,10 @@ module.exports = { lang: req.query.lng, req: apiUtils.getRequestLogObject(req) } + if (/[^a-z\-\*]/i.test(opts.lang)) { + res.json({}); + return; + } runtimeAPI.plugins.getPluginCatalogs(opts).then(function(result) { res.json(result); }).catch(function(err) { diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/index.js b/packages/node_modules/@node-red/editor-api/lib/editor/index.js index 71876eaa6..2e8333f3f 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/index.js @@ -76,7 +76,7 @@ module.exports = { editorApp.get("/icons/:scope/:module/:icon",ui.icon); var theme = require("./theme"); - theme.init(settings); + theme.init(settings, runtimeAPI); editorApp.use("/theme",theme.app()); editorApp.use("/",ui.editorResources); diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js index 17dbbafea..52f7974ec 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js @@ -41,6 +41,10 @@ var theme = null; var themeContext = clone(defaultContext); var themeSettings = null; +var activeTheme = null; +var activeThemeInitialised = false; + +var runtimeAPI; var themeApp; function serveFile(app,baseUrl,file) { @@ -58,7 +62,7 @@ function serveFile(app,baseUrl,file) { } } -function serveFilesFromTheme(themeValue, themeApp, directory) { +function serveFilesFromTheme(themeValue, themeApp, directory, baseDirectory) { var result = []; if (themeValue) { var array = themeValue; @@ -67,7 +71,14 @@ function serveFilesFromTheme(themeValue, themeApp, directory) { } for (var i=0;i theme.id); res.json(themeContext); }) @@ -185,10 +200,38 @@ module.exports = { themeSettings.projects = theme.projects; } - + if (theme.theme) { + themeSettings.theme = theme.theme; + } return themeApp; }, - context: function() { + context: async function() { + if (activeTheme && !activeThemeInitialised) { + const themePlugin = await runtimeAPI.plugins.getPlugin({ + id:activeTheme + }); + if (themePlugin) { + if (themePlugin.css) { + const cssFiles = serveFilesFromTheme( + themePlugin.css, + themeApp, + "/css/", + themePlugin.path + ); + themeContext.page.css = cssFiles.concat(themeContext.page.css || []) + } + if (themePlugin.scripts) { + const scriptFiles = serveFilesFromTheme( + themePlugin.scripts, + themeApp, + "/scripts/", + themePlugin.path + ) + themeContext.page.scripts = scriptFiles.concat(themeContext.page.scripts || []) + } + } + activeThemeInitialised = true; + } return themeContext; }, settings: function() { diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/ui.js b/packages/node_modules/@node-red/editor-api/lib/editor/ui.js index 24b819fec..a4812a668 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/ui.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/ui.js @@ -68,8 +68,8 @@ module.exports = { apiUtils.rejectHandler(req,res,err); }) }, - editor: function(req,res) { - res.send(Mustache.render(editorTemplate,theme.context())); + editor: async function(req,res) { + res.send(Mustache.render(editorTemplate,await theme.context())); }, editorResources: express.static(path.join(editorClientDir,'public')) }; diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index a2bcb0e7d..0b33855fe 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -681,9 +681,12 @@ var RED = (function() { $('').html(theme.header.title).appendTo(logo); } } + if (theme.themes) { + knownThemes = theme.themes; + } }); } - + var knownThemes = null; var initialised = false; function init(options) { @@ -703,7 +706,13 @@ var RED = (function() { buildEditor(options); RED.i18n.init(options, function() { - RED.settings.init(options, loadEditor); + RED.settings.init(options, function() { + if (knownThemes) { + RED.settings.editorTheme = RED.settings.editorTheme || {}; + RED.settings.editorTheme.themes = knownThemes; + } + loadEditor(); + }); }) } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js b/packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js index 07ab12180..2bdae4b13 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js @@ -109,13 +109,19 @@ RED.userSettings = (function() { function compText(a, b) { return a.text.localeCompare(b.text); } - + var viewSettings = [ { options: [ {setting:"editor-language",local: true, label:"menu.label.view.language",options:function(done){ done([{val:'',text:RED._('menu.label.view.browserDefault')}].concat(RED.settings.theme("languages").map(localeToName).sort(compText))) }}, ] - },{ + }, + // { + // options: [ + // {setting:"theme", label:"Theme",options:function(done){ done([{val:'',text:'default'}].concat(RED.settings.theme("themes"))) }}, + // ] + // }, + { title: "menu.label.view.grid", options: [ {setting:"view-show-grid",oldSetting:"menu-menu-item-view-show-grid",label:"menu.label.view.showGrid", default: true, toggle:true,onchange:"core:toggle-show-grid"}, diff --git a/packages/node_modules/@node-red/registry/lib/plugins.js b/packages/node_modules/@node-red/registry/lib/plugins.js index c2bb2298d..82b230011 100644 --- a/packages/node_modules/@node-red/registry/lib/plugins.js +++ b/packages/node_modules/@node-red/registry/lib/plugins.js @@ -24,6 +24,9 @@ function registerPlugin(nodeSetId,id,definition) { pluginToId[id] = nodeSetId; plugins[id] = definition; var module = registry.getModule(moduleId); + + definition.path = module.path; + module.plugins[pluginId].plugins.push(definition); if (definition.type) { pluginsByType[definition.type] = pluginsByType[definition.type] || []; diff --git a/packages/node_modules/@node-red/runtime/lib/api/plugins.js b/packages/node_modules/@node-red/runtime/lib/api/plugins.js index ee35d445a..076638640 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/plugins.js +++ b/packages/node_modules/@node-red/runtime/lib/api/plugins.js @@ -9,21 +9,59 @@ var api = module.exports = { runtime = _runtime; }, + /** + * Gets a plugin definition from the registry + * @param {Object} opts + * @param {String} opts.id - the id of the plugin to get + * @param {User} opts.user - the user calling the api + * @param {Object} opts.req - the request to log (optional) + * @return {Promise} - the plugin definition + * @memberof @node-red/runtime_plugins + */ + getPlugin: async function(opts) { + return runtime.plugins.getPlugin(opts.id); + }, + + /** + * Gets all plugin definitions of a given type + * @param {Object} opts + * @param {String} opts.type - the type of the plugins to get + * @param {User} opts.user - the user calling the api + * @param {Object} opts.req - the request to log (optional) + * @return {Promise} - the plugin definitions + * @memberof @node-red/runtime_plugins + */ + getPluginsByType: async function(opts) { + return runtime.plugins.getPluginsByType(opts.type); + }, /** * Gets the editor content for an individual plugin - * @param {Object} opts + * @param {String} opts.lang - the locale language to return * @param {User} opts.user - the user calling the api * @param {Object} opts.req - the request to log (optional) * @return {Promise} - the node information - * @memberof @node-red/runtime_nodes + * @memberof @node-red/runtime_plugins */ getPluginList: async function(opts) { runtime.log.audit({event: "plugins.list.get"}, opts.req); return runtime.plugins.getPluginList(); }, + /** + * Gets the editor content for all registered plugins + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {User} opts.user - the user calling the api + * @param {Object} opts.req - the request to log (optional) + * @return {Promise} - the node information + * @memberof @node-red/runtime_plugins + */ getPluginConfigs: async function(opts) { + if (/[^a-z\-]/i.test(opts.lang)) { + throw new Error("Invalid language: "+opts.lang) + return; + } runtime.log.audit({event: "plugins.configs.get"}, opts.req); return runtime.plugins.getPluginConfigs(opts.lang); }, @@ -34,7 +72,7 @@ var api = module.exports = { * @param {User} opts.lang - the i18n language to return. If not set, uses runtime default (en-US) * @param {Object} opts.req - the request to log (optional) * @return {Promise} - the message catalogs - * @memberof @node-red/runtime_nodes + * @memberof @node-red/runtime_plugins */ getPluginCatalogs: async function(opts) { var lang = opts.lang; diff --git a/test/unit/@node-red/editor-api/lib/editor/theme_spec.js b/test/unit/@node-red/editor-api/lib/editor/theme_spec.js index 5c8123d61..2fa43e01a 100644 --- a/test/unit/@node-red/editor-api/lib/editor/theme_spec.js +++ b/test/unit/@node-red/editor-api/lib/editor/theme_spec.js @@ -33,11 +33,11 @@ describe("api/editor/theme", function () { theme.init({settings: {}}); fs.statSync.restore(); }); - it("applies the default theme", function () { + it("applies the default theme", async function () { var result = theme.init({}); should.not.exist(result); - var context = theme.context(); + var context = await theme.context(); context.should.have.a.property("page"); context.page.should.have.a.property("title", "Node-RED"); context.page.should.have.a.property("favicon", "favicon.ico"); @@ -52,7 +52,7 @@ describe("api/editor/theme", function () { should.not.exist(theme.settings()); }); - it("picks up custom theme", function () { + it("picks up custom theme", async function () { theme.init({ editorTheme: { page: { @@ -104,7 +104,7 @@ describe("api/editor/theme", function () { theme.app(); - var context = theme.context(); + var context = await theme.context(); context.should.have.a.property("page"); context.page.should.have.a.property("title", "Test Page Title"); context.page.should.have.a.property("favicon", "theme/favicon/favicon"); From 34ef055d7b454cbee0fafae0cd082eb6dc8c64a2 Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Thu, 28 Jan 2021 05:32:15 +0900 Subject: [PATCH 22/39] Fix line break of subflow label on palette (#2828) * fix line break of subflow label on palette * handle line break on palette --- .../@node-red/editor-client/src/js/ui/palette.js | 7 ++++++- .../node_modules/@node-red/editor-client/src/js/ui/view.js | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js index bd0b55cba..1a82a3e89 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js @@ -97,13 +97,18 @@ RED.palette = (function() { label = RED.utils.sanitize(label); - var words = label.split(/[ -]/); + var words = label.split(/([ -]|\\n )/); var displayLines = []; var currentLine = ""; for (var i=0;i Date: Wed, 27 Jan 2021 20:32:52 +0000 Subject: [PATCH 23/39] Allow nested msg properties in msg/flow/global expressions (#2822) * Allow nested msg properties in msg/flow/global expressions * Remove typo in RED.utils Co-authored-by: Nick O'Leary --- .../editor-client/src/js/ui/utils.js | 85 +++++++++++--- .../nodes/core/function/15-change.js | 8 ++ .../node_modules/@node-red/util/lib/util.js | 85 +++++++++++++- test/nodes/core/function/10-switch_spec.js | 31 ++++- test/nodes/core/function/15-change_spec.js | 107 +++++++++++++++++- test/unit/@node-red/util/lib/util_spec.js | 57 +++++++++- 6 files changed, 342 insertions(+), 31 deletions(-) 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 230f561f9..599b5dd8e 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js @@ -615,18 +615,25 @@ RED.utils = (function() { return element; } - function normalisePropertyExpression(str) { + function createError(code, message) { + var e = new Error(message); + e.code = code; + return e; + } + + function normalisePropertyExpression(str,msg) { // This must be kept in sync with validatePropertyExpression // in editor/js/ui/utils.js var length = str.length; if (length === 0) { - throw new Error("Invalid property expression: zero-length"); + throw createError("INVALID_EXPR","Invalid property expression: zero-length"); } var parts = []; var start = 0; var inString = false; var inBox = false; + var boxExpression = false; var quoteChar; var v; for (var i=0;i 0) { + throw createError("INVALID_EXPR","Invalid property expression: unmatched '[' at position "+i); + } + continue; + } else if (!/["'\d]/.test(str[i+1])) { + // Next char is either a quote or a number + throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1)); } start = i+1; inBox = true; } else if (c === ']') { if (!inBox) { - throw new Error("Invalid property expression: unexpected "+c+" at position "+i); + throw createError("INVALID_EXPR","Invalid property expression: unexpected "+c+" at position "+i); } if (start != i) { v = str.substring(start,i); if (/^\d+$/.test(v)) { parts.push(parseInt(v)); } else { - throw new Error("Invalid property expression: unexpected array expression at position "+start); + throw createError("INVALID_EXPR","Invalid property expression: unexpected array expression at position "+start); } } start = i+1; inBox = false; } else if (c === ' ') { - throw new Error("Invalid property expression: unexpected ' ' at position "+i); + throw createError("INVALID_EXPR","Invalid property expression: unexpected ' ' at position "+i); } } else { if (c === quoteChar) { if (i-start === 0) { - throw new Error("Invalid property expression: zero-length string at position "+start); + throw createError("INVALID_EXPR","Invalid property expression: zero-length string at position "+start); } parts.push(str.substring(start,i)); // If inBox, next char must be a ]. Otherwise it may be [ or . if (inBox && !/\]/.test(str[i+1])) { - throw new Error("Invalid property expression: unexpected array expression at position "+start); + throw createError("INVALID_EXPR","Invalid property expression: unexpected array expression at position "+start); } else if (!inBox && i+1!==length && !/[\[\.]/.test(str[i+1])) { - throw new Error("Invalid property expression: unexpected "+str[i+1]+" expression at position "+(i+1)); + throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" expression at position "+(i+1)); } start = i+1; inString = false; @@ -711,7 +760,7 @@ RED.utils = (function() { } if (inBox || inString) { - throw new Error("Invalid property expression: unterminated expression"); + throw new createError("INVALID_EXPR","Invalid property expression: unterminated expression"); } if (start < length) { parts.push(str.substring(start)); diff --git a/packages/node_modules/@node-red/nodes/core/function/15-change.js b/packages/node_modules/@node-red/nodes/core/function/15-change.js index 0eb18eff3..55b9f44e9 100644 --- a/packages/node_modules/@node-red/nodes/core/function/15-change.js +++ b/packages/node_modules/@node-red/nodes/core/function/15-change.js @@ -168,6 +168,10 @@ module.exports = function(RED) { return getFromValueType(RED.util.getMessageProperty(msg,rule.from),done); } else if (rule.fromt === 'flow' || rule.fromt === 'global') { var contextKey = RED.util.parseContextStore(rule.from); + if (/\[msg\./.test(context.key)) { + // The key has a nest msg. reference to evaluate first + context.key = RED.util.normalisePropertyExpression(contextKey.key,msg,true); + } node.context()[rule.fromt].get(contextKey.key, contextKey.store, (err,fromValue) => { if (err) { done(err) @@ -243,6 +247,10 @@ module.exports = function(RED) { return done(undefined,msg); } else if (rule.pt === 'flow' || rule.pt === 'global') { var contextKey = RED.util.parseContextStore(property); + if (/\[msg/.test(contextKey.key)) { + // The key has a nest msg. reference to evaluate first + contextKey.key = RED.util.normalisePropertyExpression(contextKey.key, msg, true) + } var target = node.context()[rule.pt]; var callback = err => { if (err) { diff --git a/packages/node_modules/@node-red/util/lib/util.js b/packages/node_modules/@node-red/util/lib/util.js index 07f506007..54da6b1f9 100644 --- a/packages/node_modules/@node-red/util/lib/util.js +++ b/packages/node_modules/@node-red/util/lib/util.js @@ -189,11 +189,17 @@ function createError(code, message) { * * For example, `a["b"].c` returns `['a','b','c']` * + * If `msg` is provided, any internal cross-references will be evaluated against that + * object. Otherwise, it will return a nested set of properties + * + * For example, without msg set, 'a[msg.foo]' returns `['a', [ 'msg', 'foo'] ]` + * But if msg is set to '{"foo": "bar"}', 'a[msg.foo]' returns `['a', 'bar' ]` + * * @param {String} str - the property expression * @return {Array} the normalised expression * @memberof @node-red/util_util */ -function normalisePropertyExpression(str) { +function normalisePropertyExpression(str, msg, toString) { // This must be kept in sync with validatePropertyExpression // in editor/js/ui/utils.js @@ -205,6 +211,7 @@ function normalisePropertyExpression(str) { var start = 0; var inString = false; var inBox = false; + var boxExpression = false; var quoteChar; var v; for (var i=0;i 0) { + throw createError("INVALID_EXPR","Invalid property expression: unmatched '[' at position "+i); + } + continue; + } else if (!/["'\d]/.test(str[i+1])) { + // Next char is either a quote or a number throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1)); } start = i+1; @@ -294,6 +347,23 @@ function normalisePropertyExpression(str) { if (start < length) { parts.push(str.substring(start)); } + + if (toString) { + var result = parts.shift(); + while(parts.length > 0) { + var p = parts.shift(); + if (typeof p === 'string') { + if (/"/.test(p)) { + p = "'"+p+"'"; + } else { + p = '"'+p+'"'; + } + } + result = result+"["+p+"]"; + } + return result; + } + return parts; } @@ -340,8 +410,7 @@ function getMessageProperty(msg,expr) { */ function getObjectProperty(msg,expr) { var result = null; - var msgPropParts = normalisePropertyExpression(expr); - var m; + var msgPropParts = normalisePropertyExpression(expr,msg); msgPropParts.reduce(function(obj, key) { result = (typeof obj[key] !== "undefined" ? obj[key] : undefined); return result; @@ -381,7 +450,7 @@ function setObjectProperty(msg,prop,value,createMissing) { if (typeof createMissing === 'undefined') { createMissing = (typeof value !== 'undefined'); } - var msgPropParts = normalisePropertyExpression(prop); + var msgPropParts = normalisePropertyExpression(prop, msg); var depth = 0; var length = msgPropParts.length; var obj = msg; @@ -553,6 +622,10 @@ function evaluateNodeProperty(value, type, node, msg, callback) { } } else if ((type === 'flow' || type === 'global') && node) { var contextKey = parseContextStore(value); + if (/\[msg/.test(contextKey.key)) { + // The key has a nest msg. reference to evaluate first + contextKey.key = normalisePropertyExpression(contextKey.key, msg, true) + } result = node.context()[type].get(contextKey.key,contextKey.store,callback); if (callback) { return; diff --git a/test/nodes/core/function/10-switch_spec.js b/test/nodes/core/function/10-switch_spec.js index b431fcaaf..dcb7dfd45 100644 --- a/test/nodes/core/function/10-switch_spec.js +++ b/test/nodes/core/function/10-switch_spec.js @@ -119,13 +119,17 @@ describe('switch Node', function() { * @param done - callback when done */ function customFlowSwitchTest(flow, shouldReceive, sendPayload, done) { + customFlowMessageSwitchTest(flow,shouldReceive,{payload: sendPayload}, done); + } + + function customFlowMessageSwitchTest(flow, shouldReceive, message, done) { helper.load(switchNode, flow, function() { var switchNode1 = helper.getNode("switchNode1"); var helperNode1 = helper.getNode("helperNode1"); helperNode1.on("input", function(msg) { try { if (shouldReceive === true) { - should.equal(msg.payload,sendPayload); + should.equal(msg,message); done(); } else { should.fail(null, null, "We should never get an input!"); @@ -134,7 +138,7 @@ describe('switch Node', function() { done(err); } }); - switchNode1.receive({payload:sendPayload}); + switchNode1.receive(message); if (shouldReceive === false) { setTimeout(function() { done(); @@ -425,6 +429,29 @@ describe('switch Node', function() { }); }); + it('should use a nested message property to compare value - matches', function(done) { + var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload[msg.topic]",rules:[{"t":"eq","v":"bar"}],checkall:true,outputs:1,wires:[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + customFlowMessageSwitchTest(flow, true, {topic:"foo",payload:{"foo":"bar"}}, done); + }) + it('should use a nested message property to compare value - no match', function(done) { + var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload[msg.topic]",rules:[{"t":"eq","v":"bar"}],checkall:true,outputs:1,wires:[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + customFlowMessageSwitchTest(flow, false, {topic:"foo",payload:{"foo":"none"}}, done); + + }) + + it('should use a nested message property to compare nested message property - matches', function(done) { + var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload[msg.topic]",rules:[{"t":"eq","v":"payload[msg.topic2]",vt:"msg"}],checkall:true,outputs:1,wires:[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + customFlowMessageSwitchTest(flow, true, {topic:"foo",topic2:"foo2",payload:{"foo":"bar","foo2":"bar"}}, done); + }) + it('should use a nested message property to compare nested message property - no match', function(done) { + var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload[msg.topic]",rules:[{"t":"eq","v":"payload[msg.topic2]",vt:"msg"}],checkall:true,outputs:1,wires:[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + customFlowMessageSwitchTest(flow, false, {topic:"foo",topic2:"foo2",payload:{"foo":"bar","foo2":"none"}}, done); + }) + it('should match regex with ignore-case flag set true', function(done) { var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"regex","v":"onetwothree","case":true}],checkall:true,outputs:1,wires:[["helperNode1"]]}, {id:"helperNode1", type:"helper", wires:[]}]; diff --git a/test/nodes/core/function/15-change_spec.js b/test/nodes/core/function/15-change_spec.js index 1809d91d9..fc0c14600 100644 --- a/test/nodes/core/function/15-change_spec.js +++ b/test/nodes/core/function/15-change_spec.js @@ -98,7 +98,7 @@ describe('change Node', function() { }); describe('#set' , function() { - + it('sets the value of the message property', function(done) { var flow = [{"id":"changeNode1","type":"change","action":"replace","property":"payload","from":"","to":"changed","reg":false,"name":"changeNode","wires":[["helperNode1"]]}, {id:"helperNode1", type:"helper", wires:[]}]; @@ -672,6 +672,111 @@ describe('change Node', function() { }); }); + it('sets the value of a message property using a nested property', function(done) { + var flow = [{"id":"changeNode1","type":"change","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"lookup[msg.topic]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.equal(2); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.receive({payload:"",lookup:{a:1,b:2},topic:"b"}); + }); + }); + + it('sets the value of a nested message property using a message property', function(done) { + var flow = [{"id":"changeNode1","type":"change","name":"","rules":[{"t":"set","p":"lookup[msg.topic]","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.lookup.b.should.equal("newValue"); + done(); + } catch(err) { + done(err); + } + }); + var msg = { + payload: "newValue", + lookup:{a:1,b:2}, + topic:"b" + } + changeNode1.receive(msg); + }); + }); + + it('sets the value of a message property using a nested property in flow context', function(done) { + var flow = [{"id":"changeNode1","type":"change","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"lookup[msg.topic]","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"wires":[["helperNode1"]],"z":"flow"}, + {id:"helperNode1", type:"helper", wires:[]}]; + + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.eql(2); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.context().flow.set("lookup",{a:1, b:2}); + changeNode1.receive({payload: "", topic: "b"}); + }); + }) + + it('sets the value of a message property using a nested property in flow context', function(done) { + var flow = [{"id":"changeNode1","type":"change","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"lookup[msg.topic]","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"wires":[["helperNode1"]],"z":"flow"}, + {id:"helperNode1", type:"helper", wires:[]}]; + + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.eql(2); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.context().flow.set("lookup",{a:1, b:2}); + changeNode1.receive({payload: "", topic: "b"}); + }); + }) + + it('sets the value of a nested flow context property using a message property', function(done) { + var flow = [{"id":"changeNode1","type":"change","name":"","rules":[{"t":"set","p":"lookup[msg.topic]","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"wires":[["helperNode1"]],"z":"flow"}, + {id:"helperNode1", type:"helper", wires:[]}]; + + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.eql("newValue"); + changeNode1.context().flow.get("lookup.b").should.eql("newValue"); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.context().flow.set("lookup",{a:1, b:2}); + changeNode1.receive({payload: "newValue", topic: "b"}); + }); + }) + + }); describe('#change', function() { it('changes the value of the message property', function(done) { diff --git a/test/unit/@node-red/util/lib/util_spec.js b/test/unit/@node-red/util/lib/util_spec.js index c4a64d53f..6db93f311 100644 --- a/test/unit/@node-red/util/lib/util_spec.js +++ b/test/unit/@node-red/util/lib/util_spec.js @@ -164,6 +164,13 @@ describe("@node-red/util/util", function() { var v2 = util.getMessageProperty({a:"foo"},"a"); v2.should.eql("foo"); }); + it('retrieves a nested property', function() { + var v = util.getMessageProperty({a:"foo",b:{foo:1,bar:2}},"msg.b[msg.a]"); + v.should.eql(1); + var v2 = util.getMessageProperty({a:"bar",b:{foo:1,bar:2}},"b[msg.a]"); + v2.should.eql(2); + }); + it('should return undefined if property does not exist', function() { var v = util.getMessageProperty({a:"foo"},"msg.b"); should.not.exist(v); @@ -331,7 +338,18 @@ describe("@node-red/util/util", function() { msg.a[0].should.eql(1); msg.a[1].should.eql(3); }) - + it('handles nested message property references', function() { + var obj = {a:"foo",b:{}}; + var result = util.setObjectProperty(obj,"b[msg.a]","bar"); + result.should.be.true(); + obj.b.should.have.property("foo","bar"); + }); + it('handles nested message property references', function() { + var obj = {a:"foo",b:{"foo":[0,0,0]}}; + var result = util.setObjectProperty(obj,"b[msg.a][2]","bar"); + result.should.be.true(); + obj.b.foo.should.eql([0,0,"bar"]) + }); }); describe('evaluateNodeProperty', function() { @@ -459,13 +477,24 @@ describe("@node-red/util/util", function() { // console.log(result); result.should.eql(expected); } - - function testInvalid(input) { + function testABCWithMessage(input,msg,expected) { + var result = util.normalisePropertyExpression(input,msg); + // console.log("+",input); + // console.log(result); + result.should.eql(expected); + } + function testInvalid(input,msg) { /*jshint immed: false */ (function() { - util.normalisePropertyExpression(input); + util.normalisePropertyExpression(input,msg); }).should.throw(); } + function testToString(input,msg,expected) { + var result = util.normalisePropertyExpression(input,msg,true); + console.log("+",input); + console.log(result); + result.should.eql(expected); + } it('pass a.b.c',function() { testABC('a.b.c',['a','b','c']); }) it('pass a["b"]["c"]',function() { testABC('a["b"]["c"]',['a','b','c']); }) it('pass a["b"].c',function() { testABC('a["b"].c',['a','b','c']); }) @@ -479,12 +508,25 @@ describe("@node-red/util/util", function() { it("pass 'a.b'[1]",function() { testABC("'a.b'[1]",['a.b',1]); }) it("pass 'a.b'.c",function() { testABC("'a.b'.c",['a.b','c']); }) + it("pass a[msg.b]",function() { testABC("a[msg.b]",["a",["msg","b"]]); }) + it("pass a[msg[msg.b]]",function() { testABC("a[msg[msg.b]]",["a",["msg",["msg","b"]]]); }) + it("pass a[msg.b]",function() { testABC("a[msg.b]",["a",["msg","b"]]); }) + it("pass a[msg.b]",function() { testABC("a[msg.b]",["a",["msg","b"]]); }) + it("pass a[msg['b]\"[']]",function() { testABC("a[msg['b]\"[']]",["a",["msg","b]\"["]]); }) + it("pass a[msg['b][']]",function() { testABC("a[msg['b][']]",["a",["msg","b]["]]); }) + it("pass b[msg.a][2]",function() { testABC("b[msg.a][2]",["b",["msg","a"],2])}) + + it("pass b[msg.a][2] (with message)",function() { testABCWithMessage("b[msg.a][2]",{a: "foo"},["b","foo",2])}) it('pass a.$b.c',function() { testABC('a.$b.c',['a','$b','c']); }) it('pass a["$b"].c',function() { testABC('a["$b"].c',['a','$b','c']); }) it('pass a._b.c',function() { testABC('a._b.c',['a','_b','c']); }) it('pass a["_b"].c',function() { testABC('a["_b"].c',['a','_b','c']); }) + it("pass a['a.b[0]'].c",function() { testToString("a['a.b[0]'].c",null,'a["a.b[0]"]["c"]'); }) + it("pass a.b.c",function() { testToString("a.b.c",null,'a["b"]["c"]'); }) + it('pass a[msg.c][0]["fred"]',function() { testToString('a[msg.c][0]["fred"]',{c:"123"},'a["123"][0]["fred"]'); }) + it("fail a'b'.c",function() { testInvalid("a'b'.c"); }) it("fail a['b'.c",function() { testInvalid("a['b'.c"); }) it("fail a[]",function() { testInvalid("a[]"); }) @@ -505,6 +547,12 @@ describe("@node-red/util/util", function() { it("fail a['']",function() { testInvalid("a['']"); }) it("fail 'a.b'c",function() { testInvalid("'a.b'c"); }) it("fail ",function() { testInvalid("");}) + it("fail a[b]",function() { testInvalid("a[b]"); }) + it("fail a[msg.]",function() { testInvalid("a[msg.]"); }) + it("fail a[msg[]",function() { testInvalid("a[msg[]"); }) + it("fail a[msg.[]]",function() { testInvalid("a[msg.[]]"); }) + it("fail a[msg['af]]",function() { testInvalid("a[msg['af]]"); }) + it("fail b[msg.undefined][2] (with message)",function() { testInvalid("b[msg.undefined][2]",{})}) }); @@ -983,4 +1031,5 @@ describe("@node-red/util/util", function() { }); }); + }); From 9eb7fad621e228fd35a9a67db20a8d9dded7486a Mon Sep 17 00:00:00 2001 From: heikokue <74769863+heikokue@users.noreply.github.com> Date: Wed, 27 Jan 2021 21:33:54 +0100 Subject: [PATCH 24/39] =?UTF-8?q?fixed=20#2790=20swapped=20description=20o?= =?UTF-8?q?f=20encodeUrl/encodeUrlComponent=20and=20d=E2=80=A6=20(#2791)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fixed #2790 swapped description of encodeUrl/encodeUrlComponent and decodeUrl/decodeUrlComponent * fixed #2790 swapped description of encodeUrl/encodeUrlComponent and decodeUrl/decodeUrlComponent also in ja, ru, zh-CN and zh-TW --- .../@node-red/editor-client/locales/en-US/jsonata.json | 8 ++++---- .../@node-red/editor-client/locales/ja/jsonata.json | 8 ++++---- .../@node-red/editor-client/locales/ru/jsonata.json | 8 ++++---- .../@node-red/editor-client/locales/zh-CN/jsonata.json | 8 ++++---- .../@node-red/editor-client/locales/zh-TW/jsonata.json | 8 ++++---- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/jsonata.json b/packages/node_modules/@node-red/editor-client/locales/en-US/jsonata.json index 57ebb00a7..282dfe551 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/jsonata.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/jsonata.json @@ -243,19 +243,19 @@ "args": "array, function", "desc": "Returns the one and only value in the `array` parameter that satisfies the `function` predicate (i.e. the `function` returns Boolean `true` when passed the value). Throws an exception if the number of matching values is not exactly one.\n\nThe function should be supplied in the following signature: `function(value [, index [, array]])` where value is each input of the array, index is the position of that value and the whole array is passed as the third argument" }, - "$encodeUrl": { + "$encodeUrlComponent": { "args": "str", "desc": "Encodes a Uniform Resource Locator (URL) component by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character.\n\nExample: `$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`" }, - "$encodeUrlComponent": { + "$encodeUrl": { "args": "str", "desc": "Encodes a Uniform Resource Locator (URL) by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character. \n\nExample: `$encodeUrl(\"https://mozilla.org/?x=шеллы\")` => `\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\"`" }, - "$decodeUrl": { + "$decodeUrlComponent": { "args": "str", "desc": "Decodes a Uniform Resource Locator (URL) component previously created by encodeUrlComponent. \n\nExample: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`" }, - "$decodeUrlComponent": { + "$decodeUrl": { "args": "str", "desc": "Decodes a Uniform Resource Locator (URL) previously created by encodeUrl. \n\nExample: `$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")` => `\"https://mozilla.org/?x=шеллы\"`" }, diff --git a/packages/node_modules/@node-red/editor-client/locales/ja/jsonata.json b/packages/node_modules/@node-red/editor-client/locales/ja/jsonata.json index 02973a69a..7f96c747a 100644 --- a/packages/node_modules/@node-red/editor-client/locales/ja/jsonata.json +++ b/packages/node_modules/@node-red/editor-client/locales/ja/jsonata.json @@ -243,19 +243,19 @@ "args": "array, function", "desc": "`array`の要素のうち、条件判定関数`function`を満たす(`function`に与えた場合に真偽値`true`を返す)要素が1つのみである場合、それを返します。マッチする要素が1つのみでない場合、例外を送出します。\n\n指定する関数は`function(value [, index [, array]])`というシグネチャでなければなりません。ここで、`value`は`array`の要素値、`index`は要素の添字、第三引数には配列全体を渡します。" }, - "$encodeUrl": { + "$encodeUrlComponent": { "args": "str", "desc": "Uniform Resource Locator (URL)を構成する文字を1、2、3、もしくは、4文字エスケープシーケンスのUTF-8文字エンコーディングで置換します。\n\n例: `$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`" }, - "$encodeUrlComponent": { + "$encodeUrl": { "args": "str", "desc": "Uniform Resource Locator (URL)要素を構成する文字を1、2、3、もしくは、4文字エスケープシーケンスのUTF-8文字エンコーディングで置換します。\n\n例: `$encodeUrl(\"https://mozilla.org/?x=шеллы\")` => `\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\"`" }, - "$decodeUrl": { + "$decodeUrlComponent": { "args": "str", "desc": "encodeUrlComponentで置換したUniform Resource Locator (URL)をデコードします。\n\n例: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`" }, - "$decodeUrlComponent": { + "$decodeUrl": { "args": "str", "desc": "encodeUrlで置換したUniform Resource Locator (URL)要素をデコードします。 \n\n例: `$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")` => `\"https://mozilla.org/?x=шеллы\"`" }, diff --git a/packages/node_modules/@node-red/editor-client/locales/ru/jsonata.json b/packages/node_modules/@node-red/editor-client/locales/ru/jsonata.json index fd0ecf24c..920042c25 100755 --- a/packages/node_modules/@node-red/editor-client/locales/ru/jsonata.json +++ b/packages/node_modules/@node-red/editor-client/locales/ru/jsonata.json @@ -243,19 +243,19 @@ "args": "array, function", "desc": "Возвращает одно-единственное значение из массива `array`, которое удовлетворяет предикату `function` (то есть когда `function` возвращает логическое `true` при передаче значения). Выдает исключение, если число подходящих значений не одно.\n\nФункция должна соответствовать следующей сигнатуре: `function(value [, index [, array]])` где value - элемент массива, index - позиция этого значения, а весь массив передается в качестве третьего аргумента" }, - "$encodeUrl": { + "$encodeUrlComponent": { "args": "str", "desc": "Кодирует компонент Uniform Resource Locator (URL), заменяя каждый экземпляр определенных символов одной, двумя, тремя или четырьмя escape-последовательностями, представляющими кодировку UTF-8 символа.\n\nПример: `$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`" }, - "$encodeUrlComponent": { + "$encodeUrl": { "args": "str", "desc": "Кодирует Uniform Resource Locator (URL), заменяя каждый экземпляр определенных символов одной, двумя, тремя или четырьмя escape-последовательностями, представляющими кодировку UTF-8 символа.\n\nПример: `$encodeUrl(\"https://mozilla.org/?x=шеллы\")` => `\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\"`" }, - "$decodeUrl": { + "$decodeUrlComponent": { "args": "str", "desc": "Декодирует компонент Uniform Resource Locator (URL), ранее созданный с помощью encodeUrlComponent.\n\nПример: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`" }, - "$decodeUrlComponent": { + "$decodeUrl": { "args": "str", "desc": "Декодирует компонент Uniform Resource Locator (URL), ранее созданный с помощью encodeUrl. \n\nПример: `$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")` => `\"https://mozilla.org/?x=шеллы\"`" }, diff --git a/packages/node_modules/@node-red/editor-client/locales/zh-CN/jsonata.json b/packages/node_modules/@node-red/editor-client/locales/zh-CN/jsonata.json index a9e6a7b1f..b4403e318 100644 --- a/packages/node_modules/@node-red/editor-client/locales/zh-CN/jsonata.json +++ b/packages/node_modules/@node-red/editor-client/locales/zh-CN/jsonata.json @@ -243,19 +243,19 @@ "args": "array, function", "desc": "返回满足参数function谓语的array参数中的唯一值 (比如:传递值时,函数返回布尔值“true”)。如果匹配值的数量不唯一时,则抛出异常。\n\n应在以下签名中提供函数: `function(value [,index [,array []]])` 其中value是数组的每个输入,index是该值的位置,整个数组作为第三个参数传递。" }, - "$encodeUrl": { + "$encodeUrlComponent": { "args": "str", "desc": "通过用表示字符的UTF-8编码的一个,两个,三个或四个转义序列替换某些字符的每个实例,对统一资源定位符(URL)组件进行编码。\n\n示例: `$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`" }, - "$encodeUrlComponent": { + "$encodeUrl": { "args": "str", "desc": "通过用表示字符的UTF-8编码的一个,两个,三个或四个转义序列替换某些字符的每个实例,对统一资源定位符(URL)进行编码。\n\n示例: `$encodeUrl(\"https://mozilla.org/?x=шеллы\")` => `\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\"`" }, - "$decodeUrl": { + "$decodeUrlComponent": { "args": "str", "desc": "解码以前由encodeUrlComponent创建的统一资源定位器(URL)组件。 \n\n示例: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`" }, - "$decodeUrlComponent": { + "$decodeUrl": { "args": "str", "desc": "解码先前由encodeUrl创建的统一资源定位符(URL)。 \n\n示例: `$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")` => `\"https://mozilla.org/?x=шеллы\"`" }, diff --git a/packages/node_modules/@node-red/editor-client/locales/zh-TW/jsonata.json b/packages/node_modules/@node-red/editor-client/locales/zh-TW/jsonata.json index 3765ab3dd..2b47c1af7 100644 --- a/packages/node_modules/@node-red/editor-client/locales/zh-TW/jsonata.json +++ b/packages/node_modules/@node-red/editor-client/locales/zh-TW/jsonata.json @@ -243,19 +243,19 @@ "args": "array, function", "desc": "返回滿足參數function謂語的array參數中的唯一值 (比如:傳遞值時,函數返回布林值“true”)。如果匹配值的數量不唯一時,則拋出異常。\n\n應在以下簽名中提供函數:`function(value [,index [,array []]])`其中value是數組的每個輸入,index是該值的位置,整個數組作為第三個參數傳遞。" }, - "$encodeUrl": { + "$encodeUrlComponent": { "args": "str", "desc": "通過用表示字符的UTF-8編碼的一個,兩個,三個或四個轉義序列替換某些字符的每個實例,對統一資源定位符(URL)組件進行編碼。\n\n示例:`$encodeUrlComponent(\"?x=test\")` => `\"%3Fx%3Dtest\"`" }, - "$encodeUrlComponent": { + "$encodeUrl": { "args": "str", "desc": "通過用表示字符的UTF-8編碼的一個,兩個,三個或四個轉義序列替換某些字符的每個實例,對統一資源定位符(URL)進行編碼。\n\n示例: `$encodeUrl(\"https://mozilla.org/?x=шеллы\")` => `\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\"`" }, - "$decodeUrl": { + "$decodeUrlComponent": { "args": "str", "desc": "解碼以前由encodeUrlComponent創建的統一資源定位器(URL)組件。 \n\n示例: `$decodeUrlComponent(\"%3Fx%3Dtest\")` => `\"?x=test\"`" }, - "$decodeUrlComponent": { + "$decodeUrl": { "args": "str", "desc": "解碼先前由encodeUrl創建的統一資源定位符(URL)。 \n\n示例: `$decodeUrl(\"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B\")` => `\"https://mozilla.org/?x=шеллы\"`" }, From 7d08de9c994aaabc7878ab0b17e358dab95b0b8d Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Thu, 28 Jan 2021 05:50:13 +0900 Subject: [PATCH 25/39] Storage examples (#2784) * add examples of file node * add file-in node examples * add watch node examples --- .../file-in/01 - Read string from a file.json | 113 ++++++++++ .../02 - Read data in specified encoding.json | 113 ++++++++++ ...ead data breaking lines into messages.json | 132 ++++++++++++ .../file-in/04 - Create a message stream.json | 201 ++++++++++++++++++ .../file/01 - Write string to a file.json | 113 ++++++++++ ...tring to a file specified by property.json | 118 ++++++++++ .../storage/file/03 - Delete a file.json | 85 ++++++++ ...04 - Specify encoding of written data.json | 113 ++++++++++ .../watch/01 - Watch change of a file.json | 108 ++++++++++ .../02 - Watch change in a directory.json | 163 ++++++++++++++ 10 files changed, 1259 insertions(+) create mode 100644 packages/node_modules/@node-red/nodes/examples/storage/file-in/01 - Read string from a file.json create mode 100644 packages/node_modules/@node-red/nodes/examples/storage/file-in/02 - Read data in specified encoding.json create mode 100644 packages/node_modules/@node-red/nodes/examples/storage/file-in/03 - Read data breaking lines into messages.json create mode 100644 packages/node_modules/@node-red/nodes/examples/storage/file-in/04 - Create a message stream.json create mode 100644 packages/node_modules/@node-red/nodes/examples/storage/file/01 - Write string to a file.json create mode 100644 packages/node_modules/@node-red/nodes/examples/storage/file/02 - Write string to a file specified by property.json create mode 100644 packages/node_modules/@node-red/nodes/examples/storage/file/03 - Delete a file.json create mode 100644 packages/node_modules/@node-red/nodes/examples/storage/file/04 - Specify encoding of written data.json create mode 100644 packages/node_modules/@node-red/nodes/examples/storage/watch/01 - Watch change of a file.json create mode 100644 packages/node_modules/@node-red/nodes/examples/storage/watch/02 - Watch change in a directory.json diff --git a/packages/node_modules/@node-red/nodes/examples/storage/file-in/01 - Read string from a file.json b/packages/node_modules/@node-red/nodes/examples/storage/file-in/01 - Read string from a file.json new file mode 100644 index 000000000..14d7152c7 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/file-in/01 - Read string from a file.json @@ -0,0 +1,113 @@ +[ + { + "id": "84222b92.d65d18", + "type": "inject", + "z": "194a3e4f.a92772", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "Hello, World!", + "payloadType": "str", + "x": 230, + "y": 220, + "wires": [ + [ + "b4b9f603.739598" + ] + ] + }, + { + "id": "7b014430.dfd94c", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "Write string to a file, then read from the file", + "info": "File-in node can read string from a file.", + "x": 260, + "y": 140, + "wires": [] + }, + { + "id": "b4b9f603.739598", + "type": "file", + "z": "194a3e4f.a92772", + "name": "", + "filename": "/tmp/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 420, + "y": 220, + "wires": [ + [ + "6dc01cac.5c4bf4" + ] + ] + }, + { + "id": "2587adb9.7e60f2", + "type": "debug", + "z": "194a3e4f.a92772", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 810, + "y": 220, + "wires": [] + }, + { + "id": "6dc01cac.5c4bf4", + "type": "file in", + "z": "194a3e4f.a92772", + "name": "", + "filename": "/tmp/hello.txt", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "x": 620, + "y": 220, + "wires": [ + [ + "2587adb9.7e60f2" + ] + ] + }, + { + "id": "f4b4309a.3b78a", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↑read result from file", + "info": "", + "x": 630, + "y": 260, + "wires": [] + }, + { + "id": "672d3693.3cabd8", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↓write to /tmp/hello.txt", + "info": "", + "x": 440, + "y": 180, + "wires": [] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/file-in/02 - Read data in specified encoding.json b/packages/node_modules/@node-red/nodes/examples/storage/file-in/02 - Read data in specified encoding.json new file mode 100644 index 000000000..d96623fb1 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/file-in/02 - Read data in specified encoding.json @@ -0,0 +1,113 @@ +[ + { + "id": "8997398f.c5d628", + "type": "inject", + "z": "194a3e4f.a92772", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "😀", + "payloadType": "str", + "x": 210, + "y": 480, + "wires": [ + [ + "56e32d23.050f44" + ] + ] + }, + { + "id": "4e598e65.1799d", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "Read data in specified encoding", + "info": "File-in node can specify encoding of data read from a file.", + "x": 230, + "y": 400, + "wires": [] + }, + { + "id": "56e32d23.050f44", + "type": "file", + "z": "194a3e4f.a92772", + "name": "", + "filename": "/tmp/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 380, + "y": 480, + "wires": [ + [ + "38fa0579.f2cd8a" + ] + ] + }, + { + "id": "d28c8994.99c0a8", + "type": "debug", + "z": "194a3e4f.a92772", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 770, + "y": 480, + "wires": [] + }, + { + "id": "38fa0579.f2cd8a", + "type": "file in", + "z": "194a3e4f.a92772", + "name": "", + "filename": "/tmp/hello.txt", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "base64", + "x": 580, + "y": 480, + "wires": [ + [ + "d28c8994.99c0a8" + ] + ] + }, + { + "id": "fa22ca20.ae4528", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↑read data from file as base64 string", + "info": "", + "x": 640, + "y": 520, + "wires": [] + }, + { + "id": "148e25ad.98891a", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↓write to /tmp/hello.txt", + "info": "", + "x": 400, + "y": 440, + "wires": [] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/file-in/03 - Read data breaking lines into messages.json b/packages/node_modules/@node-red/nodes/examples/storage/file-in/03 - Read data breaking lines into messages.json new file mode 100644 index 000000000..b3d35a4da --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/file-in/03 - Read data breaking lines into messages.json @@ -0,0 +1,132 @@ +[ + { + "id": "6a0b1d03.d4cee4", + "type": "inject", + "z": "194a3e4f.a92772", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 220, + "y": 740, + "wires": [ + [ + "d4b00cb7.a5a23" + ] + ] + }, + { + "id": "f17ea1d1.8ecc3", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "Read data breaking lines into individual messages", + "info": "File-in node can break read text into messages with individual lines", + "x": 290, + "y": 660, + "wires": [] + }, + { + "id": "99ae7806.1d6428", + "type": "file", + "z": "194a3e4f.a92772", + "name": "", + "filename": "/tmp/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 540, + "y": 740, + "wires": [ + [ + "70d7892f.d27db8" + ] + ] + }, + { + "id": "7ed8282c.92b338", + "type": "debug", + "z": "194a3e4f.a92772", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 810, + "y": 800, + "wires": [] + }, + { + "id": "70d7892f.d27db8", + "type": "file in", + "z": "194a3e4f.a92772", + "name": "", + "filename": "/tmp/hello.txt", + "format": "lines", + "chunk": false, + "sendError": false, + "encoding": "none", + "x": 620, + "y": 800, + "wires": [ + [ + "7ed8282c.92b338" + ] + ] + }, + { + "id": "c1b7e05.1d94b2", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↑read data from file breaking lines into messages", + "info": "", + "x": 720, + "y": 840, + "wires": [] + }, + { + "id": "a5f647b2.cf27a8", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↓write to /tmp/hello.txt", + "info": "", + "x": 560, + "y": 700, + "wires": [] + }, + { + "id": "d4b00cb7.a5a23", + "type": "template", + "z": "194a3e4f.a92772", + "name": "data", + "field": "payload", + "fieldType": "msg", + "format": "handlebars", + "syntax": "plain", + "template": "one\ntwo\nthree!", + "output": "str", + "x": 370, + "y": 740, + "wires": [ + [ + "99ae7806.1d6428" + ] + ] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/file-in/04 - Create a message stream.json b/packages/node_modules/@node-red/nodes/examples/storage/file-in/04 - Create a message stream.json new file mode 100644 index 000000000..18b462755 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/file-in/04 - Create a message stream.json @@ -0,0 +1,201 @@ +[ + { + "id": "bdd57acc.2edc48", + "type": "inject", + "z": "194a3e4f.a92772", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 220, + "y": 1040, + "wires": [ + [ + "7a069b01.0c2324" + ] + ] + }, + { + "id": "1fd12220.33953e", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "Creating a message stream from lines of data", + "info": "File-in node can break read text into messages with individual lines. The messages creates a stream of messages.", + "x": 270, + "y": 960, + "wires": [] + }, + { + "id": "ab6eb213.2a08d", + "type": "file", + "z": "194a3e4f.a92772", + "name": "", + "filename": "/tmp/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 540, + "y": 1040, + "wires": [ + [ + "b7ed49b0.649fb8" + ] + ] + }, + { + "id": "c48d8ae0.9ff3a8", + "type": "debug", + "z": "194a3e4f.a92772", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 810, + "y": 1140, + "wires": [] + }, + { + "id": "b7ed49b0.649fb8", + "type": "file in", + "z": "194a3e4f.a92772", + "name": "", + "filename": "/tmp/hello.txt", + "format": "lines", + "chunk": false, + "sendError": false, + "encoding": "none", + "x": 280, + "y": 1140, + "wires": [ + [ + "83073ebe.fcce4" + ] + ] + }, + { + "id": "3c33e69f.6a04ba", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↑read data from file breaking lines into messages", + "info": "", + "x": 380, + "y": 1180, + "wires": [] + }, + { + "id": "3598bf7d.5712a", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↓write to /tmp/hello.txt", + "info": "", + "x": 560, + "y": 1000, + "wires": [] + }, + { + "id": "7a069b01.0c2324", + "type": "template", + "z": "194a3e4f.a92772", + "name": "data", + "field": "payload", + "fieldType": "msg", + "format": "handlebars", + "syntax": "plain", + "template": "Apple\nBanana\nGrape\nOrange", + "output": "str", + "x": 370, + "y": 1040, + "wires": [ + [ + "ab6eb213.2a08d" + ] + ] + }, + { + "id": "8d4ed1d0.821fe", + "type": "join", + "z": "194a3e4f.a92772", + "name": "", + "mode": "auto", + "build": "string", + "property": "payload", + "propertyType": "msg", + "key": "topic", + "joiner": "\\n", + "joinerType": "str", + "accumulate": "false", + "timeout": "", + "count": "", + "reduceRight": false, + "x": 630, + "y": 1140, + "wires": [ + [ + "c48d8ae0.9ff3a8" + ] + ] + }, + { + "id": "83073ebe.fcce4", + "type": "switch", + "z": "194a3e4f.a92772", + "name": "< D", + "property": "payload", + "propertyType": "msg", + "rules": [ + { + "t": "lt", + "v": "D", + "vt": "str" + } + ], + "checkall": "true", + "repair": true, + "outputs": 1, + "x": 470, + "y": 1140, + "wires": [ + [ + "8d4ed1d0.821fe" + ] + ] + }, + { + "id": "2088e195.f7aebe", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↓filter data before \"D\"", + "info": "", + "x": 520, + "y": 1100, + "wires": [] + }, + { + "id": "b848cdc7.61e06", + "type": "comment", + "z": "194a3e4f.a92772", + "name": "↑join to single string", + "info": "", + "x": 670, + "y": 1180, + "wires": [] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/file/01 - Write string to a file.json b/packages/node_modules/@node-red/nodes/examples/storage/file/01 - Write string to a file.json new file mode 100644 index 000000000..2be033021 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/file/01 - Write string to a file.json @@ -0,0 +1,113 @@ +[ + { + "id": "84222b92.d65d18", + "type": "inject", + "z": "4b63452d.672afc", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "Hello, World!", + "payloadType": "str", + "x": 230, + "y": 200, + "wires": [ + [ + "b4b9f603.739598" + ] + ] + }, + { + "id": "7b014430.dfd94c", + "type": "comment", + "z": "4b63452d.672afc", + "name": "Write string to a file, then read from the file", + "info": "File node can write string to a file.", + "x": 260, + "y": 120, + "wires": [] + }, + { + "id": "b4b9f603.739598", + "type": "file", + "z": "4b63452d.672afc", + "name": "", + "filename": "/tmp/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 420, + "y": 200, + "wires": [ + [ + "6dc01cac.5c4bf4" + ] + ] + }, + { + "id": "2587adb9.7e60f2", + "type": "debug", + "z": "4b63452d.672afc", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 810, + "y": 200, + "wires": [] + }, + { + "id": "6dc01cac.5c4bf4", + "type": "file in", + "z": "4b63452d.672afc", + "name": "", + "filename": "/tmp/hello.txt", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "x": 620, + "y": 200, + "wires": [ + [ + "2587adb9.7e60f2" + ] + ] + }, + { + "id": "f4b4309a.3b78a", + "type": "comment", + "z": "4b63452d.672afc", + "name": "↑read result from file", + "info": "", + "x": 630, + "y": 240, + "wires": [] + }, + { + "id": "672d3693.3cabd8", + "type": "comment", + "z": "4b63452d.672afc", + "name": "↓write to /tmp/hello.txt", + "info": "", + "x": 440, + "y": 160, + "wires": [] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/file/02 - Write string to a file specified by property.json b/packages/node_modules/@node-red/nodes/examples/storage/file/02 - Write string to a file specified by property.json new file mode 100644 index 000000000..6aaff500a --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/file/02 - Write string to a file specified by property.json @@ -0,0 +1,118 @@ +[ + { + "id": "704479e1.399388", + "type": "inject", + "z": "4b63452d.672afc", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "filename", + "v": "/tmp/hello.txt", + "vt": "str" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "Hello, World!", + "payloadType": "str", + "x": 230, + "y": 400, + "wires": [ + [ + "402f3b7e.988014" + ] + ] + }, + { + "id": "8e876a75.e9beb8", + "type": "comment", + "z": "4b63452d.672afc", + "name": "Write string to a file specied by filename property, the read from the file", + "info": "File node can target file using `filename` property.", + "x": 350, + "y": 320, + "wires": [] + }, + { + "id": "402f3b7e.988014", + "type": "file", + "z": "4b63452d.672afc", + "name": "", + "filename": "", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 390, + "y": 400, + "wires": [ + [ + "26e077d6.bbcd98" + ] + ] + }, + { + "id": "97b6b6b2.a54b38", + "type": "debug", + "z": "4b63452d.672afc", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 770, + "y": 400, + "wires": [] + }, + { + "id": "26e077d6.bbcd98", + "type": "file in", + "z": "4b63452d.672afc", + "name": "", + "filename": "/tmp/hello.txt", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "x": 580, + "y": 400, + "wires": [ + [ + "97b6b6b2.a54b38" + ] + ] + }, + { + "id": "85062297.da79", + "type": "comment", + "z": "4b63452d.672afc", + "name": "↑read result from file", + "info": "", + "x": 590, + "y": 440, + "wires": [] + }, + { + "id": "7316c4fc.b1dcdc", + "type": "comment", + "z": "4b63452d.672afc", + "name": "↓write to file specified by filename property", + "info": "", + "x": 500, + "y": 360, + "wires": [] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/file/03 - Delete a file.json b/packages/node_modules/@node-red/nodes/examples/storage/file/03 - Delete a file.json new file mode 100644 index 000000000..fe6ac166b --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/file/03 - Delete a file.json @@ -0,0 +1,85 @@ +[ + { + "id": "4ac00fb0.d5f52", + "type": "inject", + "z": "4b63452d.672afc", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 220, + "y": 600, + "wires": [ + [ + "542cc2f4.92857c" + ] + ] + }, + { + "id": "671f8295.0e6f6c", + "type": "comment", + "z": "4b63452d.672afc", + "name": "Delete a file", + "info": "File node can delete a file.", + "x": 170, + "y": 540, + "wires": [] + }, + { + "id": "542cc2f4.92857c", + "type": "file", + "z": "4b63452d.672afc", + "name": "", + "filename": "/tmp/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "delete", + "encoding": "none", + "x": 420, + "y": 600, + "wires": [ + [ + "a24da523.5babe8" + ] + ] + }, + { + "id": "a24da523.5babe8", + "type": "debug", + "z": "4b63452d.672afc", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 630, + "y": 600, + "wires": [] + }, + { + "id": "51157051.2f62", + "type": "comment", + "z": "4b63452d.672afc", + "name": "↓delete a file", + "info": "", + "x": 390, + "y": 560, + "wires": [] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/file/04 - Specify encoding of written data.json b/packages/node_modules/@node-red/nodes/examples/storage/file/04 - Specify encoding of written data.json new file mode 100644 index 000000000..4dabbd670 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/file/04 - Specify encoding of written data.json @@ -0,0 +1,113 @@ +[ + { + "id": "e4ef1f5e.7cd82", + "type": "inject", + "z": "4b63452d.672afc", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "8J+YgA==", + "payloadType": "str", + "x": 220, + "y": 820, + "wires": [ + [ + "72b37cc8.177054" + ] + ] + }, + { + "id": "f5997af4.5a9298", + "type": "comment", + "z": "4b63452d.672afc", + "name": "Specify encoding of written data", + "info": "File node can specify encoding of data.", + "x": 230, + "y": 740, + "wires": [] + }, + { + "id": "72b37cc8.177054", + "type": "file", + "z": "4b63452d.672afc", + "name": "", + "filename": "/tmp/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "base64", + "x": 400, + "y": 820, + "wires": [ + [ + "2da33ec.f45cac2" + ] + ] + }, + { + "id": "2e814354.278c8c", + "type": "debug", + "z": "4b63452d.672afc", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 790, + "y": 820, + "wires": [] + }, + { + "id": "2da33ec.f45cac2", + "type": "file in", + "z": "4b63452d.672afc", + "name": "", + "filename": "/tmp/hello.txt", + "format": "utf8", + "chunk": false, + "sendError": false, + "encoding": "none", + "x": 600, + "y": 820, + "wires": [ + [ + "2e814354.278c8c" + ] + ] + }, + { + "id": "ec754c99.84bfd", + "type": "comment", + "z": "4b63452d.672afc", + "name": "↓write string with base64 encoding", + "info": "", + "x": 460, + "y": 780, + "wires": [] + }, + { + "id": "3e6704ff.4ce25c", + "type": "comment", + "z": "4b63452d.672afc", + "name": "↑read result from file", + "info": "", + "x": 610, + "y": 860, + "wires": [] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/watch/01 - Watch change of a file.json b/packages/node_modules/@node-red/nodes/examples/storage/watch/01 - Watch change of a file.json new file mode 100644 index 000000000..873e99b89 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/watch/01 - Watch change of a file.json @@ -0,0 +1,108 @@ +[ + { + "id": "84222b92.d65d18", + "type": "inject", + "z": "a7ac8a68.0f7218", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "Hello, World!", + "payloadType": "str", + "x": 230, + "y": 160, + "wires": [ + [ + "b4b9f603.739598" + ] + ] + }, + { + "id": "7b014430.dfd94c", + "type": "comment", + "z": "a7ac8a68.0f7218", + "name": "Watch changes of a file", + "info": "Watch node can watch and report changes of a file.", + "x": 200, + "y": 80, + "wires": [] + }, + { + "id": "b4b9f603.739598", + "type": "file", + "z": "a7ac8a68.0f7218", + "name": "", + "filename": "/tmp/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 420, + "y": 160, + "wires": [ + [] + ] + }, + { + "id": "672d3693.3cabd8", + "type": "comment", + "z": "a7ac8a68.0f7218", + "name": "↓write to /tmp/hello.txt", + "info": "", + "x": 440, + "y": 120, + "wires": [] + }, + { + "id": "15f1f5aa.506ffa", + "type": "watch", + "z": "a7ac8a68.0f7218", + "name": "", + "files": "/tmp/hello.txt", + "recursive": "", + "x": 410, + "y": 200, + "wires": [ + [ + "a91562b9.ca805" + ] + ] + }, + { + "id": "a91562b9.ca805", + "type": "debug", + "z": "a7ac8a68.0f7218", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 610, + "y": 200, + "wires": [] + }, + { + "id": "2ab4eba8.267d64", + "type": "comment", + "z": "a7ac8a68.0f7218", + "name": "↑watch changes of /tmp/hello.txt", + "info": "", + "x": 470, + "y": 240, + "wires": [] + } +] \ No newline at end of file diff --git a/packages/node_modules/@node-red/nodes/examples/storage/watch/02 - Watch change in a directory.json b/packages/node_modules/@node-red/nodes/examples/storage/watch/02 - Watch change in a directory.json new file mode 100644 index 000000000..6b7fd0b2e --- /dev/null +++ b/packages/node_modules/@node-red/nodes/examples/storage/watch/02 - Watch change in a directory.json @@ -0,0 +1,163 @@ +[ + { + "id": "acec9dcc.eca82", + "type": "inject", + "z": "a7ac8a68.0f7218", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "Hello, World!", + "payloadType": "str", + "x": 230, + "y": 460, + "wires": [ + [ + "137d60e2.0e267f" + ] + ] + }, + { + "id": "cf011d1e.8afa6", + "type": "comment", + "z": "a7ac8a68.0f7218", + "name": "Watch changes in a directory", + "info": "Watch node can watch and report changes in a directory", + "x": 220, + "y": 340, + "wires": [] + }, + { + "id": "137d60e2.0e267f", + "type": "file", + "z": "a7ac8a68.0f7218", + "name": "", + "filename": "/tmp/HG/hello.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 430, + "y": 460, + "wires": [ + [] + ] + }, + { + "id": "9dfce283.63e5a", + "type": "comment", + "z": "a7ac8a68.0f7218", + "name": "↓write to /tmp/HG/*", + "info": "", + "x": 430, + "y": 420, + "wires": [] + }, + { + "id": "cbfb788b.297f98", + "type": "watch", + "z": "a7ac8a68.0f7218", + "name": "", + "files": "/tmp/HG", + "recursive": false, + "x": 400, + "y": 540, + "wires": [ + [ + "3c691cd5.a0f2b4" + ] + ] + }, + { + "id": "3c691cd5.a0f2b4", + "type": "debug", + "z": "a7ac8a68.0f7218", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 550, + "y": 540, + "wires": [] + }, + { + "id": "c0d7ca6e.cad418", + "type": "comment", + "z": "a7ac8a68.0f7218", + "name": "↑watch changes in /tmp/HG", + "info": "", + "x": 460, + "y": 580, + "wires": [] + }, + { + "id": "a04e5231.5a2e1", + "type": "inject", + "z": "a7ac8a68.0f7218", + "name": "", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "Goodbye, World!", + "payloadType": "str", + "x": 240, + "y": 500, + "wires": [ + [ + "655d7bab.cda6f4" + ] + ] + }, + { + "id": "655d7bab.cda6f4", + "type": "file", + "z": "a7ac8a68.0f7218", + "name": "", + "filename": "/tmp/HG/goodbye.txt", + "appendNewline": true, + "createDir": false, + "overwriteFile": "true", + "encoding": "none", + "x": 440, + "y": 500, + "wires": [ + [] + ] + }, + { + "id": "f3b42209.a7673", + "type": "comment", + "z": "a7ac8a68.0f7218", + "name": "Notice: Create /tmp/HG directory before deploying this example", + "info": "", + "x": 330, + "y": 380, + "wires": [] + } +] \ No newline at end of file From 9b5ed8407f293da22e66c5e0249064f80df25995 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 27 Jan 2021 22:06:12 +0000 Subject: [PATCH 26/39] Broaden lang verification to include * --- .../@node-red/editor-api/lib/editor/locales.js | 2 +- packages/node_modules/@node-red/runtime/lib/api/nodes.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/locales.js b/packages/node_modules/@node-red/editor-api/lib/editor/locales.js index ebe3e3281..f9453f55b 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/locales.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/locales.js @@ -41,7 +41,7 @@ module.exports = { var namespace = req.params[0]; namespace = namespace.replace(/\.json$/,""); var lang = req.query.lng || i18n.defaultLang; //apiUtil.determineLangFromHeaders(req.acceptsLanguages() || []); - if (/[^a-z\-]/i.test(lang)) { + if (/[^a-z\-\*]/i.test(lang)) { res.json({}); return; } diff --git a/packages/node_modules/@node-red/runtime/lib/api/nodes.js b/packages/node_modules/@node-red/runtime/lib/api/nodes.js index aa5eca100..a0ce200b3 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/nodes.js +++ b/packages/node_modules/@node-red/runtime/lib/api/nodes.js @@ -99,7 +99,7 @@ var api = module.exports = { return new Promise(function(resolve,reject) { var id = opts.id; var lang = opts.lang; - if (/[^a-z\-]/i.test(opts.lang)) { + if (/[^a-z\-\*]/i.test(opts.lang)) { reject(new Error("Invalid language: "+opts.lang)); return } @@ -128,7 +128,7 @@ var api = module.exports = { getNodeConfigs: function(opts) { return new Promise(function(resolve,reject) { runtime.log.audit({event: "nodes.configs.get"}, opts.req); - if (/[^a-z\-]/i.test(opts.lang)) { + if (/[^a-z\-\*]/i.test(opts.lang)) { reject(new Error("Invalid language: "+opts.lang)); return } @@ -406,7 +406,7 @@ var api = module.exports = { var namespace = opts.module; var lang = opts.lang; var prevLang = runtime.i18n.i.language; - if (/[^a-z\-]/i.test(lang)) { + if (/[^a-z\-\*]/i.test(lang)) { reject(new Error("Invalid language: "+lang)); return } @@ -439,7 +439,7 @@ var api = module.exports = { return new Promise(function(resolve,reject) { var namespace = opts.module; var lang = opts.lang; - if (/[^a-z\-]/i.test(lang)) { + if (/[^a-z\-\*]/i.test(lang)) { reject(new Error("Invalid language: "+lang)); return } From 7068c175f288c4f7860380e1d4a315b9223b2777 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 27 Jan 2021 22:53:06 +0000 Subject: [PATCH 27/39] Ensure subflow help is picked up for palette tooltip Fixes #2834 --- .../node_modules/@node-red/editor-client/src/js/ui/palette.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js index ecee89d70..a11758759 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js @@ -417,7 +417,8 @@ RED.palette = (function() { RED.workspaces.show(nt.substring(8)); e.preventDefault(); }); - nodeInfo = RED.utils.renderMarkdown(def.info||""); + var subflow = RED.nodes.subflow(nt.substring(8)); + nodeInfo = RED.utils.renderMarkdown(subflow.info||""); } setLabel(nt,d,label,nodeInfo); From 2e73b229d7f36b855c3dbc5690b9050befe234a1 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 28 Jan 2021 00:41:19 +0000 Subject: [PATCH 28/39] Add easier ways to find subflow instances --- .../editor-client/src/js/ui/palette.js | 4 +- .../src/js/ui/tab-info-outliner.js | 46 +++++++++---------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js index 0e7367ebf..3772f83d9 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js @@ -170,7 +170,9 @@ RED.palette = (function() { metaData = typeInfo.set.module+" : "; } metaData += type; - $('').appendTo(popOverContent) + var safeType = type.replace(/'/g,"\\'"); + $('').appendTo(popOverContent) + $('').appendTo(popOverContent) $('

    ',{style:"font-size: 0.8em"}).text(metaData).appendTo(popOverContent); } } catch(err) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js index f0c6e631a..5ba6e5dfe 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js @@ -119,34 +119,17 @@ RED.sidebar.info.outliner = (function() { return div; } - function getSubflowLabel(n) { - - var div = $('

    ',{class:"red-ui-info-outline-item"}); - RED.utils.createNodeIcon(n).appendTo(div); - var contentDiv = $('
    ',{class:"red-ui-search-result-description"}).appendTo(div); - var labelText = getNodeLabelText(n); - var label = $('
    ',{class:"red-ui-search-result-node-label red-ui-info-outline-item-label"}).appendTo(contentDiv); - if (labelText) { - label.text(labelText) - } else { - label.html(" ") - } - - addControls(n, div); - - return div; - - - // var div = $('
    ',{class:"red-ui-info-outline-item red-ui-info-outline-item-flow"}); - // var contentDiv = $('
    ',{class:"red-ui-search-result-description red-ui-info-outline-item-label"}).appendTo(div); - // contentDiv.text(n.name || n.id); - // addControls(n, div); - // return div; - } - function addControls(n,div) { var controls = $('
    ',{class:"red-ui-info-outline-item-controls red-ui-info-outline-item-hover-controls"}).appendTo(div); + if (n.type === "subflow") { + var subflowInstanceBadge = $('').text(n.instances.length).appendTo(controls).on("click",function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + RED.search.show("type:subflow:"+n.id); + }) + // RED.popover.tooltip(userCountBadge,function() { return RED._('editor.nodesUse',{count:n.users.length})}); + } if (n._def.category === "config" && n.type !== "group") { var userCountBadge = $('').text(n.users.length).appendTo(controls).on("click",function(evt) { evt.preventDefault(); @@ -486,6 +469,13 @@ RED.sidebar.info.outliner = (function() { existingObject.treeList.remove(); delete objects[n.id] + if (/^subflow:/.test(n.type)) { + var sfType = n.type.substring(8); + if (objects[sfType]) { + objects[sfType].element.find(".red-ui-info-outline-item-control-users").text(RED.nodes.subflow(sfType).instances.length); + } + } + // If this is a group being removed, it may have an empty item if (empties[n.id]) { delete empties[n.id]; @@ -587,6 +577,12 @@ RED.sidebar.info.outliner = (function() { configNodeTypes[parent].types[n.type].treeList.addChild(objects[n.id]); } objects[n.id].element.toggleClass("red-ui-info-outline-item-disabled", !!n.d) + if (/^subflow:/.test(n.type)) { + var sfType = n.type.substring(8); + if (objects[sfType]) { + objects[sfType].element.find(".red-ui-info-outline-item-control-users").text(RED.nodes.subflow(sfType).instances.length); + } + } updateSearch(); } From 83d12f7d39ebfde15cb9729dbc2da9787a135aa2 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 28 Jan 2021 11:14:47 +0000 Subject: [PATCH 29/39] Handle timeouts when trying to load node credentials in editor Fixes #2840 --- .../editor-client/locales/en-US/editor.json | 4 +- .../editor-client/src/js/ui/editor.js | 66 ++++++++++++++++--- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 7cdfde99d..1851c2355 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -397,6 +397,7 @@ "icon": "Icon", "inputType": "Input type", "selectType": "select types...", + "loadCredentials": "Loading node credentials", "inputs" : { "input": "input", "select": "select", @@ -431,7 +432,8 @@ }, "errors": { "scopeChange": "Changing the scope will make it unavailable to nodes in other flows that use it", - "invalidProperties": "Invalid properties:" + "invalidProperties": "Invalid properties:", + "credentialLoadFailed": "Failed to load node credentials" } }, "keyboard": { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js index aa2bc2f8f..527f27bf1 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js @@ -496,11 +496,13 @@ RED.editor = (function() { populateCredentialsInputs(node, definition.credentials, node.credentials, prefix); completePrepare(); } else { - $.getJSON(getCredentialsURL(node.type, node.id), function (data) { - node.credentials = data; - node.credentials._ = $.extend(true,{},data); - if (!/^subflow:/.test(definition.type)) { - populateCredentialsInputs(node, definition.credentials, node.credentials, prefix); + getNodeCredentials(node.type, node.id, function(data) { + if (data) { + node.credentials = data; + node.credentials._ = $.extend(true,{},data); + if (!/^subflow:/.test(definition.type)) { + populateCredentialsInputs(node, definition.credentials, node.credentials, prefix); + } } completePrepare(); }); @@ -1088,8 +1090,11 @@ RED.editor = (function() { node.infoEditor = nodeInfoEditor; return nodeInfoEditor; } +var buildingEditDialog = false; function showEditDialog(node, defaultTab) { + if (buildingEditDialog) { return } + buildingEditDialog = true; var editing_node = node; var isDefaultIcon; var defaultIcon; @@ -1614,6 +1619,7 @@ RED.editor = (function() { if (defaultTab) { editorTabs.activateTab(defaultTab); } + buildingEditDialog = false; done(); }); }, @@ -1665,6 +1671,8 @@ RED.editor = (function() { * prefix - the input prefix of the parent property */ function showEditConfigNodeDialog(name,type,id,prefix) { + if (buildingEditDialog) { return } + buildingEditDialog = true; var adding = (id == "_ADD_"); var node_def = RED.nodes.getType(type); var editing_config_node = RED.nodes.node(id); @@ -1828,6 +1836,7 @@ RED.editor = (function() { trayBody.i18n(); trayFooter.i18n(); finishedBuilding = true; + buildingEditDialog = false; done(); }); }, @@ -2151,6 +2160,8 @@ RED.editor = (function() { } function showEditSubflowDialog(subflow) { + if (buildingEditDialog) { return } + buildingEditDialog = true; var editing_node = subflow; editStack.push(subflow); RED.view.state(RED.state.EDITING); @@ -2407,15 +2418,17 @@ RED.editor = (function() { buildEditForm(nodePropertiesTab.content,"dialog-form","subflow-template", undefined, editing_node); trayBody.i18n(); - - $.getJSON(getCredentialsURL("subflow", subflow.id), function (data) { - subflow.credentials = data; - subflow.credentials._ = $.extend(true,{},data); - + getNodeCredentials("subflow", subflow.id, function(data) { + if (data) { + subflow.credentials = data; + subflow.credentials._ = $.extend(true,{},data); + } $("#subflow-input-name").val(subflow.name); RED.text.bidi.prepareInput($("#subflow-input-name")); finishedBuilding = true; + buildingEditDialog = false; + done(); }); }, @@ -2436,7 +2449,39 @@ RED.editor = (function() { RED.tray.show(trayOptions); } + function getNodeCredentials(type, id, done) { + var timeoutNotification; + var intialTimeout = setTimeout(function() { + timeoutNotification = RED.notify($('

    ').i18n(),{fixed: true}) + },800); + + $.ajax({ + url: getCredentialsURL(type,id), + dataType: 'json', + success: function(data) { + if (timeoutNotification) { + timeoutNotification.close(); + timeoutNotification = null; + } + clearTimeout(intialTimeout); + done(data); + }, + error: function(jqXHR,status,error) { + if (timeoutNotification) { + timeoutNotification.close(); + timeoutNotification = null; + } + clearTimeout(intialTimeout); + RED.notify(RED._("editor.errors.credentialLoadFailed"),"error") + done(null); + }, + timeout: 10000, + }); + } + function showEditGroupDialog(group) { + if (buildingEditDialog) { return } + buildingEditDialog = true; var editing_node = group; editStack.push(group); RED.view.state(RED.state.EDITING); @@ -2660,6 +2705,7 @@ RED.editor = (function() { prepareEditDialog(group,group._def,"node-input", function() { trayBody.i18n(); finishedBuilding = true; + buildingEditDialog = false; done(); }); }, From f4c87af5c152f6254c5077abd2d1186634d1440a Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 28 Jan 2021 11:30:10 +0000 Subject: [PATCH 30/39] Increase credential load timeout to 30secs --- .../node_modules/@node-red/editor-client/src/js/ui/editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js index 527f27bf1..82a81da95 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js @@ -2475,7 +2475,7 @@ var buildingEditDialog = false; RED.notify(RED._("editor.errors.credentialLoadFailed"),"error") done(null); }, - timeout: 10000, + timeout: 30000, }); } From a50404b141a0bf31862f05c1b0a4137fd7ecb6ee Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 28 Jan 2021 22:07:19 +0000 Subject: [PATCH 31/39] Add enable/disable toggle button for groups in info-outliner --- .../src/js/ui/tab-info-outliner.js | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js index f0c6e631a..2b11007ee 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js @@ -169,7 +169,7 @@ RED.sidebar.info.outliner = (function() { // evt.stopPropagation(); // RED.view.reveal(n.id); // }) - if (n.type !== 'group' && n.type !== 'subflow') { + if (n.type !== 'subflow') { var toggleButton = $('').appendTo(controls).on("click",function(evt) { evt.preventDefault(); evt.stopPropagation(); @@ -179,6 +179,45 @@ RED.sidebar.info.outliner = (function() { } else { RED.workspaces.disable(n.id) } + } else if (n.type === 'group') { + var groupNodes = RED.group.getNodes(n,true); + var groupHistoryEvent = { + t:'multi', + events:[], + dirty: RED.nodes.dirty() + } + var targetState; + groupNodes.forEach(function(n) { + if (n.type !== 'group') { + if (targetState === undefined) { + targetState = !n.d; + } + if (!!n.d !== targetState) { + var historyEvent = { + t: "edit", + node: n, + changed: n.changed, + changes: { + d: n.d + } + } + if (n.d) { + delete n.d; + } else { + n.d = true; + } + n.dirty = true; + n.changed = true; + RED.events.emit("nodes:change",n); + groupHistoryEvent.events.push(historyEvent); + } + } + if (groupHistoryEvent.events.length > 0) { + RED.history.push(groupHistoryEvent); + RED.nodes.dirty(true) + RED.view.redraw(); + } + }) } else { // TODO: this ought to be a utility function in RED.nodes var historyEvent = { @@ -198,11 +237,15 @@ RED.sidebar.info.outliner = (function() { n.dirty = true; n.changed = true; RED.events.emit("nodes:change",n); + RED.history.push(historyEvent); RED.nodes.dirty(true) RED.view.redraw(); } }); RED.popover.tooltip(toggleButton,function() { + if (n.type === "group") { + return RED._("common.label.enable")+" / "+RED._("common.label.disable") + } return RED._("common.label."+(((n.type==='tab' && n.disabled) || (n.type!=='tab' && n.d))?"enable":"disable")); }); } else { From bb4b252401336a27326961425857fbc7daaa56a2 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 28 Jan 2021 23:08:47 +0000 Subject: [PATCH 32/39] Add confirm dialog when deleting subflow with instances in use --- .../editor-client/locales/en-US/editor.json | 1 + .../editor-client/src/js/ui/subflow.js | 41 ++++++++++++++++--- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 7cdfde99d..881e9897c 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -338,6 +338,7 @@ "output": "outputs:", "status": "status node", "deleteSubflow": "delete subflow", + "confirmDelete": "Are you sure you want to delete this subflow?", "info": "Description", "category": "Category", "module": "Module", diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js index b4cf4be08..6227ef37b 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js @@ -464,12 +464,43 @@ RED.subflow = (function() { $("#red-ui-subflow-delete").on("click", function(event) { event.preventDefault(); - var startDirty = RED.nodes.dirty(); - var historyEvent = removeSubflow(RED.workspaces.active()); - historyEvent.t = 'delete'; - historyEvent.dirty = startDirty; + var subflow = RED.nodes.subflow(RED.workspaces.active()); + if (subflow.instances.length > 0) { + var msg = $('
    ') + $('

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

    ').text(RED._("subflow.confirmDelete")).appendTo(msg); + var confirmDeleteNotification = RED.notify(msg, { + modal: true, + fixed: true, + buttons: [ + { + text: RED._('common.label.cancel'), + click: function() { + confirmDeleteNotification.close(); + } + }, + { + text: RED._('workspace.confirmDelete'), + class: "primary", + click: function() { + confirmDeleteNotification.close(); + completeDelete(); + } + } + ] + }); - RED.history.push(historyEvent); + return; + } else { + completeDelete(); + } + function completeDelete() { + var startDirty = RED.nodes.dirty(); + var historyEvent = removeSubflow(RED.workspaces.active()); + historyEvent.t = 'delete'; + historyEvent.dirty = startDirty; + RED.history.push(historyEvent); + } }); From dad47ade387dbc008b3e18f12ed4b9e0364d7449 Mon Sep 17 00:00:00 2001 From: Kazuhiro Ito Date: Fri, 29 Jan 2021 11:05:15 +0900 Subject: [PATCH 33/39] Add config node to refer to when exporting subflow --- .../@node-red/editor-client/src/js/nodes.js | 57 +++++++++++-------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index 81ac4b771..7f7778ee8 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -849,40 +849,47 @@ RED.nodes = (function() { subflowSet.push(n); } }); + RED.nodes.eachConfig(function(n) { + if (n.z == subflowId) { + subflowSet.push(n); + } + }); var exportableSubflow = createExportableNodeSet(subflowSet, exportedIds, exportedSubflows, exportedConfigNodes); nns = exportableSubflow.concat(nns); } } if (node.type !== "subflow") { - var convertedNode = RED.nodes.convertNode(node); - for (var d in node._def.defaults) { - if (node._def.defaults[d].type) { - var nodeList = node[d]; - if (!Array.isArray(nodeList)) { - nodeList = [nodeList]; - } - nodeList = nodeList.filter(function(id) { - if (id in configNodes) { - var confNode = configNodes[id]; - if (confNode._def.exportable !== false) { - if (!(id in exportedConfigNodes)) { - exportedConfigNodes[id] = true; - set.push(confNode); - return true; - } - } - return false; + if (nns.findIndex((v) => v && v.id === node.id) == -1) { + var convertedNode = RED.nodes.convertNode(node); + for (var d in node._def.defaults) { + if (node._def.defaults[d].type) { + var nodeList = node[d]; + if (!Array.isArray(nodeList)) { + nodeList = [nodeList]; + } + nodeList = nodeList.filter(function(id) { + if (id in configNodes) { + var confNode = configNodes[id]; + if (confNode._def.exportable !== false) { + if (!(id in exportedConfigNodes)) { + exportedConfigNodes[id] = true; + set.push(confNode); + return true; + } + } + return false; + } + return true; + }) + if (nodeList.length === 0) { + convertedNode[d] = Array.isArray(node[d])?[]:"" + } else { + convertedNode[d] = Array.isArray(node[d])?nodeList:nodeList[0] } - return true; - }) - if (nodeList.length === 0) { - convertedNode[d] = Array.isArray(node[d])?[]:"" - } else { - convertedNode[d] = Array.isArray(node[d])?nodeList:nodeList[0] } } + nns.push(convertedNode); } - nns.push(convertedNode); if (node.type === "group") { nns = nns.concat(createExportableNodeSet(node.nodes, exportedIds, exportedSubflows, exportedConfigNodes)); } From 3bd1bfc769d911ed97a200fcf030b36a336b5f5f Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 29 Jan 2021 10:16:40 +0000 Subject: [PATCH 34/39] Fix check for existing config nodes in subflow export set --- .../@node-red/editor-client/src/js/nodes.js | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index 7f7778ee8..236f7fa57 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -852,6 +852,7 @@ RED.nodes = (function() { RED.nodes.eachConfig(function(n) { if (n.z == subflowId) { subflowSet.push(n); + exportedConfigNodes[n.id] = true; } }); var exportableSubflow = createExportableNodeSet(subflowSet, exportedIds, exportedSubflows, exportedConfigNodes); @@ -859,37 +860,35 @@ RED.nodes = (function() { } } if (node.type !== "subflow") { - if (nns.findIndex((v) => v && v.id === node.id) == -1) { - var convertedNode = RED.nodes.convertNode(node); - for (var d in node._def.defaults) { - if (node._def.defaults[d].type) { - var nodeList = node[d]; - if (!Array.isArray(nodeList)) { - nodeList = [nodeList]; - } - nodeList = nodeList.filter(function(id) { - if (id in configNodes) { - var confNode = configNodes[id]; - if (confNode._def.exportable !== false) { - if (!(id in exportedConfigNodes)) { - exportedConfigNodes[id] = true; - set.push(confNode); - return true; - } + var convertedNode = RED.nodes.convertNode(node); + for (var d in node._def.defaults) { + if (node._def.defaults[d].type) { + var nodeList = node[d]; + if (!Array.isArray(nodeList)) { + nodeList = [nodeList]; + } + nodeList = nodeList.filter(function(id) { + if (id in configNodes) { + var confNode = configNodes[id]; + if (confNode._def.exportable !== false) { + if (!(id in exportedConfigNodes)) { + exportedConfigNodes[id] = true; + set.push(confNode); + return true; } - return false; } - return true; - }) - if (nodeList.length === 0) { - convertedNode[d] = Array.isArray(node[d])?[]:"" - } else { - convertedNode[d] = Array.isArray(node[d])?nodeList:nodeList[0] + return false; } + return true; + }) + if (nodeList.length === 0) { + convertedNode[d] = Array.isArray(node[d])?[]:"" + } else { + convertedNode[d] = Array.isArray(node[d])?nodeList:nodeList[0] } } - nns.push(convertedNode); } + nns.push(convertedNode); if (node.type === "group") { nns = nns.concat(createExportableNodeSet(node.nodes, exportedIds, exportedSubflows, exportedConfigNodes)); } From e5b7ccb612c62f842416db708ec566eb32e572ed Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 29 Jan 2021 10:42:04 +0000 Subject: [PATCH 35/39] Add subflow edit button to palette tooltip --- .../@node-red/editor-client/src/js/ui/palette.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js index 3772f83d9..0cb202051 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js @@ -170,9 +170,16 @@ RED.palette = (function() { metaData = typeInfo.set.module+" : "; } metaData += type; + + if (/^subflow:/.test(type)) { + $('').appendTo(popOverContent) + } + var safeType = type.replace(/'/g,"\\'"); - $('').appendTo(popOverContent) - $('').appendTo(popOverContent) + + $('').appendTo(popOverContent) + $('').appendTo(popOverContent) + $('

    ',{style:"font-size: 0.8em"}).text(metaData).appendTo(popOverContent); } } catch(err) { From 32692dce077f271e3b4e7b929c7b4dd6aa9bf814 Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Sat, 30 Jan 2021 09:43:01 +0900 Subject: [PATCH 36/39] fix jshint failure --- .../@node-red/editor-client/src/js/ui/tab-info-outliner.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js index 6949ad261..891f9f7dc 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info-outliner.js @@ -175,7 +175,8 @@ RED.sidebar.info.outliner = (function() { if (targetState === undefined) { targetState = !n.d; } - if (!!n.d !== targetState) { + var state = !!n.d; + if (state !== targetState) { var historyEvent = { t: "edit", node: n, From 7bde7f0cfd0b02dd37974ae25f46e86aad4525c6 Mon Sep 17 00:00:00 2001 From: Matthias Radde Date: Sat, 30 Jan 2021 19:30:29 +0100 Subject: [PATCH 37/39] fixing minor typo in node's documentation (#2848) --- .../@node-red/nodes/locales/de/sequence/19-batch.html | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/locales/de/sequence/19-batch.html b/packages/node_modules/@node-red/nodes/locales/de/sequence/19-batch.html index 68df80963..aeab9c15e 100755 --- a/packages/node_modules/@node-red/nodes/locales/de/sequence/19-batch.html +++ b/packages/node_modules/@node-red/nodes/locales/de/sequence/19-batch.html @@ -20,19 +20,18 @@

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

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

    Speichern der Nachrichten

    From 74db3e17d075f23d9c95d7871586cf461524c456 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 1 Feb 2021 13:39:39 +0000 Subject: [PATCH 38/39] Restrict project file access to inside the project directory --- .../localfilesystem/projects/Project.js | 20 ++++++++++++++++++- .../storage/localfilesystem/projects/index.js | 19 ++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js index 0fbf89de4..4cef78b04 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js @@ -305,6 +305,9 @@ Project.prototype.update = function (user, data) { return Promise.reject("Invalid package file: "+data.files.package) } var root = data.files.package.substring(0,data.files.package.length-12); + if (/^\.\./.test(fspath.relative(this.path,fspath.join(this.path,data.files.package)))) { + return Promise.reject("Invalid package file: "+data.files.package) + } this.paths.root = root; this.paths['package.json'] = data.files.package; globalProjectSettings.projects[this.name].rootPath = root; @@ -322,12 +325,18 @@ Project.prototype.update = function (user, data) { } if (data.files.hasOwnProperty('flow') && this.package['node-red'].settings.flowFile !== data.files.flow.substring(this.paths.root.length)) { + if (/^\.\./.test(fspath.relative(this.path,fspath.join(this.path,data.files.flow)))) { + return Promise.reject("Invalid flow file: "+data.files.flow) + } this.paths.flowFile = data.files.flow; this.package['node-red'].settings.flowFile = data.files.flow.substring(this.paths.root.length); savePackage = true; flowFilesChanged = true; } if (data.files.hasOwnProperty('credentials') && this.package['node-red'].settings.credentialsFile !== data.files.credentials.substring(this.paths.root.length)) { + if (/^\.\./.test(fspath.relative(this.path,fspath.join(this.path,data.files.credentials)))) { + return Promise.reject("Invalid credentials file: "+data.files.credentials) + } this.paths.credentialsFile = data.files.credentials; this.package['node-red'].settings.credentialsFile = data.files.credentials.substring(this.paths.root.length); // Don't know if the credSecret is invalid or not so clear the flag @@ -490,6 +499,10 @@ Project.prototype.getFile = function (filePath,treeish) { if (treeish !== "_") { return gitTools.getFile(this.path, filePath, treeish); } else { + let fullPath = fspath.join(this.path,filePath); + if (/^\.\./.test(fspath.relative(this.path,fullPath))) { + throw new Error("Invalid file name") + } return fs.readFile(fspath.join(this.path,filePath),"utf8"); } }; @@ -639,6 +652,11 @@ Project.prototype.pull = function (user,remoteBranchName,setRemote,allowUnrelate Project.prototype.resolveMerge = function (file,resolutions) { var filePath = fspath.join(this.path,file); + + if (/^\.\./.test(fspath.relative(this.path,filePath))) { + throw new Error("Invalid file name") + } + var self = this; if (typeof resolutions === 'string') { return util.writeFile(filePath, resolutions).then(function() { @@ -1062,7 +1080,7 @@ function loadProject(projectPath) { function init(_settings, _runtime) { settings = _settings; runtime = _runtime; - projectsDir = fspath.join(settings.userDir,"projects"); + projectsDir = fspath.resolve(fspath.join(settings.userDir,"projects")); authCache.init(); } diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js index 5e1d4f15b..24f18b097 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js @@ -110,7 +110,7 @@ function init(_settings, _runtime) { globalGitUser = gitConfig.user; Projects.init(settings,runtime); sshTools.init(settings); - projectsDir = fspath.join(settings.userDir,"projects"); + projectsDir = fspath.resolve(fspath.join(settings.userDir,"projects")); if (!settings.readOnly) { return fs.ensureDir(projectsDir) //TODO: this is accessing settings from storage directly as settings @@ -207,9 +207,16 @@ function getBackupFilename(filename) { } function loadProject(name) { + let fullPath = fspath.resolve(fspath.join(projectsDir,name)); var projectPath = name; if (projectPath.indexOf(fspath.sep) === -1) { - projectPath = fspath.join(projectsDir,name); + projectPath = fullPath; + } else { + // Ensure this project dir is under projectsDir; + let relativePath = fspath.relative(projectsDir,fullPath); + if (/^\.\./.test(relativePath)) { + throw new Error("Invalid project name") + } } return Projects.load(projectPath).then(function(project) { activeProject = project; @@ -234,6 +241,10 @@ function deleteProject(user, name) { throw e; } var projectPath = fspath.join(projectsDir,name); + let relativePath = fspath.relative(projectsDir,projectPath); + if (/^\.\./.test(relativePath)) { + throw new Error("Invalid project name") + } return Projects.delete(user, projectPath); } @@ -392,6 +403,10 @@ function createProject(user, metadata) { metadata.files.credentialSecret = currentEncryptionKey; } metadata.path = fspath.join(projectsDir,metadata.name); + if (/^\.\./.test(fspath.relative(projectsDir,metadata.path))) { + throw new Error("Invalid project name") + } + return Projects.create(user, metadata).then(function(p) { return setActiveProject(user, p.name); }).then(function() { From 23f0cd3a2610c68688ea1f04fd61e9b8f6982166 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 2 Feb 2021 13:11:33 +0000 Subject: [PATCH 39/39] Bump for 1.2.8 --- CHANGELOG.md | 20 +++++++++ package.json | 4 +- .../@node-red/editor-api/package.json | 6 +-- .../@node-red/editor-client/package.json | 2 +- .../node_modules/@node-red/nodes/package.json | 2 +- .../@node-red/registry/package.json | 4 +- .../@node-red/runtime/package.json | 6 +-- .../node_modules/@node-red/util/package.json | 2 +- packages/node_modules/node-red/package.json | 10 ++--- test/nodes/core/function/10-function_spec.js | 44 +++++++++++++++++++ 10 files changed, 82 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdbb4ada9..7af2084c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +### 1.2.8: Maintenance Release + +Editor + + - Ensure subflow help is picked up for palette tooltip Fixes #2834 + - Improve Ru locale (#2826) @alexk111 + - Fix scrollbars (#2825) @alexk111 + +Runtime + + - Restrict project file access to inside the project directory + - Validate user-provided language parameter before passing to i18n + - Fix grunt release mkdir issue on Node.js 14 (#2827) @alexk111 + - Prevent crash when coreNodesDir is empty (#2831) @hardillb + +Nodes + + - Batch node: Fixing minor typo in node's documentation (#2848) @matthiasradde + - Split node: Handle out of order messages as long as one of the messages has msg.parts.count set to the proper value (#2748) @s4ke + ### 1.2.7: Maintenance Release Editor diff --git a/package.json b/package.json index a914a1498..47ec07a5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "1.2.7", + "version": "1.2.8", "description": "Low-code programming for event-driven applications", "homepage": "http://nodered.org", "license": "Apache-2.0", @@ -107,7 +107,7 @@ "marked": "1.2.7", "minami": "1.2.3", "mocha": "^5.2.0", - "node-red-node-test-helper": "^0.2.5", + "node-red-node-test-helper": "^0.2.6", "node-sass": "^4.14.1", "nodemon": "2.0.6", "should": "13.2.3", diff --git a/packages/node_modules/@node-red/editor-api/package.json b/packages/node_modules/@node-red/editor-api/package.json index 89838e602..80374bc72 100644 --- a/packages/node_modules/@node-red/editor-api/package.json +++ b/packages/node_modules/@node-red/editor-api/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-api", - "version": "1.2.7", + "version": "1.2.8", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,8 +16,8 @@ } ], "dependencies": { - "@node-red/util": "1.2.7", - "@node-red/editor-client": "1.2.7", + "@node-red/util": "1.2.8", + "@node-red/editor-client": "1.2.8", "bcryptjs": "2.4.3", "body-parser": "1.19.0", "clone": "2.1.2", diff --git a/packages/node_modules/@node-red/editor-client/package.json b/packages/node_modules/@node-red/editor-client/package.json index 5adb7deed..dc6e33a2c 100644 --- a/packages/node_modules/@node-red/editor-client/package.json +++ b/packages/node_modules/@node-red/editor-client/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-client", - "version": "1.2.7", + "version": "1.2.8", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/@node-red/nodes/package.json b/packages/node_modules/@node-red/nodes/package.json index 25b277921..0c51268fd 100644 --- a/packages/node_modules/@node-red/nodes/package.json +++ b/packages/node_modules/@node-red/nodes/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/nodes", - "version": "1.2.7", + "version": "1.2.8", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/@node-red/registry/package.json b/packages/node_modules/@node-red/registry/package.json index 74816922e..6fbd70d7d 100644 --- a/packages/node_modules/@node-red/registry/package.json +++ b/packages/node_modules/@node-red/registry/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/registry", - "version": "1.2.7", + "version": "1.2.8", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,7 +16,7 @@ } ], "dependencies": { - "@node-red/util": "1.2.7", + "@node-red/util": "1.2.8", "semver": "6.3.0", "tar": "6.0.5", "uglify-js": "3.12.4", diff --git a/packages/node_modules/@node-red/runtime/package.json b/packages/node_modules/@node-red/runtime/package.json index 10adaa423..dd0624882 100644 --- a/packages/node_modules/@node-red/runtime/package.json +++ b/packages/node_modules/@node-red/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/runtime", - "version": "1.2.7", + "version": "1.2.8", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,8 +16,8 @@ } ], "dependencies": { - "@node-red/registry": "1.2.7", - "@node-red/util": "1.2.7", + "@node-red/registry": "1.2.8", + "@node-red/util": "1.2.8", "async-mutex": "0.2.6", "clone": "2.1.2", "express": "4.17.1", diff --git a/packages/node_modules/@node-red/util/package.json b/packages/node_modules/@node-red/util/package.json index fafa7f6ec..e017c6c51 100644 --- a/packages/node_modules/@node-red/util/package.json +++ b/packages/node_modules/@node-red/util/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/util", - "version": "1.2.7", + "version": "1.2.8", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/node-red/package.json b/packages/node_modules/node-red/package.json index 3f18a811e..0b20f87b5 100644 --- a/packages/node_modules/node-red/package.json +++ b/packages/node_modules/node-red/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "1.2.7", + "version": "1.2.8", "description": "Low-code programming for event-driven applications", "homepage": "http://nodered.org", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "flow" ], "dependencies": { - "@node-red/editor-api": "1.2.7", - "@node-red/runtime": "1.2.7", - "@node-red/util": "1.2.7", - "@node-red/nodes": "1.2.7", + "@node-red/editor-api": "1.2.8", + "@node-red/runtime": "1.2.8", + "@node-red/util": "1.2.8", + "@node-red/nodes": "1.2.8", "basic-auth": "2.0.1", "bcryptjs": "2.4.3", "express": "4.17.1", diff --git a/test/nodes/core/function/10-function_spec.js b/test/nodes/core/function/10-function_spec.js index 63e1031b6..d737875cc 100644 --- a/test/nodes/core/function/10-function_spec.js +++ b/test/nodes/core/function/10-function_spec.js @@ -53,6 +53,49 @@ describe('function node', function() { }); }); + it('should send returned message using send()', function(done) { + var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"node.send(msg);"}, + {id:"n2", type:"helper"}]; + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'foo'); + done(); + }); + n1.receive({payload:"foo",topic: "bar"}); + }); + }); + + it('should do something with the catch node', function(done) { + var flow = [{"id":"funcNode","type":"function","wires":[["goodNode"]],"func":"node.error('This is an error', msg);"},{"id":"goodNode","type":"helper"},{"id":"badNode","type":"helper"},{"id":"catchNode","type":"catch","scope":null,"uncaught":false,"wires":[["badNode"]]}]; + var catchNodeModule = require("nr-test-utils").require("@node-red/nodes/core/common/25-catch.js") + helper.load([catchNodeModule, functionNode], flow, function() { + var funcNode = helper.getNode("funcNode"); + var catchNode = helper.getNode("catchNode"); + var goodNode = helper.getNode("goodNode"); + var badNode = helper.getNode("badNode"); + + badNode.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', 'foo'); + msg.should.have.property('error'); + msg.error.should.have.property('message',"This is an error"); + msg.error.should.have.property('source'); + msg.error.source.should.have.property('id', "funcNode"); + done(); + }); + funcNode.receive({payload:"foo",topic: "bar"}); + }); + }); + + + + + +/* + it('should be loaded', function(done) { var flow = [{id:"n1", type:"function", name: "function" }]; helper.load(functionNode, flow, function() { @@ -1560,4 +1603,5 @@ describe('function node', function() { }); }) + */ });