Compare commits

..

5 Commits

Author SHA1 Message Date
Nick O'Leary
b1a706f811 Add comment on RED.nodes.getNodeLinkCount api 2022-01-12 18:04:53 +00:00
Nick O'Leary
c27dd336d9 Move ports behind node body and increase touch zone for each port 2022-01-12 18:01:11 +00:00
Nick O'Leary
5e9ff98c49 Add arrow-heads to links 2022-01-12 18:01:06 +00:00
Nick O'Leary
50cb074172 Visually distinguish ports with connected wires 2022-01-12 18:01:01 +00:00
Nick O'Leary
510a09ecba Add RED.nodes.getNodeLinkCount to provide O(1) lookup of link count on node port 2022-01-12 18:00:53 +00:00
27 changed files with 3128 additions and 282 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -26,13 +26,13 @@
}
],
"dependencies": {
"acorn": "8.7.0",
"acorn": "8.6.0",
"acorn-walk": "8.2.0",
"ajv": "8.8.2",
"async-mutex": "0.3.2",
"basic-auth": "2.0.1",
"bcryptjs": "2.4.3",
"body-parser": "1.19.1",
"body-parser": "1.19.0",
"cheerio": "1.0.0-rc.10",
"clone": "2.1.2",
"content-type": "1.0.4",
@@ -41,7 +41,7 @@
"cors": "2.8.5",
"cronosjs": "1.7.1",
"denque": "2.0.1",
"express": "4.17.2",
"express": "4.17.1",
"express-session": "1.17.2",
"form-data": "4.0.0",
"fs-extra": "10.0.0",
@@ -50,7 +50,7 @@
"hash-sum": "2.0.0",
"hpagent": "0.1.2",
"https-proxy-agent": "5.0.0",
"i18next": "21.6.6",
"i18next": "21.5.4",
"iconv-lite": "0.6.3",
"is-utf8": "0.2.1",
"js-yaml": "3.14.1",
@@ -61,21 +61,21 @@
"memorystore": "1.6.6",
"mime": "2.5.2",
"moment-timezone": "0.5.34",
"mqtt": "4.3.4",
"mqtt": "4.2.8",
"multer": "1.4.3",
"mustache": "4.2.0",
"node-red-admin": "^2.2.1",
"nopt": "5.0.0",
"oauth2orize": "1.11.1",
"on-headers": "1.0.2",
"passport": "0.5.2",
"passport": "0.5.0",
"passport-http-bearer": "1.0.1",
"passport-oauth2-client-password": "0.1.2",
"raw-body": "2.4.2",
"semver": "7.3.5",
"tar": "6.1.11",
"tough-cookie": "4.0.0",
"uglify-js": "3.14.5",
"uglify-js": "3.14.4",
"uuid": "8.3.2",
"ws": "7.5.1",
"xml2js": "0.4.23"
@@ -84,7 +84,7 @@
"bcrypt": "5.0.1"
},
"devDependencies": {
"dompurify": "2.3.4",
"dompurify": "2.3.3",
"grunt": "1.4.1",
"grunt-chmod": "~1.1.1",
"grunt-cli": "~1.4.3",
@@ -113,11 +113,11 @@
"node-red-node-test-helper": "^0.2.7",
"nodemon": "2.0.15",
"proxy": "^1.0.2",
"sass": "1.48.0",
"sass": "1.44.0",
"should": "13.2.3",
"sinon": "11.1.2",
"stoppable": "^1.1.0",
"supertest": "6.2.1"
"supertest": "6.1.6"
},
"engines": {
"node": ">=12"

View File

@@ -19,11 +19,11 @@
"@node-red/util": "2.2.0-beta.1",
"@node-red/editor-client": "2.2.0-beta.1",
"bcryptjs": "2.4.3",
"body-parser": "1.19.1",
"body-parser": "1.19.0",
"clone": "2.1.2",
"cors": "2.8.5",
"express-session": "1.17.2",
"express": "4.17.2",
"express": "4.17.1",
"memorystore": "1.6.6",
"mime": "2.5.2",
"multer": "1.4.3",
@@ -31,7 +31,7 @@
"oauth2orize": "1.11.1",
"passport-http-bearer": "1.0.1",
"passport-oauth2-client-password": "0.1.2",
"passport": "0.5.2",
"passport": "0.5.0",
"ws": "7.5.1"
},
"optionalDependencies": {

View File

@@ -594,7 +594,9 @@ RED.nodes = (function() {
}
allNodes.addNode(n);
if (!nodeLinks[n.id]) {
nodeLinks[n.id] = {in:[],out:[]};
nodeLinks[n.id] = {
inCount:[],outCount:[],in:[],out:[]
};
}
}
RED.events.emit('nodes:add',n);
@@ -604,15 +606,19 @@ RED.nodes = (function() {
if (l.source) {
// Possible the node hasn't been added yet
if (!nodeLinks[l.source.id]) {
nodeLinks[l.source.id] = {in:[],out:[]};
nodeLinks[l.source.id] = {inCount:[],outCount:[],in:[],out:[]};
}
nodeLinks[l.source.id].out.push(l);
nodeLinks[l.source.id].outCount[l.sourcePort] = (nodeLinks[l.source.id].outCount[l.sourcePort] || 0) + 1
l.source.dirty = true;
}
if (l.target) {
if (!nodeLinks[l.target.id]) {
nodeLinks[l.target.id] = {in:[],out:[]};
nodeLinks[l.target.id] = {inCount:[],outCount:[],in:[],out:[]};
}
nodeLinks[l.target.id].in.push(l);
nodeLinks[l.target.id].inCount[0] = (nodeLinks[l.target.id].inCount[0] || 0) + 1
l.target.dirty = true;
}
if (l.source.z === l.target.z && linkTabMap[l.source.z]) {
linkTabMap[l.source.z].push(l);
@@ -761,15 +767,19 @@ RED.nodes = (function() {
if (index != -1) {
links.splice(index,1);
if (l.source && nodeLinks[l.source.id]) {
l.source.dirty = true;
var sIndex = nodeLinks[l.source.id].out.indexOf(l)
if (sIndex !== -1) {
nodeLinks[l.source.id].out.splice(sIndex,1)
nodeLinks[l.source.id].outCount[l.sourcePort]--
}
}
if (l.target && nodeLinks[l.target.id]) {
l.target.dirty = true;
var tIndex = nodeLinks[l.target.id].in.indexOf(l)
if (tIndex !== -1) {
nodeLinks[l.target.id].in.splice(tIndex,1)
nodeLinks[l.target.id].inCount[0]--
}
}
if (l.source.z === l.target.z && linkTabMap[l.source.z]) {
@@ -2706,6 +2716,20 @@ RED.nodes = (function() {
}
return [];
},
getNodeLinkCount: function(id,portType,index) {
// We *could* just let callers use `getNodeLinks` and get the
// the length for themselves. However, that function creates
// a clone of the array - which is needless work if all you
// want is the length
if (nodeLinks[id]) {
if (portType === 1) {
return nodeLinks[id].inCount[index] || 0
} else {
return nodeLinks[id].outCount[index] || 0
}
}
return 0;
},
addWorkspace: addWorkspace,
removeWorkspace: removeWorkspace,
getWorkspaceOrder: function() { return workspacesOrder },

View File

@@ -946,25 +946,28 @@ RED.clipboard = (function() {
if (truncated) {
msg += "_truncated";
}
navigator.clipboard.writeText(value).then(function () {
if (element) {
var popover = RED.popover.create({
target: element,
direction: 'left',
size: 'small',
content: RED._(msg)
});
setTimeout(function() {
popover.close();
},1000);
popover.open();
}
if (currentFocus) {
$(currentFocus).focus();
}
}).catch(err => { console.error("Failed to copy:",err) });
$("#red-ui-clipboard-hidden").val(value).focus().select();
var result = document.execCommand("copy");
if (result && element) {
var popover = RED.popover.create({
target: element,
direction: 'left',
size: 'small',
content: RED._(msg)
});
setTimeout(function() {
popover.close();
},1000);
popover.open();
}
$("#red-ui-clipboard-hidden").val("");
if (currentFocus) {
$(currentFocus).focus();
}
return result;
}
function importNodes(nodesStr,addFlow) {
var newNodes = nodesStr;
if (typeof nodesStr === 'string') {
@@ -1239,6 +1242,8 @@ RED.clipboard = (function() {
init: function() {
setupDialogs();
$('<textarea type="text" id="red-ui-clipboard-hidden" tabIndex="-1">').appendTo("#red-ui-editor");
RED.actions.add("core:show-export-dialog",showExportNodes);
RED.actions.add("core:show-import-dialog",showImportNodes);

View File

@@ -247,7 +247,7 @@
var currentExpression = expressionEditor.getValue();
var expr;
var usesContext = false;
var legacyMode = /(^|[^a-zA-Z0-9_'".])msg([^a-zA-Z0-9_'"]|$)/.test(currentExpression);
var legacyMode = /(^|[^a-zA-Z0-9_'"])msg([^a-zA-Z0-9_'"]|$)/.test(currentExpression);
$(".red-ui-editor-type-expression-legacy").toggle(legacyMode);
try {
expr = jsonata(currentExpression);

View File

@@ -256,10 +256,6 @@ RED.tourGuide = (function() {
}
$('<div>').css("text-align","left").html(getLocaleText(step.description)).appendTo(stepDescription);
if (step.image) {
$(`<img src="red/tours/${step.image}" />`).appendTo(stepDescription)
}
var stepToolbar = $('<div>',{class:"red-ui-tourGuide-toolbar"}).appendTo(stepContent);
// var breadcrumbs = $('<div>',{class:"red-ui-tourGuide-breadcrumbs"}).appendTo(stepToolbar);

View File

@@ -37,6 +37,8 @@ RED.view = (function() {
node_height = 30,
dblClickInterval = 650;
var ARROW_HEADS = false;
var touchLongPressTimeout = 1000,
startTouchDistance = 0,
startTouchCenter = [],
@@ -835,8 +837,9 @@ RED.view = (function() {
} else {
scale = 0.4-0.2*(Math.max(0,(node_width-Math.min(Math.abs(dx),Math.abs(dy)))/node_width));
}
var result;
if (dx*sc > 0) {
return "M "+origX+" "+origY+
result = "M "+origX+" "+origY+
" C "+(origX+sc*(node_width*scale))+" "+(origY+scaleY*node_height)+" "+
(destX-sc*(scale)*node_width)+" "+(destY-scaleY*node_height)+" "+
destX+" "+destY
@@ -877,7 +880,7 @@ RED.view = (function() {
}
cp[2][0] = topX;
}
return "M "+origX+" "+origY+
result = "M "+origX+" "+origY+
" C "+
cp[0][0]+" "+cp[0][1]+" "+
cp[1][0]+" "+cp[1][1]+" "+
@@ -892,6 +895,7 @@ RED.view = (function() {
cp[4][0]+" "+cp[4][1]+" "+
destX+" "+destY
}
return result;
}
function addNode(type,x,y) {
@@ -4131,6 +4135,11 @@ RED.view = (function() {
}
node[0][0].__portGroup__ = document.createElementNS("http://www.w3.org/2000/svg","g");
node[0][0].__portGroup__.__data__ = d;
nodeContents.appendChild(node[0][0].__portGroup__);
var mainRect = document.createElementNS("http://www.w3.org/2000/svg","rect");
mainRect.__data__ = d;
mainRect.setAttribute("class", "red-ui-flow-node "+(d.type == "unknown"?"red-ui-flow-node-unknown":""));
@@ -4334,7 +4343,7 @@ RED.view = (function() {
this.__textGroup__.setAttribute("transform", "translate(38,"+yp+")");
}
var inputPorts = thisNode.selectAll(".red-ui-flow-port-input");
var inputPorts = d3.select(this.__portGroup__).selectAll(".red-ui-flow-port-input");
if ((!isLink || (showAllLinkPorts === -1 && !activeLinkNodes[d.id])) && d.inputs === 0 && !inputPorts.empty()) {
inputPorts.each(function(d,i) {
RED.hooks.trigger("viewRemovePort",{
@@ -4345,8 +4354,9 @@ RED.view = (function() {
portIndex: 0
})
}).remove();
this.__inputs__ = [];
} else if (((isLink && (showAllLinkPorts===PORT_TYPE_INPUT||activeLinkNodes[d.id]))|| d.inputs === 1) && inputPorts.empty()) {
var inputGroup = thisNode.append("g").attr("class","red-ui-flow-port-input");
var inputGroup = d3.select(this.__portGroup__).append("g").attr("class","red-ui-flow-port-input");
var inputGroupPorts;
if (d.type === "link in") {
@@ -4355,7 +4365,8 @@ RED.view = (function() {
.attr("r",5)
.attr("class","red-ui-flow-port red-ui-flow-link-port")
} else {
inputGroupPorts = inputGroup.append("rect").attr("class","red-ui-flow-port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
inputGroupPorts = inputGroup.append("path").attr("class","red-ui-flow-port-background").attr("d","M5 -1.5 h -16 v 13 h 16 z")
inputGroup.append("path").attr("class","red-ui-flow-port red-ui-flow-port-shape").attr("d","M5 0 h -3 c -4 0 -4 0 -4 4 v 2 c 0 4 0 4 4 4 h 3 z ")
}
inputGroup[0][0].__port__ = inputGroupPorts[0][0];
inputGroupPorts[0][0].__data__ = this.__data__;
@@ -4367,6 +4378,8 @@ RED.view = (function() {
.on("touchend",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} )
.on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);})
.on("mouseout",function(d) {portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);});
this.__inputs__.push(inputGroup[0][0]);
RED.hooks.trigger("viewAddPort",{node:d,el: this, port: inputGroup[0][0], portType: "input", portIndex: 0})
}
var numOutputs = d.outputs;
@@ -4403,15 +4416,19 @@ RED.view = (function() {
portPort.setAttribute("cy",5);
portPort.setAttribute("r",5);
portPort.setAttribute("class","red-ui-flow-port red-ui-flow-link-port");
portGroup.appendChild(portPort);
} else {
portPort = document.createElementNS("http://www.w3.org/2000/svg","rect");
portPort.setAttribute("rx",3);
portPort.setAttribute("ry",3);
portPort.setAttribute("width",10);
portPort.setAttribute("height",10);
portPort.setAttribute("class","red-ui-flow-port");
portPort = document.createElementNS("http://www.w3.org/2000/svg","path");
portPort.setAttribute("d","M5 -1.5 h 16 v 13 h -16 z ");
portPort.setAttribute("class","red-ui-flow-port-background");
portGroup.appendChild(portPort);
var visiblePort = document.createElementNS("http://www.w3.org/2000/svg","path");
visiblePort.setAttribute("d","M5 0 h 4 c 4 0 4 0 4 4 v 2 c 0 4 0 4 -4 4 h -4 z ");
visiblePort.setAttribute("class","red-ui-flow-port red-ui-flow-port-shape");
portGroup.appendChild(visiblePort);
}
portGroup.appendChild(portPort);
portGroup.__port__ = portPort;
portPort.__data__ = this.__data__;
portPort.__portType__ = PORT_TYPE_OUTPUT;
@@ -4423,7 +4440,7 @@ RED.view = (function() {
portPort.addEventListener("mouseover", portMouseOverProxy);
portPort.addEventListener("mouseout", portMouseOutProxy);
this.appendChild(portGroup);
this.__portGroup__.appendChild(portGroup);
this.__outputs__.push(portGroup);
RED.hooks.trigger("viewAddPort",{node:d,el: this, port: portGroup, portType: "output", portIndex: portIndex})
} else {
@@ -4514,6 +4531,14 @@ RED.view = (function() {
// });
}
for (var i=0,l=this.__outputs__.length;i<l;i++) {
this.__outputs__[i].classList.toggle("red-ui-flow-port-connected",RED.nodes.getNodeLinkCount(d.id,PORT_TYPE_OUTPUT,i) > 0 )
}
for (var i=0,l=this.__inputs__.length;i<l;i++) {
this.__inputs__[i].classList.toggle("red-ui-flow-port-connected",RED.nodes.getNodeLinkCount(d.id,PORT_TYPE_INPUT,i) > 0 )
}
if (d.dirtyStatus) {
redrawStatus(d,this);
}
@@ -4594,18 +4619,14 @@ RED.view = (function() {
d.y1 = d.source.y+y;
d.x2 = d.target.x-d.target.w/2;
d.y2 = d.target.y;
// return "M "+d.x1+" "+d.y1+
// " C "+(d.x1+scale*node_width)+" "+(d.y1+scaleY*node_height)+" "+
// (d.x2-scale*node_width)+" "+(d.y2-scaleY*node_height)+" "+
// d.x2+" "+d.y2;
var path = generateLinkPath(d.x1,d.y1,d.x2,d.y2,1);
var targetOffset = ARROW_HEADS?(d.link?16:13):0
var path = generateLinkPath(d.x1+5,d.y1,d.x2-targetOffset,d.y2,1);
if (/NaN/.test(path)) {
path = ""
}
this.__pathBack__.setAttribute("d",path);
this.__pathOutline__.setAttribute("d",path);
this.__pathLine__.setAttribute("d",path);
this.__pathLine__.setAttribute("d",path + (ARROW_HEADS?"m0 0, l 0 -2 l 3 2 l -3 2 z":""));
this.__pathLine__.classList.toggle("red-ui-flow-node-disabled",!!(d.source.d || d.target.d));
this.__pathLine__.classList.toggle("red-ui-flow-subflow-link", !d.link && activeSubflow);
}

View File

@@ -66,7 +66,7 @@ RED.workspaces = (function() {
var tabId = RED.nodes.id();
do {
workspaceIndex += 1;
} while ($("#red-ui-workspace-tabs li[flowname='"+RED._('workspace.defaultName',{number:workspaceIndex})+"']").size() !== 0);
} while ($("#red-ui-workspace-tabs a[title='"+RED._('workspace.defaultName',{number:workspaceIndex})+"']").size() !== 0);
ws = {
type: "tab",
@@ -79,15 +79,12 @@ RED.workspaces = (function() {
};
RED.nodes.addWorkspace(ws,targetIndex);
workspace_tabs.addTab(ws,targetIndex);
workspace_tabs.activateTab(tabId);
if (!skipHistoryEntry) {
RED.history.push({t:'add',workspaces:[ws],dirty:RED.nodes.dirty()});
RED.nodes.dirty(true);
}
}
$("#red-ui-tab-"+(ws.id.replace(".","-"))).attr("flowname",ws.label)
RED.view.focus();
return ws;
}
@@ -586,7 +583,7 @@ RED.workspaces = (function() {
refresh: function() {
RED.nodes.eachWorkspace(function(ws) {
workspace_tabs.renameTab(ws.id,ws.label);
$("#red-ui-tab-"+(ws.id.replace(".","-"))).attr("flowname",ws.label)
})
RED.nodes.eachSubflow(function(sf) {
if (workspace_tabs.contains(sf.id)) {

View File

@@ -196,6 +196,12 @@ $view-background: $secondary-background;
$view-select-mode-background: $secondary-background-selected;
$view-grid-color: #eee;
$link-color: #999;
$link-link-color: #aaa;
$link-disabled-color: #ccc;
$link-link-active-color: #ff7f0e;
$link-unknown-color: #f00;
$node-label-color: #333;
$node-port-label-color: #888;
$node-border: #999;
@@ -203,8 +209,13 @@ $node-border-unknown: #f33;
$node-border-placeholder: #aaa;
$node-background-placeholder: #eee;
$node-port-border: $node-border;
$node-port-border-connected: $link-color;
$node-port-background: #d9d9d9;
$node-port-background-hover: #eee;
$node-port-background-connected: $link-color;
$node-icon-color: #fff;
$node-icon-background-color: rgba(0,0,0,0.05);
$node-icon-background-color-fill: #000;
@@ -232,12 +243,6 @@ $node-status-colors: (
$node-selected-color: #ff7f0e;
$port-selected-color: #ff7f0e;
$link-color: #999;
$link-link-color: #aaa;
$link-disabled-color: #ccc;
$link-link-active-color: #ff7f0e;
$link-unknown-color: #f00;
$clipboard-textarea-background: #F3E7E7;

View File

@@ -356,6 +356,10 @@ button.red-ui-button-small
background: $secondary-background;
}
#red-ui-clipboard-hidden {
position: absolute;
top: -3000px;
}
.form-row .red-ui-editor-node-label-form-row {
margin: 5px 0 0 50px;
label {

View File

@@ -185,12 +185,29 @@
}
.red-ui-flow-port {
stroke: $node-border;
stroke: $node-port-border;
stroke-width: 1;
fill: $node-port-background;
cursor: crosshair;
}
.red-ui-flow-port-background {
opacity: 0;
stroke: none;
fill: #f00;
cursor: crosshair;
}
.red-ui-flow-port-shape {
pointer-events: none;
cursor: crosshair;
}
.red-ui-flow-port-connected .red-ui-flow-port {
fill: $node-port-background-connected;
stroke: $node-port-border-connected;
}
.red-ui-flow-node-error {
fill: $node-status-error-background;
stroke: $node-status-error-border;
@@ -280,9 +297,10 @@ g.red-ui-flow-node-selected {
text-anchor:start;
}
.red-ui-flow-port-hovered {
stroke: $port-selected-color;
fill: $port-selected-color;
.red-ui-flow-port-hovered:not(.red-ui-flow-port-background),
.red-ui-flow-port-background.red-ui-flow-port-hovered + .red-ui-flow-port-shape {
stroke: $port-selected-color !important;
fill: $port-selected-color !important;
}
.red-ui-flow-subflow-port {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -1,12 +1,12 @@
export default {
version: "2.2.0",
version: "2.1.0",
steps: [
{
titleIcon: "fa fa-map-o",
title: {
"en-US": "Welcome to Node-RED 2.2!",
"ja": "Node-RED 2.2へようこそ!"
},
"en-US": "Welcome to Node-RED 2.1!",
"ja": "Node-RED 2.1へようこそ!"
},
description: {
"en-US": "Let's take a moment to discover the new features in this release.",
"ja": "本リリースの新機能を見つけてみましょう。"
@@ -14,84 +14,215 @@ export default {
},
{
title: {
"en-US": "Search history",
"en-US": "A new Tour Guide",
"ja": "新しいツアーガイド"
},
description: {
"en-US": "<p>The Search dialog now keeps a history of your searches, making it easier to go back to a previous search.</p>"
"en-US": "<p>First, as you've already found, we now have this tour of new features. We'll only show the tour the first time you open the editor for each new version of Node-RED.</p>" +
"<p>You can choose not to see this tour in the future by disabling it under the View tab of User Settings.</p>",
"ja": "<p>最初に、既に見つけている様に、新機能の本ツアーがあります。本ツアーは、新バージョンのNode-REDフローエディタを初めて開いた時のみ表示されます。</p>" +
"<p>ユーザ設定の表示タブの中で、この機能を無効化することで、本ツアーを表示しないようにすることもできます。</p>"
}
},
{
title: {
"en-US": "New Edit menu",
"ja": "新しい編集メニュー"
},
element: "#red-ui-search .red-ui-searchBox-form",
prepare(done) {
RED.search.show();
setTimeout(done,400);
prepare() {
$("#red-ui-header-button-sidemenu").trigger("click");
$("#menu-item-edit-menu").parent().addClass("open");
},
complete() {
RED.search.hide();
$("#menu-item-edit-menu").parent().removeClass("open");
},
},
{
title: {
"en-US": "New wiring actions",
},
// image: "images/",
element: "#menu-item-edit-menu-submenu",
interactive: false,
direction: "left",
description: {
"en-US": `<p>A pair of new actions have been added to help with wiring nodes together:</p>
<ul>
<li><b><code>Wire Series Of Nodes</code></b> - adds a wire (if necessary) between each pair of nodes in the order they were selected.</li>
<li><b><code>Wire Node To Multiple</code></b> - wires the first node selected to all of the other selected nodes.</li>
</ul>
<p>Actions can be accessed from the Action List in the main menu.</p>`
},
},
{
title: {
"en-US": "Deleting nodes and reconnecting wires",
},
image: "images/delete-repair.gif",
description: {
"en-US": `<p>It is now possible to delete a selection of nodes and automatically repair the wiring behind them.</p>
<p>This is really useful if you want to remove a node from the middle of the flow.</p>
<p>Hold the Ctrl (or Cmd) key when you press Delete and the nodes will be gone and the wires repaired.</p>
`
"en-US": "<p>The main menu has been updated with a new 'Edit' section. This includes all of the familar options, like cut/paste and undo/redo.</p>" +
"<p>The menu now displays keyboard shortcuts for the options.</p>",
"ja": "<p>メインメニューに「編集」セクションが追加されました。本セクションには、切り取り/貼り付けや、変更操作を戻す/やり直しの様な使い慣れたオプションが含まれています。</p>" +
"<p>本メニューには、オプションのためのキーボードショートカットも表示されるようになりました。</p>"
}
},
{
title: {
"en-US": "Detaching nodes from a flow",
"en-US": "Arranging nodes",
"ja": "ノードの配置"
},
image: "images/detach-repair.gif",
prepare() {
$("#red-ui-header-button-sidemenu").trigger("click");
$("#menu-item-arrange-menu").parent().addClass("open");
},
complete() {
$("#menu-item-arrange-menu").parent().removeClass("open");
},
element: "#menu-item-arrange-menu-submenu",
interactive: false,
direction: "left",
description: {
"en-US": `<p>If you want to remove a node from a flow without deleting it,
you can use the <b><code>Detach Selected Nodes</code></b> action.</p>
<p>The nodes will be removed from their flow, the wiring repaired behind them, and then attached to the mouse
so you can drop them wherever you want in the workspace.</p>
<p>There isn't a default keyboard shortcut assigned for this new action, but
you can add your own via the Keyboard pane of the main Settings dialog.</p>`
"en-US": "<p>The new 'Arrange' section of the menu provides new options to help arrange your nodes. You can align them to a common edge, spread them out evenly or change their order.</p>",
"ja": "<p>メニューの新しい「配置」セクションには、ノードの配置を助ける新しいオプションが提供されています。ノードの端を揃えたり、均等に配置したり、表示順序を変更したりできます。</p>"
}
},
{
title: {
"en-US": "More wiring tricks",
"en-US": "Hiding tabs",
"ja": "タブの非表示"
},
image: "images/slice.gif",
element: "#red-ui-workspace-tabs > li.active",
description: {
"en-US": `<p>A couple more wiring tricks to share.</p>
<p>You can now select multiple wires by holding the Ctrl (or Cmd) key
when clicking on a wire. This makes it easier to delete multiple wires in one go.</p>
<p>If you hold the Ctrl (or Cmd) key, then click and drag with the right-hand mouse button,
you can slice through wires to remove them.</p>`
"en-US": '<p>Tabs can now be hidden by clicking their <i class="fa fa-eye-slash"></i> icon.</p><p>The Info Sidebar will still list all of your tabs, and tell you which ones are currently hidden.',
"ja": '<p><i class="fa fa-eye-slash"></i> アイコンをクリックすることで、タブを非表示にできます。</p><p>情報サイドバーには、全てのタブが一覧表示されており、現在非表示になっているタブを確認できます。'
},
interactive: false,
prepare() {
$("#red-ui-workspace-tabs > li.active .red-ui-tab-close").css("display","block");
},
complete() {
$("#red-ui-workspace-tabs > li.active .red-ui-tab-close").css("display","");
}
},
{
title: {
"en-US": "Node Updates",
"en-US": "Tab menu",
"ja": "タブメニュー"
},
// image: "images/",
element: "#red-ui-workspace-tabs-menu",
description: {
"en-US": `<ul>
<li>The JSON node will now handle parsing Buffer payloads</li>
<li>The TCP Client nodes support TLS connections</li>
<li>The WebSocket node allows you to specify a sub-protocol when connecting</li>
</ul>`
"en-US": "<p>The new tab menu also provides lots of new options for your tabs.</p>",
"ja": "<p>新しいタブメニューには、タブに関する沢山の新しいオプションが提供されています。</p>"
},
interactive: false,
direction: "left",
prepare() {
$("#red-ui-workspace > .red-ui-tabs > .red-ui-tabs-menu a").trigger("click");
},
complete() {
$(document).trigger("click");
}
},
{
title: {
"en-US": "Flow and Group level environment variables",
"ja": "フローとグループの環境変数"
},
element: "#red-ui-workspace-tabs > li.active",
interactive: false,
description: {
"en-US": "<p>Flows and Groups can now have their own environment variables that can be referenced by nodes inside them.</p>",
"ja": "<p>フローとグループには、内部のノードから参照できる環境変数を設定できるようになりました。</p>"
}
},
{
prepare(done) {
RED.editor.editFlow(RED.nodes.workspace(RED.workspaces.active()),"editor-tab-envProperties");
setTimeout(done,700);
},
element: "#red-ui-tab-editor-tab-envProperties-link-button",
description: {
"en-US": "<p>Their edit dialogs have a new Environment Variables section.</p>",
"ja": "<p>編集ダイアログに環境変数セクションが追加されました。</p>"
}
},
{
element: ".node-input-env-container-row",
direction: "left",
description: {
"en-US": '<p>The environment variables are listed in this table and new ones can be added by clicking the <i class="fa fa-plus"></i> button.</p>',
"ja": '<p>この表に環境変数が一覧表示されており、<i class="fa fa-plus"></i>ボタンをクリックすることで新しい変数を追加できます。</p>'
},
complete(done) {
$("#node-dialog-cancel").trigger("click");
setTimeout(done,500);
}
},
{
title: {
"en-US": "Link Call node added",
"ja": "Link Callードを追加"
},
prepare(done) {
this.paletteWasClosed = $("#red-ui-main-container").hasClass("red-ui-palette-closed");
RED.actions.invoke("core:toggle-palette",true)
$('[data-palette-type="link call"]')[0].scrollIntoView({block:"center"})
setTimeout(done,100);
},
element: '[data-palette-type="link call"]',
direction: "right",
description: {
"en-US": "<p>The <code>Link Call</code> node lets you call another flow that begins with a <code>Link In</code> node and get the result back when the message reaches a <code>Link Out</code> node.</p>",
"ja": "<p><code>Link Call</code>ノードを用いることで、<code>Link In</code>ノードから始まるフローを呼び出し、<code>Link Out</code>ノードに到達した時に、結果を取得できます。</p>"
}
},
{
title: {
"en-US": "MQTT nodes support dynamic connections",
"ja": "MQTTードが動的接続をサポート"
},
prepare(done) {
$('[data-palette-type="mqtt out"]')[0].scrollIntoView({block:"center"})
setTimeout(done,100);
},
element: '[data-palette-type="mqtt out"]',
direction: "right",
description: {
"en-US": '<p>The <code>MQTT</code> nodes now support creating their connections and subscriptions dynamically.</p>',
"ja": '<p><code>MQTT</code>ノードは、動的な接続や購読ができるようになりました。</p>'
},
},
{
title: {
"en-US": "File nodes renamed",
"ja": "ファイルノードの名前変更"
},
prepare(done) {
$('[data-palette-type="file"]')[0].scrollIntoView({block:"center"});
setTimeout(done,100);
},
complete() {
if (this.paletteWasClosed) {
RED.actions.invoke("core:toggle-palette",false)
}
},
element: '[data-palette-type="file"]',
direction: "right",
description: {
"en-US": "<p>The file nodes have been renamed to make it clearer which node does what.</p>",
"ja": "<p>fileードの名前が変更され、どのードが何を行うかが明確になりました。</p>"
}
},
{
title: {
"en-US": "Deep copy option on Change node",
"ja": "Changeードのディープコピーオプション"
},
prepare(done) {
var def = RED.nodes.getType('change');
RED.editor.edit({id:"test",type:"change",rules:[{t:"set",p:"payload",pt:"msg", tot:"msg",to:"anotherProperty"}],_def:def, _:def._});
setTimeout(done,700);
},
complete(done) {
$("#node-dialog-cancel").trigger("click");
setTimeout(done,500);
},
element: function() {
return $(".node-input-rule-property-deepCopy").next();
},
description: {
"en-US": "<p>The Set rule has a new option to create a deep copy of the value. This ensures a complete copy is made, rather than using a reference.</p>",
"ja": "<p>値を代入に、値のディープコピーを作成するオプションが追加されました。これによって参照ではなく、完全なコピーが作成されます。</p>"
}
},
{
title: {
"en-US": "And that's not all...",
"ja": "これが全てではありません..."
},
description: {
"en-US": "<p>There are many more smaller changes, including:</p><ul><li>Auto-complete suggestions in the <code>msg</code> TypedInput.</li><li>Support for <code>msg.resetTimeout</code> in the <code>Join</code> node.</li><li>Pushing messages to the front of the queue in the <code>Delay</code> node's rate limiting mode.</li><li>An optional second output on the <code>Delay</code> node for rate limited messages.</li></ul>",
"ja": "<p>以下の様な小さな変更が沢山あります:</p><ul><li><code>msg</code> TypedInputの自動補完提案</li><li><code>Join</code>ノードで<code>msg.resetTimeout</code>のサポート</li><li><code>Delay</code>ノードの流量制御モードにおいて先頭メッセージをキューに追加</li><li><code>Delay</code>ードで流量制限されたメッセージ向けの任意の2つ目の出力</li></ul>"
}
}
]

View File

@@ -91,21 +91,21 @@
<div id="func-tab-init" style="display:none">
<div class="form-row node-text-editor-row" style="position:relative">
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-init-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-init-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 5;"><button id="node-init-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div>
</div>
<div id="func-tab-body" style="display:none">
<div class="form-row node-text-editor-row" style="position:relative">
<div style="height: 220px; min-height:150px;" class="node-text-editor" id="node-input-func-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 5;"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div>
</div>
<div id="func-tab-finalize" style="display:none">
<div class="form-row node-text-editor-row" style="position:relative">
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-finalize-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-finalize-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 5;"><button id="node-finalize-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div>
</div>

View File

@@ -111,24 +111,22 @@ module.exports = function(RED) {
if (node.isServer) {
node._clients[id] = socket;
node.emit('opened',{count:Object.keys(node._clients).length,id:id});
} else {
if (node.heartbeat) {
node.heartbeatInterval = setInterval(function() {
if (socket.nrPendingHeartbeat) {
// No pong received
socket.terminate();
socket.nrErrorHandler(new Error("timeout"));
return;
}
socket.nrPendingHeartbeat = true;
socket.ping();
},node.heartbeat);
}
}
socket.on('open',function() {
if (!node.isServer) {
if (node.heartbeat) {
clearInterval(node.heartbeatInterval);
node.heartbeatInterval = setInterval(function() {
if (socket.nrPendingHeartbeat) {
// No pong received
socket.terminate();
socket.nrErrorHandler(new Error("timeout"));
return;
}
socket.nrPendingHeartbeat = true;
try {
socket.ping();
} catch(err) {}
},node.heartbeat);
}
node.emit('opened',{count:'',id:id});
}
});

View File

@@ -49,11 +49,7 @@ module.exports = function(RED) {
}
var value = RED.util.getMessageProperty(msg,node.property);
if (value !== undefined) {
if (typeof value === "string" || Buffer.isBuffer(value)) {
// if (Buffer.isBuffer(value) && node.action !== "obj") {
// node.warn(RED._("json.errors.dropped")); done();
// }
// else
if (typeof value === "string") {
if (node.action === "" || node.action === "obj") {
try {
RED.util.setMessageProperty(msg,node.property,JSON.parse(value));

View File

@@ -15,10 +15,10 @@
}
],
"dependencies": {
"acorn": "8.7.0",
"acorn": "8.6.0",
"acorn-walk": "8.2.0",
"ajv": "8.8.2",
"body-parser": "1.19.1",
"body-parser": "1.19.0",
"cheerio": "1.0.0-rc.10",
"content-type": "1.0.4",
"cookie-parser": "1.4.6",
@@ -36,7 +36,7 @@
"is-utf8": "0.2.1",
"js-yaml": "3.14.1",
"media-typer": "1.1.0",
"mqtt": "4.3.4",
"mqtt": "4.2.8",
"multer": "1.4.3",
"mustache": "4.2.0",
"on-headers": "1.0.2",

View File

@@ -21,6 +21,6 @@
"fs-extra": "10.0.0",
"semver": "7.3.5",
"tar": "6.1.11",
"uglify-js": "3.14.5"
"uglify-js": "3.14.4"
}
}

View File

@@ -20,7 +20,7 @@
"@node-red/util": "2.2.0-beta.1",
"async-mutex": "0.3.2",
"clone": "2.1.2",
"express": "4.17.2",
"express": "4.17.1",
"fs-extra": "10.0.0",
"json-stringify-safe": "5.0.1"
}

View File

@@ -32,14 +32,8 @@ function wrapEventFunction(obj,func) {
return function(eventName, listener) {
if (deprecatedEvents.hasOwnProperty(eventName)) {
const log = require("@node-red/util").log;
const stack = (new Error().stack).split("\n");
let location = "(unknown)"
// See https://github.com/node-red/node-red/issues/3292
if (stack.length > 2) {
location = stack[2].split("(")[1].slice(0,-1);
}
log.warn(`[RED.events] Deprecated use of "${eventName}" event from "${location}". Use "${deprecatedEvents[eventName]}" instead.`)
const stack = (new Error().stack).split("\n")[2].split("(")[1].slice(0,-1);
log.warn(`[RED.events] Deprecated use of "${eventName}" event from "${stack}". Use "${deprecatedEvents[eventName]}" instead.`)
}
return events["_"+func].call(events,eventName,listener)
}

View File

@@ -686,7 +686,7 @@ function prepareJSONataExpression(value,node) {
return moment(arg1, arg2, arg3, arg4);
});
expr.registerFunction('clone', cloneMessage, '<(oa)-:o>');
expr._legacyMode = /(^|[^a-zA-Z0-9_'".])msg([^a-zA-Z0-9_'"]|$)/.test(value);
expr._legacyMode = /(^|[^a-zA-Z0-9_'"])msg([^a-zA-Z0-9_'"]|$)/.test(value);
expr._node = node;
return expr;
}

View File

@@ -16,7 +16,7 @@
],
"dependencies": {
"fs-extra": "10.0.0",
"i18next": "21.6.6",
"i18next": "21.5.4",
"json-stringify-safe": "5.0.1",
"jsonata": "1.8.5",
"lodash.clonedeep": "^4.5.0",

View File

@@ -37,7 +37,7 @@
"@node-red/nodes": "2.2.0-beta.1",
"basic-auth": "2.0.1",
"bcryptjs": "2.4.3",
"express": "4.17.2",
"express": "4.17.1",
"fs-extra": "10.0.0",
"node-red-admin": "^2.2.1",
"nopt": "5.0.0",

View File

@@ -50,24 +50,6 @@ describe('JSON node', function() {
});
});
it('should convert a buffer of a valid json string to a javascript object', function(done) {
var flow = [{id:"jn1",type:"json",action:"obj",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
msg.should.have.property('topic', 'bar');
msg.payload.should.have.property('employees');
msg.payload.employees[0].should.have.property('firstName', 'John');
msg.payload.employees[0].should.have.property('lastName', 'Smith');
done();
});
var jsonString = Buffer.from('{"employees":[{"firstName":"John", "lastName":"Smith"}]}');
jn1.receive({payload:jsonString,topic: "bar"});
});
});
it('should convert a javascript object to a json string', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
@@ -184,55 +166,29 @@ describe('JSON node', function() {
});
});
it('should log an error if asked to parse an invalid json string in a buffer', function(done) {
var flow = [{id:"jn1",type:"json",action:"obj",wires:[["jn2"]]},
it('should log an error if asked to parse something thats not json or js', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
try {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn1.receive({payload:Buffer.from('{"name":foo}'),topic: "bar"});
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.should.startWith("Unexpected token o");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) { done(err) }
},20);
} catch(err) {
done(err);
}
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.toString().should.eql('json.errors.dropped-object');
done();
} catch(err) {
done(err);
}
},50);
jn1.receive({payload:Buffer.from("a")});
});
});
// it('should log an error if asked to parse something thats not json or js and not in force object mode', function(done) {
// var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
// {id:"jn2", type:"helper"}];
// helper.load(jsonNode, flow, function() {
// var jn1 = helper.getNode("jn1");
// var jn2 = helper.getNode("jn2");
// setTimeout(function() {
// try {
// var logEvents = helper.log().args.filter(function(evt) {
// return evt[0].type == "json";
// });
// logEvents.should.have.length(1);
// logEvents[0][0].should.have.a.property('msg');
// logEvents[0][0].msg.toString().should.eql('json.errors.dropped');
// done();
// } catch(err) {
// done(err);
// }
// },50);
// jn1.receive({payload:Buffer.from("abcd")});
// });
// });
it('should pass straight through if no payload set', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];