Merge branch 'dev' into pr_2042

This commit is contained in:
Nick O'Leary 2019-02-04 14:39:00 +00:00
commit 5110eaff96
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
29 changed files with 784 additions and 129 deletions

View File

@ -2,7 +2,31 @@
Runtime Runtime
- Bump JSONata to 1.6.5 - Bump JSONata to 1.6.4
- Add Flow.getSetting for resolving env-var properties
- Refactor Subflow logic into own class
- Restore RED.auth to node-red module api
- Tidy up when usage in Flow and Node
Editor
- German translation
- Change default dropdown appearance and sidebar tab menu handling
- Handle multiple-select box when nothing selected Fixes #2021
- Handle i18n properly when key is a valid sub-identifier Fixes #2028
- Avoid duplicate links when missing node type installed Fixes #2032
- Add View Tools
- Don't collapse version control header when clicking refresh
- Add fast entry via keyboard for string of nodes
- Check for undeployed change before showing open project dialog
- Add placeholder node when in quick-add mode
- Move nodes to top-left corner when converting to subflow
Nodes
- Debug: Allow debug edit expression to be sent to status
- WebSocket: Fix missing translated help
#### 0.20.0-beta.3: Beta Release #### 0.20.0-beta.3: Beta Release

View File

@ -15,6 +15,7 @@
**/ **/
var path = require("path"); var path = require("path");
var fs = require("fs-extra");
module.exports = function(grunt) { module.exports = function(grunt) {
@ -442,7 +443,9 @@ module.exports = function(grunt) {
'packages/node_modules/@node-red/runtime/lib/api/*.js', 'packages/node_modules/@node-red/runtime/lib/api/*.js',
'packages/node_modules/@node-red/runtime/lib/events.js', 'packages/node_modules/@node-red/runtime/lib/events.js',
'packages/node_modules/@node-red/util/**/*.js', 'packages/node_modules/@node-red/util/**/*.js',
], 'packages/node_modules/@node-red/editor-api/lib/index.js',
'packages/node_modules/@node-red/editor-api/lib/auth/index.js'
],
options: { options: {
destination: 'docs', destination: 'docs',
configure: './jsdoc.json' configure: './jsdoc.json'
@ -553,6 +556,13 @@ module.exports = function(grunt) {
}); });
}); });
grunt.registerTask('verifyUiTestDependencies', function() {
if (!fs.existsSync(path.join("node_modules", "chromedriver"))) {
grunt.fail.fatal('You need to run "npm install chromedriver@2" before running UI test.');
return false;
}
});
grunt.registerTask('setDevEnv', grunt.registerTask('setDevEnv',
'Sets NODE_ENV=development so non-minified assets are used', 'Sets NODE_ENV=development so non-minified assets are used',
function () { function () {
@ -573,7 +583,7 @@ module.exports = function(grunt) {
grunt.registerTask('test-ui', grunt.registerTask('test-ui',
'Builds editor content then runs unit tests on editor ui', 'Builds editor content then runs unit tests on editor ui',
['build','jshint:editor','webdriver:all']); ['verifyUiTestDependencies','build','jshint:editor','webdriver:all']);
grunt.registerTask('test-nodes', grunt.registerTask('test-nodes',
'Runs unit tests on core nodes', 'Runs unit tests on core nodes',

View File

@ -15,7 +15,6 @@
}, },
"templates": { "templates": {
"systemName": "Node-RED Runtime API", "systemName": "Node-RED Runtime API",
"theme":"yeti",
"footer": "", "footer": "",
"copyright": "Released under the Apache License v2.0", "copyright": "Released under the Apache License v2.0",
"default": { "default": {

View File

@ -1,6 +1,6 @@
{ {
"name": "node-red", "name": "node-red",
"version": "0.20.0-beta.3", "version": "0.20.0-beta.4",
"description": "A visual tool for wiring the Internet of Things", "description": "A visual tool for wiring the Internet of Things",
"homepage": "http://nodered.org", "homepage": "http://nodered.org",
"license": "Apache-2.0", "license": "Apache-2.0",
@ -24,7 +24,7 @@
} }
], ],
"dependencies": { "dependencies": {
"ajv": "6.6.2", "ajv": "6.7.0",
"basic-auth": "2.0.1", "basic-auth": "2.0.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"body-parser": "1.18.3", "body-parser": "1.18.3",
@ -56,7 +56,7 @@
"node-red-node-feedparser": "^0.1.14", "node-red-node-feedparser": "^0.1.14",
"node-red-node-rbe": "0.2.*", "node-red-node-rbe": "0.2.*",
"node-red-node-sentiment": "^0.1.0", "node-red-node-sentiment": "^0.1.0",
"node-red-node-tail": "^0.0.1", "node-red-node-tail": "^0.0.2",
"node-red-node-twitter": "^1.1.0", "node-red-node-twitter": "^1.1.0",
"nopt": "4.0.1", "nopt": "4.0.1",
"oauth2orize": "1.11.0", "oauth2orize": "1.11.0",
@ -67,17 +67,15 @@
"raw-body": "2.3.3", "raw-body": "2.3.3",
"request": "2.88.0", "request": "2.88.0",
"semver": "5.6.0", "semver": "5.6.0",
"sentiment": "2.1.0",
"uglify-js": "3.4.9", "uglify-js": "3.4.9",
"when": "3.7.8", "when": "3.7.8",
"ws": "6.1.2", "ws": "6.1.3",
"xml2js": "0.4.19" "xml2js": "0.4.19"
}, },
"optionalDependencies": { "optionalDependencies": {
"bcrypt": "~2.0.0" "bcrypt": "~2.0.0"
}, },
"devDependencies": { "devDependencies": {
"chromedriver": "2.45.0",
"grunt": "~1.0.3", "grunt": "~1.0.3",
"grunt-chmod": "~1.1.1", "grunt-chmod": "~1.1.1",
"grunt-cli": "~1.3.2", "grunt-cli": "~1.3.2",
@ -107,7 +105,7 @@
"should": "^8.4.0", "should": "^8.4.0",
"sinon": "1.17.7", "sinon": "1.17.7",
"stoppable": "^1.1.0", "stoppable": "^1.1.0",
"supertest": "3.3.0", "supertest": "3.4.2",
"wdio-chromedriver-service": "^0.1.5", "wdio-chromedriver-service": "^0.1.5",
"wdio-mocha-framework": "^0.6.4", "wdio-mocha-framework": "^0.6.4",
"wdio-spec-reporter": "^0.1.5", "wdio-spec-reporter": "^0.1.5",

View File

@ -14,6 +14,11 @@
* limitations under the License. * limitations under the License.
**/ **/
/**
* @mixin @node-red/editor-api_auth
*/
var passport = require("passport"); var passport = require("passport");
var oauth2orize = require("oauth2orize"); var oauth2orize = require("oauth2orize");
@ -44,7 +49,14 @@ function init(_settings,storage) {
Tokens.init(mergedAdminAuth,storage); Tokens.init(mergedAdminAuth,storage);
} }
} }
/**
* Returns an Express middleware function that ensures the user making a request
* has the necessary permission.
*
* @param {String} permission - the permission required for the request, such as `flows.write`
* @return {Function} - an Express middleware
* @memberof @node-red/editor-api_auth
*/
function needsPermission(permission) { function needsPermission(permission) {
return function(req,res,next) { return function(req,res,next) {
if (settings && settings.adminAuth) { if (settings && settings.adminAuth) {

View File

@ -14,6 +14,16 @@
* limitations under the License. * limitations under the License.
**/ **/
/**
* This module provides an Express application to serve the Node-RED editor.
*
* It implements the Node-RED HTTP Admin API the Editor uses to interact
* with the Node-RED runtime.
*
* @namespace @node-red/editor-api
*/
var express = require("express"); var express = require("express");
var bodyParser = require("body-parser"); var bodyParser = require("body-parser");
var util = require('util'); var util = require('util');
@ -28,6 +38,15 @@ var adminApp;
var server; var server;
var editor; var editor;
/**
* Initialise the module.
* @param {Object} settings The runtime settings
* @param {HTTPServer} server An instance of HTTP Server
* @param {Storage} storage An instance of Node-RED Storage
* @param {Runtime} runtimeAPI An instance of Node-RED Runtime
* @memberof @node-red/editor-api
*/
function init(settings,_server,storage,runtimeAPI) { function init(settings,_server,storage,runtimeAPI) {
server = _server; server = _server;
if (settings.httpAdminRoot !== false) { if (settings.httpAdminRoot !== false) {
@ -80,6 +99,12 @@ function init(settings,_server,storage,runtimeAPI) {
adminApp = null; adminApp = null;
} }
} }
/**
* Start the module.
* @return {Promise} resolves when the application is ready to handle requests
* @memberof @node-red/editor-api
*/
function start() { function start() {
if (editor) { if (editor) {
return editor.start(); return editor.start();
@ -87,6 +112,12 @@ function start() {
return when.resolve(); return when.resolve();
} }
} }
/**
* Stop the module.
* @return {Promise} resolves when the application is stopped
* @memberof @node-red/editor-api
*/
function stop() { function stop() {
if (editor) { if (editor) {
editor.stop(); editor.stop();
@ -97,8 +128,18 @@ module.exports = {
init: init, init: init,
start: start, start: start,
stop: stop, stop: stop,
/**
* @memberof @node-red/editor-api
* @mixes @node-red/editor-api_auth
*/
auth: { auth: {
needsPermission: auth.needsPermission needsPermission: auth.needsPermission
}, },
/**
* The Express app used to serve the Node-RED Editor
* @type ExpressApplication
* @memberof @node-red/editor-api
*/
get httpAdmin() { return adminApp; } get httpAdmin() { return adminApp; }
}; };

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/editor-api", "name": "@node-red/editor-api",
"version": "0.20.0-beta.3", "version": "0.20.0-beta.4",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,8 +16,8 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/util": "0.20.0-beta.3", "@node-red/util": "0.20.0-beta.4",
"@node-red/editor-client": "0.20.0-beta.3", "@node-red/editor-client": "0.20.0-beta.4",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"body-parser": "1.18.3", "body-parser": "1.18.3",
"clone": "2.1.2", "clone": "2.1.2",
@ -32,6 +32,6 @@
"passport-oauth2-client-password": "0.1.2", "passport-oauth2-client-password": "0.1.2",
"passport": "0.4.0", "passport": "0.4.0",
"when": "3.7.8", "when": "3.7.8",
"ws": "6.1.2" "ws": "6.1.3"
} }
} }

View File

@ -273,6 +273,7 @@
"editSubflowProperties": "edit properties", "editSubflowProperties": "edit properties",
"input": "inputs:", "input": "inputs:",
"output": "outputs:", "output": "outputs:",
"status": "status node",
"deleteSubflow": "delete subflow", "deleteSubflow": "delete subflow",
"info": "Description", "info": "Description",
"category": "Category", "category": "Category",

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/editor-client", "name": "@node-red/editor-client",
"version": "0.20.0-beta.3", "version": "0.20.0-beta.4",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -125,14 +125,20 @@ RED.history = (function() {
}); });
} }
} }
if (ev.subflow && ev.subflow.hasOwnProperty('instances')) { if (ev.subflow) {
ev.subflow.instances.forEach(function(n) { if (ev.subflow.hasOwnProperty('instances')) {
var node = RED.nodes.node(n.id); ev.subflow.instances.forEach(function(n) {
if (node) { var node = RED.nodes.node(n.id);
node.changed = n.changed; if (node) {
node.dirty = true; node.changed = n.changed;
} node.dirty = true;
}); }
});
}
if (ev.subflow.hasOwnProperty('status')) {
subflow = RED.nodes.subflow(ev.subflow.id);
subflow.status = ev.subflow.status;
}
} }
if (subflow) { if (subflow) {
RED.nodes.filterNodes({type:"subflow:"+subflow.id}).forEach(function(n) { RED.nodes.filterNodes({type:"subflow:"+subflow.id}).forEach(function(n) {
@ -232,6 +238,11 @@ RED.history = (function() {
} }
}); });
} }
if (ev.subflow.hasOwnProperty('status')) {
if (ev.subflow.status) {
delete ev.node.status;
}
}
RED.editor.validateNode(ev.node); RED.editor.validateNode(ev.node);
RED.nodes.filterNodes({type:"subflow:"+ev.node.id}).forEach(function(n) { RED.nodes.filterNodes({type:"subflow:"+ev.node.id}).forEach(function(n) {
n.inputs = ev.node.in.length; n.inputs = ev.node.in.length;
@ -262,6 +273,8 @@ RED.history = (function() {
} else if (ev.t == "createSubflow") { } else if (ev.t == "createSubflow") {
if (ev.nodes) { if (ev.nodes) {
RED.nodes.filterNodes({z:ev.subflow.subflow.id}).forEach(function(n) { RED.nodes.filterNodes({z:ev.subflow.subflow.id}).forEach(function(n) {
n.x += ev.subflow.offsetX;
n.y += ev.subflow.offsetY;
n.z = ev.activeWorkspace; n.z = ev.activeWorkspace;
n.dirty = true; n.dirty = true;
}); });
@ -288,6 +301,7 @@ RED.history = (function() {
RED.workspaces.order(ev.order); RED.workspaces.order(ev.order);
} }
} }
Object.keys(modifiedTabs).forEach(function(id) { Object.keys(modifiedTabs).forEach(function(id) {
var subflow = RED.nodes.subflow(id); var subflow = RED.nodes.subflow(id);
if (subflow) { if (subflow) {
@ -301,6 +315,7 @@ RED.history = (function() {
RED.palette.refresh(); RED.palette.refresh();
RED.workspaces.refresh(); RED.workspaces.refresh();
RED.sidebar.config.refresh(); RED.sidebar.config.refresh();
RED.subflow.refresh();
} }
} }

View File

@ -575,6 +575,18 @@ RED.nodes = (function() {
node.icon = n.icon; node.icon = n.icon;
} }
} }
if (n.status) {
node.status = {x: n.status.x, y: n.status.y, wires:[]};
links.forEach(function(d) {
if (d.target === n.status) {
if (d.source.type != "subflow") {
node.status.wires.push({id:d.source.id, port:d.sourcePort})
} else {
node.status.wires.push({id:n.id, port:0})
}
}
});
}
return node; return node;
} }
@ -855,6 +867,12 @@ RED.nodes = (function() {
output.i = i; output.i = i;
output.id = getID(); output.id = getID();
}); });
if (n.status) {
n.status.type = "subflow";
n.status.direction = "status";
n.status.z = n.id;
n.status.id = getID();
}
new_subflows.push(n); new_subflows.push(n);
addSubflow(n,createNewIds); addSubflow(n,createNewIds);
} }
@ -1194,6 +1212,19 @@ RED.nodes = (function() {
}); });
delete output.wires; delete output.wires;
}); });
if (n.status) {
n.status.wires.forEach(function(wire) {
var link;
if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) {
link = {source:n.in[wire.port], sourcePort:wire.port,target:n.status};
} else {
link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:n.status};
}
addLink(link);
new_links.push(link);
});
delete n.status.wires;
}
} }
RED.workspaces.refresh(); RED.workspaces.refresh();

