mirror of
https://github.com/node-red/node-red.git
synced 2025-03-01 10:36:34 +00:00
Merge branch 'master' into refactor-history
This commit is contained in:
commit
db82b10e25
41
CHANGELOG.md
41
CHANGELOG.md
@ -1,3 +1,44 @@
|
||||
#### 4.0.7: Maintenance Release
|
||||
|
||||
Editor
|
||||
|
||||
- Fix def can be undefined if the type is missing (#4997) @GogoVega
|
||||
- Fix the user list of nested config node (#4995) @GogoVega
|
||||
- Support custom login message and button (#4993) @knolleary
|
||||
|
||||
#### 4.0.6: Maintenance Release
|
||||
|
||||
Editor
|
||||
|
||||
- Roll up various fixes on config node change history (#4975) @knolleary
|
||||
- Add quotes when installing local tgz to fix spacing in the file path (#4949) @AGhorab-upland
|
||||
- Validate json dropped into editor to avoid unhelpful error messages (#4964) @knolleary
|
||||
- Fix junction insert position via context menu (#4974) @knolleary
|
||||
- Apply zoom scale when calculating annotation positions (#4981) @knolleary
|
||||
- Handle the import of an incomplete Subflow (#4811) @GogoVega
|
||||
- Fix updating the Subflow name during a copy (#4809) @GogoVega
|
||||
- Rename variable to avoid confusion in view.js (#4963) @knolleary
|
||||
- Change groups.length to groups.size (#4959) @hungtcs
|
||||
- Remove disabled node types from QuickAddDialog list (#4946) @GogoVega
|
||||
- Fix `setModulePendingUpdated` with plugins (#4939) @GogoVega
|
||||
- Missing getSubscriptions in the docs while its implemented (#4934) @ersinpw
|
||||
- Apply `envVarExcludes` setting to `util.getSetting` into the function node (#4925) @GogoVega
|
||||
- Fix `envVar` editable list should be sortable (#4932) @GogoVega
|
||||
- Improve the node name auto-generated with the first available number (#4912) @GogoVega
|
||||
|
||||
Runtime
|
||||
|
||||
- Get the env config node from the parent subflow (#4960) @GogoVega
|
||||
- Update dependencies (#4987) @knolleary
|
||||
|
||||
Nodes
|
||||
|
||||
- Performance : make reading single buffer / string file faster by not re-allocating and handling huge buffers (#4980) @Fadoli
|
||||
- Make delay node rate limit reset consistent - not send on reset. (#4940) @dceejay
|
||||
- Fix trigger node date handling for latest time type input (#4915) @dceejay
|
||||
- Fix delay node not dropping when nodeMessageBufferMaxLength is set (#4973)
|
||||
- Ensure node.sep is honoured when generating CSV (#4982) @knolleary
|
||||
|
||||
#### 4.0.5: Maintenance Release
|
||||
|
||||
Editor
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "node-red",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"description": "Low-code programming for event-driven applications",
|
||||
"homepage": "https://nodered.org",
|
||||
"license": "Apache-2.0",
|
||||
@ -41,7 +41,7 @@
|
||||
"cors": "2.8.5",
|
||||
"cronosjs": "1.7.1",
|
||||
"denque": "2.1.0",
|
||||
"express": "4.21.1",
|
||||
"express": "4.21.2",
|
||||
"express-session": "1.18.1",
|
||||
"form-data": "4.0.0",
|
||||
"fs-extra": "11.2.0",
|
||||
|
@ -126,6 +126,14 @@ async function login(req,res) {
|
||||
if (themeContext.login && themeContext.login.image) {
|
||||
response.image = themeContext.login.image;
|
||||
}
|
||||
if (themeContext.login?.message) {
|
||||
response.loginMessage = themeContext.login?.message
|
||||
}
|
||||
if (themeContext.login?.button) {
|
||||
response.prompts = [
|
||||
{ type: "button", ...themeContext.login.button }
|
||||
]
|
||||
}
|
||||
}
|
||||
res.json(response);
|
||||
}
|
||||
|
@ -206,14 +206,26 @@ module.exports = {
|
||||
}
|
||||
|
||||
if (theme.login) {
|
||||
let themeContextLogin = {}
|
||||
let hasLoginTheme = false
|
||||
if (theme.login.image) {
|
||||
url = serveFile(themeApp,"/login/",theme.login.image);
|
||||
if (url) {
|
||||
themeContext.login = {
|
||||
image: url
|
||||
}
|
||||
themeContextLogin.image = url
|
||||
hasLoginTheme = true
|
||||
}
|
||||
}
|
||||
if (theme.login.message) {
|
||||
themeContextLogin.message = theme.login.message
|
||||
hasLoginTheme = true
|
||||
}
|
||||
if (theme.login.button) {
|
||||
themeContextLogin.button = theme.login.button
|
||||
hasLoginTheme = true
|
||||
}
|
||||
if (hasLoginTheme) {
|
||||
themeContext.login = themeContextLogin
|
||||
}
|
||||
}
|
||||
themeApp.get("/", async function(req,res) {
|
||||
const themePluginList = await runtimeAPI.plugins.getPluginsByType({type:"node-red-theme"});
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/editor-api",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
"repository": {
|
||||
@ -16,14 +16,14 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@node-red/util": "4.0.5",
|
||||
"@node-red/editor-client": "4.0.5",
|
||||
"@node-red/util": "4.0.7",
|
||||
"@node-red/editor-client": "4.0.7",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.20.3",
|
||||
"clone": "2.1.2",
|
||||
"cors": "2.8.5",
|
||||
"express-session": "1.18.1",
|
||||
"express": "4.21.1",
|
||||
"express": "4.21.2",
|
||||
"memorystore": "1.6.7",
|
||||
"mime": "3.0.0",
|
||||
"multer": "1.4.5-lts.1",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/editor-client",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -899,8 +899,58 @@ RED.history = (function () {
|
||||
RED.events.emit("nodes:change", newConfigNode);
|
||||
}
|
||||
});
|
||||
} else if (i === "env" && ev.node.type.indexOf("subflow:") === 0) {
|
||||
// Subflow can have config node in node.env
|
||||
let nodeList = ev.node.env || [];
|
||||
nodeList = nodeList.reduce((list, prop) => {
|
||||
if (prop.type === "conf-type" && prop.value) {
|
||||
list.push(prop.value);
|
||||
}
|
||||
return list;
|
||||
}, []);
|
||||
|
||||
nodeList.forEach(function(id) {
|
||||
const configNode = RED.nodes.node(id);
|
||||
if (configNode) {
|
||||
if (configNode.users.indexOf(ev.node) !== -1) {
|
||||
configNode.users.splice(configNode.users.indexOf(ev.node), 1);
|
||||
RED.events.emit("nodes:change", configNode);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
nodeList = ev.changes.env || [];
|
||||
nodeList = nodeList.reduce((list, prop) => {
|
||||
if (prop.type === "conf-type" && prop.value) {
|
||||
list.push(prop.value);
|
||||
}
|
||||
return list;
|
||||
}, []);
|
||||
|
||||
nodeList.forEach(function(id) {
|
||||
const configNode = RED.nodes.node(id);
|
||||
if (configNode) {
|
||||
if (configNode.users.indexOf(ev.node) === -1) {
|
||||
configNode.users.push(ev.node);
|
||||
RED.events.emit("nodes:change", configNode);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (i === "credentials" && ev.changes[i]) {
|
||||
// Reset - Only want to keep the changes
|
||||
inverseEv.changes[i] = {};
|
||||
for (const [key, value] of Object.entries(ev.changes[i])) {
|
||||
// Edge case: node.credentials is cleared after a deploy, so we can't
|
||||
// capture values for the inverse event when undoing past a deploy
|
||||
if (ev.node.credentials) {
|
||||
inverseEv.changes[i][key] = ev.node.credentials[key];
|
||||
}
|
||||
ev.node.credentials[key] = value;
|
||||
}
|
||||
} else {
|
||||
ev.node[i] = ev.changes[i];
|
||||
}
|
||||
ev.node[i] = ev.changes[i];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1003,6 +1053,25 @@ RED.history = (function () {
|
||||
RED.editor.validateNode(ev.node);
|
||||
}
|
||||
|
||||
// If it's a Config Node, validate user nodes too.
|
||||
// NOTE: The Config Node must be validated before validating users.
|
||||
if (ev.node.users) {
|
||||
const validatedNodes = new Set();
|
||||
const userStack = ev.node.users.slice();
|
||||
|
||||
validatedNodes.add(ev.node.id);
|
||||
while (userStack.length) {
|
||||
const node = userStack.pop();
|
||||
if (!validatedNodes.has(node.id)) {
|
||||
validatedNodes.add(node.id);
|
||||
if (node.users) {
|
||||
userStack.push(...node.users);
|
||||
}
|
||||
RED.editor.validateNode(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add links previously removed (undo)
|
||||
if (ev.links) {
|
||||
inverseEv.createdLinks = [];
|
||||
|
@ -73,7 +73,13 @@ RED.nodes = (function() {
|
||||
|
||||
var exports = {
|
||||
setModulePendingUpdated: function(module,version) {
|
||||
moduleList[module].pending_version = version;
|
||||
if (!!RED.plugins.getModule(module)) {
|
||||
// The module updated is a plugin
|
||||
RED.plugins.getModule(module).pending_version = version;
|
||||
} else {
|
||||
moduleList[module].pending_version = version;
|
||||
}
|
||||
|
||||
RED.events.emit("registry:module-updated",{module:module,version:version});
|
||||
},
|
||||
getModule: function(module) {
|
||||
@ -701,12 +707,15 @@ RED.nodes = (function() {
|
||||
}
|
||||
n["_"] = RED._;
|
||||
}
|
||||
|
||||
// Both node and config node can use a config node
|
||||
updateConfigNodeUsers(newNode, { action: "add" });
|
||||
|
||||
if (n._def.category == "config") {
|
||||
configNodes[n.id] = n;
|
||||
configNodes[n.id] = newNode;
|
||||
} else {
|
||||
if (n.wires && (n.wires.length > n.outputs)) { n.outputs = n.wires.length; }
|
||||
n.dirty = true;
|
||||
updateConfigNodeUsers(n);
|
||||
if (n._def.category == "subflows" && typeof n.i === "undefined") {
|
||||
var nextId = 0;
|
||||
RED.nodes.eachNode(function(node) {
|
||||
@ -768,9 +777,11 @@ RED.nodes = (function() {
|
||||
var removedLinks = [];
|
||||
var removedNodes = [];
|
||||
var node;
|
||||
|
||||
if (id in configNodes) {
|
||||
node = configNodes[id];
|
||||
delete configNodes[id];
|
||||
updateConfigNodeUsers(node, { action: "remove" });
|
||||
RED.events.emit('nodes:remove',node);
|
||||
RED.workspaces.refresh();
|
||||
} else if (allNodes.hasNode(id)) {
|
||||
@ -779,6 +790,9 @@ RED.nodes = (function() {
|
||||
delete nodeLinks[id];
|
||||
removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); });
|
||||
removedLinks.forEach(removeLink);
|
||||
updateConfigNodeUsers(node, { action: "remove" });
|
||||
|
||||
// TODO: Legacy code for exclusive config node
|
||||
var updatedConfigNode = false;
|
||||
for (var d in node._def.defaults) {
|
||||
if (node._def.defaults.hasOwnProperty(d)) {
|
||||
@ -792,10 +806,6 @@ RED.nodes = (function() {
|
||||
if (configNode._def.exclusive) {
|
||||
removeNode(node[d]);
|
||||
removedNodes.push(configNode);
|
||||
} else {
|
||||
var users = configNode.users;
|
||||
users.splice(users.indexOf(node),1);
|
||||
RED.events.emit('nodes:change',configNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1032,23 +1042,34 @@ RED.nodes = (function() {
|
||||
return {nodes:removedNodes,links:removedLinks, groups: removedGroups, junctions: removedJunctions};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Subflow to the Workspace
|
||||
*
|
||||
* @param {object} sf The Subflow to add.
|
||||
* @param {boolean|undefined} createNewIds Whether to update the name.
|
||||
*/
|
||||
function addSubflow(sf, createNewIds) {
|
||||
if (createNewIds) {
|
||||
var subflowNames = Object.keys(subflows).map(function(sfid) {
|
||||
return subflows[sfid].name;
|
||||
});
|
||||
// Update the Subflow name to highlight that this is a copy
|
||||
const subflowNames = Object.keys(subflows).map(function (sfid) {
|
||||
return subflows[sfid].name || "";
|
||||
})
|
||||
subflowNames.sort()
|
||||
|
||||
subflowNames.sort();
|
||||
var copyNumber = 1;
|
||||
var subflowName = sf.name;
|
||||
let copyNumber = 1;
|
||||
let subflowName = sf.name;
|
||||
subflowNames.forEach(function(name) {
|
||||
if (subflowName == name) {
|
||||
subflowName = sf.name + " (" + copyNumber + ")";
|
||||
copyNumber++;
|
||||
subflowName = sf.name+" ("+copyNumber+")";
|
||||
}
|
||||
});
|
||||
|
||||
sf.name = subflowName;
|
||||
}
|
||||
|
||||
sf.instances = [];
|
||||
|
||||
subflows[sf.id] = sf;
|
||||
allNodes.addTab(sf.id);
|
||||
linkTabMap[sf.id] = [];
|
||||
@ -1101,7 +1122,7 @@ RED.nodes = (function() {
|
||||
module: "node-red"
|
||||
}
|
||||
});
|
||||
sf.instances = [];
|
||||
|
||||
sf._def = RED.nodes.getType("subflow:"+sf.id);
|
||||
RED.events.emit("subflows:add",sf);
|
||||
}
|
||||
@ -1743,7 +1764,8 @@ RED.nodes = (function() {
|
||||
// Remove the old subflow definition - but leave the instances in place
|
||||
var removalResult = RED.subflow.removeSubflow(n.id, true);
|
||||
// Create the list of nodes for the new subflow def
|
||||
var subflowNodes = [n].concat(zMap[n.id]);
|
||||
// Need to sort the list in order to remove missing nodes
|
||||
var subflowNodes = [n].concat(zMap[n.id]).filter((s) => !!s);
|
||||
// Import the new subflow - no clashes should occur as we've removed
|
||||
// the old version
|
||||
var result = importNodes(subflowNodes);
|
||||
@ -1780,9 +1802,20 @@ RED.nodes = (function() {
|
||||
// Replace config nodes
|
||||
//
|
||||
configNodeIds.forEach(function(id) {
|
||||
removedNodes = removedNodes.concat(convertNode(getNode(id)));
|
||||
const configNode = getNode(id);
|
||||
const currentUserCount = configNode.users;
|
||||
|
||||
// Add a snapshot of the Config Node
|
||||
removedNodes = removedNodes.concat(convertNode(configNode));
|
||||
|
||||
// Remove the Config Node instance
|
||||
removeNode(id);
|
||||
importNodes([newConfigNodes[id]])
|
||||
|
||||
// Import the new one
|
||||
importNodes([newConfigNodes[id]]);
|
||||
|
||||
// Re-attributes the user count
|
||||
getNode(id).users = currentUserCount;
|
||||
});
|
||||
|
||||
return {
|
||||
@ -2023,6 +2056,8 @@ RED.nodes = (function() {
|
||||
if (matchingSubflow) {
|
||||
subflow_denylist[n.id] = matchingSubflow;
|
||||
} else {
|
||||
const oldId = n.id;
|
||||
|
||||
subflow_map[n.id] = n;
|
||||
if (createNewIds || options.importMap[n.id] === "copy") {
|
||||
nid = getID();
|
||||
@ -2050,7 +2085,7 @@ RED.nodes = (function() {
|
||||
n.status.id = getID();
|
||||
}
|
||||
new_subflows.push(n);
|
||||
addSubflow(n,createNewIds || options.importMap[n.id] === "copy");
|
||||
addSubflow(n,createNewIds || options.importMap[oldId] === "copy");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2159,6 +2194,21 @@ RED.nodes = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
// Config node can use another config node, must ensure that this other
|
||||
// config node is added before to exists when updating the user list
|
||||
const configNodeFilter = function (node) {
|
||||
let count = 0;
|
||||
if (node._def?.defaults) {
|
||||
for (const def of Object.values(node._def.defaults)) {
|
||||
if (def.type) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
};
|
||||
new_nodes.sort((a, b) => configNodeFilter(a) - configNodeFilter(b));
|
||||
|
||||
// Find regular flow nodes and subflow instances
|
||||
for (i=0;i<newNodes.length;i++) {
|
||||
n = newNodes[i];
|
||||
@ -2170,7 +2220,7 @@ RED.nodes = (function() {
|
||||
x:parseFloat(n.x || 0),
|
||||
y:parseFloat(n.y || 0),
|
||||
z:n.z,
|
||||
type:0,
|
||||
type: n.type,
|
||||
info: n.info,
|
||||
changed:false,
|
||||
_config:{}
|
||||
@ -2231,7 +2281,6 @@ RED.nodes = (function() {
|
||||
}
|
||||
}
|
||||
}
|
||||
node.type = n.type;
|
||||
node._def = def;
|
||||
if (node.type === "group") {
|
||||
node._def = RED.group.def;
|
||||
@ -2261,6 +2310,15 @@ RED.nodes = (function() {
|
||||
outputs: n.outputs|| (n.wires && n.wires.length) || 0,
|
||||
set: registry.getNodeSet("node-red/unknown")
|
||||
}
|
||||
var orig = {};
|
||||
for (var p in n) {
|
||||
if (n.hasOwnProperty(p) && p!="x" && p!="y" && p!="z" && p!="id" && p!="wires") {
|
||||
orig[p] = n[p];
|
||||
}
|
||||
}
|
||||
node._orig = orig;
|
||||
node.name = n.type;
|
||||
node.type = "unknown";
|
||||
} else {
|
||||
if (subflow_denylist[parentId] || createNewIds || options.importMap[n.id] === "copy") {
|
||||
parentId = subflow.id;
|
||||
@ -2321,29 +2379,31 @@ RED.nodes = (function() {
|
||||
node.type = "unknown";
|
||||
}
|
||||
if (node._def.category != "config") {
|
||||
if (n.hasOwnProperty('inputs')) {
|
||||
node.inputs = n.inputs;
|
||||
if (n.hasOwnProperty('inputs') && node._def.defaults.hasOwnProperty("inputs")) {
|
||||
node.inputs = parseInt(n.inputs, 10);
|
||||
node._config.inputs = JSON.stringify(n.inputs);
|
||||
} else {
|
||||
node.inputs = node._def.inputs;
|
||||
}
|
||||
if (n.hasOwnProperty('outputs')) {
|
||||
node.outputs = n.outputs;
|
||||
if (n.hasOwnProperty('outputs') && node._def.defaults.hasOwnProperty("outputs")) {
|
||||
node.outputs = parseInt(n.outputs, 10);
|
||||
node._config.outputs = JSON.stringify(n.outputs);
|
||||
} else {
|
||||
node.outputs = node._def.outputs;
|
||||
}
|
||||
if (node.hasOwnProperty('wires') && node.wires.length > node.outputs) {
|
||||
if (!node._def.defaults.hasOwnProperty("outputs") || !isNaN(parseInt(n.outputs))) {
|
||||
// If 'wires' is longer than outputs, clip wires
|
||||
console.log("Warning: node.wires longer than node.outputs - trimming wires:",node.id," wires:",node.wires.length," outputs:",node.outputs);
|
||||
node.wires = node.wires.slice(0,node.outputs);
|
||||
} else {
|
||||
// The node declares outputs in its defaults, but has not got a valid value
|
||||
// Defer to the length of the wires array
|
||||
|
||||
// The node declares outputs in its defaults, but has not got a valid value
|
||||
// Defer to the length of the wires array
|
||||
if (node.hasOwnProperty('wires')) {
|
||||
if (isNaN(node.outputs)) {
|
||||
node.outputs = node.wires.length;
|
||||
} else if (node.wires.length > node.outputs) {
|
||||
// If 'wires' is longer than outputs, clip wires
|
||||
console.log("Warning: node.wires longer than node.outputs - trimming wires:", node.id, " wires:", node.wires.length, " outputs:", node.outputs);
|
||||
node.wires = node.wires.slice(0, node.outputs);
|
||||
}
|
||||
}
|
||||
|
||||
for (d in node._def.defaults) {
|
||||
if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') {
|
||||
node[d] = n[d];
|
||||
@ -2440,11 +2500,6 @@ RED.nodes = (function() {
|
||||
nodeList = nodeList.map(function(id) {
|
||||
var node = node_map[id];
|
||||
if (node) {
|
||||
if (node._def.category === 'config') {
|
||||
if (node.users.indexOf(n) === -1) {
|
||||
node.users.push(n);
|
||||
}
|
||||
}
|
||||
return node.id;
|
||||
}
|
||||
return id;
|
||||
@ -2458,9 +2513,11 @@ RED.nodes = (function() {
|
||||
n = new_subflows[i];
|
||||
n.in.forEach(function(input) {
|
||||
input.wires.forEach(function(wire) {
|
||||
var link = {source:input, sourcePort:0, target:node_map[wire.id]};
|
||||
addLink(link);
|
||||
new_links.push(link);
|
||||
if (node_map.hasOwnProperty(wire.id)) {
|
||||
var link = {source:input, sourcePort:0, target:node_map[wire.id]};
|
||||
addLink(link);
|
||||
new_links.push(link);
|
||||
}
|
||||
});
|
||||
delete input.wires;
|
||||
});
|
||||
@ -2469,11 +2526,13 @@ RED.nodes = (function() {
|
||||
var link;
|
||||
if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) {
|
||||
link = {source:n.in[wire.port], sourcePort:wire.port,target:output};
|
||||
} else {
|
||||
} else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) {
|
||||
link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:output};
|
||||
}
|
||||
addLink(link);
|
||||
new_links.push(link);
|
||||
if (link) {
|
||||
addLink(link);
|
||||
new_links.push(link);
|
||||
}
|
||||
});
|
||||
delete output.wires;
|
||||
});
|
||||
@ -2482,11 +2541,13 @@ RED.nodes = (function() {
|
||||
var link;
|
||||
if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) {
|
||||
link = {source:n.in[wire.port], sourcePort:wire.port,target:n.status};
|
||||
} else {
|
||||
} else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) {
|
||||
link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:n.status};
|
||||
}
|
||||
addLink(link);
|
||||
new_links.push(link);
|
||||
if (link) {
|
||||
addLink(link);
|
||||
new_links.push(link);
|
||||
}
|
||||
});
|
||||
delete n.status.wires;
|
||||
}
|
||||
@ -2665,25 +2726,79 @@ RED.nodes = (function() {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Update any config nodes referenced by the provided node to ensure their 'users' list is correct
|
||||
function updateConfigNodeUsers(n) {
|
||||
for (var d in n._def.defaults) {
|
||||
if (n._def.defaults.hasOwnProperty(d)) {
|
||||
var property = n._def.defaults[d];
|
||||
/**
|
||||
* Update any config nodes referenced by the provided node to ensure
|
||||
* their 'users' list is correct.
|
||||
*
|
||||
* @param {object} node The node in which to check if it contains references
|
||||
* @param {object} options Options to apply.
|
||||
* @param {"add" | "remove"} [options.action] Add or remove the node from
|
||||
* the Config Node users list. Default `add`.
|
||||
* @param {boolean} [options.emitEvent] Emit the `nodes:changes` event.
|
||||
* Default true.
|
||||
*/
|
||||
function updateConfigNodeUsers(node, options) {
|
||||
const defaultOptions = { action: "add", emitEvent: true };
|
||||
options = Object.assign({}, defaultOptions, options);
|
||||
|
||||
for (var d in node._def.defaults) {
|
||||
if (node._def.defaults.hasOwnProperty(d)) {
|
||||
var property = node._def.defaults[d];
|
||||
if (property.type) {
|
||||
var type = registry.getNodeType(property.type);
|
||||
// Need to ensure the type is a config node to not treat links nodes
|
||||
if (type && type.category == "config") {
|
||||
var configNode = configNodes[n[d]];
|
||||
var configNode = configNodes[node[d]];
|
||||
if (configNode) {
|
||||
if (configNode.users.indexOf(n) === -1) {
|
||||
configNode.users.push(n);
|
||||
RED.events.emit('nodes:change',configNode)
|
||||
if (options.action === "add") {
|
||||
if (configNode.users.indexOf(node) === -1) {
|
||||
configNode.users.push(node);
|
||||
if (options.emitEvent) {
|
||||
RED.events.emit('nodes:change', configNode);
|
||||
}
|
||||
}
|
||||
} else if (options.action === "remove") {
|
||||
if (configNode.users.indexOf(node) !== -1) {
|
||||
const users = configNode.users;
|
||||
users.splice(users.indexOf(node), 1);
|
||||
if (options.emitEvent) {
|
||||
RED.events.emit('nodes:change', configNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subflows can have config node env
|
||||
if (node.type.indexOf("subflow:") === 0) {
|
||||
node.env?.forEach((prop) => {
|
||||
if (prop.type === "conf-type" && prop.value) {
|
||||
// Add the node to the config node users
|
||||
const configNode = getNode(prop.value);
|
||||
if (configNode) {
|
||||
if (options.action === "add") {
|
||||
if (configNode.users.indexOf(node) === -1) {
|
||||
configNode.users.push(node);
|
||||
if (options.emitEvent) {
|
||||
RED.events.emit('nodes:change', configNode);
|
||||
}
|
||||
}
|
||||
} else if (options.action === "remove") {
|
||||
if (configNode.users.indexOf(node) !== -1) {
|
||||
const users = configNode.users;
|
||||
users.splice(users.indexOf(node), 1);
|
||||
if (options.emitEvent) {
|
||||
RED.events.emit('nodes:change', configNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function flowVersion(version) {
|
||||
|
@ -334,6 +334,30 @@ RED.clipboard = (function() {
|
||||
},100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the provided string looks like valid flow json
|
||||
* @param {string} flowString the string to validate
|
||||
* @returns If valid, returns the node array
|
||||
*/
|
||||
function validateFlowString(flowString) {
|
||||
const res = JSON.parse(flowString)
|
||||
if (!Array.isArray(res)) {
|
||||
throw new Error(RED._("clipboard.import.errors.notArray"));
|
||||
}
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
if (typeof res[i] !== "object") {
|
||||
throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i}));
|
||||
}
|
||||
if (!Object.hasOwn(res[i], 'id')) {
|
||||
throw new Error(RED._("clipboard.import.errors.missingId",{index:i}));
|
||||
}
|
||||
if (!Object.hasOwn(res[i], 'type')) {
|
||||
throw new Error(RED._("clipboard.import.errors.missingType",{index:i}));
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
var validateImportTimeout;
|
||||
function validateImport() {
|
||||
if (activeTab === "red-ui-clipboard-dialog-import-tab-clipboard") {
|
||||
@ -351,21 +375,7 @@ RED.clipboard = (function() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!/^\[[\s\S]*\]$/m.test(v)) {
|
||||
throw new Error(RED._("clipboard.import.errors.notArray"));
|
||||
}
|
||||
var res = JSON.parse(v);
|
||||
for (var i=0;i<res.length;i++) {
|
||||
if (typeof res[i] !== "object") {
|
||||
throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i}));
|
||||
}
|
||||
if (!res[i].hasOwnProperty('id')) {
|
||||
throw new Error(RED._("clipboard.import.errors.missingId",{index:i}));
|
||||
}
|
||||
if (!res[i].hasOwnProperty('type')) {
|
||||
throw new Error(RED._("clipboard.import.errors.missingType",{index:i}));
|
||||
}
|
||||
}
|
||||
validateFlowString(v)
|
||||
currentPopoverError = null;
|
||||
popover.close(true);
|
||||
importInput.removeClass("input-error");
|
||||
@ -998,16 +1008,16 @@ RED.clipboard = (function() {
|
||||
}
|
||||
|
||||
function importNodes(nodesStr,addFlow) {
|
||||
var newNodes = nodesStr;
|
||||
let newNodes = nodesStr;
|
||||
if (typeof nodesStr === 'string') {
|
||||
try {
|
||||
nodesStr = nodesStr.trim();
|
||||
if (nodesStr.length === 0) {
|
||||
return;
|
||||
}
|
||||
newNodes = JSON.parse(nodesStr);
|
||||
newNodes = validateFlowString(nodesStr)
|
||||
} catch(err) {
|
||||
var e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
|
||||
const e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
|
||||
e.code = "NODE_RED";
|
||||
throw e;
|
||||
}
|
||||
@ -1342,6 +1352,7 @@ RED.clipboard = (function() {
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
console.warn('Import failed: ', err)
|
||||
// Ensure any errors throw above doesn't stop the drop target from
|
||||
// being hidden.
|
||||
}
|
||||
|
@ -54,15 +54,15 @@ RED.contextMenu = (function () {
|
||||
}
|
||||
}
|
||||
|
||||
const scale = RED.view.scale()
|
||||
const offset = $("#red-ui-workspace-chart").offset()
|
||||
|
||||
let addX = options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()
|
||||
let addY = options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()
|
||||
let addX = (options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()) / scale
|
||||
let addY = (options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()) / scale
|
||||
|
||||
if (RED.view.snapGrid) {
|
||||
const gridSize = RED.view.gridSize()
|
||||
addX = gridSize * Math.floor(addX / gridSize)
|
||||
addY = gridSize * Math.floor(addY / gridSize)
|
||||
addX = gridSize * Math.round(addX / gridSize)
|
||||
addY = gridSize * Math.round(addY / gridSize)
|
||||
}
|
||||
|
||||
if (RED.settings.theme("menu.menu-item-action-list", true)) {
|
||||
@ -87,7 +87,9 @@ RED.contextMenu = (function () {
|
||||
},
|
||||
(hasLinks) ? { // has least 1 wire selected
|
||||
label: RED._("contextMenu.junction"),
|
||||
onselect: 'core:split-wires-with-junctions',
|
||||
onselect: function () {
|
||||
RED.actions.invoke('core:split-wires-with-junctions', { x: addX, y: addY })
|
||||
},
|
||||
disabled: !canEdit || !hasLinks
|
||||
} : {
|
||||
label: RED._("contextMenu.junction"),
|
||||
|
@ -808,6 +808,20 @@ RED.editor = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
const oldCreds = {};
|
||||
if (editing_node._def.credentials) {
|
||||
for (const prop in editing_node._def.credentials) {
|
||||
if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) {
|
||||
if (editing_node._def.credentials[prop].type === 'password') {
|
||||
oldCreds['has_' + prop] = editing_node.credentials['has_' + prop];
|
||||
}
|
||||
if (prop in editing_node.credentials) {
|
||||
oldCreds[prop] = editing_node.credentials[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const rc = editing_node._def.oneditsave.call(editing_node);
|
||||
if (rc === true) {
|
||||
@ -839,6 +853,25 @@ RED.editor = (function() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (editing_node._def.credentials) {
|
||||
for (const prop in editing_node._def.credentials) {
|
||||
if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) {
|
||||
if (oldCreds[prop] !== editing_node.credentials[prop]) {
|
||||
if (editing_node.credentials[prop] === '__PWRD__') {
|
||||
// The password may not exist in oldCreds
|
||||
// The value '__PWRD__' means the password exists,
|
||||
// so ignore this change
|
||||
continue;
|
||||
}
|
||||
editState.changes.credentials = editState.changes.credentials || {};
|
||||
editState.changes.credentials['has_' + prop] = oldCreds['has_' + prop];
|
||||
editState.changes.credentials[prop] = oldCreds[prop];
|
||||
editState.changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1481,134 +1514,181 @@ RED.editor = (function() {
|
||||
},
|
||||
{
|
||||
id: "node-config-dialog-ok",
|
||||
text: adding?RED._("editor.configAdd"):RED._("editor.configUpdate"),
|
||||
text: adding ? RED._("editor.configAdd") : RED._("editor.configUpdate"),
|
||||
class: "primary",
|
||||
click: function() {
|
||||
var editState = {
|
||||
// TODO: Already defined
|
||||
const configProperty = name;
|
||||
const configType = type;
|
||||
const configTypeDef = RED.nodes.getType(configType);
|
||||
|
||||
const wasChanged = editing_config_node.changed;
|
||||
const editState = {
|
||||
changes: {},
|
||||
changed: false,
|
||||
outputMap: null
|
||||
};
|
||||
var configProperty = name;
|
||||
var configId = editing_config_node.id;
|
||||
var configType = type;
|
||||
var configAdding = adding;
|
||||
var configTypeDef = RED.nodes.getType(configType);
|
||||
var d;
|
||||
var input;
|
||||
|
||||
// Call `oneditsave` and search for changes
|
||||
handleEditSave(editing_config_node, editState);
|
||||
|
||||
if (configTypeDef.oneditsave) {
|
||||
try {
|
||||
configTypeDef.oneditsave.call(editing_config_node);
|
||||
} catch(err) {
|
||||
console.warn("oneditsave",editing_config_node.id,editing_config_node.type,err.toString());
|
||||
}
|
||||
}
|
||||
|
||||
for (d in configTypeDef.defaults) {
|
||||
if (configTypeDef.defaults.hasOwnProperty(d)) {
|
||||
var newValue;
|
||||
input = $("#node-config-input-"+d);
|
||||
if (input.attr('type') === "checkbox") {
|
||||
newValue = input.prop('checked');
|
||||
} else if ("format" in configTypeDef.defaults[d] && configTypeDef.defaults[d].format !== "" && input[0].nodeName === "DIV") {
|
||||
newValue = input.text();
|
||||
} else {
|
||||
newValue = input.val();
|
||||
}
|
||||
if (newValue != null && newValue !== editing_config_node[d]) {
|
||||
if (editing_config_node._def.defaults[d].type) {
|
||||
if (newValue == "_ADD_") {
|
||||
newValue = "";
|
||||
}
|
||||
// Change to a related config node
|
||||
var configNode = RED.nodes.node(editing_config_node[d]);
|
||||
if (configNode) {
|
||||
var users = configNode.users;
|
||||
users.splice(users.indexOf(editing_config_node),1);
|
||||
RED.events.emit("nodes:change",configNode);
|
||||
}
|
||||
configNode = RED.nodes.node(newValue);
|
||||
if (configNode) {
|
||||
configNode.users.push(editing_config_node);
|
||||
RED.events.emit("nodes:change",configNode);
|
||||
}
|
||||
}
|
||||
editing_config_node[d] = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activeEditPanes.forEach(function(pane) {
|
||||
// Search for changes in the edit box (panes)
|
||||
activeEditPanes.forEach(function (pane) {
|
||||
if (pane.apply) {
|
||||
pane.apply.call(pane, editState);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
editing_config_node.label = configTypeDef.label;
|
||||
|
||||
var scope = $("#red-ui-editor-config-scope").val();
|
||||
editing_config_node.z = scope;
|
||||
// TODO: Why?
|
||||
editing_config_node.label = configTypeDef.label
|
||||
|
||||
// Check if disabled has changed
|
||||
if ($("#node-config-input-node-disabled").prop('checked')) {
|
||||
if (editing_config_node.d !== true) {
|
||||
editState.changes.d = editing_config_node.d;
|
||||
editState.changed = true;
|
||||
editing_config_node.d = true;
|
||||
}
|
||||
} else {
|
||||
if (editing_config_node.d === true) {
|
||||
editState.changes.d = editing_config_node.d;
|
||||
editState.changed = true;
|
||||
delete editing_config_node.d;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: must be undefined if no scope used
|
||||
const scope = $("#red-ui-editor-config-scope").val() || undefined;
|
||||
|
||||
// Check if the scope has changed
|
||||
if (editing_config_node.z !== scope) {
|
||||
editState.changes.z = editing_config_node.z;
|
||||
editState.changed = true;
|
||||
editing_config_node.z = scope;
|
||||
}
|
||||
|
||||
// Search for nodes that use this config node that are no longer
|
||||
// in scope, so must be removed
|
||||
const historyEvents = [];
|
||||
if (scope) {
|
||||
// Search for nodes that use this one that are no longer
|
||||
// in scope, so must be removed
|
||||
editing_config_node.users = editing_config_node.users.filter(function(n) {
|
||||
var keep = true;
|
||||
for (var d in n._def.defaults) {
|
||||
if (n._def.defaults.hasOwnProperty(d)) {
|
||||
if (n._def.defaults[d].type === editing_config_node.type &&
|
||||
n[d] === editing_config_node.id &&
|
||||
n.z !== scope) {
|
||||
keep = false;
|
||||
// Remove the reference to this node
|
||||
// and revalidate
|
||||
n[d] = null;
|
||||
n.dirty = true;
|
||||
n.changed = true;
|
||||
validateNode(n);
|
||||
const newUsers = editing_config_node.users.filter(function (node) {
|
||||
let keepNode = false;
|
||||
let nodeModified = null;
|
||||
|
||||
for (const d in node._def.defaults) {
|
||||
if (node._def.defaults.hasOwnProperty(d)) {
|
||||
if (node._def.defaults[d].type === editing_config_node.type) {
|
||||
if (node[d] === editing_config_node.id) {
|
||||
if (node.z === editing_config_node.z) {
|
||||
// The node is kept only if at least one property uses
|
||||
// this config node in the correct scope.
|
||||
keepNode = true;
|
||||
} else {
|
||||
if (!nodeModified) {
|
||||
nodeModified = {
|
||||
t: "edit",
|
||||
node: node,
|
||||
changes: { [d]: node[d] },
|
||||
changed: node.changed,
|
||||
dirty: node.dirty
|
||||
};
|
||||
} else {
|
||||
nodeModified.changes[d] = node[d];
|
||||
}
|
||||
|
||||
// Remove the reference to the config node
|
||||
node[d] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return keep;
|
||||
});
|
||||
}
|
||||
|
||||
if (configAdding) {
|
||||
RED.nodes.add(editing_config_node);
|
||||
}
|
||||
|
||||
validateNode(editing_config_node);
|
||||
var validatedNodes = {};
|
||||
validatedNodes[editing_config_node.id] = true;
|
||||
|
||||
var userStack = editing_config_node.users.slice();
|
||||
while(userStack.length > 0) {
|
||||
var user = userStack.pop();
|
||||
if (!validatedNodes[user.id]) {
|
||||
validatedNodes[user.id] = true;
|
||||
if (user.users) {
|
||||
userStack = userStack.concat(user.users);
|
||||
// Add the node modified to the history
|
||||
if (nodeModified) {
|
||||
historyEvents.push(nodeModified);
|
||||
}
|
||||
validateNode(user);
|
||||
|
||||
// Mark as changed and revalidate this node
|
||||
if (!keepNode) {
|
||||
node.changed = true;
|
||||
node.dirty = true;
|
||||
validateNode(node);
|
||||
RED.events.emit("nodes:change", node);
|
||||
}
|
||||
|
||||
return keepNode;
|
||||
});
|
||||
|
||||
// Check if users are changed
|
||||
if (editing_config_node.users.length !== newUsers.length) {
|
||||
editState.changes.users = editing_config_node.users;
|
||||
editState.changed = true;
|
||||
editing_config_node.users = newUsers;
|
||||
}
|
||||
}
|
||||
RED.nodes.dirty(true);
|
||||
RED.view.redraw(true);
|
||||
if (!configAdding) {
|
||||
RED.events.emit("editor:save",editing_config_node);
|
||||
RED.events.emit("nodes:change",editing_config_node);
|
||||
|
||||
if (editState.changed) {
|
||||
// Set the congig node as changed
|
||||
editing_config_node.changed = true;
|
||||
}
|
||||
|
||||
// Now, validate the config node
|
||||
validateNode(editing_config_node);
|
||||
|
||||
// And validate nodes using this config node too
|
||||
const validatedNodes = new Set();
|
||||
const userStack = editing_config_node.users.slice();
|
||||
|
||||
validatedNodes.add(editing_config_node.id);
|
||||
while (userStack.length) {
|
||||
const node = userStack.pop();
|
||||
if (!validatedNodes.has(node.id)) {
|
||||
validatedNodes.add(node.id);
|
||||
if (node.users) {
|
||||
userStack.push(...node.users);
|
||||
}
|
||||
validateNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
let historyEvent = {
|
||||
t: "edit",
|
||||
node: editing_config_node,
|
||||
changes: editState.changes,
|
||||
changed: wasChanged,
|
||||
dirty: RED.nodes.dirty()
|
||||
};
|
||||
|
||||
if (historyEvents.length) {
|
||||
// Need a multi events
|
||||
historyEvent = {
|
||||
t: "multi",
|
||||
events: [historyEvent].concat(historyEvents),
|
||||
dirty: historyEvent.dirty
|
||||
};
|
||||
}
|
||||
|
||||
if (!adding) {
|
||||
// This event is triggered when the edit box is saved,
|
||||
// regardless of whether there are any modifications.
|
||||
RED.events.emit("editor:save", editing_config_node);
|
||||
}
|
||||
|
||||
if (editState.changed) {
|
||||
if (adding) {
|
||||
RED.history.push({ t: "add", nodes: [editing_config_node.id], dirty: RED.nodes.dirty() });
|
||||
// Add the new config node and trigger the `nodes:add` event
|
||||
RED.nodes.add(editing_config_node);
|
||||
} else {
|
||||
RED.history.push(historyEvent);
|
||||
RED.events.emit("nodes:change", editing_config_node);
|
||||
}
|
||||
|
||||
RED.nodes.dirty(true);
|
||||
RED.view.redraw(true);
|
||||
}
|
||||
|
||||
RED.tray.close(function() {
|
||||
var filter = null;
|
||||
// when editing a config via subflow edit panel, the `configProperty` will not
|
||||
|
@ -20,10 +20,31 @@
|
||||
apply: function(editState) {
|
||||
var old_env = node.env;
|
||||
var new_env = [];
|
||||
|
||||
if (/^subflow:/.test(node.type)) {
|
||||
// Get the list of environment variables from the node properties
|
||||
new_env = RED.subflow.exportSubflowInstanceEnv(node);
|
||||
}
|
||||
|
||||
if (old_env && old_env.length) {
|
||||
old_env.forEach(function (prop) {
|
||||
if (prop.type === "conf-type" && prop.value) {
|
||||
const stillInUse = new_env?.some((p) => p.type === "conf-type" && p.name === prop.name && p.value === prop.value);
|
||||
if (!stillInUse) {
|
||||
// Remove the node from the config node users
|
||||
// Only for empty value or modified
|
||||
const configNode = RED.nodes.node(prop.value);
|
||||
if (configNode) {
|
||||
if (configNode.users.indexOf(node) !== -1) {
|
||||
configNode.users.splice(configNode.users.indexOf(node), 1);
|
||||
RED.events.emit('nodes:change', configNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get the values from the Properties table tab
|
||||
var items = this.list.editableList('items');
|
||||
items.each(function (i,el) {
|
||||
@ -41,7 +62,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (new_env && new_env.length > 0) {
|
||||
new_env.forEach(function(prop) {
|
||||
if (prop.type === "cred") {
|
||||
@ -52,6 +72,15 @@
|
||||
editState.changed = true;
|
||||
}
|
||||
delete prop.value;
|
||||
} else if (prop.type === "conf-type" && prop.value) {
|
||||
const configNode = RED.nodes.node(prop.value);
|
||||
if (configNode) {
|
||||
if (configNode.users.indexOf(node) === -1) {
|
||||
// Add the node to the config node users
|
||||
configNode.users.push(node);
|
||||
RED.events.emit('nodes:change', configNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -44,6 +44,7 @@
|
||||
apply: function(editState) {
|
||||
var newValue;
|
||||
var d;
|
||||
// If the node is a subflow, the node's properties (exepts name) are saved by `envProperties`
|
||||
if (node._def.defaults) {
|
||||
for (d in node._def.defaults) {
|
||||
if (node._def.defaults.hasOwnProperty(d)) {
|
||||
@ -131,9 +132,16 @@
|
||||
}
|
||||
}
|
||||
if (node._def.credentials) {
|
||||
var credDefinition = node._def.credentials;
|
||||
var credsChanged = updateNodeCredentials(node,credDefinition,this.inputClass);
|
||||
editState.changed = editState.changed || credsChanged;
|
||||
const credDefinition = node._def.credentials;
|
||||
const credChanges = updateNodeCredentials(node, credDefinition, this.inputClass);
|
||||
|
||||
if (Object.keys(credChanges).length) {
|
||||
editState.changed = true;
|
||||
editState.changes.credentials = {
|
||||
...(editState.changes.credentials || {}),
|
||||
...credChanges
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -161,10 +169,11 @@
|
||||
* @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
|
||||
* @return {object} an object containing the modified properties
|
||||
*/
|
||||
function updateNodeCredentials(node, credDefinition, prefix) {
|
||||
var changed = false;
|
||||
const changes = {};
|
||||
|
||||
if (!node.credentials) {
|
||||
node.credentials = {_:{}};
|
||||
} else if (!node.credentials._) {
|
||||
@ -177,22 +186,33 @@
|
||||
if (input.length > 0) {
|
||||
var value = input.val();
|
||||
if (credDefinition[cred].type == 'password') {
|
||||
node.credentials['has_' + cred] = (value !== "");
|
||||
if (value == '__PWRD__') {
|
||||
continue;
|
||||
if (value === '__PWRD__') {
|
||||
// A cred value exists - no changes
|
||||
} else if (value === '' && node.credentials['has_' + cred] === false) {
|
||||
// Empty cred value exists - no changes
|
||||
} else if (value === node.credentials[cred]) {
|
||||
// A cred value exists locally in the editor - no changes
|
||||
// Like the user sets a value, saves the config,
|
||||
// reopens the config and save the config again
|
||||
} else {
|
||||
changes['has_' + cred] = node.credentials['has_' + cred];
|
||||
changes[cred] = node.credentials[cred];
|
||||
node.credentials[cred] = value;
|
||||
}
|
||||
changed = true;
|
||||
|
||||
}
|
||||
node.credentials[cred] = value;
|
||||
if (value != node.credentials._[cred]) {
|
||||
changed = true;
|
||||
node.credentials['has_' + cred] = (value !== '');
|
||||
} else {
|
||||
// Since these creds are loaded by the editor,
|
||||
// values can be directly compared
|
||||
if (value !== node.credentials[cred]) {
|
||||
changes[cred] = node.credentials[cred];
|
||||
node.credentials[cred] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
|
||||
})();
|
||||
|
@ -1362,7 +1362,7 @@ RED.subflow = (function() {
|
||||
item.value = ""+input.prop("checked");
|
||||
break;
|
||||
case "conf-types":
|
||||
item.value = input.val()
|
||||
item.value = input.val() === "_ADD_" ? "" : input.val();
|
||||
item.type = "conf-type"
|
||||
}
|
||||
if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) {
|
||||
|
@ -382,7 +382,7 @@ RED.typeSearch = (function() {
|
||||
var items = [];
|
||||
RED.nodes.registry.getNodeTypes().forEach(function(t) {
|
||||
var def = RED.nodes.getType(t);
|
||||
if (def.category !== 'config' && t !== 'unknown' && t !== 'tab') {
|
||||
if (def.set?.enabled !== false && def.category !== 'config' && t !== 'unknown' && t !== 'tab') {
|
||||
items.push({type:t,def: def, label:getTypeLabel(t,def)});
|
||||
}
|
||||
});
|
||||
|
@ -11,7 +11,7 @@ RED.view.annotations = (function() {
|
||||
}
|
||||
let badgeRDX = 0;
|
||||
let badgeLDX = 0;
|
||||
|
||||
const scale = RED.view.scale()
|
||||
for (let i=0,l=evt.el.__annotations__.length;i<l;i++) {
|
||||
const annotation = evt.el.__annotations__[i];
|
||||
if (annotations.hasOwnProperty(annotation.id)) {
|
||||
@ -42,15 +42,17 @@ RED.view.annotations = (function() {
|
||||
}
|
||||
if (isBadge) {
|
||||
if (showAnnotation) {
|
||||
const rect = annotation.element.getBoundingClientRect();
|
||||
// getBoundingClientRect is in real-world scale so needs to be adjusted according to
|
||||
// the current scale factor
|
||||
const rectWidth = annotation.element.getBoundingClientRect().width / scale;
|
||||
let annotationX
|
||||
if (!opts.align || opts.align === 'right') {
|
||||
annotationX = evt.node.w - 3 - badgeRDX - rect.width
|
||||
badgeRDX += rect.width + 4;
|
||||
annotationX = evt.node.w - 3 - badgeRDX - rectWidth
|
||||
badgeRDX += rectWidth + 4;
|
||||
|
||||
} else if (opts.align === 'left') {
|
||||
annotationX = 3 + badgeLDX
|
||||
badgeLDX += rect.width + 4;
|
||||
badgeLDX += rectWidth + 4;
|
||||
}
|
||||
annotation.element.setAttribute("transform", "translate("+annotationX+", -8)");
|
||||
}
|
||||
|
@ -1154,11 +1154,11 @@ RED.view.tools = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function addJunctionsToWires(wires) {
|
||||
function addJunctionsToWires(options = {}) {
|
||||
if (RED.workspaces.isLocked()) {
|
||||
return
|
||||
}
|
||||
let wiresToSplit = wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
|
||||
let wiresToSplit = options.wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
|
||||
if (!wiresToSplit) {
|
||||
return
|
||||
}
|
||||
@ -1206,21 +1206,26 @@ RED.view.tools = (function() {
|
||||
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 (addedJunctions.length === 0 && Object.hasOwn(options, 'x') && Object.hasOwn(options, 'y')) {
|
||||
junction.x = options.x
|
||||
junction.y = options.y
|
||||
} else {
|
||||
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));
|
||||
@ -1410,7 +1415,7 @@ RED.view.tools = (function() {
|
||||
RED.actions.add("core:wire-multiple-to-node", function() { wireMultipleToNode() })
|
||||
|
||||
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:split-wires-with-junctions", function (options) { addJunctionsToWires(options) });
|
||||
|
||||
RED.actions.add("core:generate-node-names", generateNodeNames )
|
||||
|
||||
|
@ -288,7 +288,7 @@ RED.view = (function() {
|
||||
}
|
||||
selectedLinks.clearUnselected()
|
||||
},
|
||||
length: () => groups.length,
|
||||
length: () => groups.size,
|
||||
forEach: (func) => { groups.forEach(func) },
|
||||
toArray: () => [...groups],
|
||||
clear: function () {
|
||||
@ -321,8 +321,8 @@ RED.view = (function() {
|
||||
evt.stopPropagation()
|
||||
RED.contextMenu.show({
|
||||
type: 'workspace',
|
||||
x:evt.clientX-5,
|
||||
y:evt.clientY-5
|
||||
x: evt.clientX,
|
||||
y: evt.clientY
|
||||
})
|
||||
return false
|
||||
})
|
||||
@ -2689,22 +2689,21 @@ RED.view = (function() {
|
||||
addToRemovedLinks(reconnectResult.removedLinks)
|
||||
}
|
||||
|
||||
var startDirty = RED.nodes.dirty();
|
||||
var startChanged = false;
|
||||
var selectedGroups = [];
|
||||
const startDirty = RED.nodes.dirty();
|
||||
let movingSelectedGroups = [];
|
||||
if (movingSet.length() > 0) {
|
||||
|
||||
for (var i=0;i<movingSet.length();i++) {
|
||||
node = movingSet.get(i).n;
|
||||
if (node.type === "group") {
|
||||
selectedGroups.push(node);
|
||||
movingSelectedGroups.push(node);
|
||||
}
|
||||
}
|
||||
// Make sure we have identified all groups about to be deleted
|
||||
for (i=0;i<selectedGroups.length;i++) {
|
||||
selectedGroups[i].nodes.forEach(function(n) {
|
||||
if (n.type === "group" && selectedGroups.indexOf(n) === -1) {
|
||||
selectedGroups.push(n);
|
||||
for (i=0;i<movingSelectedGroups.length;i++) {
|
||||
movingSelectedGroups[i].nodes.forEach(function(n) {
|
||||
if (n.type === "group" && movingSelectedGroups.indexOf(n) === -1) {
|
||||
movingSelectedGroups.push(n);
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -2721,7 +2720,7 @@ RED.view = (function() {
|
||||
addToRemovedLinks(removedEntities.links);
|
||||
if (node.g) {
|
||||
var group = RED.nodes.group(node.g);
|
||||
if (selectedGroups.indexOf(group) === -1) {
|
||||
if (movingSelectedGroups.indexOf(group) === -1) {
|
||||
// Don't use RED.group.removeFromGroup as that emits
|
||||
// a change event on the node - but we're deleting it
|
||||
var index = group.nodes.indexOf(node);
|
||||
@ -2735,7 +2734,7 @@ RED.view = (function() {
|
||||
removedLinks = removedLinks.concat(result.links);
|
||||
if (node.g) {
|
||||
var group = RED.nodes.group(node.g);
|
||||
if (selectedGroups.indexOf(group) === -1) {
|
||||
if (movingSelectedGroups.indexOf(group) === -1) {
|
||||
// Don't use RED.group.removeFromGroup as that emits
|
||||
// a change event on the node - but we're deleting it
|
||||
var index = group.nodes.indexOf(node);
|
||||
@ -2757,8 +2756,8 @@ RED.view = (function() {
|
||||
|
||||
// Groups must be removed in the right order - from inner-most
|
||||
// to outermost.
|
||||
for (i = selectedGroups.length-1; i>=0; i--) {
|
||||
var g = selectedGroups[i];
|
||||
for (i = movingSelectedGroups.length-1; i>=0; i--) {
|
||||
var g = movingSelectedGroups[i];
|
||||
removedGroups.push(g);
|
||||
RED.nodes.removeGroup(g);
|
||||
}
|
||||
@ -5175,8 +5174,8 @@ RED.view = (function() {
|
||||
var delta = Infinity;
|
||||
for (var i = 0; i < lineLength; i++) {
|
||||
var linePos = pathLine.getPointAtLength(i);
|
||||
var posDeltaX = Math.abs(linePos.x-d3.event.offsetX)
|
||||
var posDeltaY = Math.abs(linePos.y-d3.event.offsetY)
|
||||
var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor))
|
||||
var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor))
|
||||
var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY
|
||||
if (posDelta < delta) {
|
||||
pos = linePos
|
||||
|
@ -168,6 +168,37 @@ RED.user = (function() {
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
if (data.prompts) {
|
||||
if (data.loginMessage) {
|
||||
const sessionMessages = $("<div/>",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields");
|
||||
$('<div>').text(data.loginMessage).appendTo(sessionMessages);
|
||||
}
|
||||
|
||||
i = 0;
|
||||
for (;i<data.prompts.length;i++) {
|
||||
var field = data.prompts[i];
|
||||
var row = $("<div/>",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields");
|
||||
var loginButton = $('<a href="#" class="red-ui-button"></a>',{style: "padding: 10px"}).appendTo(row).on("click", function() {
|
||||
document.location = field.url;
|
||||
});
|
||||
if (field.image) {
|
||||
$("<img>",{src:field.image}).appendTo(loginButton);
|
||||
} else if (field.label) {
|
||||
var label = $('<span></span>').text(field.label);
|
||||
if (field.icon) {
|
||||
$('<i></i>',{class: "fa fa-2x "+field.icon, style:"vertical-align: middle"}).appendTo(loginButton);
|
||||
label.css({
|
||||
"verticalAlign":"middle",
|
||||
"marginLeft":"8px"
|
||||
});
|
||||
|
||||
}
|
||||
label.appendTo(loginButton);
|
||||
}
|
||||
loginButton.button();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (opts.cancelable) {
|
||||
$("#node-dialog-login-cancel").button().on("click", function( event ) {
|
||||
|
@ -253,7 +253,13 @@ module.exports = function(RED) {
|
||||
if (node.allowrate && m.hasOwnProperty("rate") && !isNaN(parseFloat(m.rate))) {
|
||||
node.rate = m.rate;
|
||||
}
|
||||
send(m);
|
||||
if (msg.hasOwnProperty("reset")) {
|
||||
if (msg.hasOwnProperty("flush")) {
|
||||
node.buffer.push({msg: m, send: send, done: done});
|
||||
}
|
||||
}
|
||||
else { send(m); }
|
||||
|
||||
node.reportDepth();
|
||||
node.intervalID = setInterval(sendMsgFromBuffer, node.rate);
|
||||
done();
|
||||
@ -285,42 +291,23 @@ module.exports = function(RED) {
|
||||
}
|
||||
}
|
||||
else if (!msg.hasOwnProperty("reset")) {
|
||||
if (maxKeptMsgsCount(node) > 0) {
|
||||
if (node.intervalID === -1) {
|
||||
node.send(msg);
|
||||
node.intervalID = setInterval(sendMsgFromBuffer, node.rate);
|
||||
} else {
|
||||
if (node.allowrate && msg.hasOwnProperty("rate") && !isNaN(parseFloat(msg.rate)) && node.rate !== msg.rate) {
|
||||
node.rate = msg.rate;
|
||||
clearInterval(node.intervalID);
|
||||
node.intervalID = setInterval(sendMsgFromBuffer, node.rate);
|
||||
}
|
||||
if (node.buffer.length < _maxKeptMsgsCount) {
|
||||
var m = RED.util.cloneMessage(msg);
|
||||
node.buffer.push({msg: m, send: send, done: done});
|
||||
} else {
|
||||
node.trace("dropped due to buffer overflow. msg._msgid = " + msg._msgid);
|
||||
node.droppedMsgs++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (node.allowrate && msg.hasOwnProperty("rate") && !isNaN(parseFloat(msg.rate))) {
|
||||
node.rate = msg.rate;
|
||||
}
|
||||
var timeSinceLast;
|
||||
if (node.lastSent) {
|
||||
timeSinceLast = process.hrtime(node.lastSent);
|
||||
}
|
||||
if (!node.lastSent) { // ensuring that we always send the first message
|
||||
node.lastSent = process.hrtime();
|
||||
send(msg);
|
||||
}
|
||||
else if ( ( (timeSinceLast[0] * SECONDS_TO_NANOS) + timeSinceLast[1] ) > (node.rate * MILLIS_TO_NANOS) ) {
|
||||
node.lastSent = process.hrtime();
|
||||
send(msg);
|
||||
} else if (node.outputs === 2) {
|
||||
send([null,msg])
|
||||
}
|
||||
if (node.allowrate && msg.hasOwnProperty("rate") && !isNaN(parseFloat(msg.rate))) {
|
||||
node.rate = msg.rate;
|
||||
}
|
||||
var timeSinceLast;
|
||||
if (node.lastSent) {
|
||||
timeSinceLast = process.hrtime(node.lastSent);
|
||||
}
|
||||
if (!node.lastSent) { // ensuring that we always send the first message
|
||||
node.lastSent = process.hrtime();
|
||||
send(msg);
|
||||
}
|
||||
else if ( ( (timeSinceLast[0] * SECONDS_TO_NANOS) + timeSinceLast[1] ) > (node.rate * MILLIS_TO_NANOS) ) {
|
||||
node.lastSent = process.hrtime();
|
||||
send(msg);
|
||||
}
|
||||
else if (node.outputs === 2) {
|
||||
send([null,msg])
|
||||
}
|
||||
done();
|
||||
}
|
||||
|
@ -367,20 +367,21 @@ module.exports = function(RED) {
|
||||
const sendHeadersAlways = node.hdrout === "all"
|
||||
const sendHeaders = !dontSendHeaders && (sendHeadersOnce || sendHeadersAlways)
|
||||
const quoteables = [node.sep, node.quo, "\n", "\r"]
|
||||
const templateQuoteables = [',', '"', "\n", "\r"]
|
||||
const templateQuoteables = [node.sep, node.quo, "\n", "\r"]
|
||||
const templateQuoteablesStrict = [',', '"', "\n", "\r"]
|
||||
let badTemplateWarnOnce = true
|
||||
|
||||
const columnStringToTemplateArray = function (col, sep) {
|
||||
// NOTE: enforce strict column template parsing in RFC4180 mode
|
||||
const parsed = csv.parse(col, { separator: sep, quote: node.quo, outputStyle: 'array', strict: true })
|
||||
if (parsed.headers.length > 0) { node.goodtmpl = true } else { node.goodtmpl = false }
|
||||
return parsed.headers.length ? parsed.headers : null
|
||||
if (parsed.data?.length === 1) { node.goodtmpl = true } else { node.goodtmpl = false }
|
||||
return node.goodtmpl ? parsed.data[0] : null
|
||||
}
|
||||
const templateArrayToColumnString = function (template, keepEmptyColumns) {
|
||||
// NOTE: enforce strict column template parsing in RFC4180 mode
|
||||
const parsed = csv.parse('', {headers: template, headersOnly:true, separator: ',', quote: node.quo, outputStyle: 'array', strict: true })
|
||||
const templateArrayToColumnString = function (template, keepEmptyColumns, separator = ',', quotables = templateQuoteablesStrict) {
|
||||
// NOTE: defaults to strict column template parsing (commas and double quotes)
|
||||
const parsed = csv.parse('', {headers: template, headersOnly:true, separator, quote: node.quo, outputStyle: 'array', strict: true })
|
||||
return keepEmptyColumns
|
||||
? parsed.headers.map(e => addQuotes(e || '', { separator: ',', quoteables: templateQuoteables}))
|
||||
? parsed.headers.map(e => addQuotes(e || '', { separator, quoteables: quotables })).join(separator)
|
||||
: parsed.header // exclues empty columns
|
||||
// TODO: resolve inconsistency between CSV->JSON and JSON->CSV
|
||||
// CSV->JSON: empty columns are excluded
|
||||
@ -447,7 +448,7 @@ module.exports = function(RED) {
|
||||
template = Object.keys(inputData[0]) || ['']
|
||||
}
|
||||
}
|
||||
stringBuilder.push(templateArrayToColumnString(template, true))
|
||||
stringBuilder.push(templateArrayToColumnString(template, true, node.sep, templateQuoteables)) // use user set separator for output data.
|
||||
if (sendHeadersOnce) { node.hdrSent = true }
|
||||
}
|
||||
|
||||
@ -483,6 +484,7 @@ module.exports = function(RED) {
|
||||
node.warn(RED._("csv.errors.obj_csv"))
|
||||
badTemplateWarnOnce = false
|
||||
}
|
||||
template = Object.keys(row) || ['']
|
||||
const rowData = []
|
||||
for (let header in inputData[0]) {
|
||||
if (row.hasOwnProperty(header)) {
|
||||
@ -518,7 +520,7 @@ module.exports = function(RED) {
|
||||
|
||||
// join lines, don't forget to add the last new line
|
||||
msg.payload = stringBuilder.join(node.ret) + node.ret
|
||||
msg.columns = templateArrayToColumnString(template)
|
||||
msg.columns = templateArrayToColumnString(template) // always strict commas + double quotes for
|
||||
if (msg.payload !== '') { send(msg) }
|
||||
done()
|
||||
}
|
||||
@ -615,16 +617,15 @@ module.exports = function(RED) {
|
||||
}
|
||||
if (msg.parts.index + 1 === msg.parts.count) {
|
||||
msg.payload = node.store
|
||||
msg.columns = csvParseResult.header
|
||||
// msg._mode = 'RFC4180 mode'
|
||||
// msg.columns = csvParseResult.header
|
||||
msg.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes for msg.columns
|
||||
delete msg.parts
|
||||
send(msg)
|
||||
node.store = []
|
||||
}
|
||||
}
|
||||
else {
|
||||
msg.columns = csvParseResult.header
|
||||
// msg._mode = 'RFC4180 mode'
|
||||
msg.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes for msg.columns
|
||||
msg.payload = data
|
||||
send(msg); // finally send the array
|
||||
}
|
||||
@ -633,7 +634,8 @@ module.exports = function(RED) {
|
||||
const len = data.length
|
||||
for (let row = 0; row < len; row++) {
|
||||
const newMessage = RED.util.cloneMessage(msg)
|
||||
newMessage.columns = csvParseResult.header
|
||||
// newMessage.columns = csvParseResult.header
|
||||
newMessage.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes for msg.columns
|
||||
newMessage.payload = data[row]
|
||||
if (!has_parts) {
|
||||
newMessage.parts = {
|
||||
|
@ -339,7 +339,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
else {
|
||||
msg.filename = filename;
|
||||
var lines = Buffer.from([]);
|
||||
const bufferArray = [];
|
||||
var spare = "";
|
||||
var count = 0;
|
||||
var type = "buffer";
|
||||
@ -397,7 +397,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
}
|
||||
else {
|
||||
lines = Buffer.concat([lines,chunk]);
|
||||
bufferArray.push(chunk);
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -413,10 +413,11 @@ module.exports = function(RED) {
|
||||
})
|
||||
.on('end', function() {
|
||||
if (node.chunk === false) {
|
||||
const buffer = Buffer.concat(bufferArray);
|
||||
if (node.format === "utf8") {
|
||||
msg.payload = decode(lines, node.encoding);
|
||||
msg.payload = decode(buffer, node.encoding);
|
||||
}
|
||||
else { msg.payload = lines; }
|
||||
else { msg.payload = buffer; }
|
||||
nodeSend(msg);
|
||||
}
|
||||
else if (node.format === "lines") {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/nodes",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -144,7 +144,7 @@ async function installModule(module,version,url) {
|
||||
if (url) {
|
||||
if (pkgurlRe.test(url) || localtgzRe.test(url)) {
|
||||
// Git remote url or Tarball url - check the valid package url
|
||||
installName = url;
|
||||
installName = localtgzRe.test(url) && slashRe.test(url) ? `"${url}"` : url;
|
||||
isRegistryPackage = false;
|
||||
} else {
|
||||
log.warn(log._("server.install.install-failed-url",{name:module,url:url}));
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/registry",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
"repository": {
|
||||
@ -16,7 +16,7 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@node-red/util": "4.0.5",
|
||||
"@node-red/util": "4.0.7",
|
||||
"clone": "2.1.2",
|
||||
"fs-extra": "11.2.0",
|
||||
"semver": "7.6.3",
|
||||
|
@ -113,6 +113,10 @@ async function evaluateEnvProperties(flow, env, credentials) {
|
||||
resolve()
|
||||
});
|
||||
}))
|
||||
} else if (type === "conf-type" && /^\${[^}]+}$/.test(value)) {
|
||||
// Get the config node from the parent subflow
|
||||
const name = value.substring(2, value.length - 1);
|
||||
value = flow.getSetting(name);
|
||||
} else {
|
||||
try {
|
||||
value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null);
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/runtime",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
"repository": {
|
||||
@ -16,11 +16,11 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@node-red/registry": "4.0.5",
|
||||
"@node-red/util": "4.0.5",
|
||||
"@node-red/registry": "4.0.7",
|
||||
"@node-red/util": "4.0.7",
|
||||
"async-mutex": "0.5.0",
|
||||
"clone": "2.1.2",
|
||||
"express": "4.21.1",
|
||||
"express": "4.21.2",
|
||||
"fs-extra": "11.2.0",
|
||||
"json-stringify-safe": "5.0.1",
|
||||
"rfdc": "^1.3.1"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/util",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
12
packages/node_modules/node-red/package.json
vendored
12
packages/node_modules/node-red/package.json
vendored
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "node-red",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"description": "Low-code programming for event-driven applications",
|
||||
"homepage": "https://nodered.org",
|
||||
"license": "Apache-2.0",
|
||||
@ -31,14 +31,14 @@
|
||||
"flow"
|
||||
],
|
||||
"dependencies": {
|
||||
"@node-red/editor-api": "4.0.5",
|
||||
"@node-red/runtime": "4.0.5",
|
||||
"@node-red/util": "4.0.5",
|
||||
"@node-red/nodes": "4.0.5",
|
||||
"@node-red/editor-api": "4.0.7",
|
||||
"@node-red/runtime": "4.0.7",
|
||||
"@node-red/util": "4.0.7",
|
||||
"@node-red/nodes": "4.0.7",
|
||||
"basic-auth": "2.0.1",
|
||||
"bcryptjs": "2.4.3",
|
||||
"cors": "2.8.5",
|
||||
"express": "4.21.1",
|
||||
"express": "4.21.2",
|
||||
"fs-extra": "11.2.0",
|
||||
"node-red-admin": "^4.0.1",
|
||||
"nopt": "5.0.0",
|
||||
|
@ -1009,6 +1009,29 @@ describe('delay Node', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('sending a msg with reset to empty queue doesnt send anything', function(done) {
|
||||
this.timeout(2000);
|
||||
var flow = [{"id":"delayNode1","type":"delay","name":"delayNode","pauseType":"rate","timeout":1,"timeoutUnits":"seconds","rate":2,"rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"wires":[["helperNode1"]]},
|
||||
{id:"helperNode1", type:"helper", wires:[]}];
|
||||
helper.load(delayNode, flow, function() {
|
||||
var delayNode1 = helper.getNode("delayNode1");
|
||||
var helperNode1 = helper.getNode("helperNode1");
|
||||
var t = Date.now();
|
||||
var c = 0;
|
||||
helperNode1.on("input", function(msg) {
|
||||
console.log("Shold not get here")
|
||||
done(e);
|
||||
});
|
||||
|
||||
setTimeout( function() {
|
||||
if (c === 0) { done(); }
|
||||
}, 250);
|
||||
|
||||
// send test messages
|
||||
delayNode1.receive({payload:1,topic:"foo",reset:true}); // send something with blank topic
|
||||
});
|
||||
});
|
||||
|
||||
/* Messaging API support */
|
||||
function mapiDoneTestHelper(done, pauseType, drop, msgAndTimings) {
|
||||
const completeNode = require("nr-test-utils").require("@node-red/nodes/core/common/24-complete.js");
|
||||
|
@ -2067,6 +2067,27 @@ describe('CSV node (RFC Mode)', function () {
|
||||
n2.on("input", function (msg) {
|
||||
try {
|
||||
msg.should.have.property('payload', '1\tfoo\t"ba""r"\tdi,ng\n');
|
||||
msg.should.have.property('columns', 'd,b,c,a'); // Strict RFC columns
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
const testJson = { d: 1, b: "foo", c: "ba\"r", a: "di,ng" };
|
||||
n1.emit("input", { payload: testJson });
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert a simple object back to a tsv with headers using a tab as a separator', function (done) {
|
||||
const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", sep: "\t", ret: '\n', hdrout: "all", wires: [["n2"]] }, // RFC-vs-Legacy difference - use line separator \n to satisfy original test
|
||||
{ id: "n2", type: "helper" }];
|
||||
helper.load(csvNode, flow, function () {
|
||||
const n1 = helper.getNode("n1");
|
||||
const n2 = helper.getNode("n2");
|
||||
n2.on("input", function (msg) {
|
||||
try {
|
||||
msg.should.have.property('payload', 'd\tb\tc\ta\n1\tfoo\t"ba""r"\tdi,ng\n');
|
||||
msg.should.have.property('columns', 'd,b,c,a'); // Strict RFC columns
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
@ -2086,6 +2107,7 @@ describe('CSV node (RFC Mode)', function () {
|
||||
n2.on("input", function (msg) {
|
||||
try {
|
||||
msg.should.have.property('payload', '4,foo,true,,0\n');
|
||||
msg.should.have.property('columns', 'a,b o,c p,e'); // Strict RFC columns
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
@ -2106,6 +2128,7 @@ describe('CSV node (RFC Mode)', function () {
|
||||
try {
|
||||
// 'payload', 'a"a,b\'b\nA1,B1\nA2,B2\n'); // Legacy
|
||||
msg.should.have.property('payload', '"a""a",b\'b\nA1,B1\nA2,B2\n'); // RFC-vs-Legacy difference - RFC4180 Section 2.6, 2.7 quote handling
|
||||
msg.should.have.property('columns', '"a""a",b\'b'); // RCF compliant column names
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
@ -2171,6 +2194,7 @@ describe('CSV node (RFC Mode)', function () {
|
||||
n2.on("input", function (msg) {
|
||||
try {
|
||||
msg.should.have.property('payload', '1,3,2,4\n4,2,3,1\n');
|
||||
msg.should.have.property('columns', 'd,b,c,a'); // Strict RFC columns
|
||||
done();
|
||||
}
|
||||
catch (e) { done(e); }
|
||||
@ -2189,6 +2213,7 @@ describe('CSV node (RFC Mode)', function () {
|
||||
n2.on("input", function (msg) {
|
||||
try {
|
||||
msg.should.have.property('payload', 'd,b,c,a\n1,3,2,4\n4,"f\ng",3,1\n');
|
||||
msg.should.have.property('columns', 'd,b,c,a'); // Strict RFC columns
|
||||
done();
|
||||
}
|
||||
catch (e) { done(e); }
|
||||
@ -2208,6 +2233,7 @@ describe('CSV node (RFC Mode)', function () {
|
||||
try {
|
||||
// 'payload', ',0,1,foo,"ba""r","di,ng","fa\nba"\n');
|
||||
msg.should.have.property('payload', ',0,1,foo\n'); // RFC-vs-Legacy difference - respect that user has specified a template with 4 columns
|
||||
msg.should.have.property('columns', 'a,b,c,d');
|
||||
done();
|
||||
}
|
||||
catch (e) { done(e); }
|
||||
@ -2327,6 +2353,7 @@ describe('CSV node (RFC Mode)', function () {
|
||||
n2.on("input", function (msg) {
|
||||
try {
|
||||
msg.should.have.property('payload', '{},"text,with,commas","This ""is"" a banana","{""sub"":""object""}"\n');
|
||||
msg.should.have.property('columns', 'a,b,c,d');
|
||||
done();
|
||||
}
|
||||
catch (e) { done(e); }
|
||||
|
@ -258,6 +258,29 @@ describe('nodes/registry/installer', function() {
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it("succeeds when file path is valid node-red module", function(done) {
|
||||
var nodeInfo = {nodes:{module:"foo",types:["a"]}};
|
||||
|
||||
var res = {
|
||||
code: 0,
|
||||
stdout:"",
|
||||
stderr:""
|
||||
}
|
||||
var p = Promise.resolve(res);
|
||||
p.catch((err)=>{});
|
||||
execResponse = p;
|
||||
|
||||
var addModule = sinon.stub(registry,"addModule").callsFake(function(md) {
|
||||
return Promise.resolve(nodeInfo);
|
||||
});
|
||||
|
||||
installer.installModule("foo",null,"/example path/foo-0.1.1.tgz").then(function(info) {
|
||||
exec.run.lastCall.args[1].should.eql([ 'install', '--no-audit', '--no-update-notifier', '--no-fund', '--save', '--save-prefix=~', '--omit=dev', '--engine-strict', '"/example path/foo-0.1.1.tgz"' ]);
|
||||
info.should.eql(nodeInfo);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it("triggers preInstall and postInstall hooks", function(done) {
|
||||
let receivedPreEvent,receivedPostEvent;
|
||||
hooks.add("preInstall", function(event) { event.args = ["a"]; receivedPreEvent = event; })
|
||||
|
Loading…
x
Reference in New Issue
Block a user