Compare commits

..

No commits in common. "master" and "4.0.5" have entirely different histories.

59 changed files with 466 additions and 1330 deletions

1
.gitignore vendored
View File

@ -28,4 +28,3 @@ docs
.nyc_output .nyc_output
sync.ffs_db sync.ffs_db
package-lock.json package-lock.json
.editorconfig

4
.nodemonignore Normal file
View File

@ -0,0 +1,4 @@
/Gruntfile.js
/.git/*
*.backup
/public/*

View File

@ -1,76 +1,3 @@
#### 4.0.9: Maintenance Release
Editor
- Add details for the dynamic subscription to match the English docs (#5050) @aikitori
- Fix tooltip snapping based on `typedInput` type (#5051) @GogoVega
- Prevent symbol usage warning in monaco (#5049) @Steve-Mcl
- Show subflow flow context under node section of sidebar (#5025) @knolleary
- feat: Add custom label for default deploy button in settings.editorTheme (#5030) @matiseni51
- Handle long auto-complete suggests (#5042) @knolleary
- Handle undefined username when generating user icon (#5043) @knolleary
- Handle dragging node into group and splicing link at same time (#5027) @knolleary
- Remember context sidebar tree state when refreshing (#5021) @knolleary
- Update sf instance env vars when removed from template (#5023) @knolleary
- Do not select group when triggering quick-add within it (#5022) @knolleary
- Fix library icon handling within library browser component (#5017) @knolleary
Runtime
- Allow env var access to context (#5016) @knolleary
- fix debug status reporting if null (#5018) @dceejay
- Fix grunt dev via better ndoemon ignore rules (#5015) @knolleary
- Fix typo in CHANGELOG (4.0.7-->4.0.8) (#5007) @natcl
Nodes
- Switch: Avoid exceeding call stack when draining message group in Switch (#5014) @knolleary
#### 4.0.8: Maintenance Release
Editor
- Fix config node sort order when importing (#5000) @knolleary
#### 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 #### 4.0.5: Maintenance Release
Editor Editor

View File

@ -1,16 +0,0 @@
{
"ignoreRoot": [
".git",
".nyc_output",
".sass-cache",
"bower-components",
"coverage"
],
"ignore": [
"/Gruntfile.js",
"/.git/*",
"*.backup",
"/public/*"
]
}

View File

@ -1,6 +1,6 @@
{ {
"name": "node-red", "name": "node-red",
"version": "4.0.9", "version": "4.0.5",
"description": "Low-code programming for event-driven applications", "description": "Low-code programming for event-driven applications",
"homepage": "https://nodered.org", "homepage": "https://nodered.org",
"license": "Apache-2.0", "license": "Apache-2.0",
@ -41,7 +41,7 @@
"cors": "2.8.5", "cors": "2.8.5",
"cronosjs": "1.7.1", "cronosjs": "1.7.1",
"denque": "2.1.0", "denque": "2.1.0",
"express": "4.21.2", "express": "4.21.1",
"express-session": "1.18.1", "express-session": "1.18.1",
"form-data": "4.0.0", "form-data": "4.0.0",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",

View File

@ -126,14 +126,6 @@ async function login(req,res) {
if (themeContext.login && themeContext.login.image) { if (themeContext.login && themeContext.login.image) {
response.image = 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); res.json(response);
} }

View File

@ -185,12 +185,13 @@ module.exports = {
} }
if (theme.deployButton) { if (theme.deployButton) {
themeSettings.deployButton = {};
if (theme.deployButton.label) {
themeSettings.deployButton.label = theme.deployButton.label;
}
if (theme.deployButton.type == "simple") { if (theme.deployButton.type == "simple") {
themeSettings.deployButton.type = theme.deployButton.type; themeSettings.deployButton = {
type: "simple"
}
if (theme.deployButton.label) {
themeSettings.deployButton.label = theme.deployButton.label;
}
if (theme.deployButton.icon) { if (theme.deployButton.icon) {
url = serveFile(themeApp,"/deploy/",theme.deployButton.icon); url = serveFile(themeApp,"/deploy/",theme.deployButton.icon);
if (url) { if (url) {
@ -205,26 +206,14 @@ module.exports = {
} }
if (theme.login) { if (theme.login) {
let themeContextLogin = {}
let hasLoginTheme = false
if (theme.login.image) { if (theme.login.image) {
url = serveFile(themeApp,"/login/",theme.login.image); url = serveFile(themeApp,"/login/",theme.login.image);
if (url) { if (url) {
themeContextLogin.image = url themeContext.login = {
hasLoginTheme = true image: url
}
} }
} }
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) { themeApp.get("/", async function(req,res) {
const themePluginList = await runtimeAPI.plugins.getPluginsByType({type:"node-red-theme"}); const themePluginList = await runtimeAPI.plugins.getPluginsByType({type:"node-red-theme"});

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/editor-api", "name": "@node-red/editor-api",
"version": "4.0.9", "version": "4.0.5",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,14 +16,14 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/util": "4.0.9", "@node-red/util": "4.0.5",
"@node-red/editor-client": "4.0.9", "@node-red/editor-client": "4.0.5",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"clone": "2.1.2", "clone": "2.1.2",
"cors": "2.8.5", "cors": "2.8.5",
"express-session": "1.18.1", "express-session": "1.18.1",
"express": "4.21.2", "express": "4.21.1",
"memorystore": "1.6.7", "memorystore": "1.6.7",
"mime": "3.0.0", "mime": "3.0.0",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",

View File

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

View File

@ -453,61 +453,10 @@ RED.history = (function() {
RED.events.emit("nodes:change",newConfigNode); 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];
} }
} }
ev.node.dirty = true; ev.node.dirty = true;
ev.node.changed = ev.changed; ev.node.changed = ev.changed;
@ -587,24 +536,6 @@ RED.history = (function() {
RED.editor.updateNodeProperties(ev.node,outputMap); RED.editor.updateNodeProperties(ev.node,outputMap);
RED.editor.validateNode(ev.node); 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);
}
}
}
if (ev.links) { if (ev.links) {
inverseEv.createdLinks = []; inverseEv.createdLinks = [];
for (i=0;i<ev.links.length;i++) { for (i=0;i<ev.links.length;i++) {

View File

@ -398,13 +398,14 @@ RED.multiplayer = (function () {
anonIconBody.setAttribute("d",`M ${radius/2} ${radius/2 + 5} h -2.5 c -2 1 -2 -5 0.5 -4.5 c 2 1 2 1 4 0 c 2.5 -0.5 2.5 5.5 0 4.5 z`); anonIconBody.setAttribute("d",`M ${radius/2} ${radius/2 + 5} h -2.5 c -2 1 -2 -5 0.5 -4.5 c 2 1 2 1 4 0 c 2.5 -0.5 2.5 5.5 0 4.5 z`);
group.appendChild(anonIconBody) group.appendChild(anonIconBody)
} else { } else {
const labelText = user.username ? user.username.substring(0,2) : user
const label = document.createElementNS("http://www.w3.org/2000/svg","text"); const label = document.createElementNS("http://www.w3.org/2000/svg","text");
if (user.username || user.email) { if (user.username) {
label.setAttribute("class","red-ui-multiplayer-annotation-label"); label.setAttribute("class","red-ui-multiplayer-annotation-label");
label.textContent = (user.username || user.email).substring(0,2) label.textContent = user.username.substring(0,2)
} else { } else {
label.setAttribute("class","red-ui-multiplayer-annotation-label red-ui-multiplayer-user-count") label.setAttribute("class","red-ui-multiplayer-annotation-label red-ui-multiplayer-user-count")
label.textContent = 'nr' label.textContent = user
} }
label.setAttribute("text-anchor", "middle") label.setAttribute("text-anchor", "middle")
label.setAttribute("x",radius/2); label.setAttribute("x",radius/2);

View File

@ -73,13 +73,7 @@ RED.nodes = (function() {
var exports = { var exports = {
setModulePendingUpdated: function(module,version) { setModulePendingUpdated: function(module,version) {
if (!!RED.plugins.getModule(module)) { moduleList[module].pending_version = version;
// 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}); RED.events.emit("registry:module-updated",{module:module,version:version});
}, },
getModule: function(module) { getModule: function(module) {
@ -707,15 +701,12 @@ RED.nodes = (function() {
} }
n["_"] = RED._; n["_"] = RED._;
} }
// Both node and config node can use a config node
updateConfigNodeUsers(newNode, { action: "add" });
if (n._def.category == "config") { if (n._def.category == "config") {
configNodes[n.id] = newNode; configNodes[n.id] = n;
} else { } else {
if (n.wires && (n.wires.length > n.outputs)) { n.outputs = n.wires.length; } if (n.wires && (n.wires.length > n.outputs)) { n.outputs = n.wires.length; }
n.dirty = true; n.dirty = true;
updateConfigNodeUsers(n);
if (n._def.category == "subflows" && typeof n.i === "undefined") { if (n._def.category == "subflows" && typeof n.i === "undefined") {
var nextId = 0; var nextId = 0;
RED.nodes.eachNode(function(node) { RED.nodes.eachNode(function(node) {
@ -777,11 +768,9 @@ RED.nodes = (function() {
var removedLinks = []; var removedLinks = [];
var removedNodes = []; var removedNodes = [];
var node; var node;
if (id in configNodes) { if (id in configNodes) {
node = configNodes[id]; node = configNodes[id];
delete configNodes[id]; delete configNodes[id];
updateConfigNodeUsers(node, { action: "remove" });
RED.events.emit('nodes:remove',node); RED.events.emit('nodes:remove',node);
RED.workspaces.refresh(); RED.workspaces.refresh();
} else if (allNodes.hasNode(id)) { } else if (allNodes.hasNode(id)) {
@ -790,9 +779,6 @@ RED.nodes = (function() {
delete nodeLinks[id]; delete nodeLinks[id];
removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); }); removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); });
removedLinks.forEach(removeLink); removedLinks.forEach(removeLink);
updateConfigNodeUsers(node, { action: "remove" });
// TODO: Legacy code for exclusive config node
var updatedConfigNode = false; var updatedConfigNode = false;
for (var d in node._def.defaults) { for (var d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d)) { if (node._def.defaults.hasOwnProperty(d)) {
@ -806,6 +792,10 @@ RED.nodes = (function() {
if (configNode._def.exclusive) { if (configNode._def.exclusive) {
removeNode(node[d]); removeNode(node[d]);
removedNodes.push(configNode); removedNodes.push(configNode);
} else {
var users = configNode.users;
users.splice(users.indexOf(node),1);
RED.events.emit('nodes:change',configNode)
} }
} }
} }
@ -1042,34 +1032,23 @@ RED.nodes = (function() {
return {nodes:removedNodes,links:removedLinks, groups: removedGroups, junctions: removedJunctions}; 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) { function addSubflow(sf, createNewIds) {
if (createNewIds) { if (createNewIds) {
// Update the Subflow name to highlight that this is a copy var subflowNames = Object.keys(subflows).map(function(sfid) {
const subflowNames = Object.keys(subflows).map(function (sfid) { return subflows[sfid].name;
return subflows[sfid].name || "";
})
subflowNames.sort()
let copyNumber = 1;
let subflowName = sf.name;
subflowNames.forEach(function(name) {
if (subflowName == name) {
subflowName = sf.name + " (" + copyNumber + ")";
copyNumber++;
}
}); });
subflowNames.sort();
var copyNumber = 1;
var subflowName = sf.name;
subflowNames.forEach(function(name) {
if (subflowName == name) {
copyNumber++;
subflowName = sf.name+" ("+copyNumber+")";
}
});
sf.name = subflowName; sf.name = subflowName;
} }
sf.instances = [];
subflows[sf.id] = sf; subflows[sf.id] = sf;
allNodes.addTab(sf.id); allNodes.addTab(sf.id);
linkTabMap[sf.id] = []; linkTabMap[sf.id] = [];
@ -1122,7 +1101,7 @@ RED.nodes = (function() {
module: "node-red" module: "node-red"
} }
}); });
sf.instances = [];
sf._def = RED.nodes.getType("subflow:"+sf.id); sf._def = RED.nodes.getType("subflow:"+sf.id);
RED.events.emit("subflows:add",sf); RED.events.emit("subflows:add",sf);
} }
@ -1764,8 +1743,7 @@ RED.nodes = (function() {
// Remove the old subflow definition - but leave the instances in place // Remove the old subflow definition - but leave the instances in place
var removalResult = RED.subflow.removeSubflow(n.id, true); var removalResult = RED.subflow.removeSubflow(n.id, true);
// Create the list of nodes for the new subflow def // Create the list of nodes for the new subflow def
// Need to sort the list in order to remove missing nodes var subflowNodes = [n].concat(zMap[n.id]);
var subflowNodes = [n].concat(zMap[n.id]).filter((s) => !!s);
// Import the new subflow - no clashes should occur as we've removed // Import the new subflow - no clashes should occur as we've removed
// the old version // the old version
var result = importNodes(subflowNodes); var result = importNodes(subflowNodes);
@ -1802,20 +1780,9 @@ RED.nodes = (function() {
// Replace config nodes // Replace config nodes
// //
configNodeIds.forEach(function(id) { configNodeIds.forEach(function(id) {
const configNode = getNode(id); removedNodes = removedNodes.concat(convertNode(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); removeNode(id);
importNodes([newConfigNodes[id]])
// Import the new one
importNodes([newConfigNodes[id]]);
// Re-attributes the user count
getNode(id).users = currentUserCount;
}); });
return { return {
@ -2056,8 +2023,6 @@ RED.nodes = (function() {
if (matchingSubflow) { if (matchingSubflow) {
subflow_denylist[n.id] = matchingSubflow; subflow_denylist[n.id] = matchingSubflow;
} else { } else {
const oldId = n.id;
subflow_map[n.id] = n; subflow_map[n.id] = n;
if (createNewIds || options.importMap[n.id] === "copy") { if (createNewIds || options.importMap[n.id] === "copy") {
nid = getID(); nid = getID();
@ -2085,7 +2050,7 @@ RED.nodes = (function() {
n.status.id = getID(); n.status.id = getID();
} }
new_subflows.push(n); new_subflows.push(n);
addSubflow(n,createNewIds || options.importMap[oldId] === "copy"); addSubflow(n,createNewIds || options.importMap[n.id] === "copy");
} }
} }
} }
@ -2099,8 +2064,6 @@ RED.nodes = (function() {
activeWorkspace = RED.workspaces.active(); activeWorkspace = RED.workspaces.active();
} }
const pendingConfigNodes = []
const pendingConfigNodeIds = new Set()
// Find all config nodes and add them // Find all config nodes and add them
for (i=0;i<newNodes.length;i++) { for (i=0;i<newNodes.length;i++) {
n = newNodes[i]; n = newNodes[i];
@ -2160,8 +2123,7 @@ RED.nodes = (function() {
type:n.type, type:n.type,
info: n.info, info: n.info,
users:[], users:[],
_config:{}, _config:{}
_configNodeReferences: new Set()
}; };
if (!n.z) { if (!n.z) {
delete configNode.z; delete configNode.z;
@ -2176,9 +2138,6 @@ RED.nodes = (function() {
if (def.defaults.hasOwnProperty(d)) { if (def.defaults.hasOwnProperty(d)) {
configNode[d] = n[d]; configNode[d] = n[d];
configNode._config[d] = JSON.stringify(n[d]); configNode._config[d] = JSON.stringify(n[d]);
if (def.defaults[d].type) {
configNode._configNodeReferences.add(n[d])
}
} }
} }
if (def.hasOwnProperty('credentials') && n.hasOwnProperty('credentials')) { if (def.hasOwnProperty('credentials') && n.hasOwnProperty('credentials')) {
@ -2195,55 +2154,11 @@ RED.nodes = (function() {
configNode.id = getID(); configNode.id = getID();
} }
node_map[n.id] = configNode; node_map[n.id] = configNode;
pendingConfigNodes.push(configNode); new_nodes.push(configNode);
pendingConfigNodeIds.add(configNode.id)
} }
} }
} }
// We need to sort new_nodes (which only contains config nodes at this point)
// to ensure they get added in the right order. If NodeA depends on NodeB, then
// NodeB must be added first.
// Limit us to 5 full iterations of the list - this should be more than
// enough to process the list as config->config node relationships are
// not very common
let iterationLimit = pendingConfigNodes.length * 5
const handledConfigNodes = new Set()
while (pendingConfigNodes.length > 0 && iterationLimit > 0) {
const node = pendingConfigNodes.shift()
let hasPending = false
// Loop through the nodes referenced by this node to see if anything
// is pending
node._configNodeReferences.forEach(id => {
if (pendingConfigNodeIds.has(id) && !handledConfigNodes.has(id)) {
// This reference is for a node we know is in this import, but
// it isn't added yet - flag as pending
hasPending = true
}
})
if (!hasPending) {
// This node has no pending config node references - safe to add
delete node._configNodeReferences
new_nodes.push(node)
handledConfigNodes.add(node.id)
} else {
// This node has pending config node references
// Put to the back of the queue
pendingConfigNodes.push(node)
}
iterationLimit--
}
if (pendingConfigNodes.length > 0) {
// We exceeded the iteration count. Could be due to reference loops
// between the config nodes. At this point, just add the remaining
// nodes as-is
pendingConfigNodes.forEach(node => {
delete node._configNodeReferences
new_nodes.push(node)
})
}
// Find regular flow nodes and subflow instances // Find regular flow nodes and subflow instances
for (i=0;i<newNodes.length;i++) { for (i=0;i<newNodes.length;i++) {
n = newNodes[i]; n = newNodes[i];
@ -2255,7 +2170,7 @@ RED.nodes = (function() {
x:parseFloat(n.x || 0), x:parseFloat(n.x || 0),
y:parseFloat(n.y || 0), y:parseFloat(n.y || 0),
z:n.z, z:n.z,
type: n.type, type:0,
info: n.info, info: n.info,
changed:false, changed:false,
_config:{} _config:{}
@ -2316,6 +2231,7 @@ RED.nodes = (function() {
} }
} }
} }
node.type = n.type;
node._def = def; node._def = def;
if (node.type === "group") { if (node.type === "group") {
node._def = RED.group.def; node._def = RED.group.def;
@ -2345,15 +2261,6 @@ RED.nodes = (function() {
outputs: n.outputs|| (n.wires && n.wires.length) || 0, outputs: n.outputs|| (n.wires && n.wires.length) || 0,
set: registry.getNodeSet("node-red/unknown") 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 { } else {
if (subflow_denylist[parentId] || createNewIds || options.importMap[n.id] === "copy") { if (subflow_denylist[parentId] || createNewIds || options.importMap[n.id] === "copy") {
parentId = subflow.id; parentId = subflow.id;
@ -2414,31 +2321,29 @@ RED.nodes = (function() {
node.type = "unknown"; node.type = "unknown";
} }
if (node._def.category != "config") { if (node._def.category != "config") {
if (n.hasOwnProperty('inputs') && node._def.defaults.hasOwnProperty("inputs")) { if (n.hasOwnProperty('inputs')) {
node.inputs = parseInt(n.inputs, 10); node.inputs = n.inputs;
node._config.inputs = JSON.stringify(n.inputs); node._config.inputs = JSON.stringify(n.inputs);
} else { } else {
node.inputs = node._def.inputs; node.inputs = node._def.inputs;
} }
if (n.hasOwnProperty('outputs') && node._def.defaults.hasOwnProperty("outputs")) { if (n.hasOwnProperty('outputs')) {
node.outputs = parseInt(n.outputs, 10); node.outputs = n.outputs;
node._config.outputs = JSON.stringify(n.outputs); node._config.outputs = JSON.stringify(n.outputs);
} else { } else {
node.outputs = node._def.outputs; node.outputs = node._def.outputs;
} }
if (node.hasOwnProperty('wires') && node.wires.length > node.outputs) {
// The node declares outputs in its defaults, but has not got a valid value if (!node._def.defaults.hasOwnProperty("outputs") || !isNaN(parseInt(n.outputs))) {
// 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 // 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); 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); 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
node.outputs = node.wires.length;
} }
} }
for (d in node._def.defaults) { for (d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') { if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') {
node[d] = n[d]; node[d] = n[d];
@ -2535,6 +2440,11 @@ RED.nodes = (function() {
nodeList = nodeList.map(function(id) { nodeList = nodeList.map(function(id) {
var node = node_map[id]; var node = node_map[id];
if (node) { if (node) {
if (node._def.category === 'config') {
if (node.users.indexOf(n) === -1) {
node.users.push(n);
}
}
return node.id; return node.id;
} }
return id; return id;
@ -2548,11 +2458,9 @@ RED.nodes = (function() {
n = new_subflows[i]; n = new_subflows[i];
n.in.forEach(function(input) { n.in.forEach(function(input) {
input.wires.forEach(function(wire) { input.wires.forEach(function(wire) {
if (node_map.hasOwnProperty(wire.id)) { var link = {source:input, sourcePort:0, target:node_map[wire.id]};
var link = {source:input, sourcePort:0, target:node_map[wire.id]}; addLink(link);
addLink(link); new_links.push(link);
new_links.push(link);
}
}); });
delete input.wires; delete input.wires;
}); });
@ -2561,13 +2469,11 @@ RED.nodes = (function() {
var link; var link;
if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) { if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) {
link = {source:n.in[wire.port], sourcePort:wire.port,target:output}; link = {source:n.in[wire.port], sourcePort:wire.port,target:output};
} else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) { } else {
link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:output}; link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:output};
} }
if (link) { addLink(link);
addLink(link); new_links.push(link);
new_links.push(link);
}
}); });
delete output.wires; delete output.wires;
}); });
@ -2576,13 +2482,11 @@ RED.nodes = (function() {
var link; var link;
if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) { if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) {
link = {source:n.in[wire.port], sourcePort:wire.port,target:n.status}; link = {source:n.in[wire.port], sourcePort:wire.port,target:n.status};
} else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) { } else {
link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:n.status}; link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:n.status};
} }
if (link) { addLink(link);
addLink(link); new_links.push(link);
new_links.push(link);
}
}); });
delete n.status.wires; delete n.status.wires;
} }
@ -2761,79 +2665,25 @@ RED.nodes = (function() {
return result; return result;
} }
/** // Update any config nodes referenced by the provided node to ensure their 'users' list is correct
* Update any config nodes referenced by the provided node to ensure function updateConfigNodeUsers(n) {
* their 'users' list is correct. for (var d in n._def.defaults) {
* if (n._def.defaults.hasOwnProperty(d)) {
* @param {object} node The node in which to check if it contains references var property = n._def.defaults[d];
* @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) { if (property.type) {
var type = registry.getNodeType(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") { if (type && type.category == "config") {
var configNode = configNodes[node[d]]; var configNode = configNodes[n[d]];
if (configNode) { if (configNode) {
if (options.action === "add") { if (configNode.users.indexOf(n) === -1) {
if (configNode.users.indexOf(node) === -1) { configNode.users.push(n);
configNode.users.push(node); RED.events.emit('nodes:change',configNode)
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) { function flowVersion(version) {

View File

@ -334,30 +334,6 @@ RED.clipboard = (function() {
},100); },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; var validateImportTimeout;
function validateImport() { function validateImport() {
if (activeTab === "red-ui-clipboard-dialog-import-tab-clipboard") { if (activeTab === "red-ui-clipboard-dialog-import-tab-clipboard") {
@ -375,7 +351,21 @@ RED.clipboard = (function() {
return; return;
} }
try { try {
validateFlowString(v) 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}));
}
}
currentPopoverError = null; currentPopoverError = null;
popover.close(true); popover.close(true);
importInput.removeClass("input-error"); importInput.removeClass("input-error");
@ -1008,16 +998,16 @@ RED.clipboard = (function() {
} }
function importNodes(nodesStr,addFlow) { function importNodes(nodesStr,addFlow) {
let newNodes = nodesStr; var newNodes = nodesStr;
if (typeof nodesStr === 'string') { if (typeof nodesStr === 'string') {
try { try {
nodesStr = nodesStr.trim(); nodesStr = nodesStr.trim();
if (nodesStr.length === 0) { if (nodesStr.length === 0) {
return; return;
} }
newNodes = validateFlowString(nodesStr) newNodes = JSON.parse(nodesStr);
} catch(err) { } catch(err) {
const e = new Error(RED._("clipboard.invalidFlow",{message:err.message})); var e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
e.code = "NODE_RED"; e.code = "NODE_RED";
throw e; throw e;
} }
@ -1352,7 +1342,6 @@ RED.clipboard = (function() {
} }
} }
} catch(err) { } catch(err) {
console.warn('Import failed: ', err)
// Ensure any errors throw above doesn't stop the drop target from // Ensure any errors throw above doesn't stop the drop target from
// being hidden. // being hidden.
} }

View File

@ -61,7 +61,7 @@
} }
this.menu = RED.popover.menu({ this.menu = RED.popover.menu({
tabSelect: true, tabSelect: true,
width: Math.max(300, this.element.width()), width: 300,
maxHeight: 200, maxHeight: 200,
class: "red-ui-autoComplete-container", class: "red-ui-autoComplete-container",
options: completions, options: completions,

View File

@ -63,7 +63,6 @@
pre: value.substring(0,idx), pre: value.substring(0,idx),
match: value.substring(idx,idx+len), match: value.substring(idx,idx+len),
post: value.substring(idx+len), post: value.substring(idx+len),
exact: idx === 0 && value.length === searchValue.length
} }
} }
function generateSpans(match) { function generateSpans(match) {
@ -84,7 +83,7 @@
const srcMatch = getMatch(optSrc, val); const srcMatch = getMatch(optSrc, val);
if (valMatch.found || srcMatch.found) { if (valMatch.found || srcMatch.found) {
const element = $('<div>',{style: "display: flex"}); const element = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{ class: "red-ui-autoComplete-completion" }); const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
valEl.append(generateSpans(valMatch)); valEl.append(generateSpans(valMatch));
valEl.appendTo(element); valEl.appendTo(element);
if (optSrc) { if (optSrc) {
@ -160,7 +159,7 @@
if (valMatch.found) { if (valMatch.found) {
const optSrc = envVarsMap[v] const optSrc = envVarsMap[v]
const element = $('<div>',{style: "display: flex"}); const element = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{ class: "red-ui-autoComplete-completion" }); const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
valEl.append(generateSpans(valMatch)) valEl.append(generateSpans(valMatch))
valEl.appendTo(element) valEl.appendTo(element)
@ -202,7 +201,7 @@
const that = this const that = this
const getContextKeysFromRuntime = function(scope, store, searchKey, done) { const getContextKeysFromRuntime = function(scope, store, searchKey, done) {
contextKnownKeys[scope] = contextKnownKeys[scope] || {} contextKnownKeys[scope] = contextKnownKeys[scope] || {}
contextKnownKeys[scope][store] = contextKnownKeys[scope][store] || new Map() contextKnownKeys[scope][store] = contextKnownKeys[scope][store] || new Set()
if (searchKey.length > 0) { if (searchKey.length > 0) {
try { try {
RED.utils.normalisePropertyExpression(searchKey) RED.utils.normalisePropertyExpression(searchKey)
@ -224,12 +223,11 @@
const result = data[store] || {} const result = data[store] || {}
const keys = result.keys || [] const keys = result.keys || []
const keyPrefix = searchKey + (searchKey.length > 0 ? '.' : '') const keyPrefix = searchKey + (searchKey.length > 0 ? '.' : '')
keys.forEach(keyInfo => { keys.forEach(key => {
const key = keyInfo.key
if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(key)) { if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(key)) {
contextKnownKeys[scope][store].set(keyPrefix + key, keyInfo) contextKnownKeys[scope][store].add(keyPrefix + key)
} else { } else {
contextKnownKeys[scope][store].set(searchKey + "[\""+key.replace(/"/,"\\\"")+"\"]", keyInfo) contextKnownKeys[scope][store].add(searchKey + "[\""+key.replace(/"/,"\\\"")+"\"]")
} }
}) })
done() done()
@ -244,14 +242,14 @@
// Get the flow id of the node we're editing // Get the flow id of the node we're editing
const editStack = RED.editor.getEditStack() const editStack = RED.editor.getEditStack()
if (editStack.length === 0) { if (editStack.length === 0) {
done(new Map()) done([])
return return
} }
const editingNode = editStack.pop() const editingNode = editStack.pop()
if (editingNode.z) { if (editingNode.z) {
scope = `${scope}/${editingNode.z}` scope = `${scope}/${editingNode.z}`
} else { } else {
done(new Map()) done([])
return return
} }
} }
@ -271,29 +269,17 @@
return function(val, done) { return function(val, done) {
getContextKeys(val, function (keys) { getContextKeys(val, function (keys) {
const matches = [] const matches = []
keys.forEach((keyInfo, v) => { keys.forEach(v => {
let optVal = v let optVal = v
let valMatch = getMatch(optVal, val); let valMatch = getMatch(optVal, val);
if (!valMatch.found && val.length > 0) { if (!valMatch.found && val.length > 0 && val.endsWith('.')) {
if (val.endsWith('.')) { // Search key ends in '.' - but doesn't match. Check again
// Search key ends in '.' - but doesn't match. Check again // with [" at the end instead so we match bracket notation
// with [" at the end instead so we match bracket notation valMatch = getMatch(optVal, val.substring(0, val.length - 1) + '["')
valMatch = getMatch(optVal, val.substring(0, val.length - 1) + '["')
// } else if (val.endsWith('[') && /^array/.test(keyInfo.format)) {
// console.log('this case')
}
} }
if (valMatch.found) { if (valMatch.found) {
const element = $('<div>',{style: "display: flex"}); const element = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{ class: "red-ui-autoComplete-completion" }); const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
// if (keyInfo.format) {
// valMatch.post += ' ' + keyInfo.format
// }
if (valMatch.exact && /^array/.test(keyInfo.format)) {
valMatch.post += `[0-${keyInfo.length}]`
optVal += '['
}
valEl.append(generateSpans(valMatch)) valEl.append(generateSpans(valMatch))
valEl.appendTo(element) valEl.appendTo(element)
matches.push({ matches.push({
@ -1581,8 +1567,7 @@
if (tooltip) { if (tooltip) {
tooltip.setContent(valid); tooltip.setContent(valid);
} else { } else {
const target = this.typeMap[type]?.options ? this.optionSelectLabel : this.elementDiv; tooltip = RED.popover.tooltip(this.elementDiv, valid);
tooltip = RED.popover.tooltip(target, valid);
this.element.data("tooltip", tooltip); this.element.data("tooltip", tooltip);
} }
} }

View File

@ -54,15 +54,15 @@ RED.contextMenu = (function () {
} }
} }
const scale = RED.view.scale()
const offset = $("#red-ui-workspace-chart").offset() const offset = $("#red-ui-workspace-chart").offset()
let addX = (options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()) / scale
let addY = (options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()) / scale let addX = options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()
let addY = options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()
if (RED.view.snapGrid) { if (RED.view.snapGrid) {
const gridSize = RED.view.gridSize() const gridSize = RED.view.gridSize()
addX = gridSize * Math.round(addX / gridSize) addX = gridSize * Math.floor(addX / gridSize)
addY = gridSize * Math.round(addY / gridSize) addY = gridSize * Math.floor(addY / gridSize)
} }
if (RED.settings.theme("menu.menu-item-action-list", true)) { if (RED.settings.theme("menu.menu-item-action-list", true)) {
@ -87,9 +87,7 @@ RED.contextMenu = (function () {
}, },
(hasLinks) ? { // has least 1 wire selected (hasLinks) ? { // has least 1 wire selected
label: RED._("contextMenu.junction"), label: RED._("contextMenu.junction"),
onselect: function () { onselect: 'core:split-wires-with-junctions',
RED.actions.invoke('core:split-wires-with-junctions', { x: addX, y: addY })
},
disabled: !canEdit || !hasLinks disabled: !canEdit || !hasLinks
} : { } : {
label: RED._("contextMenu.junction"), label: RED._("contextMenu.junction"),

View File

@ -44,7 +44,6 @@ RED.deploy = (function() {
/** /**
* options: * options:
* type: "default" - Button with drop-down options - no further customisation available * type: "default" - Button with drop-down options - no further customisation available
* label: the text to display - default: "Deploy"
* type: "simple" - Button without dropdown. Customisations: * type: "simple" - Button without dropdown. Customisations:
* label: the text to display - default: "Deploy" * label: the text to display - default: "Deploy"
* icon : the icon to use. Null removes the icon. default: "red/images/deploy-full-o.svg" * icon : the icon to use. Null removes the icon. default: "red/images/deploy-full-o.svg"
@ -52,14 +51,13 @@ RED.deploy = (function() {
function init(options) { function init(options) {
options = options || {}; options = options || {};
var type = options.type || "default"; var type = options.type || "default";
var label = options.label || RED._("deploy.deploy");
if (type == "default") { if (type == "default") {
$('<li><span class="red-ui-deploy-button-group button-group">'+ $('<li><span class="red-ui-deploy-button-group button-group">'+
'<a id="red-ui-header-button-deploy" class="red-ui-deploy-button disabled" href="#">'+ '<a id="red-ui-header-button-deploy" class="red-ui-deploy-button disabled" href="#">'+
'<span class="red-ui-deploy-button-content">'+ '<span class="red-ui-deploy-button-content">'+
'<img id="red-ui-header-button-deploy-icon" src="red/images/deploy-full-o.svg"> '+ '<img id="red-ui-header-button-deploy-icon" src="red/images/deploy-full-o.svg"> '+
'<span>'+label+'</span>'+ '<span>'+RED._("deploy.deploy")+'</span>'+
'</span>'+ '</span>'+
'<span class="red-ui-deploy-button-spinner hide">'+ '<span class="red-ui-deploy-button-spinner hide">'+
'<img src="red/images/spin.svg"/>'+ '<img src="red/images/spin.svg"/>'+
@ -80,6 +78,7 @@ RED.deploy = (function() {
mainMenuItems.push({id:"deploymenu-item-reload", icon:"red/images/deploy-reload.svg",label:RED._("deploy.restartFlows"),sublabel:RED._("deploy.restartFlowsDesc"),onselect:"core:restart-flows"}) mainMenuItems.push({id:"deploymenu-item-reload", icon:"red/images/deploy-reload.svg",label:RED._("deploy.restartFlows"),sublabel:RED._("deploy.restartFlowsDesc"),onselect:"core:restart-flows"})
RED.menu.init({id:"red-ui-header-button-deploy-options", options: mainMenuItems }); RED.menu.init({id:"red-ui-header-button-deploy-options", options: mainMenuItems });
} else if (type == "simple") { } else if (type == "simple") {
var label = options.label || RED._("deploy.deploy");
var icon = 'red/images/deploy-full-o.svg'; var icon = 'red/images/deploy-full-o.svg';
if (options.hasOwnProperty('icon')) { if (options.hasOwnProperty('icon')) {
icon = options.icon; icon = options.icon;
@ -425,15 +424,11 @@ RED.deploy = (function() {
const unknownNodes = []; const unknownNodes = [];
const invalidNodes = []; const invalidNodes = [];
const isDisabled = function (node) {
return (node.d || RED.nodes.workspace(node.z)?.disabled);
};
RED.nodes.eachConfig(function (node) { RED.nodes.eachConfig(function (node) {
if (node.valid === undefined) { if (node.valid === undefined) {
RED.editor.validateNode(node); RED.editor.validateNode(node);
} }
if (!node.valid && !isDisabled(node)) { if (!node.valid && !node.d) {
invalidNodes.push(getNodeInfo(node)); invalidNodes.push(getNodeInfo(node));
} }
if (node.type === "unknown") { if (node.type === "unknown") {
@ -443,7 +438,7 @@ RED.deploy = (function() {
} }
}); });
RED.nodes.eachNode(function (node) { RED.nodes.eachNode(function (node) {
if (!node.valid && !isDisabled(node)) { if (!node.valid && !node.d) {
invalidNodes.push(getNodeInfo(node)); invalidNodes.push(getNodeInfo(node));
} }
if (node.type === "unknown") { if (node.type === "unknown") {
@ -457,7 +452,7 @@ RED.deploy = (function() {
const unusedConfigNodes = []; const unusedConfigNodes = [];
RED.nodes.eachConfig(function (node) { RED.nodes.eachConfig(function (node) {
if ((node._def.hasUsers !== false) && (node.users.length === 0) && !isDisabled(node)) { if ((node._def.hasUsers !== false) && (node.users.length === 0)) {
unusedConfigNodes.push(getNodeInfo(node)); unusedConfigNodes.push(getNodeInfo(node));
hasUnusedConfig = true; hasUnusedConfig = true;
} }

View File

@ -808,20 +808,6 @@ 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 { try {
const rc = editing_node._def.oneditsave.call(editing_node); const rc = editing_node._def.oneditsave.call(editing_node);
if (rc === true) { if (rc === true) {
@ -853,25 +839,6 @@ 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;
}
}
}
}
} }
} }
@ -1514,181 +1481,134 @@ RED.editor = (function() {
}, },
{ {
id: "node-config-dialog-ok", id: "node-config-dialog-ok",
text: adding ? RED._("editor.configAdd") : RED._("editor.configUpdate"), text: adding?RED._("editor.configAdd"):RED._("editor.configUpdate"),
class: "primary", class: "primary",
click: function() { click: function() {
// TODO: Already defined var editState = {
const configProperty = name;
const configType = type;
const configTypeDef = RED.nodes.getType(configType);
const wasChanged = editing_config_node.changed;
const editState = {
changes: {}, changes: {},
changed: false, changed: false,
outputMap: null 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 if (configTypeDef.oneditsave) {
handleEditSave(editing_config_node, editState); try {
configTypeDef.oneditsave.call(editing_config_node);
} catch(err) {
console.warn("oneditsave",editing_config_node.id,editing_config_node.type,err.toString());
}
}
// Search for changes in the edit box (panes) for (d in configTypeDef.defaults) {
activeEditPanes.forEach(function (pane) { 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) {
if (pane.apply) { if (pane.apply) {
pane.apply.call(pane, editState); pane.apply.call(pane, editState);
} }
}); })
// TODO: Why? editing_config_node.label = configTypeDef.label;
editing_config_node.label = configTypeDef.label
var scope = $("#red-ui-editor-config-scope").val();
editing_config_node.z = scope;
// Check if disabled has changed
if ($("#node-config-input-node-disabled").prop('checked')) { if ($("#node-config-input-node-disabled").prop('checked')) {
if (editing_config_node.d !== true) { if (editing_config_node.d !== true) {
editState.changes.d = editing_config_node.d;
editState.changed = true;
editing_config_node.d = true; editing_config_node.d = true;
} }
} else { } else {
if (editing_config_node.d === true) { if (editing_config_node.d === true) {
editState.changes.d = editing_config_node.d;
editState.changed = true;
delete editing_config_node.d; 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) { if (scope) {
const newUsers = editing_config_node.users.filter(function (node) { // Search for nodes that use this one that are no longer
let keepNode = false; // in scope, so must be removed
let nodeModified = null; editing_config_node.users = editing_config_node.users.filter(function(n) {
var keep = true;
for (const d in node._def.defaults) { for (var d in n._def.defaults) {
if (node._def.defaults.hasOwnProperty(d)) { if (n._def.defaults.hasOwnProperty(d)) {
if (node._def.defaults[d].type === editing_config_node.type) { if (n._def.defaults[d].type === editing_config_node.type &&
if (node[d] === editing_config_node.id) { n[d] === editing_config_node.id &&
if (node.z === editing_config_node.z) { n.z !== scope) {
// The node is kept only if at least one property uses keep = false;
// this config node in the correct scope. // Remove the reference to this node
keepNode = true; // and revalidate
} else { n[d] = null;
if (!nodeModified) { n.dirty = true;
nodeModified = { n.changed = true;
t: "edit", validateNode(n);
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;
// Add the node modified to the history
if (nodeModified) {
historyEvents.push(nodeModified);
}
// 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;
}
} }
if (editState.changed) { if (configAdding) {
// Set the congig node as changed RED.nodes.add(editing_config_node);
editing_config_node.changed = true;
} }
// Now, validate the config node
validateNode(editing_config_node); validateNode(editing_config_node);
var validatedNodes = {};
validatedNodes[editing_config_node.id] = true;
// And validate nodes using this config node too var userStack = editing_config_node.users.slice();
const validatedNodes = new Set(); while(userStack.length > 0) {
const userStack = editing_config_node.users.slice(); var user = userStack.pop();
if (!validatedNodes[user.id]) {
validatedNodes.add(editing_config_node.id); validatedNodes[user.id] = true;
while (userStack.length) { if (user.users) {
const node = userStack.pop(); userStack = userStack.concat(user.users);
if (!validatedNodes.has(node.id)) {
validatedNodes.add(node.id);
if (node.users) {
userStack.push(...node.users);
} }
validateNode(node); validateNode(user);
} }
} }
RED.nodes.dirty(true);
let historyEvent = { RED.view.redraw(true);
t: "edit", if (!configAdding) {
node: editing_config_node, RED.events.emit("editor:save",editing_config_node);
changes: editState.changes, RED.events.emit("nodes:change",editing_config_node);
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() { RED.tray.close(function() {
var filter = null; var filter = null;
// when editing a config via subflow edit panel, the `configProperty` will not // when editing a config via subflow edit panel, the `configProperty` will not
@ -1836,18 +1756,8 @@ RED.editor = (function() {
} }
}); });
} }
let envToRemove = new Set()
if (!isSameObj(old_env, new_env)) { if (!isSameObj(old_env, new_env)) {
// Get a list of env properties that have been removed
// by comparing old_env and new_env
if (old_env) {
old_env.forEach(env => { envToRemove.add(env.name) })
}
if (new_env) {
new_env.forEach(env => {
envToRemove.delete(env.name)
})
}
editState.changes.env = editing_node.env; editState.changes.env = editing_node.env;
editing_node.env = new_env; editing_node.env = new_env;
editState.changed = true; editState.changed = true;
@ -1856,11 +1766,10 @@ RED.editor = (function() {
if (editState.changed) { if (editState.changed) {
let wasChanged = editing_node.changed; var wasChanged = editing_node.changed;
editing_node.changed = true; editing_node.changed = true;
validateNode(editing_node); validateNode(editing_node);
let subflowInstances = []; var subflowInstances = [];
let instanceHistoryEvents = []
RED.nodes.eachNode(function(n) { RED.nodes.eachNode(function(n) {
if (n.type == "subflow:"+editing_node.id) { if (n.type == "subflow:"+editing_node.id) {
subflowInstances.push({ subflowInstances.push({
@ -1870,35 +1779,13 @@ RED.editor = (function() {
n._def.color = editing_node.color; n._def.color = editing_node.color;
n.changed = true; n.changed = true;
n.dirty = true; n.dirty = true;
if (n.env) {
const oldEnv = n.env
const newEnv = []
let envChanged = false
n.env.forEach((env, index) => {
if (envToRemove.has(env.name)) {
envChanged = true
} else {
newEnv.push(env)
}
})
if (envChanged) {
instanceHistoryEvents.push({
t: 'edit',
node: n,
changes: { env: oldEnv },
dirty: n.dirty,
changed: n.changed
})
n.env = newEnv
}
}
updateNodeProperties(n); updateNodeProperties(n);
validateNode(n); validateNode(n);
} }
}); });
RED.events.emit("subflows:change",editing_node); RED.events.emit("subflows:change",editing_node);
RED.nodes.dirty(true); RED.nodes.dirty(true);
let historyEvent = { var historyEvent = {
t:'edit', t:'edit',
node:editing_node, node:editing_node,
changes:editState.changes, changes:editState.changes,
@ -1908,13 +1795,7 @@ RED.editor = (function() {
instances:subflowInstances instances:subflowInstances
} }
}; };
if (instanceHistoryEvents.length > 0) {
historyEvent = {
t: 'multi',
events: [ historyEvent, ...instanceHistoryEvents ],
dirty: wasDirty
}
}
RED.history.push(historyEvent); RED.history.push(historyEvent);
} }
editing_node.dirty = true; editing_node.dirty = true;

View File

@ -691,7 +691,6 @@ RED.editor.codeEditor.monaco = (function() {
2322, //Type 'unknown' is not assignable to type 'string' 2322, //Type 'unknown' is not assignable to type 'string'
2339, //property does not exist on 2339, //property does not exist on
2345, //Argument of type xxx is not assignable to parameter of type 'DateTimeFormatOptions' 2345, //Argument of type xxx is not assignable to parameter of type 'DateTimeFormatOptions'
2538, //Ignore symbols as index property error.
7043, //i forget what this one is, 7043, //i forget what this one is,
80001, //Convert to ES6 module 80001, //Convert to ES6 module
80004, //JSDoc types may be moved to TypeScript types. 80004, //JSDoc types may be moved to TypeScript types.

View File

@ -131,7 +131,7 @@ RED.editor.envVarList = (function() {
nameField.trigger('change'); nameField.trigger('change');
} }
}, },
sortable: true, sortable: ".red-ui-editableList-item-handle",
removable: false removable: false
}); });
var parentEnv = {}; var parentEnv = {};

View File

@ -27,12 +27,6 @@
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
function file2Text(file,cb) {
file.arrayBuffer().then(d => {
cb( new TextDecoder().decode(d) )
}).catch(ex => { cb(`error: ${ex}`) })
}
var initialized = false; var initialized = false;
var currentEditor = null; var currentEditor = null;
/** /**
@ -58,7 +52,6 @@
if (files.length === 1) { if (files.length === 1) {
var file = files[0]; var file = files[0];
var name = file.name.toLowerCase(); var name = file.name.toLowerCase();
var fileType = file.type.toLowerCase();
if (name.match(/\.(apng|avif|gif|jpeg|png|svg|webp)$/)) { if (name.match(/\.(apng|avif|gif|jpeg|png|svg|webp)$/)) {
file2base64Image(file, function (image) { file2base64Image(file, function (image) {
@ -70,29 +63,6 @@
}); });
return; return;
} }
if ( fileType.startsWith("text/") ) {
file2Text(file, function (txt) {
var session = currentEditor.getSession();
var pos = session.getCursorPosition();
session.insert(pos, txt);
$("#red-ui-image-drop-target").hide();
});
return;
}
}
} else if ($.inArray("text/plain", ev.originalEvent.dataTransfer.types) != -1) {
let item = Object.values(ev.originalEvent.dataTransfer.items).filter(d => d.type == "text/plain")[0]
if (item) {
item.getAsString(txt => {
var session = currentEditor.getSession();
var pos = session.getCursorPosition();
session.insert(pos, txt);
$("#red-ui-image-drop-target").hide();
})
return
} }
} }
$("#red-ui-image-drop-target").hide(); $("#red-ui-image-drop-target").hide();

View File

@ -20,31 +20,10 @@
apply: function(editState) { apply: function(editState) {
var old_env = node.env; var old_env = node.env;
var new_env = []; var new_env = [];
if (/^subflow:/.test(node.type)) { if (/^subflow:/.test(node.type)) {
// Get the list of environment variables from the node properties
new_env = RED.subflow.exportSubflowInstanceEnv(node); 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 // Get the values from the Properties table tab
var items = this.list.editableList('items'); var items = this.list.editableList('items');
items.each(function (i,el) { items.each(function (i,el) {
@ -62,6 +41,7 @@
} }
}); });
if (new_env && new_env.length > 0) { if (new_env && new_env.length > 0) {
new_env.forEach(function(prop) { new_env.forEach(function(prop) {
if (prop.type === "cred") { if (prop.type === "cred") {
@ -72,15 +52,6 @@
editState.changed = true; editState.changed = true;
} }
delete prop.value; 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);
}
}
} }
}); });
} }

View File

@ -44,7 +44,6 @@
apply: function(editState) { apply: function(editState) {
var newValue; var newValue;
var d; var d;
// If the node is a subflow, the node's properties (exepts name) are saved by `envProperties`
if (node._def.defaults) { if (node._def.defaults) {
for (d in node._def.defaults) { for (d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d)) { if (node._def.defaults.hasOwnProperty(d)) {
@ -132,16 +131,9 @@
} }
} }
if (node._def.credentials) { if (node._def.credentials) {
const credDefinition = node._def.credentials; var credDefinition = node._def.credentials;
const credChanges = updateNodeCredentials(node, credDefinition, this.inputClass); var credsChanged = updateNodeCredentials(node,credDefinition,this.inputClass);
editState.changed = editState.changed || credsChanged;
if (Object.keys(credChanges).length) {
editState.changed = true;
editState.changes.credentials = {
...(editState.changes.credentials || {}),
...credChanges
};
}
} }
} }
} }
@ -169,11 +161,10 @@
* @param node - the node containing the credentials * @param node - the node containing the credentials
* @param credDefinition - definition of the credentials * @param credDefinition - definition of the credentials
* @param prefix - prefix of the input fields * @param prefix - prefix of the input fields
* @return {object} an object containing the modified properties * @return {boolean} whether anything has changed
*/ */
function updateNodeCredentials(node, credDefinition, prefix) { function updateNodeCredentials(node, credDefinition, prefix) {
const changes = {}; var changed = false;
if (!node.credentials) { if (!node.credentials) {
node.credentials = {_:{}}; node.credentials = {_:{}};
} else if (!node.credentials._) { } else if (!node.credentials._) {
@ -186,33 +177,22 @@
if (input.length > 0) { if (input.length > 0) {
var value = input.val(); var value = input.val();
if (credDefinition[cred].type == 'password') { if (credDefinition[cred].type == 'password') {
if (value === '__PWRD__') { node.credentials['has_' + cred] = (value !== "");
// A cred value exists - no changes if (value == '__PWRD__') {
} else if (value === '' && node.credentials['has_' + cred] === false) { continue;
// 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['has_' + cred] = (value !== ''); }
} else { node.credentials[cred] = value;
// Since these creds are loaded by the editor, if (value != node.credentials._[cred]) {
// values can be directly compared changed = true;
if (value !== node.credentials[cred]) {
changes[cred] = node.credentials[cred];
node.credentials[cred] = value;
}
} }
} }
} }
} }
return changed;
return changes;
} }
})(); })();

