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

Merge branch 'node-red:master' into master

This commit is contained in:
GerwinvBeek 2022-06-16 17:51:03 +02:00 committed by GitHub
commit 82ebd02110
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 1269 additions and 311 deletions

View File

@ -192,6 +192,7 @@ module.exports = function(grunt) {
"packages/node_modules/@node-red/editor-client/src/js/ui/library.js", "packages/node_modules/@node-red/editor-client/src/js/ui/library.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/notifications.js", "packages/node_modules/@node-red/editor-client/src/js/ui/notifications.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/search.js", "packages/node_modules/@node-red/editor-client/src/js/ui/search.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/actionList.js", "packages/node_modules/@node-red/editor-client/src/js/ui/actionList.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js", "packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js",
"packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js", "packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js",

View File

@ -49,7 +49,7 @@
"hash-sum": "2.0.0", "hash-sum": "2.0.0",
"hpagent": "1.0.0", "hpagent": "1.0.0",
"https-proxy-agent": "5.0.1", "https-proxy-agent": "5.0.1",
"i18next": "21.8.2", "i18next": "21.8.10",
"iconv-lite": "0.6.3", "iconv-lite": "0.6.3",
"is-utf8": "0.2.1", "is-utf8": "0.2.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
@ -62,7 +62,7 @@
"moment": "2.29.3", "moment": "2.29.3",
"moment-timezone": "0.5.34", "moment-timezone": "0.5.34",
"mqtt": "4.3.7", "mqtt": "4.3.7",
"multer": "1.4.4", "multer": "1.4.5-lts.1",
"mustache": "4.2.0", "mustache": "4.2.0",
"node-red-admin": "^3.0.0", "node-red-admin": "^3.0.0",
"node-watch": "0.7.3", "node-watch": "0.7.3",
@ -76,7 +76,7 @@
"semver": "7.3.7", "semver": "7.3.7",
"tar": "6.1.11", "tar": "6.1.11",
"tough-cookie": "4.0.0", "tough-cookie": "4.0.0",
"uglify-js": "3.15.5", "uglify-js": "3.16.0",
"uuid": "8.3.2", "uuid": "8.3.2",
"ws": "7.5.6", "ws": "7.5.6",
"xml2js": "0.4.23" "xml2js": "0.4.23"
@ -105,16 +105,16 @@
"grunt-sass": "~3.1.0", "grunt-sass": "~3.1.0",
"grunt-simple-mocha": "~0.4.1", "grunt-simple-mocha": "~0.4.1",
"grunt-simple-nyc": "^3.0.1", "grunt-simple-nyc": "^3.0.1",
"i18next-http-backend": "1.4.0", "i18next-http-backend": "1.4.1",
"jquery-i18next": "1.2.1", "jquery-i18next": "1.2.1",
"jsdoc-nr-template": "github:node-red/jsdoc-nr-template", "jsdoc-nr-template": "github:node-red/jsdoc-nr-template",
"marked": "4.0.15", "marked": "4.0.17",
"minami": "1.2.3", "minami": "1.2.3",
"mocha": "9.2.2", "mocha": "9.2.2",
"node-red-node-test-helper": "^0.2.7", "node-red-node-test-helper": "^0.2.7",
"nodemon": "2.0.16", "nodemon": "2.0.16",
"proxy": "^1.0.2", "proxy": "^1.0.2",
"sass": "1.51.0", "sass": "1.52.3",
"should": "13.2.3", "should": "13.2.3",
"sinon": "11.1.2", "sinon": "11.1.2",
"stoppable": "^1.1.0", "stoppable": "^1.1.0",

View File

@ -26,7 +26,7 @@
"express": "4.18.1", "express": "4.18.1",
"memorystore": "1.6.7", "memorystore": "1.6.7",
"mime": "3.0.0", "mime": "3.0.0",
"multer": "1.4.4", "multer": "1.4.5-lts.1",
"mustache": "4.2.0", "mustache": "4.2.0",
"oauth2orize": "1.11.1", "oauth2orize": "1.11.1",
"passport-http-bearer": "1.0.1", "passport-http-bearer": "1.0.1",

View File

@ -21,28 +21,12 @@ RED.actions = (function() {
function getAction(name) { function getAction(name) {
return actions[name].handler; return actions[name].handler;
} }
function invokeAction() { function getActionLabel(name) {
var args = Array.prototype.slice.call(arguments); let def = actions[name]
var name = args.shift(); if (!def) {
if (actions.hasOwnProperty(name)) { return ''
var handler = actions[name].handler;
handler.apply(null, args);
}
}
function listActions() {
var result = [];
var missing = [];
Object.keys(actions).forEach(function(action) {
var def = actions[action];
var shortcut = RED.keyboard.getShortcut(action);
var isUser = false;
if (shortcut) {
isUser = shortcut.user;
} else {
isUser = !!RED.keyboard.getUserShortcut(action);
} }
if (!def.label) { if (!def.label) {
var name = action;
var options = def.options; var options = def.options;
var key = options ? options.label : undefined; var key = options ? options.label : undefined;
if (!key) { if (!key) {
@ -58,12 +42,36 @@ RED.actions = (function() {
return " "+arguments[4].toUpperCase(); return " "+arguments[4].toUpperCase();
} }
}); });
missing.push(key);
} }
def.label = label; def.label = label;
} }
//console.log("; missing:", missing); return def.label
}
function invokeAction() {
var args = Array.prototype.slice.call(arguments);
var name = args.shift();
if (actions.hasOwnProperty(name)) {
var handler = actions[name].handler;
handler.apply(null, args);
}
}
function listActions() {
var result = [];
Object.keys(actions).forEach(function(action) {
var def = actions[action];
var shortcut = RED.keyboard.getShortcut(action);
var isUser = false;
if (shortcut) {
isUser = shortcut.user;
} else {
isUser = !!RED.keyboard.getUserShortcut(action);
}
if (!def.label) {
def.label = getActionLabel(action)
}
result.push({ result.push({
id:action, id:action,
scope:shortcut?shortcut.scope:undefined, scope:shortcut?shortcut.scope:undefined,
@ -79,6 +87,7 @@ RED.actions = (function() {
add: addAction, add: addAction,
remove: removeAction, remove: removeAction,
get: getAction, get: getAction,
getLabel: getActionLabel,
invoke: invokeAction, invoke: invokeAction,
list: listActions list: listActions
} }

View File

@ -16,6 +16,7 @@
RED.menu = (function() { RED.menu = (function() {
var menuItems = {}; var menuItems = {};
let menuItemCount = 0
function createMenuItem(opt) { function createMenuItem(opt) {
var item; var item;
@ -59,15 +60,16 @@ RED.menu = (function() {
item = $('<li class="red-ui-menu-divider"></li>'); item = $('<li class="red-ui-menu-divider"></li>');
} else { } else {
item = $('<li></li>'); item = $('<li></li>');
if (!opt.id) {
opt.id = 'red-ui-menu-item-'+(menuItemCount++)
}
if (opt.group) { if (opt.group) {
item.addClass("red-ui-menu-group-"+opt.group); item.addClass("red-ui-menu-group-"+opt.group);
} }
var linkContent = '<a '+(opt.id?'id="'+opt.id+'" ':'')+'tabindex="-1" href="#">'; var linkContent = '<a '+(opt.id?'id="'+opt.id+'" ':'')+'tabindex="-1" href="#">';
if (opt.toggle) { if (opt.toggle) {
linkContent += '<i class="fa fa-square pull-left"></i>'; linkContent += '<i class="fa fa-square'+(opt.direction!=='right'?" pull-left":"")+'"></i>';
linkContent += '<i class="fa fa-check-square pull-left"></i>'; linkContent += '<i class="fa fa-check-square'+(opt.direction!=='right'?" pull-left":"")+'"></i>';
} }
if (opt.icon !== undefined) { if (opt.icon !== undefined) {
@ -77,12 +79,15 @@ RED.menu = (function() {
linkContent += '<i class="'+(opt.icon?opt.icon:'" style="display: inline-block;"')+'"></i> '; linkContent += '<i class="'+(opt.icon?opt.icon:'" style="display: inline-block;"')+'"></i> ';
} }
} }
let label = opt.label
if (!opt.label && typeof opt.onselect === 'string') {
label = RED.actions.getLabel(opt.onselect)
}
if (opt.sublabel) { if (opt.sublabel) {
linkContent += '<span class="red-ui-menu-label-container"><span class="red-ui-menu-label">'+opt.label+'</span>'+ linkContent += '<span class="red-ui-menu-label-container"><span class="red-ui-menu-label">'+label+'</span>'+
'<span class="red-ui-menu-sublabel">'+opt.sublabel+'</span></span>' '<span class="red-ui-menu-sublabel">'+opt.sublabel+'</span></span>'
} else { } else {
linkContent += '<span class="red-ui-menu-label"><span>'+opt.label+'</span></span>' linkContent += '<span class="red-ui-menu-label"><span>'+label+'</span></span>'
} }
linkContent += '</a>'; linkContent += '</a>';
@ -126,10 +131,21 @@ RED.menu = (function() {
}); });
} }
if (opt.options) { if (opt.options) {
item.addClass("red-ui-menu-dropdown-submenu pull-left"); item.addClass("red-ui-menu-dropdown-submenu"+(opt.direction!=='right'?" pull-left":""));
var submenu = $('<ul id="'+opt.id+'-submenu" class="red-ui-menu-dropdown"></ul>').appendTo(item); var submenu = $('<ul id="'+opt.id+'-submenu" class="red-ui-menu-dropdown"></ul>').appendTo(item);
for (var i=0;i<opt.options.length;i++) { for (var i=0;i<opt.options.length;i++) {
if (opt.options[i]) {
if (opt.onpreselect && opt.options[i].onpreselect === undefined) {
opt.options[i].onpreselect = opt.onpreselect
}
if (opt.onpostselect && opt.options[i].onpostselect === undefined) {
opt.options[i].onpostselect = opt.onpostselect
}
opt.options[i].direction = opt.direction
}
var li = createMenuItem(opt.options[i]); var li = createMenuItem(opt.options[i]);
if (li) { if (li) {
li.appendTo(submenu); li.appendTo(submenu);
@ -147,7 +163,9 @@ RED.menu = (function() {
} }
function createMenu(options) { function createMenu(options) {
var topMenu = $("<ul/>",{class:"red-ui-menu red-ui-menu-dropdown pull-right"}); var topMenu = $("<ul/>",{class:"red-ui-menu red-ui-menu-dropdown pull-right"});
if (options.direction) {
topMenu.addClass("red-ui-menu-dropdown-direction-"+options.direction)
}
if (options.id) { if (options.id) {
topMenu.attr({id:options.id+"-submenu"}); topMenu.attr({id:options.id+"-submenu"});
var menuParent = $("#"+options.id); var menuParent = $("#"+options.id);
@ -175,6 +193,15 @@ RED.menu = (function() {
var lastAddedSeparator = false; var lastAddedSeparator = false;
for (var i=0;i<options.options.length;i++) { for (var i=0;i<options.options.length;i++) {
var opt = options.options[i]; var opt = options.options[i];
if (opt) {
if (options.onpreselect && opt.onpreselect === undefined) {
opt.onpreselect = options.onpreselect
}
if (options.onpostselect && opt.onpostselect === undefined) {
opt.onpostselect = options.onpostselect
}
opt.direction = options.direction || 'left'
}
if (opt !== null || !lastAddedSeparator) { if (opt !== null || !lastAddedSeparator) {
var li = createMenuItem(opt); var li = createMenuItem(opt);
if (li) { if (li) {
@ -190,6 +217,9 @@ RED.menu = (function() {
function triggerAction(id, args) { function triggerAction(id, args) {
var opt = menuItems[id]; var opt = menuItems[id];
var callback = opt.onselect; var callback = opt.onselect;
if (opt.onpreselect) {
opt.onpreselect.call(opt,args)
}
if (typeof opt.onselect === 'string') { if (typeof opt.onselect === 'string') {
callback = RED.actions.get(opt.onselect); callback = RED.actions.get(opt.onselect);
} }
@ -198,6 +228,9 @@ RED.menu = (function() {
} else { } else {
console.log("No callback for",id,opt.onselect); console.log("No callback for",id,opt.onselect);
} }
if (opt.onpostselect) {
opt.onpostselect.call(opt,args)
}
} }
function isSelected(id) { function isSelected(id) {

View File

@ -610,10 +610,13 @@ RED.popover = (function() {
var target = options.target; var target = options.target;
var align = options.align || "right"; var align = options.align || "right";
var offset = options.offset || [0,0]; var offset = options.offset || [0,0];
var xPos = options.x;
var yPos = options.y;
var isAbsolutePosition = (xPos !== undefined && yPos !== undefined)
var pos = target.offset(); var pos = isAbsolutePosition?{left:xPos, top: yPos}:target.offset();
var targetWidth = target.width(); var targetWidth = isAbsolutePosition?0:target.width();
var targetHeight = target.outerHeight(); var targetHeight = isAbsolutePosition?0:target.outerHeight();
var panelHeight = panel.height(); var panelHeight = panel.height();
var panelWidth = panel.width(); var panelWidth = panel.width();

View File

@ -0,0 +1,175 @@
RED.contextMenu = (function() {
let menu;
function createMenu() {
// menu = RED.popover.menu({
// options: [
// {
// label: 'delete selection',
// onselect: function() {
// RED.actions.invoke('core:delete-selection')
// RED.view.focus()
// }
// },
// { label: 'world' }
// ],
// width: 200,
// })
}
function disposeMenu() {
$(document).off("mousedown.red-ui-workspace-context-menu");
if (menu) {
menu.remove();
}
menu = null;
}
function show(options) {
if (menu) {
menu.remove()
}
const selection = RED.view.selection()
const hasSelection = (selection.nodes && selection.nodes.length > 0);
const hasMultipleSelection = hasSelection && selection.nodes.length > 1;
const hasLinks = selection.links && selection.links.length > 0;
const isSingleLink = !hasSelection && hasLinks && selection.links.length === 1
const isMultipleLinks = !hasSelection && hasLinks && selection.links.length > 1
const canDelete = hasSelection || hasLinks
const isGroup = hasSelection && selection.nodes.length === 1 && selection.nodes[0].type === 'group'
const canRemoveFromGroup = hasSelection && !!selection.nodes[0].g
const menuItems = [
{ onselect: 'core:show-action-list', onpostselect: function() {} },
{
label: 'Insert',
options: [
{
label: 'Node',
onselect: function() {
RED.view.showQuickAddDialog({
position: [ options.x - offset.left, options.y - offset.top ],
touchTrigger: true,
splice: isSingleLink?selection.links[0]:undefined,
// spliceMultiple: isMultipleLinks
})
}
},
{
label: 'Junction',
onselect: 'core:split-wires-with-junctions',
disabled: hasSelection || !hasLinks
},
{
label: 'Link Nodes',
onselect: 'core:split-wire-with-link-nodes',
disabled: hasSelection || !hasLinks
}
]
}
]
// menuItems.push(
// {
// label: (isSingleLink || isMultipleLinks)?'Insert into wire...':'Add node...',
// onselect: function() {
// RED.view.showQuickAddDialog({
// position: [ options.x - offset.left, options.y - offset.top ],
// touchTrigger: true,
// splice: isSingleLink?selection.links[0]:undefined,
// spliceMultiple: isMultipleLinks
// })
// }
// },
// )
// if (hasLinks && !hasSelection) {
// menuItems.push({ onselect: 'core:split-wires-with-junctions', label: 'Insert junction'})
// }
menuItems.push(
null,
{ onselect: 'core:undo', disabled: RED.history.list().length === 0 },
{ onselect: 'core:redo', disabled: RED.history.listRedo().length === 0 },
null,
{ onselect: 'core:cut-selection-to-internal-clipboard', label: RED._("keyboard.cutNode"), disabled: !hasSelection},
{ onselect: 'core:copy-selection-to-internal-clipboard', label: RED._("keyboard.copyNode"), disabled: !hasSelection },
{ onselect: 'core:paste-from-internal-clipboard', label: RED._("keyboard.pasteNode"), disabled: !RED.view.clipboard() },
{ onselect: 'core:delete-selection', disabled: !canDelete },
{ onselect: 'core:show-export-dialog', label: RED._("menu.label.export") },
{ onselect: 'core:select-all-nodes' }
)
if (hasSelection) {
menuItems.push(
null,
isGroup ?
{ onselect: 'core:ungroup-selection', disabled: !isGroup }
: { onselect: 'core:group-selection', disabled: !hasSelection }
)
if (canRemoveFromGroup) {
menuItems.push({ onselect: 'core:remove-selection-from-group', label: RED._("menu.label.groupRemoveSelection") })
}
}
const offset = $("#red-ui-workspace-chart").offset()
menu = RED.menu.init({
direction: 'right',
onpreselect: function() {
disposeMenu()
},
onpostselect: function() {
RED.view.focus()
},
options: menuItems
});
menu.attr("id","red-ui-workspace-context-menu");
menu.css({
position: "absolute"
})
menu.appendTo("body");
// TODO: prevent the menu from overflowing the window.
var top = options.y
var left = options.x
if (top+menu.height()-$(document).scrollTop() > $(window).height()) {
top -= (top+menu.height())-$(window).height() + 22;
}
if (left+menu.width()-$(document).scrollLeft() > $(window).width()) {
left -= (left+menu.width())-$(window).width() + 18;
}
menu.css({
top: top+"px",
left: left+"px"
})
$(".red-ui-menu.red-ui-menu-dropdown").hide();
$(document).on("mousedown.red-ui-workspace-context-menu", function(evt) {
if (menu && menu[0].contains(evt.target)) {
return
}
disposeMenu()
});
menu.show();
// menu.show({
// target: $('#red-ui-main-container'),
// x: options.x,
// y: options.y
// })
}
return {
show: show
}
})()

View File

@ -104,7 +104,9 @@ RED.typeSearch = (function() {
var index = Math.max(0,selected); var index = Math.max(0,selected);
if (index < children.length) { if (index < children.length) {
var n = $(children[index]).find(".red-ui-editableList-item-content").data('data'); var n = $(children[index]).find(".red-ui-editableList-item-content").data('data');
if (!/^_action_:/.test(n.type)) {
typesUsed[n.type] = Date.now(); typesUsed[n.type] = Date.now();
}
if (n.def.outputs === 0) { if (n.def.outputs === 0) {
confirm(n); confirm(n);
} else { } else {
@ -173,6 +175,8 @@ RED.typeSearch = (function() {
var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(div); var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(div);
if (object.type === "junction") { if (object.type === "junction") {
nodeDiv.addClass("red-ui-palette-icon-junction"); nodeDiv.addClass("red-ui-palette-icon-junction");
} else if (/^_action_:/.test(object.type)) {
nodeDiv.addClass("red-ui-palette-icon-junction")
} else { } else {
var colour = RED.utils.getNodeColor(object.type,def); var colour = RED.utils.getNodeColor(object.type,def);
nodeDiv.css('backgroundColor',colour); nodeDiv.css('backgroundColor',colour);
@ -182,12 +186,15 @@ RED.typeSearch = (function() {
var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv); var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
RED.utils.createIconElement(icon_url, iconContainer, false); RED.utils.createIconElement(icon_url, iconContainer, false);
if (object.type !== "junction" && def.inputs > 0) {
if (!/^_action_:/.test(object.type) && object.type !== "junction") {
if (def.inputs > 0) {
$('<div/>',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv); $('<div/>',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv);
} }
if (object.type !== "junction" && def.outputs > 0) { if (def.outputs > 0) {
$('<div/>',{class:"red-ui-search-result-node-port red-ui-search-result-node-output"}).appendTo(nodeDiv); $('<div/>',{class:"red-ui-search-result-node-port red-ui-search-result-node-output"}).appendTo(nodeDiv);
} }
}
var contentDiv = $('<div>',{class:"red-ui-search-result-description"}).appendTo(div); var contentDiv = $('<div>',{class:"red-ui-search-result-description"}).appendTo(div);
@ -207,7 +214,9 @@ RED.typeSearch = (function() {
} }
function confirm(def) { function confirm(def) {
hide(); hide();
if (!/^_action_:/.test(def.type)) {
typesUsed[def.type] = Date.now(); typesUsed[def.type] = Date.now();
}
addCallback(def.type); addCallback(def.type);
} }
@ -316,6 +325,7 @@ RED.typeSearch = (function() {
function applyFilter(filter,type,def) { function applyFilter(filter,type,def) {
return !filter || return !filter ||
( (
(!filter.spliceMultiple) &&
(!filter.type || type === filter.type) && (!filter.type || type === filter.type) &&
(!filter.input || type === 'junction' || def.inputs > 0) && (!filter.input || type === 'junction' || def.inputs > 0) &&
(!filter.output || type === 'junction' || def.outputs > 0) (!filter.output || type === 'junction' || def.outputs > 0)
@ -330,6 +340,13 @@ RED.typeSearch = (function() {
'inject','debug','function','change','switch','junction' 'inject','debug','function','change','switch','junction'
].filter(function(t) { return applyFilter(opts.filter,t,RED.nodes.getType(t)); }); ].filter(function(t) { return applyFilter(opts.filter,t,RED.nodes.getType(t)); });
// if (opts.filter && opts.filter.input && opts.filter.output && !opts.filter.type) {
// if (opts.filter.spliceMultiple) {
// common.push('_action_:core:split-wires-with-junctions')
// }
// common.push('_action_:core:split-wire-with-link-nodes')
// }
var recentlyUsed = Object.keys(typesUsed); var recentlyUsed = Object.keys(typesUsed);
recentlyUsed.sort(function(a,b) { recentlyUsed.sort(function(a,b) {
return typesUsed[b]-typesUsed[a]; return typesUsed[b]-typesUsed[a];
@ -354,6 +371,8 @@ RED.typeSearch = (function() {
var itemDef = RED.nodes.getType(common[i]); var itemDef = RED.nodes.getType(common[i]);
if (common[i] === 'junction') { if (common[i] === 'junction') {
itemDef = { inputs:1, outputs: 1, label: 'junction', type: 'junction'} itemDef = { inputs:1, outputs: 1, label: 'junction', type: 'junction'}
} else if (/^_action_:/.test(common[i]) ) {
itemDef = { inputs:1, outputs: 1, label: common[i], type: common[i]}
} }
if (itemDef) { if (itemDef) {
item = { item = {

View File

@ -1032,6 +1032,8 @@ RED.utils = (function() {
return "font-awesome/fa-circle-o" return "font-awesome/fa-circle-o"
} else if (def.category === 'config') { } else if (def.category === 'config') {
return RED.settings.apiRootUrl+"icons/node-red/cog.svg" return RED.settings.apiRootUrl+"icons/node-red/cog.svg"
} else if ((node && /^_action_:/.test(node.type)) || /^_action_:/.test(def.type)) {
return "font-awesome/fa-cogs"
} else if (node && node.type === 'tab') { } else if (node && node.type === 'tab') {
return "red-ui-icons/red-ui-icons-flow" return "red-ui-icons/red-ui-icons-flow"
// return RED.settings.apiRootUrl+"images/subflow_tab.svg" // return RED.settings.apiRootUrl+"images/subflow_tab.svg"

View File

@ -336,17 +336,17 @@ RED.view.tools = (function() {
} }
function addNode() { // function addNode() {
var selection = RED.view.selection(); // var selection = RED.view.selection();
if (selection.nodes && selection.nodes.length === 1 && selection.nodes[0].outputs > 0) { // if (selection.nodes && selection.nodes.length === 1 && selection.nodes[0].outputs > 0) {
var selectedNode = selection.nodes[0]; // var selectedNode = selection.nodes[0];
RED.view.showQuickAddDialog([ // RED.view.showQuickAddDialog([
selectedNode.x + selectedNode.w + 50,selectedNode.y // selectedNode.x + selectedNode.w + 50,selectedNode.y
]) // ])
} else { // } else {
RED.view.showQuickAddDialog(); // RED.view.showQuickAddDialog();
} // }
} // }
function gotoNearestNode(direction) { function gotoNearestNode(direction) {
@ -815,6 +815,9 @@ RED.view.tools = (function() {
*/ */
function splitWiresWithLinkNodes(wires) { function splitWiresWithLinkNodes(wires) {
let wiresToSplit = wires || RED.view.selection().links; let wiresToSplit = wires || RED.view.selection().links;
if (!wiresToSplit) {
return
}
if (!Array.isArray(wiresToSplit)) { if (!Array.isArray(wiresToSplit)) {
wiresToSplit = [wiresToSplit]; wiresToSplit = [wiresToSplit];
} }
@ -1047,6 +1050,135 @@ RED.view.tools = (function() {
} }
} }
function addJunctionsToWires(wires) {
let wiresToSplit = wires || RED.view.selection().links;
if (!wiresToSplit) {
return
}
if (!Array.isArray(wiresToSplit)) {
wiresToSplit = [wiresToSplit];
}
if (wiresToSplit.length === 0) {
return;
}
var removedLinks = new Set()
var addedLinks = []
var addedJunctions = []
var groupedLinks = {}
wiresToSplit.forEach(function(l) {
var sourceId = l.source.id+":"+l.sourcePort
groupedLinks[sourceId] = groupedLinks[sourceId] || []
groupedLinks[sourceId].push(l)
groupedLinks[l.target.id] = groupedLinks[l.target.id] || []
groupedLinks[l.target.id].push(l)
});
var linkGroups = Object.keys(groupedLinks)
linkGroups.sort(function(A,B) {
return groupedLinks[B].length - groupedLinks[A].length
})
linkGroups.forEach(function(gid) {
var links = groupedLinks[gid]
var junction = {
_def: {defaults:{}},
type: 'junction',
z: RED.workspaces.active(),
id: RED.nodes.id(),
x: 0,
y: 0,
w: 0, h: 0,
outputs: 1,
inputs: 1,
dirty: true
}
links = links.filter(function(l) { return !removedLinks.has(l) })
if (links.length === 0) {
return
}
let pointCount = 0
links.forEach(function(l) {
if (l._sliceLocation) {
junction.x += l._sliceLocation.x
junction.y += l._sliceLocation.y
delete l._sliceLocation
pointCount++
} else {
junction.x += l.source.x + l.source.w/2 + l.target.x - l.target.w/2
junction.y += l.source.y + l.target.y
pointCount += 2
}
})
junction.x = Math.round(junction.x/pointCount)
junction.y = Math.round(junction.y/pointCount)
if (RED.view.snapGrid) {
let gridSize = RED.view.gridSize()
junction.x = (gridSize*Math.round(junction.x/gridSize));
junction.y = (gridSize*Math.round(junction.y/gridSize));
}
var nodeGroups = new Set()
RED.nodes.addJunction(junction)
addedJunctions.push(junction)
let newLink
if (gid === links[0].source.id+":"+links[0].sourcePort) {
newLink = {
source: links[0].source,
sourcePort: links[0].sourcePort,
target: junction
}
} else {
newLink = {
source: junction,
sourcePort: 0,
target: links[0].target
}
}
addedLinks.push(newLink)
RED.nodes.addLink(newLink)
links.forEach(function(l) {
removedLinks.add(l)
RED.nodes.removeLink(l)
let newLink
if (gid === l.target.id) {
newLink = {
source: l.source,
sourcePort: l.sourcePort,
target: junction
}
} else {
newLink = {
source: junction,
sourcePort: 0,
target: l.target
}
}
addedLinks.push(newLink)
RED.nodes.addLink(newLink)
nodeGroups.add(l.source.g || "__NONE__")
nodeGroups.add(l.target.g || "__NONE__")
})
if (nodeGroups.size === 1) {
var group = nodeGroups.values().next().value
if (group !== "__NONE__") {
RED.group.addToGroup(RED.nodes.group(group), junction)
}
}
})
if (addedJunctions.length > 0) {
RED.history.push({
t: 'add',
links: addedLinks,
junctions: addedJunctions,
removedLinks: Array.from(removedLinks)
})
RED.nodes.dirty(true)
}
RED.view.redraw(true);
}
return { 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); })
@ -1109,6 +1241,7 @@ RED.view.tools = (function() {
RED.actions.add("core:wire-node-to-multiple", function() { wireNodeToMultiple() }) RED.actions.add("core:wire-node-to-multiple", function() { wireNodeToMultiple() })
RED.actions.add("core:split-wire-with-link-nodes", function () { splitWiresWithLinkNodes() }); RED.actions.add("core:split-wire-with-link-nodes", function () { splitWiresWithLinkNodes() });
RED.actions.add("core:split-wires-with-junctions", function () { addJunctionsToWires() });
RED.actions.add("core:generate-node-names", generateNodeNames ) RED.actions.add("core:generate-node-names", generateNodeNames )

View File

@ -206,7 +206,15 @@ RED.view = (function() {
function init() { function init() {
chart = $("#red-ui-workspace-chart"); chart = $("#red-ui-workspace-chart");
chart.on('contextmenu', function(evt) {
evt.preventDefault()
evt.stopPropagation()
RED.contextMenu.show({
x:evt.clientX-5,
y:evt.clientY-5
})
return false
})
outer = d3.select("#red-ui-workspace-chart") outer = d3.select("#red-ui-workspace-chart")
.append("svg:svg") .append("svg:svg")
.attr("width", space_width) .attr("width", space_width)
@ -230,6 +238,7 @@ RED.view = (function() {
.on("mousedown", canvasMouseDown) .on("mousedown", canvasMouseDown)
.on("mouseup", canvasMouseUp) .on("mouseup", canvasMouseUp)
.on("mouseenter", function() { .on("mouseenter", function() {
d3.select(document).on('mouseup.red-ui-workspace-tracker', null)
if (lasso) { if (lasso) {
if (d3.event.buttons !== 1) { if (d3.event.buttons !== 1) {
lasso.remove(); lasso.remove();
@ -245,6 +254,7 @@ RED.view = (function() {
} }
} }
}) })
.on("mouseleave", canvasMouseLeave)
.on("touchend", function() { .on("touchend", function() {
d3.event.preventDefault(); d3.event.preventDefault();
clearTimeout(touchStartTime); clearTimeout(touchStartTime);
@ -385,6 +395,9 @@ RED.view = (function() {
drag_lines = []; drag_lines = [];
RED.events.on("workspace:change",function(event) { RED.events.on("workspace:change",function(event) {
// Just in case the mouse left the workspace whilst doing an action,
// put us back into default mode so the refresh works
mouse_mode = 0
if (event.old !== 0) { if (event.old !== 0) {
workspaceScrollPositions[event.old] = { workspaceScrollPositions[event.old] = {
left:chart.scrollLeft(), left:chart.scrollLeft(),
@ -987,7 +1000,10 @@ RED.view = (function() {
scroll_position = [chart.scrollLeft(),chart.scrollTop()]; scroll_position = [chart.scrollLeft(),chart.scrollTop()];
return; return;
} }
if (!mousedown_node && !mousedown_link && !mousedown_group) { if (d3.event.button === 2) {
return
}
if (!mousedown_node && !mousedown_link && !mousedown_group && !d3.event.shiftKey) {
selectedLinks.clear(); selectedLinks.clear();
updateSelection(); updateSelection();
} }
@ -1041,6 +1057,7 @@ RED.view = (function() {
options = options || {}; options = options || {};
var point = options.position || lastClickPosition; var point = options.position || lastClickPosition;
var spliceLink = options.splice; var spliceLink = options.splice;
var spliceMultipleLinks = options.spliceMultiple
var targetGroup = options.group; var targetGroup = options.group;
var touchTrigger = options.touchTrigger; var touchTrigger = options.touchTrigger;
@ -1053,6 +1070,10 @@ RED.view = (function() {
var ox = point[0]; var ox = point[0];
var oy = point[1]; var oy = point[1];
const offset = $("#red-ui-workspace-chart").offset()
var clientX = ox + offset.left
var clientY = oy + offset.top
if (RED.settings.get("editor").view['view-snap-grid']) { if (RED.settings.get("editor").view['view-snap-grid']) {
// eventLayer.append("circle").attr("cx",point[0]).attr("cy",point[1]).attr("r","2").attr('fill','red') // eventLayer.append("circle").attr("cx",point[0]).attr("cy",point[1]).attr("r","2").attr('fill','red')
point[0] = Math.round(point[0] / gridSize) * gridSize; point[0] = Math.round(point[0] / gridSize) * gridSize;
@ -1104,8 +1125,12 @@ RED.view = (function() {
} }
hideDragLines(); hideDragLines();
} }
if (spliceLink) { if (spliceLink || spliceMultipleLinks) {
filter = {input:true, output:true} filter = {
input:true,
output:true,
spliceMultiple: spliceMultipleLinks
}
} }
var rebuildQuickAddLink = function() { var rebuildQuickAddLink = function() {
@ -1130,8 +1155,8 @@ RED.view = (function() {
var lastAddedWidth; var lastAddedWidth;
RED.typeSearch.show({ RED.typeSearch.show({
x:d3.event.clientX-mainPos.left-node_width/2 - (ox-point[0]), x:clientX-mainPos.left-node_width/2 - (ox-point[0]),
y:d3.event.clientY-mainPos.top+ node_height/2 + 5 - (oy-point[1]), y:clientY-mainPos.top+ node_height/2 + 5 - (oy-point[1]),
disableFocus: touchTrigger, disableFocus: touchTrigger,
filter: filter, filter: filter,
move: function(dx,dy) { move: function(dx,dy) {
@ -1167,7 +1192,13 @@ RED.view = (function() {
var nn; var nn;
var historyEvent; var historyEvent;
if (type === 'junction') { if (/^_action_:/.test(type)) {
const actionName = type.substring(9)
quickAddActive = false;
ghostNode.remove();
RED.actions.invoke(actionName)
return
} else if (type === 'junction') {
nn = { nn = {
_def: {defaults:{}}, _def: {defaults:{}},
type: 'junction', type: 'junction',
@ -1733,7 +1764,14 @@ RED.view = (function() {
redraw(); redraw();
} }
} }
function canvasMouseLeave() {
if (mouse_mode !== 0 && d3.event.buttons !== 0) {
d3.select(document).on('mouseup.red-ui-workspace-tracker', function() {
d3.select(document).on('mouseup.red-ui-workspace-tracker', null)
canvasMouseUp.call(this)
})
}
}
function canvasMouseUp() { function canvasMouseUp() {
lastClickPosition = [d3.event.offsetX/scaleFactor,d3.event.offsetY/scaleFactor]; lastClickPosition = [d3.event.offsetX/scaleFactor,d3.event.offsetY/scaleFactor];
if (RED.view.DEBUG) { if (RED.view.DEBUG) {
@ -1832,8 +1870,20 @@ RED.view = (function() {
} }
} }
}) })
activeLinks.forEach(function(link) {
if (!link.selected) {
var sourceY = link.source.y
var targetY = link.target.y
var sourceX = link.source.x+(link.source.w/2) + 10
var targetX = link.target.x-(link.target.w/2) - 10
if (
sourceX > x && sourceX < x2 && sourceY > y && sourceY < y2 &&
targetX > x && targetX < x2 && targetY > y && targetY < y2
) {
selectedLinks.add(link);
}
}
})
// var selectionChanged = false; // var selectionChanged = false;
// do { // do {
@ -1881,114 +1931,118 @@ RED.view = (function() {
slicePath = null; slicePath = null;
RED.view.redraw(true); RED.view.redraw(true);
} else if (mouse_mode == RED.state.SLICING_JUNCTION) { } else if (mouse_mode == RED.state.SLICING_JUNCTION) {
var removedLinks = new Set() RED.actions.invoke("core:split-wires-with-junctions")
var addedLinks = []
var addedJunctions = []
var groupedLinks = {}
selectedLinks.forEach(function(l) {
var sourceId = l.source.id+":"+l.sourcePort
groupedLinks[sourceId] = groupedLinks[sourceId] || []
groupedLinks[sourceId].push(l)
groupedLinks[l.target.id] = groupedLinks[l.target.id] || []
groupedLinks[l.target.id].push(l)
});
var linkGroups = Object.keys(groupedLinks)
linkGroups.sort(function(A,B) {
return groupedLinks[B].length - groupedLinks[A].length
})
linkGroups.forEach(function(gid) {
var links = groupedLinks[gid]
var junction = {
_def: {defaults:{}},
type: 'junction',
z: RED.workspaces.active(),
id: RED.nodes.id(),
x: 0,
y: 0,
w: 0, h: 0,
outputs: 1,
inputs: 1,
dirty: true
}
links = links.filter(function(l) { return !removedLinks.has(l) })
if (links.length === 0) {
return
}
links.forEach(function(l) {
junction.x += l._sliceLocation.x
junction.y += l._sliceLocation.y
})
junction.x = Math.round(junction.x/links.length)
junction.y = Math.round(junction.y/links.length)
if (snapGrid) {
junction.x = (gridSize*Math.round(junction.x/gridSize));
junction.y = (gridSize*Math.round(junction.y/gridSize));
}
var nodeGroups = new Set()
RED.nodes.addJunction(junction)
addedJunctions.push(junction)
let newLink
if (gid === links[0].source.id+":"+links[0].sourcePort) {
newLink = {
source: links[0].source,
sourcePort: links[0].sourcePort,
target: junction
}
} else {
newLink = {
source: junction,
sourcePort: 0,
target: links[0].target
}
}
addedLinks.push(newLink)
RED.nodes.addLink(newLink)
links.forEach(function(l) {
removedLinks.add(l)
RED.nodes.removeLink(l)
let newLink
if (gid === l.target.id) {
newLink = {
source: l.source,
sourcePort: l.sourcePort,
target: junction
}
} else {
newLink = {
source: junction,
sourcePort: 0,
target: l.target
}
}
addedLinks.push(newLink)
RED.nodes.addLink(newLink)
nodeGroups.add(l.source.g || "__NONE__")
nodeGroups.add(l.target.g || "__NONE__")
})
if (nodeGroups.size === 1) {
var group = nodeGroups.values().next().value
if (group !== "__NONE__") {
RED.group.addToGroup(RED.nodes.group(group), junction)
}
}
})
slicePath.remove(); slicePath.remove();
slicePath = null; slicePath = null;
if (addedJunctions.length > 0) { // var removedLinks = new Set()
RED.history.push({ // var addedLinks = []
t: 'add', // var addedJunctions = []
links: addedLinks, //
junctions: addedJunctions, // var groupedLinks = {}
removedLinks: Array.from(removedLinks) // selectedLinks.forEach(function(l) {
}) // var sourceId = l.source.id+":"+l.sourcePort
RED.nodes.dirty(true) // groupedLinks[sourceId] = groupedLinks[sourceId] || []
} // groupedLinks[sourceId].push(l)
RED.view.redraw(true); //
// groupedLinks[l.target.id] = groupedLinks[l.target.id] || []
// groupedLinks[l.target.id].push(l)
// });
// var linkGroups = Object.keys(groupedLinks)
// linkGroups.sort(function(A,B) {
// return groupedLinks[B].length - groupedLinks[A].length
// })
// linkGroups.forEach(function(gid) {
// var links = groupedLinks[gid]
// var junction = {
// _def: {defaults:{}},
// type: 'junction',
// z: RED.workspaces.active(),
// id: RED.nodes.id(),
// x: 0,
// y: 0,
// w: 0, h: 0,
// outputs: 1,
// inputs: 1,
// dirty: true
// }
// links = links.filter(function(l) { return !removedLinks.has(l) })
// if (links.length === 0) {
// return
// }
// links.forEach(function(l) {
// junction.x += l._sliceLocation.x
// junction.y += l._sliceLocation.y
// })
// junction.x = Math.round(junction.x/links.length)
// junction.y = Math.round(junction.y/links.length)
// if (snapGrid) {
// junction.x = (gridSize*Math.round(junction.x/gridSize));
// junction.y = (gridSize*Math.round(junction.y/gridSize));
// }
//
// var nodeGroups = new Set()
//
// RED.nodes.addJunction(junction)
// addedJunctions.push(junction)
// let newLink
// if (gid === links[0].source.id+":"+links[0].sourcePort) {
// newLink = {
// source: links[0].source,
// sourcePort: links[0].sourcePort,
// target: junction
// }
// } else {
// newLink = {
// source: junction,
// sourcePort: 0,
// target: links[0].target
// }
// }
// addedLinks.push(newLink)
// RED.nodes.addLink(newLink)
// links.forEach(function(l) {
// removedLinks.add(l)
// RED.nodes.removeLink(l)
// let newLink
// if (gid === l.target.id) {
// newLink = {
// source: l.source,
// sourcePort: l.sourcePort,
// target: junction
// }
// } else {
// newLink = {
// source: junction,
// sourcePort: 0,
// target: l.target
// }
// }
// addedLinks.push(newLink)
// RED.nodes.addLink(newLink)
// nodeGroups.add(l.source.g || "__NONE__")
// nodeGroups.add(l.target.g || "__NONE__")
// })
// if (nodeGroups.size === 1) {
// var group = nodeGroups.values().next().value
// if (group !== "__NONE__") {
// RED.group.addToGroup(RED.nodes.group(group), junction)
// }
// }
// })
// slicePath.remove();
// slicePath = null;
//
// if (addedJunctions.length > 0) {
// RED.history.push({
// t: 'add',
// links: addedLinks,
// junctions: addedJunctions,
// removedLinks: Array.from(removedLinks)
// })
// RED.nodes.dirty(true)
// }
// RED.view.redraw(true);
} }
if (mouse_mode == RED.state.MOVING_ACTIVE) { if (mouse_mode == RED.state.MOVING_ACTIVE) {
if (movingSet.length() > 0) { if (movingSet.length() > 0) {
@ -5736,10 +5790,15 @@ RED.view = (function() {
node.dirty = true; node.dirty = true;
node.dirtyStatus = true; node.dirtyStatus = true;
node.changed = true; node.changed = true;
if (node.type === "junction") {
RED.events.emit("junctions:change",node);
}
else {
RED.events.emit("nodes:change",node); RED.events.emit("nodes:change",node);
} }
} }
} }
}
if (historyEvents.length > 0) { if (historyEvents.length > 0) {
RED.history.push({ RED.history.push({
t:"multi", t:"multi",

View File

@ -46,7 +46,7 @@
& > li > a, & > li > a,
& > li > a:focus { & > li > a:focus {
display: block; display: block;
padding: 4px 12px 4px 32px; padding: 4px 20px 4px 12px;
clear: both; clear: both;
font-weight: normal; font-weight: normal;
line-height: 20px; line-height: 20px;
@ -54,7 +54,10 @@
white-space: normal !important; white-space: normal !important;
outline: none; outline: none;
} }
& > li.pull-left > a,
& > li.pull-left > a:focus {
padding: 4px 12px 4px 32px;
}
& > .active > a, & > .active > a,
& > .active > a:hover, & > .active > a:hover,
& > .active > a:focus { & > .active > a:focus {
@ -145,8 +148,8 @@
position: relative; position: relative;
& > .red-ui-menu-dropdown { & > .red-ui-menu-dropdown {
top: 0; top: 0;
left: 100%; left: calc(100% - 5px);
margin-top: -6px; margin-top: 0;
margin-left: -1px; margin-left: -1px;
} }
&.open > .red-ui-menu-dropdown, &.open > .red-ui-menu-dropdown,
@ -175,10 +178,10 @@
} }
} }
.red-ui-menu-dropdown-submenu>a:after { .red-ui-menu-dropdown-submenu.pull-left>a:after {
display: none; display: none;
} }
.red-ui-menu-dropdown-submenu>a:before { .red-ui-menu-dropdown-submenu.pull-left>a:before {
display: block; display: block;
float: left; float: left;
width: 0; width: 0;
@ -192,7 +195,25 @@
border-width: 5px 5px 5px 0; border-width: 5px 5px 5px 0;
content: " "; content: " ";
} }
.red-ui-menu-dropdown-direction-right {
.red-ui-menu-dropdown-submenu>a:after {
display: none;
}
.red-ui-menu-dropdown-submenu>a:before {
display: block;
float: right;
width: 0;
height: 0;
margin-top: 5px;
margin-right: -15px;
/* Caret Arrow */
border-color: transparent;
border-left-color: $menuCaret;
border-style: solid;
border-width: 5px 0 5px 5px;
content: " ";
}
}
.red-ui-menu-dropdown-submenu.disabled > a:before { .red-ui-menu-dropdown-submenu.disabled > a:before {
border-right-color: $menuCaret; border-right-color: $menuCaret;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -1,15 +1,27 @@
export default { export default {
version: "3.0.0-beta.1", version: "3.0.0-beta.3",
steps: [ steps: [
{ {
titleIcon: "fa fa-map-o", titleIcon: "fa fa-map-o",
title: { title: {
"en-US": "Welcome to Node-RED 3.0 Beta 1!", "en-US": "Welcome to Node-RED 3.0 Beta 3!",
"ja": "Node-RED 3.0 ベータ1へようこそ!" "ja": "Node-RED 3.0 ベータ3へようこそ!"
}, },
description: { description: {
"en-US": "<p>This is the first Beta release of Node-RED 3.0. It contains just about everything we have planned for the final release.</p><p>Let's take a moment to discover the new features in this release.</p>", "en-US": "<p>This is the final beta release of Node-RED 3.0.</p><p>Let's take a moment to discover the new features in this release.</p>",
"ja": "<p>これはNode-RED 3.0の最初のベータリリースです。これには、最終リリースで計画しているほぼ全ての機能が含まれています。</p><p>本リリースの新機能を見つけてみましょう。</p>" // "ja": "<p>これはNode-RED 3.0の最初のベータリリースです。これには、最終リリースで計画しているほぼ全ての機能が含まれています。</p><p>本リリースの新機能を見つけてみましょう。</p>"
}
},
{
title: {
"en-US": "Context Menu"
},
image: 'images/context-menu.png',
description: {
"en-US": `<p>The editor now has its own context menu when you
right-click in the workspace.</p>
<p>This makes many of the built-in actions much easier
to access.</p>`
} }
}, },
{ {
@ -19,12 +31,13 @@ export default {
}, },
image: 'images/junction-slice.gif', image: 'images/junction-slice.gif',
description: { description: {
"en-US": `<p>To make it easier to route wires around your flows, it is now possible to "en-US": `<p>To make it easier to route wires around your flows,
add junction nodes that give you more control.</p> it is now possible to add junction nodes that give
<p>Junctions can be added to wires by holding the Shift key, then click and drag with you more control.</p>
the right-hand mouse button across the wires.</p>`, <p>Junctions can be added to wires by holding the Alt key
"ja": `<p>フローのワイヤーの経路をより制御しやすくするために、分岐点ノードを追加できるようになりました。</p> then click and drag the mouse across the wires.</p>`,
<p>シフトキーを押しながらマウスの右ボタンをクリックしワイヤーを横切るようにドラッグすることで分岐点を追加できます</p>` // "ja": `<p>フローのワイヤーの経路をより制御しやすくするために、分岐点ノードを追加できるようになりました。</p>
// <p>シフトキーを押しながら、マウスの右ボタンをクリックし、ワイヤーを横切るようにドラッグすることで、分岐点を追加できます。</p>`
}, },
}, },
{ {

View File

@ -460,7 +460,7 @@
} }
}); });
var buildEditor = function(id, stateId, focus, value, defaultValue, extraLibs) { var buildEditor = function(id, stateId, focus, value, defaultValue, extraLibs, offset) {
var editor = RED.editor.createEditor({ var editor = RED.editor.createEditor({
id: id, id: id,
mode: 'ace/mode/nrjavascript', mode: 'ace/mode/nrjavascript',
@ -484,14 +484,14 @@
extraLibs: extraLibs extraLibs: extraLibs
}); });
if (defaultValue && value === "") { if (defaultValue && value === "") {
editor.moveCursorTo(defaultValue.split("\n").length - 1, 0); editor.moveCursorTo(defaultValue.split("\n").length +offset, 0);
} }
editor.__stateId = stateId; editor.__stateId = stateId;
return editor; return editor;
} }
this.initEditor = buildEditor('node-input-init-editor', this.id + "/" + "initEditor", false, $("#node-input-initialize").val(), RED._("node-red:function.text.initialize")) this.initEditor = buildEditor('node-input-init-editor', this.id + "/" + "initEditor", false, $("#node-input-initialize").val(), RED._("node-red:function.text.initialize"), undefined, 0);
this.editor = buildEditor('node-input-func-editor', this.id + "/" + "editor", true, $("#node-input-func").val(), undefined, that.libs || []) this.editor = buildEditor('node-input-func-editor', this.id + "/" + "editor", true, $("#node-input-func").val(), undefined, that.libs || [], undefined, -1);
this.finalizeEditor = buildEditor('node-input-finalize-editor', this.id + "/" + "finalizeEditor", false, $("#node-input-finalize").val(), RED._("node-red:function.text.finalize")) this.finalizeEditor = buildEditor('node-input-finalize-editor', this.id + "/" + "finalizeEditor", false, $("#node-input-finalize").val(), RED._("node-red:function.text.finalize"), undefined, 0);
RED.library.create({ RED.library.create({
url:"functions", // where to get the data from url:"functions", // where to get the data from

View File

@ -198,8 +198,8 @@
category: 'storage', category: 'storage',
defaults: { defaults: {
name: {value:""}, name: {value:""},
filename: {value:"filename"}, filename: {value:""},
filenameType: {value:"msg"}, filenameType: {value:"str"},
appendNewline: {value:true}, appendNewline: {value:true},
createDir: {value:false}, createDir: {value:false},
overwriteFile: {value:"false"}, overwriteFile: {value:"false"},
@ -236,8 +236,8 @@
label: node._("file.encoding.setbymsg") label: node._("file.encoding.setbymsg")
}).text(label).appendTo(encSel); }).text(label).appendTo(encSel);
$("#node-input-filename").typedInput({ $("#node-input-filename").typedInput({
default: "msg", default: "str",
types: ["str", "msg", "jsonata", "env"], types: [{label:RED._("node-red:file.label.path"), value:"str", icon:""}, "msg", "jsonata", "env"],
typeField: $("#node-input-filenameType") typeField: $("#node-input-filenameType")
}); });
if(typeof node.filenameType == 'undefined') { if(typeof node.filenameType == 'undefined') {
@ -297,8 +297,8 @@
category: 'storage', category: 'storage',
defaults: { defaults: {
name: {value:""}, name: {value:""},
filename: {value:"filename"}, filename: {value:""},
filenameType: {value:"msg"}, filenameType: {value:"str"},
format: {value:"utf8"}, format: {value:"utf8"},
chunk: {value:false}, chunk: {value:false},
sendError: {value: false}, sendError: {value: false},
@ -341,8 +341,8 @@
label: label label: label
}).text(label).appendTo(encSel); }).text(label).appendTo(encSel);
$("#node-input-filename").typedInput({ $("#node-input-filename").typedInput({
default: "msg", default: "str",
types: ["str", "msg", "jsonata", "env"], types: [{label:RED._("node-red:file.label.path"), value:"str", icon:""}, "msg", "jsonata", "env"],
typeField: $("#node-input-filenameType") typeField: $("#node-input-filenameType")
}); });
if(typeof node.filenameType == 'undefined') { if(typeof node.filenameType == 'undefined') {

View File

@ -928,6 +928,7 @@
"write": "write file", "write": "write file",
"read": "read file", "read": "read file",
"filename": "Filename", "filename": "Filename",
"path": "path",
"action": "Action", "action": "Action",
"addnewline": "Add newline (\\n) to each payload?", "addnewline": "Add newline (\\n) to each payload?",
"createdir": "Create directory if it doesn't exist?", "createdir": "Create directory if it doesn't exist?",

View File

@ -36,7 +36,7 @@
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"media-typer": "1.1.0", "media-typer": "1.1.0",
"mqtt": "4.3.7", "mqtt": "4.3.7",
"multer": "1.4.4", "multer": "1.4.5-lts.1",
"mustache": "4.2.0", "mustache": "4.2.0",
"node-watch": "0.7.3", "node-watch": "0.7.3",
"on-headers": "1.0.2", "on-headers": "1.0.2",

View File

@ -88,9 +88,10 @@ function getLocalFile(file) {
/** /**
* Synchronously walks the directory looking for node files. * Synchronously walks the directory looking for node files.
* @param dir the directory to search * @param dir the directory to search
* @param skipValidNodeRedModules a flag to skip lading icons & files if the directory a valid node-red module
* @return an array of fully-qualified paths to .js files * @return an array of fully-qualified paths to .js files
*/ */
function getLocalNodeFiles(dir) { function getLocalNodeFiles(dir, skipValidNodeRedModules) {
dir = path.resolve(dir); dir = path.resolve(dir);
var result = []; var result = [];
@ -102,6 +103,14 @@ function getLocalNodeFiles(dir) {
return {files: [], icons: []}; return {files: [], icons: []};
} }
files.sort(); files.sort();
// when loading local files, if the path is a valid node-red module
// dont include it (will be picked up in scanTreeForNodesModules)
if(skipValidNodeRedModules && files.indexOf("package.json") >= 0) {
const package = getPackageDetails(dir)
if(package.isNodeRedModule) {
return {files: [], icons: []};
}
}
files.forEach(function(fn) { files.forEach(function(fn) {
var stats = fs.statSync(path.join(dir,fn)); var stats = fs.statSync(path.join(dir,fn));
if (stats.isFile()) { if (stats.isFile()) {
@ -114,7 +123,7 @@ function getLocalNodeFiles(dir) {
} else if (stats.isDirectory()) { } else if (stats.isDirectory()) {
// Ignore /.dirs/, /lib/ /node_modules/ // Ignore /.dirs/, /lib/ /node_modules/
if (!/^(\..*|lib|icons|node_modules|test|locales)$/.test(fn)) { if (!/^(\..*|lib|icons|node_modules|test|locales)$/.test(fn)) {
var subDirResults = getLocalNodeFiles(path.join(dir,fn)); var subDirResults = getLocalNodeFiles(path.join(dir,fn), skipValidNodeRedModules);
result = result.concat(subDirResults.files); result = result.concat(subDirResults.files);
icons = icons.concat(subDirResults.icons); icons = icons.concat(subDirResults.icons);
} else if (fn === "icons") { } else if (fn === "icons") {
@ -126,11 +135,19 @@ function getLocalNodeFiles(dir) {
return {files: result, icons: icons} return {files: result, icons: icons}
} }
function scanDirForNodesModules(dir,moduleName) { function scanDirForNodesModules(dir,moduleName,package) {
var results = []; let results = [];
var scopeName; let scopeName;
let files
try { try {
var files = fs.readdirSync(dir); let isNodeRedModule = false
if(package) {
dir = path.join(package.moduleDir,'..')
files = [path.basename(package.moduleDir)]
moduleName = (package.package ? package.package.name : null) || moduleName
isNodeRedModule = package.isNodeRedModule
} else {
files = fs.readdirSync(dir);
if (moduleName) { if (moduleName) {
var m = /^(?:(@[^/]+)[/])?([^@/]+)/.exec(moduleName); var m = /^(?:(@[^/]+)[/])?([^@/]+)/.exec(moduleName);
if (m) { if (m) {
@ -138,9 +155,10 @@ function scanDirForNodesModules(dir,moduleName) {
moduleName = m[2]; moduleName = m[2];
} }
} }
for (var i=0;i<files.length;i++) { }
var fn = files[i]; for (let i=0;i<files.length;i++) {
if (/^@/.test(fn)) { let fn = files[i];
if (!isNodeRedModule && /^@/.test(fn)) {
if (scopeName && scopeName === fn) { if (scopeName && scopeName === fn) {
// Looking for a specific scope/module // Looking for a specific scope/module
results = results.concat(scanDirForNodesModules(path.join(dir,fn),moduleName)); results = results.concat(scanDirForNodesModules(path.join(dir,fn),moduleName));
@ -149,16 +167,18 @@ function scanDirForNodesModules(dir,moduleName) {
results = results.concat(scanDirForNodesModules(path.join(dir,fn),moduleName)); results = results.concat(scanDirForNodesModules(path.join(dir,fn),moduleName));
} }
} else { } else {
if (isIncluded(fn) && !isExcluded(fn) && (!moduleName || fn == moduleName)) { if ((isNodeRedModule || (!moduleName || fn == moduleName)) && (isIncluded(fn) && !isExcluded(fn))) {
var pkgfn = path.join(dir,fn,"package.json");
try { try {
var pkg = require(pkgfn); const moduleDir = isNodeRedModule ? package.moduleDir : path.join(dir,fn);
if (pkg['node-red']) { const pkg = package || getPackageDetails(moduleDir)
if (!registryUtil.checkModuleAllowed(pkg.name,pkg.version,loadAllowList,loadDenyList)) { if(pkg.error) {
log.debug("! Module: "+pkg.name+" "+pkg.version+ " *ignored due to denyList*"); throw pkg.error
}
if (pkg.isNodeRedModule) {
if (!pkg.allowed) {
log.debug("! Module: "+pkg.package.name+" "+pkg.package.version+ " *ignored due to denyList*");
} else { } else {
var moduleDir = path.join(dir,fn); results.push({dir:moduleDir,package:pkg.package});
results.push({dir:moduleDir,package:pkg});
} }
} }
} catch(err) { } catch(err) {
@ -183,10 +203,13 @@ function scanDirForNodesModules(dir,moduleName) {
* @return a list of node modules: {dir,package} * @return a list of node modules: {dir,package}
*/ */
function scanTreeForNodesModules(moduleName) { function scanTreeForNodesModules(moduleName) {
var dir = settings.coreNodesDir; let coreNodesDir = settings.coreNodesDir;
var results = []; let results = [];
var userDir; let userDir;
let nodesDir;
if(settings.nodesDir) {
nodesDir = Array.isArray(settings.nodesDir) ? settings.nodesDir : [settings.nodesDir]
}
if (settings.userDir) { if (settings.userDir) {
packageList = getPackageList(); packageList = getPackageList();
userDir = path.join(settings.userDir,"node_modules"); userDir = path.join(settings.userDir,"node_modules");
@ -201,15 +224,46 @@ function scanTreeForNodesModules(moduleName) {
}); });
} }
if (dir) { if (coreNodesDir) {
var up = path.resolve(path.join(dir,"..")); var up = path.resolve(path.join(coreNodesDir,".."));
while (up !== dir) { while (up !== coreNodesDir) {
var pm = path.join(dir,"node_modules"); var pm = path.join(coreNodesDir,"node_modules");
if (pm != userDir) { if (pm != userDir) {
results = results.concat(scanDirForNodesModules(pm,moduleName)); results = results.concat(scanDirForNodesModules(pm,moduleName));
} }
dir = up; coreNodesDir = up;
up = path.resolve(path.join(dir,"..")); up = path.resolve(path.join(coreNodesDir,".."));
}
}
// scan nodesDir for any node-red modules
/*
1. if !exist(package.json) || !package.json.has(node-red) => look for node_modules
2. exist(package.json) && package.json.has(node-red) => load this only
3. in original scan of nodesDir, ignore if:(exist(package.json) && package.json.has(node-red))
*/
if (nodesDir) {
for (let dirIndex = 0; dirIndex < nodesDir.length; dirIndex++) {
const nodeDir = nodesDir[dirIndex];
const packageDetails = getPackageDetails(nodeDir)
if(packageDetails.isNodeRedModule) {
//we have found a node-red module, scan it
const nrModules = scanDirForNodesModules(nodeDir, packageDetails.package.name, packageDetails);
results = results.concat(nrModules);
} else if (packageDetails.has_node_modules) {
//If this dir has a `node_modues` dir, scan it
const nodeModulesDir = path.join(nodeDir, 'node_modules')
const nrModules = scanDirForNodesModules(nodeModulesDir, moduleName );
results = results.concat(nrModules);
} else {
//If this is not a node-red module AND it does NOT have a node_modules dir,
//it may be a directory of project directories or a node_modules dir?
//scan this instead
const nrModules = scanDirForNodesModules(nodeDir, moduleName);
results = results.concat(nrModules);
}
} }
} }
return results; return results;
@ -274,24 +328,26 @@ function getModuleNodeFiles(module) {
} }
function getNodeFiles(disableNodePathScan) { function getNodeFiles(disableNodePathScan) {
var dir;
// Find all of the nodes to load // Find all of the nodes to load
var nodeFiles = []; let results;
var results; let nodesDir;
if(settings.nodesDir) {
var dir; nodesDir = Array.isArray(settings.nodesDir) ? settings.nodesDir : [settings.nodesDir]
var iconList = []; }
let dir;
let nodeFiles = [];
let iconList = [];
if (settings.coreNodesDir) { if (settings.coreNodesDir) {
results = getLocalNodeFiles(path.resolve(settings.coreNodesDir)); results = getLocalNodeFiles(path.resolve(settings.coreNodesDir));
nodeFiles = nodeFiles.concat(results.files); nodeFiles = nodeFiles.concat(results.files);
iconList = iconList.concat(results.icons); iconList = iconList.concat(results.icons);
var defaultLocalesPath = path.join(settings.coreNodesDir,"locales"); let defaultLocalesPath = path.join(settings.coreNodesDir,"locales");
i18n.registerMessageCatalog("node-red",defaultLocalesPath,"messages.json"); i18n.registerMessageCatalog("node-red",defaultLocalesPath,"messages.json");
} }
if (settings.userDir) { if (settings.userDir) {
dir = path.join(settings.userDir,"lib","icons"); dir = path.join(settings.userDir,"lib","icons");
var icons = scanIconDir(dir); let icons = scanIconDir(dir);
if (icons.length > 0) { if (icons.length > 0) {
iconList.push({path:dir,icons:icons}); iconList.push({path:dir,icons:icons});
} }
@ -301,13 +357,9 @@ function getNodeFiles(disableNodePathScan) {
nodeFiles = nodeFiles.concat(results.files); nodeFiles = nodeFiles.concat(results.files);
iconList = iconList.concat(results.icons); iconList = iconList.concat(results.icons);
} }
if (settings.nodesDir) { if (nodesDir) {
dir = settings.nodesDir; for (let i = 0; i < nodesDir.length; i++) {
if (typeof settings.nodesDir == "string") { results = getLocalNodeFiles(nodesDir[i], true);
dir = [dir];
}
for (var i=0;i<dir.length;i++) {
results = getLocalNodeFiles(dir[i]);
nodeFiles = nodeFiles.concat(results.files); nodeFiles = nodeFiles.concat(results.files);
iconList = iconList.concat(results.icons); iconList = iconList.concat(results.icons);
} }
@ -479,7 +531,52 @@ function getPackageList() {
} }
return list; return list;
} }
/**
* Gets the package json object for the supplied `dir`.
* If there is no package.json or the `node-red` section is missing, `result.isNodeRedModule` will be `false`.
* If there is no package.json `isPackage` will be `false`.
* If an error occurs, `result.error` will contain the error.
* @param {string} dir The directory to inspect
*/
function getPackageDetails(dir) {
const result = {
/** @type {string} The package directory */
moduleDir: dir,
/** @type {string} The full file path of package.json for this package */
packageFile: null,
/** @type {boolean} True if this is a valid node-red module */
isNodeRedModule: false,
/** @type {boolean} True if a package.json file is present */
isPackage: false,
/** @type {boolean} True if this a node-red module and passes the checks */
allowed: false,
/** @type {object} The contents of package.json */
package: null,
}
if (!dir) { return result }
try {
const packagefile = path.join(dir,'package.json')
result.has_node_modules = fs.existsSync(path.join(dir,'node_modules'))
if(!fs.existsSync(packagefile)) {
return result
}
result.packageFile = packagefile
const pkg = require(packagefile)
result.package = pkg
if(result.package) {
result.allowed = true
result.isPackage = true
result.isNodeRedModule = typeof result.package['node-red'] === 'object'
if(result.isNodeRedModule) {
result.isNodeRedModule = true;
result.allowed = registryUtil.checkModuleAllowed(pkg.name,pkg.version,loadAllowList,loadDenyList)
}
}
} catch(err) {
result.error = err; // this is not a package we are interested in!
}
return result || result;
}
module.exports = { module.exports = {
init: init, init: init,
getNodeFiles: getNodeFiles, getNodeFiles: getNodeFiles,

View File

@ -21,6 +21,6 @@
"fs-extra": "10.1.0", "fs-extra": "10.1.0",
"semver": "7.3.7", "semver": "7.3.7",
"tar": "6.1.11", "tar": "6.1.11",
"uglify-js": "3.15.5" "uglify-js": "3.16.0"
} }
} }

View File

@ -16,7 +16,7 @@
], ],
"dependencies": { "dependencies": {
"fs-extra": "10.1.0", "fs-extra": "10.1.0",
"i18next": "21.8.2", "i18next": "21.8.10",
"json-stringify-safe": "5.0.1", "json-stringify-safe": "5.0.1",
"jsonata": "1.8.6", "jsonata": "1.8.6",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",

View File

@ -14,26 +14,41 @@
* limitations under the License. * limitations under the License.
**/ **/
var should = require("should"); const should = require("should");
var sinon = require("sinon"); const sinon = require("sinon");
var path = require("path"); const path = require("path");
var NR_TEST_UTILS = require("nr-test-utils"); const NR_TEST_UTILS = require("nr-test-utils");
var localfilesystem = NR_TEST_UTILS.require("@node-red/registry/lib/localfilesystem"); const localfilesystem = NR_TEST_UTILS.require("@node-red/registry/lib/localfilesystem");
var resourcesDir = path.resolve(path.join(__dirname,"resources","local")); const resourcesDir = path.resolve(path.join(__dirname,"resources","local"));
var userDir = path.resolve(path.join(__dirname,"resources","userDir")); const userDir = path.resolve(path.join(__dirname,"resources","userDir"));
var moduleDir = path.resolve(path.join(__dirname,"resources","local","TestNodeModule"));
var i18n = NR_TEST_UTILS.require("@node-red/util").i18n; const nodesDir1 = path.resolve(path.join(__dirname,"resources","nodesDir1"))
const nodesDir2 = path.resolve(path.join(__dirname,"resources","nodesDir2"))
const nodesDir3 =path.resolve(path.join(__dirname,"resources","nodesDir3"))
const moduleDir = path.resolve(path.join(__dirname,"resources","local","TestNodeModule"));
const i18n = NR_TEST_UTILS.require("@node-red/util").i18n;
describe("red/nodes/registry/localfilesystem",function() { describe("red/nodes/registry/localfilesystem",function() {
var stubs = [];
function stubPathJoin() {
var _join = path.join;
stubs.push(sinon.stub(path,"join").callsFake(function() {
if (arguments[0] == resourcesDir) {
// This stops the module tree scan from going any higher
// up the tree than resourcesDir.
return arguments[0];
}
return _join.apply(null,arguments);
}));
}
beforeEach(function() { beforeEach(function() {
stubs.push(sinon.stub(i18n,"registerMessageCatalog").callsFake(function() { return Promise.resolve(); })); stubs.push(sinon.stub(i18n,"registerMessageCatalog").callsFake(function() { return Promise.resolve(); }));
}) })
var stubs = [];
afterEach(function() { afterEach(function() {
while(stubs.length) { while(stubs.length) {
stubs.pop().restore(); stubs.pop().restore();
@ -129,16 +144,76 @@ describe("red/nodes/registry/localfilesystem",function() {
checkNodes(nm.nodes,['TestNode5'],['TestNode1']); checkNodes(nm.nodes,['TestNode5'],['TestNode1']);
done(); done();
}); });
it("Finds nodes module path",function(done) { it("Finds nodes and icons only in nodesDir with files, icons and valid node-red packages",function(done) {
var _join = path.join; localfilesystem.init({nodesDir:nodesDir1});
stubs.push(sinon.stub(path,"join").callsFake(function() { const nodeList = localfilesystem.getNodeFiles(true);
if (arguments[0] == resourcesDir) { nodeList.should.have.a.property("node-red");
// This stops the module tree scan from going any higher const nm = nodeList['node-red'];
// up the tree than resourcesDir. nm.should.have.a.property('name','node-red');
return arguments[0]; nm.should.have.a.property("nodes");
nm.should.have.a.property("icons");
checkNodes(nm.nodes,['loose1', 'loose2'], []);
//1 icon in nodesDir1/icons/ - should be found
//2 icons in nodesDir1/loose2/icons/ - should be found
//1 icons in nodesDir1/node-red-node-testnode/icons/ - should be found
//1 icons in nodesDir1/regular_module/icons/ - should NOT be found
//total icon sets 3, total icons 4
nm.icons.should.have.a.property("length", 3);
nm.icons[0].should.have.a.property("path")
nm.icons[0].should.have.a.property("icons", ['loose1.svg'])
nm.icons[1].should.have.a.property("path")
nm.icons[1].should.have.a.property("icons", ['loose2.svg', 'loose2b.svg'])
nm.icons[2].should.have.a.property("path")
nm.icons[2].should.have.a.property("icons", ['test.svg'])
done();
});
it("Should not find node-red node in nodesDir with files, icons and valid node-red packages",function(done) {
// path contains a regular node module and a node-red node module
localfilesystem.init({nodesDir:path.join(nodesDir1)});
const nodeList = localfilesystem.getNodeFiles(true);
nodeList.should.have.a.property("node-red");
const nm = nodeList['node-red'];
nm.should.have.a.property('name','node-red');
nm.should.have.a.property("nodes");
nm.nodes.should.have.a.property("loose1");
nm.nodes.should.have.a.property("loose2");
nm.nodes.should.not.have.a.property("regular_module");
nm.nodes.should.not.have.a.property("node-red-node-testnode");
for (let key of Object.keys(nm.nodes)) {
const n = nm.nodes[key];
n.file.indexOf("regular_module").should.eql(-1, `found icons in a node-red module`)
n.file.indexOf("node-red-node-testnode").should.eql(-1, `found icons in a node-red module`)
} }
return _join.apply(null,arguments); //1 icon in nodesDir1/icons/ - should be found
})); //2 icons in nodesDir1/loose2/icons/ - should be found
//1 icons in nodesDir1/node-red-node-testnode/icons/ - should be found
//1 icons in nodesDir1/regular_module/icons/ - should NOT be found
//total icon sets 3, total icons 4
nm.should.have.a.property("icons");
nm.icons.should.have.a.property("length", 3);
let iconCount = 0;
for (let index = 0; index < nm.icons.length; index++) {
const iconDir = nm.icons[index];
iconCount += iconDir.icons.length
iconDir.path.indexOf("node-red-node-testnode").should.eql(-1, `should not find icons in a node-red module`)
}
should(iconCount).eql(4, "Should find only 4 icons")
done();
});
it("Should not find node-red node in nodesDir when regular package and valid node-red packages",function(done) {
localfilesystem.init({nodesDir:path.join(nodesDir1,"regular_module")});
const nodeList = localfilesystem.getNodeFiles(true);
nodeList.should.have.a.property("node-red");
const nm = nodeList['node-red'];
nm.should.have.a.property('name','node-red');
nm.should.have.a.property("nodes", {});
nm.should.have.a.property("icons");
nm.icons.should.have.a.property("length", 1); //should find 1 icons folder
nm.icons[0].should.have.a.property("icons", [ 'test.svg' ]); //should find 1 icon in regular package
done();
});
it("Finds nodes module path",function(done) {
stubPathJoin()
localfilesystem.init({coreNodesDir:moduleDir}); localfilesystem.init({coreNodesDir:moduleDir});
var nodeList = localfilesystem.getNodeFiles(); var nodeList = localfilesystem.getNodeFiles();
nodeList.should.have.a.property("node-red"); nodeList.should.have.a.property("node-red");
@ -166,8 +241,6 @@ describe("red/nodes/registry/localfilesystem",function() {
i18n.registerMessageCatalog.lastCall.args[1].should.eql(path.resolve(path.join(moduleDir,"locales"))); i18n.registerMessageCatalog.lastCall.args[1].should.eql(path.resolve(path.join(moduleDir,"locales")));
i18n.registerMessageCatalog.lastCall.args[2].should.eql('messages.json'); i18n.registerMessageCatalog.lastCall.args[2].should.eql('messages.json');
done(); done();
}); });
it.skip("finds locales directory"); it.skip("finds locales directory");
@ -205,15 +278,7 @@ describe("red/nodes/registry/localfilesystem",function() {
}); });
describe("#getModuleFiles",function() { describe("#getModuleFiles",function() {
it("gets a nodes module files",function(done) { it("gets a nodes module files",function(done) {
var _join = path.join; stubPathJoin()
stubs.push(sinon.stub(path,"join").callsFake(function() {
if (arguments[0] == resourcesDir) {
// This stops the module tree scan from going any higher
// up the tree than resourcesDir.
return arguments[0];
}
return _join.apply(null,arguments);
}));
localfilesystem.init({coreNodesDir:moduleDir}); localfilesystem.init({coreNodesDir:moduleDir});
var nodeModule = localfilesystem.getModuleFiles('TestNodeModule'); var nodeModule = localfilesystem.getModuleFiles('TestNodeModule');
nodeModule.should.have.a.property('TestNodeModule'); nodeModule.should.have.a.property('TestNodeModule');
@ -230,16 +295,87 @@ describe("red/nodes/registry/localfilesystem",function() {
done(); done();
}); });
it("Finds only 1 node-red node in nodesDir amongst legacy nodes and regular nodes",function(done) {
stubPathJoin()
localfilesystem.init({nodesDir:[path.join(nodesDir1,"node-red-node-testnode")]});
const nodeModule = localfilesystem.getModuleFiles();
const loaded = Object.keys(nodeModule)
loaded.should.have.a.property("length", 1)
loaded.indexOf('node-red-node-testnode').should.greaterThan(-1, "Should load node-red-node-testnode")
nodeModule['node-red-node-testnode'].should.have.a.property('name','node-red-node-testnode');
nodeModule['node-red-node-testnode'].should.have.a.property('version','1.0.0');
nodeModule['node-red-node-testnode'].should.have.a.property('nodes');
nodeModule['node-red-node-testnode'].should.have.a.property('path');
nodeModule['node-red-node-testnode'].should.have.a.property('user', false);
checkNodes(nodeModule['node-red-node-testnode'].nodes,['testnode'],[],'node-red-node-testnode');
done();
});
it("Finds a node-red node in nodesDir with a sub dir containing valid node-red package",function(done) {
stubPathJoin()
localfilesystem.init({nodesDir:[path.join(nodesDir1,"node-red-node-testnode")]});
const nodeModule = localfilesystem.getModuleFiles();
const loaded = Object.keys(nodeModule)
nodeModule['node-red-node-testnode'].should.have.a.property('name','node-red-node-testnode');
nodeModule['node-red-node-testnode'].should.have.a.property('version','1.0.0');
nodeModule['node-red-node-testnode'].should.have.a.property('nodes');
nodeModule['node-red-node-testnode'].should.have.a.property('path');
nodeModule['node-red-node-testnode'].should.have.a.property('user', false);
checkNodes(nodeModule['node-red-node-testnode'].nodes,['testnode'],[],'node-red-node-testnode');
done();
});
it("Finds 2 node-red modules and 1 plugin in nodesDir (in root of dir)",function(done) {
stubPathJoin()
localfilesystem.init({nodesDir:[nodesDir2]});
const nodeModule = localfilesystem.getModuleFiles();
const loaded = Object.keys(nodeModule)
loaded.should.have.a.property("length", 3)
loaded.indexOf('@test/testnode').should.greaterThan(-1, "Should load @test/testnode")
loaded.indexOf('testnode2').should.greaterThan(-1, "Should load testnode2")
loaded.indexOf('test-theme2').should.greaterThan(-1, "Should load test-theme2")
nodeModule['@test/testnode'].should.have.a.property('name','@test/testnode');
nodeModule['@test/testnode'].should.have.a.property('version','1.0.0');
nodeModule['@test/testnode'].should.have.a.property('nodes');
nodeModule['@test/testnode'].should.have.a.property('path');
nodeModule['@test/testnode'].should.have.a.property('user', false);
nodeModule['testnode2'].should.have.a.property('name','testnode2');
nodeModule['testnode2'].should.have.a.property('version','1.0.0');
nodeModule['testnode2'].should.have.a.property('nodes');
nodeModule['testnode2'].should.have.a.property('path');
nodeModule['testnode2'].should.have.a.property('user', false);
nodeModule['test-theme2'].should.have.a.property('name','test-theme2');
nodeModule['test-theme2'].should.have.a.property('version','0.0.1');
nodeModule['test-theme2'].should.have.a.property('nodes', {});
nodeModule['test-theme2'].should.have.a.property('path');
nodeModule['test-theme2'].should.have.a.property('user', false);
nodeModule['test-theme2'].should.have.a.property('plugins');
nodeModule['test-theme2'].plugins.should.have.a.property('test-theme2');
nodeModule['test-theme2'].plugins['test-theme2'].should.have.a.property('name','test-theme2');
nodeModule['test-theme2'].plugins['test-theme2'].should.have.a.property('module','test-theme2');
nodeModule['test-theme2'].plugins['test-theme2'].should.have.a.property('version', '0.0.1');
nodeModule['test-theme2'].plugins['test-theme2'].should.have.a.property('file');
nodeModule['test-theme2'].plugins['test-theme2'].should.have.a.property('local', false);
done();
});
it("Finds 2 node-red modules and 1 plugin in nodesDir pointing to a node_modules dir",function(done) {
stubPathJoin()
localfilesystem.init({nodesDir:[path.join(nodesDir3, "node_modules")]});
const nodeModule = localfilesystem.getModuleFiles();
const loaded = Object.keys(nodeModule)
loaded.should.have.a.property("length", 3)
loaded.indexOf('@test/testnode').should.greaterThan(-1, "Should load @test/testnode")
loaded.indexOf('@test/test-theme3').should.greaterThan(-1, "Should load test-theme3")
loaded.indexOf('testnode3').should.greaterThan(-1, "Should load testnode3")
done();
});
it("throws an error if a node isn't found",function(done) { it("throws an error if a node isn't found",function(done) {
var _join = path.join; stubPathJoin()
stubs.push(sinon.stub(path,"join").callsFake(function() {
if (arguments[0] == resourcesDir) {
// This stops the module tree scan from going any higher
// up the tree than resourcesDir.
return arguments[0];
}
return _join.apply(null,arguments);
}));
localfilesystem.init({coreNodesDir:moduleDir}); localfilesystem.init({coreNodesDir:moduleDir});
/*jshint immed: false */ /*jshint immed: false */
(function(){ (function(){
@ -250,15 +386,7 @@ describe("red/nodes/registry/localfilesystem",function() {
it.skip("finds locales directory"); it.skip("finds locales directory");
it.skip("finds icon path directory"); it.skip("finds icon path directory");
it("scans icon files with a module file",function(done) { it("scans icon files with a module file",function(done) {
var _join = path.join; stubPathJoin()
stubs.push(sinon.stub(path,"join").callsFake(function() {
if (arguments[0] == resourcesDir) {
// This stops the module tree scan from going any higher
// up the tree than resourcesDir.
return arguments[0];
}
return _join.apply(null,arguments);
}));
localfilesystem.init({ localfilesystem.init({
coreNodesDir: moduleDir coreNodesDir: moduleDir
}); });

View File

@ -0,0 +1 @@
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><path d="M10.004 14.499h20M10.004 46.503h20M10.004 22.5h20M10.004 30.501h20M10.004 38.502h20" stroke="#fff" stroke-width="2.9997000000000003"/></svg>

After

Width:  |  Height:  |  Size: 236 B

View File

@ -0,0 +1,5 @@
<script>
(function() {
console.log("hello from loose1.html")
})()
</script>

View File

@ -0,0 +1,4 @@
(function() {
console.log("hello from loose1.js")
})()

View File

@ -0,0 +1 @@
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><path d="M30.999 31.005v-3h-6.762s.812-12.397 1.162-14 .597-3.35 2.628-3.103 1.971 3.103 1.971 3.103l4.862-.016s-.783-3.984-2.783-5.984-7.946-1.7-9.633.03c-1.687 1.73-2.302 5.065-2.597 6.422-.588 4.5-.854 9.027-1.248 13.547h-8.6v3H18.1s-.812 12.398-1.162 14-.597 3.35-2.628 3.103-1.972-3.102-1.972-3.102l-4.862.015s.783 3.985 2.783 5.985c2 2 7.946 1.699 9.634-.031 1.687-1.73 2.302-5.065 2.597-6.422.587-4.5.854-9.027 1.248-13.547z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 539 B

View File

@ -0,0 +1 @@
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><path d="M7 38.98v3.983h11v12l13-23H19l-.463.017c-1.28 4.048-5.066 6.983-9.537 6.983zm12-11.017h12l-13-23v12H7V20.9l2 .064c4.467 0 8.25 2.93 9.534 6.972zM6.95 24.22a6 6 0 1 1-.083 11.456" fill="#fff" style="isolation:auto;mix-blend-mode:normal"/></svg>

After

Width:  |  Height:  |  Size: 339 B

View File

@ -0,0 +1,5 @@
<script>
(function() {
console.log("hello from loose2.html")
})()
</script>

View File

@ -0,0 +1,4 @@
(function() {
console.log("hello from loose2.js")
})()

View File

@ -0,0 +1 @@
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><path d="M30.999 31.005v-3h-6.762s.812-12.397 1.162-14 .597-3.35 2.628-3.103 1.971 3.103 1.971 3.103l4.862-.016s-.783-3.984-2.783-5.984-7.946-1.7-9.633.03c-1.687 1.73-2.302 5.065-2.597 6.422-.588 4.5-.854 9.027-1.248 13.547h-8.6v3H18.1s-.812 12.398-1.162 14-.597 3.35-2.628 3.103-1.972-3.102-1.972-3.102l-4.862.015s.783 3.985 2.783 5.985c2 2 7.946 1.699 9.634-.031 1.687-1.73 2.302-5.065 2.597-6.422.587-4.5.854-9.027 1.248-13.547z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 539 B

View File

@ -0,0 +1,4 @@
(function() {
console.log("hello from regular module main.js")
})()

View File

@ -0,0 +1,19 @@
{
"name": "node-red-node-testnode",
"version": "1.0.0",
"description": "A node-red node that does nothing other than exist",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"node-red"
],
"node-red": {
"nodes": {
"testnode": "index.js"
}
},
"author": "@testyMcTersterson",
"license": "MIT"
}

View File

@ -0,0 +1 @@
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><path d="M30.999 31.005v-3h-6.762s.812-12.397 1.162-14 .597-3.35 2.628-3.103 1.971 3.103 1.971 3.103l4.862-.016s-.783-3.984-2.783-5.984-7.946-1.7-9.633.03c-1.687 1.73-2.302 5.065-2.597 6.422-.588 4.5-.854 9.027-1.248 13.547h-8.6v3H18.1s-.812 12.398-1.162 14-.597 3.35-2.628 3.103-1.972-3.102-1.972-3.102l-4.862.015s.783 3.985 2.783 5.985c2 2 7.946 1.699 9.634-.031 1.687-1.73 2.302-5.065 2.597-6.422.587-4.5.854-9.027 1.248-13.547z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 539 B

View File

@ -0,0 +1,4 @@
(function() {
console.log("hello from regular module main.js")
})()

View File

@ -0,0 +1,14 @@
{
"name": "regular_node",
"version": "1.0.0",
"description": "A regular node that does nothing other than exist",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"test"
],
"author": "@testyMcTersterson",
"license": "MIT"
}

View File

@ -0,0 +1,5 @@
<script>
(function() {
console.log("hello from @test/testnode index.html")
})()
</script>

View File

@ -0,0 +1,4 @@
(function() {
console.log("hello from @test/testnode index.js")
})()

View File

@ -0,0 +1,20 @@
{
"name": "@test/testnode",
"version": "1.0.0",
"description": "A test node that does nothing other than exist",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"node-red",
"test"
],
"node-red": {
"nodes": {
"testnode": "index.js"
}
},
"author": "@testyMcTersterson",
"license": "MIT"
}

View File

@ -0,0 +1,5 @@
<script>
(function() {
console.log("hello from testnode2 index.js")
})()
</script>

View File

@ -0,0 +1,4 @@
(function() {
console.log("hello from testnode2 index.js")
})()

View File

@ -0,0 +1,20 @@
{
"name": "testnode2",
"version": "1.0.0",
"description": "A test node that does nothing other than exist",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"node-red",
"test"
],
"node-red": {
"nodes": {
"testnode2": "index.js"
}
},
"author": "@testyMcTersterson",
"license": "MIT"
}

View File

@ -0,0 +1,3 @@
(function() {
console.log("Hi from test plugin client side")
})()

View File

@ -0,0 +1,14 @@
module.exports = function (RED) {
RED.plugins.registerPlugin('test-theme', {
type: 'node-red-theme',
scripts: [
'files/clientside.js'
],
css: [
'files/theme.css',
],
monacoOptions: {
theme: "vs"
}
})
}

View File

@ -0,0 +1 @@
:root{--red-ui-primary-background: #f2f3fb;}

View File

@ -0,0 +1,24 @@
{
"name": "test-theme2",
"version": "0.0.1",
"description": "test theme for Node-RED",
"keywords": [
"node-red",
"plugin",
"theme"
],
"author": {
"name": "testy-McTesterson"
},
"license": "MIT",
"node-red": {
"version": ">=2.2.0",
"plugins": {
"test-theme2": "files/plugin.js"
}
},
"engines": {
"node": ">=12.x"
}
}

View File

@ -0,0 +1,5 @@
<script>
(function() {
console.log("hello from @test/testnode index.html")
})()
</script>

View File

@ -0,0 +1,4 @@
(function() {
console.log("hello from @test/testnode index.js")
})()

View File

@ -0,0 +1,20 @@
{
"name": "@test/testnode",
"version": "1.0.0",
"description": "A test node that does nothing other than exist",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"node-red",
"test"
],
"node-red": {
"nodes": {
"testnode": "index.js"
}
},
"author": "@testyMcTersterson",
"license": "MIT"
}

View File

@ -0,0 +1,3 @@
(function() {
console.log("Hi from test plugin client side")
})()

View File

@ -0,0 +1,14 @@
module.exports = function (RED) {
RED.plugins.registerPlugin('test-theme', {
type: 'node-red-theme',
scripts: [
'files/clientside.js'
],
css: [
'files/theme.css',
],
monacoOptions: {
theme: "vs"
}
})
}

View File

@ -0,0 +1 @@
:root{--red-ui-primary-background: #f2f3fb;}

View File

@ -0,0 +1,24 @@
{
"name": "@test/test-theme3",
"version": "0.0.1",
"description": "test theme for Node-RED",
"keywords": [
"node-red",
"plugin",
"theme"
],
"author": {
"name": "testy-McTesterson"
},
"license": "MIT",
"node-red": {
"version": ">=2.2.0",
"plugins": {
"test-theme3": "files/plugin.js"
}
},
"engines": {
"node": ">=12.x"
}
}

View File

@ -0,0 +1,5 @@
<script>
(function() {
console.log("hello from testnode3 index.js")
})()
</script>

View File

@ -0,0 +1,4 @@
(function() {
console.log("hello from testnode3 index.js")
})()

View File

@ -0,0 +1,20 @@
{
"name": "testnode3",
"version": "1.0.0",
"description": "A test node that does nothing other than exist",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"node-red",
"test"
],
"node-red": {
"nodes": {
"testnode3": "index.js"
}
},
"author": "@testyMcTersterson",
"license": "MIT"
}