mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/master'
This commit is contained in:
		| @@ -1,4 +0,0 @@ | |||||||
| /Gruntfile.js |  | ||||||
| /.git/* |  | ||||||
| *.backup |  | ||||||
| /public/* |  | ||||||
							
								
								
									
										47
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,3 +1,50 @@ | |||||||
|  | #### 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 | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								nodemon.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								nodemon.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | { | ||||||
|  |     "ignoreRoot": [ | ||||||
|  |         ".git", | ||||||
|  |         ".nyc_output", | ||||||
|  |         ".sass-cache", | ||||||
|  |         "bower-components", | ||||||
|  |         "coverage" | ||||||
|  |     ], | ||||||
|  |     "ignore": [ | ||||||
|  |         "/Gruntfile.js", | ||||||
|  |         "/.git/*", | ||||||
|  |         "*.backup", | ||||||
|  |         "/public/*" | ||||||
|  |     ] | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "node-red", |     "name": "node-red", | ||||||
|     "version": "4.0.5", |     "version": "4.0.8", | ||||||
|     "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", | ||||||
|   | |||||||
| @@ -126,6 +126,14 @@ 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); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -206,13 +206,25 @@ 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) { | ||||||
|                     themeContext.login = { |                     themeContextLogin.image = url | ||||||
|                         image: url |                     hasLoginTheme = true | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |             if (theme.login.message) { | ||||||
|  |                 themeContextLogin.message = theme.login.message | ||||||
|  |                 hasLoginTheme = true | ||||||
|  |             } | ||||||
|  |             if (theme.login.button) { | ||||||
|  |                 themeContextLogin.button = theme.login.button | ||||||
|  |                 hasLoginTheme = true | ||||||
|  |             } | ||||||
|  |             if (hasLoginTheme) { | ||||||
|  |                 themeContext.login = themeContextLogin | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         themeApp.get("/", async function(req,res) { |         themeApp.get("/", async function(req,res) { | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "@node-red/editor-api", |     "name": "@node-red/editor-api", | ||||||
|     "version": "4.0.5", |     "version": "4.0.8", | ||||||
|     "license": "Apache-2.0", |     "license": "Apache-2.0", | ||||||
|     "main": "./lib/index.js", |     "main": "./lib/index.js", | ||||||
|     "repository": { |     "repository": { | ||||||
| @@ -16,8 +16,8 @@ | |||||||
|         } |         } | ||||||
|     ], |     ], | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|         "@node-red/util": "4.0.5", |         "@node-red/util": "4.0.8", | ||||||
|         "@node-red/editor-client": "4.0.5", |         "@node-red/editor-client": "4.0.8", | ||||||
|         "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", | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "@node-red/editor-client", |     "name": "@node-red/editor-client", | ||||||
|     "version": "4.0.5", |     "version": "4.0.8", | ||||||
|     "license": "Apache-2.0", |     "license": "Apache-2.0", | ||||||
|     "repository": { |     "repository": { | ||||||
|         "type": "git", |         "type": "git", | ||||||
|   | |||||||
| @@ -453,10 +453,61 @@ 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; | ||||||
|  |  | ||||||
| @@ -536,6 +587,24 @@ 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++) { | ||||||
|   | |||||||
| @@ -707,12 +707,15 @@ 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] = n; |             configNodes[n.id] = newNode; | ||||||
|         } 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) { | ||||||
| @@ -774,9 +777,11 @@ 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)) { | ||||||
| @@ -785,6 +790,9 @@ 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)) { | ||||||
| @@ -798,10 +806,6 @@ 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) |  | ||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
| @@ -1798,9 +1802,20 @@ RED.nodes = (function() { | |||||||
|         // Replace config nodes |         // Replace config nodes | ||||||
|         // |         // | ||||||
|         configNodeIds.forEach(function(id) { |         configNodeIds.forEach(function(id) { | ||||||
|             removedNodes = removedNodes.concat(convertNode(getNode(id))); |             const configNode = getNode(id); | ||||||
|  |             const currentUserCount = configNode.users; | ||||||
|  |  | ||||||
|  |             // Add a snapshot of the Config Node | ||||||
|  |             removedNodes = removedNodes.concat(convertNode(configNode)); | ||||||
|  |  | ||||||
|  |             // Remove the Config Node instance | ||||||
|             removeNode(id); |             removeNode(id); | ||||||
|             importNodes([newConfigNodes[id]]) |  | ||||||
|  |             // Import the new one | ||||||
|  |             importNodes([newConfigNodes[id]]); | ||||||
|  |  | ||||||
|  |             // Re-attributes the user count | ||||||
|  |             getNode(id).users = currentUserCount; | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         return { |         return { | ||||||
| @@ -2084,6 +2099,8 @@ 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]; | ||||||
| @@ -2143,7 +2160,8 @@ 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; | ||||||
| @@ -2158,6 +2176,9 @@ 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')) { | ||||||
| @@ -2174,11 +2195,55 @@ RED.nodes = (function() { | |||||||
|                         configNode.id = getID(); |                         configNode.id = getID(); | ||||||
|                     } |                     } | ||||||
|                     node_map[n.id] = configNode; |                     node_map[n.id] = configNode; | ||||||
|                     new_nodes.push(configNode); |                     pendingConfigNodes.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]; | ||||||
| @@ -2349,29 +2414,31 @@ RED.nodes = (function() { | |||||||
|                             node.type = "unknown"; |                             node.type = "unknown"; | ||||||
|                         } |                         } | ||||||
|                         if (node._def.category != "config") { |                         if (node._def.category != "config") { | ||||||
|                             if (n.hasOwnProperty('inputs')) { |                             if (n.hasOwnProperty('inputs') && node._def.defaults.hasOwnProperty("inputs")) { | ||||||
|                                 node.inputs = n.inputs; |                                 node.inputs = parseInt(n.inputs, 10); | ||||||
|                                 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')) { |                             if (n.hasOwnProperty('outputs') && node._def.defaults.hasOwnProperty("outputs")) { | ||||||
|                                 node.outputs = n.outputs; |                                 node.outputs = parseInt(n.outputs, 10); | ||||||
|                                 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) { |  | ||||||
|                                 if (!node._def.defaults.hasOwnProperty("outputs") || !isNaN(parseInt(n.outputs))) { |  | ||||||
|                                     // If 'wires' is longer than outputs, clip wires |  | ||||||
|                                     console.log("Warning: node.wires longer than node.outputs - trimming wires:",node.id," wires:",node.wires.length," outputs:",node.outputs); |  | ||||||
|                                     node.wires = node.wires.slice(0,node.outputs); |  | ||||||
|                                 } else { |  | ||||||
|                             // The node declares outputs in its defaults, but has not got a valid value |                             // The node declares outputs in its defaults, but has not got a valid value | ||||||
|                             // Defer to the length of the wires array |                             // Defer to the length of the wires array | ||||||
|  |                             if (node.hasOwnProperty('wires')) { | ||||||
|  |                                 if (isNaN(node.outputs)) { | ||||||
|                                     node.outputs = node.wires.length; |                                     node.outputs = node.wires.length; | ||||||
|  |                                 } else if (node.wires.length > node.outputs) { | ||||||
|  |                                     // If 'wires' is longer than outputs, clip wires | ||||||
|  |                                     console.log("Warning: node.wires longer than node.outputs - trimming wires:", node.id, " wires:", node.wires.length, " outputs:", node.outputs); | ||||||
|  |                                     node.wires = node.wires.slice(0, node.outputs); | ||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
|  |  | ||||||
|                             for (d in node._def.defaults) { |                             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]; | ||||||
| @@ -2468,11 +2535,6 @@ 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; | ||||||
| @@ -2699,19 +2761,43 @@ RED.nodes = (function() { | |||||||
|         return result; |         return result; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Update any config nodes referenced by the provided node to ensure their 'users' list is correct |     /** | ||||||
|     function updateConfigNodeUsers(n) { |      * Update any config nodes referenced by the provided node to ensure | ||||||
|         for (var d in n._def.defaults) { |      * their 'users' list is correct. | ||||||
|             if (n._def.defaults.hasOwnProperty(d)) { |      * | ||||||
|                 var property = n._def.defaults[d]; |      * @param {object} node The node in which to check if it contains references | ||||||
|  |      * @param {object} options Options to apply. | ||||||
|  |      * @param {"add" | "remove"} [options.action] Add or remove the node from | ||||||
|  |      * the Config Node users list. Default `add`. | ||||||
|  |      * @param {boolean} [options.emitEvent] Emit the `nodes:changes` event. | ||||||
|  |      * Default true. | ||||||
|  |      */ | ||||||
|  |     function updateConfigNodeUsers(node, options) { | ||||||
|  |         const defaultOptions = { action: "add", emitEvent: true }; | ||||||
|  |         options = Object.assign({}, defaultOptions, options); | ||||||
|  |  | ||||||
|  |         for (var d in node._def.defaults) { | ||||||
|  |             if (node._def.defaults.hasOwnProperty(d)) { | ||||||
|  |                 var property = node._def.defaults[d]; | ||||||
|                 if (property.type) { |                 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[n[d]]; |                         var configNode = configNodes[node[d]]; | ||||||
|                         if (configNode) { |                         if (configNode) { | ||||||
|                             if (configNode.users.indexOf(n) === -1) { |                             if (options.action === "add") { | ||||||
|                                 configNode.users.push(n); |                                 if (configNode.users.indexOf(node) === -1) { | ||||||
|                                 RED.events.emit('nodes:change',configNode) |                                     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); | ||||||
|                                     } |                                     } | ||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
| @@ -2719,6 +2805,36 @@ RED.nodes = (function() { | |||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 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) { | ||||||
|         if (version !== undefined) { |         if (version !== undefined) { | ||||||
|   | |||||||
| @@ -808,6 +808,20 @@ RED.editor = (function() { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             const oldCreds = {}; | ||||||
|  |             if (editing_node._def.credentials) { | ||||||
|  |                 for (const prop in editing_node._def.credentials) { | ||||||
|  |                     if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) { | ||||||
|  |                         if (editing_node._def.credentials[prop].type === 'password') { | ||||||
|  |                             oldCreds['has_' + prop] = editing_node.credentials['has_' + prop]; | ||||||
|  |                         } | ||||||
|  |                         if (prop in editing_node.credentials) { | ||||||
|  |                             oldCreds[prop] = editing_node.credentials[prop]; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|             try { |             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) { | ||||||
| @@ -839,6 +853,25 @@ RED.editor = (function() { | |||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             if (editing_node._def.credentials) { | ||||||
|  |                 for (const prop in editing_node._def.credentials) { | ||||||
|  |                     if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) { | ||||||
|  |                         if (oldCreds[prop] !== editing_node.credentials[prop]) { | ||||||
|  |                             if (editing_node.credentials[prop] === '__PWRD__') { | ||||||
|  |                                 // The password may not exist in oldCreds | ||||||
|  |                                 // The value '__PWRD__' means the password exists, | ||||||
|  |                                 // so ignore this change | ||||||
|  |                                 continue; | ||||||
|  |                             } | ||||||
|  |                             editState.changes.credentials = editState.changes.credentials || {}; | ||||||
|  |                             editState.changes.credentials['has_' + prop] = oldCreds['has_' + prop]; | ||||||
|  |                             editState.changes.credentials[prop] = oldCreds[prop]; | ||||||
|  |                             editState.changed = true; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -1481,134 +1514,181 @@ RED.editor = (function() { | |||||||
|             }, |             }, | ||||||
|             { |             { | ||||||
|                 id: "node-config-dialog-ok", |                 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() { | ||||||
|                     var editState = { |                     // TODO: Already defined | ||||||
|  |                     const configProperty = name; | ||||||
|  |                     const configType = type; | ||||||
|  |                     const configTypeDef = RED.nodes.getType(configType); | ||||||
|  |  | ||||||
|  |                     const wasChanged = editing_config_node.changed; | ||||||
|  |                     const editState = { | ||||||
|                         changes: {}, |                         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; |  | ||||||
|                      |                      | ||||||
|                     if (configTypeDef.oneditsave) { |                     // Call `oneditsave` and search for changes | ||||||
|                         try { |                     handleEditSave(editing_config_node, editState); | ||||||
|                             configTypeDef.oneditsave.call(editing_config_node); |  | ||||||
|                         } catch(err) { |  | ||||||
|                             console.warn("oneditsave",editing_config_node.id,editing_config_node.type,err.toString()); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     for (d in configTypeDef.defaults) { |                     // Search for changes in the edit box (panes) | ||||||
|                         if (configTypeDef.defaults.hasOwnProperty(d)) { |                     activeEditPanes.forEach(function (pane) { | ||||||
|                             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); | ||||||
|                         } |                         } | ||||||
|                     }) |                     }); | ||||||
|  |  | ||||||
|                     editing_config_node.label = configTypeDef.label; |                     // TODO: Why? | ||||||
|  |                     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; | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     if (scope) { |                     // NOTE: must be undefined if no scope used | ||||||
|                         // Search for nodes that use this one that are no longer |                     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 |                     // in scope, so must be removed | ||||||
|                         editing_config_node.users = editing_config_node.users.filter(function(n) { |                     const historyEvents = []; | ||||||
|                             var keep = true; |                     if (scope) { | ||||||
|                             for (var d in n._def.defaults) { |                         const newUsers = editing_config_node.users.filter(function (node) { | ||||||
|                                 if (n._def.defaults.hasOwnProperty(d)) { |                             let keepNode = false; | ||||||
|                                     if (n._def.defaults[d].type === editing_config_node.type && |                             let nodeModified = null; | ||||||
|                                         n[d] === editing_config_node.id && |  | ||||||
|                                         n.z !== scope) { |                             for (const d in node._def.defaults) { | ||||||
|                                             keep = false; |                                 if (node._def.defaults.hasOwnProperty(d)) { | ||||||
|                                             // Remove the reference to this node |                                     if (node._def.defaults[d].type === editing_config_node.type) { | ||||||
|                                             // and revalidate |                                         if (node[d] === editing_config_node.id) { | ||||||
|                                             n[d] = null; |                                             if (node.z === editing_config_node.z) { | ||||||
|                                             n.dirty = true; |                                                 // The node is kept only if at least one property uses | ||||||
|                                             n.changed = true; |                                                 // this config node in the correct scope. | ||||||
|                                             validateNode(n); |                                                 keepNode = true; | ||||||
|  |                                             } else { | ||||||
|  |                                                 if (!nodeModified) { | ||||||
|  |                                                     nodeModified = { | ||||||
|  |                                                         t: "edit", | ||||||
|  |                                                         node: node, | ||||||
|  |                                                         changes: { [d]: node[d] }, | ||||||
|  |                                                         changed: node.changed, | ||||||
|  |                                                         dirty: node.dirty | ||||||
|  |                                                     }; | ||||||
|  |                                                 } else { | ||||||
|  |                                                     nodeModified.changes[d] = node[d]; | ||||||
|  |                                                 } | ||||||
|  |  | ||||||
|  |                                                 // Remove the reference to the config node | ||||||
|  |                                                 node[d] = ""; | ||||||
|                                             } |                                             } | ||||||
|                                         } |                                         } | ||||||
|                                     } |                                     } | ||||||
|                             return keep; |                                 } | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             // 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 (configAdding) { |                     if (editState.changed) { | ||||||
|                         RED.nodes.add(editing_config_node); |                         // Set the congig node as changed | ||||||
|  |                         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; |  | ||||||
|  |  | ||||||
|                     var userStack = editing_config_node.users.slice(); |                     // And validate nodes using this config node too | ||||||
|                     while(userStack.length > 0) { |                     const validatedNodes = new Set(); | ||||||
|                         var user = userStack.pop(); |                     const userStack = editing_config_node.users.slice(); | ||||||
|                         if (!validatedNodes[user.id]) { |  | ||||||
|                             validatedNodes[user.id] = true; |                     validatedNodes.add(editing_config_node.id); | ||||||
|                             if (user.users) { |                     while (userStack.length) { | ||||||
|                                 userStack = userStack.concat(user.users); |                         const node = userStack.pop(); | ||||||
|  |                         if (!validatedNodes.has(node.id)) { | ||||||
|  |                             validatedNodes.add(node.id); | ||||||
|  |                             if (node.users) { | ||||||
|  |                                 userStack.push(...node.users); | ||||||
|                             } |                             } | ||||||
|                             validateNode(user); |                             validateNode(node); | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|  |                     let historyEvent = { | ||||||
|  |                         t: "edit", | ||||||
|  |                         node: editing_config_node, | ||||||
|  |                         changes: editState.changes, | ||||||
|  |                         changed: wasChanged, | ||||||
|  |                         dirty: RED.nodes.dirty() | ||||||
|  |                     }; | ||||||
|  |  | ||||||
|  |                     if (historyEvents.length) { | ||||||
|  |                         // Need a multi events | ||||||
|  |                         historyEvent = { | ||||||
|  |                             t: "multi", | ||||||
|  |                             events: [historyEvent].concat(historyEvents), | ||||||
|  |                             dirty: historyEvent.dirty | ||||||
|  |                         }; | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if (!adding) { | ||||||
|  |                         // This event is triggered when the edit box is saved, | ||||||
|  |                         // regardless of whether there are any modifications. | ||||||
|  |                         RED.events.emit("editor:save", editing_config_node); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if (editState.changed) { | ||||||
|  |                         if (adding) { | ||||||
|  |                             RED.history.push({ t: "add", nodes: [editing_config_node.id], dirty: RED.nodes.dirty() }); | ||||||
|  |                             // Add the new config node and trigger the `nodes:add` event | ||||||
|  |                             RED.nodes.add(editing_config_node); | ||||||
|  |                         } else { | ||||||
|  |                             RED.history.push(historyEvent); | ||||||
|  |                             RED.events.emit("nodes:change", editing_config_node); | ||||||
|  |                         } | ||||||
|  |  | ||||||
|                         RED.nodes.dirty(true); |                         RED.nodes.dirty(true); | ||||||
|                         RED.view.redraw(true); |                         RED.view.redraw(true); | ||||||
|                     if (!configAdding) { |  | ||||||
|                         RED.events.emit("editor:save",editing_config_node); |  | ||||||
|                         RED.events.emit("nodes:change",editing_config_node); |  | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     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 | ||||||
| @@ -1756,8 +1836,18 @@ 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; | ||||||
| @@ -1766,10 +1856,11 @@ RED.editor = (function() { | |||||||
|  |  | ||||||
|  |  | ||||||
|                         if (editState.changed) { |                         if (editState.changed) { | ||||||
|                             var wasChanged = editing_node.changed; |                             let wasChanged = editing_node.changed; | ||||||
|                             editing_node.changed = true; |                             editing_node.changed = true; | ||||||
|                             validateNode(editing_node); |                             validateNode(editing_node); | ||||||
|                             var subflowInstances = []; |                             let 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({ | ||||||
| @@ -1779,13 +1870,35 @@ 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); | ||||||
|                             var historyEvent = { |                             let historyEvent = { | ||||||
|                                 t:'edit', |                                 t:'edit', | ||||||
|                                 node:editing_node, |                                 node:editing_node, | ||||||
|                                 changes:editState.changes, |                                 changes:editState.changes, | ||||||
| @@ -1795,7 +1908,13 @@ 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; | ||||||
|   | |||||||
| @@ -20,10 +20,31 @@ | |||||||
|             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) { | ||||||
| @@ -41,7 +62,6 @@ | |||||||
|                     } |                     } | ||||||
|                 }); |                 }); | ||||||
|  |  | ||||||
|  |  | ||||||
|                 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") { | ||||||
| @@ -52,6 +72,15 @@ | |||||||
|                                 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); | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|                         } |                         } | ||||||
|                     }); |                     }); | ||||||
|                 } |                 } | ||||||
|   | |||||||
| @@ -44,6 +44,7 @@ | |||||||
|             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)) { | ||||||
| @@ -131,9 +132,16 @@ | |||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 if (node._def.credentials) { |                 if (node._def.credentials) { | ||||||
|                     var credDefinition = node._def.credentials; |                     const credDefinition = node._def.credentials; | ||||||
|                     var credsChanged = updateNodeCredentials(node,credDefinition,this.inputClass); |                     const credChanges = 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 | ||||||
|  |                         }; | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -161,10 +169,11 @@ | |||||||
|      * @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 {boolean} whether anything has changed |      * @return {object} an object containing the modified properties | ||||||
|      */ |      */ | ||||||
|     function updateNodeCredentials(node, credDefinition, prefix) { |     function updateNodeCredentials(node, credDefinition, prefix) { | ||||||
|         var changed = false; |         const changes = {}; | ||||||
|  |  | ||||||
|         if (!node.credentials) { |         if (!node.credentials) { | ||||||
|             node.credentials = {_:{}}; |             node.credentials = {_:{}}; | ||||||
|         } else if (!node.credentials._) { |         } else if (!node.credentials._) { | ||||||
| @@ -177,22 +186,33 @@ | |||||||
|                 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') { | ||||||
|                         node.credentials['has_' + cred] = (value !== ""); |                         if (value === '__PWRD__') { | ||||||
|                         if (value == '__PWRD__') { |                             // A cred value exists - no changes | ||||||
|                             continue; |                         } else if (value === '' && node.credentials['has_' + cred] === false) { | ||||||
|                         } |                             // Empty cred value exists - no changes | ||||||
|                         changed = true; |                         } 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; |                             node.credentials[cred] = value; | ||||||
|                     if (value != node.credentials._[cred]) { |  | ||||||
|                         changed = true; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return changed; |  | ||||||
|                         } |                         } | ||||||
|  |  | ||||||
|  |                         node.credentials['has_' + cred] = (value !== ''); | ||||||
|  |                     } else { | ||||||
|  |                         // Since these creds are loaded by the editor, | ||||||
|  |                         // values can be directly compared | ||||||
|  |                         if (value !== node.credentials[cred]) { | ||||||
|  |                             changes[cred] = node.credentials[cred]; | ||||||
|  |                             node.credentials[cred] = value; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return changes; | ||||||
|  |     } | ||||||
| })(); | })(); | ||||||
|   | |||||||
| @@ -245,10 +245,15 @@ 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: lib.icon || 'fa fa-hdd-o', |                             icon, | ||||||
|                             label: RED._(lib.label||lib.id), |                             label: RED._(lib.label||lib.id), | ||||||
|                             path: "", |                             path: "", | ||||||
|                             expanded: true, |                             expanded: true, | ||||||
| @@ -303,10 +308,15 @@ 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: lib.icon || 'fa fa-hdd-o', |                             icon, | ||||||
|                             label: RED._(lib.label||lib.id), |                             label: RED._(lib.label||lib.id), | ||||||
|                             path: "", |                             path: "", | ||||||
|                             expanded: true, |                             expanded: true, | ||||||
|   | |||||||
| @@ -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() |                         item.value = input.val() === "_ADD_" ? "" : 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) { | ||||||
|   | |||||||
| @@ -18,8 +18,6 @@ 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; | ||||||
| @@ -27,6 +25,8 @@ RED.sidebar.context = (function() { | |||||||
|     var flowSection; |     var flowSection; | ||||||
|     var globalSection; |     var globalSection; | ||||||
|  |  | ||||||
|  |     const expandedPaths = {} | ||||||
|  |  | ||||||
|     var currentNode; |     var currentNode; | ||||||
|     var currentFlow; |     var currentFlow; | ||||||
|  |  | ||||||
| @@ -212,14 +212,41 @@ 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) { | ||||||
|                     var k = keys[i]; |                     const k = keys[i]; | ||||||
|                     var l2 = sortedData[k].length; |                     let payload = v.msg; | ||||||
|                     var propRow = $('<tr class="red-ui-help-info-row"><td class="red-ui-sidebar-context-property"></td><td></td></tr>').appendTo(container); |                     let format = v.format; | ||||||
|                     var obj = $(propRow.children()[0]); |                     const tools = $('<span class="button-group"></span>'); | ||||||
|  |                     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) | ||||||
|                     var refreshItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-refresh"></i></button>').appendTo(tools).on("click", function(e) { |                     const 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) { | ||||||
| @@ -229,16 +256,14 @@ 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")); | ||||||
|                     var deleteItem = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-trash"></i></button>').appendTo(tools).on("click", function(e) { |                     const 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({ | ||||||
| @@ -246,7 +271,7 @@ RED.sidebar.context = (function() { | |||||||
|                             target: propRow, |                             target: propRow, | ||||||
|                             direction: "left", |                             direction: "left", | ||||||
|                             content: function() { |                             content: function() { | ||||||
|                                 var content = $('<div>'); |                                 const 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); | ||||||
| @@ -269,16 +294,15 @@ 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), { | ||||||
|                                                     typeHint: data.format, |                                                     ...objectElementOptions, | ||||||
|                                                     sourceId: id+"."+k, |                                                     typeHint: data.format | ||||||
|                                                     tools: tools, |  | ||||||
|                                                     path: k |  | ||||||
|                                                 }).appendTo(propRow.children()[1]); |                                                 }).appendTo(propRow.children()[1]); | ||||||
|                                             } |                                             } | ||||||
|                                         }); |                                         }); | ||||||
| @@ -293,14 +317,7 @@ RED.sidebar.context = (function() { | |||||||
|  |  | ||||||
|                     }); |                     }); | ||||||
|                     RED.popover.tooltip(deleteItem,RED._("sidebar.context.delete")); |                     RED.popover.tooltip(deleteItem,RED._("sidebar.context.delete")); | ||||||
|                     var payload = v.msg; |                     RED.utils.createObjectElement(RED.utils.decodeObject(payload,format), objectElementOptions).appendTo(propRow.children()[1]); | ||||||
|                     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])) | ||||||
|                     } |                     } | ||||||
|   | |||||||
| @@ -230,7 +230,7 @@ RED.utils = (function() { | |||||||
|     var pinnedPaths = {}; |     var pinnedPaths = {}; | ||||||
|     var formattedPaths = {}; |     var formattedPaths = {}; | ||||||
|  |  | ||||||
|     function addMessageControls(obj,sourceId,key,msg,rootPath,strippedKey,extraTools) { |     function addMessageControls(obj,sourceId,key,msg,rootPath,strippedKey,extraTools,enablePinning) { | ||||||
|         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 (strippedKey !== undefined && strippedKey !== '') { |         if (enablePinning && 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,13 +281,16 @@ RED.utils = (function() { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     function checkExpanded(strippedKey,expandPaths,minRange,maxRange) { |     function checkExpanded(strippedKey, expandPaths, { minRange, maxRange, expandLeafNodes }) { | ||||||
|         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] === "[") { | ||||||
| @@ -394,6 +397,8 @@ 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; | ||||||
| @@ -416,11 +421,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); |             addMessageControls(header,sourceId,path,obj,rootPath,strippedKey,tools, enablePinning); | ||||||
|         } |         } | ||||||
|         if (!key) { |         if (!key) { | ||||||
|             element.addClass("red-ui-debug-msg-top-level"); |             element.addClass("red-ui-debug-msg-top-level"); | ||||||
|             if (sourceId) { |             if (sourceId && !expandPaths) { | ||||||
|                 var pinned = pinnedPaths[sourceId]; |                 var pinned = pinnedPaths[sourceId]; | ||||||
|                 expandPaths = []; |                 expandPaths = []; | ||||||
|                 if (pinned) { |                 if (pinned) { | ||||||
| @@ -476,7 +481,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)); |                 },function(state) {if (ontoggle) { ontoggle(path,state);}}, checkExpanded(strippedKey, expandPaths, { expandLeafNodes })); | ||||||
|             } |             } | ||||||
|             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)) { | ||||||
| @@ -592,14 +597,16 @@ 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, | ||||||
|                                     ontoggle: ontoggle, |                                     expandLeafNodes, | ||||||
|                                     exposeApi: exposeApi, |                                     ontoggle, | ||||||
|  |                                     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); | ||||||
|                         } |                         } | ||||||
| @@ -623,21 +630,23 @@ 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, | ||||||
|                                                 ontoggle: ontoggle, |                                                 expandLeafNodes, | ||||||
|                                                 exposeApi: exposeApi, |                                                 ontoggle, | ||||||
|  |                                                 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,Math.min(fullLength-1,(minRange+9)))); |                             checkExpanded(strippedKey,expandPaths,{ minRange, maxRange: Math.min(fullLength-1,(minRange+9)), expandLeafNodes})); | ||||||
|                             $('<span class="red-ui-debug-msg-object-key"></span>').html("["+minRange+" … "+Math.min(fullLength-1,(minRange+9))+"]").appendTo(header); |                             $('<span class="red-ui-debug-msg-object-key"></span>').html("["+minRange+" … "+Math.min(fullLength-1,(minRange+9))+"]").appendTo(header); | ||||||
|                         } |                         } | ||||||
|                         if (fullLength < originalLength) { |                         if (fullLength < originalLength) { | ||||||
| @@ -646,7 +655,7 @@ RED.utils = (function() { | |||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 function(state) {if (ontoggle) { ontoggle(path,state);}}, |                 function(state) {if (ontoggle) { ontoggle(path,state);}}, | ||||||
|                 checkExpanded(strippedKey,expandPaths)); |                 checkExpanded(strippedKey, expandPaths, { expandLeafNodes })); | ||||||
|             } |             } | ||||||
|         } else if (typeof obj === 'object') { |         } else if (typeof obj === 'object') { | ||||||
|             element.addClass('collapsed'); |             element.addClass('collapsed'); | ||||||
| @@ -680,14 +689,16 @@ RED.utils = (function() { | |||||||
|                                 typeHint: false, |                                 typeHint: false, | ||||||
|                                 hideKey: false, |                                 hideKey: false, | ||||||
|                                 path: newPath, |                                 path: newPath, | ||||||
|                                 sourceId: sourceId, |                                 sourceId, | ||||||
|                                 rootPath: rootPath, |                                 rootPath, | ||||||
|                                 expandPaths: expandPaths, |                                 expandPaths, | ||||||
|                                 ontoggle: ontoggle, |                                 expandLeafNodes, | ||||||
|                                 exposeApi: exposeApi, |                                 ontoggle, | ||||||
|  |                                 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); | ||||||
|                     } |                     } | ||||||
| @@ -696,7 +707,7 @@ RED.utils = (function() { | |||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 function(state) {if (ontoggle) { ontoggle(path,state);}}, |                 function(state) {if (ontoggle) { ontoggle(path,state);}}, | ||||||
|                 checkExpanded(strippedKey,expandPaths)); |                 checkExpanded(strippedKey, expandPaths, { expandLeafNodes })); | ||||||
|             } |             } | ||||||
|             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); | ||||||
|   | |||||||
| @@ -1265,11 +1265,6 @@ 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]; | ||||||
| @@ -1591,9 +1586,6 @@ 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(); | ||||||
|   | |||||||
| @@ -168,6 +168,37 @@ RED.user = (function() { | |||||||
|                     } |                     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |                 } else { | ||||||
|  |                     if (data.prompts) { | ||||||
|  |                         if (data.loginMessage) { | ||||||
|  |                             const sessionMessages = $("<div/>",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields"); | ||||||
|  |                             $('<div>').text(data.loginMessage).appendTo(sessionMessages); | ||||||
|  |                         } | ||||||
|  |  | ||||||
|  |                         i = 0; | ||||||
|  |                         for (;i<data.prompts.length;i++) { | ||||||
|  |                             var field = data.prompts[i]; | ||||||
|  |                             var row = $("<div/>",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields"); | ||||||
|  |                             var loginButton = $('<a href="#" class="red-ui-button"></a>',{style: "padding: 10px"}).appendTo(row).on("click", function() { | ||||||
|  |                                 document.location = field.url; | ||||||
|  |                             }); | ||||||
|  |                             if (field.image) { | ||||||
|  |                                 $("<img>",{src:field.image}).appendTo(loginButton); | ||||||
|  |                             } else if (field.label) { | ||||||
|  |                                 var label = $('<span></span>').text(field.label); | ||||||
|  |                                 if (field.icon) { | ||||||
|  |                                     $('<i></i>',{class: "fa fa-2x "+field.icon, style:"vertical-align: middle"}).appendTo(loginButton); | ||||||
|  |                                     label.css({ | ||||||
|  |                                         "verticalAlign":"middle", | ||||||
|  |                                         "marginLeft":"8px" | ||||||
|  |                                     }); | ||||||
|  |  | ||||||
|  |                                 } | ||||||
|  |                                 label.appendTo(loginButton); | ||||||
|  |                             } | ||||||
|  |                             loginButton.button(); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|                 if (opts.cancelable) { |                 if (opts.cancelable) { | ||||||
|                     $("#node-dialog-login-cancel").button().on("click", function( event ) { |                     $("#node-dialog-login-cancel").button().on("click", function( event ) { | ||||||
|   | |||||||
| @@ -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' && hasOwnProperty.call(output, "fill") && hasOwnProperty.call(output, "shape") && hasOwnProperty.call(output, "text")) { |                         if (typeof output === 'object' && output?.fill && output?.shape && output?.text) { | ||||||
|                             fill = output.fill; |                             fill = output.fill; | ||||||
|                             shape = output.shape; |                             shape = output.shape; | ||||||
|                             st = output.text; |                             st = output.text; | ||||||
|   | |||||||
| @@ -511,9 +511,10 @@ 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); | ||||||
|   | |||||||
| @@ -352,7 +352,9 @@ 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); | ||||||
|  |                         }) | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             }) |             }) | ||||||
| @@ -505,7 +507,9 @@ module.exports = function(RED) { | |||||||
|                 if (err) { |                 if (err) { | ||||||
|                     node.error(err,nextMsg); |                     node.error(err,nextMsg); | ||||||
|                 } |                 } | ||||||
|  |                 setImmediate(() => { | ||||||
|                     processMessageQueue() |                     processMessageQueue() | ||||||
|  |                 }) | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -367,20 +367,21 @@ 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 = [',', '"', "\n", "\r"] |             const templateQuoteables = [node.sep, node.quo, "\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.headers.length > 0) { node.goodtmpl = true } else { node.goodtmpl = false } |                 if (parsed.data?.length === 1) { node.goodtmpl = true } else { node.goodtmpl = false } | ||||||
|                 return parsed.headers.length ? parsed.headers : null |                 return node.goodtmpl ? parsed.data[0] : null | ||||||
|             } |             } | ||||||
|             const templateArrayToColumnString = function (template, keepEmptyColumns) { |             const templateArrayToColumnString = function (template, keepEmptyColumns, separator = ',', quotables = templateQuoteablesStrict) { | ||||||
|                 // NOTE: enforce strict column template parsing in RFC4180 mode |                 // NOTE: defaults to strict column template parsing (commas and double quotes) | ||||||
|                 const parsed = csv.parse('', {headers: template, headersOnly:true, separator: ',', quote: node.quo, outputStyle: 'array', strict: true }) |                 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: templateQuoteables})) |                     ? parsed.headers.map(e => addQuotes(e || '', { separator, quoteables: quotables })).join(separator) | ||||||
|                     : 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 | ||||||
| @@ -447,7 +448,7 @@ module.exports = function(RED) { | |||||||
|                                         template = Object.keys(inputData[0]) || [''] |                                         template = Object.keys(inputData[0]) || [''] | ||||||
|                                     } |                                     } | ||||||
|                                 } |                                 } | ||||||
|                                 stringBuilder.push(templateArrayToColumnString(template, true)) |                                 stringBuilder.push(templateArrayToColumnString(template, true, node.sep, templateQuoteables)) // use user set separator for output data. | ||||||
|                                 if (sendHeadersOnce) { node.hdrSent = true } |                                 if (sendHeadersOnce) { node.hdrSent = true } | ||||||
|                             } |                             } | ||||||
|  |  | ||||||
| @@ -483,6 +484,7 @@ 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)) { | ||||||
| @@ -518,7 +520,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) |                             msg.columns = templateArrayToColumnString(template) // always strict commas + double quotes  for  | ||||||
|                             if (msg.payload !== '') { send(msg) } |                             if (msg.payload !== '') { send(msg) } | ||||||
|                             done() |                             done() | ||||||
|                         } |                         } | ||||||
| @@ -615,16 +617,15 @@ 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._mode = 'RFC4180 mode' |                                         msg.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes  for msg.columns | ||||||
|                                         delete msg.parts |                                         delete msg.parts | ||||||
|                                         send(msg) |                                         send(msg) | ||||||
|                                         node.store = [] |                                         node.store = [] | ||||||
|                                     } |                                     } | ||||||
|                                 } |                                 } | ||||||
|                                 else { |                                 else { | ||||||
|                                     msg.columns = csvParseResult.header |                                     msg.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes  for msg.columns | ||||||
|                                     // msg._mode = 'RFC4180 mode' |  | ||||||
|                                     msg.payload = data |                                     msg.payload = data | ||||||
|                                     send(msg); // finally send the array |                                     send(msg); // finally send the array | ||||||
|                                 } |                                 } | ||||||
| @@ -633,7 +634,8 @@ 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 = { | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "@node-red/nodes", |     "name": "@node-red/nodes", | ||||||
|     "version": "4.0.5", |     "version": "4.0.8", | ||||||
|     "license": "Apache-2.0", |     "license": "Apache-2.0", | ||||||
|     "repository": { |     "repository": { | ||||||
|         "type": "git", |         "type": "git", | ||||||
|   | |||||||
| @@ -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 = url; |                 installName = localtgzRe.test(url) && slashRe.test(url) ? `"${url}"` : 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})); | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "@node-red/registry", |     "name": "@node-red/registry", | ||||||
|     "version": "4.0.5", |     "version": "4.0.8", | ||||||
|     "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.5", |         "@node-red/util": "4.0.8", | ||||||
|         "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", | ||||||
|   | |||||||
| @@ -719,6 +719,14 @@ 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); | ||||||
|   | |||||||
| @@ -49,6 +49,14 @@ 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 = { | ||||||
|   | |||||||
| @@ -100,7 +100,24 @@ 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', {_flow: flow}, null, (err, result) => { |                 redUtil.evaluateNodeProperty(value, 'jsonata',{ | ||||||
|  |                     // 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} | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "@node-red/runtime", |     "name": "@node-red/runtime", | ||||||
|     "version": "4.0.5", |     "version": "4.0.8", | ||||||
|     "license": "Apache-2.0", |     "license": "Apache-2.0", | ||||||
|     "main": "./lib/index.js", |     "main": "./lib/index.js", | ||||||
|     "repository": { |     "repository": { | ||||||
| @@ -16,8 +16,8 @@ | |||||||
|         } |         } | ||||||
|     ], |     ], | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|         "@node-red/registry": "4.0.5", |         "@node-red/registry": "4.0.8", | ||||||
|         "@node-red/util": "4.0.5", |         "@node-red/util": "4.0.8", | ||||||
|         "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.2", | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "@node-red/util", |     "name": "@node-red/util", | ||||||
|     "version": "4.0.5", |     "version": "4.0.8", | ||||||
|     "license": "Apache-2.0", |     "license": "Apache-2.0", | ||||||
|     "repository": { |     "repository": { | ||||||
|         "type": "git", |         "type": "git", | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								packages/node_modules/node-red/package.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								packages/node_modules/node-red/package.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "node-red", |     "name": "node-red", | ||||||
|     "version": "4.0.5", |     "version": "4.0.8", | ||||||
|     "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,10 +31,10 @@ | |||||||
|         "flow" |         "flow" | ||||||
|     ], |     ], | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|         "@node-red/editor-api": "4.0.5", |         "@node-red/editor-api": "4.0.8", | ||||||
|         "@node-red/runtime": "4.0.5", |         "@node-red/runtime": "4.0.8", | ||||||
|         "@node-red/util": "4.0.5", |         "@node-red/util": "4.0.8", | ||||||
|         "@node-red/nodes": "4.0.5", |         "@node-red/nodes": "4.0.8", | ||||||
|         "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", | ||||||
|   | |||||||
| @@ -2067,6 +2067,27 @@ 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); | ||||||
| @@ -2086,6 +2107,7 @@ 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); | ||||||
| @@ -2106,6 +2128,7 @@ 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); | ||||||
| @@ -2171,6 +2194,7 @@ 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); } | ||||||
| @@ -2189,6 +2213,7 @@ 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); } | ||||||
| @@ -2208,6 +2233,7 @@ 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); } | ||||||
| @@ -2327,6 +2353,7 @@ 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); } | ||||||
|   | |||||||
| @@ -258,6 +258,29 @@ 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; }) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user