View File

@ -245,15 +245,10 @@ RED.library = (function() {
if (lib.types && lib.types.indexOf(options.url) === -1) { if (lib.types && lib.types.indexOf(options.url) === -1) {
return; return;
} }
let icon = 'fa fa-hdd-o';
if (lib.icon) {
const fullIcon = RED.utils.separateIconPath(lib.icon);
icon = (fullIcon.module==="font-awesome"?"fa ":"")+fullIcon.file;
}
listing.push({ listing.push({
library: lib.id, library: lib.id,
type: options.url, type: options.url,
icon, icon: lib.icon || 'fa fa-hdd-o',
label: RED._(lib.label||lib.id), label: RED._(lib.label||lib.id),
path: "", path: "",
expanded: true, expanded: true,
@ -308,15 +303,10 @@ RED.library = (function() {
if (lib.types && lib.types.indexOf(options.url) === -1) { if (lib.types && lib.types.indexOf(options.url) === -1) {
return; return;
} }
let icon = 'fa fa-hdd-o';
if (lib.icon) {
const fullIcon = RED.utils.separateIconPath(lib.icon);
icon = (fullIcon.module==="font-awesome"?"fa ":"")+fullIcon.file;
}
listing.push({ listing.push({
library: lib.id, library: lib.id,
type: options.url, type: options.url,
icon, icon: lib.icon || 'fa fa-hdd-o',
label: RED._(lib.label||lib.id), label: RED._(lib.label||lib.id),
path: "", path: "",
expanded: true, expanded: true,

View File

@ -1362,7 +1362,7 @@ RED.subflow = (function() {
item.value = ""+input.prop("checked"); item.value = ""+input.prop("checked");
break; break;
case "conf-types": case "conf-types":
item.value = input.val() === "_ADD_" ? "" : input.val(); item.value = input.val()
item.type = "conf-type" item.type = "conf-type"
} }
if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) { if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) {

View File

@ -56,16 +56,7 @@ RED.sidebar.config = (function() {
} else { } else {
$('<span class="red-ui-palette-node-config-label" data-i18n="sidebar.config.'+name+'">').appendTo(header); $('<span class="red-ui-palette-node-config-label" data-i18n="sidebar.config.'+name+'">').appendTo(header);
} }
$('<span class="red-ui-sidebar-node-config-filter-info"></span>').appendTo(header); $('<span class="red-ui-sidebar-node-config-filter-info"></span>').appendTo(header);
const changeBadgeContainer = $('<svg class="red-ui-sidebar-config-category-changed red-ui-flow-node-changed" width="10" height="10" viewBox="-1 -1 12 12"></svg>').appendTo(header);
const changeBadge = document.createElementNS("http://www.w3.org/2000/svg", "circle");
changeBadge.setAttribute("cx", "5");
changeBadge.setAttribute("cy", "5");
changeBadge.setAttribute("r", "5");
changeBadgeContainer.append(changeBadge);
category = $('<ul class="red-ui-palette-content red-ui-sidebar-node-config-list"></ul>').appendTo(container); category = $('<ul class="red-ui-palette-content red-ui-sidebar-node-config-list"></ul>').appendTo(container);
category.on("click", function(e) { category.on("click", function(e) {
$(content).find(".red-ui-palette-node").removeClass("selected"); $(content).find(".red-ui-palette-node").removeClass("selected");
@ -159,6 +150,9 @@ RED.sidebar.config = (function() {
$('<li class="red-ui-palette-node-config-type">'+node.type+'</li>').appendTo(list); $('<li class="red-ui-palette-node-config-type">'+node.type+'</li>').appendTo(list);
currentType = node.type; currentType = node.type;
} }
if (node.changed) {
labelText += "!!"
}
var entry = $('<li class="red-ui-palette-node_id_'+node.id.replace(/\./g,"-")+'"></li>').appendTo(list); var entry = $('<li class="red-ui-palette-node_id_'+node.id.replace(/\./g,"-")+'"></li>').appendTo(list);
var nodeDiv = $('<div class="red-ui-palette-node-config red-ui-palette-node"></div>').appendTo(entry); var nodeDiv = $('<div class="red-ui-palette-node-config red-ui-palette-node"></div>').appendTo(entry);
entry.data('node',node.id); entry.data('node',node.id);
@ -187,29 +181,15 @@ RED.sidebar.config = (function() {
} }
} }
if (node.changed) {
const nodeDivAnnotations = $('<svg class="red-ui-palette-node-annotations red-ui-flow-node-changed" width="10" height="10" viewBox="-1 -1 12 12"></svg>').appendTo(nodeDiv);
const changeBadge = document.createElementNS("http://www.w3.org/2000/svg", "circle");
changeBadge.setAttribute("cx", "5");
changeBadge.setAttribute("cy", "5");
changeBadge.setAttribute("r", "5");
nodeDivAnnotations.append($(changeBadge));
const categoryHeader = list.parent().find(".red-ui-sidebar-config-tray-header.red-ui-palette-header");
categoryHeader.addClass("red-ui-sidebar-config-changed");
nodeDiv.addClass("red-ui-palette-node-config-changed");
}
if (!node.valid) { if (!node.valid) {
const nodeDivAnnotations = $('<svg class="red-ui-palette-node-annotations red-ui-flow-node-error" width="10" height="10"></svg>').appendTo(nodeDiv); nodeDiv.addClass("red-ui-palette-node-config-invalid")
const errorBadge = document.createElementNS("http://www.w3.org/2000/svg", "path"); const nodeDivAnnotations = $('<svg class="red-ui-palette-node-annotations red-ui-flow-node-error" width="10" height="10"></svg>').appendTo(nodeDiv)
errorBadge.setAttribute("d", "M 0,9 l 10,0 -5,-8 z"); const errorBadge = document.createElementNS("http://www.w3.org/2000/svg","path");
nodeDivAnnotations.append($(errorBadge)); errorBadge.setAttribute("d","M 0,9 l 10,0 -5,-8 z");
nodeDivAnnotations.append($(errorBadge))
nodeDiv.addClass("red-ui-palette-node-config-invalid");
RED.popover.tooltip(nodeDivAnnotations, function () { RED.popover.tooltip(nodeDivAnnotations, function () {
if (node.validationErrors && node.validationErrors.length > 0) { if (node.validationErrors && node.validationErrors.length > 0) {
return RED._("editor.errors.invalidProperties") + "<br> - " + node.validationErrors.join("<br> - "); return RED._("editor.errors.invalidProperties")+"<br> - "+node.validationErrors.join("<br> - ")
} }
}) })
} }
@ -272,10 +252,6 @@ RED.sidebar.config = (function() {
$(this).remove(); $(this).remove();
delete categories[id]; delete categories[id];
} }
// Remove the `changed` badge from the category header
const categoryHeader = $(this).find(".red-ui-sidebar-config-tray-header.red-ui-palette-header");
categoryHeader.removeClass("red-ui-sidebar-config-changed");
}) })
var globalConfigNodes = []; var globalConfigNodes = [];
var configList = {}; var configList = {};

View File

@ -18,6 +18,8 @@ RED.sidebar.context = (function() {
var content; var content;
var sections; var sections;
var localCache = {};
var flowAutoRefresh; var flowAutoRefresh;
var nodeAutoRefresh; var nodeAutoRefresh;
var nodeSection; var nodeSection;
@ -25,8 +27,6 @@ RED.sidebar.context = (function() {
var flowSection; var flowSection;
var globalSection; var globalSection;
const expandedPaths = {}
var currentNode; var currentNode;
var currentFlow; var currentFlow;
@ -212,41 +212,14 @@ RED.sidebar.context = (function() {
var l = keys.length; var l = keys.length;
for (var i = 0; i < l; i++) { for (var i = 0; i < l; i++) {
sortedData[keys[i]].forEach(function(v) { sortedData[keys[i]].forEach(function(v) {
const k = keys[i]; var k = keys[i];
let payload = v.msg; var l2 = sortedData[k].length;
let format = v.format; var propRow = $('<tr class="red-ui-help-info-row"><td class="red-ui-sidebar-context-property"></td><td></td></tr>').appendTo(container);
const tools = $('<span class="button-group"></span>'); var obj = $(propRow.children()[0]);
expandedPaths[id + "." + k] = expandedPaths[id + "." + k] || new Set()
const objectElementOptions = {
typeHint: format,
sourceId: id + "." + k,
tools,
path: k,
rootPath: k,
exposeApi: true,
ontoggle: function(path,state) {
path = path.substring(k.length+1)
if (state) {
expandedPaths[id+"."+k].add(path)
} else {
// if 'a' has been collapsed, we want to remove 'a.b' and 'a[0]...' from the set
// of collapsed paths
for (let expandedPath of expandedPaths[id+"."+k]) {
if (expandedPath.startsWith(path+".") || expandedPath.startsWith(path+"[")) {
expandedPaths[id+"."+k].delete(expandedPath)
}
}
expandedPaths[id+"."+k].delete(path)
}
},
expandPaths: [ ...expandedPaths[id+"."+k] ].sort(),
expandLeafNodes: true
}
const propRow = $('<tr class="red-ui-help-info-row"><td class="red-ui-sidebar-context-property"></td><td></td></tr>').appendTo(container);
const obj = $(propRow.children()[0]);
obj.text(k); obj.text(k);
var tools = $('<span class="button-group"></span>');
const urlSafeK = encodeURIComponent(k) const urlSafeK = encodeURIComponent(k)
const refreshItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-refresh"></i></button>').appendTo(tools).on("click", function(e) { var refreshItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-refresh"></i></button>').appendTo(tools).on("click", function(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
$.getJSON(baseUrl+"/"+urlSafeK+"?store="+v.store, function(data) { $.getJSON(baseUrl+"/"+urlSafeK+"?store="+v.store, function(data) {
@ -256,14 +229,16 @@ RED.sidebar.context = (function() {
tools.detach(); tools.detach();
$(propRow.children()[1]).empty(); $(propRow.children()[1]).empty();
RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), { RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), {
...objectElementOptions,
typeHint: data.format, typeHint: data.format,
sourceId: id+"."+k,
tools: tools,
path: k
}).appendTo(propRow.children()[1]); }).appendTo(propRow.children()[1]);
} }
}) })
}); });
RED.popover.tooltip(refreshItem,RED._("sidebar.context.refrsh")); RED.popover.tooltip(refreshItem,RED._("sidebar.context.refrsh"));
const deleteItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-trash"></i></button>').appendTo(tools).on("click", function(e) { var deleteItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-trash"></i></button>').appendTo(tools).on("click", function(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
var popover = RED.popover.create({ var popover = RED.popover.create({
@ -271,7 +246,7 @@ RED.sidebar.context = (function() {
target: propRow, target: propRow,
direction: "left", direction: "left",
content: function() { content: function() {
const content = $('<div>'); var content = $('<div>');
$('<p data-i18n="sidebar.context.deleteConfirm"></p>').appendTo(content); $('<p data-i18n="sidebar.context.deleteConfirm"></p>').appendTo(content);
var row = $('<p>').appendTo(content); var row = $('<p>').appendTo(content);
var bg = $('<span class="button-group"></span>').appendTo(row); var bg = $('<span class="button-group"></span>').appendTo(row);
@ -294,15 +269,16 @@ RED.sidebar.context = (function() {
if (container.children().length === 0) { if (container.children().length === 0) {
$('<tr class="red-ui-help-info-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.empty"></td></tr>').appendTo(container).i18n(); $('<tr class="red-ui-help-info-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.empty"></td></tr>').appendTo(container).i18n();
} }
delete expandedPaths[id + "." + k]
} else { } else {
payload = data.msg; payload = data.msg;
format = data.format; format = data.format;
tools.detach(); tools.detach();
$(propRow.children()[1]).empty(); $(propRow.children()[1]).empty();
RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), { RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), {
...objectElementOptions, typeHint: data.format,
typeHint: data.format sourceId: id+"."+k,
tools: tools,
path: k
}).appendTo(propRow.children()[1]); }).appendTo(propRow.children()[1]);
} }
}); });
@ -317,7 +293,14 @@ RED.sidebar.context = (function() {
}); });
RED.popover.tooltip(deleteItem,RED._("sidebar.context.delete")); RED.popover.tooltip(deleteItem,RED._("sidebar.context.delete"));
RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), objectElementOptions).appendTo(propRow.children()[1]); var payload = v.msg;
var format = v.format;
RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), {
typeHint: v.format,
sourceId: id+"."+k,
tools: tools,
path: k
}).appendTo(propRow.children()[1]);
if (contextStores.length > 1) { if (contextStores.length > 1) {
$("<span>",{class:"red-ui-sidebar-context-property-storename"}).text(v.store).appendTo($(propRow.children()[0])) $("<span>",{class:"red-ui-sidebar-context-property-storename"}).text(v.store).appendTo($(propRow.children()[0]))
} }

View File

@ -382,7 +382,7 @@ RED.typeSearch = (function() {
var items = []; var items = [];
RED.nodes.registry.getNodeTypes().forEach(function(t) { RED.nodes.registry.getNodeTypes().forEach(function(t) {
var def = RED.nodes.getType(t); var def = RED.nodes.getType(t);
if (def.set?.enabled !== false && def.category !== 'config' && t !== 'unknown' && t !== 'tab') { if (def.category !== 'config' && t !== 'unknown' && t !== 'tab') {
items.push({type:t,def: def, label:getTypeLabel(t,def)}); items.push({type:t,def: def, label:getTypeLabel(t,def)});
} }
}); });

View File

@ -230,7 +230,7 @@ RED.utils = (function() {
var pinnedPaths = {}; var pinnedPaths = {};
var formattedPaths = {}; var formattedPaths = {};
function addMessageControls(obj,sourceId,key,msg,rootPath,strippedKey,extraTools,enablePinning) { function addMessageControls(obj,sourceId,key,msg,rootPath,strippedKey,extraTools) {
if (!pinnedPaths.hasOwnProperty(sourceId)) { if (!pinnedPaths.hasOwnProperty(sourceId)) {
pinnedPaths[sourceId] = {} pinnedPaths[sourceId] = {}
} }
@ -250,7 +250,7 @@ RED.utils = (function() {
RED.clipboard.copyText(msg,copyPayload,"clipboard.copyMessageValue"); RED.clipboard.copyText(msg,copyPayload,"clipboard.copyMessageValue");
}) })
RED.popover.tooltip(copyPayload,RED._("node-red:debug.sidebar.copyPayload")); RED.popover.tooltip(copyPayload,RED._("node-red:debug.sidebar.copyPayload"));
if (enablePinning && strippedKey !== undefined && strippedKey !== '') { if (strippedKey !== undefined && strippedKey !== '') {
var isPinned = pinnedPaths[sourceId].hasOwnProperty(strippedKey); var isPinned = pinnedPaths[sourceId].hasOwnProperty(strippedKey);
var pinPath = $('<button class="red-ui-button red-ui-button-small red-ui-debug-msg-tools-pin"><i class="fa fa-map-pin"></i></button>').appendTo(tools).on("click", function(e) { var pinPath = $('<button class="red-ui-button red-ui-button-small red-ui-debug-msg-tools-pin"><i class="fa fa-map-pin"></i></button>').appendTo(tools).on("click", function(e) {
@ -281,16 +281,13 @@ RED.utils = (function() {
} }
} }
} }
function checkExpanded(strippedKey, expandPaths, { minRange, maxRange, expandLeafNodes }) { function checkExpanded(strippedKey,expandPaths,minRange,maxRange) {
if (expandPaths && expandPaths.length > 0) { if (expandPaths && expandPaths.length > 0) {
if (strippedKey === '' && minRange === undefined) { if (strippedKey === '' && minRange === undefined) {
return true; return true;
} }
for (var i=0;i<expandPaths.length;i++) { for (var i=0;i<expandPaths.length;i++) {
var p = expandPaths[i]; var p = expandPaths[i];
if (expandLeafNodes && p === strippedKey) {
return true
}
if (p.indexOf(strippedKey) === 0 && (p[strippedKey.length] === "." || p[strippedKey.length] === "[") ) { if (p.indexOf(strippedKey) === 0 && (p[strippedKey.length] === "." || p[strippedKey.length] === "[") ) {
if (minRange !== undefined && p[strippedKey.length] === "[") { if (minRange !== undefined && p[strippedKey.length] === "[") {
@ -397,8 +394,6 @@ RED.utils = (function() {
var sourceId = options.sourceId; var sourceId = options.sourceId;
var rootPath = options.rootPath; var rootPath = options.rootPath;
var expandPaths = options.expandPaths; var expandPaths = options.expandPaths;
const enablePinning = options.enablePinning
const expandLeafNodes = options.expandLeafNodes;
var ontoggle = options.ontoggle; var ontoggle = options.ontoggle;
var exposeApi = options.exposeApi; var exposeApi = options.exposeApi;
var tools = options.tools; var tools = options.tools;
@ -421,11 +416,11 @@ RED.utils = (function() {
} }
header = $('<span class="red-ui-debug-msg-row"></span>').appendTo(element); header = $('<span class="red-ui-debug-msg-row"></span>').appendTo(element);
if (sourceId) { if (sourceId) {
addMessageControls(header,sourceId,path,obj,rootPath,strippedKey,tools, enablePinning); addMessageControls(header,sourceId,path,obj,rootPath,strippedKey,tools);
} }
if (!key) { if (!key) {
element.addClass("red-ui-debug-msg-top-level"); element.addClass("red-ui-debug-msg-top-level");
if (sourceId && !expandPaths) { if (sourceId) {
var pinned = pinnedPaths[sourceId]; var pinned = pinnedPaths[sourceId];
expandPaths = []; expandPaths = [];
if (pinned) { if (pinned) {
@ -481,7 +476,7 @@ RED.utils = (function() {
$('<span class="red-ui-debug-msg-type-meta red-ui-debug-msg-object-type-header"></span>').text(typeHint||'string').appendTo(header); $('<span class="red-ui-debug-msg-type-meta red-ui-debug-msg-object-type-header"></span>').text(typeHint||'string').appendTo(header);
var row = $('<div class="red-ui-debug-msg-object-entry collapsed"></div>').appendTo(element); var row = $('<div class="red-ui-debug-msg-object-entry collapsed"></div>').appendTo(element);
$('<pre class="red-ui-debug-msg-type-string"></pre>').text(obj).appendTo(row); $('<pre class="red-ui-debug-msg-type-string"></pre>').text(obj).appendTo(row);
},function(state) {if (ontoggle) { ontoggle(path,state);}}, checkExpanded(strippedKey, expandPaths, { expandLeafNodes })); },function(state) {if (ontoggle) { ontoggle(path,state);}}, checkExpanded(strippedKey,expandPaths));
} }
e = $('<span class="red-ui-debug-msg-type-string red-ui-debug-msg-object-header"></span>').html('"'+formatString(sanitize(obj))+'"').appendTo(entryObj); e = $('<span class="red-ui-debug-msg-type-string red-ui-debug-msg-object-header"></span>').html('"'+formatString(sanitize(obj))+'"').appendTo(entryObj);
if (/^#[0-9a-f]{6}$/i.test(obj)) { if (/^#[0-9a-f]{6}$/i.test(obj)) {
@ -597,16 +592,14 @@ RED.utils = (function() {
typeHint: type==='buffer'?'hex':false, typeHint: type==='buffer'?'hex':false,
hideKey: false, hideKey: false,
path: path+"["+i+"]", path: path+"["+i+"]",
sourceId, sourceId: sourceId,
rootPath, rootPath: rootPath,
expandPaths, expandPaths: expandPaths,
expandLeafNodes, ontoggle: ontoggle,
ontoggle, exposeApi: exposeApi,
exposeApi,
// tools: tools // Do not pass tools down as we // tools: tools // Do not pass tools down as we
// keep them attached to the top-level header // keep them attached to the top-level header
nodeSelector: options.nodeSelector, nodeSelector: options.nodeSelector,
enablePinning
} }
).appendTo(row); ).appendTo(row);
} }
@ -630,23 +623,21 @@ RED.utils = (function() {
typeHint: type==='buffer'?'hex':false, typeHint: type==='buffer'?'hex':false,
hideKey: false, hideKey: false,
path: path+"["+i+"]", path: path+"["+i+"]",
sourceId, sourceId: sourceId,
rootPath, rootPath: rootPath,
expandPaths, expandPaths: expandPaths,
expandLeafNodes, ontoggle: ontoggle,
ontoggle, exposeApi: exposeApi,
exposeApi,
// tools: tools // Do not pass tools down as we // tools: tools // Do not pass tools down as we
// keep them attached to the top-level header // keep them attached to the top-level header
nodeSelector: options.nodeSelector, nodeSelector: options.nodeSelector,
enablePinning
} }
).appendTo(row); ).appendTo(row);
} }
} }
})(), })(),
(function() { var path = path+"["+i+"]"; return function(state) {if (ontoggle) { ontoggle(path,state);}}})(), (function() { var path = path+"["+i+"]"; return function(state) {if (ontoggle) { ontoggle(path,state);}}})(),
checkExpanded(strippedKey,expandPaths,{ minRange, maxRange: Math.min(fullLength-1,(minRange+9)), expandLeafNodes})); checkExpanded(strippedKey,expandPaths,minRange,Math.min(fullLength-1,(minRange+9))));
$('<span class="red-ui-debug-msg-object-key"></span>').html("["+minRange+" &hellip; "+Math.min(fullLength-1,(minRange+9))+"]").appendTo(header); $('<span class="red-ui-debug-msg-object-key"></span>').html("["+minRange+" &hellip; "+Math.min(fullLength-1,(minRange+9))+"]").appendTo(header);
} }
if (fullLength < originalLength) { if (fullLength < originalLength) {
@ -655,7 +646,7 @@ RED.utils = (function() {
} }
}, },
function(state) {if (ontoggle) { ontoggle(path,state);}}, function(state) {if (ontoggle) { ontoggle(path,state);}},
checkExpanded(strippedKey, expandPaths, { expandLeafNodes })); checkExpanded(strippedKey,expandPaths));
} }
} else if (typeof obj === 'object') { } else if (typeof obj === 'object') {
element.addClass('collapsed'); element.addClass('collapsed');
@ -689,16 +680,14 @@ RED.utils = (function() {
typeHint: false, typeHint: false,
hideKey: false, hideKey: false,
path: newPath, path: newPath,
sourceId, sourceId: sourceId,
rootPath, rootPath: rootPath,
expandPaths, expandPaths: expandPaths,
expandLeafNodes, ontoggle: ontoggle,
ontoggle, exposeApi: exposeApi,
exposeApi,
// tools: tools // Do not pass tools down as we // tools: tools // Do not pass tools down as we
// keep them attached to the top-level header // keep them attached to the top-level header
nodeSelector: options.nodeSelector, nodeSelector: options.nodeSelector,
enablePinning
} }
).appendTo(row); ).appendTo(row);
} }
@ -707,7 +696,7 @@ RED.utils = (function() {
} }
}, },
function(state) {if (ontoggle) { ontoggle(path,state);}}, function(state) {if (ontoggle) { ontoggle(path,state);}},
checkExpanded(strippedKey, expandPaths, { expandLeafNodes })); checkExpanded(strippedKey,expandPaths));
} }
if (key) { if (key) {
$('<span class="red-ui-debug-msg-type-meta"></span>').text(type).appendTo(entryObj); $('<span class="red-ui-debug-msg-type-meta"></span>').text(type).appendTo(entryObj);

View File

@ -11,7 +11,7 @@ RED.view.annotations = (function() {
} }
let badgeRDX = 0; let badgeRDX = 0;
let badgeLDX = 0; let badgeLDX = 0;
const scale = RED.view.scale()
for (let i=0,l=evt.el.__annotations__.length;i<l;i++) { for (let i=0,l=evt.el.__annotations__.length;i<l;i++) {
const annotation = evt.el.__annotations__[i]; const annotation = evt.el.__annotations__[i];
if (annotations.hasOwnProperty(annotation.id)) { if (annotations.hasOwnProperty(annotation.id)) {
@ -42,17 +42,15 @@ RED.view.annotations = (function() {
} }
if (isBadge) { if (isBadge) {
if (showAnnotation) { if (showAnnotation) {
// getBoundingClientRect is in real-world scale so needs to be adjusted according to const rect = annotation.element.getBoundingClientRect();
// the current scale factor
const rectWidth = annotation.element.getBoundingClientRect().width / scale;
let annotationX let annotationX
if (!opts.align || opts.align === 'right') { if (!opts.align || opts.align === 'right') {
annotationX = evt.node.w - 3 - badgeRDX - rectWidth annotationX = evt.node.w - 3 - badgeRDX - rect.width
badgeRDX += rectWidth + 4; badgeRDX += rect.width + 4;
} else if (opts.align === 'left') { } else if (opts.align === 'left') {
annotationX = 3 + badgeLDX annotationX = 3 + badgeLDX
badgeLDX += rectWidth + 4; badgeLDX += rect.width + 4;
} }
annotation.element.setAttribute("transform", "translate("+annotationX+", -8)"); annotation.element.setAttribute("transform", "translate("+annotationX+", -8)");
} }

View File

@ -1102,27 +1102,18 @@ RED.view.tools = (function() {
const paletteLabel = RED.utils.getPaletteLabel(n.type, nodeDef) const paletteLabel = RED.utils.getPaletteLabel(n.type, nodeDef)
const defaultNodeNameRE = new RegExp('^'+paletteLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')+' (\\d+)$') const defaultNodeNameRE = new RegExp('^'+paletteLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')+' (\\d+)$')
if (!typeIndex.hasOwnProperty(n.type)) { if (!typeIndex.hasOwnProperty(n.type)) {
const existingNodes = RED.nodes.filterNodes({ type: n.type }); const existingNodes = RED.nodes.filterNodes({type: n.type})
const existingIds = existingNodes.reduce((ids, node) => { let maxNameNumber = 0;
let match = defaultNodeNameRE.exec(node.name); existingNodes.forEach(n => {
let match = defaultNodeNameRE.exec(n.name)
if (match) { if (match) {
const nodeNumber = parseInt(match[1], 10); let nodeNumber = parseInt(match[1])
if (!ids.includes(nodeNumber)) { if (nodeNumber > maxNameNumber) {
ids.push(nodeNumber); maxNameNumber = nodeNumber
} }
} }
return ids; })
}, []).sort((a, b) => a - b); typeIndex[n.type] = maxNameNumber + 1
let availableNameNumber = 1;
for (let i = 0; i < existingIds.length; i++) {
if (existingIds[i] !== availableNameNumber) {
break;
}
availableNameNumber++;
}
typeIndex[n.type] = availableNameNumber;
} }
if ((options.renameBlank && n.name === '') || (options.renameClash && defaultNodeNameRE.test(n.name))) { if ((options.renameBlank && n.name === '') || (options.renameClash && defaultNodeNameRE.test(n.name))) {
if (generateHistory) { if (generateHistory) {
@ -1154,11 +1145,11 @@ RED.view.tools = (function() {
} }
} }
function addJunctionsToWires(options = {}) { function addJunctionsToWires(wires) {
if (RED.workspaces.isLocked()) { if (RED.workspaces.isLocked()) {
return return
} }
let wiresToSplit = options.wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link)); let wiresToSplit = wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link));
if (!wiresToSplit) { if (!wiresToSplit) {
return return
} }
@ -1206,26 +1197,21 @@ RED.view.tools = (function() {
if (links.length === 0) { if (links.length === 0) {
return return
} }
if (addedJunctions.length === 0 && Object.hasOwn(options, 'x') && Object.hasOwn(options, 'y')) { let pointCount = 0
junction.x = options.x links.forEach(function(l) {
junction.y = options.y if (l._sliceLocation) {
} else { junction.x += l._sliceLocation.x
let pointCount = 0 junction.y += l._sliceLocation.y
links.forEach(function(l) { delete l._sliceLocation
if (l._sliceLocation) { pointCount++
junction.x += l._sliceLocation.x } else {
junction.y += l._sliceLocation.y junction.x += l.source.x + l.source.w/2 + l.target.x - l.target.w/2
delete l._sliceLocation junction.y += l.source.y + l.target.y
pointCount++ pointCount += 2
} 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 junction.x = Math.round(junction.x/pointCount)
pointCount += 2 junction.y = Math.round(junction.y/pointCount)
}
})
junction.x = Math.round(junction.x/pointCount)
junction.y = Math.round(junction.y/pointCount)
}
if (RED.view.snapGrid) { if (RED.view.snapGrid) {
let gridSize = RED.view.gridSize() let gridSize = RED.view.gridSize()
junction.x = (gridSize*Math.round(junction.x/gridSize)); junction.x = (gridSize*Math.round(junction.x/gridSize));
@ -1415,7 +1401,7 @@ RED.view.tools = (function() {
RED.actions.add("core:wire-multiple-to-node", function() { wireMultipleToNode() }) 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-wire-with-link-nodes", function () { splitWiresWithLinkNodes() });
RED.actions.add("core:split-wires-with-junctions", function (options) { addJunctionsToWires(options) }); 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

@ -288,7 +288,7 @@ RED.view = (function() {
} }
selectedLinks.clearUnselected() selectedLinks.clearUnselected()
}, },
length: () => groups.size, length: () => groups.length,
forEach: (func) => { groups.forEach(func) }, forEach: (func) => { groups.forEach(func) },
toArray: () => [...groups], toArray: () => [...groups],
clear: function () { clear: function () {
@ -321,8 +321,8 @@ RED.view = (function() {
evt.stopPropagation() evt.stopPropagation()
RED.contextMenu.show({ RED.contextMenu.show({
type: 'workspace', type: 'workspace',
x: evt.clientX, x:evt.clientX-5,
y: evt.clientY y:evt.clientY-5
}) })
return false return false
}) })
@ -1265,6 +1265,11 @@ RED.view = (function() {
var targetGroup = options.group; var targetGroup = options.group;
var touchTrigger = options.touchTrigger; var touchTrigger = options.touchTrigger;
if (targetGroup) {
selectedGroups.add(targetGroup,false);
RED.view.redraw();
}
// `point` is the place in the workspace the mouse has clicked. // `point` is the place in the workspace the mouse has clicked.
// This takes into account scrolling and scaling of the workspace. // This takes into account scrolling and scaling of the workspace.
var ox = point[0]; var ox = point[0];
@ -1586,6 +1591,9 @@ RED.view = (function() {
// auto select dropped node - so info shows (if visible) // auto select dropped node - so info shows (if visible)
clearSelection(); clearSelection();
nn.selected = true; nn.selected = true;
if (targetGroup) {
selectedGroups.add(targetGroup,false);
}
movingSet.add(nn); movingSet.add(nn);
updateActiveNodes(); updateActiveNodes();
updateSelection(); updateSelection();
@ -2170,22 +2178,17 @@ RED.view = (function() {
n.n.moved = true; n.n.moved = true;
} }
} }
// If a node has moved and ends up being spliced into a link, keep
// track of which historyEvent to add the splice info to // Check to see if we need to splice a link
let targetSpliceEvent = null
if (moveEvent.nodes.length > 0) { if (moveEvent.nodes.length > 0) {
historyEvent.events.push(moveEvent) historyEvent.events.push(moveEvent)
targetSpliceEvent = moveEvent if (activeSpliceLink) {
var linkToSplice = d3.select(activeSpliceLink).data()[0];
spliceLink(linkToSplice, movingSet.get(0).n, moveEvent)
}
} }
if (moveAndChangedGroupEvent.nodes.length > 0) { if (moveAndChangedGroupEvent.nodes.length > 0) {
historyEvent.events.push(moveAndChangedGroupEvent) historyEvent.events.push(moveAndChangedGroupEvent)
targetSpliceEvent = moveAndChangedGroupEvent
}
// activeSpliceLink will only be set if the movingSet has a single
// node that is able to splice.
if (targetSpliceEvent && activeSpliceLink) {
var linkToSplice = d3.select(activeSpliceLink).data()[0];
spliceLink(linkToSplice, movingSet.get(0).n, targetSpliceEvent)
} }
// Only continue if something has moved // Only continue if something has moved
@ -2686,21 +2689,22 @@ RED.view = (function() {
addToRemovedLinks(reconnectResult.removedLinks) addToRemovedLinks(reconnectResult.removedLinks)
} }
const startDirty = RED.nodes.dirty(); var startDirty = RED.nodes.dirty();
let movingSelectedGroups = []; var startChanged = false;
var selectedGroups = [];
if (movingSet.length() > 0) { if (movingSet.length() > 0) {
for (var i=0;i<movingSet.length();i++) { for (var i=0;i<movingSet.length();i++) {
node = movingSet.get(i).n; node = movingSet.get(i).n;
if (node.type === "group") { if (node.type === "group") {
movingSelectedGroups.push(node); selectedGroups.push(node);
} }
} }
// Make sure we have identified all groups about to be deleted // Make sure we have identified all groups about to be deleted
for (i=0;i<movingSelectedGroups.length;i++) { for (i=0;i<selectedGroups.length;i++) {
movingSelectedGroups[i].nodes.forEach(function(n) { selectedGroups[i].nodes.forEach(function(n) {
if (n.type === "group" && movingSelectedGroups.indexOf(n) === -1) { if (n.type === "group" && selectedGroups.indexOf(n) === -1) {
movingSelectedGroups.push(n); selectedGroups.push(n);
} }
}) })
} }
@ -2717,7 +2721,7 @@ RED.view = (function() {
addToRemovedLinks(removedEntities.links); addToRemovedLinks(removedEntities.links);
if (node.g) { if (node.g) {
var group = RED.nodes.group(node.g); var group = RED.nodes.group(node.g);
if (movingSelectedGroups.indexOf(group) === -1) { if (selectedGroups.indexOf(group) === -1) {
// Don't use RED.group.removeFromGroup as that emits // Don't use RED.group.removeFromGroup as that emits
// a change event on the node - but we're deleting it // a change event on the node - but we're deleting it
var index = group.nodes.indexOf(node); var index = group.nodes.indexOf(node);
@ -2731,7 +2735,7 @@ RED.view = (function() {
removedLinks = removedLinks.concat(result.links); removedLinks = removedLinks.concat(result.links);
if (node.g) { if (node.g) {
var group = RED.nodes.group(node.g); var group = RED.nodes.group(node.g);
if (movingSelectedGroups.indexOf(group) === -1) { if (selectedGroups.indexOf(group) === -1) {
// Don't use RED.group.removeFromGroup as that emits // Don't use RED.group.removeFromGroup as that emits
// a change event on the node - but we're deleting it // a change event on the node - but we're deleting it
var index = group.nodes.indexOf(node); var index = group.nodes.indexOf(node);
@ -2753,8 +2757,8 @@ RED.view = (function() {
// Groups must be removed in the right order - from inner-most // Groups must be removed in the right order - from inner-most
// to outermost. // to outermost.
for (i = movingSelectedGroups.length-1; i>=0; i--) { for (i = selectedGroups.length-1; i>=0; i--) {
var g = movingSelectedGroups[i]; var g = selectedGroups[i];
removedGroups.push(g); removedGroups.push(g);
RED.nodes.removeGroup(g); RED.nodes.removeGroup(g);
} }
@ -5171,8 +5175,8 @@ RED.view = (function() {
var delta = Infinity; var delta = Infinity;
for (var i = 0; i < lineLength; i++) { for (var i = 0; i < lineLength; i++) {
var linePos = pathLine.getPointAtLength(i); var linePos = pathLine.getPointAtLength(i);
var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor)) var posDeltaX = Math.abs(linePos.x-d3.event.offsetX)
var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor)) var posDeltaY = Math.abs(linePos.y-d3.event.offsetY)
var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY
if (posDelta < delta) { if (posDelta < delta) {
pos = linePos pos = linePos

View File

@ -168,37 +168,6 @@ 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) { if (opts.cancelable) {
$("#node-dialog-login-cancel").button().on("click", function( event ) { $("#node-dialog-login-cancel").button().on("click", function( event ) {
@ -351,10 +320,10 @@ RED.user = (function() {
userIcon.css({ userIcon.css({
backgroundImage: "url("+user.image+")", backgroundImage: "url("+user.image+")",
}) })
} else if (user.anonymous || (!user.username && !user.email)) { } else if (user.anonymous) {
$('<i class="fa fa-user"></i>').appendTo(userIcon); $('<i class="fa fa-user"></i>').appendTo(userIcon);
} else { } else {
$('<span>').text((user.username || user.email).substring(0,2)).appendTo(userIcon); $('<span>').text(user.username.substring(0,2)).appendTo(userIcon);
} }
if (user.profileColor !== undefined) { if (user.profileColor !== undefined) {
userIcon.addClass('red-ui-user-profile-color-' + user.profileColor) userIcon.addClass('red-ui-user-profile-color-' + user.profileColor)

View File

@ -84,11 +84,6 @@ ul.red-ui-sidebar-node-config-list {
background: var(--red-ui-node-config-background); background: var(--red-ui-node-config-background);
color: var(--red-ui-primary-text-color); color: var(--red-ui-primary-text-color);
cursor: pointer; cursor: pointer;
&.red-ui-palette-node-config-invalid.red-ui-palette-node-config-changed {
.red-ui-palette-node-annotations.red-ui-flow-node-error {
left: calc(100% - 28px);
}
}
} }
ul.red-ui-sidebar-node-config-list li.red-ui-palette-node-config-type { ul.red-ui-sidebar-node-config-list li.red-ui-palette-node-config-type {
color: var(--red-ui-secondary-text-color); color: var(--red-ui-secondary-text-color);
@ -120,15 +115,6 @@ ul.red-ui-sidebar-node-config-list li.red-ui-palette-node-config-type {
.red-ui-palette-node-config-invalid { .red-ui-palette-node-config-invalid {
border-color: var(--red-ui-form-input-border-error-color) border-color: var(--red-ui-form-input-border-error-color)
} }
.red-ui-sidebar-config-tray-header.red-ui-palette-header:not(.red-ui-sidebar-config-changed) .red-ui-flow-node-changed {
display: none;
}
.red-ui-sidebar-config-tray-header.red-ui-palette-header.red-ui-sidebar-config-changed .red-ui-flow-node-changed {
display: inline-block;
position: absolute;
top: 1px;
right: 1px;
}
.red-ui-palette-node-annotations { .red-ui-palette-node-annotations {
position: absolute; position: absolute;
left: calc(100% - 15px); left: calc(100% - 15px);

View File

@ -2,15 +2,4 @@
&.red-ui-popover-panel { &.red-ui-popover-panel {
border-top: none; border-top: none;
} }
}
.red-ui-autoComplete-completion {
font-family: var(--red-ui-monospace-font);
white-space: nowrap;
overflow: hidden;
flex-grow: 1;
text-overflow: ellipsis;
direction: rtl;
text-align: left;
} }

View File

@ -148,7 +148,7 @@ module.exports = function(RED) {
var st = (typeof output === 'string') ? output : util.inspect(output); var st = (typeof output === 'string') ? output : util.inspect(output);
var fill = "grey"; var fill = "grey";
var shape = "dot"; var shape = "dot";
if (typeof output === 'object' && output?.fill && output?.shape && output?.text) { if (typeof output === 'object' && hasOwnProperty.call(output, "fill") && hasOwnProperty.call(output, "shape") && hasOwnProperty.call(output, "text")) {
fill = output.fill; fill = output.fill;
shape = output.shape; shape = output.shape;
st = output.text; st = output.text;

View File

@ -511,10 +511,9 @@ RED.debug = (function() {
typeHint: format, typeHint: format,
hideKey: false, hideKey: false,
path: path, path: path,
sourceId: sourceNode && sourceNode.id, sourceId: sourceNode&&sourceNode.id,
rootPath: path, rootPath: path,
nodeSelector: config.messageSourceClick, nodeSelector: config.messageSourceClick,
enablePinning: true
}); });
// Do this in a separate step so the element functions aren't stripped // Do this in a separate step so the element functions aren't stripped
debugMessage.appendTo(el); debugMessage.appendTo(el);

View File

@ -111,6 +111,8 @@ module.exports = function(RED) {
throw new Error(RED._("function.error.externalModuleNotAllowed")); throw new Error(RED._("function.error.externalModuleNotAllowed"));
} }
var functionText = "var results = null;"+ var functionText = "var results = null;"+
"results = (async function(msg,__send__,__done__){ "+ "results = (async function(msg,__send__,__done__){ "+
"var __msgid__ = msg._msgid;"+ "var __msgid__ = msg._msgid;"+
@ -164,13 +166,7 @@ module.exports = function(RED) {
Buffer:Buffer, Buffer:Buffer,
Date: Date, Date: Date,
RED: { RED: {
util: { util: RED.util
...RED.util,
getSetting: function (_node, name, _flow) {
// Ensure `node` argument is the Function node and do not allow flow to be overridden.
return RED.util.getSetting(node, name);
}
}
}, },
__node__: { __node__: {
id: node.id, id: node.id,

View File

@ -352,9 +352,7 @@ module.exports = function(RED) {
if (msgs.length === 0) { if (msgs.length === 0) {
done() done()
} else { } else {
setImmediate(() => { drainMessageGroup(msgs,count,done);
drainMessageGroup(msgs,count,done);
})
} }
} }
}) })
@ -507,9 +505,7 @@ module.exports = function(RED) {
if (err) { if (err) {
node.error(err,nextMsg); node.error(err,nextMsg);
} }
setImmediate(() => { processMessageQueue()
processMessageQueue()
})
}); });
} }

View File

@ -253,13 +253,7 @@ module.exports = function(RED) {
if (node.allowrate && m.hasOwnProperty("rate") && !isNaN(parseFloat(m.rate))) { if (node.allowrate && m.hasOwnProperty("rate") && !isNaN(parseFloat(m.rate))) {
node.rate = m.rate; node.rate = m.rate;
} }
if (msg.hasOwnProperty("reset")) { send(m);
if (msg.hasOwnProperty("flush")) {
node.buffer.push({msg: m, send: send, done: done});
}
}
else { send(m); }
node.reportDepth(); node.reportDepth();
node.intervalID = setInterval(sendMsgFromBuffer, node.rate); node.intervalID = setInterval(sendMsgFromBuffer, node.rate);
done(); done();
@ -291,23 +285,42 @@ module.exports = function(RED) {
} }
} }
else if (!msg.hasOwnProperty("reset")) { else if (!msg.hasOwnProperty("reset")) {
if (node.allowrate && msg.hasOwnProperty("rate") && !isNaN(parseFloat(msg.rate))) { if (maxKeptMsgsCount(node) > 0) {
node.rate = msg.rate; if (node.intervalID === -1) {
} node.send(msg);
var timeSinceLast; node.intervalID = setInterval(sendMsgFromBuffer, node.rate);
if (node.lastSent) { } else {
timeSinceLast = process.hrtime(node.lastSent); if (node.allowrate && msg.hasOwnProperty("rate") && !isNaN(parseFloat(msg.rate)) && node.rate !== msg.rate) {
} node.rate = msg.rate;
if (!node.lastSent) { // ensuring that we always send the first message clearInterval(node.intervalID);
node.lastSent = process.hrtime(); node.intervalID = setInterval(sendMsgFromBuffer, node.rate);
send(msg); }
} if (node.buffer.length < _maxKeptMsgsCount) {
else if ( ( (timeSinceLast[0] * SECONDS_TO_NANOS) + timeSinceLast[1] ) > (node.rate * MILLIS_TO_NANOS) ) { var m = RED.util.cloneMessage(msg);
node.lastSent = process.hrtime(); node.buffer.push({msg: m, send: send, done: done});
send(msg); } else {
} node.trace("dropped due to buffer overflow. msg._msgid = " + msg._msgid);
else if (node.outputs === 2) { node.droppedMsgs++;
send([null,msg]) }
}
} 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])
}
} }
done(); done();
} }

View File

@ -24,14 +24,6 @@ module.exports = function(RED) {
this.op2 = n.op2 || "0"; this.op2 = n.op2 || "0";
this.op1type = n.op1type || "str"; this.op1type = n.op1type || "str";
this.op2type = n.op2type || "str"; this.op2type = n.op2type || "str";
// If the op1/2type is 'date', then we need to leave op1/2 alone so that
// evaluateNodeProperty works as expected.
if (this.op1type === 'date' && this.op1 === '1') {
this.op1 = ''
}
if (this.op2type === 'date' && this.op2 === '0') {
this.op2 = ''
}
this.second = (n.outputs == 2) ? true : false; this.second = (n.outputs == 2) ? true : false;
this.topic = n.topic || "topic"; this.topic = n.topic || "topic";
@ -201,7 +193,7 @@ module.exports = function(RED) {
if (node.op2type !== "nul") { if (node.op2type !== "nul") {
var promise = Promise.resolve(); var promise = Promise.resolve();
msg2 = RED.util.cloneMessage(msg); msg2 = RED.util.cloneMessage(msg);
if (node.op2type === "flow" || node.op2type === "global" || node.op2type === "date") { if (node.op2type === "flow" || node.op2type === "global") {
promise = new Promise((resolve,reject) => { promise = new Promise((resolve,reject) => {
RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => { RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => {
if (err) { if (err) {
@ -221,6 +213,7 @@ module.exports = function(RED) {
} }
else { else {
msg2.payload = node.topics[topic].m2; msg2.payload = node.topics[topic].m2;
if (node.op2type === "date") { msg2.payload = Date.now(); }
if (node.second === true) { msgInfo.send([null,msg2]); } if (node.second === true) { msgInfo.send([null,msg2]); }
else { msgInfo.send(msg2); } else { msgInfo.send(msg2); }
} }

View File

@ -367,21 +367,20 @@ module.exports = function(RED) {
const sendHeadersAlways = node.hdrout === "all" const sendHeadersAlways = node.hdrout === "all"
const sendHeaders = !dontSendHeaders && (sendHeadersOnce || sendHeadersAlways) const sendHeaders = !dontSendHeaders && (sendHeadersOnce || sendHeadersAlways)
const quoteables = [node.sep, node.quo, "\n", "\r"] const quoteables = [node.sep, node.quo, "\n", "\r"]
const templateQuoteables = [node.sep, node.quo, "\n", "\r"] const templateQuoteables = [',', '"', "\n", "\r"]
const templateQuoteablesStrict = [',', '"', "\n", "\r"]
let badTemplateWarnOnce = true let badTemplateWarnOnce = true
const columnStringToTemplateArray = function (col, sep) { const columnStringToTemplateArray = function (col, sep) {
// NOTE: enforce strict column template parsing in RFC4180 mode // NOTE: enforce strict column template parsing in RFC4180 mode
const parsed = csv.parse(col, { separator: sep, quote: node.quo, outputStyle: 'array', strict: true }) const parsed = csv.parse(col, { separator: sep, quote: node.quo, outputStyle: 'array', strict: true })
if (parsed.data?.length === 1) { node.goodtmpl = true } else { node.goodtmpl = false } if (parsed.headers.length > 0) { node.goodtmpl = true } else { node.goodtmpl = false }
return node.goodtmpl ? parsed.data[0] : null return parsed.headers.length ? parsed.headers : null
} }
const templateArrayToColumnString = function (template, keepEmptyColumns, separator = ',', quotables = templateQuoteablesStrict) { const templateArrayToColumnString = function (template, keepEmptyColumns) {
// NOTE: defaults to strict column template parsing (commas and double quotes) // 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 parsed = csv.parse('', {headers: template, headersOnly:true, separator: ',', quote: node.quo, outputStyle: 'array', strict: true })
return keepEmptyColumns return keepEmptyColumns
? parsed.headers.map(e => addQuotes(e || '', { separator, quoteables: quotables })).join(separator) ? parsed.headers.map(e => addQuotes(e || '', { separator: ',', quoteables: templateQuoteables}))
: parsed.header // exclues empty columns : parsed.header // exclues empty columns
// TODO: resolve inconsistency between CSV->JSON and JSON->CSV // TODO: resolve inconsistency between CSV->JSON and JSON->CSV
// CSV->JSON: empty columns are excluded // CSV->JSON: empty columns are excluded
@ -448,7 +447,7 @@ module.exports = function(RED) {
template = Object.keys(inputData[0]) || [''] template = Object.keys(inputData[0]) || ['']
} }
} }
stringBuilder.push(templateArrayToColumnString(template, true, node.sep, templateQuoteables)) // use user set separator for output data. stringBuilder.push(templateArrayToColumnString(template, true))
if (sendHeadersOnce) { node.hdrSent = true } if (sendHeadersOnce) { node.hdrSent = true }
} }
@ -484,7 +483,6 @@ module.exports = function(RED) {
node.warn(RED._("csv.errors.obj_csv")) node.warn(RED._("csv.errors.obj_csv"))
badTemplateWarnOnce = false badTemplateWarnOnce = false
} }
template = Object.keys(row) || ['']
const rowData = [] const rowData = []
for (let header in inputData[0]) { for (let header in inputData[0]) {
if (row.hasOwnProperty(header)) { if (row.hasOwnProperty(header)) {
@ -520,7 +518,7 @@ module.exports = function(RED) {
// join lines, don't forget to add the last new line // join lines, don't forget to add the last new line
msg.payload = stringBuilder.join(node.ret) + node.ret msg.payload = stringBuilder.join(node.ret) + node.ret
msg.columns = templateArrayToColumnString(template) // always strict commas + double quotes for msg.columns = templateArrayToColumnString(template)
if (msg.payload !== '') { send(msg) } if (msg.payload !== '') { send(msg) }
done() done()
} }
@ -617,15 +615,16 @@ module.exports = function(RED) {
} }
if (msg.parts.index + 1 === msg.parts.count) { if (msg.parts.index + 1 === msg.parts.count) {
msg.payload = node.store msg.payload = node.store
// msg.columns = csvParseResult.header msg.columns = csvParseResult.header
msg.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes for msg.columns // msg._mode = 'RFC4180 mode'
delete msg.parts delete msg.parts
send(msg) send(msg)
node.store = [] node.store = []
} }
} }
else { else {
msg.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes for msg.columns msg.columns = csvParseResult.header
// msg._mode = 'RFC4180 mode'
msg.payload = data msg.payload = data
send(msg); // finally send the array send(msg); // finally send the array
} }
@ -634,8 +633,7 @@ module.exports = function(RED) {
const len = data.length const len = data.length
for (let row = 0; row < len; row++) { for (let row = 0; row < len; row++) {
const newMessage = RED.util.cloneMessage(msg) 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] newMessage.payload = data[row]
if (!has_parts) { if (!has_parts) {
newMessage.parts = { newMessage.parts = {

View File

@ -339,7 +339,7 @@ module.exports = function(RED) {
} }
else { else {
msg.filename = filename; msg.filename = filename;
const bufferArray = []; var lines = Buffer.from([]);
var spare = ""; var spare = "";
var count = 0; var count = 0;
var type = "buffer"; var type = "buffer";
@ -397,7 +397,7 @@ module.exports = function(RED) {
} }
} }
else { else {
bufferArray.push(chunk); lines = Buffer.concat([lines,chunk]);
} }
} }
}) })
@ -413,11 +413,10 @@ module.exports = function(RED) {
}) })
.on('end', function() { .on('end', function() {
if (node.chunk === false) { if (node.chunk === false) {
const buffer = Buffer.concat(bufferArray);
if (node.format === "utf8") { if (node.format === "utf8") {
msg.payload = decode(buffer, node.encoding); msg.payload = decode(lines, node.encoding);
} }
else { msg.payload = buffer; } else { msg.payload = lines; }
nodeSend(msg); nodeSend(msg);
} }
else if (node.format === "lines") { else if (node.format === "lines") {

View File

@ -39,36 +39,10 @@
<dd><b>MQTTv5</b>: Ablaufzeit der Nachricht in Sekunden.</dd> <dd><b>MQTTv5</b>: Ablaufzeit der Nachricht in Sekunden.</dd>
</dl> </dl>
<h3>Details</h3> <h3>Details</h3>
<p>Das abonnierte Topic darf MQTT-Platzhalterzeichen (wildcards) enthalten (+ für eine Ebene und # für mehrere Ebenen).</p> <p>Das abonnierte Topic darf MQTT-Platzhalterzeichen (wildcards) enthalten (+ für eine Ebene und # für mehrere Ebenen).</p>
<p>Diese Node erfordert eine Verbindung zu einem MQTT-Broker, der über die Auswahlliste selektiert werden kann. Eine neue Verbindung wird durch Klicken auf das Stiftsymbol erstellt.</p> <p>Dieser Node erfordert eine Verbindung zu einem MQTT-Broker, der über die Auswahlliste selektiert werden kann.
Eine neue Verbindung wird durch Klicken auf das Stiftsymbol erstellt.</p>
<p>Mehrere MQTT-Nodes (in oder out) können bei Bedarf dieselbe Broker-Verbindung nutzen.</p> <p>Mehrere MQTT-Nodes (in oder out) können bei Bedarf dieselbe Broker-Verbindung nutzen.</p>
<h4>Dynamische Steuerung</h4>
Die von der Node genutzte Verbindung kann dynamisch gesteuert werden, wenn die MQTT-Node eine der folgenden Nachrichten erhält. Die Payload dieser Nachrichten werden nicht veröffentlicht.
<h4>Eingangsdaten</h4>
<p>Nur Verfügbar, wenn die Node für dynamische Abonnements konfiguriert wurde.</p>
<dl class="message-properties">
<dt>action <span class="property-type">string</span></dt>
<dd>Der Name der Aktion, die die MQTT-Node ausführen soll. Verfügbare Aktionen sind: <code>"connect"</code>, <code>"disconnect"</code>, <code>"getSubscriptions"</code>, <code>"subscribe"</code> und <code>"unsubscribe"</code>.</dd>
<dt class="optional">topic <span class="property-type">string|object|array</span></dt>
<dd>Bei den Aktionen <code>"subscribe"</code> und <code>"unsubscribe"</code> gibt diese Eigenschaft die MQTT-Topic an. Dabei kann es sich um Folgendes handeln:
<ul>
<li>eine Zeichenfolge, die den Topic-Filter enthält</li>
<li>ein Objekt mit den Eigenschaften <code>topic</code> und <code>qos</code></li>
<li>ein Array aus Zeichenfolgen oder Objekten, um mehrere Topics gleichzeitig zu verwalten</li>
</ul>
</dd>
<dt class="optional">broker <span class="property-type">broker</span> </dt>
<dd>Für die Aktion <code>"connect"</code> kann diese Eigenschaft jede der einzelnen Broker-Konfigurationseinstellungen überschreiben, einschließlich: <ul>
<li><code>broker</code></li>
<li><code>port</code></li>
<li><code>url</code> - überschreibt Broker/Port, um eine vollständige Verbindungs-URL bereitzustellen</li>
<li><code>username</code></li>
<li><code>password</code></li>
</ul>
<p>Wenn diese Eigenschaft gesetzt ist und der Broker bereits verbunden ist, wird ein Fehler protokolliert, es sei denn, die Eigenschaft <code>force</code> gesetzt - in diesem Fall wird die Verbindung zum Broker getrennt, die neuen Einstellungen angewendet und erneut verbunden.</p>
</dd>
</dl>
</script> </script>
<script type="text/html" data-help-name="mqtt out"> <script type="text/html" data-help-name="mqtt out">

View File

@ -48,8 +48,7 @@
<dl class="message-properties"> <dl class="message-properties">
<dt>action <span class="property-type">string</span></dt> <dt>action <span class="property-type">string</span></dt>
<dd>the name of the action the node should perform. Available actions are: <code>"connect"</code>, <dd>the name of the action the node should perform. Available actions are: <code>"connect"</code>,
<code>"disconnect"</code>, <code>"getSubscriptions"</code>, <code>"subscribe"</code> and <code>"disconnect"</code>, <code>"subscribe"</code> and <code>"unsubscribe"</code>.</dd>
<code>"unsubscribe"</code>.</dd>
<dt class="optional">topic <span class="property-type">string|object|array</span></dt> <dt class="optional">topic <span class="property-type">string|object|array</span></dt>
<dd>For the <code>"subscribe"</code> and <code>"unsubscribe"</code> actions, this property <dd>For the <code>"subscribe"</code> and <code>"unsubscribe"</code> actions, this property
provides the topic. It can be set as either:<ul> provides the topic. It can be set as either:<ul>

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/nodes", "name": "@node-red/nodes",
"version": "4.0.9", "version": "4.0.5",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -144,7 +144,7 @@ async function installModule(module,version,url) {
if (url) { if (url) {
if (pkgurlRe.test(url) || localtgzRe.test(url)) { if (pkgurlRe.test(url) || localtgzRe.test(url)) {
// Git remote url or Tarball url - check the valid package url // Git remote url or Tarball url - check the valid package url
installName = localtgzRe.test(url) && slashRe.test(url) ? `"${url}"` : url; installName = url;
isRegistryPackage = false; isRegistryPackage = false;
} else { } else {
log.warn(log._("server.install.install-failed-url",{name:module,url:url})); log.warn(log._("server.install.install-failed-url",{name:module,url:url}));

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/registry", "name": "@node-red/registry",
"version": "4.0.9", "version": "4.0.5",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,7 +16,7 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/util": "4.0.9", "@node-red/util": "4.0.5",
"clone": "2.1.2", "clone": "2.1.2",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"semver": "7.6.3", "semver": "7.6.3",

View File

@ -96,11 +96,7 @@ var api = module.exports = {
} else if (scope === 'node') { } else if (scope === 'node') {
var node = runtime.nodes.getNode(id); var node = runtime.nodes.getNode(id);
if (node) { if (node) {
if (/^subflow:/.test(node.type)) { ctx = node.context();
ctx = runtime.nodes.getContext(node.id);
} else {
ctx = node.context();
}
} }
} }
if (ctx) { if (ctx) {
@ -108,25 +104,13 @@ var api = module.exports = {
store = store || availableStores.default; store = store || availableStores.default;
ctx.get(key,store,function(err, v) { ctx.get(key,store,function(err, v) {
if (opts.keysOnly) { if (opts.keysOnly) {
const result = {}
if (Array.isArray(v)) { if (Array.isArray(v)) {
result.format = `array[${v.length}]` resolve({ [store]: { format: `array[${v.length}]`}})
} else if (typeof v === 'object') { } else if (typeof v === 'object') {
result.keys = Object.keys(v).map(k => { resolve({ [store]: { keys: Object.keys(v), format: 'Object' } })
if (Array.isArray(v[k])) {
return { key: k, format: `array[${v[k].length}]`, length: v[k].length }
} else if (typeof v[k] === 'object') {
return { key: k, format: 'object' }
} else {
return { key: k }
}
})
result.format = 'object'
} else { } else {
result.keys = [] resolve({ [store]: { keys: [] }})
} }
resolve({ [store]: result })
return
} }
var encoded = util.encodeObject({msg:v}); var encoded = util.encodeObject({msg:v});
if (store !== availableStores.default) { if (store !== availableStores.default) {
@ -163,7 +147,7 @@ var api = module.exports = {
} }
return return
} }
result[store] = { keys: keys.map(key => { return { key }}) } result[store] = { keys }
c--; c--;
if (c === 0) { if (c === 0) {
if (!errorReported) { if (!errorReported) {

View File

@ -719,14 +719,6 @@ class Flow {
}); });
} }
getContext(scope) {
if (scope === 'flow') {
return this.context
} else if (scope === 'global') {
return context.get('global')
}
}
dump() { dump() {
console.log("==================") console.log("==================")
console.log(this.TYPE, this.id); console.log(this.TYPE, this.id);

View File

@ -49,14 +49,6 @@ class Group {
} }
return this.parent.getSetting(key); return this.parent.getSetting(key);
} }
error(msg) {
this.parent.error(msg);
}
getContext(scope) {
return this.parent.getContext(scope);
}
} }
module.exports = { module.exports = {

View File

@ -100,24 +100,7 @@ async function evaluateEnvProperties(flow, env, credentials) {
} }
} else if (type ==='jsonata') { } else if (type ==='jsonata') {
pendingEvaluations.push(new Promise((resolve, _) => { pendingEvaluations.push(new Promise((resolve, _) => {
redUtil.evaluateNodeProperty(value, 'jsonata',{ redUtil.evaluateNodeProperty(value, 'jsonata', {_flow: flow}, null, (err, result) => {
// Fake a node object to provide access to _flow and context
_flow: flow,
context: () => {
return {
flow: {
get: (value, store, callback) => {
return flow.getContext('flow').get(value, store, callback)
}
},
global: {
get: (value, store, callback) => {
return flow.getContext('global').get(value, store, callback)
}
}
}
}
}, null, (err, result) => {
if (!err) { if (!err) {
if (typeof result === 'object') { if (typeof result === 'object') {
result = { value: result, __clone__: true} result = { value: result, __clone__: true}
@ -130,10 +113,6 @@ async function evaluateEnvProperties(flow, env, credentials) {
resolve() 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 { } else {
try { try {
value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null); value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null);

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/runtime", "name": "@node-red/runtime",
"version": "4.0.9", "version": "4.0.5",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,11 +16,11 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/registry": "4.0.9", "@node-red/registry": "4.0.5",
"@node-red/util": "4.0.9", "@node-red/util": "4.0.5",
"async-mutex": "0.5.0", "async-mutex": "0.5.0",
"clone": "2.1.2", "clone": "2.1.2",
"express": "4.21.2", "express": "4.21.1",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"json-stringify-safe": "5.0.1", "json-stringify-safe": "5.0.1",
"rfdc": "^1.3.1" "rfdc": "^1.3.1"

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/util", "name": "@node-red/util",
"version": "4.0.9", "version": "4.0.5",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,6 +1,6 @@
{ {
"name": "node-red", "name": "node-red",
"version": "4.0.9", "version": "4.0.5",
"description": "Low-code programming for event-driven applications", "description": "Low-code programming for event-driven applications",
"homepage": "https://nodered.org", "homepage": "https://nodered.org",
"license": "Apache-2.0", "license": "Apache-2.0",
@ -31,14 +31,14 @@
"flow" "flow"
], ],
"dependencies": { "dependencies": {
"@node-red/editor-api": "4.0.9", "@node-red/editor-api": "4.0.5",
"@node-red/runtime": "4.0.9", "@node-red/runtime": "4.0.5",
"@node-red/util": "4.0.9", "@node-red/util": "4.0.5",
"@node-red/nodes": "4.0.9", "@node-red/nodes": "4.0.5",
"basic-auth": "2.0.1", "basic-auth": "2.0.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"cors": "2.8.5", "cors": "2.8.5",
"express": "4.21.2", "express": "4.21.1",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"node-red-admin": "^4.0.1", "node-red-admin": "^4.0.1",
"nopt": "5.0.0", "nopt": "5.0.0",

View File

@ -1009,29 +1009,6 @@ 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 */ /* Messaging API support */
function mapiDoneTestHelper(done, pauseType, drop, msgAndTimings) { function mapiDoneTestHelper(done, pauseType, drop, msgAndTimings) {
const completeNode = require("nr-test-utils").require("@node-red/nodes/core/common/24-complete.js"); const completeNode = require("nr-test-utils").require("@node-red/nodes/core/common/24-complete.js");

View File

@ -111,15 +111,7 @@ describe('trigger node', function() {
try { try {
if (rval) { if (rval) {
msg.should.have.property("payload"); msg.should.have.property("payload");
if (type == "date" && val == "1") { should.deepEqual(msg.payload, rval);
should.deepEqual(Math.round(msg.payload/1000000), Math.round(Date.now()/1000000));
}
else if (type == "date" && val == "iso") {
should.deepEqual(msg.payload.substr(0,11), rval.substr(0,11));
}
else {
should.deepEqual(msg.payload, rval);
}
} }
else { else {
msg.should.have.property("payload", val); msg.should.have.property("payload", val);
@ -134,7 +126,6 @@ describe('trigger node', function() {
}); });
it('should output 2st value when triggered ('+type+')', function(done) { it('should output 2st value when triggered ('+type+')', function(done) {
if (type == "date" && val == "1") { val = "0"; }
var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:"foo", op1type:"str", op2:val, op2type:type, duration:"20", wires:[["n2"]] }, var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:"foo", op1type:"str", op2:val, op2type:type, duration:"20", wires:[["n2"]] },
{id:"n2", type:"helper"} ]; {id:"n2", type:"helper"} ];
process.env[val] = rval; process.env[val] = rval;
@ -151,15 +142,7 @@ describe('trigger node', function() {
else { else {
if (rval) { if (rval) {
msg.should.have.property("payload"); msg.should.have.property("payload");
if (type == "date" && val == "0") { should.deepEqual(msg.payload, rval);
;(Math.round(msg.payload/1000000)).should.be.approximately(parseInt(Date.now()/1000000), 1);
}
else if (type == "date" && val == "iso") {
should.deepEqual(msg.payload.substr(0,11), rval.substr(0,11));
}
else {
should.deepEqual(msg.payload, rval);
}
} }
else { else {
msg.should.have.property("payload", val); msg.should.have.property("payload", val);
@ -183,9 +166,6 @@ describe('trigger node', function() {
var val_buf = "[1,2,3,4,5]"; var val_buf = "[1,2,3,4,5]";
basicTest("bin", val_buf, Buffer.from(JSON.parse(val_buf))); basicTest("bin", val_buf, Buffer.from(JSON.parse(val_buf)));
basicTest("env", "NR-TEST", "env-val"); basicTest("env", "NR-TEST", "env-val");
basicTest("date", "1", Date.now());
basicTest("date", "iso", (new Date()).toISOString());
// basicTest("date", "object", Date.now());
it('should output 1 then 0 when triggered (default)', function(done) { it('should output 1 then 0 when triggered (default)', function(done) {
var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", duration:"20", wires:[["n2"]] }, var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", duration:"20", wires:[["n2"]] },

View File

@ -2067,27 +2067,6 @@ describe('CSV node (RFC Mode)', function () {
n2.on("input", function (msg) { n2.on("input", function (msg) {
try { try {
msg.should.have.property('payload', '1\tfoo\t"ba""r"\tdi,ng\n'); 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(); done();
} catch (e) { } catch (e) {
done(e); done(e);
@ -2107,7 +2086,6 @@ describe('CSV node (RFC Mode)', function () {
n2.on("input", function (msg) { n2.on("input", function (msg) {
try { try {
msg.should.have.property('payload', '4,foo,true,,0\n'); 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(); done();
} catch (e) { } catch (e) {
done(e); done(e);
@ -2128,7 +2106,6 @@ describe('CSV node (RFC Mode)', function () {
try { try {
// 'payload', 'a"a,b\'b\nA1,B1\nA2,B2\n'); // Legacy // '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('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(); done();
} catch (e) { } catch (e) {
done(e); done(e);
@ -2194,7 +2171,6 @@ describe('CSV node (RFC Mode)', function () {
n2.on("input", function (msg) { n2.on("input", function (msg) {
try { try {
msg.should.have.property('payload', '1,3,2,4\n4,2,3,1\n'); 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(); done();
} }
catch (e) { done(e); } catch (e) { done(e); }
@ -2213,7 +2189,6 @@ describe('CSV node (RFC Mode)', function () {
n2.on("input", function (msg) { n2.on("input", function (msg) {
try { try {
msg.should.have.property('payload', 'd,b,c,a\n1,3,2,4\n4,"f\ng",3,1\n'); 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(); done();
} }
catch (e) { done(e); } catch (e) { done(e); }
@ -2233,7 +2208,6 @@ describe('CSV node (RFC Mode)', function () {
try { try {
// 'payload', ',0,1,foo,"ba""r","di,ng","fa\nba"\n'); // '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('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(); done();
} }
catch (e) { done(e); } catch (e) { done(e); }
@ -2353,7 +2327,6 @@ describe('CSV node (RFC Mode)', function () {
n2.on("input", function (msg) { n2.on("input", function (msg) {
try { try {
msg.should.have.property('payload', '{},"text,with,commas","This ""is"" a banana","{""sub"":""object""}"\n'); 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(); done();
} }
catch (e) { done(e); } catch (e) { done(e); }

View File

@ -258,29 +258,6 @@ describe('nodes/registry/installer', function() {
}).catch(done); }).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) { it("triggers preInstall and postInstall hooks", function(done) {
let receivedPreEvent,receivedPostEvent; let receivedPreEvent,receivedPostEvent;
hooks.add("preInstall", function(event) { event.args = ["a"]; receivedPreEvent = event; }) hooks.add("preInstall", function(event) { event.args = ["a"]; receivedPreEvent = event; })