1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Merge branch 'dev' into theme-monaco-theme

This commit is contained in:
Steve-Mcl 2021-09-21 14:06:54 +01:00
commit 5825da9c76
65 changed files with 4070 additions and 2680 deletions

View File

@ -1,3 +1,49 @@
#### 2.0.6: Maintenance Release
Editor
- Fix typo in ko editor.json Fixes #3119
- Change fade color when hovering an inactive tab (#3106) @bonanitech
- Ensure treeList row has suitable min-height when no content Fixes #3109
Runtime
- Update tar to latest (#3128) @aksswami
- Give passport verify callback the same arity as the original callback (#3117) @dschmidt
- Handle HTTPS Key and certificate as string or buffer (#3115) @bartbutenaers
#### 2.0.5: Maintenance Release
Editor
- Remove default ctrl-enter keybinding from monaco editor Fixes #3093
Runtime
- Update tar dependency
- Add support for maintenance streams in generate-publish-script
Nodes
- Fix regression in Join node when manual joining array with msg.parts present Fixes #3096
#### 2.0.4: Maintenance Release
Editor
- Fix tab fade CSS for when using themes (#3085) @bonanitech
- Handle just-copied-but-not-deployed node with credentials in editor Fixes #3090
Nodes
- Filter: Fix RBE node handling of default topi property Fixes #3087
- HTTP Request: Handle partially encoded url query strings in request node
- HTTP Request: Fix support for supplied CA certs (#3089) @hardillb
- HTTP Request: Ensure TLS Cert is used (#3092) @hardillb
- Inject: Fix inject now button unable to send empty props
- Inject: Inject now button success notification should use label with updated props
#### 2.0.3: Maintenance Release #### 2.0.3: Maintenance Release
Nodes Nodes

View File

@ -162,7 +162,6 @@ module.exports = function(grunt) {
"packages/node_modules/@node-red/editor-client/src/js/ui/common/stack.js", "packages/node_modules/@node-red/editor-client/src/js/ui/common/stack.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js", "packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/common/toggleButton.js", "packages/node_modules/@node-red/editor-client/src/js/ui/common/toggleButton.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/common/colorPicker.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/actions.js", "packages/node_modules/@node-red/editor-client/src/js/ui/actions.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js", "packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/diff.js", "packages/node_modules/@node-red/editor-client/src/js/ui/diff.js",
@ -182,6 +181,7 @@ module.exports = function(grunt) {
"packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js", "packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js", "packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/editor.js", "packages/node_modules/@node-red/editor-client/src/js/ui/editor.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/*.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/editors/*.js", "packages/node_modules/@node-red/editor-client/src/js/ui/editors/*.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/*.js", "packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/*.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/event-log.js", "packages/node_modules/@node-red/editor-client/src/js/ui/event-log.js",

View File

@ -1,6 +1,6 @@
{ {
"name": "node-red", "name": "node-red",
"version": "2.0.3", "version": "2.0.6",
"description": "Low-code programming for event-driven applications", "description": "Low-code programming for event-driven applications",
"homepage": "http://nodered.org", "homepage": "http://nodered.org",
"license": "Apache-2.0", "license": "Apache-2.0",
@ -28,8 +28,8 @@
"dependencies": { "dependencies": {
"acorn": "8.4.1", "acorn": "8.4.1",
"acorn-walk": "8.1.1", "acorn-walk": "8.1.1",
"ajv": "8.6.0", "ajv": "8.6.2",
"async-mutex": "0.3.1", "async-mutex": "0.3.2",
"basic-auth": "2.0.1", "basic-auth": "2.0.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"body-parser": "1.19.0", "body-parser": "1.19.0",
@ -55,14 +55,14 @@
"is-utf8": "0.2.1", "is-utf8": "0.2.1",
"js-yaml": "3.14.1", "js-yaml": "3.14.1",
"json-stringify-safe": "5.0.1", "json-stringify-safe": "5.0.1",
"jsonata": "1.8.4", "jsonata": "1.8.5",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"media-typer": "1.1.0", "media-typer": "1.1.0",
"memorystore": "1.6.6", "memorystore": "1.6.6",
"mime": "2.5.2", "mime": "2.5.2",
"moment-timezone": "0.5.33", "moment-timezone": "0.5.33",
"mqtt": "4.2.8", "mqtt": "4.2.8",
"multer": "1.4.2", "multer": "1.4.3",
"mustache": "4.2.0", "mustache": "4.2.0",
"node-red-admin": "^2.2.0", "node-red-admin": "^2.2.0",
"nopt": "5.0.0", "nopt": "5.0.0",
@ -73,9 +73,9 @@
"passport-oauth2-client-password": "0.1.2", "passport-oauth2-client-password": "0.1.2",
"raw-body": "2.4.1", "raw-body": "2.4.1",
"semver": "7.3.5", "semver": "7.3.5",
"tar": "6.1.0", "tar": "6.1.11",
"tough-cookie": "4.0.0", "tough-cookie": "4.0.0",
"uglify-js": "3.13.10", "uglify-js": "3.14.1",
"uuid": "8.3.2", "uuid": "8.3.2",
"ws": "7.5.1", "ws": "7.5.1",
"xml2js": "0.4.23" "xml2js": "0.4.23"
@ -84,7 +84,7 @@
"bcrypt": "5.0.1" "bcrypt": "5.0.1"
}, },
"devDependencies": { "devDependencies": {
"dompurify": "2.2.9", "dompurify": "2.3.1",
"grunt": "1.4.1", "grunt": "1.4.1",
"grunt-chmod": "~1.1.1", "grunt-chmod": "~1.1.1",
"grunt-cli": "~1.4.3", "grunt-cli": "~1.4.3",
@ -109,15 +109,15 @@
"jsdoc-nr-template": "github:node-red/jsdoc-nr-template", "jsdoc-nr-template": "github:node-red/jsdoc-nr-template",
"marked": "2.1.3", "marked": "2.1.3",
"minami": "1.2.3", "minami": "1.2.3",
"mocha": "9.0.1", "mocha": "9.1.1",
"node-red-node-test-helper": "^0.2.7", "node-red-node-test-helper": "^0.2.7",
"nodemon": "2.0.8", "nodemon": "2.0.12",
"proxy": "^1.0.2", "proxy": "^1.0.2",
"sass": "1.35.1", "sass": "1.39.0",
"should": "13.2.3", "should": "13.2.3",
"sinon": "11.1.1", "sinon": "11.1.2",
"stoppable": "^1.1.0", "stoppable": "^1.1.0",
"supertest": "6.1.3" "supertest": "6.1.6"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=12"

View File

@ -173,27 +173,30 @@ function genericStrategy(adminApp,strategy) {
adminApp.use(passport.session()); adminApp.use(passport.session());
var options = strategy.options; var options = strategy.options;
var verify = function() {
var originalDone = arguments[arguments.length-1];
if (options.verify) {
var args = Array.from(arguments);
args[args.length-1] = function(err,profile) {
if (err) {
return originalDone(err);
} else {
return completeVerify(profile,originalDone);
}
};
passport.use(new strategy.strategy(options, options.verify.apply(null,args);
function() { } else {
var originalDone = arguments[arguments.length-1]; var profile = arguments[arguments.length - 2];
if (options.verify) { return completeVerify(profile,originalDone);
var args = Array.from(arguments);
args[args.length-1] = function(err,profile) {
if (err) {
return originalDone(err);
} else {
return completeVerify(profile,originalDone);
}
};
options.verify.apply(null,args);
} else {
var profile = arguments[arguments.length - 2];
return completeVerify(profile,originalDone);
}
} }
)); };
// Give our callback the same arity as the original one from options
if (options.verify) {
Object.defineProperty(verify, "length", { value: options.verify.length })
}
passport.use(new strategy.strategy(options, verify));
adminApp.get('/auth/strategy', adminApp.get('/auth/strategy',
passport.authenticate(strategy.name, {session:false, failureRedirect: settings.httpAdminRoot }), passport.authenticate(strategy.name, {session:false, failureRedirect: settings.httpAdminRoot }),

View File

@ -24,7 +24,10 @@ var defaultContext = {
page: { page: {
title: "Node-RED", title: "Node-RED",
favicon: "favicon.ico", favicon: "favicon.ico",
tabicon: "red/images/node-red-icon-black.svg" tabicon: {
icon: "red/images/node-red-icon-black.svg",
colour: "#8f0000"
}
}, },
header: { header: {
title: "Node-RED", title: "Node-RED",
@ -123,9 +126,13 @@ module.exports = {
} }
if (theme.page.tabicon) { if (theme.page.tabicon) {
url = serveFile(themeApp,"/tabicon/",theme.page.tabicon) let icon = theme.page.tabicon.icon || theme.page.tabicon
url = serveFile(themeApp,"/tabicon/", icon)
if (url) { if (url) {
themeContext.page.tabicon = url; themeContext.page.tabicon.icon = url;
}
if (theme.page.tabicon.colour) {
themeContext.page.tabicon.colour = theme.page.tabicon.colour
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/editor-api", "name": "@node-red/editor-api",
"version": "2.0.3", "version": "2.0.6",
"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": "2.0.3", "@node-red/util": "2.0.6",
"@node-red/editor-client": "2.0.3", "@node-red/editor-client": "2.0.6",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"body-parser": "1.19.0", "body-parser": "1.19.0",
"clone": "2.1.2", "clone": "2.1.2",
@ -26,7 +26,7 @@
"express": "4.17.1", "express": "4.17.1",
"memorystore": "1.6.6", "memorystore": "1.6.6",
"mime": "2.5.2", "mime": "2.5.2",
"multer": "1.4.2", "multer": "1.4.3",
"mustache": "4.2.0", "mustache": "4.2.0",
"oauth2orize": "1.11.0", "oauth2orize": "1.11.0",
"passport-http-bearer": "1.0.1", "passport-http-bearer": "1.0.1",

View File

@ -116,7 +116,20 @@
"groupSelection": "Group selection", "groupSelection": "Group selection",
"ungroupSelection": "Ungroup selection", "ungroupSelection": "Ungroup selection",
"groupMergeSelection": "Merge selection", "groupMergeSelection": "Merge selection",
"groupRemoveSelection": "Remove from group" "groupRemoveSelection": "Remove from group",
"arrange":"Arrange",
"alignLeft":"Align to left",
"alignCenter":"Align to center",
"alignRight":"Align to right",
"alignTop":"Align to top",
"alignMiddle":"Align to middle",
"alignBottom":"Align to bottom",
"distributeHorizontally":"Distribute horizontally",
"distributeVertically":"Distribute vertically",
"moveToBack":"Move to back",
"moveToFront":"Move to front",
"moveBackwards":"Move backwards",
"moveForwards":"Move forwards"
} }
}, },
"actions": { "actions": {
@ -450,8 +463,9 @@
"unassigned": "Unassigned", "unassigned": "Unassigned",
"global": "global", "global": "global",
"workspace": "workspace", "workspace": "workspace",
"selectAll": "Select all nodes", "selectAll": "Select all",
"selectAllConnected": "Select all connected nodes", "selectNone": "Select none",
"selectAllConnected": "Select connected",
"addRemoveNode": "Add/remove node from selection", "addRemoveNode": "Add/remove node from selection",
"editSelected": "Edit selected node", "editSelected": "Edit selected node",
"deleteSelected": "Delete selected nodes or link", "deleteSelected": "Delete selected nodes or link",
@ -464,7 +478,10 @@
"copyNode": "Copy selected nodes", "copyNode": "Copy selected nodes",
"cutNode": "Cut selected nodes", "cutNode": "Cut selected nodes",
"pasteNode": "Paste nodes", "pasteNode": "Paste nodes",
"undoChange": "Undo the last change performed", "copyGroupStyle": "Copy group style",
"pasteGroupStyle": "Paste group style",
"undoChange": "Undo",
"redoChange": "Redo",
"searchBox": "Open search box", "searchBox": "Open search box",
"managePalette": "Manage palette", "managePalette": "Manage palette",
"actionList":"Action list" "actionList":"Action list"

View File

@ -56,7 +56,7 @@
"displayConfig": "설정노드 보기", "displayConfig": "설정노드 보기",
"import": "가져오기", "import": "가져오기",
"export": "내보내기", "export": "내보내기",
"search": "플로우 색", "search": "플로우 색",
"searchInput": "플로우 검색", "searchInput": "플로우 검색",
"subflows": "보조 플로우", "subflows": "보조 플로우",
"createSubflow": "보조 플로우 생성", "createSubflow": "보조 플로우 생성",

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/editor-client", "name": "@node-red/editor-client",
"version": "2.0.3", "version": "2.0.6",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -558,11 +558,22 @@ RED.history = (function() {
} else if (ev.t == "reorder") { } else if (ev.t == "reorder") {
inverseEv = { inverseEv = {
t: 'reorder', t: 'reorder',
order: RED.nodes.getWorkspaceOrder(),
dirty: RED.nodes.dirty() dirty: RED.nodes.dirty()
}; };
if (ev.order) { if (ev.workspaces) {
RED.workspaces.order(ev.order); inverseEv.workspaces = {
from: ev.workspaces.to,
to: ev.workspaces.from
}
RED.workspaces.order(ev.workspaces.from);
}
if (ev.nodes) {
inverseEv.nodes = {
z: ev.nodes.z,
from: ev.nodes.to,
to: ev.nodes.from
}
RED.nodes.setNodeOrder(ev.nodes.z,ev.nodes.from);
} }
} else if (ev.t == "createGroup") { } else if (ev.t == "createGroup") {
inverseEv = { inverseEv = {
@ -658,6 +669,8 @@ RED.history = (function() {
push: function(ev) { push: function(ev) {
undoHistory.push(ev); undoHistory.push(ev);
redoHistory = []; redoHistory = [];
RED.menu.setDisabled("menu-item-edit-undo", false);
RED.menu.setDisabled("menu-item-edit-redo", true);
}, },
pop: function() { pop: function() {
var ev = undoHistory.pop(); var ev = undoHistory.pop();
@ -665,6 +678,8 @@ RED.history = (function() {
if (rev) { if (rev) {
redoHistory.push(rev); redoHistory.push(rev);
} }
RED.menu.setDisabled("menu-item-edit-undo", undoHistory.length === 0);
RED.menu.setDisabled("menu-item-edit-redo", redoHistory.length === 0);
}, },
peek: function() { peek: function() {
return undoHistory[undoHistory.length-1]; return undoHistory[undoHistory.length-1];
@ -672,6 +687,8 @@ RED.history = (function() {
clear: function() { clear: function() {
undoHistory = []; undoHistory = [];
redoHistory = []; redoHistory = [];
RED.menu.setDisabled("menu-item-edit-undo", true);
RED.menu.setDisabled("menu-item-edit-redo", true);
}, },
redo: function() { redo: function() {
var ev = redoHistory.pop(); var ev = redoHistory.pop();
@ -681,6 +698,8 @@ RED.history = (function() {
undoHistory.push(uev); undoHistory.push(uev);
} }
} }
RED.menu.setDisabled("menu-item-edit-undo", undoHistory.length === 0);
RED.menu.setDisabled("menu-item-edit-redo", redoHistory.length === 0);
} }
} }

View File

@ -77,6 +77,15 @@
"right": "core:go-to-nearest-node-on-right", "right": "core:go-to-nearest-node-on-right",
"left": "core:go-to-nearest-node-on-left", "left": "core:go-to-nearest-node-on-left",
"up": "core:go-to-nearest-node-above", "up": "core:go-to-nearest-node-above",
"down": "core:go-to-nearest-node-below" "down": "core:go-to-nearest-node-below",
"alt-a g": "core:align-selection-to-grid",
"alt-a l": "core:align-selection-to-left",
"alt-a r": "core:align-selection-to-right",
"alt-a t": "core:align-selection-to-top",
"alt-a b": "core:align-selection-to-bottom",
"alt-a m": "core:align-selection-to-middle",
"alt-a c": "core:align-selection-to-center",
"alt-a h": "core:distribute-selection-horizontally",
"alt-a v": "core:distribute-selection-vertically"
} }
} }

View File

@ -16,8 +16,6 @@
RED.nodes = (function() { RED.nodes = (function() {
var node_defs = {}; var node_defs = {};
var nodes = {};
var nodeTabMap = {};
var linkTabMap = {}; var linkTabMap = {};
var configNodes = {}; var configNodes = {};
@ -41,6 +39,7 @@ RED.nodes = (function() {
RED.events.emit("workspace:dirty",{dirty:dirty}); RED.events.emit("workspace:dirty",{dirty:dirty});
} }
// The registry holds information about all node types.
var registry = (function() { var registry = (function() {
var moduleList = {}; var moduleList = {};
var nodeList = []; var nodeList = [];
@ -53,7 +52,8 @@ RED.nodes = (function() {
defaults: { defaults: {
label: {value:""}, label: {value:""},
disabled: {value: false}, disabled: {value: false},
info: {value: ""} info: {value: ""},
env: {value: []}
} }
}; };
@ -209,6 +209,279 @@ RED.nodes = (function() {
return exports; return exports;
})(); })();
// allNodes holds information about the Flow nodes.
var allNodes = (function() {
var nodes = {};
var tabMap = {};
var api = {
addTab: function(id) {
tabMap[id] = [];
},
hasTab: function(z) {
return tabMap.hasOwnProperty(z)
},
removeTab: function(id) {
delete tabMap[id];
},
addNode: function(n) {
nodes[n.id] = n;
if (tabMap.hasOwnProperty(n.z)) {
tabMap[n.z].push(n);
} else {
console.warn("Node added to unknown tab/subflow:",n);
tabMap["_"] = tabMap["_"] || [];
tabMap["_"].push(n);
}
},
removeNode: function(n) {
delete nodes[n.id]
if (tabMap.hasOwnProperty(n.z)) {
var i = tabMap[n.z].indexOf(n);
if (i > -1) {
tabMap[n.z].splice(i,1);
}
}
},
hasNode: function(id) {
return nodes.hasOwnProperty(id);
},
getNode: function(id) {
return nodes[id]
},
moveNode: function(n, newZ) {
api.removeNode(n);
tabMap[newZ] = tabMap[newZ] || [];
tabMap[newZ].push(n);
},
moveNodesForwards: function(nodes) {
var result = [];
if (!Array.isArray(nodes)) {
nodes = [nodes]
}
// Can only do this for nodes on the same tab.
// Use nodes[0] to get the z
var tabNodes = tabMap[nodes[0].z];
var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" }));
var moved = new Set();
for (var i = tabNodes.length-1; i >= 0; i--) {
if (toMove.size === 0) {
break;
}
var n = tabNodes[i];
if (toMove.has(n)) {
// This is a node to move.
if (i < tabNodes.length-1 && !moved.has(tabNodes[i+1])) {
// Remove from current position
tabNodes.splice(i,1);
// Add it back one position higher
tabNodes.splice(i+1,0,n);
n._reordered = true;
result.push(n);
}
toMove.delete(n);
moved.add(n);
}
}
if (result.length > 0) {
RED.events.emit('nodes:reorder',{
z: nodes[0].z,
nodes: result
});
}
return result;
},
moveNodesBackwards: function(nodes) {
var result = [];
if (!Array.isArray(nodes)) {
nodes = [nodes]
}
// Can only do this for nodes on the same tab.
// Use nodes[0] to get the z
var tabNodes = tabMap[nodes[0].z];
var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" }));
var moved = new Set();
for (var i = 0; i < tabNodes.length; i++) {
if (toMove.size === 0) {
break;
}
var n = tabNodes[i];
if (toMove.has(n)) {
// This is a node to move.
if (i > 0 && !moved.has(tabNodes[i-1])) {
// Remove from current position
tabNodes.splice(i,1);
// Add it back one position lower
tabNodes.splice(i-1,0,n);
n._reordered = true;
result.push(n);
}
toMove.delete(n);
moved.add(n);
}
}
if (result.length > 0) {
RED.events.emit('nodes:reorder',{
z: nodes[0].z,
nodes: result
});
}
return result;
},
moveNodesToFront: function(nodes) {
var result = [];
if (!Array.isArray(nodes)) {
nodes = [nodes]
}
// Can only do this for nodes on the same tab.
// Use nodes[0] to get the z
var tabNodes = tabMap[nodes[0].z];
var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" }));
var target = tabNodes.length-1;
for (var i = tabNodes.length-1; i >= 0; i--) {
if (toMove.size === 0) {
break;
}
var n = tabNodes[i];
if (toMove.has(n)) {
// This is a node to move.
if (i < target) {
// Remove from current position
tabNodes.splice(i,1);
tabNodes.splice(target,0,n);
n._reordered = true;
result.push(n);
}
target--;
toMove.delete(n);
}
}
if (result.length > 0) {
RED.events.emit('nodes:reorder',{
z: nodes[0].z,
nodes: result
});
}
return result;
},
moveNodesToBack: function(nodes) {
var result = [];
if (!Array.isArray(nodes)) {
nodes = [nodes]
}
// Can only do this for nodes on the same tab.
// Use nodes[0] to get the z
var tabNodes = tabMap[nodes[0].z];
var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" }));
var target = 0;
for (var i = 0; i < tabNodes.length; i++) {
if (toMove.size === 0) {
break;
}
var n = tabNodes[i];
if (toMove.has(n)) {
// This is a node to move.
if (i > target) {
// Remove from current position
tabNodes.splice(i,1);
// Add it back one position lower
tabNodes.splice(target,0,n);
n._reordered = true;
result.push(n);
}
target++;
toMove.delete(n);
}
}
if (result.length > 0) {
RED.events.emit('nodes:reorder',{
z: nodes[0].z,
nodes: result
});
}
return result;
},
getNodes: function(z) {
return tabMap[z];
},
clear: function() {
nodes = {};
tabMap = {};
},
eachNode: function(cb) {
var nodeList,i,j;
for (i in subflows) {
if (subflows.hasOwnProperty(i)) {
nodeList = tabMap[i];
for (j = 0; j < nodeList.length; j++) {
if (cb(nodeList[j]) === false) {
return;
}
}
}
}
for (i = 0; i < workspacesOrder.length; i++) {
nodeList = tabMap[workspacesOrder[i]];
for (j = 0; j < nodeList.length; j++) {
if (cb(nodeList[j]) === false) {
return;
}
}
}
// Flow nodes that do not have a valid tab/subflow
if (tabMap["_"]) {
nodeList = tabMap["_"];
for (j = 0; j < nodeList.length; j++) {
if (cb(nodeList[j]) === false) {
return;
}
}
}
},
filterNodes: function(filter) {
var result = [];
var searchSet = null;
var doZFilter = false;
if (filter.hasOwnProperty("z")) {
if (tabMap.hasOwnProperty(filter.z)) {
searchSet = tabMap[filter.z];
} else {
doZFilter = true;
}
}
if (searchSet === null) {
searchSet = nodes;
}
for (var n=0;n<searchSet.length;n++) {
var node = searchSet[n];
if (filter.hasOwnProperty("type") && node.type !== filter.type) {
continue;
}
if (doZFilter && node.z !== filter.z) {
continue;
}
result.push(node);
}
return result;
},
getNodeOrder: function(z) {
return tabMap[z].map(function(n) { return n.id })
},
setNodeOrder: function(z, order) {
var orderMap = {};
order.forEach(function(id,i) {
orderMap[id] = i;
})
tabMap[z].sort(function(A,B) {
A._reordered = true;
B._reordered = true;
return orderMap[A.id] - orderMap[B.id];
})
}
}
return api;
})()
function getID() { function getID() {
var bytes = []; var bytes = [];
for (var i=0;i<8;i++) { for (var i=0;i<8;i++) {
@ -294,15 +567,10 @@ RED.nodes = (function() {
}); });
n.i = nextId+1; n.i = nextId+1;
} }
nodes[n.id] = n; allNodes.addNode(n);
if (!nodeLinks[n.id]) { if (!nodeLinks[n.id]) {
nodeLinks[n.id] = {in:[],out:[]}; nodeLinks[n.id] = {in:[],out:[]};
} }
if (nodeTabMap[n.z]) {
nodeTabMap[n.z][n.id] = n;
} else {
console.warn("Node added to unknown tab/subflow:",n);
}
} }
RED.events.emit('nodes:add',n); RED.events.emit('nodes:add',n);
} }
@ -330,10 +598,8 @@ RED.nodes = (function() {
function getNode(id) { function getNode(id) {
if (id in configNodes) { if (id in configNodes) {
return configNodes[id]; return configNodes[id];
} else if (id in nodes) {
return nodes[id];
} }
return null; return allNodes.getNode(id);
} }
function removeNode(id) { function removeNode(id) {
@ -345,13 +611,10 @@ RED.nodes = (function() {
delete configNodes[id]; delete configNodes[id];
RED.events.emit('nodes:remove',node); RED.events.emit('nodes:remove',node);
RED.workspaces.refresh(); RED.workspaces.refresh();
} else if (id in nodes) { } else if (allNodes.hasNode(id)) {
node = nodes[id]; node = allNodes.getNode(id);
delete nodes[id] allNodes.removeNode(node);
delete nodeLinks[id]; delete nodeLinks[id];
if (nodeTabMap[node.z]) {
delete nodeTabMap[node.z][node.id];
}
removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); }); removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); });
removedLinks.forEach(removeLink); removedLinks.forEach(removeLink);
var updatedConfigNode = false; var updatedConfigNode = false;
@ -409,18 +672,32 @@ RED.nodes = (function() {
return {links:removedLinks,nodes:removedNodes}; return {links:removedLinks,nodes:removedNodes};
} }
function moveNodesForwards(nodes) {
return allNodes.moveNodesForwards(nodes);
}
function moveNodesBackwards(nodes) {
return allNodes.moveNodesBackwards(nodes);
}
function moveNodesToFront(nodes) {
return allNodes.moveNodesToFront(nodes);
}
function moveNodesToBack(nodes) {
return allNodes.moveNodesToBack(nodes);
}
function getNodeOrder(z) {
return allNodes.getNodeOrder(z);
}
function setNodeOrder(z, order) {
allNodes.setNodeOrder(z,order);
}
function moveNodeToTab(node, z) { function moveNodeToTab(node, z) {
if (node.type === "group") { if (node.type === "group") {
moveGroupToTab(node,z); moveGroupToTab(node,z);
return; return;
} }
if (nodeTabMap[node.z]) { allNodes.moveNode(node,z);
delete nodeTabMap[node.z][node.id];
}
if (!nodeTabMap[z]) {
nodeTabMap[z] = {};
}
nodeTabMap[z][node.id] = node;
var nl = nodeLinks[node.id]; var nl = nodeLinks[node.id];
if (nl) { if (nl) {
nl.in.forEach(function(l) { nl.in.forEach(function(l) {
@ -482,7 +759,7 @@ RED.nodes = (function() {
function addWorkspace(ws,targetIndex) { function addWorkspace(ws,targetIndex) {
workspaces[ws.id] = ws; workspaces[ws.id] = ws;
nodeTabMap[ws.id] = {}; allNodes.addTab(ws.id);
linkTabMap[ws.id] = []; linkTabMap[ws.id] = [];
ws._def = RED.nodes.getType('tab'); ws._def = RED.nodes.getType('tab');
@ -506,21 +783,16 @@ RED.nodes = (function() {
var removedGroups = []; var removedGroups = [];
if (ws) { if (ws) {
delete workspaces[id]; delete workspaces[id];
delete nodeTabMap[id]; allNodes.removeTab(id);
delete linkTabMap[id]; delete linkTabMap[id];
workspacesOrder.splice(workspacesOrder.indexOf(id),1); workspacesOrder.splice(workspacesOrder.indexOf(id),1);
var i; var i;
var node; var node;
// TODO: this should use nodeTabMap
for (i in nodes) { if (allNodes.hasTab(id)) {
if (nodes.hasOwnProperty(i)) { removedNodes = allNodes.getNodes(id).slice()
node = nodes[i];
if (node.z == id) {
removedNodes.push(node);
}
}
} }
for(i in configNodes) { for (i in configNodes) {
if (configNodes.hasOwnProperty(i)) { if (configNodes.hasOwnProperty(i)) {
node = configNodes[i]; node = configNodes[i];
if (node.z == id) { if (node.z == id) {
@ -572,7 +844,7 @@ RED.nodes = (function() {
sf.name = subflowName; sf.name = subflowName;
} }
subflows[sf.id] = sf; subflows[sf.id] = sf;
nodeTabMap[sf.id] = {}; allNodes.addTab(sf.id);
linkTabMap[sf.id] = []; linkTabMap[sf.id] = [];
RED.nodes.registerType("subflow:"+sf.id, { RED.nodes.registerType("subflow:"+sf.id, {
@ -591,18 +863,18 @@ RED.nodes = (function() {
inputLabels: function(i) { return sf.inputLabels?sf.inputLabels[i]:null }, inputLabels: function(i) { return sf.inputLabels?sf.inputLabels[i]:null },
outputLabels: function(i) { return sf.outputLabels?sf.outputLabels[i]:null }, outputLabels: function(i) { return sf.outputLabels?sf.outputLabels[i]:null },
oneditprepare: function() { oneditprepare: function() {
RED.subflow.buildEditForm("subflow",this); if (this.type !== 'subflow') {
RED.subflow.buildPropertiesForm(this); // A subflow instance node
RED.subflow.buildEditForm("subflow",this);
} else {
// A subflow template node
RED.subflow.buildEditForm("subflow-template", this);
}
}, },
oneditresize: function(size) { oneditresize: function(size) {
// var rows = $(".dialog-form>div:not(.node-input-env-container-row)"); if (this.type === 'subflow') {
var height = size.height; $("#node-input-env-container").editableList('height',size.height - 80);
// for (var i=0; i<rows.size(); i++) { }
// height -= $(rows[i]).outerHeight(true);
// }
// var editorRow = $("#dialog-form>div.node-input-env-container-row");
// height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$("ol.red-ui-editor-subflow-env-list").editableList('height',height);
}, },
set:{ set:{
module: "node-red" module: "node-red"
@ -618,27 +890,24 @@ RED.nodes = (function() {
function removeSubflow(sf) { function removeSubflow(sf) {
if (subflows[sf.id]) { if (subflows[sf.id]) {
delete subflows[sf.id]; delete subflows[sf.id];
delete nodeTabMap[sf.id]; allNodes.removeTab(sf.id);
registry.removeNodeType("subflow:"+sf.id); registry.removeNodeType("subflow:"+sf.id);
RED.events.emit("subflows:remove",sf); RED.events.emit("subflows:remove",sf);
} }
} }
function subflowContains(sfid,nodeid) { function subflowContains(sfid,nodeid) {
for (var i in nodes) { var sfNodes = allNodes.getNodes(sfid);
if (nodes.hasOwnProperty(i)) { for (var i = 0; i<sfNodes.length; i++) {
var node = nodes[i]; var node = sfNodes[i];
if (node.z === sfid) { var m = /^subflow:(.+)$/.exec(node.type);
var m = /^subflow:(.+)$/.exec(node.type); if (m) {
if (m) { if (m[1] === nodeid) {
if (m[1] === nodeid) { return true;
return true; } else {
} else { var result = subflowContains(m[1],nodeid);
var result = subflowContains(m[1],nodeid); if (result) {
if (result) { return true;
return true;
}
}
} }
} }
} }
@ -681,7 +950,13 @@ RED.nodes = (function() {
} }
function convertWorkspace(n) { function convertWorkspace(n,opts) {
var exportCreds = true;
if (opts) {
if (opts.hasOwnProperty("credentials")) {
exportCreds = opts.credentials;
}
}
var node = {}; var node = {};
node.id = n.id; node.id = n.id;
node.type = n.type; node.type = n.type;
@ -690,6 +965,23 @@ RED.nodes = (function() {
node[d] = n[d]; node[d] = n[d];
} }
} }
if (exportCreds) {
var credentialSet = {};
if (n.credentials) {
for (var tabCred in n.credentials) {
if (n.credentials.hasOwnProperty(tabCred)) {
if (!n.credentials._ ||
n.credentials["has_"+tabCred] != n.credentials._["has_"+tabCred] ||
(n.credentials["has_"+tabCred] && n.credentials[tabCred])) {
credentialSet[tabCred] = n.credentials[tabCred];
}
}
}
if (Object.keys(credentialSet).length > 0) {
node.credentials = credentialSet;
}
}
}
return node; return node;
} }
/** /**
@ -710,7 +1002,7 @@ RED.nodes = (function() {
} }
if (n.type === 'tab') { if (n.type === 'tab') {
return convertWorkspace(n); return convertWorkspace(n, { credentials: exportCreds });
} }
var node = {}; var node = {};
node.id = n.id; node.id = n.id;
@ -739,8 +1031,10 @@ RED.nodes = (function() {
} }
if (exportCreds) { if (exportCreds) {
var credentialSet = {}; var credentialSet = {};
if (/^subflow:/.test(node.type) && n.credentials) { if ((/^subflow:/.test(node.type) ||
// A subflow instance node can have arbitrary creds (node.type === "group")) &&
n.credentials) {
// A subflow instance/group node can have arbitrary creds
for (var sfCred in n.credentials) { for (var sfCred in n.credentials) {
if (n.credentials.hasOwnProperty(sfCred)) { if (n.credentials.hasOwnProperty(sfCred)) {
if (!n.credentials._ || if (!n.credentials._ ||
@ -934,11 +1228,15 @@ RED.nodes = (function() {
function createExportableSubflow(id) { function createExportableSubflow(id) {
var sf = getSubflow(id); var sf = getSubflow(id);
var nodeSet = [sf]; var nodeSet;
var sfNodeIds = Object.keys(nodeTabMap[sf.id]||{}); var sfNodes = allNodes.getNodes(sf.id);
for (var i=0, l=sfNodeIds.length; i<l; i++) { if (sfNodes) {
nodeSet.push(nodeTabMap[sf.id][sfNodeIds[i]]); nodeSet = sfNodes.slice();
nodeSet.unshift(sf);
} else {
nodeSet = [sf];
} }
console.log(nodeSet);
return createExportableNodeSet(nodeSet); return createExportableNodeSet(nodeSet);
} }
/** /**
@ -965,12 +1263,9 @@ RED.nodes = (function() {
if (!exportedSubflows[subflowId]) { if (!exportedSubflows[subflowId]) {
exportedSubflows[subflowId] = true; exportedSubflows[subflowId] = true;
var subflow = getSubflow(subflowId); var subflow = getSubflow(subflowId);
var subflowSet = [subflow]; var subflowSet = allNodes.getNodes(subflowId).slice();
RED.nodes.eachNode(function(n) { subflowSet.unshift(subflow);
if (n.z == subflowId) {
subflowSet.push(n);
}
});
RED.nodes.eachConfig(function(n) { RED.nodes.eachConfig(function(n) {
if (n.z == subflowId) { if (n.z == subflowId) {
subflowSet.push(n); subflowSet.push(n);
@ -1030,7 +1325,7 @@ RED.nodes = (function() {
var i; var i;
for (i=0;i<workspacesOrder.length;i++) { for (i=0;i<workspacesOrder.length;i++) {
if (workspaces[workspacesOrder[i]].type == "tab") { if (workspaces[workspacesOrder[i]].type == "tab") {
nns.push(convertWorkspace(workspaces[workspacesOrder[i]])); nns.push(convertWorkspace(workspaces[workspacesOrder[i]], opts));
} }
} }
for (i in subflows) { for (i in subflows) {
@ -1048,11 +1343,9 @@ RED.nodes = (function() {
nns.push(convertNode(configNodes[i], opts)); nns.push(convertNode(configNodes[i], opts));
} }
} }
for (i in nodes) { RED.nodes.eachNode(function(n) {
if (nodes.hasOwnProperty(i)) { nns.push(convertNode(n, opts));
nns.push(convertNode(nodes[i], opts)); })
}
}
return nns; return nns;
} }
@ -1149,7 +1442,7 @@ RED.nodes = (function() {
var nodeZ = n.z || "__global__"; var nodeZ = n.z || "__global__";
imported.zMap[nodeZ] = imported.zMap[nodeZ] || []; imported.zMap[nodeZ] = imported.zMap[nodeZ] || [];
imported.zMap[nodeZ].push(n) imported.zMap[nodeZ].push(n)
if (nodes[n.id] || configNodes[n.id] || workspaces[n.id] || subflows[n.id] || groups[n.id]) { if (allNodes.hasNode(n.id) || configNodes[n.id] || workspaces[n.id] || subflows[n.id] || groups[n.id]) {
imported.conflicted[n.id] = n; imported.conflicted[n.id] = n;
} }
}) })
@ -1157,7 +1450,6 @@ RED.nodes = (function() {
} }
/** /**
* Replace the provided nodes. * Replace the provided nodes.
* This must contain complete Subflow defs or complete Flow Tabs. * This must contain complete Subflow defs or complete Flow Tabs.
@ -1316,7 +1608,7 @@ RED.nodes = (function() {
if (!options.generateIds) { if (!options.generateIds) {
if (!options.importMap[id]) { if (!options.importMap[id]) {
// No conflict resolution for this node // No conflict resolution for this node
var existing = nodes[id] || configNodes[id] || workspaces[id] || subflows[id] || groups[id]; var existing = allNodes.getNode(id) || configNodes[id] || workspaces[id] || subflows[id] || groups[id];
if (existing) { if (existing) {
existingNodes.push({existing:existing, imported:n}); existingNodes.push({existing:existing, imported:n});
} }
@ -1387,7 +1679,8 @@ RED.nodes = (function() {
type: "tab", type: "tab",
disabled: false, disabled: false,
label: RED._("clipboard.recoveredNodes"), label: RED._("clipboard.recoveredNodes"),
info: RED._("clipboard.recoveredNodesInfo") info: RED._("clipboard.recoveredNodesInfo"),
env: []
} }
addWorkspace(recoveryWorkspace); addWorkspace(recoveryWorkspace);
RED.workspaces.add(recoveryWorkspace); RED.workspaces.add(recoveryWorkspace);
@ -1518,7 +1811,7 @@ RED.nodes = (function() {
// Add a tab if there isn't one there already // Add a tab if there isn't one there already
if (defaultWorkspace == null) { if (defaultWorkspace == null) {
defaultWorkspace = { type:"tab", id:getID(), disabled: false, info:"", label:RED._('workspace.defaultName',{number:1})}; defaultWorkspace = { type:"tab", id:getID(), disabled: false, info:"", label:RED._('workspace.defaultName',{number:1}), env:[]};
addWorkspace(defaultWorkspace); addWorkspace(defaultWorkspace);
RED.workspaces.add(defaultWorkspace); RED.workspaces.add(defaultWorkspace);
new_workspaces.push(defaultWorkspace); new_workspaces.push(defaultWorkspace);
@ -1979,32 +2272,9 @@ RED.nodes = (function() {
// TODO: supports filter.z|type // TODO: supports filter.z|type
function filterNodes(filter) { function filterNodes(filter) {
var result = []; return allNodes.filterNodes(filter);
var searchSet = null;
var doZFilter = false;
if (filter.hasOwnProperty("z")) {
if (nodeTabMap.hasOwnProperty(filter.z)) {
searchSet = Object.keys(nodeTabMap[filter.z]);
} else {
doZFilter = true;
}
}
if (searchSet === null) {
searchSet = Object.keys(nodes);
}
for (var n=0;n<searchSet.length;n++) {
var node = nodes[searchSet[n]];
if (filter.hasOwnProperty("type") && node.type !== filter.type) {
continue;
}
if (doZFilter && node.z !== filter.z) {
continue;
}
result.push(node);
}
return result;
} }
function filterLinks(filter) { function filterLinks(filter) {
var result = []; var result = [];
var candidateLinks = []; var candidateLinks = [];
@ -2092,9 +2362,8 @@ RED.nodes = (function() {
} }
function clear() { function clear() {
nodes = {}; allNodes.clear();
links = []; links = [];
nodeTabMap = {};
linkTabMap = {}; linkTabMap = {};
nodeLinks = {}; nodeLinks = {};
configNodes = {}; configNodes = {};
@ -2186,10 +2455,7 @@ RED.nodes = (function() {
if (configNodes.hasOwnProperty(n.id)) { if (configNodes.hasOwnProperty(n.id)) {
delete configNodes[n.id]; delete configNodes[n.id];
} else { } else {
delete nodes[n.id]; allNodes.removeNode(n);
if (nodeTabMap[n.z]) {
delete nodeTabMap[n.z][n.id];
}
} }
reimportList.push(convertNode(n)); reimportList.push(convertNode(n));
RED.events.emit('nodes:remove',n); RED.events.emit('nodes:remove',n);
@ -2246,6 +2512,13 @@ RED.nodes = (function() {
remove: removeNode, remove: removeNode,
clear: clear, clear: clear,
moveNodesForwards: moveNodesForwards,
moveNodesBackwards: moveNodesBackwards,
moveNodesToFront: moveNodesToFront,
moveNodesToBack: moveNodesToBack,
getNodeOrder: getNodeOrder,
setNodeOrder: setNodeOrder,
moveNodeToTab: moveNodeToTab, moveNodeToTab: moveNodeToTab,
addLink: addLink, addLink: addLink,
@ -2265,16 +2538,10 @@ RED.nodes = (function() {
addGroup: addGroup, addGroup: addGroup,
removeGroup: removeGroup, removeGroup: removeGroup,
group: function(id) { return groups[id] }, group: function(id) { return groups[id] },
groups: function(z) { return groupsByZ[z]||[] }, groups: function(z) { return groupsByZ[z]?groupsByZ[z].slice():[] },
eachNode: function(cb) { eachNode: function(cb) {
for (var id in nodes) { allNodes.eachNode(cb);
if (nodes.hasOwnProperty(id)) {
if (cb(nodes[id]) === false) {
break;
}
}
}
}, },
eachLink: function(cb) { eachLink: function(cb) {
for (var l=0;l<links.length;l++) { for (var l=0;l<links.length;l++) {

View File

@ -559,6 +559,22 @@ var RED = (function() {
{id:"menu-item-projects-settings",label:RED._("menu.label.projects-settings"),disabled:false,onselect:"core:show-project-settings"} {id:"menu-item-projects-settings",label:RED._("menu.label.projects-settings"),disabled:false,onselect:"core:show-project-settings"}
]}); ]});
} }
menuOptions.push({id:"menu-item-edit-menu", label:"Edit", options: [
{id: "menu-item-edit-undo", label:RED._("keyboard.undoChange"), disabled: true, onselect: "core:undo"},
{id: "menu-item-edit-redo", label:RED._("keyboard.redoChange"), disabled: true, onselect: "core:redo"},
null,
{id: "menu-item-edit-cut", label:RED._("keyboard.cutNode"), onselect: "core:cut-selection-to-internal-clipboard"},
{id: "menu-item-edit-copy", label:RED._("keyboard.copyNode"), onselect: "core:copy-selection-to-internal-clipboard"},
{id: "menu-item-edit-paste", label:RED._("keyboard.pasteNode"), disabled: true, onselect: "core:paste-from-internal-clipboard"},
null,
{id: "menu-item-edit-copy-group-style", label:RED._("keyboard.copyGroupStyle"), onselect: "core:copy-group-style"},
{id: "menu-item-edit-paste-group-style", label:RED._("keyboard.pasteGroupStyle"), disabled: true, onselect: "core:paste-group-style"},
null,
{id: "menu-item-edit-select-all", label:RED._("keyboard.selectAll"), onselect: "core:select-all-nodes"},
{id: "menu-item-edit-select-connected", label:RED._("keyboard.selectAllConnected"), onselect: "core:select-connected-nodes"},
{id: "menu-item-edit-select-none", label:RED._("keyboard.selectNone"), onselect: "core:select-none"}
]});
menuOptions.push({id:"menu-item-view-menu",label:RED._("menu.label.view.view"),options:[ menuOptions.push({id:"menu-item-view-menu",label:RED._("menu.label.view.view"),options:[
{id:"menu-item-palette",label:RED._("menu.label.palette.show"),toggle:true,onselect:"core:toggle-palette", selected: true}, {id:"menu-item-palette",label:RED._("menu.label.palette.show"),toggle:true,onselect:"core:toggle-palette", selected: true},
{id:"menu-item-sidebar",label:RED._("menu.label.sidebar.show"),toggle:true,onselect:"core:toggle-sidebar", selected: true}, {id:"menu-item-sidebar",label:RED._("menu.label.sidebar.show"),toggle:true,onselect:"core:toggle-sidebar", selected: true},
@ -566,6 +582,25 @@ var RED = (function() {
{id:"menu-item-action-list",label:RED._("keyboard.actionList"),onselect:"core:show-action-list"}, {id:"menu-item-action-list",label:RED._("keyboard.actionList"),onselect:"core:show-action-list"},
null null
]}); ]});
menuOptions.push({id:"menu-item-arrange-menu", label:RED._("menu.label.arrange"), options: [
{id: "menu-item-view-tools-move-to-back", label:RED._("menu.label.moveToBack"), disabled: true, onselect: "core:move-selection-to-back"},
{id: "menu-item-view-tools-move-to-front", label:RED._("menu.label.moveToFront"), disabled: true, onselect: "core:move-selection-to-front"},
{id: "menu-item-view-tools-move-backwards", label:RED._("menu.label.moveBackwards"), disabled: true, onselect: "core:move-selection-backwards"},
{id: "menu-item-view-tools-move-forwards", label:RED._("menu.label.moveForwards"), disabled: true, onselect: "core:move-selection-forwards"},
null,
{id: "menu-item-view-tools-align-left", label:RED._("menu.label.alignLeft"), disabled: true, onselect: "core:align-selection-to-left"},
{id: "menu-item-view-tools-align-center", label:RED._("menu.label.alignCenter"), disabled: true, onselect: "core:align-selection-to-center"},
{id: "menu-item-view-tools-align-right", label:RED._("menu.label.alignRight"), disabled: true, onselect: "core:align-selection-to-right"},
null,
{id: "menu-item-view-tools-align-top", label:RED._("menu.label.alignTop"), disabled: true, onselect: "core:align-selection-to-top"},
{id: "menu-item-view-tools-align-middle", label:RED._("menu.label.alignMiddle"), disabled: true, onselect: "core:align-selection-to-middle"},
{id: "menu-item-view-tools-align-bottom", label:RED._("menu.label.alignBottom"), disabled: true, onselect: "core:align-selection-to-bottom"},
null,
{id: "menu-item-view-tools-distribute-horizontally", label:RED._("menu.label.distributeHorizontally"), disabled: true, onselect: "core:distribute-selection-horizontally"},
{id: "menu-item-view-tools-distribute-veritcally", label:RED._("menu.label.distributeVertically"), disabled: true, onselect: "core:distribute-selection-vertically"}
]});
menuOptions.push(null); menuOptions.push(null);
if (RED.settings.theme("menu.menu-item-import-library", true)) { if (RED.settings.theme("menu.menu-item-import-library", true)) {
menuOptions.push({id: "menu-item-import", label: RED._("menu.label.import"), onselect: "core:show-import-dialog"}); menuOptions.push({id: "menu-item-import", label: RED._("menu.label.import"), onselect: "core:show-import-dialog"});
@ -626,7 +661,6 @@ var RED = (function() {
RED.user.init(); RED.user.init();
RED.notifications.init(); RED.notifications.init();
RED.library.init(); RED.library.init();
RED.keyboard.init();
RED.palette.init(); RED.palette.init();
RED.eventLog.init(); RED.eventLog.init();
@ -655,7 +689,7 @@ var RED = (function() {
RED.deploy.init(RED.settings.theme("deployButton",null)); RED.deploy.init(RED.settings.theme("deployButton",null));
buildMainMenu(); RED.keyboard.init(buildMainMenu);
RED.nodes.init(); RED.nodes.init();
RED.comms.connect(); RED.comms.connect();

View File

@ -88,6 +88,13 @@ RED.menu = (function() {
linkContent += '</a>'; linkContent += '</a>';
var link = $(linkContent).appendTo(item); var link = $(linkContent).appendTo(item);
opt.link = link;
if (typeof opt.onselect === 'string') {
var shortcut = RED.keyboard.getShortcut(opt.onselect);
if (shortcut && shortcut.key) {
opt.shortcutSpan = $('<span class="red-ui-popover-key">'+RED.keyboard.formatKey(shortcut.key, true)+'</span>').appendTo(link);
}
}
menuItems[opt.id] = opt; menuItems[opt.id] = opt;
@ -276,6 +283,22 @@ RED.menu = (function() {
} }
} }
function refreshShortcuts() {
for (var id in menuItems) {
if (menuItems.hasOwnProperty(id)) {
var opt = menuItems[id];
if (typeof opt.onselect === "string" && opt.shortcutSpan) {
opt.shortcutSpan.remove();
delete opt.shortcutSpan;
var shortcut = RED.keyboard.getShortcut(opt.onselect);
if (shortcut && shortcut.key) {
opt.shortcutSpan = $('<span class="red-ui-popover-key">'+RED.keyboard.formatKey(shortcut.key, true)+'</span>').appendTo(opt.link);
}
}
}
}
}
return { return {
init: createMenu, init: createMenu,
setSelected: setSelected, setSelected: setSelected,
@ -284,6 +307,7 @@ RED.menu = (function() {
setDisabled: setDisabled, setDisabled: setDisabled,
addItem: addItem, addItem: addItem,
removeItem: removeItem, removeItem: removeItem,
setAction: setAction setAction: setAction,
refreshShortcuts: refreshShortcuts
} }
})(); })();

View File

@ -664,6 +664,8 @@ RED.tabs = (function() {
link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); }) link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); })
$('<span class="red-ui-tabs-fade"></span>').appendTo(li);
if (tab.closeable) { if (tab.closeable) {
li.addClass("red-ui-tabs-closeable") li.addClass("red-ui-tabs-closeable")
var closeLink = $("<a/>",{href:"#",class:"red-ui-tab-close"}).appendTo(li); var closeLink = $("<a/>",{href:"#",class:"red-ui-tab-close"}).appendTo(li);
@ -674,8 +676,6 @@ RED.tabs = (function() {
}); });
} }
$('<span class="red-ui-tabs-fade"></span>').appendTo(li);
var badges = $('<span class="red-ui-tabs-badges"></span>').appendTo(li); var badges = $('<span class="red-ui-tabs-badges"></span>').appendTo(li);
if (options.onselect) { if (options.onselect) {
$('<i class="red-ui-tabs-badge-changed fa fa-circle"></i>').appendTo(badges); $('<i class="red-ui-tabs-badge-changed fa fa-circle"></i>').appendTo(badges);

File diff suppressed because it is too large Load Diff

View File

@ -31,7 +31,7 @@
function setMode(mode, cb) function setMode(mode, cb)
function getRange(); function getRange();
function replace(range, text) function replace(range, text)
function selectAll function selectAll
function clearSelection function clearSelection
function getSelectedText() function getSelectedText()
function destroy() function destroy()
@ -154,9 +154,9 @@ RED.editor.codeEditor.monaco = (function() {
function init(options) { function init(options) {
//Handles orphaned models //Handles orphaned models
//ensure loaded models that are not explicitly destroyed by a call to .destroy() are disposed //ensure loaded models that are not explicitly destroyed by a call to .destroy() are disposed
RED.events.on("editor:close",function() { RED.events.on("editor:close",function() {
let models = window.monaco ? monaco.editor.getModels() : null; let models = window.monaco ? monaco.editor.getModels() : null;
if(models && models.length) { if(models && models.length) {
console.warn("Cleaning up monaco models left behind. Any node that calls createEditor() should call .destroy().") console.warn("Cleaning up monaco models left behind. Any node that calls createEditor() should call .destroy().")
@ -765,10 +765,10 @@ RED.editor.codeEditor.monaco = (function() {
//by default, set javascript editors to text mode. //by default, set javascript editors to text mode.
//when element becomes visible, it will be (re) set to javascript mode //when element becomes visible, it will be (re) set to javascript mode
//this is to ensure multiple editors sharing the model dont present its //this is to ensure multiple editors sharing the model dont present its
//consts & lets to each other //consts & lets to each other
if(editorOptions.language == "javascript") { if(editorOptions.language == "javascript") {
editorOptions._language = editorOptions.language; editorOptions._language = editorOptions.language;
editorOptions.language = "text" editorOptions.language = "text"
} }
@ -942,6 +942,15 @@ RED.editor.codeEditor.monaco = (function() {
/*********** Create the monaco editor ***************/ /*********** Create the monaco editor ***************/
var ed = monaco.editor.create(el, editorOptions); var ed = monaco.editor.create(el, editorOptions);
//Unbind ctrl-Enter (default action is to insert a newline in editor) This permits the shortcut to close the tray.
try {
ed._standaloneKeybindingService.addDynamicKeybinding(
'-editor.action.insertLineAfter', // command ID prefixed by '-'
null, // keybinding
() => {} // need to pass an empty handler
);
} catch (error) { }
ed.nodered = { ed.nodered = {
refreshModuleLibs: refreshModuleLibs //expose this for function node externalModules refresh refreshModuleLibs: refreshModuleLibs //expose this for function node externalModules refresh
} }
@ -988,7 +997,7 @@ RED.editor.codeEditor.monaco = (function() {
if (cb && typeof cb == "function") { if (cb && typeof cb == "function") {
cb(); cb();
} }
if(resize) { if(resize) {
this.resize(); //cause a call to layout() this.resize(); //cause a call to layout()
} }
} }
@ -1242,7 +1251,7 @@ RED.editor.codeEditor.monaco = (function() {
} }
ed._mode = editorOptions.language; ed._mode = editorOptions.language;
//as models are signleton, consts and let are avialable to other javascript instances //as models are signleton, consts and let are avialable to other javascript instances
//so when not focused, set editor mode to text temporarily to avoid multiple defs //so when not focused, set editor mode to text temporarily to avoid multiple defs
if(editorOptions._language) { if(editorOptions._language) {
@ -1264,15 +1273,15 @@ RED.editor.codeEditor.monaco = (function() {
try { try {
var options = { var options = {
root: $(element).closest("div.red-ui-tray-content")[0] || document, root: $(element).closest("div.red-ui-tray-content")[0] || document,
attributes: true, attributes: true,
childList: true, childList: true,
}; };
var observer = new IntersectionObserver(function(entries, observer) { var observer = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) { entries.forEach(function(entry) {
callback(entry.intersectionRatio > 0, 5, entry.target); callback(entry.intersectionRatio > 0, 5, entry.target);
}); });
}, options); }, options);
observer.observe(element); observer.observe(element);
} catch (e1) { } catch (e1) {
//browser not supporting IntersectionObserver? then fall back to polling! //browser not supporting IntersectionObserver? then fall back to polling!
try { try {
@ -1291,7 +1300,7 @@ RED.editor.codeEditor.monaco = (function() {
function onVisibilityChange(visible, delay, element) { function onVisibilityChange(visible, delay, element) {
if(visible) { if(visible) {
if(ed._mode == "javascript" && ed._tempMode == "text") { if(ed._mode == "javascript" && ed._tempMode == "text") {
ed._tempMode = ""; ed._tempMode = "";
setTimeout(function() { setTimeout(function() {
if(element.parentElement) { //ensure el is still in DOM if(element.parentElement) { //ensure el is still in DOM
ed.setMode('javascript', undefined, false); ed.setMode('javascript', undefined, false);
@ -1301,7 +1310,7 @@ RED.editor.codeEditor.monaco = (function() {
} else if(ed._mode == "javascript" && ed._tempMode != "text") { } else if(ed._mode == "javascript" && ed._tempMode != "text") {
if(element.parentElement) { //ensure el is still in DOM if(element.parentElement) { //ensure el is still in DOM
ed.setMode('text', undefined, false); ed.setMode('text', undefined, false);
ed._tempMode = "text"; ed._tempMode = "text";
} }
} }
} }
@ -1377,4 +1386,4 @@ RED.editor.codeEditor.monaco = (function() {
*/ */
create: create create: create
} }
})(); })();

View File

@ -1,4 +1,4 @@
RED.colorPicker = (function() { RED.editor.colorPicker = RED.colorPicker = (function() {
function create(options) { function create(options) {
var color = options.value; var color = options.value;

View File

@ -0,0 +1,616 @@
RED.editor.envVarList = (function() {
var currentLocale = 'en-US';
var DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env'];
var DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred'];
/**
* Create env var edit interface
* @param container - container
* @param node - subflow node
*/
function buildPropertiesList(envContainer, node) {
var isTemplateNode = (node.type === "subflow");
envContainer
.css({
'min-height':'150px',
'min-width':'450px'
})
.editableList({
header: isTemplateNode?$('<div><div><div></div><div data-i18n="common.label.name"></div><div data-i18n="editor-tab.defaultValue"></div><div></div></div></div>'):undefined,
addItem: function(container, i, opt) {
// If this is an instance node, these are properties unique to
// this instance - ie opt.parent will not be defined.
if (isTemplateNode) {
container.addClass("red-ui-editor-subflow-env-editable")
}
var envRow = $('<div/>').appendTo(container);
var nameField = null;
var valueField = null;
nameField = $('<input/>', {
class: "node-input-env-name",
type: "text",
placeholder: RED._("common.label.name")
}).attr("autocomplete","disable").appendTo(envRow).val(opt.name);
valueField = $('<input/>',{
style: "width:100%",
class: "node-input-env-value",
type: "text",
}).attr("autocomplete","disable").appendTo(envRow)
valueField.typedInput({default:'str',types:isTemplateNode?DEFAULT_ENV_TYPE_LIST:DEFAULT_ENV_TYPE_LIST_INC_CRED});
valueField.typedInput('type', opt.type);
if (opt.type === "cred") {
if (opt.value) {
valueField.typedInput('value', opt.value);
} else if (node.credentials && node.credentials[opt.name]) {
valueField.typedInput('value', node.credentials[opt.name]);
} else if (node.credentials && node.credentials['has_'+opt.name]) {
valueField.typedInput('value', "__PWRD__");
} else {
valueField.typedInput('value', "");
}
} else {
valueField.typedInput('value', opt.value);
}
opt.nameField = nameField;
opt.valueField = valueField;
var actionButton = $('<a/>',{href:"#",class:"red-ui-editableList-item-remove red-ui-button red-ui-button-small"}).appendTo(envRow);
$('<i/>',{class:"fa "+(opt.parent?"fa-reply":"fa-remove")}).appendTo(actionButton);
var removeTip = RED.popover.tooltip(actionButton,RED._("subflow.env.remove"));
actionButton.on("click", function(evt) {
evt.preventDefault();
removeTip.close();
container.parent().addClass("red-ui-editableList-item-deleting")
container.fadeOut(300, function() {
envContainer.editableList('removeItem',opt);
});
});
if (isTemplateNode) {
// Add the UI customisation row
// if `opt.ui` does not exist, then apply defaults. If these
// defaults do not change then they will get stripped off
// before saving.
if (opt.type === 'cred') {
opt.ui = opt.ui || {
icon: "",
type: "cred"
}
opt.ui.type = "cred";
} else {
opt.ui = opt.ui || {
icon: "",
type: "input",
opts: {types:DEFAULT_ENV_TYPE_LIST}
}
}
opt.ui.label = opt.ui.label || {};
opt.ui.type = opt.ui.type || "input";
var uiRow = $('<div/>').appendTo(container).hide();
// save current info for reverting on cancel
// var copy = $.extend(true, {}, ui);
$('<a href="#"><i class="fa fa-angle-right"></a>').prependTo(envRow).on("click", function (evt) {
evt.preventDefault();
if ($(this).hasClass('expanded')) {
uiRow.slideUp();
$(this).removeClass('expanded');
} else {
uiRow.slideDown();
$(this).addClass('expanded');
}
});
buildEnvEditRow(uiRow, opt.ui, nameField, valueField);
nameField.trigger('change');
}
},
sortable: ".red-ui-editableList-item-handle",
removable: false
});
var parentEnv = {};
var envList = [];
if (/^subflow:/.test(node.type)) {
var subflowDef = RED.nodes.subflow(node.type.substring(8));
if (subflowDef.env) {
subflowDef.env.forEach(function(env) {
var item = {
name:env.name,
parent: {
type: env.type,
value: env.value,
ui: env.ui
}
}
envList.push(item);
parentEnv[env.name] = item;
})
}
}
if (node.env) {
for (var i = 0; i < node.env.length; i++) {
var env = node.env[i];
if (parentEnv.hasOwnProperty(env.name)) {
parentEnv[env.name].type = env.type;
parentEnv[env.name].value = env.value;
} else {
envList.push({
name: env.name,
type: env.type,
value: env.value,
ui: env.ui
});
}
}
}
envList.forEach(function(env) {
if (env.parent && env.parent.ui && env.parent.ui.type === 'hide') {
return;
}
if (!isTemplateNode && env.parent) {
return;
}
envContainer.editableList('addItem', JSON.parse(JSON.stringify(env)));
});
}
/**
* Create UI edit interface for environment variable
* @param container - container
* @param env - env var definition
* @param nameField - name field of env var
* @param valueField - value field of env var
*/
function buildEnvEditRow(container, ui, nameField, valueField) {
container.addClass("red-ui-editor-subflow-env-ui-row")
var topRow = $('<div></div>').appendTo(container);
$('<div></div>').appendTo(topRow);
$('<div>').text(RED._("editor.icon")).appendTo(topRow);
$('<div>').text(RED._("editor.label")).appendTo(topRow);
$('<div>').text(RED._("editor.inputType")).appendTo(topRow);
var row = $('<div></div>').appendTo(container);
$('<div><i class="red-ui-editableList-item-handle fa fa-bars"></i></div>').appendTo(row);
var typeOptions = {
'input': {types:DEFAULT_ENV_TYPE_LIST},
'select': {opts:[]},
'spinner': {},
'cred': {}
};
if (ui.opts) {
typeOptions[ui.type] = ui.opts;
} else {
// Pick up the default values if not otherwise provided
ui.opts = typeOptions[ui.type];
}
var iconCell = $('<div></div>').appendTo(row);
var iconButton = $('<a href="#"></a>').appendTo(iconCell);
iconButton.on("click", function(evt) {
evt.preventDefault();
var icon = ui.icon || "";
var iconPath = (icon ? RED.utils.separateIconPath(icon) : {});
RED.editor.iconPicker.show(iconButton, null, iconPath, true, function (newIcon) {
iconButton.empty();
var path = newIcon || "";
var newPath = RED.utils.separateIconPath(path);
if (newPath) {
$('<i class="fa"></i>').addClass(newPath.file).appendTo(iconButton);
}
ui.icon = path;
});
})
if (ui.icon) {
var newPath = RED.utils.separateIconPath(ui.icon);
$('<i class="fa '+newPath.file+'"></i>').appendTo(iconButton);
}
var labelCell = $('<div></div>').appendTo(row);
var label = ui.label && ui.label[currentLocale] || "";
var labelInput = $('<input type="text">').val(label).appendTo(labelCell);
ui.labelField = labelInput;
labelInput.on('change', function(evt) {
ui.label = ui.label || {};
var val = $(this).val().trim();
if (val === "") {
delete ui.label[currentLocale];
} else {
ui.label[currentLocale] = val;
}
})
var labelIcon = $('<span class="red-ui-editor-subflow-env-lang-icon"><i class="fa fa-language"></i></span>').appendTo(labelCell);
RED.popover.tooltip(labelIcon,function() {
var langs = Object.keys(ui.label);
var content = $("<div>");
if (langs.indexOf(currentLocale) === -1) {
langs.push(currentLocale);
langs.sort();
}
langs.forEach(function(l) {
var row = $('<div>').appendTo(content);
$('<span>').css({display:"inline-block",width:"120px"}).text(RED._("languages."+l)+(l===currentLocale?"*":"")).appendTo(row);
$('<span>').text(ui.label[l]||"").appendTo(row);
});
return content;
})
nameField.on('change',function(evt) {
labelInput.attr("placeholder",$(this).val())
});
var inputCell = $('<div></div>').appendTo(row);
var inputCellInput = $('<input type="text">').css("width","100%").appendTo(inputCell);
if (ui.type === "input") {
inputCellInput.val(ui.opts.types.join(","));
}
var checkbox;
var selectBox;
inputCellInput.typedInput({
types: [
{
value:"input",
label:RED._("editor.inputs.input"), icon:"fa fa-i-cursor",showLabel:false,multiple:true,options:[
{value:"str",label:RED._("editor.types.str"),icon:"red/images/typedInput/az.svg"},
{value:"num",label:RED._("editor.types.num"),icon:"red/images/typedInput/09.svg"},
{value:"bool",label:RED._("editor.types.bool"),icon:"red/images/typedInput/bool.svg"},
{value:"json",label:RED._("editor.types.json"),icon:"red/images/typedInput/json.svg"},
{value: "bin",label: RED._("editor.types.bin"),icon: "red/images/typedInput/bin.svg"},
{value: "env",label: RED._("editor.types.env"),icon: "red/images/typedInput/env.svg"},
{value: "cred",label: RED._("editor.types.cred"),icon: "fa fa-lock"}
],
default: DEFAULT_ENV_TYPE_LIST,
valueLabel: function(container,value) {
container.css("padding",0);
var innerContainer = $('<div class="red-ui-editor-subflow-env-input-type"></div>').appendTo(container);
var input = $('<div class="placeholder-input">').appendTo(innerContainer);
$('<span><i class="fa fa-i-cursor"></i></span>').appendTo(input);
if (value.length) {
value.forEach(function(v) {
if (!/^fa /.test(v.icon)) {
$('<img>',{src:v.icon,style:"max-width:14px; padding: 0 3px; margin-top:-4px; margin-left: 1px"}).appendTo(input);
} else {
var s = $('<span>',{style:"max-width:14px; padding: 0 3px; margin-top:-4px; margin-left: 1px"}).appendTo(input);
$("<i>",{class: v.icon}).appendTo(s);
}
})
} else {
$('<span class="red-ui-editor-subflow-env-input-type-placeholder"></span>').text(RED._("editor.selectType")).appendTo(input);
}
}
},
{
value: "cred",
label: RED._("typedInput.type.cred"), icon:"fa fa-lock", showLabel: false,
valueLabel: function(container,value) {
container.css("padding",0);
var innerContainer = $('<div class="red-ui-editor-subflow-env-input-type">').css({
"border-top-right-radius": "4px",
"border-bottom-right-radius": "4px"
}).appendTo(container);
$('<div class="placeholder-input">').html("&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;").appendTo(innerContainer);
}
},
{
value:"select",
label:RED._("editor.inputs.select"), icon:"fa fa-tasks",showLabel:false,
valueLabel: function(container,value) {
container.css("padding","0");
selectBox = $('<select></select>').appendTo(container);
if (ui.opts && Array.isArray(ui.opts.opts)) {
ui.opts.opts.forEach(function(o) {
var label = lookupLabel(o.l, o.l["en-US"]||o.v, currentLocale);
// $('<option>').val((o.t||'str')+":"+o.v).text(label).appendTo(selectBox);
$('<option>').val(o.v).text(label).appendTo(selectBox);
})
}
selectBox.on('change', function(evt) {
var v = selectBox.val();
// var parts = v.split(":");
// var t = parts.shift();
// v = parts.join(":");
//
// valueField.typedInput("type",'str')
valueField.typedInput("value",v)
});
selectBox.val(valueField.typedInput("value"));
// selectBox.val(valueField.typedInput('type')+":"+valueField.typedInput("value"));
},
expand: {
icon: "fa-caret-down",
minWidth: 400,
content: function(container) {
var content = $('<div class="red-ui-editor-subflow-ui-edit-panel">').appendTo(container);
var optList = $('<ol>').appendTo(content).editableList({
header:$("<div><div>"+RED._("editor.select.label")+"</div><div>"+RED._("editor.select.value")+"</div></div>"),
addItem: function(row,index,itemData) {
var labelDiv = $('<div>').appendTo(row);
var label = lookupLabel(itemData.l, "", currentLocale);
itemData.label = $('<input type="text">').val(label).appendTo(labelDiv);
itemData.label.on('keydown', function(evt) {
if (evt.keyCode === 13) {
itemData.input.focus();
evt.preventDefault();
}
});
var labelIcon = $('<span class="red-ui-editor-subflow-env-lang-icon"><i class="fa fa-language"></i></span>').appendTo(labelDiv);
RED.popover.tooltip(labelIcon,function() {
return currentLocale;
})
itemData.input = $('<input type="text">').val(itemData.v).appendTo(row);
// Problem using a TI here:
// - this is in a popout panel
// - clicking the expand button in the TI will close the parent edit tray
// and open the type editor.
// - but it leaves the popout panel over the top.
// - there is no way to get back to the popout panel after closing the type editor
//.typedInput({default:itemData.t||'str', types:DEFAULT_ENV_TYPE_LIST});
itemData.input.on('keydown', function(evt) {
if (evt.keyCode === 13) {
// Enter or Tab
var index = optList.editableList('indexOf',itemData);
var length = optList.editableList('length');
if (index + 1 === length) {
var newItem = {};
optList.editableList('addItem',newItem);
setTimeout(function() {
if (newItem.label) {
newItem.label.focus();
}
},100)
} else {
var nextItem = optList.editableList('getItemAt',index+1);
if (nextItem.label) {
nextItem.label.focus()
}
}
evt.preventDefault();
}
});
},
sortable: true,
removable: true,
height: 160
})
if (ui.opts.opts.length > 0) {
ui.opts.opts.forEach(function(o) {
optList.editableList('addItem',$.extend(true,{},o))
})
} else {
optList.editableList('addItem',{})
}
return {
onclose: function() {
var items = optList.editableList('items');
var vals = [];
items.each(function (i,el) {
var data = el.data('data');
var l = data.label.val().trim();
var v = data.input.val();
// var t = data.input.typedInput('type');
// var v = data.input.typedInput('value');
if (l.length > 0) {
data.l = data.l || {};
data.l[currentLocale] = l;
}
data.v = v;
if (l.length > 0 || v.length > 0) {
var val = {l:data.l,v:data.v};
// if (t !== 'str') {
// val.t = t;
// }
vals.push(val);
}
});
ui.opts.opts = vals;
inputCellInput.typedInput('value',Date.now())
}
}
}
}
},
{
value:"checkbox",
label:RED._("editor.inputs.checkbox"), icon:"fa fa-check-square-o",showLabel:false,
valueLabel: function(container,value) {
container.css("padding",0);
checkbox = $('<input type="checkbox">').appendTo(container);
checkbox.on('change', function(evt) {
valueField.typedInput('value',$(this).prop('checked')?"true":"false");
})
checkbox.prop('checked',valueField.typedInput('value')==="true");
}
},
{
value:"spinner",
label:RED._("editor.inputs.spinner"), icon:"fa fa-sort-numeric-asc", showLabel:false,
valueLabel: function(container,value) {
container.css("padding",0);
var innerContainer = $('<div class="red-ui-editor-subflow-env-input-type"></div>').appendTo(container);
var input = $('<div class="placeholder-input">').appendTo(innerContainer);
$('<span><i class="fa fa-sort-numeric-asc"></i></span>').appendTo(input);
var min = ui.opts && ui.opts.min;
var max = ui.opts && ui.opts.max;
var label = "";
if (min !== undefined && max !== undefined) {
label = Math.min(min,max)+" - "+Math.max(min,max);
} else if (min !== undefined) {
label = "> "+min;
} else if (max !== undefined) {
label = "< "+max;
}
$('<span>').css("margin-left","15px").text(label).appendTo(input);
},
expand: {
icon: "fa-caret-down",
content: function(container) {
var content = $('<div class="red-ui-editor-subflow-ui-edit-panel">').appendTo(container);
content.css("padding","8px 5px")
var min = ui.opts.min;
var max = ui.opts.max;
var minInput = $('<input type="number" style="margin-bottom:0; width:60px">');
minInput.val(min);
var maxInput = $('<input type="number" style="margin-bottom:0; width:60px">');
maxInput.val(max);
$('<div class="form-row" style="margin-bottom:3px"><label>'+RED._("editor.spinner.min")+'</label></div>').append(minInput).appendTo(content);
$('<div class="form-row" style="margin-bottom:0"><label>'+RED._("editor.spinner.max")+'</label></div>').append(maxInput).appendTo(content);
return {
onclose: function() {
var min = minInput.val().trim();
var max = maxInput.val().trim();
if (min !== "") {
ui.opts.min = parseInt(min);
} else {
delete ui.opts.min;
}
if (max !== "") {
ui.opts.max = parseInt(max);
} else {
delete ui.opts.max;
}
inputCellInput.typedInput('value',Date.now())
}
}
}
}
},
{
value:"none",
label:RED._("editor.inputs.none"), icon:"fa fa-times",hasValue:false
},
{
value:"hide",
label:RED._("editor.inputs.hidden"), icon:"fa fa-ban",hasValue:false
}
],
default: 'none'
}).on("typedinputtypechange", function(evt,type) {
ui.type = $(this).typedInput("type");
ui.opts = typeOptions[ui.type];
if (ui.type === 'input') {
// In the case of 'input' type, the typedInput uses the multiple-option
// mode. Its value needs to be set to a comma-separately list of the
// selected options.
inputCellInput.typedInput('value',ui.opts.types.join(","))
} else {
// No other type cares about `value`, but doing this will
// force a refresh of the label now that `ui.opts` has
// been updated.
inputCellInput.typedInput('value',Date.now())
}
switch (ui.type) {
case 'input':
valueField.typedInput('types',ui.opts.types);
break;
case 'select':
valueField.typedInput('types',['str']);
break;
case 'checkbox':
valueField.typedInput('types',['bool']);
break;
case 'spinner':
valueField.typedInput('types',['num']);
break;
case 'cred':
valueField.typedInput('types',['cred']);
break;
default:
valueField.typedInput('types',DEFAULT_ENV_TYPE_LIST)
}
if (ui.type === 'checkbox') {
valueField.typedInput('type','bool');
} else if (ui.type === 'spinner') {
valueField.typedInput('type','num');
}
if (ui.type !== 'checkbox') {
checkbox = null;
}
}).on("change", function(evt,type) {
if (ui.type === 'input') {
var types = inputCellInput.typedInput('value');
ui.opts.types = (types === "") ? ["str"] : types.split(",");
valueField.typedInput('types',ui.opts.types);
}
});
valueField.on("change", function(evt) {
if (checkbox) {
checkbox.prop('checked',$(this).typedInput('value')==="true")
}
})
// Set the input to the right type. This will trigger the 'typedinputtypechange'
// event handler (just above ^^) to update the value if needed
inputCellInput.typedInput('type',ui.type)
}
function setLocale(l, list) {
currentLocale = l;
if (list) {
var items = list.editableList("items");
items.each(function (i, item) {
var entry = $(this).data('data');
var labelField = entry.ui.labelField;
labelField.val(lookupLabel(entry.ui.label, "", currentLocale));
if (labelField.timeout) {
clearTimeout(labelField.timeout);
delete labelField.timeout;
}
labelField.addClass("input-updated");
labelField.timeout = setTimeout(function() {
delete labelField.timeout
labelField.removeClass("input-updated");
},3000);
});
}
}
/**
* Lookup text for specific locale
* @param labels - dict of labels
* @param defaultLabel - fallback label if not found
* @param locale - target locale
* @returns {string} text for specified locale
*/
function lookupLabel(labels, defaultLabel, locale) {
if (labels) {
if (labels[locale]) {
return labels[locale];
}
if (locale) {
var lang = locale.substring(0, 2);
if (labels[lang]) {
return labels[lang];
}
}
}
return defaultLabel;
}
return {
create: buildPropertiesList,
setLocale: setLocale,
lookupLabel: lookupLabel,
DEFAULT_ENV_TYPE_LIST: DEFAULT_ENV_TYPE_LIST,
DEFAULT_ENV_TYPE_LIST_INC_CRED: DEFAULT_ENV_TYPE_LIST_INC_CRED
}
})();

View File

@ -0,0 +1,99 @@
RED.editor.iconPicker = (function() {
function showIconPicker(container, backgroundColor, iconPath, faOnly, done) {
var picker = $('<div class="red-ui-icon-picker">');
var searchDiv = $("<div>",{class:"red-ui-search-container"}).appendTo(picker);
searchInput = $('<input type="text">').attr("placeholder",RED._("editor.searchIcons")).appendTo(searchDiv).searchBox({
delay: 50,
change: function() {
var searchTerm = $(this).val().trim();
if (searchTerm === "") {
iconList.find(".red-ui-icon-list-module").show();
iconList.find(".red-ui-icon-list-icon").show();
} else {
iconList.find(".red-ui-icon-list-module").hide();
iconList.find(".red-ui-icon-list-icon").each(function(i,n) {
if ($(n).data('icon').indexOf(searchTerm) === -1) {
$(n).hide();
} else {
$(n).show();
}
});
}
}
});
var row = $('<div>').appendTo(picker);
var iconList = $('<div class="red-ui-icon-list">').appendTo(picker);
var metaRow = $('<div class="red-ui-icon-meta"></div>').appendTo(picker);
var summary = $('<span>').appendTo(metaRow);
var resetButton = $('<button type="button" class="red-ui-button red-ui-button-small">'+RED._("editor.useDefault")+'</button>').appendTo(metaRow).on("click", function(e) {
e.preventDefault();
iconPanel.hide();
done(null);
});
if (!backgroundColor && faOnly) {
iconList.addClass("red-ui-icon-list-dark");
}
setTimeout(function() {
var iconSets = RED.nodes.getIconSets();
Object.keys(iconSets).forEach(function(moduleName) {
if (faOnly && (moduleName !== "font-awesome")) {
return;
}
var icons = iconSets[moduleName];
if (icons.length > 0) {
// selectIconModule.append($("<option></option>").val(moduleName).text(moduleName));
var header = $('<div class="red-ui-icon-list-module"></div>').text(moduleName).appendTo(iconList);
$('<i class="fa fa-cube"></i>').prependTo(header);
icons.forEach(function(icon) {
var iconDiv = $('<div>',{class:"red-ui-icon-list-icon"}).appendTo(iconList);
var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(iconDiv);
var icon_url = RED.settings.apiRootUrl+"icons/"+moduleName+"/"+icon;
iconDiv.data('icon',icon_url);
if (backgroundColor) {
nodeDiv.css({
'backgroundColor': backgroundColor
});
var borderColor = RED.utils.getDarkerColor(backgroundColor);
if (borderColor !== backgroundColor) {
nodeDiv.css('border-color',borderColor)
}
}
var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
RED.utils.createIconElement(icon_url, iconContainer, true);
if (iconPath.module === moduleName && iconPath.file === icon) {
iconDiv.addClass("selected");
}
iconDiv.on("mouseover", function() {
summary.text(icon);
})
iconDiv.on("mouseout", function() {
summary.html("&nbsp;");
})
iconDiv.on("click", function() {
iconPanel.hide();
done(moduleName+"/"+icon);
})
})
}
});
setTimeout(function() {
spinner.remove();
},50);
},300);
var spinner = RED.utils.addSpinnerOverlay(iconList,true);
var iconPanel = RED.popover.panel(picker);
iconPanel.show({
target: container
})
picker.slideDown(100);
searchInput.trigger("focus");
}
return {
show: showIconPicker
}
})();

View File

@ -0,0 +1,514 @@
;(function() {
RED.editor.registerEditPane("editor-tab-appearance", function(node) {
return {
label: RED._("editor-tab.appearance"),
name: RED._("editor-tab.appearance"),
iconClass: "fa fa-object-group",
create: function(container) {
this.content = container;
buildAppearanceForm(this.content,node);
if (node.type === 'subflow') {
this.defaultIcon = "node-red/subflow.svg";
} else {
var iconPath = RED.utils.getDefaultNodeIcon(node._def,node);
this.defaultIcon = iconPath.module+"/"+iconPath.file;
if (node.icon && node.icon !== this.defaultIcon) {
this.isDefaultIcon = false;
} else {
this.isDefaultIcon = true;
}
}
},
resize: function(size) {
},
close: function() {
},
show: function() {
refreshLabelForm(this.content, node);
},
apply: function(editState) {
if (updateLabels(node, editState.changes, editState.outputMap)) {
editState.changed = true;
}
if (!node._def.defaults || !node._def.defaults.hasOwnProperty("icon")) {
var icon = $("#red-ui-editor-node-icon").val()||""
if (!this.isDefaultIcon) {
if (icon !== node.icon) {
editState.changes.icon = node.icon;
node.icon = icon;
editState.changed = true;
}
} else {
if (icon !== "" && icon !== this.defaultIcon) {
editState.changes.icon = node.icon;
node.icon = icon;
editState.changed = true;
} else {
var iconPath = RED.utils.getDefaultNodeIcon(node._def, node);
var currentDefaultIcon = iconPath.module+"/"+iconPath.file;
if (this.defaultIcon !== currentDefaultIcon) {
editState.changes.icon = node.icon;
node.icon = currentDefaultIcon;
editState.changed = true;
}
}
}
}
if (node.type === "subflow") {
var newCategory = $("#subflow-appearance-input-category").val().trim();
if (newCategory === "_custom_") {
newCategory = $("#subflow-appearance-input-custom-category").val().trim();
if (newCategory === "") {
newCategory = node.category;
}
}
if (newCategory === 'subflows') {
newCategory = '';
}
if (newCategory != node.category) {
editState.changes['category'] = node.category;
node.category = newCategory;
editState.changed = true;
}
var oldColor = node.color;
var newColor = $("#red-ui-editor-node-color").val();
if (oldColor !== newColor) {
editState.changes.color = node.color;
node.color = newColor;
editState.changed = true;
RED.utils.clearNodeColorCache();
if (node.type === "subflow") {
var nodeDefinition = RED.nodes.getType(
"subflow:" + node.id
);
nodeDefinition["color"] = newColor;
}
}
}
if (!$("#node-input-show-label").prop('checked')) {
// Not checked - hide label
if (!/^link (in|out)$/.test(node.type)) {
// Not a link node - default state is true
if (node.l !== false) {
editState.changes.l = node.l
editState.changed = true;
}
node.l = false;
} else {
// A link node - default state is false
if (node.hasOwnProperty('l') && node.l) {
editState.changes.l = node.l
editState.changed = true;
}
delete node.l;
}
} else {
// Checked - show label
if (!/^link (in|out)$/.test(node.type)) {
// Not a link node - default state is true
if (node.hasOwnProperty('l') && !node.l) {
editState.changes.l = node.l
editState.changed = true;
}
delete node.l;
} else {
if (!node.l) {
editState.changes.l = node.l
editState.changed = true;
}
node.l = true;
}
}
}
}
});
function buildAppearanceForm(container,node) {
var dialogForm = $('<form class="dialog-form form-horizontal" autocomplete="off"></form>').appendTo(container);
var i,row;
if (node.type === "subflow") {
var categoryRow = $("<div/>", {
class: "form-row"
}).appendTo(dialogForm);
$("<label/>", {
for: "subflow-appearance-input-category",
"data-i18n": "editor:subflow.category"
}).appendTo(categoryRow);
var categorySelector = $("<select/>", {
id: "subflow-appearance-input-category"
}).css({
width: "250px"
}).appendTo(categoryRow);
$("<input/>", {
type: "text",
id: "subflow-appearance-input-custom-category"
}).css({
display: "none",
"margin-left": "10px",
width: "calc(100% - 250px)"
}).appendTo(categoryRow);
var categories = RED.palette.getCategories();
categories.sort(function(A,B) {
return A.label.localeCompare(B.label);
})
categories.forEach(function(cat) {
categorySelector.append($("<option/>").val(cat.id).text(cat.label));
})
categorySelector.append($("<option/>").attr('disabled',true).text("---"));
categorySelector.append($("<option/>").val("_custom_").text(RED._("palette.addCategory")));
$("#subflow-appearance-input-category").on("change", function() {
var val = $(this).val();
if (val === "_custom_") {
$("#subflow-appearance-input-category").width(120);
$("#subflow-appearance-input-custom-category").show();
} else {
$("#subflow-appearance-input-category").width(250);
$("#subflow-appearance-input-custom-category").hide();
}
})
$("#subflow-appearance-input-category").val(node.category||"subflows");
var userCount = 0;
var subflowType = "subflow:"+node.id;
// RED.nodes.eachNode(function(n) {
// if (n.type === subflowType) {
// userCount++;
// }
// });
$("#red-ui-editor-subflow-user-count")
.text(RED._("subflow.subflowInstances", {count:node.instances.length})).show();
}
$('<div class="form-row">'+
'<label for="node-input-show-label-btn" data-i18n="editor.label"></label>'+
'<span style="margin-right: 2px;"/>'+
'<input type="checkbox" id="node-input-show-label"/>'+
'</div>').appendTo(dialogForm);
$("#node-input-show-label").toggleButton({
enabledLabel: RED._("editor.show"),
disabledLabel: RED._("editor.hide")
})
if (!node.hasOwnProperty("l")) {
// Show label if type not link
node.l = !/^link (in|out)$/.test(node._def.type);
}
$("#node-input-show-label").prop("checked",node.l).trigger("change");
if (node.type === "subflow") {
// subflow template can select its color
var color = node.color ? node.color : "#DDAA99";
var colorRow = $("<div/>", {
class: "form-row"
}).appendTo(dialogForm);
$("<label/>").text(RED._("editor.color")).appendTo(colorRow);
var recommendedColors = [
"#DDAA99",
"#3FADB5", "#87A980", "#A6BBCF",
"#AAAA66", "#C0C0C0", "#C0DEED",
"#C7E9C0", "#D7D7A0", "#D8BFD8",
"#DAC4B4", "#DEB887", "#DEBD5C",
"#E2D96E", "#E6E0F8", "#E7E7AE",
"#E9967A", "#F3B567", "#FDD0A2",
"#FDF0C2", "#FFAAAA", "#FFCC66",
"#FFF0F0", "#FFFFFF"
]
RED.editor.colorPicker.create({
id: "red-ui-editor-node-color",
value: color,
palette: recommendedColors,
sortPalette: function (a, b) {return a.l - b.l;}
}).appendTo(colorRow);
$("#red-ui-editor-node-color").on('change', function(ev) {
// Horribly out of scope...
var colour = $(this).val();
nodeDiv.css('backgroundColor',colour);
var borderColor = RED.utils.getDarkerColor(colour);
if (borderColor !== colour) {
nodeDiv.css('border-color',borderColor)
}
})
}
// If a node has icon property in defaults, the icon of the node cannot be modified. (e.g, ui_button node in dashboard)
if ((!node._def.defaults || !node._def.defaults.hasOwnProperty("icon"))) {
var iconRow = $('<div class="form-row"></div>').appendTo(dialogForm);
$('<label data-i18n="editor.settingIcon">').appendTo(iconRow);
var iconButton = $('<button type="button" class="red-ui-button red-ui-editor-node-appearance-button">').appendTo(iconRow);
$('<i class="fa fa-caret-down"></i>').appendTo(iconButton);
var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(iconButton);
var colour = RED.utils.getNodeColor(node.type, node._def);
var icon_url = RED.utils.getNodeIcon(node._def,node);
nodeDiv.css('backgroundColor',colour);
var borderColor = RED.utils.getDarkerColor(colour);
if (borderColor !== colour) {
nodeDiv.css('border-color',borderColor)
}
var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
RED.utils.createIconElement(icon_url, iconContainer, true);
iconButton.on("click", function(e) {
e.preventDefault();
var iconPath;
var icon = $("#red-ui-editor-node-icon").val()||"";
if (icon) {
iconPath = RED.utils.separateIconPath(icon);
} else {
iconPath = RED.utils.getDefaultNodeIcon(node._def, node);
}
var backgroundColor = RED.utils.getNodeColor(node.type, node._def);
if (node.type === "subflow") {
backgroundColor = $("#red-ui-editor-node-color").val();
}
RED.editor.iconPicker.show(iconButton,backgroundColor,iconPath,false,function(newIcon) {
$("#red-ui-editor-node-icon").val(newIcon||"");
var icon_url = RED.utils.getNodeIcon(node._def,{type:node.type,icon:newIcon});
RED.utils.createIconElement(icon_url, iconContainer, true);
});
});
RED.popover.tooltip(iconButton, function() {
return $("#red-ui-editor-node-icon").val() || RED._("editor.default");
})
$('<input type="hidden" id="red-ui-editor-node-icon">').val(node.icon).appendTo(iconRow);
}
$('<div class="form-row"><span data-i18n="editor.portLabels"></span></div>').appendTo(dialogForm);
var inputCount = node.inputs || node._def.inputs || 0;
var outputCount = node.outputs || node._def.outputs || 0;
if (node.type === 'subflow') {
inputCount = node.in.length;
outputCount = node.out.length;
}
var inputLabels = node.inputLabels || [];
var outputLabels = node.outputLabels || [];
var inputPlaceholder = node._def.inputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel");
var outputPlaceholder = node._def.outputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel");
$('<div class="form-row"><span style="margin-left: 50px;" data-i18n="editor.labelInputs"></span><div id="red-ui-editor-node-label-form-inputs"></div></div>').appendTo(dialogForm);
var inputsDiv = $("#red-ui-editor-node-label-form-inputs");
if (inputCount > 0) {
for (i=0;i<inputCount;i++) {
buildLabelRow("input",i,inputLabels[i],inputPlaceholder).appendTo(inputsDiv);
}
} else {
buildLabelRow().appendTo(inputsDiv);
}
$('<div class="form-row"><span style="margin-left: 50px;" data-i18n="editor.labelOutputs"></span><div id="red-ui-editor-node-label-form-outputs"></div></div>').appendTo(dialogForm);
var outputsDiv = $("#red-ui-editor-node-label-form-outputs");
if (outputCount > 0) {
for (i=0;i<outputCount;i++) {
buildLabelRow("output",i,outputLabels[i],outputPlaceholder).appendTo(outputsDiv);
}
} else {
buildLabelRow().appendTo(outputsDiv);
}
}
function refreshLabelForm(container,node) {
var inputPlaceholder = node._def.inputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel");
var outputPlaceholder = node._def.outputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel");
var inputsDiv = $("#red-ui-editor-node-label-form-inputs");
var outputsDiv = $("#red-ui-editor-node-label-form-outputs");
var inputCount;
var formInputs = $("#node-input-inputs").val();
if (formInputs === undefined) {
if (node.type === 'subflow') {
inputCount = node.in.length;
} else {
inputCount = node.inputs || node._def.inputs || 0;
}
} else {
inputCount = Math.min(1,Math.max(0,parseInt(formInputs)));
if (isNaN(inputCount)) {
inputCount = 0;
}
}
var children = inputsDiv.children();
var childCount = children.length;
if (childCount === 1 && $(children[0]).hasClass('red-ui-editor-node-label-form-none')) {
childCount--;
}
if (childCount < inputCount) {
if (childCount === 0) {
// remove the 'none' placeholder
$(children[0]).remove();
}
for (i = childCount;i<inputCount;i++) {
buildLabelRow("input",i,"",inputPlaceholder).appendTo(inputsDiv);
}
} else if (childCount > inputCount) {
for (i=inputCount;i<childCount;i++) {
$(children[i]).remove();
}
if (inputCount === 0) {
buildLabelRow().appendTo(inputsDiv);
}
}
var outputCount;
var i;
var formOutputs = $("#node-input-outputs").val();
if (formOutputs === undefined) {
if (node.type === 'subflow') {
outputCount = node.out.length;
} else {
inputCount = node.outputs || node._def.outputs || 0;
}
} else if (isNaN(formOutputs)) {
var outputMap = JSON.parse(formOutputs);
var keys = Object.keys(outputMap);
children = outputsDiv.children();
childCount = children.length;
if (childCount === 1 && $(children[0]).hasClass('red-ui-editor-node-label-form-none')) {
childCount--;
}
outputCount = 0;
var rows = [];
keys.forEach(function(p) {
var row = $("#red-ui-editor-node-label-form-output-"+p).parent();
if (row.length === 0 && outputMap[p] !== -1) {
if (childCount === 0) {
$(children[0]).remove();
childCount = -1;
}
row = buildLabelRow("output",p,"",outputPlaceholder);
} else {
row.detach();
}
if (outputMap[p] !== -1) {
outputCount++;
rows.push({i:parseInt(outputMap[p]),r:row});
}
});
rows.sort(function(A,B) {
return A.i-B.i;
})
rows.forEach(function(r,i) {
r.r.find("label").text((i+1)+".");
r.r.appendTo(outputsDiv);
})
if (rows.length === 0) {
buildLabelRow("output",i,"").appendTo(outputsDiv);
} else {
}
} else {
outputCount = Math.max(0,parseInt(formOutputs));
}
children = outputsDiv.children();
childCount = children.length;
if (childCount === 1 && $(children[0]).hasClass('red-ui-editor-node-label-form-none')) {
childCount--;
}
if (childCount < outputCount) {
if (childCount === 0) {
// remove the 'none' placeholder
$(children[0]).remove();
}
for (i = childCount;i<outputCount;i++) {
buildLabelRow("output",i,"").appendTo(outputsDiv);
}
} else if (childCount > outputCount) {
for (i=outputCount;i<childCount;i++) {
$(children[i]).remove();
}
if (outputCount === 0) {
buildLabelRow().appendTo(outputsDiv);
}
}
}
function buildLabelRow(type, index, value, placeHolder) {
var result = $('<div>',{class:"red-ui-editor-node-label-form-row"});
if (type === undefined) {
$('<span>').text(RED._("editor.noDefaultLabel")).appendTo(result);
result.addClass("red-ui-editor-node-label-form-none");
} else {
result.addClass("");
var id = "red-ui-editor-node-label-form-"+type+"-"+index;
$('<label>',{for:id}).text((index+1)+".").appendTo(result);
var input = $('<input>',{type:"text",id:id, placeholder: placeHolder}).val(value).appendTo(result);
var clear = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-times"></i></button>').appendTo(result);
clear.on("click", function(evt) {
evt.preventDefault();
input.val("");
})
}
return result;
}
function updateLabels(node, changes, outputMap) {
var inputLabels = $("#red-ui-editor-node-label-form-inputs").children().find("input");
var outputLabels = $("#red-ui-editor-node-label-form-outputs").children().find("input");
var hasNonBlankLabel = false;
var changed = false;
var newValue = inputLabels.map(function() {
var v = $(this).val();
hasNonBlankLabel = hasNonBlankLabel || v!== "";
return v;
}).toArray().slice(0,node.inputs);
if ((node.inputLabels === undefined && hasNonBlankLabel) ||
(node.inputLabels !== undefined && JSON.stringify(newValue) !== JSON.stringify(node.inputLabels))) {
changes.inputLabels = node.inputLabels;
node.inputLabels = newValue;
changed = true;
}
hasNonBlankLabel = false;
newValue = new Array(node.outputs);
outputLabels.each(function() {
var index = $(this).attr('id').substring("red-ui-editor-node-label-form-output-".length);
if (outputMap && outputMap.hasOwnProperty(index)) {
index = parseInt(outputMap[index]);
if (index === -1) {
return;
}
}
var v = $(this).val();
hasNonBlankLabel = hasNonBlankLabel || v!== "";
newValue[index] = v;
});
if ((node.outputLabels === undefined && hasNonBlankLabel) ||
(node.outputLabels !== undefined && JSON.stringify(newValue) !== JSON.stringify(node.outputLabels))) {
changes.outputLabels = node.outputLabels;
node.outputLabels = newValue;
changed = true;
}
return changed;
}
})();

View File

@ -0,0 +1,70 @@
;(function() {
RED.editor.registerEditPane("editor-tab-description", function(node) {
return {
label: RED._("editor-tab.description"),
name: RED._("editor-tab.description"),
iconClass: "fa fa-file-text-o",
create: function(container) {
this.editor = buildDescriptionForm(container,node);
RED.e = this.editor;
},
resize: function(size) {
this.editor.resize();
},
close: function() {
this.editor.destroy();
this.editor = null;
},
show: function() {
this.editor.focus();
},
apply: function(editState) {
var oldInfo = node.info;
var newInfo = this.editor.getValue();
if (!!oldInfo) {
// Has existing info property
if (newInfo.trim() === "") {
// New value is blank - remove the property
editState.changed = true;
editState.changes.info = oldInfo;
delete node.info;
} else if (newInfo !== oldInfo) {
// New value is different
editState.changed = true;
editState.changes.info = oldInfo;
node.info = newInfo;
}
} else {
// No existing info
if (newInfo.trim() !== "") {
// New value is not blank
editState.changed = true;
editState.changes.info = undefined;
node.info = newInfo;
}
}
}
}
});
function buildDescriptionForm(container,node) {
var dialogForm = $('<form class="dialog-form form-horizontal" autocomplete="off"></form>').appendTo(container);
var toolbarRow = $('<div></div>').appendTo(dialogForm);
var row = $('<div class="form-row node-text-editor-row" style="position:relative; padding-top: 4px; height: 100%"></div>').appendTo(dialogForm);
var editorId = "node-info-input-info-editor-"+Math.floor(1000*Math.random());
$('<div style="height: 100%" class="node-text-editor" id="'+editorId+'" ></div>').appendTo(row);
var nodeInfoEditor = RED.editor.createEditor({
id: editorId,
mode: 'ace/mode/markdown',
value: ""
});
if (node.info) {
nodeInfoEditor.getSession().setValue(node.info, -1);
}
node.infoEditor = nodeInfoEditor;
return nodeInfoEditor;
}
})();

View File

@ -0,0 +1,69 @@
;(function() {
RED.editor.registerEditPane("editor-tab-envProperties", function(node) {
return {
label: RED._("editor-tab.envProperties"),
name: RED._("editor-tab.envProperties"),
iconClass: "fa fa-list",
create: function(container) {
var form = $('<form class="dialog-form form-horizontal"></form>').appendTo(container);
var listContainer = $('<div class="form-row node-input-env-container-row"></div>').appendTo(form);
this.list = $('<ol></ol>').appendTo(listContainer);
RED.editor.envVarList.create(this.list, node);
},
resize: function(size) {
this.list.editableList('height',size.height);
},
close: function() {
},
apply: function(editState) {
var old_env = node.env;
var new_env = [];
if (/^subflow:/.test(node.type)) {
new_env = RED.subflow.exportSubflowInstanceEnv(node);
}
// Get the values from the Properties table tab
var items = this.list.editableList('items');
items.each(function (i,el) {
var data = el.data('data');
var item;
if (data.nameField && data.valueField) {
item = {
name: data.nameField.val(),
value: data.valueField.typedInput("value"),
type: data.valueField.typedInput("type")
}
if (item.name.trim() !== "") {
new_env.push(item);
}
}
});
if (new_env && new_env.length > 0) {
new_env.forEach(function(prop) {
if (prop.type === "cred") {
node.credentials = node.credentials || {_:{}};
node.credentials[prop.name] = prop.value;
node.credentials['has_'+prop.name] = (prop.value !== "");
if (prop.value !== '__PWRD__') {
editState.changed = true;
}
delete prop.value;
}
});
}
if (!isSameObj(old_env, new_env)) {
node.env = new_env;
editState.changes.env = node.env;
editState.changed = true;
}
}
}
});
function isSameObj(env0, env1) {
return (JSON.stringify(env0) === JSON.stringify(env1));
}
})();

View File

@ -0,0 +1,60 @@
;(function() {
RED.editor.registerEditPane("editor-tab-flow-properties", function(node) {
return {
label: RED._("editor-tab.properties"),
name: RED._("editor-tab.properties"),
iconClass: "fa fa-cog",
create: function(container) {
var dialogForm = $('<form id="dialog-form" class="form-horizontal"></form>').appendTo(container);
$('<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" data-i18n="[placeholder]common.label.name">'+
'</div>').appendTo(dialogForm);
var row = $('<div class="form-row node-text-editor-row">'+
'<label for="node-input-info" data-i18n="editor:workspace.info" style="width:300px;"></label>'+
'<div style="min-height:150px;" class="node-text-editor" id="node-input-info"></div>'+
'</div>').appendTo(dialogForm);
this.tabflowEditor = RED.editor.createEditor({
id: 'node-input-info',
mode: 'ace/mode/markdown',
value: ""
});
$('<input type="text" style="display: none;" />').prependTo(dialogForm);
dialogForm.on("submit", function(e) { e.preventDefault();});
$("#node-input-name").val(node.label);
RED.text.bidi.prepareInput($("#node-input-name"));
this.tabflowEditor.getSession().setValue(node.info || "", -1);
},
resize: function(size) {
$("#node-input-info").css("height", (size.height-70)+"px");
this.tabflowEditor.resize();
},
close: function() {
this.tabflowEditor.destroy();
},
apply: function(editState) {
var label = $( "#node-input-name" ).val();
if (node.label != label) {
editState.changes.label = node.label;
editState.changed = true;
node.label = label;
}
var info = this.tabflowEditor.getValue();
if (node.info !== info) {
editState.changes.info = node.info;
editState.changed = true;
node.info = info;
}
$("#red-ui-tab-"+(node.id.replace(".","-"))).toggleClass('red-ui-workspace-disabled',!!node.disabled);
$("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!node.disabled);
}
}
});
})();

View File

@ -0,0 +1,181 @@
;(function() {
RED.editor.registerEditPane("editor-tab-properties", function(node) {
return {
label: RED._("editor-tab.properties"),
name: RED._("editor-tab.properties"),
iconClass: "fa fa-cog",
create: function(container) {
var nodeType = node.type;
if (node.type === "subflow") {
nodeType = "subflow-template";
} else if (node.type.substring(0,8) == "subflow:") {
nodeType = "subflow";
}
var i18nNamespace;
if (node._def.set.module === "node-red") {
i18nNamespace = "node-red";
} else {
i18nNamespace = node._def.set.id;
}
var formStyle = "dialog-form";
this.inputClass = "node-input";
if (node._def.category === "config" && nodeType !== "group") {
this.inputClass = "node-config-input";
formStyle = "node-config-dialog-edit-form";
}
RED.editor.buildEditForm(container,formStyle,nodeType,i18nNamespace,node);
},
resize: function(size) {
if (node && node._def.oneditresize) {
try {
node._def.oneditresize.call(node,size);
} catch(err) {
console.log("oneditresize",node.id,node.type,err.toString());
}
}
},
close: function() {
},
apply: function(editState) {
var newValue;
var d;
if (node._def.defaults) {
for (d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d)) {
var input = $("#"+this.inputClass+"-"+d);
if (input.attr('type') === "checkbox") {
newValue = input.prop('checked');
} else if (input.prop("nodeName") === "select" && input.attr("multiple") === "multiple") {
// An empty select-multiple box returns null.
// Need to treat that as an empty array.
newValue = input.val();
if (newValue == null) {
newValue = [];
}
} else if ("format" in node._def.defaults[d] && node._def.defaults[d].format !== "" && input[0].nodeName === "DIV") {
newValue = input.text();
} else {
newValue = input.val();
}
if (newValue != null) {
if (d === "outputs") {
if (newValue.trim() === "") {
continue;
}
if (isNaN(newValue)) {
editState.outputMap = JSON.parse(newValue);
var outputCount = 0;
var outputsChanged = false;
var keys = Object.keys(editState.outputMap);
keys.forEach(function(p) {
if (isNaN(p)) {
// New output;
outputCount ++;
delete editState.outputMap[p];
} else {
editState.outputMap[p] = editState.outputMap[p]+"";
if (editState.outputMap[p] !== "-1") {
outputCount++;
if (editState.outputMap[p] !== p) {
// Output moved
outputsChanged = true;
} else {
delete editState.outputMap[p];
}
} else {
// Output removed
outputsChanged = true;
}
}
});
newValue = outputCount;
if (outputsChanged) {
editState.changed = true;
}
} else {
newValue = parseInt(newValue);
}
}
if (node._def.defaults[d].type) {
if (newValue == "_ADD_") {
newValue = "";
}
}
if (node[d] != newValue) {
if (node._def.defaults[d].type) {
// Change to a related config node
var configNode = RED.nodes.node(node[d]);
if (configNode) {
var users = configNode.users;
users.splice(users.indexOf(node),1);
RED.events.emit("nodes:change",configNode);
}
configNode = RED.nodes.node(newValue);
if (configNode) {
configNode.users.push(node);
RED.events.emit("nodes:change",configNode);
}
}
editState.changes[d] = node[d];
node[d] = newValue;
editState.changed = true;
}
}
}
}
}
if (node._def.credentials) {
var credDefinition = node._def.credentials;
var credsChanged = updateNodeCredentials(node,credDefinition,this.inputClass);
editState.changed = editState.changed || credsChanged;
}
}
}
});
/**
* Update the node credentials from the edit form
* @param node - the node containing the credentials
* @param credDefinition - definition of the credentials
* @param prefix - prefix of the input fields
* @return {boolean} whether anything has changed
*/
function updateNodeCredentials(node, credDefinition, prefix) {
var changed = false;
if (!node.credentials) {
node.credentials = {_:{}};
} else if (!node.credentials._) {
node.credentials._ = {};
}
for (var cred in credDefinition) {
if (credDefinition.hasOwnProperty(cred)) {
var input = $("#" + prefix + '-' + cred);
if (input.length > 0) {
var value = input.val();
if (credDefinition[cred].type == 'password') {
node.credentials['has_' + cred] = (value !== "");
if (value == '__PWRD__') {
continue;
}
changed = true;
}
node.credentials[cred] = value;
if (value != node.credentials._[cred]) {
changed = true;
}
}
}
}
return changed;
}
})();

View File

@ -0,0 +1,179 @@
(function() {
var _subflowModulePaneTemplate = '<form class="dialog-form form-horizontal" autocomplete="off">'+
'<div class="form-row">'+
'<label for="subflow-input-module-module" data-i18n="[append]editor:subflow.module"><i class="fa fa-cube"></i> </label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-module" data-i18n="[placeholder]common.label.name">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-type" data-i18n="[append]editor:subflow.type"> </label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-type">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-version" data-i18n="[append]editor:subflow.version"></label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-version" data-i18n="[placeholder]editor:subflow.versionPlaceholder">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-desc" data-i18n="[append]editor:subflow.desc"></label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-desc">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-license" data-i18n="[append]editor:subflow.license"></label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-license">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-author" data-i18n="[append]editor:subflow.author"></label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-author" data-i18n="[placeholder]editor:subflow.authorPlaceholder">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-keywords" data-i18n="[append]editor:subflow.keys"></label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-keywords" data-i18n="[placeholder]editor:subflow.keysPlaceholder">'+
'</div>'+
'</form>';
RED.editor.registerEditPane("editor-tab-subflow-module", function(node) {
return {
label: RED._("editor-tab.module"),
name: RED._("editor-tab.module"),
iconClass: "fa fa-cube",
create: function(container) {
buildModuleForm(container, node);
},
resize: function(size) {
},
close: function() {
},
apply: function(editState) {
var newMeta = exportSubflowModuleProperties(node);
if (!isSameObj(node.meta,newMeta)) {
editState.changes.meta = node.meta;
node.meta = newMeta;
editState.changed = true;
}
}
}
});
function isSameObj(env0, env1) {
return (JSON.stringify(env0) === JSON.stringify(env1));
}
function setupInputValidation(input,validator) {
var errorTip;
var validateTimeout;
var validateFunction = function() {
if (validateTimeout) {
return;
}
validateTimeout = setTimeout(function() {
var error = validator(input.val());
// if (!error && errorTip) {
// errorTip.close();
// errorTip = null;
// } else if (error && !errorTip) {
// errorTip = RED.popover.create({
// tooltip: true,
// target:input,
// size: "small",
// direction: "bottom",
// content: error,
// }).open();
// }
input.toggleClass("input-error",!!error);
validateTimeout = null;
})
}
input.on("change keyup paste", validateFunction);
}
function buildModuleForm(container, node) {
$(_subflowModulePaneTemplate).appendTo(container);
var moduleProps = node.meta || {};
[
'module',
'type',
'version',
'author',
'desc',
'keywords',
'license'
].forEach(function(property) {
$("#subflow-input-module-"+property).val(moduleProps[property]||"")
})
$("#subflow-input-module-type").attr("placeholder",node.id);
setupInputValidation($("#subflow-input-module-module"), function(newValue) {
newValue = newValue.trim();
var isValid = newValue.length < 215;
isValid = isValid && !/^[._]/.test(newValue);
isValid = isValid && !/[A-Z]/.test(newValue);
if (newValue !== encodeURIComponent(newValue)) {
var m = /^@([^\/]+)\/([^\/]+)$/.exec(newValue);
if (m) {
isValid = isValid && (m[1] === encodeURIComponent(m[1]) && m[2] === encodeURIComponent(m[2]))
} else {
isValid = false;
}
}
return isValid?"":"Invalid module name"
})
setupInputValidation($("#subflow-input-module-version"), function(newValue) {
newValue = newValue.trim();
var isValid = newValue === "" ||
/^(\d|[1-9]\d*)\.(\d|[1-9]\d*)\.(\d|[1-9]\d*)(-(0|[1-9A-Za-z-][0-9A-Za-z-]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9A-Za-z-][0-9A-Za-z-]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/.test(newValue);
return isValid?"":"Invalid version number"
})
var licenses = ["none", "Apache-2.0", "BSD-3-Clause", "BSD-2-Clause", "GPL-2.0", "GPL-3.0", "MIT", "MPL-2.0", "CDDL-1.0", "EPL-2.0"];
var typedLicenses = {
types: licenses.map(function(l) {
return {
value: l,
label: l === "none" ? RED._("editor:subflow.licenseNone") : l,
hasValue: false
};
})
}
typedLicenses.types.push({
value:"_custom_", label:RED._("editor:subflow.licenseOther"), icon:"red/images/typedInput/az.svg"
})
if (!moduleProps.license) {
typedLicenses.default = "none";
} else if (licenses.indexOf(moduleProps.license) > -1) {
typedLicenses.default = moduleProps.license;
} else {
typedLicenses.default = "_custom_";
}
$("#subflow-input-module-license").typedInput(typedLicenses)
}
function exportSubflowModuleProperties(node) {
var value;
var moduleProps = {};
[
'module',
'type',
'version',
'author',
'desc',
'keywords'
].forEach(function(property) {
value = $("#subflow-input-module-"+property).val().trim();
if (value) {
moduleProps[property] = value;
}
})
var selectedLicenseType = $("#subflow-input-module-license").typedInput("type");
if (selectedLicenseType === '_custom_') {
value = $("#subflow-input-module-license").val();
if (value) {
moduleProps.license = value;
}
} else if (selectedLicenseType !== "none") {
moduleProps.license = selectedLicenseType;
}
return moduleProps;
}
})();

View File

@ -87,16 +87,18 @@ RED.group = (function() {
"label-position": "nw" "label-position": "nw"
}; };
var groupDef = { var groupDef = {
defaults:{ defaults:{
name:{value:""}, name:{value:""},
style:{value:{label:true}}, style:{value:{label:true}},
nodes:{value:[]} nodes:{value:[]},
env: {value:[]},
}, },
category: "config", category: "config",
oneditprepare: function() { oneditprepare: function() {
var style = this.style || {}; var style = this.style || {};
RED.colorPicker.create({ RED.editor.colorPicker.create({
id:"node-input-style-stroke", id:"node-input-style-stroke",
value: style.stroke || defaultGroupStyle.stroke || "#a4a4a4", value: style.stroke || defaultGroupStyle.stroke || "#a4a4a4",
palette: colorPalette, palette: colorPalette,
@ -107,7 +109,7 @@ RED.group = (function() {
none: true, none: true,
opacity: style.hasOwnProperty('stroke-opacity')?style['stroke-opacity']:(defaultGroupStyle.hasOwnProperty('stroke-opacity')?defaultGroupStyle['stroke-opacity']:1.0) opacity: style.hasOwnProperty('stroke-opacity')?style['stroke-opacity']:(defaultGroupStyle.hasOwnProperty('stroke-opacity')?defaultGroupStyle['stroke-opacity']:1.0)
}).appendTo("#node-input-row-style-stroke"); }).appendTo("#node-input-row-style-stroke");
RED.colorPicker.create({ RED.editor.colorPicker.create({
id:"node-input-style-fill", id:"node-input-style-fill",
value: style.fill || defaultGroupStyle.fill ||"none", value: style.fill || defaultGroupStyle.fill ||"none",
palette: colorPalette, palette: colorPalette,
@ -124,7 +126,7 @@ RED.group = (function() {
value:style["label-position"] || "nw" value:style["label-position"] || "nw"
}).appendTo("#node-input-row-style-label-position"); }).appendTo("#node-input-row-style-label-position");
RED.colorPicker.create({ RED.editor.colorPicker.create({
id:"node-input-style-color", id:"node-input-style-color",
value: style.color || defaultGroupStyle.color ||"#a4a4a4", value: style.color || defaultGroupStyle.color ||"#a4a4a4",
palette: colorPalette, palette: colorPalette,
@ -144,7 +146,6 @@ RED.group = (function() {
}) })
$("#node-input-style-label").prop("checked", this.style.label) $("#node-input-style-label").prop("checked", this.style.label)
$("#node-input-style-label").trigger("change"); $("#node-input-style-label").trigger("change");
}, },
oneditresize: function(size) { oneditresize: function(size) {
}, },
@ -183,7 +184,9 @@ RED.group = (function() {
var activateUngroup = false; var activateUngroup = false;
var activateMerge = false; var activateMerge = false;
var activateRemove = false; var activateRemove = false;
var singleGroupSelected = false;
if (activateGroup) { if (activateGroup) {
singleGroupSelected = selection.nodes.length === 1 && selection.nodes[0].type === 'group';
selection.nodes.forEach(function (n) { selection.nodes.forEach(function (n) {
if (n.type === "group") { if (n.type === "group") {
activateUngroup = true; activateUngroup = true;
@ -200,6 +203,8 @@ RED.group = (function() {
RED.menu.setDisabled("menu-item-group-ungroup", !activateUngroup); RED.menu.setDisabled("menu-item-group-ungroup", !activateUngroup);
RED.menu.setDisabled("menu-item-group-merge", !activateMerge); RED.menu.setDisabled("menu-item-group-merge", !activateMerge);
RED.menu.setDisabled("menu-item-group-remove", !activateRemove); RED.menu.setDisabled("menu-item-group-remove", !activateRemove);
RED.menu.setDisabled("menu-item-edit-copy-group-style", !singleGroupSelected);
RED.menu.setDisabled("menu-item-edit-paste-group-style", !activateUngroup);
}); });
RED.actions.add("core:group-selection", function() { groupSelection() }) RED.actions.add("core:group-selection", function() { groupSelection() })
@ -252,6 +257,7 @@ RED.group = (function() {
if (selection.nodes && selection.nodes.length === 1 && selection.nodes[0].type === 'group') { if (selection.nodes && selection.nodes.length === 1 && selection.nodes[0].type === 'group') {
groupStyleClipboard = JSON.parse(JSON.stringify(selection.nodes[0].style)); groupStyleClipboard = JSON.parse(JSON.stringify(selection.nodes[0].style));
RED.notify(RED._("clipboard.groupStyleCopied"),{id:"clipboard"}) RED.notify(RED._("clipboard.groupStyleCopied"),{id:"clipboard"})
RED.menu.setDisabled("menu-item-edit-paste-group-style", false)
} }
} }
function pasteGroupStyle() { function pasteGroupStyle() {

View File

@ -131,7 +131,7 @@ RED.keyboard = (function() {
return mergedKeymap; return mergedKeymap;
} }
function init() { function init(done) {
// Migrate from pre-0.18 // Migrate from pre-0.18
migrateOldKeymap(); migrateOldKeymap();
@ -164,6 +164,7 @@ RED.keyboard = (function() {
} }
} }
} }
done();
}); });
RED.userSettings.add({ RED.userSettings.add({
@ -174,6 +175,9 @@ RED.keyboard = (function() {
setTimeout(function() { setTimeout(function() {
$("#red-ui-settings-tab-keyboard-filter").trigger("focus"); $("#red-ui-settings-tab-keyboard-filter").trigger("focus");
},200); },200);
},
close: function() {
RED.menu.refreshShortcuts();
} }
}) })
} }

View File

@ -536,7 +536,7 @@ RED.library = (function() {
// evt.preventDefault(); // evt.preventDefault();
// var icon = libraryFields['icon'].input.val() || ""; // var icon = libraryFields['icon'].input.val() || "";
// var iconPath = (icon ? RED.utils.separateIconPath(icon) : {}); // var iconPath = (icon ? RED.utils.separateIconPath(icon) : {});
// RED.editor.showIconPicker(iconButton, null, iconPath, true, function (newIcon) { // RED.editor.iconPicker.show(iconButton, null, iconPath, true, function (newIcon) {
// iconButton.empty(); // iconButton.empty();
// var path = newIcon || ""; // var path = newIcon || "";
// var newPath = RED.utils.separateIconPath(path); // var newPath = RED.utils.separateIconPath(path);

View File

@ -16,8 +16,6 @@
RED.subflow = (function() { RED.subflow = (function() {
var currentLocale = "en-US";
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">'+ '<div class="form-row">'+
'<label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label>'+ '<label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label>'+
@ -37,7 +35,7 @@ RED.subflow = (function() {
'<div id="subflow-env-tabs-content">'+ '<div id="subflow-env-tabs-content">'+
'<div id="subflow-env-tab-edit">'+ '<div id="subflow-env-tab-edit">'+
'<div class="form-row node-input-env-container-row" id="subflow-input-edit-ui">'+ '<div class="form-row node-input-env-container-row" id="subflow-input-edit-ui">'+
'<ol class="red-ui-editor-subflow-env-list" id="node-input-env-container"></ol>'+ '<ol id="node-input-env-container"></ol>'+
'<div class="node-input-env-locales-row"><i class="fa fa-language"></i> <select id="subflow-input-env-locale"></select></div>'+ '<div class="node-input-env-locales-row"><i class="fa fa-language"></i> <select id="subflow-input-env-locale"></select></div>'+
'</div>'+ '</div>'+
'</div>'+ '</div>'+
@ -47,37 +45,6 @@ RED.subflow = (function() {
'</div>'+ '</div>'+
'</script>'; '</script>';
var _subflowModulePaneTemplate = '<form class="dialog-form form-horizontal" autocomplete="off">'+
'<div class="form-row">'+
'<label for="subflow-input-module-module" data-i18n="[append]editor:subflow.module"><i class="fa fa-cube"></i> </label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-module" data-i18n="[placeholder]common.label.name">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-type" data-i18n="[append]editor:subflow.type"> </label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-type">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-version" data-i18n="[append]editor:subflow.version"></label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-version" data-i18n="[placeholder]editor:subflow.versionPlaceholder">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-desc" data-i18n="[append]editor:subflow.desc"></label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-desc">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-license" data-i18n="[append]editor:subflow.license"></label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-license">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-author" data-i18n="[append]editor:subflow.author"></label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-author" data-i18n="[placeholder]editor:subflow.authorPlaceholder">'+
'</div>'+
'<div class="form-row">'+
'<label for="subflow-input-module-keywords" data-i18n="[append]editor:subflow.keys"></label>'+
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-keywords" data-i18n="[placeholder]editor:subflow.keysPlaceholder">'+
'</div>'+
'</form>';
function findAvailableSubflowIOPosition(subflow,isInput) { function findAvailableSubflowIOPosition(subflow,isInput) {
var pos = {x:50,y:30}; var pos = {x:50,y:30};
if (!isInput) { if (!isInput) {
@ -909,7 +876,6 @@ RED.subflow = (function() {
* Create interface for controlling env var UI definition * Create interface for controlling env var UI definition
*/ */
function buildEnvControl(envList,node) { function buildEnvControl(envList,node) {
var tabs = RED.tabs.create({ var tabs = RED.tabs.create({
id: "subflow-env-tabs", id: "subflow-env-tabs",
onchange: function(tab) { onchange: function(tab) {
@ -950,588 +916,11 @@ RED.subflow = (function() {
locales.val(locale); locales.val(locale);
locales.on("change", function() { locales.on("change", function() {
currentLocale = $(this).val(); RED.editor.envVarList.setLocale($(this).val(), $("#node-input-env-container"));
var items = $("#node-input-env-container").editableList("items");
items.each(function (i, item) {
var entry = $(this).data('data');
var labelField = entry.ui.labelField;
labelField.val(lookupLabel(entry.ui.label, "", currentLocale));
if (labelField.timeout) {
clearTimeout(labelField.timeout);
delete labelField.timeout;
}
labelField.addClass("input-updated");
labelField.timeout = setTimeout(function() {
delete labelField.timeout
labelField.removeClass("input-updated");
},3000);
});
}); });
RED.editor.envVarList.setLocale(locale);
} }
var DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env'];
var DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred'];
/**
* Create env var edit interface
* @param container - container
* @param node - subflow node
*/
function buildPropertiesList(envContainer, node) {
var isTemplateNode = (node.type === "subflow");
if (isTemplateNode) {
buildEnvControl(envContainer, node);
}
envContainer
.css({
'min-height':'150px',
'min-width':'450px'
})
.editableList({
header: isTemplateNode?$('<div><div><div></div><div data-i18n="common.label.name"></div><div data-i18n="editor-tab.defaultValue"></div><div></div></div></div>'):undefined,
addItem: function(container, i, opt) {
// If this is an instance node, these are properties unique to
// this instance - ie opt.parent will not be defined.
if (isTemplateNode) {
container.addClass("red-ui-editor-subflow-env-editable")
}
var envRow = $('<div/>').appendTo(container);
var nameField = null;
var valueField = null;
nameField = $('<input/>', {
class: "node-input-env-name",
type: "text",
placeholder: RED._("common.label.name")
}).attr("autocomplete","disable").appendTo(envRow).val(opt.name);
valueField = $('<input/>',{
style: "width:100%",
class: "node-input-env-value",
type: "text",
}).attr("autocomplete","disable").appendTo(envRow)
valueField.typedInput({default:'str',types:isTemplateNode?DEFAULT_ENV_TYPE_LIST:DEFAULT_ENV_TYPE_LIST_INC_CRED});
valueField.typedInput('type', opt.type);
if (opt.type === "cred") {
if (opt.value) {
valueField.typedInput('value', opt.value);
} else if (node.credentials && node.credentials[opt.name]) {
valueField.typedInput('value', node.credentials[opt.name]);
} else if (node.credentials && node.credentials['has_'+opt.name]) {
valueField.typedInput('value', "__PWRD__");
} else {
valueField.typedInput('value', "");
}
} else {
valueField.typedInput('value', opt.value);
}
opt.nameField = nameField;
opt.valueField = valueField;
var actionButton = $('<a/>',{href:"#",class:"red-ui-editableList-item-remove red-ui-button red-ui-button-small"}).appendTo(envRow);
$('<i/>',{class:"fa "+(opt.parent?"fa-reply":"fa-remove")}).appendTo(actionButton);
var removeTip = RED.popover.tooltip(actionButton,RED._("subflow.env.remove"));
actionButton.on("click", function(evt) {
evt.preventDefault();
removeTip.close();
container.parent().addClass("red-ui-editableList-item-deleting")
container.fadeOut(300, function() {
envContainer.editableList('removeItem',opt);
});
});
if (isTemplateNode) {
// Add the UI customisation row
// if `opt.ui` does not exist, then apply defaults. If these
// defaults do not change then they will get stripped off
// before saving.
if (opt.type === 'cred') {
opt.ui = opt.ui || {
icon: "",
type: "cred"
}
opt.ui.type = "cred";
} else {
opt.ui = opt.ui || {
icon: "",
type: "input",
opts: {types:DEFAULT_ENV_TYPE_LIST}
}
}
opt.ui.label = opt.ui.label || {};
opt.ui.type = opt.ui.type || "input";
var uiRow = $('<div/>').appendTo(container).hide();
// save current info for reverting on cancel
// var copy = $.extend(true, {}, ui);
$('<a href="#"><i class="fa fa-angle-right"></a>').prependTo(envRow).on("click", function (evt) {
evt.preventDefault();
if ($(this).hasClass('expanded')) {
uiRow.slideUp();
$(this).removeClass('expanded');
} else {
uiRow.slideDown();
$(this).addClass('expanded');
}
});
buildEnvEditRow(uiRow, opt.ui, nameField, valueField);
nameField.trigger('change');
}
},
sortable: ".red-ui-editableList-item-handle",
removable: false
});
var parentEnv = {};
var envList = [];
if (/^subflow:/.test(node.type)) {
var subflowDef = RED.nodes.subflow(node.type.substring(8));
if (subflowDef.env) {
subflowDef.env.forEach(function(env) {
var item = {
name:env.name,
parent: {
type: env.type,
value: env.value,
ui: env.ui
}
}
envList.push(item);
parentEnv[env.name] = item;
})
}
}
if (node.env) {
for (var i = 0; i < node.env.length; i++) {
var env = node.env[i];
if (parentEnv.hasOwnProperty(env.name)) {
parentEnv[env.name].type = env.type;
parentEnv[env.name].value = env.value;
} else {
envList.push({
name: env.name,
type: env.type,
value: env.value,
ui: env.ui
});
}
}
}
envList.forEach(function(env) {
if (env.parent && env.parent.ui && env.parent.ui.type === 'hide') {
return;
}
if (!isTemplateNode && env.parent) {
return;
}
envContainer.editableList('addItem', JSON.parse(JSON.stringify(env)));
});
}
/**
* Create UI edit interface for environment variable
* @param container - container
* @param env - env var definition
* @param nameField - name field of env var
* @param valueField - value field of env var
*/
function buildEnvEditRow(container, ui, nameField, valueField) {
container.addClass("red-ui-editor-subflow-env-ui-row")
var topRow = $('<div></div>').appendTo(container);
$('<div></div>').appendTo(topRow);
$('<div>').text(RED._("editor.icon")).appendTo(topRow);
$('<div>').text(RED._("editor.label")).appendTo(topRow);
$('<div>').text(RED._("editor.inputType")).appendTo(topRow);
var row = $('<div></div>').appendTo(container);
$('<div><i class="red-ui-editableList-item-handle fa fa-bars"></i></div>').appendTo(row);
var typeOptions = {
'input': {types:DEFAULT_ENV_TYPE_LIST},
'select': {opts:[]},
'spinner': {},
'cred': {}
};
if (ui.opts) {
typeOptions[ui.type] = ui.opts;
} else {
// Pick up the default values if not otherwise provided
ui.opts = typeOptions[ui.type];
}
var iconCell = $('<div></div>').appendTo(row);
var iconButton = $('<a href="#"></a>').appendTo(iconCell);
iconButton.on("click", function(evt) {
evt.preventDefault();
var icon = ui.icon || "";
var iconPath = (icon ? RED.utils.separateIconPath(icon) : {});
RED.editor.showIconPicker(iconButton, null, iconPath, true, function (newIcon) {
iconButton.empty();
var path = newIcon || "";
var newPath = RED.utils.separateIconPath(path);
if (newPath) {
$('<i class="fa"></i>').addClass(newPath.file).appendTo(iconButton);
}
ui.icon = path;
});
})
if (ui.icon) {
var newPath = RED.utils.separateIconPath(ui.icon);
$('<i class="fa '+newPath.file+'"></i>').appendTo(iconButton);
}
var labelCell = $('<div></div>').appendTo(row);
var label = ui.label && ui.label[currentLocale] || "";
var labelInput = $('<input type="text">').val(label).appendTo(labelCell);
ui.labelField = labelInput;
labelInput.on('change', function(evt) {
ui.label = ui.label || {};
var val = $(this).val().trim();
if (val === "") {
delete ui.label[currentLocale];
} else {
ui.label[currentLocale] = val;
}
})
var labelIcon = $('<span class="red-ui-editor-subflow-env-lang-icon"><i class="fa fa-language"></i></span>').appendTo(labelCell);
RED.popover.tooltip(labelIcon,function() {
var langs = Object.keys(ui.label);
var content = $("<div>");
if (langs.indexOf(currentLocale) === -1) {
langs.push(currentLocale);
langs.sort();
}
langs.forEach(function(l) {
var row = $('<div>').appendTo(content);
$('<span>').css({display:"inline-block",width:"120px"}).text(RED._("languages."+l)+(l===currentLocale?"*":"")).appendTo(row);
$('<span>').text(ui.label[l]||"").appendTo(row);
});
return content;
})
nameField.on('change',function(evt) {
labelInput.attr("placeholder",$(this).val())
});
var inputCell = $('<div></div>').appendTo(row);
var inputCellInput = $('<input type="text">').css("width","100%").appendTo(inputCell);
if (ui.type === "input") {
inputCellInput.val(ui.opts.types.join(","));
}
var checkbox;
var selectBox;
inputCellInput.typedInput({
types: [
{
value:"input",
label:RED._("editor.inputs.input"), icon:"fa fa-i-cursor",showLabel:false,multiple:true,options:[
{value:"str",label:RED._("editor.types.str"),icon:"red/images/typedInput/az.svg"},
{value:"num",label:RED._("editor.types.num"),icon:"red/images/typedInput/09.svg"},
{value:"bool",label:RED._("editor.types.bool"),icon:"red/images/typedInput/bool.svg"},
{value:"json",label:RED._("editor.types.json"),icon:"red/images/typedInput/json.svg"},
{value: "bin",label: RED._("editor.types.bin"),icon: "red/images/typedInput/bin.svg"},
{value: "env",label: RED._("editor.types.env"),icon: "red/images/typedInput/env.svg"},
{value: "cred",label: RED._("editor.types.cred"),icon: "fa fa-lock"}
],
default: DEFAULT_ENV_TYPE_LIST,
valueLabel: function(container,value) {
container.css("padding",0);
var innerContainer = $('<div class="red-ui-editor-subflow-env-input-type"></div>').appendTo(container);
var input = $('<div class="placeholder-input">').appendTo(innerContainer);
$('<span><i class="fa fa-i-cursor"></i></span>').appendTo(input);
if (value.length) {
value.forEach(function(v) {
if (!/^fa /.test(v.icon)) {
$('<img>',{src:v.icon,style:"max-width:14px; padding: 0 3px; margin-top:-4px; margin-left: 1px"}).appendTo(input);
} else {
var s = $('<span>',{style:"max-width:14px; padding: 0 3px; margin-top:-4px; margin-left: 1px"}).appendTo(input);
$("<i>",{class: v.icon}).appendTo(s);
}
})
} else {
$('<span class="red-ui-editor-subflow-env-input-type-placeholder"></span>').text(RED._("editor.selectType")).appendTo(input);
}
}
},
{
value: "cred",
label: RED._("typedInput.type.cred"), icon:"fa fa-lock", showLabel: false,
valueLabel: function(container,value) {
container.css("padding",0);
var innerContainer = $('<div class="red-ui-editor-subflow-env-input-type">').css({
"border-top-right-radius": "4px",
"border-bottom-right-radius": "4px"
}).appendTo(container);
$('<div class="placeholder-input">').html("&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;").appendTo(innerContainer);
}
},
{
value:"select",
label:RED._("editor.inputs.select"), icon:"fa fa-tasks",showLabel:false,
valueLabel: function(container,value) {
container.css("padding","0");
selectBox = $('<select></select>').appendTo(container);
if (ui.opts && Array.isArray(ui.opts.opts)) {
ui.opts.opts.forEach(function(o) {
var label = lookupLabel(o.l, o.l["en-US"]||o.v, currentLocale);
// $('<option>').val((o.t||'str')+":"+o.v).text(label).appendTo(selectBox);
$('<option>').val(o.v).text(label).appendTo(selectBox);
})
}
selectBox.on('change', function(evt) {
var v = selectBox.val();
// var parts = v.split(":");
// var t = parts.shift();
// v = parts.join(":");
//
// valueField.typedInput("type",'str')
valueField.typedInput("value",v)
});
selectBox.val(valueField.typedInput("value"));
// selectBox.val(valueField.typedInput('type')+":"+valueField.typedInput("value"));
},
expand: {
icon: "fa-caret-down",
minWidth: 400,
content: function(container) {
var content = $('<div class="red-ui-editor-subflow-ui-edit-panel">').appendTo(container);
var optList = $('<ol>').appendTo(content).editableList({
header:$("<div><div>"+RED._("editor.select.label")+"</div><div>"+RED._("editor.select.value")+"</div></div>"),
addItem: function(row,index,itemData) {
var labelDiv = $('<div>').appendTo(row);
var label = lookupLabel(itemData.l, "", currentLocale);
itemData.label = $('<input type="text">').val(label).appendTo(labelDiv);
itemData.label.on('keydown', function(evt) {
if (evt.keyCode === 13) {
itemData.input.focus();
evt.preventDefault();
}
});
var labelIcon = $('<span class="red-ui-editor-subflow-env-lang-icon"><i class="fa fa-language"></i></span>').appendTo(labelDiv);
RED.popover.tooltip(labelIcon,function() {
return currentLocale;
})
itemData.input = $('<input type="text">').val(itemData.v).appendTo(row);
// Problem using a TI here:
// - this is in a popout panel
// - clicking the expand button in the TI will close the parent edit tray
// and open the type editor.
// - but it leaves the popout panel over the top.
// - there is no way to get back to the popout panel after closing the type editor
//.typedInput({default:itemData.t||'str', types:DEFAULT_ENV_TYPE_LIST});
itemData.input.on('keydown', function(evt) {
if (evt.keyCode === 13) {
// Enter or Tab
var index = optList.editableList('indexOf',itemData);
var length = optList.editableList('length');
if (index + 1 === length) {
var newItem = {};
optList.editableList('addItem',newItem);
setTimeout(function() {
if (newItem.label) {
newItem.label.focus();
}
},100)
} else {
var nextItem = optList.editableList('getItemAt',index+1);
if (nextItem.label) {
nextItem.label.focus()
}
}
evt.preventDefault();
}
});
},
sortable: true,
removable: true,
height: 160
})
if (ui.opts.opts.length > 0) {
ui.opts.opts.forEach(function(o) {
optList.editableList('addItem',$.extend(true,{},o))
})
} else {
optList.editableList('addItem',{})
}
return {
onclose: function() {
var items = optList.editableList('items');
var vals = [];
items.each(function (i,el) {
var data = el.data('data');
var l = data.label.val().trim();
var v = data.input.val();
// var t = data.input.typedInput('type');
// var v = data.input.typedInput('value');
if (l.length > 0) {
data.l = data.l || {};
data.l[currentLocale] = l;
}
data.v = v;
if (l.length > 0 || v.length > 0) {
var val = {l:data.l,v:data.v};
// if (t !== 'str') {
// val.t = t;
// }
vals.push(val);
}
});
ui.opts.opts = vals;
inputCellInput.typedInput('value',Date.now())
}
}
}
}
},
{
value:"checkbox",
label:RED._("editor.inputs.checkbox"), icon:"fa fa-check-square-o",showLabel:false,
valueLabel: function(container,value) {
container.css("padding",0);
checkbox = $('<input type="checkbox">').appendTo(container);
checkbox.on('change', function(evt) {
valueField.typedInput('value',$(this).prop('checked')?"true":"false");
})
checkbox.prop('checked',valueField.typedInput('value')==="true");
}
},
{
value:"spinner",
label:RED._("editor.inputs.spinner"), icon:"fa fa-sort-numeric-asc", showLabel:false,
valueLabel: function(container,value) {
container.css("padding",0);
var innerContainer = $('<div class="red-ui-editor-subflow-env-input-type"></div>').appendTo(container);
var input = $('<div class="placeholder-input">').appendTo(innerContainer);
$('<span><i class="fa fa-sort-numeric-asc"></i></span>').appendTo(input);
var min = ui.opts && ui.opts.min;
var max = ui.opts && ui.opts.max;
var label = "";
if (min !== undefined && max !== undefined) {
label = Math.min(min,max)+" - "+Math.max(min,max);
} else if (min !== undefined) {
label = "> "+min;
} else if (max !== undefined) {
label = "< "+max;
}
$('<span>').css("margin-left","15px").text(label).appendTo(input);
},
expand: {
icon: "fa-caret-down",
content: function(container) {
var content = $('<div class="red-ui-editor-subflow-ui-edit-panel">').appendTo(container);
content.css("padding","8px 5px")
var min = ui.opts.min;
var max = ui.opts.max;
var minInput = $('<input type="number" style="margin-bottom:0; width:60px">');
minInput.val(min);
var maxInput = $('<input type="number" style="margin-bottom:0; width:60px">');
maxInput.val(max);
$('<div class="form-row" style="margin-bottom:3px"><label>'+RED._("editor.spinner.min")+'</label></div>').append(minInput).appendTo(content);
$('<div class="form-row" style="margin-bottom:0"><label>'+RED._("editor.spinner.max")+'</label></div>').append(maxInput).appendTo(content);
return {
onclose: function() {
var min = minInput.val().trim();
var max = maxInput.val().trim();
if (min !== "") {
ui.opts.min = parseInt(min);
} else {
delete ui.opts.min;
}
if (max !== "") {
ui.opts.max = parseInt(max);
} else {
delete ui.opts.max;
}
inputCellInput.typedInput('value',Date.now())
}
}
}
}
},
{
value:"none",
label:RED._("editor.inputs.none"), icon:"fa fa-times",hasValue:false
},
{
value:"hide",
label:RED._("editor.inputs.hidden"), icon:"fa fa-ban",hasValue:false
}
],
default: 'none'
}).on("typedinputtypechange", function(evt,type) {
ui.type = $(this).typedInput("type");
ui.opts = typeOptions[ui.type];
if (ui.type === 'input') {
// In the case of 'input' type, the typedInput uses the multiple-option
// mode. Its value needs to be set to a comma-separately list of the
// selected options.
inputCellInput.typedInput('value',ui.opts.types.join(","))
} else {
// No other type cares about `value`, but doing this will
// force a refresh of the label now that `ui.opts` has
// been updated.
inputCellInput.typedInput('value',Date.now())
}
switch (ui.type) {
case 'input':
valueField.typedInput('types',ui.opts.types);
break;
case 'select':
valueField.typedInput('types',['str']);
break;
case 'checkbox':
valueField.typedInput('types',['bool']);
break;
case 'spinner':
valueField.typedInput('types',['num']);
break;
case 'cred':
valueField.typedInput('types',['cred']);
break;
default:
valueField.typedInput('types',DEFAULT_ENV_TYPE_LIST)
}
if (ui.type === 'checkbox') {
valueField.typedInput('type','bool');
} else if (ui.type === 'spinner') {
valueField.typedInput('type','num');
}
if (ui.type !== 'checkbox') {
checkbox = null;
}
}).on("change", function(evt,type) {
if (ui.type === 'input') {
var types = inputCellInput.typedInput('value');
ui.opts.types = (types === "") ? ["str"] : types.split(",");
valueField.typedInput('types',ui.opts.types);
}
});
valueField.on("change", function(evt) {
if (checkbox) {
checkbox.prop('checked',$(this).typedInput('value')==="true")
}
})
// Set the input to the right type. This will trigger the 'typedinputtypechange'
// event handler (just above ^^) to update the value if needed
inputCellInput.typedInput('type',ui.type)
}
function buildEnvUIRow(row, tenv, ui, node) { function buildEnvUIRow(row, tenv, ui, node) {
ui.label = ui.label||{}; ui.label = ui.label||{};
@ -1540,7 +929,7 @@ RED.subflow = (function() {
ui.opts = {}; ui.opts = {};
} else if (!ui.type) { } else if (!ui.type) {
ui.type = "input"; ui.type = "input";
ui.opts = {types:DEFAULT_ENV_TYPE_LIST} ui.opts = { types: RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST }
} else { } else {
if (!ui.opts) { if (!ui.opts) {
ui.opts = (ui.type === "select") ? {opts:[]} : {}; ui.opts = (ui.type === "select") ? {opts:[]} : {};
@ -1549,7 +938,7 @@ RED.subflow = (function() {
var labels = ui.label || {}; var labels = ui.label || {};
var locale = RED.i18n.lang(); var locale = RED.i18n.lang();
var labelText = lookupLabel(labels, labels["en-US"]||tenv.name, locale); var labelText = RED.editor.envVarList.lookupLabel(labels, labels["en-US"]||tenv.name, locale);
var label = $('<label>').appendTo(row); var label = $('<label>').appendTo(row);
$('<span>&nbsp;</span>').appendTo(row); $('<span>&nbsp;</span>').appendTo(row);
var labelContainer = $('<span></span>').appendTo(label); var labelContainer = $('<span></span>').appendTo(label);
@ -1602,7 +991,7 @@ RED.subflow = (function() {
input = $('<select>').css('width','70%').appendTo(row); input = $('<select>').css('width','70%').appendTo(row);
if (ui.opts.opts) { if (ui.opts.opts) {
ui.opts.opts.forEach(function(o) { ui.opts.opts.forEach(function(o) {
$('<option>').val(o.v).text(lookupLabel(o.l, o.l['en-US']||o.v, locale)).appendTo(input); $('<option>').val(o.v).text(RED.editor.envVarList.lookupLabel(o.l, o.l['en-US']||o.v, locale)).appendTo(input);
}) })
} }
input.val(val.value); input.val(val.value);
@ -1668,9 +1057,8 @@ RED.subflow = (function() {
* @param uiContainer - container for UI * @param uiContainer - container for UI
* @param envList - env var definitions of template * @param envList - env var definitions of template
*/ */
function buildEnvUI(uiContainer, envList,node) { function buildEnvUI(uiContainer, envList, node) {
uiContainer.empty(); uiContainer.empty();
var elementID = 0;
for (var i = 0; i < envList.length; i++) { for (var i = 0; i < envList.length; i++) {
var tenv = envList[i]; var tenv = envList[i];
if (tenv.ui && tenv.ui.type === 'hide') { if (tenv.ui && tenv.ui.type === 'hide') {
@ -1678,8 +1066,6 @@ RED.subflow = (function() {
} }
var row = $("<div/>", { class: "form-row" }).appendTo(uiContainer); var row = $("<div/>", { class: "form-row" }).appendTo(uiContainer);
buildEnvUIRow(row,tenv, tenv.ui || {}, node); buildEnvUIRow(row,tenv, tenv.ui || {}, node);
// console.log(ui);
} }
} }
// buildEnvUI // buildEnvUI
@ -1715,7 +1101,7 @@ RED.subflow = (function() {
// icon: "", // icon: "",
// label: {}, // label: {},
// type: "input", // type: "input",
// opts: {types:DEFAULT_ENV_TYPE_LIST} // opts: {types:RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST}
// } // }
if (!ui.icon) { if (!ui.icon) {
delete ui.icon; delete ui.icon;
@ -1725,7 +1111,7 @@ RED.subflow = (function() {
} }
switch (ui.type) { switch (ui.type) {
case "input": case "input":
if (JSON.stringify(ui.opts) === JSON.stringify({types:DEFAULT_ENV_TYPE_LIST})) { if (JSON.stringify(ui.opts) === JSON.stringify({types:RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST})) {
// This is the default input config. Delete it as it will // This is the default input config. Delete it as it will
// be applied automatically // be applied automatically
delete ui.type; delete ui.type;
@ -1841,7 +1227,6 @@ RED.subflow = (function() {
function exportSubflowInstanceEnv(node) { function exportSubflowInstanceEnv(node) {
var env = []; var env = [];
// First, get the values for the SubflowTemplate defined properties // First, get the values for the SubflowTemplate defined properties
// - these are the ones with custom UI elements // - these are the ones with custom UI elements
var parentEnv = getSubflowInstanceParentEnv(node); var parentEnv = getSubflowInstanceParentEnv(node);
@ -1853,7 +1238,7 @@ RED.subflow = (function() {
ui.type = "cred"; ui.type = "cred";
} else { } else {
ui.type = "input"; ui.type = "input";
ui.opts = {types:DEFAULT_ENV_TYPE_LIST} ui.opts = {types:RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST}
} }
} else { } else {
ui.opts = ui.opts || {}; ui.opts = ui.opts || {};
@ -1893,22 +1278,6 @@ RED.subflow = (function() {
} }
} }
}) })
// Second, get the values from the Properties table tab
var items = $('#red-ui-editor-subflow-env-list').editableList('items');
items.each(function (i,el) {
var data = el.data('data');
var item;
if (data.nameField && data.valueField) {
item = {
name: data.nameField.val(),
value: data.valueField.typedInput("value"),
type: data.valueField.typedInput("type")
}
if (item.name.trim() !== "") {
env.push(item);
}
}
});
return env; return env;
} }
@ -1916,164 +1285,18 @@ RED.subflow = (function() {
return 'node-input-subflow-env-'+name.replace(/[^a-z0-9-_]/ig,"_"); return 'node-input-subflow-env-'+name.replace(/[^a-z0-9-_]/ig,"_");
} }
/** // Called by subflow.oneditprepare for both instances and templates
* Lookup text for specific locale
* @param labels - dict of labels
* @param defaultLabel - fallback label if not found
* @param locale - target locale
* @returns {string} text for specified locale
*/
function lookupLabel(labels, defaultLabel, locale) {
if (labels) {
if (labels[locale]) {
return labels[locale];
}
if (locale) {
var lang = locale.substring(0, 2);
if (labels[lang]) {
return labels[lang];
}
}
}
return defaultLabel;
}
function buildEditForm(type,node) { function buildEditForm(type,node) {
if (type === "subflow-template") { if (type === "subflow-template") {
buildPropertiesList($('#node-input-env-container'), node); // This is the tabbed UI that offers the env list - with UI options
// plus the preview tab
buildEnvControl($('#node-input-env-container'), node);
RED.editor.envVarList.create($('#node-input-env-container'), node);
} else if (type === "subflow") { } else if (type === "subflow") {
// This gets called by the subflow type `oneditprepare` function // This is the rendered version of the subflow env var list
// registered in nodes.js#addSubflow()
buildEnvUI($("#subflow-input-ui"), getSubflowInstanceParentEnv(node), node); buildEnvUI($("#subflow-input-ui"), getSubflowInstanceParentEnv(node), node);
} }
} }
function buildPropertiesForm(node) {
var container = $('#editor-subflow-envProperties-content');
var form = $('<form class="dialog-form form-horizontal"></form>').appendTo(container);
var listContainer = $('<div class="form-row node-input-env-container-row"></div>').appendTo(form);
var list = $('<ol id="red-ui-editor-subflow-env-list" class="red-ui-editor-subflow-env-list"></ol>').appendTo(listContainer);
buildPropertiesList(list, node);
}
function setupInputValidation(input,validator) {
var errorTip;
var validateTimeout;
var validateFunction = function() {
if (validateTimeout) {
return;
}
validateTimeout = setTimeout(function() {
var error = validator(input.val());
// if (!error && errorTip) {
// errorTip.close();
// errorTip = null;
// } else if (error && !errorTip) {
// errorTip = RED.popover.create({
// tooltip: true,
// target:input,
// size: "small",
// direction: "bottom",
// content: error,
// }).open();
// }
input.toggleClass("input-error",!!error);
validateTimeout = null;
})
}
input.on("change keyup paste", validateFunction);
}
function buildModuleForm(container, node) {
$(_subflowModulePaneTemplate).appendTo(container);
var moduleProps = node.meta || {};
[
'module',
'type',
'version',
'author',
'desc',
'keywords',
'license'
].forEach(function(property) {
$("#subflow-input-module-"+property).val(moduleProps[property]||"")
})
$("#subflow-input-module-type").attr("placeholder",node.id);
setupInputValidation($("#subflow-input-module-module"), function(newValue) {
newValue = newValue.trim();
var isValid = newValue.length < 215;
isValid = isValid && !/^[._]/.test(newValue);
isValid = isValid && !/[A-Z]/.test(newValue);
if (newValue !== encodeURIComponent(newValue)) {
var m = /^@([^\/]+)\/([^\/]+)$/.exec(newValue);
if (m) {
isValid = isValid && (m[1] === encodeURIComponent(m[1]) && m[2] === encodeURIComponent(m[2]))
} else {
isValid = false;
}
}
return isValid?"":"Invalid module name"
})
setupInputValidation($("#subflow-input-module-version"), function(newValue) {
newValue = newValue.trim();
var isValid = newValue === "" ||
/^(\d|[1-9]\d*)\.(\d|[1-9]\d*)\.(\d|[1-9]\d*)(-(0|[1-9A-Za-z-][0-9A-Za-z-]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9A-Za-z-][0-9A-Za-z-]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/.test(newValue);
return isValid?"":"Invalid version number"
})
var licenses = ["none", "Apache-2.0", "BSD-3-Clause", "BSD-2-Clause", "GPL-2.0", "GPL-3.0", "MIT", "MPL-2.0", "CDDL-1.0", "EPL-2.0"];
var typedLicenses = {
types: licenses.map(function(l) {
return {
value: l,
label: l === "none" ? RED._("editor:subflow.licenseNone") : l,
hasValue: false
};
})
}
typedLicenses.types.push({
value:"_custom_", label:RED._("editor:subflow.licenseOther"), icon:"red/images/typedInput/az.svg"
})
if (!moduleProps.license) {
typedLicenses.default = "none";
} else if (licenses.indexOf(moduleProps.license) > -1) {
typedLicenses.default = moduleProps.license;
} else {
typedLicenses.default = "_custom_";
}
$("#subflow-input-module-license").typedInput(typedLicenses)
}
function exportSubflowModuleProperties(node) {
var value;
var moduleProps = {};
[
'module',
'type',
'version',
'author',
'desc',
'keywords'
].forEach(function(property) {
value = $("#subflow-input-module-"+property).val().trim();
if (value) {
moduleProps[property] = value;
}
})
var selectedLicenseType = $("#subflow-input-module-license").typedInput("type");
if (selectedLicenseType === '_custom_') {
value = $("#subflow-input-module-license").val();
if (value) {
moduleProps.license = value;
}
} else if (selectedLicenseType !== "none") {
moduleProps.license = selectedLicenseType;
}
return moduleProps;
}
return { return {
init: init, init: init,
@ -2085,14 +1308,9 @@ RED.subflow = (function() {
removeOutput: removeSubflowOutput, removeOutput: removeSubflowOutput,
removeStatus: removeSubflowStatus, removeStatus: removeSubflowStatus,
buildEditForm: buildEditForm, buildEditForm: buildEditForm,
buildPropertiesForm: buildPropertiesForm,
buildModuleForm: buildModuleForm,
exportSubflowTemplateEnv: exportEnvList, exportSubflowTemplateEnv: exportEnvList,
exportSubflowInstanceEnv: exportSubflowInstanceEnv, exportSubflowInstanceEnv: exportSubflowInstanceEnv
exportSubflowModuleProperties: exportSubflowModuleProperties
} }
})(); })();

View File

@ -311,6 +311,7 @@ RED.sidebar.info.outliner = (function() {
RED.events.on("nodes:add",onNodeAdd); RED.events.on("nodes:add",onNodeAdd);
RED.events.on("nodes:remove",onObjectRemove); RED.events.on("nodes:remove",onObjectRemove);
RED.events.on("nodes:change",onNodeChange); RED.events.on("nodes:change",onNodeChange);
// RED.events.on("nodes:reorder",onNodesReorder);
RED.events.on("groups:add",onNodeAdd); RED.events.on("groups:add",onNodeAdd);
RED.events.on("groups:remove",onObjectRemove); RED.events.on("groups:remove",onObjectRemove);
@ -369,6 +370,21 @@ RED.sidebar.info.outliner = (function() {
return indexMap[A.id] - indexMap[B.id] return indexMap[A.id] - indexMap[B.id]
}) })
} }
// function onNodesReorder(event) {
// //
// var nodes = RED.nodes.getNodeOrder(event.z);
// var indexMap = {};
// nodes.forEach(function(id,index) {
// indexMap[id] = index;
// })
// var existingObject = objects[event.z];
// existingObject.treeList.sortChildren(function(A,B) {
// if (A.children && !B.children) { return -1 }
// if (!A.children && B.children) { return 1 }
// if (A.children && B.children) { return -1 }
// return indexMap[A.id] - indexMap[B.id]
// })
// }
function onSubflowAdd(sf) { function onSubflowAdd(sf) {
objects[sf.id] = { objects[sf.id] = {
id: sf.id, id: sf.id,

View File

@ -427,18 +427,309 @@ RED.view.tools = (function() {
} }
} }
} }
} }
function alignSelectionToEdge(direction) {
var selection = RED.view.selection();
if (selection.nodes && selection.nodes.length > 1) {
var changedNodes = [];
var bounds = {
minX: Number.MAX_SAFE_INTEGER,
minY: Number.MAX_SAFE_INTEGER,
maxX: Number.MIN_SAFE_INTEGER,
maxY: Number.MIN_SAFE_INTEGER
}
selection.nodes.forEach(function(n) {
if (n.type === "group") {
bounds.minX = Math.min(bounds.minX, n.x);
bounds.minY = Math.min(bounds.minY, n.y);
bounds.maxX = Math.max(bounds.maxX, n.x + n.w);
bounds.maxY = Math.max(bounds.maxY, n.y + n.h);
} else {
bounds.minX = Math.min(bounds.minX, n.x - n.w/2);
bounds.minY = Math.min(bounds.minY, n.y - n.h/2);
bounds.maxX = Math.max(bounds.maxX, n.x + n.w/2);
bounds.maxY = Math.max(bounds.maxY, n.y + n.h/2);
}
});
bounds.midX = bounds.minX + (bounds.maxX - bounds.minX)/2;
bounds.midY = bounds.minY + (bounds.maxY - bounds.minY)/2;
selection.nodes.forEach(function(n) {
var targetX;
var targetY;
var isGroup = n.type==="group";
switch(direction) {
case 'top':
targetX = n.x;
targetY = bounds.minY + (isGroup?0:(n.h/2));
break;
case 'bottom':
targetX = n.x;
targetY = bounds.maxY - (isGroup?n.h:(n.h/2));
break;
case 'left':
targetX = bounds.minX + (isGroup?0:(n.w/2));
targetY = n.y;
break;
case 'right':
targetX = bounds.maxX - (isGroup?n.w:(n.w/2));
targetY = n.y;
break;
case 'middle':
targetX = n.x;
targetY = bounds.midY - (isGroup?n.h/2:0)
break;
case 'center':
targetX = bounds.midX - (isGroup?n.w/2:0)
targetY = n.y;
break;
}
if (n.x !== targetX || n.y !== targetY) {
if (!isGroup) {
changedNodes.push({
n:n,
ox: n.x,
oy: n.y,
moved: n.moved
});
n.x = targetX;
n.y = targetY;
n.dirty = true;
n.moved = true;
} else {
var groupNodes = RED.group.getNodes(n, true);
var deltaX = n.x - targetX;
var deltaY = n.y - targetY;
groupNodes.forEach(function(gn) {
if (gn.type !== "group" ) {
changedNodes.push({
n:gn,
ox: gn.x,
oy: gn.y,
moved: gn.moved
});
gn.x = gn.x - deltaX;
gn.y = gn.y - deltaY;
gn.dirty = true;
gn.moved = true;
}
})
}
}
});
if (changedNodes.length > 0) {
RED.history.push({t:"move",nodes:changedNodes,dirty:RED.nodes.dirty()});
RED.nodes.dirty(true);
RED.view.redraw(true);
}
}
}
function distributeSelection(direction) {
var selection = RED.view.selection();
if (selection.nodes && selection.nodes.length > 2) {
var changedNodes = [];
var bounds = {
minX: Number.MAX_SAFE_INTEGER,
minY: Number.MAX_SAFE_INTEGER,
maxX: Number.MIN_SAFE_INTEGER,
maxY: Number.MIN_SAFE_INTEGER
}
var startAnchors = [];
var endAnchors = [];
selection.nodes.forEach(function(n) {
var nx,ny;
if (n.type === "group") {
nx = n.x + n.w/2;
ny = n.y + n.h/2;
} else {
nx = n.x;
ny = n.y;
}
if (direction === "h") {
if (nx < bounds.minX) {
startAnchors = [];
bounds.minX = nx;
}
if (nx === bounds.minX) {
startAnchors.push(n);
}
if (nx > bounds.maxX) {
endAnchors = [];
bounds.maxX = nx;
}
if (nx === bounds.maxX) {
endAnchors.push(n);
}
} else {
if (ny < bounds.minY) {
startAnchors = [];
bounds.minY = ny;
}
if (ny === bounds.minY) {
startAnchors.push(n);
}
if (ny > bounds.maxY) {
endAnchors = [];
bounds.maxY = ny;
}
if (ny === bounds.maxY) {
endAnchors.push(n);
}
}
});
var startAnchor = startAnchors[0];
var endAnchor = endAnchors[0];
var nodeSpace = 0;
var nodesToMove = selection.nodes.filter(function(n) {
if (n.id !== startAnchor.id && n.id !== endAnchor.id) {
nodeSpace += direction === 'h'?n.w:n.h;
return true;
}
return false;
}).sort(function(A,B) {
if (direction === 'h') {
return A.x - B.x
} else {
return A.y - B.y
}
})
var saX = startAnchor.x + startAnchor.w/2;
var saY = startAnchor.y + startAnchor.h/2;
if (startAnchor.type === "group") {
saX = startAnchor.x + startAnchor.w;
saY = startAnchor.y + startAnchor.h;
}
var eaX = endAnchor.x;
var eaY = endAnchor.y;
if (endAnchor.type !== "group") {
eaX -= endAnchor.w/2;
eaY -= endAnchor.h/2;
}
var spaceToFill = direction === 'h'?(eaX - saX - nodeSpace): (eaY - saY - nodeSpace);
var spaceBetweenNodes = spaceToFill / (nodesToMove.length + 1);
var tx = saX;
var ty = saY;
while(nodesToMove.length > 0) {
if (direction === 'h') {
tx += spaceBetweenNodes;
} else {
ty += spaceBetweenNodes;
}
var nextNode = nodesToMove.shift();
var isGroup = nextNode.type==="group";
var nx = nextNode.x;
var ny = nextNode.y;
if (!isGroup) {
tx += nextNode.w/2;
ty += nextNode.h/2;
}
if ((direction === 'h' && nx !== tx) || (direction === 'v' && ny !== ty)) {
if (!isGroup) {
changedNodes.push({
n:nextNode,
ox: nextNode.x,
oy: nextNode.y,
moved: nextNode.moved
});
if (direction === 'h') {
nextNode.x = tx;
} else {
nextNode.y = ty;
}
nextNode.dirty = true;
nextNode.moved = true;
} else {
var groupNodes = RED.group.getNodes(nextNode, true);
var deltaX = direction === 'h'? nx - tx : 0;
var deltaY = direction === 'v'? ny - ty : 0;
groupNodes.forEach(function(gn) {
if (gn.type !== "group" ) {
changedNodes.push({
n:gn,
ox: gn.x,
oy: gn.y,
moved: gn.moved
});
gn.x = gn.x - deltaX;
gn.y = gn.y - deltaY;
gn.dirty = true;
gn.moved = true;
}
})
}
}
if (isGroup) {
tx += nextNode.w;
ty += nextNode.h;
} else {
tx += nextNode.w/2;
ty += nextNode.h/2;
}
}
if (changedNodes.length > 0) {
RED.history.push({t:"move",nodes:changedNodes,dirty:RED.nodes.dirty()});
RED.nodes.dirty(true);
RED.view.redraw(true);
}
}
}
function reorderSelection(dir) {
var selection = RED.view.selection();
if (selection.nodes) {
var nodesToMove = [];
selection.nodes.forEach(function(n) {
if (n.type === "group") {
nodesToMove = nodesToMove.concat(RED.group.getNodes(n, true).filter(function(n) {
return n.type !== "group";
}))
} else if (n.type !== "subflow"){
nodesToMove.push(n);
}
})
if (nodesToMove.length > 0) {
var z = nodesToMove[0].z;
var existingOrder = RED.nodes.getNodeOrder(z);
var movedNodes;
if (dir === "forwards") {
movedNodes = RED.nodes.moveNodesForwards(nodesToMove);
} else if (dir === "backwards") {
movedNodes = RED.nodes.moveNodesBackwards(nodesToMove);
} else if (dir === "front") {
movedNodes = RED.nodes.moveNodesToFront(nodesToMove);
} else if (dir === "back") {
movedNodes = RED.nodes.moveNodesToBack(nodesToMove);
}
if (movedNodes.length > 0) {
var newOrder = RED.nodes.getNodeOrder(z);
RED.history.push({t:"reorder",nodes:{z:z,from:existingOrder,to:newOrder},dirty:RED.nodes.dirty()});
RED.nodes.dirty(true);
RED.view.redraw(true);
}
}
}
}
return { return {
init: function() { init: function() {
RED.actions.add("core:show-selected-node-labels", function() { setSelectedNodeLabelState(true); }) RED.actions.add("core:show-selected-node-labels", function() { setSelectedNodeLabelState(true); })
RED.actions.add("core:hide-selected-node-labels", function() { setSelectedNodeLabelState(false); }) RED.actions.add("core:hide-selected-node-labels", function() { setSelectedNodeLabelState(false); })
RED.actions.add("core:align-selection-to-grid", alignToGrid);
RED.actions.add("core:scroll-view-up", function() { RED.view.scroll(0,-RED.view.gridSize());}); RED.actions.add("core:scroll-view-up", function() { RED.view.scroll(0,-RED.view.gridSize());});
RED.actions.add("core:scroll-view-right", function() { RED.view.scroll(RED.view.gridSize(),0);}); RED.actions.add("core:scroll-view-right", function() { RED.view.scroll(RED.view.gridSize(),0);});
RED.actions.add("core:scroll-view-down", function() { RED.view.scroll(0,RED.view.gridSize());}); RED.actions.add("core:scroll-view-down", function() { RED.view.scroll(0,RED.view.gridSize());});
@ -454,6 +745,12 @@ RED.view.tools = (function() {
RED.actions.add("core:move-selection-down", function() { moveSelection(0,1);}); RED.actions.add("core:move-selection-down", function() { moveSelection(0,1);});
RED.actions.add("core:move-selection-left", function() { moveSelection(-1,0);}); RED.actions.add("core:move-selection-left", function() { moveSelection(-1,0);});
RED.actions.add("core:move-selection-forwards", function() { reorderSelection('forwards') })
RED.actions.add("core:move-selection-backwards", function() { reorderSelection('backwards') })
RED.actions.add("core:move-selection-to-front", function() { reorderSelection('front') })
RED.actions.add("core:move-selection-to-back", function() { reorderSelection('back') })
RED.actions.add("core:step-selection-up", function() { moveSelection(0,-RED.view.gridSize());}); RED.actions.add("core:step-selection-up", function() { moveSelection(0,-RED.view.gridSize());});
RED.actions.add("core:step-selection-right", function() { moveSelection(RED.view.gridSize(),0);}); RED.actions.add("core:step-selection-right", function() { moveSelection(RED.view.gridSize(),0);});
RED.actions.add("core:step-selection-down", function() { moveSelection(0,RED.view.gridSize());}); RED.actions.add("core:step-selection-down", function() { moveSelection(0,RED.view.gridSize());});
@ -474,6 +771,20 @@ RED.view.tools = (function() {
RED.actions.add("core:go-to-nearest-node-on-right", function() { gotoNearestNode('right')}) RED.actions.add("core:go-to-nearest-node-on-right", function() { gotoNearestNode('right')})
RED.actions.add("core:go-to-nearest-node-above", function() { gotoNearestNode('up') }) RED.actions.add("core:go-to-nearest-node-above", function() { gotoNearestNode('up') })
RED.actions.add("core:go-to-nearest-node-below", function() { gotoNearestNode('down') }) RED.actions.add("core:go-to-nearest-node-below", function() { gotoNearestNode('down') })
RED.actions.add("core:align-selection-to-grid", alignToGrid);
RED.actions.add("core:align-selection-to-left", function() { alignSelectionToEdge('left') })
RED.actions.add("core:align-selection-to-right", function() { alignSelectionToEdge('right') })
RED.actions.add("core:align-selection-to-top", function() { alignSelectionToEdge('top') })
RED.actions.add("core:align-selection-to-bottom", function() { alignSelectionToEdge('bottom') })
RED.actions.add("core:align-selection-to-middle", function() { alignSelectionToEdge('middle') })
RED.actions.add("core:align-selection-to-center", function() { alignSelectionToEdge('center') })
RED.actions.add("core:distribute-selection-horizontally", function() { distributeSelection('h') })
RED.actions.add("core:distribute-selection-vertically", function() { distributeSelection('v') })
// RED.actions.add("core:add-node", function() { addNode() }) // RED.actions.add("core:add-node", function() { addNode() })
}, },
/** /**

View File

@ -501,6 +501,28 @@ RED.view = (function() {
RED.actions.add("core:copy-selection-to-internal-clipboard",copySelection); RED.actions.add("core:copy-selection-to-internal-clipboard",copySelection);
RED.actions.add("core:cut-selection-to-internal-clipboard",function(){copySelection();deleteSelection();}); RED.actions.add("core:cut-selection-to-internal-clipboard",function(){copySelection();deleteSelection();});
RED.actions.add("core:paste-from-internal-clipboard",function(){importNodes(clipboard,{generateIds: true});}); RED.actions.add("core:paste-from-internal-clipboard",function(){importNodes(clipboard,{generateIds: true});});
RED.events.on("view:selection-changed", function(selection) {
var hasSelection = (selection.nodes && selection.nodes.length > 0);
var hasMultipleSelection = hasSelection && selection.nodes.length > 1;
RED.menu.setDisabled("menu-item-edit-cut",!hasSelection);
RED.menu.setDisabled("menu-item-edit-copy",!hasSelection);
RED.menu.setDisabled("menu-item-edit-select-connected",!hasSelection);
RED.menu.setDisabled("menu-item-view-tools-move-to-back",!hasSelection);
RED.menu.setDisabled("menu-item-view-tools-move-to-front",!hasSelection);
RED.menu.setDisabled("menu-item-view-tools-move-backwards",!hasSelection);
RED.menu.setDisabled("menu-item-view-tools-move-forwards",!hasSelection);
RED.menu.setDisabled("menu-item-view-tools-align-left",!hasMultipleSelection);
RED.menu.setDisabled("menu-item-view-tools-align-center",!hasMultipleSelection);
RED.menu.setDisabled("menu-item-view-tools-align-right",!hasMultipleSelection);
RED.menu.setDisabled("menu-item-view-tools-align-top",!hasMultipleSelection);
RED.menu.setDisabled("menu-item-view-tools-align-middle",!hasMultipleSelection);
RED.menu.setDisabled("menu-item-view-tools-align-bottom",!hasMultipleSelection);
RED.menu.setDisabled("menu-item-view-tools-distribute-horizontally",!hasMultipleSelection);
RED.menu.setDisabled("menu-item-view-tools-distribute-veritcally",!hasMultipleSelection);
})
RED.actions.add("core:delete-selection",deleteSelection); RED.actions.add("core:delete-selection",deleteSelection);
RED.actions.add("core:edit-selected-node",editSelection); RED.actions.add("core:edit-selected-node",editSelection);
RED.actions.add("core:go-to-selection",function() { RED.actions.add("core:go-to-selection",function() {
@ -655,14 +677,17 @@ RED.view = (function() {
var activeWorkspace = RED.workspaces.active(); var activeWorkspace = RED.workspaces.active();
activeNodes = RED.nodes.filterNodes({z:activeWorkspace}); activeNodes = RED.nodes.filterNodes({z:activeWorkspace});
activeNodes.forEach(function(n,i) {
n._index = i;
})
activeLinks = RED.nodes.filterLinks({ activeLinks = RED.nodes.filterLinks({
source:{z:activeWorkspace}, source:{z:activeWorkspace},
target:{z:activeWorkspace} target:{z:activeWorkspace}
}); });
activeGroups = RED.nodes.groups(activeWorkspace)||[]; activeGroups = RED.nodes.groups(activeWorkspace)||[];
activeGroups.forEach(function(g) { activeGroups.forEach(function(g,i) {
g._index = i;
if (g.g) { if (g.g) {
g._root = g.g; g._root = g.g;
g._depth = 1; g._depth = 1;
@ -695,7 +720,8 @@ RED.view = (function() {
if (a._root === b._root) { if (a._root === b._root) {
return a._depth - b._depth; return a._depth - b._depth;
} else { } else {
return a._root.localeCompare(b._root); // return a._root.localeCompare(b._root);
return a._index - b._index;
} }
}); });
@ -704,7 +730,8 @@ RED.view = (function() {
if (a._root === b._root) { if (a._root === b._root) {
return a._depth - b._depth; return a._depth - b._depth;
} else { } else {
return a._root.localeCompare(b._root); return a._index - b._index;
// return a._root.localeCompare(b._root);
} }
}) })
} }
@ -2332,6 +2359,7 @@ RED.view = (function() {
} }
} }
clipboard = JSON.stringify(nns); clipboard = JSON.stringify(nns);
RED.menu.setDisabled("menu-item-edit-paste", false);
if (nodeCount > 0) { if (nodeCount > 0) {
RED.notify(RED._("clipboard.nodeCopied",{count:nodeCount}),{id:"clipboard"}); RED.notify(RED._("clipboard.nodeCopied",{count:nodeCount}),{id:"clipboard"});
} else if (groupCount > 0) { } else if (groupCount > 0) {
@ -3807,7 +3835,6 @@ RED.view = (function() {
.attr("class", "red-ui-flow-node red-ui-flow-node-group") .attr("class", "red-ui-flow-node red-ui-flow-node-group")
.classed("red-ui-flow-subflow", activeSubflow != null); .classed("red-ui-flow-subflow", activeSubflow != null);
nodeEnter.each(function(d,i) { nodeEnter.each(function(d,i) {
this.__outputs__ = []; this.__outputs__ = [];
this.__inputs__ = []; this.__inputs__ = [];
@ -3953,7 +3980,12 @@ RED.view = (function() {
RED.hooks.trigger("viewAddNode",{node:d,el:this}) RED.hooks.trigger("viewAddNode",{node:d,el:this})
}); });
var nodesReordered = false;
node.each(function(d,i) { node.each(function(d,i) {
if (d._reordered) {
nodesReordered = true;
delete d._reordered;
}
if (d.dirty) { if (d.dirty) {
var self = this; var self = this;
var thisNode = d3.select(this); var thisNode = d3.select(this);
@ -4261,6 +4293,13 @@ RED.view = (function() {
RED.hooks.trigger("viewRedrawNode",{node:d,el:this}) RED.hooks.trigger("viewRedrawNode",{node:d,el:this})
}); });
if (nodesReordered) {
node.sort(function(a,b) {
return a._index - b._index;
})
}
var link = linkLayer.selectAll(".red-ui-flow-link").data( var link = linkLayer.selectAll(".red-ui-flow-link").data(
activeLinks, activeLinks,
function(d) { function(d) {
@ -4499,7 +4538,7 @@ RED.view = (function() {
if (a._root === b._root) { if (a._root === b._root) {
return a._depth - b._depth; return a._depth - b._depth;
} else { } else {
return a._root.localeCompare(b._root); return a._index - b._index;
} }
}) })
} }

View File

@ -23,6 +23,9 @@ RED.workspaces = (function() {
var viewStack = []; var viewStack = [];
var viewStackPos = 0; var viewStackPos = 0;
function isSameObj(env0, env1) {
return (JSON.stringify(env0) === JSON.stringify(env1));
}
function addToViewStack(id) { function addToViewStack(id) {
if (viewStackPos !== viewStack.length) { if (viewStackPos !== viewStack.length) {
@ -43,7 +46,7 @@ RED.workspaces = (function() {
workspaceIndex += 1; workspaceIndex += 1;
} while ($("#red-ui-workspace-tabs a[title='"+RED._('workspace.defaultName',{number:workspaceIndex})+"']").size() !== 0); } while ($("#red-ui-workspace-tabs a[title='"+RED._('workspace.defaultName',{number:workspaceIndex})+"']").size() !== 0);
ws = {type:"tab",id:tabId,disabled: false,info:"",label:RED._('workspace.defaultName',{number:workspaceIndex})}; ws = {type:"tab",id:tabId,disabled: false,info:"",label:RED._('workspace.defaultName',{number:workspaceIndex}),env:[]};
RED.nodes.addWorkspace(ws,targetIndex); RED.nodes.addWorkspace(ws,targetIndex);
workspace_tabs.addTab(ws,targetIndex); workspace_tabs.addTab(ws,targetIndex);
workspace_tabs.activateTab(tabId); workspace_tabs.activateTab(tabId);
@ -55,6 +58,7 @@ RED.workspaces = (function() {
RED.view.focus(); RED.view.focus();
return ws; return ws;
} }
function deleteWorkspace(ws) { function deleteWorkspace(ws) {
if (workspaceTabCount === 1) { if (workspaceTabCount === 1) {
return; return;
@ -78,165 +82,9 @@ RED.workspaces = (function() {
if (subflow) { if (subflow) {
RED.editor.editSubflow(subflow); RED.editor.editSubflow(subflow);
} }
return; } else {
RED.editor.editFlow(workspace);
} }
RED.view.state(RED.state.EDITING);
var tabflowEditor;
var trayOptions = {
title: RED._("workspace.editFlow",{name:RED.utils.sanitize(workspace.label)}),
buttons: [
{
id: "node-dialog-delete",
class: 'leftButton'+((workspaceTabCount === 1)?" disabled":""),
text: RED._("common.label.delete"), //'<i class="fa fa-trash"></i>',
click: function() {
deleteWorkspace(workspace);
RED.tray.close();
}
},
{
id: "node-dialog-cancel",
text: RED._("common.label.cancel"),
click: function() {
RED.tray.close();
}
},
{
id: "node-dialog-ok",
class: "primary",
text: RED._("common.label.done"),
click: function() {
var label = $( "#node-input-name" ).val();
var changed = false;
var changes = {};
if (workspace.label != label) {
changes.label = workspace.label;
changed = true;
workspace.label = label;
workspace_tabs.renameTab(workspace.id,label);
}
var disabled = $("#node-input-disabled").prop("checked");
if (workspace.disabled !== disabled) {
changes.disabled = workspace.disabled;
changed = true;
workspace.disabled = disabled;
}
var info = tabflowEditor.getValue();
if (workspace.info !== info) {
changes.info = workspace.info;
changed = true;
workspace.info = info;
}
$("#red-ui-tab-"+(workspace.id.replace(".","-"))).toggleClass('red-ui-workspace-disabled',!!workspace.disabled);
$("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!workspace.disabled);
if (changed) {
var historyEvent = {
t: "edit",
changes:changes,
node: workspace,
dirty: RED.nodes.dirty()
}
workspace.changed = true;
RED.history.push(historyEvent);
RED.nodes.dirty(true);
RED.sidebar.config.refresh();
if (changes.hasOwnProperty('disabled')) {
RED.nodes.eachNode(function(n) {
if (n.z === workspace.id) {
n.dirty = true;
}
});
RED.view.redraw();
}
RED.events.emit("flows:change",workspace);
}
RED.tray.close();
}
}
],
resize: function(dimensions) {
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var editorRow = $("#dialog-form>div.node-text-editor-row");
var height = $("#dialog-form").height();
for (var i=0; i<rows.size(); i++) {
height -= $(rows[i]).outerHeight(true);
}
height -= (parseInt($("#dialog-form").css("marginTop"))+parseInt($("#dialog-form").css("marginBottom")));
$(".node-text-editor").css("height",height+"px");
tabflowEditor.resize();
},
open: function(tray) {
var trayFooter = tray.find(".red-ui-tray-footer");
var trayBody = tray.find('.red-ui-tray-body');
var trayFooterLeft = $('<div class="red-ui-tray-footer-left"></div>').appendTo(trayFooter)
var dialogForm = $('<form id="dialog-form" class="form-horizontal"></form>').appendTo(trayBody);
$('<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" data-i18n="[placeholder]common.label.name">'+
'</div>').appendTo(dialogForm);
if (!workspace.hasOwnProperty("disabled")) {
workspace.disabled = false;
}
$('<input id="node-input-disabled" type="checkbox">').prop("checked",workspace.disabled).appendTo(trayFooterLeft).toggleButton({
enabledIcon: "fa-circle-thin",
disabledIcon: "fa-ban",
invertState: true
})
var row = $('<div class="form-row node-text-editor-row">'+
'<label for="node-input-info" data-i18n="editor:workspace.info" style="width:300px;"></label>'+
'<div style="min-height:250px;" class="node-text-editor" id="node-input-info"></div>'+
'</div>').appendTo(dialogForm);
tabflowEditor = RED.editor.createEditor({
id: 'node-input-info',
mode: 'ace/mode/markdown',
value: ""
});
$('#node-info-input-info-expand').on("click", function(e) {
e.preventDefault();
var value = tabflowEditor.getValue();
RED.editor.editMarkdown({
value: value,
width: "Infinity",
cursor: tabflowEditor.getCursorPosition(),
complete: function(v,cursor) {
tabflowEditor.setValue(v, -1);
tabflowEditor.gotoLine(cursor.row+1,cursor.column,false);
setTimeout(function() {
tabflowEditor.focus();
},300);
}
})
});
$('<input type="text" style="display: none;" />').prependTo(dialogForm);
dialogForm.on("submit", function(e) { e.preventDefault();});
$("#node-input-name").val(workspace.label);
RED.text.bidi.prepareInput($("#node-input-name"));
tabflowEditor.getSession().setValue(workspace.info || "", -1);
dialogForm.i18n();
},
close: function() {
if (RED.view.state() != RED.state.IMPORT_DRAGGING) {
RED.view.state(RED.state.DEFAULT);
}
var selection = RED.view.selection();
if (!selection.nodes && !selection.links && workspace.id === activeWorkspace) {
RED.sidebar.info.refresh(workspace);
}
tabflowEditor.destroy();
}
}
RED.tray.show(trayOptions);
} }
@ -293,7 +141,14 @@ RED.workspaces = (function() {
} }
}, },
onreorder: function(oldOrder, newOrder) { onreorder: function(oldOrder, newOrder) {
RED.history.push({t:'reorder',order:oldOrder,dirty:RED.nodes.dirty()}); RED.history.push({
t:'reorder',
workspaces: {
from:oldOrder,
to:newOrder
},
dirty:RED.nodes.dirty()
});
RED.nodes.dirty(true); RED.nodes.dirty(true);
setWorkspaceOrder(newOrder); setWorkspaceOrder(newOrder);
}, },
@ -445,7 +300,6 @@ RED.workspaces = (function() {
} }
} }
function removeWorkspace(ws) { function removeWorkspace(ws) {
if (!ws) { if (!ws) {
deleteWorkspace(RED.nodes.workspace(activeWorkspace)); deleteWorkspace(RED.nodes.workspace(activeWorkspace));
@ -474,7 +328,10 @@ RED.workspaces = (function() {
return { return {
init: init, init: init,
add: addWorkspace, add: addWorkspace,
// remove: remove workspace without editor history etc
remove: removeWorkspace, remove: removeWorkspace,
// delete: remove workspace and update editor history
delete: deleteWorkspace,
order: setWorkspaceOrder, order: setWorkspaceOrder,
edit: editWorkspace, edit: editWorkspace,
contains: function(id) { contains: function(id) {

View File

@ -68,6 +68,10 @@
& > .disabled > a:hover, & > .disabled > a:hover,
& > .disabled > a:focus { & > .disabled > a:focus {
color: $menuDisabledColor; color: $menuDisabledColor;
.red-ui-popover-key {
color: $menuDisabledColor;
border-color: $menuDisabledColor;
}
} }
& > .disabled > a:hover, & > .disabled > a:hover,
@ -102,6 +106,14 @@
display: none; display: none;
} }
} }
.red-ui-popover-key {
border: none;
padding: 0;
font-size: 13px;
float: right;
color: $menuColor;
border-color: $menuColor;
}
} }
} }
@ -209,4 +221,4 @@ ul.red-ui-menu:not(.red-ui-menu-dropdown) {
} }
} }
} }

View File

@ -191,14 +191,17 @@
margin-top: 0; margin-top: 0;
li a { li a {
color: $header-menu-color; color: $header-menu-color;
padding: 3px 40px; padding: 3px 10px 3px 40px;
img { img {
max-width: 100%; max-width: 100%;
margin-right: 10px; margin-right: 10px;
padding: 4px; padding: 4px;
border: 3px solid transparent; border: 3px solid transparent;
} }
.red-ui-popover-key {
color: $header-menu-color-disabled !important;
border-color: $header-menu-color-disabled !important;
}
&.active img { &.active img {
border: 3px solid $header-menu-item-border-active; border: 3px solid $header-menu-item-border-active;
} }

View File

@ -104,7 +104,7 @@
} }
.red-ui-tabs-fade { .red-ui-tabs-fade {
background-image: linear-gradient(to right, rgba(255,255,255,0.001), $tab-background-active); background-image: linear-gradient(to right, change-color($tab-background-active, $alpha: 0.001), $tab-background-active);
} }
} }
@ -112,7 +112,7 @@
&:not(.active) { &:not(.active) {
background: $tab-background-selected; background: $tab-background-selected;
.red-ui-tabs-fade { .red-ui-tabs-fade {
background-image: linear-gradient(to right, rgba(255,255,255,0.001), $tab-background-selected); background-image: linear-gradient(to right, change-color($tab-background-selected, $alpha: 0.001), $tab-background-selected);
} }
.red-ui-tabs-badge-selected { .red-ui-tabs-badge-selected {
background: $tab-background-selected; background: $tab-background-selected;
@ -131,6 +131,9 @@
&:not(.active) a:hover { &:not(.active) a:hover {
color: $workspace-button-color-hover; color: $workspace-button-color-hover;
background: $tab-background-hover; background: $tab-background-hover;
&+.red-ui-tabs-fade {
background-image: linear-gradient(to right, change-color($tab-background-hover, $alpha: 0.001), $tab-background-hover);
}
} }
} }
} }
@ -308,7 +311,7 @@
top: 0; top: 0;
right: 0; right: 0;
width: 15px; width: 15px;
background-image: linear-gradient(to right, rgba(255,255,255,0.001), $tab-background-inactive); background-image: linear-gradient(to right, change-color($tab-background-inactive, $alpha: 0.001), $tab-background-inactive);
pointer-events: none; pointer-events: none;
} }

View File

@ -101,6 +101,9 @@
} }
.red-ui-treeList-label-text { .red-ui-treeList-label-text {
margin-left: 4px; margin-left: 4px;
&:empty {
min-height: 20px;
}
} }
.red-ui-treeList-sublabel-text { .red-ui-treeList-sublabel-text {
top: 0; top: 0;

View File

@ -23,7 +23,7 @@
--> -->
<title>{{ page.title }}</title> <title>{{ page.title }}</title>
<link rel="icon" type="image/png" href="{{ page.favicon }}"> <link rel="icon" type="image/png" href="{{ page.favicon }}">
<link rel="mask-icon" href="{{ page.tabicon }}" color="#8f0000"> <link rel="mask-icon" href="{{ page.tabicon.icon }}" color="{{ page.tabicon.colour }}">
<link rel="stylesheet" href="vendor/jquery/css/base/jquery-ui.min.css"> <link rel="stylesheet" href="vendor/jquery/css/base/jquery-ui.min.css">
<link rel="stylesheet" href="vendor/font-awesome/css/font-awesome.min.css"> <link rel="stylesheet" href="vendor/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="red/style.min.css"> <link rel="stylesheet" href="red/style.min.css">

View File

@ -193,7 +193,7 @@
} }
/** Perform inject, optionally sending a custom msg (refactored for re-use in the form inject button)*/ /** Perform inject, optionally sending a custom msg (refactored for re-use in the form inject button)*/
function doInject(node, customMsg) { function doInject(node, customMsg) {
var label = node._def.label.call(node); var label = node._def.label.call(node,customMsg?customMsg.__user_inject_props__:undefined);
if (label.length > 30) { if (label.length > 30) {
label = label.substring(0, 50) + "..."; label = label.substring(0, 50) + "...";
} }
@ -201,7 +201,8 @@
$.ajax({ $.ajax({
url: "inject/" + node.id, url: "inject/" + node.id,
type: "POST", type: "POST",
data: customMsg, data: JSON.stringify(customMsg||{}),
contentType: "application/json; charset=utf-8",
success: function (resp) { success: function (resp) {
RED.notify(node._("inject.success", { label: label }), { type: "success", id: "inject", timeout: 2000 }); RED.notify(node._("inject.success", { label: label }), { type: "success", id: "inject", timeout: 2000 });
}, },
@ -291,7 +292,7 @@
} }
return lab; return lab;
}, },
label: function() { label: function(customProps) {
var suffix = ""; var suffix = "";
// if fire once then add small indication // if fire once then add small indication
if (this.once) { if (this.once) {
@ -304,11 +305,23 @@
if (this.name) { if (this.name) {
return this.name+suffix; return this.name+suffix;
} }
var payload = "";
var payload = this.payload || ""; var payloadType = "str";
var payloadType = this.payloadType || "str"; var topic = "";
var topic = this.topic || ""; if (customProps) {
for (var i=0;i<customProps.length;i++) {
if (customProps[i].p === "payload") {
payload = customProps[i].v;
payloadType = customProps[i].vt;
} else if (customProps[i].p === "topic") {
topic = customProps[i].v;
}
}
} else {
payload = this.payload || "";
payloadType = this.payloadType || "str";
topic = this.topic || "";
}
if (payloadType === "string" || if (payloadType === "string" ||
payloadType === "str" || payloadType === "str" ||
payloadType === "num" || payloadType === "num" ||
@ -496,11 +509,8 @@
label: node._("inject.injectNow"), label: node._("inject.injectNow"),
click: function(e) { click: function(e) {
var items = eList.editableList('items'); var items = eList.editableList('items');
var result = getProps(items); var props = getProps(items);
var m = {__user_inject_props__: []}; var m = {__user_inject_props__: props.props};
if (result && result.props && result.props.length) {
m.__user_inject_props__ = result.props;
}
doInject(node, m); doInject(node, m);
} }
} }

View File

@ -101,7 +101,7 @@ module.exports = function(RED) {
this.on("input", function(msg, send, done) { this.on("input", function(msg, send, done) {
var errors = []; var errors = [];
var props = this.props; var props = this.props;
if(msg.__user_inject_props__ && Array.isArray(msg.__user_inject_props__)) { if (msg.__user_inject_props__ && Array.isArray(msg.__user_inject_props__)) {
props = msg.__user_inject_props__; props = msg.__user_inject_props__;
} }
delete msg.__user_inject_props__; delete msg.__user_inject_props__;

View File

@ -27,10 +27,14 @@
<label for="node-input-property"><i class="fa fa-ellipsis-h"></i> <span data-i18n="node-red:common.label.property"></span></label> <label for="node-input-property"><i class="fa fa-ellipsis-h"></i> <span data-i18n="node-red:common.label.property"></span></label>
<input type="text" id="node-input-property" style="width:70%;"/> <input type="text" id="node-input-property" style="width:70%;"/>
</div> </div>
<div class="form-row"> <div class="form-row" style="margin-bottom: 0px;">
<label> </label> <label> </label>
<input type="checkbox" id="node-input-septopics" style="display:inline-block; width:20px; vertical-align:baseline;"> <input type="checkbox" id="node-input-septopics" style="display:inline-block; width:20px; vertical-align:baseline;">
<span data-i18n="rbe.label.septopics"></span> <input type="text" id="node-input-topi" style="width:27%;"/> <label style="width: auto" for="node-input-septopics" data-i18n="rbe.label.septopics"></label>
</div>
<div class="form-row">
<label> </label>
<input type="text" id="node-input-topi" style="width:70%;"/>
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="rbe.label.name"></span></label> <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="rbe.label.name"></span></label>
@ -70,6 +74,10 @@
if (this.septopics === undefined) { if (this.septopics === undefined) {
$("#node-input-septopics").prop('checked', true); $("#node-input-septopics").prop('checked', true);
} }
if (this.topi === undefined) {
$("#node-input-topi").val("topic");
}
$("#node-input-property").typedInput({default:'msg',types:['msg']}); $("#node-input-property").typedInput({default:'msg',types:['msg']});
$("#node-input-topi").typedInput({default:'msg',types:['msg']}); $("#node-input-topi").typedInput({default:'msg',types:['msg']});
//$( "#node-input-gap" ).spinner({min:0}); //$( "#node-input-gap" ).spinner({min:0});
@ -88,6 +96,11 @@
$("#node-startvalue").hide(); $("#node-startvalue").hide();
} }
}); });
$("#node-input-septopics").on("change", function() {
$("#node-input-topi").typedInput("disable",!this.checked);
})
$("#node-input-topi").typedInput("disable",!!!this.septopics);
} }
}); });
</script> </script>

View File

@ -134,7 +134,29 @@ in your Node-RED user directory (${RED.settings.userDir}).
url = "http://"+url; url = "http://"+url;
} }
} }
url = encodeURI(url);
// The Request module used in Node-RED 1.x was tolerant of query strings that
// were partially encoded. For example - "?a=hello%20there&b=20%"
// The GOT module doesn't like that.
// The following is an attempt to normalise the url to ensure it is properly
// encoded. We cannot just encode it directly as we don't want any valid
// encoded entity to end up doubly encoded.
if (url.indexOf("?") > -1) {
// Only do this if there is a query string to deal with
const [hostPath, ...queryString] = url.split("?")
const query = queryString.join("?");
if (query) {
// Look for any instance of % not followed by two hex chars.
// Replace any we find with %25.
const escapedQueryString = query.replace(/(%.?.?)/g, function(v) {
if (/^%[a-f0-9]{2}/i.test(v)) {
return v;
}
return v.replace(/%/,"%25")
})
url = hostPath+"?"+escapedQueryString;
}
}
var method = nodeMethod.toUpperCase() || "GET"; var method = nodeMethod.toUpperCase() || "GET";
if (msg.method && n.method && (n.method !== "use")) { // warn if override option not set if (msg.method && n.method && (n.method !== "use")) { // warn if override option not set
@ -449,6 +471,14 @@ in your Node-RED user directory (${RED.settings.userDir}).
if (tlsNode) { if (tlsNode) {
opts.https = {}; opts.https = {};
tlsNode.addTLSOptions(opts.https); tlsNode.addTLSOptions(opts.https);
if (opts.https.ca) {
opts.https.certificateAuthority = opts.https.ca;
delete opts.https.ca;
}
if (opts.https.cert) {
opts.https.certificate = opts.https.cert;
delete opts.https.cert;
}
} else { } else {
if (msg.hasOwnProperty('rejectUnauthorized')) { if (msg.hasOwnProperty('rejectUnauthorized')) {
opts.https = { rejectUnauthorized: msg.rejectUnauthorized }; opts.https = { rejectUnauthorized: msg.rejectUnauthorized };

View File

@ -629,9 +629,6 @@ module.exports = function(RED) {
if (node.build === 'object') { if (node.build === 'object') {
propertyKey = RED.util.getMessageProperty(msg,node.key); propertyKey = RED.util.getMessageProperty(msg,node.key);
} }
if (msg.hasOwnProperty("parts")) {
propertyIndex = msg.parts.index;
}
} }
if (msg.hasOwnProperty("reset")) { if (msg.hasOwnProperty("reset")) {

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/nodes", "name": "@node-red/nodes",
"version": "2.0.3", "version": "2.0.6",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",
@ -17,7 +17,7 @@
"dependencies": { "dependencies": {
"acorn": "8.4.1", "acorn": "8.4.1",
"acorn-walk": "8.1.1", "acorn-walk": "8.1.1",
"ajv": "8.6.0", "ajv": "8.6.2",
"body-parser": "1.19.0", "body-parser": "1.19.0",
"cheerio": "1.0.0-rc.10", "cheerio": "1.0.0-rc.10",
"content-type": "1.0.4", "content-type": "1.0.4",
@ -37,7 +37,7 @@
"js-yaml": "3.14.1", "js-yaml": "3.14.1",
"media-typer": "1.1.0", "media-typer": "1.1.0",
"mqtt": "4.2.8", "mqtt": "4.2.8",
"multer": "1.4.2", "multer": "1.4.3",
"mustache": "4.2.0", "mustache": "4.2.0",
"on-headers": "1.0.2", "on-headers": "1.0.2",
"raw-body": "2.4.1", "raw-body": "2.4.1",

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/registry", "name": "@node-red/registry",
"version": "2.0.3", "version": "2.0.6",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,11 +16,11 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/util": "2.0.3", "@node-red/util": "2.0.6",
"clone": "2.1.2", "clone": "2.1.2",
"fs-extra": "10.0.0", "fs-extra": "10.0.0",
"semver": "7.3.5", "semver": "7.3.5",
"tar": "6.1.0", "tar": "6.1.11",
"uglify-js": "3.13.10" "uglify-js": "3.14.1"
} }
} }

View File

@ -233,7 +233,9 @@ var api = module.exports = {
} }
var sendCredentials = {}; var sendCredentials = {};
var cred; var cred;
if (/^subflow(:|$)/.test(opts.type)) { if (/^subflow(:|$)/.test(opts.type) ||
(opts.type === "tab") ||
(opts.type === "group")) {
for (cred in credentials) { for (cred in credentials) {
if (credentials.hasOwnProperty(cred)) { if (credentials.hasOwnProperty(cred)) {
sendCredentials['has_'+cred] = credentials[cred] != null && credentials[cred] !== ''; sendCredentials['has_'+cred] = credentials[cred] != null && credentials[cred] !== '';

View File

@ -20,6 +20,7 @@ const events = require("@node-red/util").events;
var flowUtil = require("./util"); var flowUtil = require("./util");
const context = require('../nodes/context'); const context = require('../nodes/context');
const hooks = require("@node-red/util").hooks; const hooks = require("@node-red/util").hooks;
const credentials = require("../nodes/credentials");
var Subflow; var Subflow;
var Log; var Log;
@ -396,6 +397,17 @@ class Flow {
return undefined; return undefined;
} }
/**
* Get a group node instance
* @param {String} id
* @return {Node} group node
*/
getGroupNode(id) {
const groups = this.global.groups;
return groups[id];
}
/** /**
* Get all of the nodes instantiated within this flow * Get all of the nodes instantiated within this flow
* @return {[type]} [description] * @return {[type]} [description]
@ -404,6 +416,82 @@ class Flow {
return this.activeNodes; return this.activeNodes;
} }
/*!
* Get value of environment variable defined in group node.
* @param {String} group - group node
* @param {String} name - name of variable
* @return {Object} object containing the value in val property or null if not defined
*/
getGroupEnvSetting(node, group, name) {
if (group) {
if (group.credentials === undefined) {
group.credentials = credentials.get(group.id) || {};
}
if (!name.startsWith("$parent.")) {
if (group.env) {
if (!group._env) {
const envs = group.env;
const entries = envs.map((env) => {
if (env.type === "cred") {
const cred = group.credentials;
if (cred.hasOwnProperty(env.name)) {
env.value = cred[env.name];
}
}
return [env.name, env];
return [env.name, env];
});
group._env = Object.fromEntries(entries);
}
const env = group._env[name];
if (env) {
let value = env.value;
const type = env.type;
if ((type !== "env") ||
(value !== name)) {
if (type === "env") {
value = value.replace(new RegExp("\\${"+name+"}","g"),"${$parent."+name+"}");
}
if (type === "bool") {
const val
= ((value === "true") ||
(value === true));
return {
val: val
};
}
if (type === "cred") {
return {
val: value
};
}
try {
var val = redUtil.evaluateNodeProperty(value, type, node, null, null);
return {
val: val
};
}
catch (e) {
this.error(e);
return null;
}
}
}
}
}
else {
name = name.substring(8);
}
if (group.g) {
const parent = this.getGroupNode(group.g);
return this.getGroupEnvSetting(node, parent, name);
}
}
return null;
}
/** /**
* Get a flow setting value. This currently automatically defers to the parent * Get a flow setting value. This currently automatically defers to the parent
* flow which, as defined in ./index.js returns `process.env[key]`. * flow which, as defined in ./index.js returns `process.env[key]`.
@ -412,6 +500,52 @@ class Flow {
* @return {[type]} [description] * @return {[type]} [description]
*/ */
getSetting(key) { getSetting(key) {
const flow = this.flow;
if (flow.credentials === undefined) {
flow.credentials = credentials.get(flow.id) || {};
}
if (flow.env) {
if (!key.startsWith("$parent.")) {
if (!flow._env) {
const envs = flow.env;
const entries = envs.map((env) => {
if (env.type === "cred") {
const cred = flow.credentials;
if (cred.hasOwnProperty(env.name)) {
env.value = cred[env.name];
}
}
return [env.name, env]
});
flow._env = Object.fromEntries(entries);
}
const env = flow._env[key];
if (env) {
let value = env.value;
const type = env.type;
if ((type !== "env") || (value !== key)) {
if (type === "env") {
value = value.replace(new RegExp("\\${"+key+"}","g"),"${$parent."+key+"}");
}
try {
if (type === "bool") {
const val = ((value === "true") ||
(value === true));
return val;
}
if (type === "cred") {
return value;
}
var val = redUtil.evaluateNodeProperty(value, type, null, null, null);
return val;
}
catch (e) {
this.error(e);
}
}
}
}
}
return this.parent.getSetting(key); return this.parent.getSetting(key);
} }

View File

@ -370,6 +370,16 @@ class Subflow extends Flow {
// name starts $parent. ... so delegate to parent automatically // name starts $parent. ... so delegate to parent automatically
name = name.substring(8); name = name.substring(8);
} }
const node = this.subflowInstance;
if (node.g) {
const group = this.getGroupNode(node.g);
const result = this.getGroupEnvSetting(node, group, name);
if (result) {
return result.val;
}
}
var parent = this.parent; var parent = this.parent;
if (parent) { if (parent) {
var val = parent.getSetting(name); var val = parent.getSetting(name);

View File

@ -539,6 +539,9 @@ async function addFlow(flow, user) {
if (flow.hasOwnProperty('disabled')) { if (flow.hasOwnProperty('disabled')) {
tabNode.disabled = flow.disabled; tabNode.disabled = flow.disabled;
} }
if (flow.hasOwnProperty('env')) {
tabNode.env = flow.env;
}
var nodes = [tabNode]; var nodes = [tabNode];
@ -599,6 +602,9 @@ function getFlow(id) {
if (flow.hasOwnProperty('info')) { if (flow.hasOwnProperty('info')) {
result.info = flow.info; result.info = flow.info;
} }
if (flow.hasOwnProperty('env')) {
result.env = flow.env;
}
if (id !== 'global') { if (id !== 'global') {
result.nodes = []; result.nodes = [];
} }
@ -694,6 +700,12 @@ async function updateFlow(id,newFlow, user) {
if (newFlow.hasOwnProperty('disabled')) { if (newFlow.hasOwnProperty('disabled')) {
tabNode.disabled = newFlow.disabled; tabNode.disabled = newFlow.disabled;
} }
if (newFlow.hasOwnProperty('env')) {
tabNode.env = newFlow.env;
}
if (newFlow.hasOwnProperty('credentials')) {
tabNode.credentials = newFlow.credentials;
}
nodes = [tabNode].concat(newFlow.nodes||[]).concat(newFlow.configs||[]); nodes = [tabNode].concat(newFlow.nodes||[]).concat(newFlow.configs||[]);
nodes.forEach(function(n) { nodes.forEach(function(n) {

View File

@ -44,24 +44,25 @@ function diffNodes(oldNode,newNode) {
var EnvVarPropertyRE_old = /^\$\((\S+)\)$/; var EnvVarPropertyRE_old = /^\$\((\S+)\)$/;
var EnvVarPropertyRE = /^\${(\S+)}$/; var EnvVarPropertyRE = /^\${(\S+)}$/;
function mapEnvVarProperties(obj,prop,flow) {
function mapEnvVarProperties(obj,prop,flow,config) {
var v = obj[prop]; var v = obj[prop];
if (Buffer.isBuffer(v)) { if (Buffer.isBuffer(v)) {
return; return;
} else if (Array.isArray(v)) { } else if (Array.isArray(v)) {
for (var i=0;i<v.length;i++) { for (var i=0;i<v.length;i++) {
mapEnvVarProperties(v,i,flow); mapEnvVarProperties(v,i,flow,config);
} }
} else if (typeof obj[prop] === 'string') { } else if (typeof obj[prop] === 'string') {
if (obj[prop][0] === "$" && (EnvVarPropertyRE_old.test(v) || EnvVarPropertyRE.test(v)) ) { if (obj[prop][0] === "$" && (EnvVarPropertyRE_old.test(v) || EnvVarPropertyRE.test(v)) ) {
var envVar = v.substring(2,v.length-1); var envVar = v.substring(2,v.length-1);
var r = flow.getSetting(envVar); var r = redUtil.getSetting(config, envVar, flow);
obj[prop] = r!==undefined?r:obj[prop]; obj[prop] = r ? r : obj[prop];
} }
} else { } else {
for (var p in v) { for (var p in v) {
if (v.hasOwnProperty(p)) { if (v.hasOwnProperty(p)) {
mapEnvVarProperties(v,p,flow); mapEnvVarProperties(v,p,flow,config);
} }
} }
} }
@ -78,7 +79,7 @@ function createNode(flow,config) {
delete conf.credentials; delete conf.credentials;
for (var p in conf) { for (var p in conf) {
if (conf.hasOwnProperty(p)) { if (conf.hasOwnProperty(p)) {
mapEnvVarProperties(conf,p,flow); mapEnvVarProperties(conf,p,flow,conf);
} }
} }
try { try {

View File

@ -38,6 +38,7 @@ function Node(n) {
this.id = n.id; this.id = n.id;
this.type = n.type; this.type = n.type;
this.z = n.z; this.z = n.z;
this.g = n.g;
this._closeCallbacks = []; this._closeCallbacks = [];
this._inputCallback = null; this._inputCallback = null;
this._inputCallbacks = null; this._inputCallbacks = null;

View File

@ -103,7 +103,7 @@ function createNode(node,def) {
// allow $(foo) syntax to substitute env variables for credentials also... // allow $(foo) syntax to substitute env variables for credentials also...
for (var p in creds) { for (var p in creds) {
if (creds.hasOwnProperty(p)) { if (creds.hasOwnProperty(p)) {
flowUtil.mapEnvVarProperties(creds,p,node._flow); flowUtil.mapEnvVarProperties(creds,p,node._flow,node);
} }
} }
node.credentials = creds; node.credentials = creds;

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/runtime", "name": "@node-red/runtime",
"version": "2.0.3", "version": "2.0.6",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,9 +16,9 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/registry": "2.0.3", "@node-red/registry": "2.0.6",
"@node-red/util": "2.0.3", "@node-red/util": "2.0.6",
"async-mutex": "0.3.1", "async-mutex": "0.3.2",
"clone": "2.1.2", "clone": "2.1.2",
"express": "4.17.1", "express": "4.17.1",
"fs-extra": "10.0.0", "fs-extra": "10.0.0",

View File

@ -521,12 +521,17 @@ function setObjectProperty(msg,prop,value,createMissing) {
* @param {String} name - name of variable * @param {String} name - name of variable
* @return {String} value of env var * @return {String} value of env var
*/ */
function getSetting(node, name) { function getSetting(node, name, flow_) {
if (node && node._flow) { var flow = (flow_ ? flow_ : (node ? node._flow : null));
var flow = node._flow; if (flow) {
if (flow) { if (node && node.g) {
return flow.getSetting(name); const group = flow.getGroupNode(node.g);
const result = flow.getGroupEnvSetting(node, group, name);
if (result) {
return result.val;
}
} }
return flow.getSetting(name);
} }
return process.env[name]; return process.env[name];
} }
@ -544,18 +549,19 @@ function getSetting(node, name) {
* @memberof @node-red/util_util * @memberof @node-red/util_util
*/ */
function evaluateEnvProperty(value, node) { function evaluateEnvProperty(value, node) {
var flow = (node && node.hasOwnProperty("_flow")) ? node._flow : null;
var result; var result;
if (/^\${[^}]+}$/.test(value)) { if (/^\${[^}]+}$/.test(value)) {
// ${ENV_VAR} // ${ENV_VAR}
var name = value.substring(2,value.length-1); var name = value.substring(2,value.length-1);
result = getSetting(node, name); result = getSetting(node, name, flow);
} else if (!/\${\S+}/.test(value)) { } else if (!/\${\S+}/.test(value)) {
// ENV_VAR // ENV_VAR
result = getSetting(node, value); result = getSetting(node, value, flow);
} else { } else {
// FOO${ENV_VAR}BAR // FOO${ENV_VAR}BAR
return value.replace(/\${([^}]+)}/g, function(match, name) { return value.replace(/\${([^}]+)}/g, function(match, name) {
var val = getSetting(node, name); var val = getSetting(node, name, flow);
return (val === undefined)?"":val; return (val === undefined)?"":val;
}); });
} }
@ -668,7 +674,7 @@ function prepareJSONataExpression(value,node) {
return node.context().global.get(val, store); return node.context().global.get(val, store);
}); });
expr.assign('env', function(name) { expr.assign('env', function(name) {
var val = getSetting(node, name); var val = getSetting(node, name, node._flow);
if (typeof val !== 'undefined') { if (typeof val !== 'undefined') {
return val; return val;
} else { } else {
@ -976,5 +982,6 @@ module.exports = {
normaliseNodeTypeName: normaliseNodeTypeName, normaliseNodeTypeName: normaliseNodeTypeName,
prepareJSONataExpression: prepareJSONataExpression, prepareJSONataExpression: prepareJSONataExpression,
evaluateJSONataExpression: evaluateJSONataExpression, evaluateJSONataExpression: evaluateJSONataExpression,
parseContextStore: parseContextStore parseContextStore: parseContextStore,
getSetting: getSetting
}; };

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/util", "name": "@node-red/util",
"version": "2.0.3", "version": "2.0.6",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",
@ -18,7 +18,7 @@
"fs-extra": "10.0.0", "fs-extra": "10.0.0",
"i18next": "20.3.2", "i18next": "20.3.2",
"json-stringify-safe": "5.0.1", "json-stringify-safe": "5.0.1",
"jsonata": "1.8.4", "jsonata": "1.8.5",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"moment-timezone": "0.5.33" "moment-timezone": "0.5.33"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "node-red", "name": "node-red",
"version": "2.0.3", "version": "2.0.6",
"description": "Low-code programming for event-driven applications", "description": "Low-code programming for event-driven applications",
"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": "2.0.3", "@node-red/editor-api": "2.0.6",
"@node-red/runtime": "2.0.3", "@node-red/runtime": "2.0.6",
"@node-red/util": "2.0.3", "@node-red/util": "2.0.6",
"@node-red/nodes": "2.0.3", "@node-red/nodes": "2.0.6",
"basic-auth": "2.0.1", "basic-auth": "2.0.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"express": "4.17.1", "express": "4.17.1",

View File

@ -234,8 +234,13 @@ httpsPromise.then(function(startupHttps) {
// Get the result of the function, because createServer doesn't accept functions as input // Get the result of the function, because createServer doesn't accept functions as input
Promise.resolve(settings.https()).then(function(refreshedHttps) { Promise.resolve(settings.https()).then(function(refreshedHttps) {
if (refreshedHttps) { if (refreshedHttps) {
// The key/cert needs to be updated in the NodeJs http(s) server, when no key/cert is yet available or when the key/cert has changed.
// Note that the refreshed key/cert can be supplied as a string or a buffer.
var updateKey = (server.key == undefined || (Buffer.isBuffer(server.key) && !server.key.equals(refreshedHttps.key)) || (typeof server.key == "string" && server.key != refreshedHttps.key));
var updateCert = (server.cert == undefined || (Buffer.isBuffer(server.cert) && !server.cert.equals(refreshedHttps.cert)) || (typeof server.cert == "string" && server.cert != refreshedHttps.cert));
// Only update the credentials in the server when key or cert has changed // Only update the credentials in the server when key or cert has changed
if(!server.key || !server.cert || !server.key.equals(refreshedHttps.key) || !server.cert.equals(refreshedHttps.cert)) { if(updateKey || updateCert) {
server.setSecureContext(refreshedHttps); server.setSecureContext(refreshedHttps);
RED.log.info(RED.log._("server.https.settings-refreshed")); RED.log.info(RED.log._("server.https.settings-refreshed"));
} }

View File

@ -4,6 +4,8 @@ const path = require("path");
const fs = require("fs-extra"); const fs = require("fs-extra");
const should = require("should"); const should = require("should");
const LATEST = "2";
function generateScript() { function generateScript() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const packages = [ const packages = [
@ -18,7 +20,13 @@ function generateScript() {
const rootPackage = require(path.join(__dirname,"..","package.json")); const rootPackage = require(path.join(__dirname,"..","package.json"));
const version = rootPackage.version; const version = rootPackage.version;
const tagArg = /-/.test(version) ? "--tag next" : "" const versionParts = version.split(".");
let tagArg = "";
if (versionParts[0] !== LATEST) {
tagArg = `--tag v${versionParts[0]}-maintenance`
} else if (/-/.test(version)) {
tagArg = "--tag next"
}
const lines = []; const lines = [];

View File

@ -542,6 +542,77 @@ describe('change Node', function() {
changeNode1.receive({payload:"123",topic:"ABC"}); changeNode1.receive({payload:"123",topic:"ABC"});
}); });
}); });
it('sets the value using env property from tab', function(done) {
var flow = [
{"id":"tab1","type":"tab","env":[
{"name":"NR_TEST_A", "value":"bar", "type": "str"}
]},
{"id":"changeNode1","type":"change","z":"tab1",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}
];
helper.load(changeNode, flow, function() {
var changeNode1 = helper.getNode("changeNode1");
var helperNode1 = helper.getNode("helperNode1");
helperNode1.on("input", function(msg) {
try {
msg.payload.should.equal("bar");
done();
} catch(err) {
done(err);
}
});
changeNode1.receive({payload:"123",topic:"ABC"});
});
});
it('sets the value using env property from group', function(done) {
var flow = [
{"id":"group1","type":"group","env":[
{"name":"NR_TEST_A", "value":"bar", "type": "str"}
]},
{"id":"changeNode1","type":"change","g":"group1",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}
];
helper.load(changeNode, flow, function() {
var changeNode1 = helper.getNode("changeNode1");
var helperNode1 = helper.getNode("helperNode1");
helperNode1.on("input", function(msg) {
try {
msg.payload.should.equal("bar");
done();
} catch(err) {
done(err);
}
});
changeNode1.receive({payload:"123",topic:"ABC"});
});
});
it('sets the value using env property from nested group', function(done) {
var flow = [
{"id":"group1","type":"group","env":[
{"name":"NR_TEST_A", "value":"bar", "type": "str"}
]},
{"id":"group2","type":"group","g":"group1","env":[]},
{"id":"changeNode1","type":"change","g":"group2",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}
];
helper.load(changeNode, flow, function() {
var changeNode1 = helper.getNode("changeNode1");
var helperNode1 = helper.getNode("helperNode1");
helperNode1.on("input", function(msg) {
try {
msg.payload.should.equal("bar");
done();
} catch(err) {
done(err);
}
});
changeNode1.receive({payload:"123",topic:"ABC"});
});
});
}); });

View File

@ -42,6 +42,8 @@ describe('HTTP Request Node', function() {
var testProxyPort = 10444; var testProxyPort = 10444;
var testProxyServerAuth; var testProxyServerAuth;
var testProxyAuthPort = 10554; var testProxyAuthPort = 10554;
var testSslClientServer;
var testSslClientPort = 10664;
//save environment variables //save environment variables
var preEnvHttpProxyLowerCase; var preEnvHttpProxyLowerCase;
@ -57,6 +59,7 @@ describe('HTTP Request Node', function() {
testServer = stoppable(http.createServer(testApp)); testServer = stoppable(http.createServer(testApp));
testServer.listen(testPort,function(err) { testServer.listen(testPort,function(err) {
testSslPort += 1; testSslPort += 1;
console.log("ssl port", testSslPort);
var sslOptions = { var sslOptions = {
key: fs.readFileSync('test/resources/ssl/server.key'), key: fs.readFileSync('test/resources/ssl/server.key'),
cert: fs.readFileSync('test/resources/ssl/server.crt') cert: fs.readFileSync('test/resources/ssl/server.crt')
@ -75,7 +78,25 @@ describe('HTTP Request Node', function() {
*/ */
}; };
testSslServer = stoppable(https.createServer(sslOptions,testApp)); testSslServer = stoppable(https.createServer(sslOptions,testApp));
testSslServer.listen(testSslPort); testSslServer.listen(testSslPort, function(err){
if (err) {
console.log(err);
} else {
console.log("started testSslServer");
}
});
testSslClientPort += 1;
var sslClientOptions = {
key: fs.readFileSync('test/resources/ssl/server.key'),
cert: fs.readFileSync('test/resources/ssl/server.crt'),
ca: fs.readFileSync('test/resources/ssl/server.crt'),
requestCert: true
};
testSslClientServer = stoppable(https.createServer(sslClientOptions, testApp));
testSslClientServer.listen(testSslClientPort, function(err){
console.log("ssl-client", err)
});
testProxyPort += 1; testProxyPort += 1;
testProxyServer = stoppable(httpProxy(http.createServer())) testProxyServer = stoppable(httpProxy(http.createServer()))
@ -121,6 +142,10 @@ describe('HTTP Request Node', function() {
return "https://localhost:"+testSslPort+url; return "https://localhost:"+testSslPort+url;
} }
function getSslClientTestURL(url) {
return "https://localhost:"+testSslClientPort+url;
}
function getDifferentTestURL(url) { function getDifferentTestURL(url) {
return "http://127.0.0.1:"+testPort+url; return "http://127.0.0.1:"+testPort+url;
} }
@ -280,6 +305,14 @@ describe('HTTP Request Node', function() {
headers:result headers:result
}); });
}) })
testApp.get('/getClientCert', function(req,res) {
if (req.client.authorized) {
res.send('hello');
} else {
res.status(401).send();
}
})
startServer(function(err) { startServer(function(err) {
if (err) { if (err) {
done(err); done(err);
@ -293,7 +326,9 @@ describe('HTTP Request Node', function() {
testProxyServer.stop(() => { testProxyServer.stop(() => {
testProxyServerAuth.stop(() => { testProxyServerAuth.stop(() => {
testSslServer.stop(() => { testSslServer.stop(() => {
helper.stopServer(done); testSslClientServer.stop(() => {
helper.stopServer(done);
})
}); });
}); });
}); });
@ -1145,7 +1180,7 @@ describe('HTTP Request Node', function() {
try { try {
msg.should.have.property('payload',{ msg.should.have.property('payload',{
query:{ a: 'b', c:[ 'T24,0°|H80%|W S8,3m/s' ] }, query:{ a: 'b', c:[ 'T24,0°|H80%|W S8,3m/s' ] },
url: '/getQueryParams?a=b&c%5B0%5D.Text=T24,0%C2%B0%7CH80%25%7CW%20S8,3m/s' url: '/getQueryParams?a=b&c[0].Text=T24,0%C2%B0|H80%25|W%20S8,3m/s'
}); });
msg.should.have.property('statusCode',200); msg.should.have.property('statusCode',200);
msg.should.have.property('headers'); msg.should.have.property('headers');
@ -1154,7 +1189,7 @@ describe('HTTP Request Node', function() {
done(err); done(err);
} }
}); });
n1.receive({url: getTestURL('/getQueryParams')+"?a=b&c[0].Text=T24,0°|H80%|W S8,3m/s"}); n1.receive({url: getTestURL('/getQueryParams')+"?a=b&c[0].Text=T24,0°|H80%|W%20S8,3m/s"});
}); });
}) })
}); });
@ -1518,6 +1553,60 @@ describe('HTTP Request Node', function() {
}); });
}); });
it('should use tls-config and verify serverCert', function(done) {
var flow = [
{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getSslTestURL('/text'),tls:"n3"},
{id:"n2", type:"helper"},
{id:"n3", type:"tls-config", cert:"test/resources/ssl/server.crt", key:"test/resources/ssl/server.key", ca:"test/resources/ssl/server.crt", verifyservercert:true}];
var testNodes = [httpRequestNode, tlsNode];
helper.load(testNodes, flow, function() {
var n3 = helper.getNode("n3");
var n2 = helper.getNode("n2");
var n1 = helper.getNode("n1");
n2.on("input", function(msg) {
try {
msg.should.have.property('payload','hello');
msg.should.have.property('statusCode',200);
msg.should.have.property('headers');
msg.headers.should.have.property('content-length',''+('hello'.length));
msg.headers.should.have.property('content-type').which.startWith('text/html');
msg.should.have.property('responseUrl').which.startWith('https://');
done();
} catch(err) {
done(err);
}
});
n1.receive({payload:"foo"});
});
});
it('should use tls-config and send client cert', function(done) {
var flow = [
{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getSslClientTestURL('/getClientCert'),tls:"n3"},
{id:"n2", type:"helper"},
{id:"n3", type:"tls-config", cert:"test/resources/ssl/server.crt", key:"test/resources/ssl/server.key", ca:"test/resources/ssl/server.crt", verifyservercert:false}];
var testNodes = [httpRequestNode,tlsNode];
helper.load(testNodes, flow, function() {
var n3 = helper.getNode("n3");
var n2 = helper.getNode("n2");
var n1 = helper.getNode("n1");
n2.on("input", function(msg) {
try {
msg.should.have.property('payload','hello');
msg.should.have.property('statusCode',200);
msg.should.have.property('headers');
msg.headers.should.have.property('content-length',''+('hello'.length));
msg.headers.should.have.property('content-type').which.startWith('text/html');
msg.should.have.property('responseUrl').which.startWith('https://');
done();
} catch(err) {
done(err);
}
});
n1.receive({payload:"foo"});
})
});
//Removing HTTP Proxy testcases as GOT + Proxy_Agent doesn't work with mock'd proxy //Removing HTTP Proxy testcases as GOT + Proxy_Agent doesn't work with mock'd proxy
/* */ /* */
it('should use http_proxy', function(done) { it('should use http_proxy', function(done) {

View File

@ -516,6 +516,28 @@ describe('JOIN node', function() {
n1.receive({payload:{a:1}}); n1.receive({payload:{a:1}});
}); });
}); });
it('should join things into an array ignoring msg.parts.index in manual mode', function(done) {
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:3, joiner:",",mode:"custom"},
{id:"n2", type:"helper"}];
helper.load(joinNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("payload");
msg.payload.should.be.an.Array();
msg.payload[0].should.equal(1);
msg.payload[1].should.equal(true);
//msg.payload[2].a.should.equal(1);
done();
}
catch(e) {done(e);}
});
n1.receive({payload:1, parts: {index: 3}});
n1.receive({payload:true, parts: {index: 0}});
n1.receive({payload:{a:1}, parts: {index: 9}});
});
});
it('should join things into an array after a count with a buffer join set', function(done) { it('should join things into an array after a count with a buffer join set', function(done) {
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:3, joinerType:"bin", joiner:"" ,mode:"custom"}, var flow = [{id:"n1", type:"join", wires:[["n2"]], count:3, joinerType:"bin", joiner:"" ,mode:"custom"},
@ -1646,7 +1668,7 @@ describe('JOIN node', function() {
}); });
}); });
it('should handle join an array when using msg.parts and duplicate indexed parts arrive', function (done) { it('should handle join an array when mode is auto and duplicate indexed parts arrive', function (done) {
var flow = [{ id: "n1", type: "join", wires: [["n2"]], joiner: "[44]", joinerType: "bin", build: "array", mode: "auto" }, var flow = [{ id: "n1", type: "join", wires: [["n2"]], joiner: "[44]", joinerType: "bin", build: "array", mode: "auto" },
{ id: "n2", type: "helper" }]; { id: "n2", type: "helper" }];
helper.load(joinNode, flow, function () { helper.load(joinNode, flow, function () {

View File

@ -447,4 +447,124 @@ describe('subflow', function() {
}); });
}); });
it('should access env var of tab', function(done) {
var flow = [
{id:"t0", type:"tab", label:"", disabled:false, info:"", env: [
{name: "K", type: "str", value: "V"}
]},
{id:"n1", x:10, y:10, z:"t0", type:"subflow:s1", wires:[["n2"]]},
{id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]},
// Subflow
{id:"s1", type:"subflow", name:"Subflow", info:"", env: [],
in:[{
x:10, y:10,
wires:[ {id:"s1-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s1-n1", port:0} ]
}]
},
{id:"s1-n1", x:10, y:10, z:"s1", type:"function",
func:"msg.V = env.get('K'); return msg;",
wires:[]}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("V", "V");
done();
}
catch (e) {
console.log(e);
done(e);
}
});
n1.receive({payload:"foo"});
});
});
it('should access env var of group', function(done) {
var flow = [
{id:"t0", type:"tab", label:"", disabled:false, info:""},
{id:"g1", z:"t0", type:"group", env:[
{name: "K", type: "str", value: "V"}
]},
{id:"n1", x:10, y:10, z:"t0", g:"g1", type:"subflow:s1", wires:[["n2"]]},
{id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]},
// Subflow
{id:"s1", type:"subflow", name:"Subflow", info:"", env: [],
in:[{
x:10, y:10,
wires:[ {id:"s1-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s1-n1", port:0} ]
}]
},
{id:"s1-n1", x:10, y:10, z:"s1", type:"function",
func:"msg.V = env.get('K'); return msg;",
wires:[]}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("V", "V");
done();
}
catch (e) {
console.log(e);
done(e);
}
});
n1.receive({payload:"foo"});
});
});
it('should access env var of nested group', function(done) {
var flow = [
{id:"t0", type:"tab", label:"", disabled:false, info:""},
{id:"g1", z:"t0", type:"group", env:[
{name: "K", type: "str", value: "V"}
]},
{id:"g2", z:"t0", g:"g1", type:"group", env:[]},
{id:"n1", x:10, y:10, z:"t0", g:"g2", type:"subflow:s1", wires:[["n2"]]},
{id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]},
// Subflow
{id:"s1", type:"subflow", name:"Subflow", info:"", env: [],
in:[{
x:10, y:10,
wires:[ {id:"s1-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s1-n1", port:0} ]
}]
},
{id:"s1-n1", x:10, y:10, z:"s1", type:"function",
func:"msg.V = env.get('K'); return msg;",
wires:[]}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("V", "V");
done();
}
catch (e) {
console.log(e);
done(e);
}
});
n1.receive({payload:"foo"});
});
});
}); });

View File

@ -41,7 +41,9 @@ describe("api/editor/theme", function () {
context.should.have.a.property("page"); context.should.have.a.property("page");
context.page.should.have.a.property("title", "Node-RED"); context.page.should.have.a.property("title", "Node-RED");
context.page.should.have.a.property("favicon", "favicon.ico"); context.page.should.have.a.property("favicon", "favicon.ico");
context.page.should.have.a.property("tabicon", "red/images/node-red-icon-black.svg"); context.page.should.have.a.property("tabicon");
context.page.tabicon.should.have.a.property("icon", "red/images/node-red-icon-black.svg");
context.page.tabicon.should.have.a.property("colour", "#8f0000");
context.should.have.a.property("header"); context.should.have.a.property("header");
context.header.should.have.a.property("title", "Node-RED"); context.header.should.have.a.property("title", "Node-RED");
context.header.should.have.a.property("image", "red/images/node-red.svg"); context.header.should.have.a.property("image", "red/images/node-red.svg");
@ -58,7 +60,10 @@ describe("api/editor/theme", function () {
page: { page: {
title: "Test Page Title", title: "Test Page Title",
favicon: "/absolute/path/to/theme/favicon", favicon: "/absolute/path/to/theme/favicon",
tabicon: "/absolute/path/to/theme/tabicon", tabicon: {
icon: "/absolute/path/to/theme/tabicon",
colour: "#8f008f"
},
css: "/absolute/path/to/custom/css/file.css", css: "/absolute/path/to/custom/css/file.css",
scripts: "/absolute/path/to/script.js" scripts: "/absolute/path/to/script.js"
}, },
@ -108,7 +113,9 @@ describe("api/editor/theme", function () {
context.should.have.a.property("page"); context.should.have.a.property("page");
context.page.should.have.a.property("title", "Test Page Title"); context.page.should.have.a.property("title", "Test Page Title");
context.page.should.have.a.property("favicon", "theme/favicon/favicon"); context.page.should.have.a.property("favicon", "theme/favicon/favicon");
context.page.should.have.a.property("tabicon", "theme/tabicon/tabicon"); context.page.should.have.a.property("tabicon")
context.page.tabicon.should.have.a.property("icon", "theme/tabicon/tabicon");
context.page.tabicon.should.have.a.property("colour", "#8f008f")
context.should.have.a.property("header"); context.should.have.a.property("header");
context.header.should.have.a.property("title", "Test Header Title"); context.header.should.have.a.property("title", "Test Header Title");
context.header.should.have.a.property("url", "http://nodered.org"); context.header.should.have.a.property("url", "http://nodered.org");
@ -142,6 +149,27 @@ describe("api/editor/theme", function () {
settings.projects.should.have.a.property("enabled", false); settings.projects.should.have.a.property("enabled", false);
}); });
it("picks up backwards compatible tabicon setting", async function () {
theme.init({
editorTheme: {
page: {
tabicon: "/absolute/path/to/theme/tabicon",
}
}
});
theme.app();
var context = await theme.context();
context.should.have.a.property("page");
context.page.should.have.a.property("tabicon");
context.page.tabicon.should.have.a.property("icon", "theme/tabicon/tabicon");
// The colour property should remain as default in this case as the
// legacy format for defining tabicon doesn't allow specifying a colour
context.page.tabicon.should.have.a.property("colour", "#8f0000");
});
it("test explicit userMenu set to true in theme setting", function () { it("test explicit userMenu set to true in theme setting", function () {
theme.init({ theme.init({
editorTheme: { editorTheme: {

View File

@ -1228,4 +1228,54 @@ describe('Flow', function() {
}) })
}) })
describe("#env", function () {
it("can instantiate a node with environment variable property values of group and tab", function (done) {
try {
after(function() {
delete process.env.V0;
delete process.env.V1;
})
process.env.V0 = "gv0";
process.env.V1 = "gv1";
var config = flowUtils.parseConfig([
{id:"t1",type:"tab",env:[
{"name": "V0", value: "v0", type: "str"}
]},
{id:"g1",type:"group",z:"t1",env:[
{"name": "V0", value: "v1", type: "str"},
{"name": "V1", value: "v2", type: "str"}
]},
{id:"g2",type:"group",z:"t1",g:"g1",env:[
{"name": "V1", value: "v3", type: "str"}
]},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"$(V0)",wires:[]},
{id:"2",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V0)",wires:[]},
{id:"3",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V1)",wires:[]},
{id:"4",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"$(V1)",wires:[]},
{id:"5",x:10,y:10,z:"t1",type:"test",foo:"$(V1)",wires:[]},
]);
var flow = Flow.create({getSetting:v=>process.env[v]},config,config.flows["t1"]);
flow.start();
var activeNodes = flow.getActiveNodes();
activeNodes["1"].foo.should.equal("v0");
activeNodes["2"].foo.should.equal("v1");
activeNodes["3"].foo.should.equal("v2");
activeNodes["4"].foo.should.equal("v3");
activeNodes["5"].foo.should.equal("gv1");
flow.stop().then(function() {
done();
});
}
catch (e) {
console.log(e.stack);
done(e);
}
});
});
}); });