View File

@ -1158,19 +1158,19 @@ RED.diff = (function() {
} }
}); });
return { var diff = {
currentConfig: currentConfig, currentConfig: currentConfig,
newConfig: newConfig, newConfig: newConfig,
added: added, added: added,
deleted: deleted, deleted: deleted,
changed: changed, changed: changed,
moved: moved moved: moved
} };
return diff;
} }
function resolveDiffs(localDiff,remoteDiff) { function resolveDiffs(localDiff,remoteDiff) {
var conflicted = {}; var conflicted = {};
var resolutions = {}; var resolutions = {};
var diff = { var diff = {
localDiff: localDiff, localDiff: localDiff,
remoteDiff: remoteDiff, remoteDiff: remoteDiff,
@ -1348,7 +1348,7 @@ RED.diff = (function() {
if (node) { if (node) {
nodeChangedStates[id] = node.changed; nodeChangedStates[id] = node.changed;
} }
localChangedStates[id] = true; localChangedStates[id] = 1;
newConfig.push(remoteDiff.newConfig.all[id]); newConfig.push(remoteDiff.newConfig.all[id]);
} }
} else { } else {
@ -1363,7 +1363,7 @@ RED.diff = (function() {
nodeChangedStates[id] = node.changed; nodeChangedStates[id] = node.changed;
} }
if (!localDiff.added.hasOwnProperty(id)) { if (!localDiff.added.hasOwnProperty(id)) {
localChangedStates[id] = true; localChangedStates[id] = 2;
newConfig.push(remoteDiff.newConfig.all[id]); newConfig.push(remoteDiff.newConfig.all[id]);
} }
} }
@ -1376,24 +1376,42 @@ RED.diff = (function() {
} }
function mergeDiff(diff) { function mergeDiff(diff) {
//console.log(diff);
var appliedDiff = applyDiff(diff); var appliedDiff = applyDiff(diff);
var newConfig = appliedDiff.config; var newConfig = appliedDiff.config;
var nodeChangedStates = appliedDiff.nodeChangedStates; var nodeChangedStates = appliedDiff.nodeChangedStates;
var localChangedStates = appliedDiff.localChangedStates; var localChangedStates = appliedDiff.localChangedStates;
var isDirty = RED.nodes.dirty();
var historyEvent = { var historyEvent = {
t:"replace", t:"replace",
config: RED.nodes.createCompleteNodeSet(), config: RED.nodes.createCompleteNodeSet(),
changed: nodeChangedStates, changed: nodeChangedStates,
dirty: RED.nodes.dirty(), dirty: isDirty,
rev: RED.nodes.version() rev: RED.nodes.version()
} }
RED.history.push(historyEvent); RED.history.push(historyEvent);
var originalFlow = RED.nodes.originalFlow();
// originalFlow is what the editor things it loaded
// - add any newly added nodes from remote diff as they are now part of the record
for (var id in diff.remoteDiff.added) {
if (diff.remoteDiff.added.hasOwnProperty(id)) {
if (diff.remoteDiff.newConfig.all.hasOwnProperty(id)) {
originalFlow.push(JSON.parse(JSON.stringify(diff.remoteDiff.newConfig.all[id])));
}
}
}
RED.nodes.clear(); RED.nodes.clear();
var imported = RED.nodes.import(newConfig); var imported = RED.nodes.import(newConfig);
// Restore the original flow so subsequent merge resolutions can properly
// identify new-vs-old
RED.nodes.originalFlow(originalFlow);
imported[0].forEach(function(n) { imported[0].forEach(function(n) {
if (nodeChangedStates[n.id] || localChangedStates[n.id]) { if (nodeChangedStates[n.id] || localChangedStates[n.id]) {
n.changed = true; n.changed = true;
@ -1402,11 +1420,16 @@ RED.diff = (function() {
RED.nodes.version(diff.remoteDiff.rev); RED.nodes.version(diff.remoteDiff.rev);
if (isDirty) {
RED.nodes.dirty(true);
}
RED.view.redraw(true); RED.view.redraw(true);
RED.palette.refresh(); RED.palette.refresh();
RED.workspaces.refresh(); RED.workspaces.refresh();
RED.sidebar.config.refresh(); RED.sidebar.config.refresh();
} }
function showTestFlowDiff(index) { function showTestFlowDiff(index) {
if (index === 1) { if (index === 1) {
var localFlow = RED.nodes.createCompleteNodeSet(); var localFlow = RED.nodes.createCompleteNodeSet();

View File

@ -16,12 +16,12 @@
RED.subflow = (function() { RED.subflow = (function() {
var _subflowEditTemplate = '<script type="text/x-red" data-template-name="subflow">'+ var _subflowEditTemplate = '<script type="text/x-red" data-template-name="subflow">'+
'<div class="form-row"><label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label><input type="text" id="node-input-name"></div>'+ '<div class="form-row"><label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label><input type="text" id="node-input-name"></div>'+
'<div class="form-row" style="margin-bottom: 0px;"><label style="width: auto;" data-i18n="[append]editor:editor-tab.env"><i class="fa fa-th-list"></i> </label></div>'+ '<div class="form-row" style="margin-bottom: 0px;"><label style="width: auto;" data-i18n="[append]editor:editor-tab.env"><i class="fa fa-th-list"></i> </label></div>'+
'<div class="form-row node-input-env-container-row"><ol id="node-input-env-container"></ol></div>'+ '<div class="form-row node-input-env-container-row"><ol id="node-input-env-container"></ol></div>'+
'</script>'; '</script>';
var _subflowTemplateEditTemplate = '<script type="text/x-red" data-template-name="subflow-template">'+ var _subflowTemplateEditTemplate = '<script type="text/x-red" data-template-name="subflow-template">'+
'<div class="form-row"><i class="fa fa-tag"></i> <label for="subflow-input-name" data-i18n="common.label.name"></label><input type="text" id="subflow-input-name"></div>'+ '<div class="form-row"><i class="fa fa-tag"></i> <label for="subflow-input-name" data-i18n="common.label.name"></label><input type="text" id="subflow-input-name"></div>'+
'<div class="form-row"><i class="fa fa-folder-o"></i> <label for="subflow-input-category" data-i18n="editor:subflow.category"></label><select style="width: 250px;" id="subflow-input-category"></select><input style="display:none; margin-left: 10px; width:calc(100% - 250px)" type="text" id="subflow-input-custom-category"></div>'+ '<div class="form-row"><i class="fa fa-folder-o"></i> <label for="subflow-input-category" data-i18n="editor:subflow.category"></label><select style="width: 250px;" id="subflow-input-category"></select><input style="display:none; margin-left: 10px; width:calc(100% - 250px)" type="text" id="subflow-input-custom-category"></div>'+
@ -30,26 +30,22 @@ RED.subflow = (function() {
'<div class="form-row form-tips" id="subflow-dialog-user-count"></div>'+ '<div class="form-row form-tips" id="subflow-dialog-user-count"></div>'+
'</script>'; '</script>';
function getSubflow() {
return RED.nodes.subflow(RED.workspaces.active());
}
function findAvailableSubflowIOPosition(subflow,isInput) { function findAvailableSubflowIOPosition(subflow,isInput) {
var pos = {x:50,y:30}; var pos = {x:50,y:30};
if (!isInput) { if (!isInput) {
pos.x += 110; pos.x += 110;
} }
for (var i=0;i<subflow.out.length+subflow.in.length;i++) { var ports = [].concat(subflow.out).concat(subflow.in);
var port; if (subflow.status) {
if (i < subflow.out.length) { ports.push(subflow.status);
port = subflow.out[i]; }
} else { ports.sort(function(A,B) {
port = subflow.in[i-subflow.out.length]; return A.x-B.x;
} });
for (var i=0; i<ports.length; i++) {
var port = ports[i];
if (port.x == pos.x && port.y == pos.y) { if (port.x == pos.x && port.y == pos.y) {
pos.x += 55; pos.x += 55;
i=0;
} }
} }
return pos; return pos;
@ -197,6 +193,61 @@ RED.subflow = (function() {
return {subflowOutputs: removedSubflowOutputs, links: removedLinks} return {subflowOutputs: removedSubflowOutputs, links: removedLinks}
} }
function addSubflowStatus() {
var subflow = RED.nodes.subflow(RED.workspaces.active());
if (subflow.status) {
return;
}
var position = findAvailableSubflowIOPosition(subflow,false);
var statusNode = {
type:"subflow",
direction:"status",
z:subflow.id,
x:position.x,
y:position.y,
id:RED.nodes.id()
};
subflow.status = statusNode;
subflow.dirty = true;
var wasDirty = RED.nodes.dirty();
var wasChanged = subflow.changed;
subflow.changed = true;
var result = refresh(true);
var historyEvent = {
t:'edit',
node:subflow,
dirty:wasDirty,
changed:wasChanged,
subflow: { status: true }
};
RED.history.push(historyEvent);
RED.view.select();
RED.nodes.dirty(true);
RED.view.redraw();
$("#workspace-subflow-status").prop("checked",!!subflow.status);
$("#workspace-subflow-status").parent().parent().toggleClass("active",!!subflow.status);
}
function removeSubflowStatus() {
var subflow = RED.nodes.subflow(RED.workspaces.active());
if (!subflow.status) {
return;
}
var subflowRemovedLinks = [];
RED.nodes.eachLink(function(l) {
if (l.target.type == "subflow" && l.target.z == subflow.id && l.target.direction == "status") {
subflowRemovedLinks.push(l);
}
});
subflowRemovedLinks.forEach(function(l) { RED.nodes.removeLink(l)});
delete subflow.status;
$("#workspace-subflow-status").prop("checked",!!subflow.status);
$("#workspace-subflow-status").parent().parent().toggleClass("active",!!subflow.status);
return { links: subflowRemovedLinks }
}
function refresh(markChange) { function refresh(markChange) {
var activeSubflow = RED.nodes.subflow(RED.workspaces.active()); var activeSubflow = RED.nodes.subflow(RED.workspaces.active());
refreshToolbar(activeSubflow); refreshToolbar(activeSubflow);
@ -225,12 +276,17 @@ RED.subflow = (function() {
} }
} }
} }
function refreshToolbar(activeSubflow) { function refreshToolbar(activeSubflow) {
if (activeSubflow) { if (activeSubflow) {
$("#workspace-subflow-input-add").toggleClass("active", activeSubflow.in.length !== 0); $("#workspace-subflow-input-add").toggleClass("active", activeSubflow.in.length !== 0);
$("#workspace-subflow-input-remove").toggleClass("active",activeSubflow.in.length === 0); $("#workspace-subflow-input-remove").toggleClass("active",activeSubflow.in.length === 0);
$("#workspace-subflow-output .spinner-value").text(activeSubflow.out.length); $("#workspace-subflow-output .spinner-value").text(activeSubflow.out.length);
$("#workspace-subflow-status").prop("checked",!!activeSubflow.status);
$("#workspace-subflow-status").parent().parent().toggleClass("active",!!activeSubflow.status);
} }
} }
@ -238,22 +294,32 @@ RED.subflow = (function() {
var toolbar = $("#workspace-toolbar"); var toolbar = $("#workspace-toolbar");
toolbar.empty(); toolbar.empty();
// Edit properties
$('<a class="button" id="workspace-subflow-edit" href="#" data-i18n="[append]subflow.editSubflowProperties"><i class="fa fa-pencil"></i> </a>').appendTo(toolbar); $('<a class="button" id="workspace-subflow-edit" href="#" data-i18n="[append]subflow.editSubflowProperties"><i class="fa fa-pencil"></i> </a>').appendTo(toolbar);
// Inputs
$('<span style="margin-left: 5px;" data-i18n="subflow.input"></span> '+ $('<span style="margin-left: 5px;" data-i18n="subflow.input"></span> '+
'<div style="display: inline-block;" class="button-group">'+ '<div style="display: inline-block;" class="button-group">'+
'<a id="workspace-subflow-input-remove" class="button active" href="#">0</a>'+ '<a id="workspace-subflow-input-remove" class="button active" href="#">0</a>'+
'<a id="workspace-subflow-input-add" class="button" href="#">1</a>'+ '<a id="workspace-subflow-input-add" class="button" href="#">1</a>'+
'</div>').appendTo(toolbar); '</div>').appendTo(toolbar);
// Outputs
$('<span style="margin-left: 5px;" data-i18n="subflow.output"></span> <div id="workspace-subflow-output" style="display: inline-block;" class="button-group spinner-group">'+ $('<span style="margin-left: 5px;" data-i18n="subflow.output"></span> <div id="workspace-subflow-output" style="display: inline-block;" class="button-group spinner-group">'+
'<a id="workspace-subflow-output-remove" class="button" href="#"><i class="fa fa-minus"></i></a>'+ '<a id="workspace-subflow-output-remove" class="button" href="#"><i class="fa fa-minus"></i></a>'+
'<div class="spinner-value">3</div>'+ '<div class="spinner-value">3</div>'+
'<a id="workspace-subflow-output-add" class="button" href="#"><i class="fa fa-plus"></i></a>'+ '<a id="workspace-subflow-output-add" class="button" href="#"><i class="fa fa-plus"></i></a>'+
'</div>').appendTo(toolbar); '</div>').appendTo(toolbar);
// Status
$('<span class="button-group"><span class="button" style="padding:0"><label for="workspace-subflow-status"><input id="workspace-subflow-status" type="checkbox"> <span data-i18n="subflow.status"></span></label></span></span>').appendTo(toolbar);
// $('<a class="button disabled" id="workspace-subflow-add-input" href="#" data-i18n="[append]subflow.input"><i class="fa fa-plus"></i> </a>').appendTo(toolbar); // $('<a class="button disabled" id="workspace-subflow-add-input" href="#" data-i18n="[append]subflow.input"><i class="fa fa-plus"></i> </a>').appendTo(toolbar);
// $('<a class="button" id="workspace-subflow-add-output" href="#" data-i18n="[append]subflow.output"><i class="fa fa-plus"></i> </a>').appendTo(toolbar); // $('<a class="button" id="workspace-subflow-add-output" href="#" data-i18n="[append]subflow.output"><i class="fa fa-plus"></i> </a>').appendTo(toolbar);
// Delete
$('<a class="button" id="workspace-subflow-delete" href="#" data-i18n="[append]subflow.deleteSubflow"><i class="fa fa-trash"></i> </a>').appendTo(toolbar); $('<a class="button" id="workspace-subflow-delete" href="#" data-i18n="[append]subflow.deleteSubflow"><i class="fa fa-trash"></i> </a>').appendTo(toolbar);
toolbar.i18n(); toolbar.i18n();
@ -280,6 +346,7 @@ RED.subflow = (function() {
RED.view.redraw(true); RED.view.redraw(true);
} }
}); });
$("#workspace-subflow-output-add").click(function(event) { $("#workspace-subflow-output-add").click(function(event) {
event.preventDefault(); event.preventDefault();
addSubflowOutput(); addSubflowOutput();
@ -289,6 +356,7 @@ RED.subflow = (function() {
event.preventDefault(); event.preventDefault();
addSubflowInput(); addSubflowInput();
}); });
$("#workspace-subflow-input-remove").click(function(event) { $("#workspace-subflow-input-remove").click(function(event) {
event.preventDefault(); event.preventDefault();
var wasDirty = RED.nodes.dirty(); var wasDirty = RED.nodes.dirty();
@ -313,6 +381,33 @@ RED.subflow = (function() {
} }
}); });
$("#workspace-subflow-status").change(function(evt) {
if (this.checked) {
addSubflowStatus();
} else {
var currentStatus = activeSubflow.status;
var wasChanged = activeSubflow.changed;
var result = removeSubflowStatus();
if (result) {
activeSubflow.changed = true;
var wasDirty = RED.nodes.dirty();
RED.history.push({
t:'delete',
links:result.links,
changed: wasChanged,
dirty:wasDirty,
subflow: {
id: activeSubflow.id,
status: currentStatus
}
});
RED.view.select();
RED.nodes.dirty(true);
RED.view.redraw();
}
}
})
$("#workspace-subflow-edit").click(function(event) { $("#workspace-subflow-edit").click(function(event) {
RED.editor.editSubflow(RED.nodes.subflow(RED.workspaces.active())); RED.editor.editSubflow(RED.nodes.subflow(RED.workspaces.active()));
event.preventDefault(); event.preventDefault();
@ -334,6 +429,7 @@ RED.subflow = (function() {
$("#chart").css({"margin-top": "40px"}); $("#chart").css({"margin-top": "40px"});
$("#workspace-toolbar").show(); $("#workspace-toolbar").show();
} }
function hideWorkspaceToolbar() { function hideWorkspaceToolbar() {
$("#workspace-toolbar").hide().empty(); $("#workspace-toolbar").hide().empty();
$("#chart").css({"margin-top": "0"}); $("#chart").css({"margin-top": "0"});
@ -379,6 +475,7 @@ RED.subflow = (function() {
subflows: [activeSubflow] subflows: [activeSubflow]
} }
} }
function init() { function init() {
RED.events.on("workspace:change",function(event) { RED.events.on("workspace:change",function(event) {
var activeSubflow = RED.nodes.subflow(event.workspace); var activeSubflow = RED.nodes.subflow(event.workspace);
@ -436,6 +533,13 @@ RED.subflow = (function() {
RED.nodes.dirty(true); RED.nodes.dirty(true);
} }
function snapToGrid(x) {
if (RED.settings.get("editor").view['view-snap-grid']) {
x = Math.round(x / RED.view.gridSize()) * RED.view.gridSize();
}
return x;
}
function convertToSubflow() { function convertToSubflow() {
var selection = RED.view.selection(); var selection = RED.view.selection();
if (!selection.nodes) { if (!selection.nodes) {
@ -451,7 +555,6 @@ RED.subflow = (function() {
var candidateOutputs = []; var candidateOutputs = [];
var candidateInputNodes = {}; var candidateInputNodes = {};
var boundingBox = [selection.nodes[0].x, var boundingBox = [selection.nodes[0].x,
selection.nodes[0].y, selection.nodes[0].y,
selection.nodes[0].x, selection.nodes[0].x,
@ -467,8 +570,14 @@ RED.subflow = (function() {
Math.max(boundingBox[3],n.y) Math.max(boundingBox[3],n.y)
] ]
} }
var offsetX = snapToGrid(boundingBox[0] - 200);
var offsetY = snapToGrid(boundingBox[1] - 80);
var center = [(boundingBox[2]+boundingBox[0]) / 2,(boundingBox[3]+boundingBox[1]) / 2];
var center = [
snapToGrid((boundingBox[2]+boundingBox[0]) / 2),
snapToGrid((boundingBox[3]+boundingBox[1]) / 2)
];
RED.nodes.eachLink(function(link) { RED.nodes.eachLink(function(link) {
if (nodes[link.source.id] && nodes[link.target.id]) { if (nodes[link.source.id] && nodes[link.target.id]) {
@ -525,8 +634,8 @@ RED.subflow = (function() {
in: Object.keys(candidateInputNodes).map(function(v,i) { var index = i; return { in: Object.keys(candidateInputNodes).map(function(v,i) { var index = i; return {
type:"subflow", type:"subflow",
direction:"in", direction:"in",
x:candidateInputNodes[v].x-(candidateInputNodes[v].w/2)-80, x:snapToGrid(candidateInputNodes[v].x-(candidateInputNodes[v].w/2)-80 - offsetX),
y:candidateInputNodes[v].y, y:snapToGrid(candidateInputNodes[v].y - offsetY),
z:subflowId, z:subflowId,
i:index, i:index,
id:RED.nodes.id(), id:RED.nodes.id(),
@ -535,8 +644,8 @@ RED.subflow = (function() {
out: candidateOutputs.map(function(v,i) { var index = i; return { out: candidateOutputs.map(function(v,i) { var index = i; return {
type:"subflow", type:"subflow",
direction:"in", direction:"in",
x:v.source.x+(v.source.w/2)+80, x:snapToGrid(v.source.x+(v.source.w/2)+80 - offsetX),
y:v.source.y, y:snapToGrid(v.source.y - offsetY),
z:subflowId, z:subflowId,
i:index, i:index,
id:RED.nodes.id(), id:RED.nodes.id(),
@ -611,6 +720,8 @@ RED.subflow = (function() {
return isLocalLink; return isLocalLink;
}); });
} }
n.x -= offsetX;
n.y -= offsetY;
n.z = subflow.id; n.z = subflow.id;
} }
@ -619,7 +730,9 @@ RED.subflow = (function() {
nodes:[subflowInstance.id], nodes:[subflowInstance.id],
links:new_links, links:new_links,
subflow: { subflow: {
subflow: subflow subflow: subflow,
offsetX: offsetX,
offsetY: offsetY
}, },
activeWorkspace: RED.workspaces.active(), activeWorkspace: RED.workspaces.active(),
@ -633,8 +746,6 @@ RED.subflow = (function() {
RED.view.redraw(true); RED.view.redraw(true);
} }
return { return {
init: init, init: init,
createSubflow: createSubflow, createSubflow: createSubflow,
@ -642,6 +753,7 @@ RED.subflow = (function() {
removeSubflow: removeSubflow, removeSubflow: removeSubflow,
refresh: refresh, refresh: refresh,
removeInput: removeSubflowInput, removeInput: removeSubflowInput,
removeOutput: removeSubflowOutput removeOutput: removeSubflowOutput,
removeStatus: removeSubflowStatus
} }
})(); })();

View File

@ -1261,6 +1261,13 @@ RED.view = (function() {
moving_set.push({n:n}); moving_set.push({n:n});
} }
}); });
if (activeSubflow.status) {
activeSubflow.status.selected = (activeSubflow.status.x > x && activeSubflow.status.x < x2 && activeSubflow.status.y > y && activeSubflow.status.y < y2);
if (activeSubflow.status.selected) {
activeSubflow.status.dirty = true;
moving_set.push({n:activeSubflow.status});
}
}
} }
updateSelection(); updateSelection();
lasso.remove(); lasso.remove();
@ -1367,6 +1374,13 @@ RED.view = (function() {
moving_set.push({n:n}); moving_set.push({n:n});
} }
}); });
if (activeSubflow.status) {
if (!activeSubflow.status.selected) {
activeSubflow.status.selected = true;
activeSubflow.status.dirty = true;
moving_set.push({n:activeSubflow.status});
}
}
} }
selected_link = null; selected_link = null;
@ -1552,6 +1566,7 @@ RED.view = (function() {
var removedLinks = []; var removedLinks = [];
var removedSubflowOutputs = []; var removedSubflowOutputs = [];
var removedSubflowInputs = []; var removedSubflowInputs = [];
var removedSubflowStatus = undefined;
var subflowInstances = []; var subflowInstances = [];
var startDirty = RED.nodes.dirty(); var startDirty = RED.nodes.dirty();
@ -1573,6 +1588,8 @@ RED.view = (function() {
removedSubflowOutputs.push(node); removedSubflowOutputs.push(node);
} else if (node.direction === "in") { } else if (node.direction === "in") {
removedSubflowInputs.push(node); removedSubflowInputs.push(node);
} else if (node.direction === "status") {
removedSubflowStatus = node;
} }
node.dirty = true; node.dirty = true;
} }
@ -1590,12 +1607,19 @@ RED.view = (function() {
removedLinks = removedLinks.concat(result.links); removedLinks = removedLinks.concat(result.links);
} }
} }
if (removedSubflowStatus) {
result = RED.subflow.removeStatus();
if (result) {
removedLinks = removedLinks.concat(result.links);
}
}
var instances = RED.subflow.refresh(true); var instances = RED.subflow.refresh(true);
if (instances) { if (instances) {
subflowInstances = instances.instances; subflowInstances = instances.instances;
} }
moving_set = []; moving_set = [];
if (removedNodes.length > 0 || removedSubflowOutputs.length > 0 || removedSubflowInputs.length > 0) { if (removedNodes.length > 0 || removedSubflowOutputs.length > 0 || removedSubflowInputs.length > 0 || removedSubflowStatus) {
RED.nodes.dirty(true); RED.nodes.dirty(true);
} }
} }
@ -1651,10 +1675,14 @@ RED.view = (function() {
subflowOutputs:removedSubflowOutputs, subflowOutputs:removedSubflowOutputs,
subflowInputs:removedSubflowInputs, subflowInputs:removedSubflowInputs,
subflow: { subflow: {
id: activeSubflow?activeSubflow.id:undefined,
instances: subflowInstances instances: subflowInstances
}, },
dirty:startDirty dirty:startDirty
}; };
if (removedSubflowStatus) {
historyEvent.subflow.status = removedSubflowStatus;
}
} }
RED.history.push(historyEvent); RED.history.push(historyEvent);
@ -2420,6 +2448,49 @@ RED.view = (function() {
inGroup.append("svg:text").attr("class","port_label").attr("x",18).attr("y",20).style("font-size","10px").text("input"); inGroup.append("svg:text").attr("class","port_label").attr("x",18).attr("y",20).style("font-size","10px").text("input");
var subflowStatus = nodeLayer.selectAll(".subflowstatus").data(activeSubflow.status?[activeSubflow.status]:[],function(d,i){ return d.id;});
subflowStatus.exit().remove();
var statusGroup = subflowStatus.enter().insert("svg:g").attr("class","node subflowstatus").attr("transform",function(d) { return "translate("+(d.x-20)+","+(d.y-20)+")"});
statusGroup.each(function(d,i) {
d.w=40;
d.h=40;
});
statusGroup.append("rect").attr("class","subflowport").attr("rx",8).attr("ry",8).attr("width",40).attr("height",40)
// TODO: This is exactly the same set of handlers used for regular nodes - DRY
.on("mouseup",nodeMouseUp)
.on("mousedown",nodeMouseDown)
.on("touchstart",function(d) {
var obj = d3.select(this);
var touch0 = d3.event.touches.item(0);
var pos = [touch0.pageX,touch0.pageY];
startTouchCenter = [touch0.pageX,touch0.pageY];
startTouchDistance = 0;
touchStartTime = setTimeout(function() {
showTouchMenu(obj,pos);
},touchLongPressTimeout);
nodeMouseDown.call(this,d)
})
.on("touchend", function(d) {
clearTimeout(touchStartTime);
touchStartTime = null;
if (RED.touch.radialMenu.active()) {
d3.event.stopPropagation();
return;
}
nodeMouseUp.call(this,d);
});
statusGroup.append("g").attr('transform','translate(-5,15)').append("rect").attr("class","port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
.on("mousedown", function(d,i){portMouseDown(d,PORT_TYPE_INPUT,0);} )
.on("touchstart", function(d,i){portMouseDown(d,PORT_TYPE_INPUT,0);} )
.on("mouseup", function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);})
.on("touchend",function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);} )
.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);});
statusGroup.append("svg:text").attr("class","port_label").attr("x",22).attr("y",20).style("font-size","10px").text("status");
subflowOutputs.each(function(d,i) { subflowOutputs.each(function(d,i) {
if (d.dirty) { if (d.dirty) {
var output = d3.select(this); var output = d3.select(this);
@ -2439,9 +2510,22 @@ RED.view = (function() {
d.dirty = false; d.dirty = false;
} }
}); });
subflowStatus.each(function(d,i) {
if (d.dirty) {
var output = d3.select(this);
output.selectAll(".subflowport").classed("node_selected",function(d) { return d.selected; })
output.selectAll(".port_index").text(function(d){ return d.i+1});
output.attr("transform", function(d) { return "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"; });
dirtyNodes[d.id] = d;
d.dirty = false;
}
});
} else { } else {
nodeLayer.selectAll(".subflowoutput").remove(); nodeLayer.selectAll(".subflowoutput").remove();
nodeLayer.selectAll(".subflowinput").remove(); nodeLayer.selectAll(".subflowinput").remove();
nodeLayer.selectAll(".subflowstatus").remove();
} }
var node = nodeLayer.selectAll(".nodegroup").data(activeNodes,function(d){return d.id}); var node = nodeLayer.selectAll(".nodegroup").data(activeNodes,function(d){return d.id});
@ -2703,6 +2787,8 @@ RED.view = (function() {
d.resize = false; d.resize = false;
} }
var thisNode = d3.select(this); var thisNode = d3.select(this);
thisNode.classed("node_subflow",function(d) { return activeSubflow != null; })
//thisNode.selectAll(".centerDot").attr({"cx":function(d) { return d.w/2;},"cy":function(d){return d.h/2}}); //thisNode.selectAll(".centerDot").attr({"cx":function(d) { return d.w/2;},"cy":function(d){return d.h/2}});
thisNode.attr("transform", function(d) { return "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"; }); thisNode.attr("transform", function(d) { return "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"; });
if (mouse_mode != RED.state.MOVING_ACTIVE) { if (mouse_mode != RED.state.MOVING_ACTIVE) {
@ -3004,6 +3090,9 @@ RED.view = (function() {
links.each(function(d) { links.each(function(d) {
var link = d3.select(this); var link = d3.select(this);
if (d.added || d===selected_link || d.selected || dirtyNodes[d.source.id] || dirtyNodes[d.target.id]) { if (d.added || d===selected_link || d.selected || dirtyNodes[d.source.id] || dirtyNodes[d.target.id]) {
if (/link_line/.test(link.attr('class'))) {
link.classed("link_subflow", function(d) { return !d.link && activeSubflow });
}
link.attr("d",function(d){ link.attr("d",function(d){
var numOutputs = d.source.outputs || 1; var numOutputs = d.source.outputs || 1;
var sourcePort = d.sourcePort || 0; var sourcePort = d.sourcePort || 0;
@ -3017,8 +3106,11 @@ RED.view = (function() {
// " C "+(d.x1+scale*node_width)+" "+(d.y1+scaleY*node_height)+" "+ // " C "+(d.x1+scale*node_width)+" "+(d.y1+scaleY*node_height)+" "+
// (d.x2-scale*node_width)+" "+(d.y2-scaleY*node_height)+" "+ // (d.x2-scale*node_width)+" "+(d.y2-scaleY*node_height)+" "+
// d.x2+" "+d.y2; // d.x2+" "+d.y2;
var path = generateLinkPath(d.x1,d.y1,d.x2,d.y2,1);
return generateLinkPath(d.x1,d.y1,d.x2,d.y2,1); if (/NaN/.test(path)) {
return ""
}
return path;
}); });
} }
}) })

View File

@ -33,6 +33,15 @@
transition: right 0.2s ease; transition: right 0.2s ease;
overflow: hidden; overflow: hidden;
label {
padding: 1px 8px;
margin: 0;
font-size: 12px;
}
input[type="checkbox"] {
margin: 0 3px 0 0 ;
padding: 0;
}
.button { .button {
@include workspace-button; @include workspace-button;
margin-right: 10px; margin-right: 10px;

View File

@ -1,35 +0,0 @@
<!--
Copyright JS Foundation and other contributors, http://js.foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script type="text/x-red" data-help-name="sentiment">
<p> Analysiert die ausgewählte Eigenschaft aus dem <code>msg</code> Objekt und fügt ein <code>sentiment</code> Objekt hinzu. </p>
<h3> Ausgaben </h3>
<dl class="message-properties">
<dt> sentiment <span class="property-type"> Objekt </span> </dt>
<dd> enthält die resultierende Stimmungslage AFINN-111. </dd>
<dt> sentiment.score <span class="property-type"> Zahl </span> </dt>
<dd> die Sentiment-Bewertung. </dd>
</dl>
<h3> Eingaben </h3>
<dl class="message-properties">
<dt> überschreibt <span class="property-type"> Objekt </span> </dt>
<dd> Ein Objekt mit Wort-Überschreibungen kann angegeben werden- <code> { word:score, ... } </code>. </dd>
</dl>
<h3> Details </h3>
<p> Eine Bewertung größer als Null ist positiv und kleiner als null ist negativ. </p>
<p> Die Bewertung liegt in der Regel im Bereich von -5 bis +5, kann jedoch höher und niedriger sein. </p>
<p>See <a href="https://github.com/thisandagain/sentiment/blob/master/README.md" target="_blank">the Sentiment docs here</a>.</p>
</script>

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/nodes", "name": "@node-red/nodes",
"version": "0.20.0-beta.3", "version": "0.20.0-beta.4",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",
@ -15,7 +15,7 @@
} }
], ],
"dependencies": { "dependencies": {
"ajv": "6.6.2", "ajv": "6.7.0",
"body-parser": "1.18.3", "body-parser": "1.18.3",
"cheerio": "0.22.0", "cheerio": "0.22.0",
"cookie-parser": "1.4.3", "cookie-parser": "1.4.3",
@ -36,8 +36,7 @@
"on-headers": "1.0.1", "on-headers": "1.0.1",
"raw-body": "2.3.3", "raw-body": "2.3.3",
"request": "2.88.0", "request": "2.88.0",
"sentiment": "2.1.0", "ws": "6.1.3",
"ws": "6.1.2",
"xml2js": "0.4.19" "xml2js": "0.4.19"
} }
} }

View File

@ -14,6 +14,16 @@
* limitations under the License. * limitations under the License.
**/ **/
/**
* This module provides the node registry for the Node-RED runtime.
*
* It is responsible for loading node modules and making them available
* to the runtime.
*
* @namespace @node-red/registry
*/
var registry = require("./registry"); var registry = require("./registry");
var loader = require("./loader"); var loader = require("./loader");
var installer = require("./installer"); var installer = require("./installer");

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/registry", "name": "@node-red/registry",
"version": "0.20.0-beta.3", "version": "0.20.0-beta.4",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,7 +16,7 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/util": "0.20.0-beta.3", "@node-red/util": "0.20.0-beta.4",
"semver": "5.6.0", "semver": "5.6.0",
"uglify-js": "3.4.9", "uglify-js": "3.4.9",
"when": "3.7.8" "when": "3.7.8"

View File

@ -38,7 +38,11 @@ function Node(n) {
// Make this a non-enumerable property as it may cause // Make this a non-enumerable property as it may cause
// circular references. Any existing code that tries to JSON serialise // circular references. Any existing code that tries to JSON serialise
// the object (such as dashboard) will not like circular refs // the object (such as dashboard) will not like circular refs
Object.defineProperty(this,'_flow', {value: n._flow, }) // The value must still be writable in the case that a node does:
// Object.assign(this,config)
// as part of its constructure - config._flow will overwrite this._flow
// which we can tolerate as they are the same object.
Object.defineProperty(this,'_flow', {value: n._flow, enumerable: false, writable: true })
} }
this.updateWires(n.wires); this.updateWires(n.wires);
} }

View File

@ -265,7 +265,6 @@ class Flow {
return Promise.all(promises); return Promise.all(promises);
} }
/** /**
* Update the flow definition. This doesn't change anything that is running. * Update the flow definition. This doesn't change anything that is running.
* This should be called after `stop` and before `start`. * This should be called after `stop` and before `start`.
@ -281,11 +280,13 @@ class Flow {
/** /**
* Get a node instance from this flow. If the node is not known to this * Get a node instance from this flow. If the node is not known to this
* flow, pass the request up to the parent. * flow, pass the request up to the parent.
* @param {[type]} id [description] * @param {String} id [description]
* @param {Boolean} cancelBubble if true, prevents the flow from passing the request to the parent
* This stops infinite loops when the parent asked this Flow for the
* node to begin with.
* @return {[type]} [description] * @return {[type]} [description]
*/ */
getNode(id) { getNode(id, cancelBubble) {
// console.log('getNode',id,!!this.activeNodes[id])
if (!id) { if (!id) {
return undefined; return undefined;
} }
@ -298,7 +299,10 @@ class Flow {
// TEMP: this is a subflow internal node within this flow // TEMP: this is a subflow internal node within this flow
return this.activeNodes[id]; return this.activeNodes[id];
} }
return this.parent.getNode(id); if (!cancelBubble) {
return this.parent.getNode(id);
}
return undefined;
} }
/** /**

View File

@ -17,6 +17,8 @@
const clone = require("clone"); const clone = require("clone");
const Flow = require('./Flow').Flow; const Flow = require('./Flow').Flow;
const util = require("util");
const redUtil = require("@node-red/util").util; const redUtil = require("@node-red/util").util;
const flowUtil = require("./util"); const flowUtil = require("./util");
@ -113,6 +115,40 @@ class Subflow extends Flow {
var self = this; var self = this;
// Create a subflow node to accept inbound messages and route appropriately // Create a subflow node to accept inbound messages and route appropriately
var Node = require("../Node"); var Node = require("../Node");
if (this.subflowDef.status) {
var subflowStatusConfig = {
id: this.subflowInstance.id+":status",
type: "subflow-status",
z: this.subflowInstance.id,
_flow: this.parent
}
this.statusNode = new Node(subflowStatusConfig);
this.statusNode.on("input", function(msg) {
if (msg.payload !== undefined) {
if (typeof msg.payload === "string") {
// if msg.payload is a String, use it as status text
self.node.status({text:msg.payload})
return;
} else if (Object.prototype.toString.call(msg.payload) === "[object Object]") {
if (msg.payload.hasOwnProperty('text') || msg.payload.hasOwnProperty('fill') || msg.payload.hasOwnProperty('shape') || Object.keys(msg.payload).length === 0) {
// msg.payload is an object that looks like a status object
self.node.status(msg.payload);
return;
}
}
// Anything else - inspect it and use as status text
var text = util.inspect(msg.payload);
if (text.length > 32) { text = text.substr(0,32) + "..."; }
self.node.status({text:text});
} else if (msg.status !== undefined) {
// if msg.status exists
self.node.status(msg.status)
}
})
}
var subflowInstanceConfig = { var subflowInstanceConfig = {
id: this.subflowInstance.id, id: this.subflowInstance.id,
type: this.subflowInstance.type, type: this.subflowInstance.type,
@ -177,7 +213,6 @@ class Subflow extends Flow {
// Wire the subflow outputs // Wire the subflow outputs
if (this.subflowDef.out) { if (this.subflowDef.out) {
var modifiedNodes = {};
for (var i=0;i<this.subflowDef.out.length;i++) { for (var i=0;i<this.subflowDef.out.length;i++) {
// i: the output index // i: the output index
// This is what this Output is wired to // This is what this Output is wired to
@ -189,7 +224,6 @@ class Subflow extends Flow {
this.node._updateWires(subflowInstanceConfig.wires); this.node._updateWires(subflowInstanceConfig.wires);
} else { } else {
var node = self.node_map[wires[j].id]; var node = self.node_map[wires[j].id];
modifiedNodes[node.id] = node;
if (!node._originalWires) { if (!node._originalWires) {
node._originalWires = clone(node.wires); node._originalWires = clone(node.wires);
} }
@ -198,6 +232,26 @@ class Subflow extends Flow {
} }
} }
} }
if (this.subflowDef.status) {
var subflowStatusId = this.statusNode.id;
wires = this.subflowDef.status.wires;
for (var j=0;j<wires.length;j++) {
if (wires[j].id === this.subflowDef.id) {
// A subflow input wired straight to a subflow output
subflowInstanceConfig.wires[wires[j].port].push(subflowStatusId);
this.node._updateWires(subflowInstanceConfig.wires);
} else {
var node = self.node_map[wires[j].id];
if (!node._originalWires) {
node._originalWires = clone(node.wires);
}
node.wires[wires[j].port] = (node.wires[wires[j].port]||[]);
node.wires[wires[j].port].push(subflowStatusId);
}
}
}
super.start(diff); super.start(diff);
} }
@ -227,6 +281,23 @@ class Subflow extends Flow {
return undefined; return undefined;
} }
/**
* Get a node instance from this subflow.
* If the subflow has a status node, check for that, otherwise use
* the super-class function
* @param {String} id [description]
* @param {Boolean} cancelBubble if true, prevents the flow from passing the request to the parent
* This stops infinite loops when the parent asked this Flow for the
* node to begin with.
* @return {[type]} [description]
*/
getNode(id, cancelBubble) {
if (this.statusNode && this.statusNode.id === id) {
return this.statusNode;
}
return super.getNode(id,cancelBubble);
}
/** /**
* Handle a status event from a node within this flow. * Handle a status event from a node within this flow.
* @param {Node} node The original node that triggered the event * @param {Node} node The original node that triggered the event
@ -240,10 +311,14 @@ class Subflow extends Flow {
handleStatus(node,statusMessage,reportingNode,muteStatus) { handleStatus(node,statusMessage,reportingNode,muteStatus) {
let handled = super.handleStatus(node,statusMessage,reportingNode,muteStatus); let handled = super.handleStatus(node,statusMessage,reportingNode,muteStatus);
if (!handled) { if (!handled) {
// No status node on this subflow caught the status message. if (!this.statusNode || node === this.node) {
// Pass up to the parent with this subflow's instance as the // No status node on this subflow caught the status message.
// reporting node // AND there is no Subflow Status node - so the user isn't
handled = this.parent.handleStatus(node,statusMessage,this.node,true); // wanting to manage status messages themselves
// Pass up to the parent with this subflow's instance as the
// reporting node
handled = this.parent.handleStatus(node,statusMessage,this.node,true);
}
} }
return handled; return handled;

View File

@ -207,11 +207,11 @@ function setFlows(_config,type,muteLog,forceStart) {
function getNode(id) { function getNode(id) {
var node; var node;
if (activeNodesToFlow[id] && activeFlows[activeNodesToFlow[id]]) { if (activeNodesToFlow[id] && activeFlows[activeNodesToFlow[id]]) {
return activeFlows[activeNodesToFlow[id]].getNode(id); return activeFlows[activeNodesToFlow[id]].getNode(id,true);
} }
for (var flowId in activeFlows) { for (var flowId in activeFlows) {
if (activeFlows.hasOwnProperty(flowId)) { if (activeFlows.hasOwnProperty(flowId)) {
node = activeFlows[flowId].getNode(id); node = activeFlows[flowId].getNode(id,true);
if (node) { if (node) {
return node; return node;
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/runtime", "name": "@node-red/runtime",
"version": "0.20.0-beta.3", "version": "0.20.0-beta.4",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,8 +16,8 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/registry": "0.20.0-beta.3", "@node-red/registry": "0.20.0-beta.4",
"@node-red/util": "0.20.0-beta.3", "@node-red/util": "0.20.0-beta.4",
"clone": "2.1.2", "clone": "2.1.2",
"express": "4.16.4", "express": "4.16.4",
"fs-extra": "7.0.1", "fs-extra": "7.0.1",

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/util", "name": "@node-red/util",
"version": "0.20.0-beta.3", "version": "0.20.0-beta.4",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -122,6 +122,13 @@ module.exports = {
*/ */
util: redUtil.util, util: redUtil.util,
/**
* This provides access to the internal nodes module of the
* runtime.
*
* @memberof node-red
*/
get nodes() { return runtime._.nodes }, get nodes() { return runtime._.nodes },
/** /**
@ -131,6 +138,12 @@ module.exports = {
*/ */
events: runtime.events, events: runtime.events,
/**
* This provides access to the internal settings module of the
* runtime.
*
* @memberof node-red
*/
get settings() { return runtime._.settings }, get settings() { return runtime._.settings },
@ -145,18 +158,21 @@ module.exports = {
/** /**
* The express application for the Editor Admin API * The express application for the Editor Admin API
* @type ExpressApplication
* @memberof node-red * @memberof node-red
*/ */
get httpAdmin() { return api.httpAdmin }, get httpAdmin() { return api.httpAdmin },
/** /**
* The express application for HTTP Nodes * The express application for HTTP Nodes
* @type ExpressApplication
* @memberof node-red * @memberof node-red
*/ */
get httpNode() { return runtime.httpNode }, get httpNode() { return runtime.httpNode },
/** /**
* The HTTP Server used by the runtime * The HTTP Server used by the runtime
* @type HTTPServer
* @memberof node-red * @memberof node-red
*/ */
get server() { return server }, get server() { return server },
@ -170,6 +186,7 @@ module.exports = {
/** /**
* The editor authentication api. * The editor authentication api.
* @see @node-red/editor-api_auth
* @memberof node-red * @memberof node-red
*/ */
auth: api.auth auth: api.auth

View File

@ -1,6 +1,6 @@
{ {
"name": "node-red", "name": "node-red",
"version": "0.20.0-beta.3", "version": "0.20.0-beta.4",
"description": "A visual tool for wiring the Internet of Things", "description": "A visual tool for wiring the Internet of Things",
"homepage": "http://nodered.org", "homepage": "http://nodered.org",
"license": "Apache-2.0", "license": "Apache-2.0",
@ -31,10 +31,10 @@
"flow" "flow"
], ],
"dependencies": { "dependencies": {
"@node-red/editor-api": "0.20.0-beta.3", "@node-red/editor-api": "0.20.0-beta.4",
"@node-red/runtime": "0.20.0-beta.3", "@node-red/runtime": "0.20.0-beta.4",
"@node-red/util": "0.20.0-beta.3", "@node-red/util": "0.20.0-beta.4",
"@node-red/nodes": "0.20.0-beta.3", "@node-red/nodes": "0.20.0-beta.4",
"basic-auth": "2.0.1", "basic-auth": "2.0.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"express": "4.16.4", "express": "4.16.4",
@ -43,7 +43,7 @@
"node-red-node-feedparser": "^0.1.14", "node-red-node-feedparser": "^0.1.14",
"node-red-node-rbe": "0.2.*", "node-red-node-rbe": "0.2.*",
"node-red-node-sentiment": "^0.1.0", "node-red-node-sentiment": "^0.1.0",
"node-red-node-tail": "^0.0.1", "node-red-node-tail": "^0.0.2",
"node-red-node-twitter": "^1.1.0", "node-red-node-twitter": "^1.1.0",
"nopt": "4.0.1", "nopt": "4.0.1",
"semver": "5.6.0" "semver": "5.6.0"

View File

@ -413,6 +413,69 @@ describe('Flow', function() {
}); });
}); });
describe('#getNode',function() {
it("gets a node known to the flow",function(done) {
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]},
{id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
{id:"4",z:"t1",type:"test",foo:"a"}
]);
var flow = Flow.create({},config,config.flows["t1"]);
flow.start();
Object.keys(flow.getActiveNodes()).should.have.length(4);
flow.getNode('1').should.have.a.property('id','1');
flow.stop().then(() => { done() });
});
it("passes to parent if node not known locally",function(done) {
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]},
{id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
{id:"4",z:"t1",type:"test",foo:"a"}
]);
var flow = Flow.create({
getNode: id => { return {id:id}}
},config,config.flows["t1"]);
flow.start();
Object.keys(flow.getActiveNodes()).should.have.length(4);
flow.getNode('1').should.have.a.property('id','1');
flow.getNode('parentNode').should.have.a.property('id','parentNode');
flow.stop().then(() => { done() });
});
it("does not pass to parent if cancelBubble set",function(done) {
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]},
{id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
{id:"4",z:"t1",type:"test",foo:"a"}
]);
var flow = Flow.create({
getNode: id => { return {id:id}}
},config,config.flows["t1"]);
flow.start();
Object.keys(flow.getActiveNodes()).should.have.length(4);
flow.getNode('1').should.have.a.property('id','1');
should.not.exist(flow.getNode('parentNode',true));
flow.stop().then(() => { done() });
});
});
describe("#handleStatus",function() { describe("#handleStatus",function() {
it("passes a status event to the adjacent status node",function(done) { it("passes a status event to the adjacent status node",function(done) {

View File

@ -298,7 +298,6 @@ describe('Subflow', function() {
done(); done();
}); });
}); });
it("instantiates a subflow inside a subflow and stops it",function(done) { it("instantiates a subflow inside a subflow and stops it",function(done) {
var config = flowUtils.parseConfig([ var config = flowUtils.parseConfig([
{id:"t1",type:"tab"}, {id:"t1",type:"tab"},
@ -452,7 +451,6 @@ describe('Subflow', function() {
done(); done();
}); });
}); });
it("passes a status event to the subflow's parent tab status node - targetted scope",function(done) { it("passes a status event to the subflow's parent tab status node - targetted scope",function(done) {
var config = flowUtils.parseConfig([ var config = flowUtils.parseConfig([
{id:"t1",type:"tab"}, {id:"t1",type:"tab"},
@ -490,9 +488,164 @@ describe('Subflow', function() {
done(); done();
}); });
}); });
}); });
describe("status node", function() {
it("emits a status event when a message is passed to a subflow-status node - msg.payload as string", function(done) {
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]},
{id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
{
id:"sf1",
type:"subflow",
name:"Subflow 2",
info:"",
in:[{wires:[{id:"sf1-1"}]}],
out:[{wires:[{id:"sf1-1",port:0}]}],
status:{wires:[{id:"sf1-1", port:0}]}
},
{id:"sf1-1",type:"test",name:"test","z":"sf1",x:166,y:99,"wires":[[]]},
{id:"sn",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]}
]);
var flow = Flow.create({},config,config.flows["t1"]);
flow.start();
var activeNodes = flow.getActiveNodes();
activeNodes["1"].receive({payload:"test-payload"});
currentNodes["sn"].should.have.a.property("handled",1);
var statusMessage = currentNodes["sn"].messages[0];
statusMessage.should.have.a.property("status");
statusMessage.status.should.have.a.property("text","test-payload");
statusMessage.status.should.have.a.property("source");
statusMessage.status.source.should.have.a.property("id","2");
statusMessage.status.source.should.have.a.property("type","subflow:sf1");
flow.stop().then(function() {
done();
});
});
it("emits a status event when a message is passed to a subflow-status node - msg.payload as status obj", function(done) {
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]},
{id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
{
id:"sf1",
type:"subflow",
name:"Subflow 2",
info:"",
in:[{wires:[{id:"sf1-1"}]}],
out:[{wires:[{id:"sf1-1",port:0}]}],
status:{wires:[{id:"sf1-1", port:0}]}
},
{id:"sf1-1",type:"test",name:"test","z":"sf1",x:166,y:99,"wires":[[]]},
{id:"sn",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]}
]);
var flow = Flow.create({},config,config.flows["t1"]);
flow.start();
var activeNodes = flow.getActiveNodes();
activeNodes["1"].receive({payload:{text:"payload-obj"}});
currentNodes["sn"].should.have.a.property("handled",1);
var statusMessage = currentNodes["sn"].messages[0];
statusMessage.should.have.a.property("status");
statusMessage.status.should.have.a.property("text","payload-obj");
statusMessage.status.should.have.a.property("source");
statusMessage.status.source.should.have.a.property("id","2");
statusMessage.status.source.should.have.a.property("type","subflow:sf1");
flow.stop().then(function() {
done();
});
});
it("emits a status event when a message is passed to a subflow-status node - msg.status", function(done) {
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]},
{id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
{
id:"sf1",
type:"subflow",
name:"Subflow 2",
info:"",
in:[{wires:[{id:"sf1-1"}]}],
out:[{wires:[{id:"sf1-1",port:0}]}],
status:{wires:[{id:"sf1-1", port:0}]}
},
{id:"sf1-1",type:"test",name:"test","z":"sf1",x:166,y:99,"wires":[[]]},
{id:"sn",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]}
]);
var flow = Flow.create({},config,config.flows["t1"]);
flow.start();
var activeNodes = flow.getActiveNodes();
activeNodes["1"].receive({status:{text:"status-obj"}});
currentNodes["sn"].should.have.a.property("handled",1);
var statusMessage = currentNodes["sn"].messages[0];
statusMessage.should.have.a.property("status");
statusMessage.status.should.have.a.property("text","status-obj");
statusMessage.status.should.have.a.property("source");
statusMessage.status.source.should.have.a.property("id","2");
statusMessage.status.source.should.have.a.property("type","subflow:sf1");
flow.stop().then(function() {
done();
});
});
it("does not emit a regular status event if it contains a subflow-status node", function(done) {
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]},
{id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]},
{
id:"sf1",
type:"subflow",
name:"Subflow 2",
info:"",
in:[{wires:[{id:"sf1-1"}]}],
out:[{wires:[{id:"sf1-1",port:0}]}],
status:{wires:[]}
},
{id:"sf1-1",type:"testStatus",name:"test-status-node","z":"sf1",x:166,y:99,"wires":[[]]},
{id:"sn",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]}
]);
var flow = Flow.create({},config,config.flows["t1"]);
flow.start();
var activeNodes = flow.getActiveNodes();
activeNodes["1"].receive({payload:"test-payload"});
currentNodes["sn"].should.have.a.property("handled",0);
flow.stop().then(function() {
done();
});
});
})
describe("#handleError",function() { describe("#handleError",function() {
it("passes an error event to the subflow's parent tab catch node - all scope",function(done) { it("passes an error event to the subflow's parent tab catch node - all scope",function(done) {
var config = flowUtils.parseConfig([ var config = flowUtils.parseConfig([
@ -526,7 +679,6 @@ describe('Subflow', function() {
done(); done();
}); });
}); });
it("passes an error event to the subflow's parent tab catch node - targetted scope",function(done) { it("passes an error event to the subflow's parent tab catch node - targetted scope",function(done) {
var config = flowUtils.parseConfig([ var config = flowUtils.parseConfig([
{id:"t1",type:"tab"}, {id:"t1",type:"tab"},
@ -563,7 +715,6 @@ describe('Subflow', function() {
}); });
}); });
}); });
describe("#env var", function() { describe("#env var", function() {