mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	Compare commits
	
		
			60 Commits
		
	
	
		
			dependabot
			...
			4.0.6
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 7a3741165b | ||
|  | e9d5d20e2d | ||
|  | 867a6ad2da | ||
|  | 03507c2a1f | ||
|  | aa79aa5479 | ||
|  | 16005a462d | ||
|  | 82c756b091 | ||
|  | b139eb4a18 | ||
|  | 6af3c8c2a9 | ||
|  | 2c3fbb1467 | ||
|  | 01716119e6 | ||
|  | a50a37ac26 | ||
|  | 11c4277466 | ||
|  | 7d284ce157 | ||
|  | aa1d5ad06b | ||
|  | 00a3010933 | ||
|  | 56a4530ec6 | ||
|  | 89e40a0b8f | ||
|  | 66bd1feb47 | ||
|  | b419e2e303 | ||
|  | dae4ba8044 | ||
|  | fe22afea6a | ||
|  | 69753a9940 | ||
|  | f6e565ba04 | ||
|  | e4fdf24545 | ||
|  | 43a9a3c3b1 | ||
|  | bfd98aaf22 | ||
|  | 4e61c54be5 | ||
|  | 39a85c721d | ||
|  | f9877f8d0b | ||
|  | 92dff4bacd | ||
|  | 338ddf17de | ||
|  | 4e6c8ea367 | ||
|  | 5f92bc83fd | ||
|  | 5e429f3be0 | ||
|  | 2a71175cd4 | ||
|  | aee531bf16 | ||
|  | 2c99909353 | ||
|  | 50e821d5d7 | ||
|  | 06f3f3c0be | ||
|  | 0b09cf5fa9 | ||
|  | 93102837dd | ||
|  | e8d81d814c | ||
|  | f6cf051282 | ||
|  | 328390c2a9 | ||
|  | 6194285b6e | ||
|  | 94e3fdd7a9 | ||
|  | 046d56d692 | ||
|  | c8a02d53e8 | ||
|  | deccfdf654 | ||
|  | f2d72b1050 | ||
|  | 3d9bc265dd | ||
|  | 966064328f | ||
|  | 83696abf9d | ||
|  | 10ac7fc369 | ||
|  | a743764345 | ||
|  | cc1c87387b | ||
|  | ed4b98b598 | ||
|  | 53e092e484 | ||
|  | eab512ef22 | 
							
								
								
									
										33
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,3 +1,36 @@ | |||||||
|  | #### 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 | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "node-red", |     "name": "node-red", | ||||||
|     "version": "4.0.5", |     "version": "4.0.6", | ||||||
|     "description": "Low-code programming for event-driven applications", |     "description": "Low-code programming for event-driven applications", | ||||||
|     "homepage": "https://nodered.org", |     "homepage": "https://nodered.org", | ||||||
|     "license": "Apache-2.0", |     "license": "Apache-2.0", | ||||||
| @@ -41,7 +41,7 @@ | |||||||
|         "cors": "2.8.5", |         "cors": "2.8.5", | ||||||
|         "cronosjs": "1.7.1", |         "cronosjs": "1.7.1", | ||||||
|         "denque": "2.1.0", |         "denque": "2.1.0", | ||||||
|         "express": "4.21.1", |         "express": "4.21.2", | ||||||
|         "express-session": "1.18.1", |         "express-session": "1.18.1", | ||||||
|         "form-data": "4.0.0", |         "form-data": "4.0.0", | ||||||
|         "fs-extra": "11.2.0", |         "fs-extra": "11.2.0", | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "@node-red/editor-api", |     "name": "@node-red/editor-api", | ||||||
|     "version": "4.0.5", |     "version": "4.0.6", | ||||||
|     "license": "Apache-2.0", |     "license": "Apache-2.0", | ||||||
|     "main": "./lib/index.js", |     "main": "./lib/index.js", | ||||||
|     "repository": { |     "repository": { | ||||||
| @@ -16,14 +16,14 @@ | |||||||
|         } |         } | ||||||
|     ], |     ], | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|         "@node-red/util": "4.0.5", |         "@node-red/util": "4.0.6", | ||||||
|         "@node-red/editor-client": "4.0.5", |         "@node-red/editor-client": "4.0.6", | ||||||
|         "bcryptjs": "2.4.3", |         "bcryptjs": "2.4.3", | ||||||
|         "body-parser": "1.20.3", |         "body-parser": "1.20.3", | ||||||
|         "clone": "2.1.2", |         "clone": "2.1.2", | ||||||
|         "cors": "2.8.5", |         "cors": "2.8.5", | ||||||
|         "express-session": "1.18.1", |         "express-session": "1.18.1", | ||||||
|         "express": "4.21.1", |         "express": "4.21.2", | ||||||
|         "memorystore": "1.6.7", |         "memorystore": "1.6.7", | ||||||
|         "mime": "3.0.0", |         "mime": "3.0.0", | ||||||
|         "multer": "1.4.5-lts.1", |         "multer": "1.4.5-lts.1", | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "@node-red/editor-client", |     "name": "@node-red/editor-client", | ||||||
|     "version": "4.0.5", |     "version": "4.0.6", | ||||||
|     "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++) { | ||||||
|   | |||||||
| @@ -708,11 +708,11 @@ RED.nodes = (function() { | |||||||
|             n["_"] = RED._; |             n["_"] = RED._; | ||||||
|         } |         } | ||||||
|         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); |             updateConfigNodeUsers(newNode, { action: "add" }); | ||||||
|             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) { | ||||||
| @@ -785,6 +785,7 @@ 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" }); | ||||||
|             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 +799,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 +1795,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 { | ||||||
| @@ -2349,29 +2357,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') && 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') && 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))) { |                             // The node declares outputs in its defaults, but has not got a valid value | ||||||
|                                     // If 'wires' is longer than outputs, clip wires |                             // Defer to the length of the wires array | ||||||
|                                     console.log("Warning: node.wires longer than node.outputs - trimming wires:",node.id," wires:",node.wires.length," outputs:",node.outputs); |                             if (node.hasOwnProperty('wires')) { | ||||||
|                                     node.wires = node.wires.slice(0,node.outputs); |                                 if (isNaN(node.outputs)) { | ||||||
|                                 } else { |  | ||||||
|                                     // The node declares outputs in its defaults, but has not got a valid value |  | ||||||
|                                     // Defer to the length of the wires array |  | ||||||
|                                     node.outputs = node.wires.length; |                                     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 +2478,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,25 +2704,78 @@ 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); | ||||||
|                     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); | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         // 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) { | ||||||
|   | |||||||
| @@ -334,6 +334,30 @@ RED.clipboard = (function() { | |||||||
|         },100); |         },100); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Validates if the provided string looks like valid flow json | ||||||
|  |      * @param {string} flowString the string to validate | ||||||
|  |      * @returns If valid, returns the node array | ||||||
|  |      */ | ||||||
|  |     function validateFlowString(flowString) { | ||||||
|  |         const res = JSON.parse(flowString) | ||||||
|  |         if (!Array.isArray(res)) { | ||||||
|  |             throw new Error(RED._("clipboard.import.errors.notArray")); | ||||||
|  |         } | ||||||
|  |         for (let i = 0; i < res.length; i++) { | ||||||
|  |             if (typeof res[i] !== "object") { | ||||||
|  |                 throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i})); | ||||||
|  |             } | ||||||
|  |             if (!Object.hasOwn(res[i], 'id')) { | ||||||
|  |                 throw new Error(RED._("clipboard.import.errors.missingId",{index:i})); | ||||||
|  |             } | ||||||
|  |             if (!Object.hasOwn(res[i], 'type')) { | ||||||
|  |                 throw new Error(RED._("clipboard.import.errors.missingType",{index:i})); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return res | ||||||
|  |     } | ||||||
|  |  | ||||||
|     var validateImportTimeout; |     var validateImportTimeout; | ||||||
|     function validateImport() { |     function validateImport() { | ||||||
|         if (activeTab === "red-ui-clipboard-dialog-import-tab-clipboard") { |         if (activeTab === "red-ui-clipboard-dialog-import-tab-clipboard") { | ||||||
| @@ -351,21 +375,7 @@ RED.clipboard = (function() { | |||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
|                 try { |                 try { | ||||||
|                     if (!/^\[[\s\S]*\]$/m.test(v)) { |                     validateFlowString(v) | ||||||
|                         throw new Error(RED._("clipboard.import.errors.notArray")); |  | ||||||
|                     } |  | ||||||
|                     var res = JSON.parse(v); |  | ||||||
|                     for (var i=0;i<res.length;i++) { |  | ||||||
|                         if (typeof res[i] !== "object") { |  | ||||||
|                             throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i})); |  | ||||||
|                         } |  | ||||||
|                         if (!res[i].hasOwnProperty('id')) { |  | ||||||
|                             throw new Error(RED._("clipboard.import.errors.missingId",{index:i})); |  | ||||||
|                         } |  | ||||||
|                         if (!res[i].hasOwnProperty('type')) { |  | ||||||
|                             throw new Error(RED._("clipboard.import.errors.missingType",{index:i})); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                     currentPopoverError = null; |                     currentPopoverError = null; | ||||||
|                     popover.close(true); |                     popover.close(true); | ||||||
|                     importInput.removeClass("input-error"); |                     importInput.removeClass("input-error"); | ||||||
| @@ -998,16 +1008,16 @@ RED.clipboard = (function() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     function importNodes(nodesStr,addFlow) { |     function importNodes(nodesStr,addFlow) { | ||||||
|         var newNodes = nodesStr; |         let newNodes = nodesStr; | ||||||
|         if (typeof nodesStr === 'string') { |         if (typeof nodesStr === 'string') { | ||||||
|             try { |             try { | ||||||
|                 nodesStr = nodesStr.trim(); |                 nodesStr = nodesStr.trim(); | ||||||
|                 if (nodesStr.length === 0) { |                 if (nodesStr.length === 0) { | ||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
|                 newNodes = JSON.parse(nodesStr); |                 newNodes = validateFlowString(nodesStr) | ||||||
|             } catch(err) { |             } catch(err) { | ||||||
|                 var e = new Error(RED._("clipboard.invalidFlow",{message:err.message})); |                 const e = new Error(RED._("clipboard.invalidFlow",{message:err.message})); | ||||||
|                 e.code = "NODE_RED"; |                 e.code = "NODE_RED"; | ||||||
|                 throw e; |                 throw e; | ||||||
|             } |             } | ||||||
| @@ -1342,6 +1352,7 @@ RED.clipboard = (function() { | |||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|                     } catch(err) { |                     } catch(err) { | ||||||
|  |                         console.warn('Import failed: ', err) | ||||||
|                         // Ensure any errors throw above doesn't stop the drop target from |                         // Ensure any errors throw above doesn't stop the drop target from | ||||||
|                         // being hidden. |                         // being hidden. | ||||||
|                     } |                     } | ||||||
|   | |||||||
| @@ -54,15 +54,15 @@ RED.contextMenu = (function () { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             const scale = RED.view.scale() | ||||||
|             const offset = $("#red-ui-workspace-chart").offset() |             const offset = $("#red-ui-workspace-chart").offset() | ||||||
|  |             let addX = (options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()) / scale | ||||||
|             let addX = options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft() |             let addY = (options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()) / scale | ||||||
|             let addY = options.y - offset.top + $("#red-ui-workspace-chart").scrollTop() |  | ||||||
|  |  | ||||||
|             if (RED.view.snapGrid) { |             if (RED.view.snapGrid) { | ||||||
|                 const gridSize = RED.view.gridSize() |                 const gridSize = RED.view.gridSize() | ||||||
|                 addX = gridSize * Math.floor(addX / gridSize) |                 addX = gridSize * Math.round(addX / gridSize) | ||||||
|                 addY = gridSize * Math.floor(addY / gridSize) |                 addY = gridSize * Math.round(addY / gridSize) | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             if (RED.settings.theme("menu.menu-item-action-list", true)) { |             if (RED.settings.theme("menu.menu-item-action-list", true)) { | ||||||
| @@ -87,7 +87,9 @@ RED.contextMenu = (function () { | |||||||
|                 }, |                 }, | ||||||
|                 (hasLinks) ? { // has least 1 wire selected |                 (hasLinks) ? { // has least 1 wire selected | ||||||
|                     label: RED._("contextMenu.junction"), |                     label: RED._("contextMenu.junction"), | ||||||
|                     onselect: 'core:split-wires-with-junctions', |                     onselect: function () { | ||||||
|  |                         RED.actions.invoke('core:split-wires-with-junctions', { x: addX, y: addY }) | ||||||
|  |                     }, | ||||||
|                     disabled: !canEdit || !hasLinks |                     disabled: !canEdit || !hasLinks | ||||||
|                 } : { |                 } : { | ||||||
|                     label: RED._("contextMenu.junction"), |                     label: RED._("contextMenu.junction"), | ||||||
|   | |||||||
| @@ -808,6 +808,20 @@ RED.editor = (function() { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             const oldCreds = {}; | ||||||
|  |             if (editing_node._def.credentials) { | ||||||
|  |                 for (const prop in editing_node._def.credentials) { | ||||||
|  |                     if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) { | ||||||
|  |                         if (editing_node._def.credentials[prop].type === 'password') { | ||||||
|  |                             oldCreds['has_' + prop] = editing_node.credentials['has_' + prop]; | ||||||
|  |                         } | ||||||
|  |                         if (prop in editing_node.credentials) { | ||||||
|  |                             oldCreds[prop] = editing_node.credentials[prop]; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|             try { |             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; |                     // Call `oneditsave` and search for changes | ||||||
|                     var configType = type; |                     handleEditSave(editing_config_node, editState); | ||||||
|                     var configAdding = adding; |  | ||||||
|                     var configTypeDef = RED.nodes.getType(configType); |  | ||||||
|                     var d; |  | ||||||
|                     var input; |  | ||||||
|  |  | ||||||
|                     if (configTypeDef.oneditsave) { |                     // Search for changes in the edit box (panes) | ||||||
|                         try { |                     activeEditPanes.forEach(function (pane) { | ||||||
|                             configTypeDef.oneditsave.call(editing_config_node); |  | ||||||
|                         } catch(err) { |  | ||||||
|                             console.warn("oneditsave",editing_config_node.id,editing_config_node.type,err.toString()); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     for (d in configTypeDef.defaults) { |  | ||||||
|                         if (configTypeDef.defaults.hasOwnProperty(d)) { |  | ||||||
|                             var newValue; |  | ||||||
|                             input = $("#node-config-input-"+d); |  | ||||||
|                             if (input.attr('type') === "checkbox") { |  | ||||||
|                                 newValue = input.prop('checked'); |  | ||||||
|                             } else if ("format" in configTypeDef.defaults[d] && configTypeDef.defaults[d].format !== "" && input[0].nodeName === "DIV") { |  | ||||||
|                                 newValue = input.text(); |  | ||||||
|                             } else { |  | ||||||
|                                 newValue = input.val(); |  | ||||||
|                             } |  | ||||||
|                             if (newValue != null && newValue !== editing_config_node[d]) { |  | ||||||
|                                 if (editing_config_node._def.defaults[d].type) { |  | ||||||
|                                     if (newValue == "_ADD_") { |  | ||||||
|                                         newValue = ""; |  | ||||||
|                                     } |  | ||||||
|                                     // Change to a related config node |  | ||||||
|                                     var configNode = RED.nodes.node(editing_config_node[d]); |  | ||||||
|                                     if (configNode) { |  | ||||||
|                                         var users = configNode.users; |  | ||||||
|                                         users.splice(users.indexOf(editing_config_node),1); |  | ||||||
|                                         RED.events.emit("nodes:change",configNode); |  | ||||||
|                                     } |  | ||||||
|                                     configNode = RED.nodes.node(newValue); |  | ||||||
|                                     if (configNode) { |  | ||||||
|                                         configNode.users.push(editing_config_node); |  | ||||||
|                                         RED.events.emit("nodes:change",configNode); |  | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|                                 editing_config_node[d] = newValue; |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     activeEditPanes.forEach(function(pane) { |  | ||||||
|                         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; | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|  |                     // NOTE: must be undefined if no scope used | ||||||
|  |                     const scope = $("#red-ui-editor-config-scope").val() || undefined; | ||||||
|  |  | ||||||
|  |                     // Check if the scope has changed | ||||||
|  |                     if (editing_config_node.z !== scope) { | ||||||
|  |                         editState.changes.z = editing_config_node.z; | ||||||
|  |                         editState.changed = true; | ||||||
|  |                         editing_config_node.z = scope; | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     // Search for nodes that use this config node that are no longer | ||||||
|  |                     // in scope, so must be removed | ||||||
|  |                     const historyEvents = []; | ||||||
|                     if (scope) { |                     if (scope) { | ||||||
|                         // Search for nodes that use this one that are no longer |                         const newUsers = editing_config_node.users.filter(function (node) { | ||||||
|                         // in scope, so must be removed |                             let keepNode = false; | ||||||
|                         editing_config_node.users = editing_config_node.users.filter(function(n) { |                             let nodeModified = null; | ||||||
|                             var keep = true; |  | ||||||
|                             for (var d in n._def.defaults) { |                             for (const d in node._def.defaults) { | ||||||
|                                 if (n._def.defaults.hasOwnProperty(d)) { |                                 if (node._def.defaults.hasOwnProperty(d)) { | ||||||
|                                     if (n._def.defaults[d].type === editing_config_node.type && |                                     if (node._def.defaults[d].type === editing_config_node.type) { | ||||||
|                                         n[d] === editing_config_node.id && |                                         if (node[d] === editing_config_node.id) { | ||||||
|                                         n.z !== scope) { |                                             if (node.z === editing_config_node.z) { | ||||||
|                                             keep = false; |                                                 // The node is kept only if at least one property uses | ||||||
|                                             // Remove the reference to this node |                                                 // this config node in the correct scope. | ||||||
|                                             // and revalidate |                                                 keepNode = true; | ||||||
|                                             n[d] = null; |                                             } else { | ||||||
|                                             n.dirty = true; |                                                 if (!nodeModified) { | ||||||
|                                             n.changed = true; |                                                     nodeModified = { | ||||||
|                                             validateNode(n); |                                                         t: "edit", | ||||||
|  |                                                         node: node, | ||||||
|  |                                                         changes: { [d]: node[d] }, | ||||||
|  |                                                         changed: node.changed, | ||||||
|  |                                                         dirty: node.dirty | ||||||
|  |                                                     }; | ||||||
|  |                                                 } else { | ||||||
|  |                                                     nodeModified.changes[d] = node[d]; | ||||||
|  |                                                 } | ||||||
|  |  | ||||||
|  |                                                 // Remove the reference to the config node | ||||||
|  |                                                 node[d] = ""; | ||||||
|  |                                             } | ||||||
|  |                                         } | ||||||
|                                     } |                                     } | ||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
|                             return keep; |  | ||||||
|                         }); |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     if (configAdding) { |                             // Add the node modified to the history | ||||||
|                         RED.nodes.add(editing_config_node); |                             if (nodeModified) { | ||||||
|                     } |                                 historyEvents.push(nodeModified); | ||||||
|  |  | ||||||
|                     validateNode(editing_config_node); |  | ||||||
|                     var validatedNodes = {}; |  | ||||||
|                     validatedNodes[editing_config_node.id] = true; |  | ||||||
|  |  | ||||||
|                     var userStack = editing_config_node.users.slice(); |  | ||||||
|                     while(userStack.length > 0) { |  | ||||||
|                         var user = userStack.pop(); |  | ||||||
|                         if (!validatedNodes[user.id]) { |  | ||||||
|                             validatedNodes[user.id] = true; |  | ||||||
|                             if (user.users) { |  | ||||||
|                                 userStack = userStack.concat(user.users); |  | ||||||
|                             } |                             } | ||||||
|                             validateNode(user); |  | ||||||
|  |                             // Mark as changed and revalidate this node | ||||||
|  |                             if (!keepNode) { | ||||||
|  |                                 node.changed = true; | ||||||
|  |                                 node.dirty = true; | ||||||
|  |                                 validateNode(node); | ||||||
|  |                                 RED.events.emit("nodes:change", node); | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             return keepNode; | ||||||
|  |                         }); | ||||||
|  |  | ||||||
|  |                         // Check if users are changed | ||||||
|  |                         if (editing_config_node.users.length !== newUsers.length) { | ||||||
|  |                             editState.changes.users = editing_config_node.users; | ||||||
|  |                             editState.changed = true; | ||||||
|  |                             editing_config_node.users = newUsers; | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                     RED.nodes.dirty(true); |  | ||||||
|                     RED.view.redraw(true); |                     if (editState.changed) { | ||||||
|                     if (!configAdding) { |                         // Set the congig node as changed | ||||||
|                         RED.events.emit("editor:save",editing_config_node); |                         editing_config_node.changed = true; | ||||||
|                         RED.events.emit("nodes:change",editing_config_node); |  | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|  |                     // Now, validate the config node | ||||||
|  |                     validateNode(editing_config_node); | ||||||
|  |  | ||||||
|  |                     // And validate nodes using this config node too | ||||||
|  |                     const validatedNodes = new Set(); | ||||||
|  |                     const userStack = editing_config_node.users.slice(); | ||||||
|  |  | ||||||
|  |                     validatedNodes.add(editing_config_node.id); | ||||||
|  |                     while (userStack.length) { | ||||||
|  |                         const node = userStack.pop(); | ||||||
|  |                         if (!validatedNodes.has(node.id)) { | ||||||
|  |                             validatedNodes.add(node.id); | ||||||
|  |                             if (node.users) { | ||||||
|  |                                 userStack.push(...node.users); | ||||||
|  |                             } | ||||||
|  |                             validateNode(node); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     let historyEvent = { | ||||||
|  |                         t: "edit", | ||||||
|  |                         node: editing_config_node, | ||||||
|  |                         changes: editState.changes, | ||||||
|  |                         changed: wasChanged, | ||||||
|  |                         dirty: RED.nodes.dirty() | ||||||
|  |                     }; | ||||||
|  |  | ||||||
|  |                     if (historyEvents.length) { | ||||||
|  |                         // Need a multi events | ||||||
|  |                         historyEvent = { | ||||||
|  |                             t: "multi", | ||||||
|  |                             events: [historyEvent].concat(historyEvents), | ||||||
|  |                             dirty: historyEvent.dirty | ||||||
|  |                         }; | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if (!adding) { | ||||||
|  |                         // This event is triggered when the edit box is saved, | ||||||
|  |                         // regardless of whether there are any modifications. | ||||||
|  |                         RED.events.emit("editor:save", editing_config_node); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if (editState.changed) { | ||||||
|  |                         if (adding) { | ||||||
|  |                             RED.history.push({ t: "add", nodes: [editing_config_node.id], dirty: RED.nodes.dirty() }); | ||||||
|  |                             // Add the new config node and trigger the `nodes:add` event | ||||||
|  |                             RED.nodes.add(editing_config_node); | ||||||
|  |                         } else { | ||||||
|  |                             RED.history.push(historyEvent); | ||||||
|  |                             RED.events.emit("nodes:change", editing_config_node); | ||||||
|  |                         } | ||||||
|  |  | ||||||
|  |                         RED.nodes.dirty(true); | ||||||
|  |                         RED.view.redraw(true); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|                     RED.tray.close(function() { |                     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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |                         } else if (value === node.credentials[cred]) { | ||||||
|  |                             // A cred value exists locally in the editor - no changes | ||||||
|  |                             // Like the user sets a value, saves the config, | ||||||
|  |                             // reopens the config and save the config again | ||||||
|  |                         } else { | ||||||
|  |                             changes['has_' + cred] = node.credentials['has_' + cred]; | ||||||
|  |                             changes[cred] = node.credentials[cred]; | ||||||
|  |                             node.credentials[cred] = value; | ||||||
|                         } |                         } | ||||||
|                         changed = true; |  | ||||||
|  |  | ||||||
|                     } |                         node.credentials['has_' + cred] = (value !== ''); | ||||||
|                     node.credentials[cred] = value; |                     } else { | ||||||
|                     if (value != node.credentials._[cred]) { |                         // Since these creds are loaded by the editor, | ||||||
|                         changed = true; |                         // values can be directly compared | ||||||
|  |                         if (value !== node.credentials[cred]) { | ||||||
|  |                             changes[cred] = node.credentials[cred]; | ||||||
|  |                             node.credentials[cred] = value; | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         return changed; |  | ||||||
|  |         return changes; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| })(); | })(); | ||||||
|   | |||||||
| @@ -1362,7 +1362,7 @@ RED.subflow = (function() { | |||||||
|                         item.value = ""+input.prop("checked"); |                         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) { | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ RED.view.annotations = (function() { | |||||||
|                 } |                 } | ||||||
|                 let badgeRDX = 0; |                 let badgeRDX = 0; | ||||||
|                 let badgeLDX = 0; |                 let badgeLDX = 0; | ||||||
|                  |                 const scale = RED.view.scale() | ||||||
|                 for (let i=0,l=evt.el.__annotations__.length;i<l;i++) { |                 for (let i=0,l=evt.el.__annotations__.length;i<l;i++) { | ||||||
|                     const annotation = evt.el.__annotations__[i]; |                     const annotation = evt.el.__annotations__[i]; | ||||||
|                     if (annotations.hasOwnProperty(annotation.id)) { |                     if (annotations.hasOwnProperty(annotation.id)) { | ||||||
| @@ -42,15 +42,17 @@ RED.view.annotations = (function() { | |||||||
|                         } |                         } | ||||||
|                         if (isBadge) { |                         if (isBadge) { | ||||||
|                             if (showAnnotation) { |                             if (showAnnotation) { | ||||||
|                                 const rect = annotation.element.getBoundingClientRect(); |                                 // getBoundingClientRect is in real-world scale so needs to be adjusted according to | ||||||
|  |                                 // the current scale factor | ||||||
|  |                                 const rectWidth = annotation.element.getBoundingClientRect().width / scale; | ||||||
|                                 let annotationX |                                 let annotationX | ||||||
|                                 if (!opts.align || opts.align === 'right') { |                                 if (!opts.align || opts.align === 'right') { | ||||||
|                                     annotationX = evt.node.w - 3 - badgeRDX - rect.width |                                     annotationX = evt.node.w - 3 - badgeRDX - rectWidth | ||||||
|                                     badgeRDX += rect.width + 4; |                                     badgeRDX += rectWidth + 4; | ||||||
|  |  | ||||||
|                                 } else if (opts.align === 'left') { |                                 } else if (opts.align === 'left') { | ||||||
|                                     annotationX = 3 + badgeLDX |                                     annotationX = 3 + badgeLDX | ||||||
|                                     badgeLDX += rect.width + 4; |                                     badgeLDX += rectWidth + 4; | ||||||
|                                 } |                                 } | ||||||
|                                 annotation.element.setAttribute("transform", "translate("+annotationX+", -8)"); |                                 annotation.element.setAttribute("transform", "translate("+annotationX+", -8)"); | ||||||
|                             } |                             } | ||||||
|   | |||||||
| @@ -1154,11 +1154,11 @@ RED.view.tools = (function() { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function addJunctionsToWires(wires) { |     function addJunctionsToWires(options = {}) { | ||||||
|         if (RED.workspaces.isLocked()) { |         if (RED.workspaces.isLocked()) { | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|         let wiresToSplit = wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link)); |         let wiresToSplit = options.wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link)); | ||||||
|         if (!wiresToSplit) { |         if (!wiresToSplit) { | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
| @@ -1206,21 +1206,26 @@ RED.view.tools = (function() { | |||||||
|             if (links.length === 0) { |             if (links.length === 0) { | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|             let pointCount = 0 |             if (addedJunctions.length === 0 && Object.hasOwn(options, 'x') && Object.hasOwn(options, 'y')) { | ||||||
|             links.forEach(function(l) { |                 junction.x = options.x | ||||||
|                 if (l._sliceLocation) { |                 junction.y = options.y | ||||||
|                     junction.x += l._sliceLocation.x |             } else { | ||||||
|                     junction.y += l._sliceLocation.y |                 let pointCount = 0 | ||||||
|                     delete l._sliceLocation |                 links.forEach(function(l) { | ||||||
|                     pointCount++ |                     if (l._sliceLocation) { | ||||||
|                 } else { |                         junction.x += l._sliceLocation.x | ||||||
|                     junction.x += l.source.x + l.source.w/2 + l.target.x - l.target.w/2 |                         junction.y += l._sliceLocation.y | ||||||
|                     junction.y += l.source.y + l.target.y |                         delete l._sliceLocation | ||||||
|                     pointCount += 2 |                         pointCount++ | ||||||
|                 } |                     } else { | ||||||
|             }) |                         junction.x += l.source.x + l.source.w/2 + l.target.x - l.target.w/2 | ||||||
|             junction.x = Math.round(junction.x/pointCount) |                         junction.y += l.source.y + l.target.y | ||||||
|             junction.y = Math.round(junction.y/pointCount) |                         pointCount += 2 | ||||||
|  |                     } | ||||||
|  |                 }) | ||||||
|  |                 junction.x = Math.round(junction.x/pointCount) | ||||||
|  |                 junction.y = Math.round(junction.y/pointCount) | ||||||
|  |             } | ||||||
|             if (RED.view.snapGrid) { |             if (RED.view.snapGrid) { | ||||||
|                 let gridSize = RED.view.gridSize() |                 let gridSize = RED.view.gridSize() | ||||||
|                 junction.x = (gridSize*Math.round(junction.x/gridSize)); |                 junction.x = (gridSize*Math.round(junction.x/gridSize)); | ||||||
| @@ -1410,7 +1415,7 @@ RED.view.tools = (function() { | |||||||
|             RED.actions.add("core:wire-multiple-to-node", function() { wireMultipleToNode() }) |             RED.actions.add("core:wire-multiple-to-node", function() { wireMultipleToNode() }) | ||||||
|  |  | ||||||
|             RED.actions.add("core:split-wire-with-link-nodes", function () { splitWiresWithLinkNodes() }); |             RED.actions.add("core:split-wire-with-link-nodes", function () { splitWiresWithLinkNodes() }); | ||||||
|             RED.actions.add("core:split-wires-with-junctions", function () { addJunctionsToWires() }); |             RED.actions.add("core:split-wires-with-junctions", function (options) { addJunctionsToWires(options) }); | ||||||
|  |  | ||||||
|             RED.actions.add("core:generate-node-names", generateNodeNames ) |             RED.actions.add("core:generate-node-names", generateNodeNames ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -321,8 +321,8 @@ RED.view = (function() { | |||||||
|             evt.stopPropagation() |             evt.stopPropagation() | ||||||
|             RED.contextMenu.show({ |             RED.contextMenu.show({ | ||||||
|                 type: 'workspace', |                 type: 'workspace', | ||||||
|                 x:evt.clientX-5, |                 x: evt.clientX, | ||||||
|                 y:evt.clientY-5 |                 y: evt.clientY | ||||||
|             }) |             }) | ||||||
|             return false |             return false | ||||||
|         }) |         }) | ||||||
| @@ -5174,8 +5174,8 @@ RED.view = (function() { | |||||||
|                                 var delta = Infinity; |                                 var delta = Infinity; | ||||||
|                                 for (var i = 0; i < lineLength; i++) { |                                 for (var i = 0; i < lineLength; i++) { | ||||||
|                                     var linePos = pathLine.getPointAtLength(i); |                                     var linePos = pathLine.getPointAtLength(i); | ||||||
|                                     var posDeltaX = Math.abs(linePos.x-d3.event.offsetX) |                                     var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor)) | ||||||
|                                     var posDeltaY = Math.abs(linePos.y-d3.event.offsetY) |                                     var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor)) | ||||||
|                                     var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY |                                     var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY | ||||||
|                                     if (posDelta < delta) { |                                     if (posDelta < delta) { | ||||||
|                                         pos = linePos |                                         pos = linePos | ||||||
|   | |||||||
| @@ -291,43 +291,23 @@ module.exports = function(RED) { | |||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 else if (!msg.hasOwnProperty("reset")) { |                 else if (!msg.hasOwnProperty("reset")) { | ||||||
|                     if (maxKeptMsgsCount(node) > 0) { |                     if (node.allowrate && msg.hasOwnProperty("rate") && !isNaN(parseFloat(msg.rate))) { | ||||||
|                         if (node.intervalID === -1) { |                         node.rate = msg.rate; | ||||||
|                             node.send(msg); |  | ||||||
|                             node.intervalID = setInterval(sendMsgFromBuffer, node.rate); |  | ||||||
|                         } else { |  | ||||||
|                             if (node.allowrate && msg.hasOwnProperty("rate") && !isNaN(parseFloat(msg.rate)) && node.rate !== msg.rate) { |  | ||||||
|                                 node.rate = msg.rate; |  | ||||||
|                                 clearInterval(node.intervalID); |  | ||||||
|                                 node.intervalID = setInterval(sendMsgFromBuffer, node.rate); |  | ||||||
|                             } |  | ||||||
|                             if (node.buffer.length < _maxKeptMsgsCount) { |  | ||||||
|                                 var m = RED.util.cloneMessage(msg); |  | ||||||
|                                 node.buffer.push({msg: m, send: send, done: done}); |  | ||||||
|                             } else { |  | ||||||
|                                 node.trace("dropped due to buffer overflow. msg._msgid = " + msg._msgid); |  | ||||||
|                                 node.droppedMsgs++; |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                     } |                     } | ||||||
|                     else { |                     var timeSinceLast; | ||||||
|                         if (node.allowrate && msg.hasOwnProperty("rate") && !isNaN(parseFloat(msg.rate))) { |                     if (node.lastSent) { | ||||||
|                             node.rate = msg.rate; |                         timeSinceLast = process.hrtime(node.lastSent); | ||||||
|                         } |                     } | ||||||
|                         var timeSinceLast; |                     if (!node.lastSent) { // ensuring that we always send the first message | ||||||
|                         if (node.lastSent) { |                         node.lastSent = process.hrtime(); | ||||||
|                             timeSinceLast = process.hrtime(node.lastSent); |                         send(msg); | ||||||
|                         } |                     } | ||||||
|                         if (!node.lastSent) { // ensuring that we always send the first message |                     else if ( ( (timeSinceLast[0] * SECONDS_TO_NANOS) + timeSinceLast[1] ) > (node.rate * MILLIS_TO_NANOS) ) { | ||||||
|                             node.lastSent = process.hrtime(); |                         node.lastSent = process.hrtime(); | ||||||
|                             send(msg); |                         send(msg); | ||||||
|                         } |                     } | ||||||
|                         else if ( ( (timeSinceLast[0] * SECONDS_TO_NANOS) + timeSinceLast[1] ) > (node.rate * MILLIS_TO_NANOS) ) { |                     else if (node.outputs === 2) { | ||||||
|                             node.lastSent = process.hrtime(); |                         send([null,msg]) | ||||||
|                             send(msg); |  | ||||||
|                         } else if (node.outputs === 2) { |  | ||||||
|                             send([null,msg]) |  | ||||||
|                         } |  | ||||||
|                     } |                     } | ||||||
|                     done(); |                     done(); | ||||||
|                 } |                 } | ||||||
|   | |||||||
| @@ -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 = { | ||||||
|   | |||||||
| @@ -339,7 +339,7 @@ module.exports = function(RED) { | |||||||
|             } |             } | ||||||
|             else { |             else { | ||||||
|                 msg.filename = filename; |                 msg.filename = filename; | ||||||
|                 var lines = Buffer.from([]); |                 const bufferArray = []; | ||||||
|                 var spare = ""; |                 var spare = ""; | ||||||
|                 var count = 0; |                 var count = 0; | ||||||
|                 var type = "buffer"; |                 var type = "buffer"; | ||||||
| @@ -397,7 +397,7 @@ module.exports = function(RED) { | |||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
|                             else { |                             else { | ||||||
|                                 lines = Buffer.concat([lines,chunk]); |                                 bufferArray.push(chunk); | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|                     }) |                     }) | ||||||
| @@ -413,10 +413,11 @@ module.exports = function(RED) { | |||||||
|                     }) |                     }) | ||||||
|                     .on('end', function() { |                     .on('end', function() { | ||||||
|                         if (node.chunk === false) { |                         if (node.chunk === false) { | ||||||
|  |                             const buffer = Buffer.concat(bufferArray); | ||||||
|                             if (node.format === "utf8") { |                             if (node.format === "utf8") { | ||||||
|                                 msg.payload = decode(lines, node.encoding); |                                 msg.payload = decode(buffer, node.encoding); | ||||||
|                             } |                             } | ||||||
|                             else { msg.payload = lines; } |                             else { msg.payload = buffer; } | ||||||
|                             nodeSend(msg); |                             nodeSend(msg); | ||||||
|                         } |                         } | ||||||
|                         else if (node.format === "lines") { |                         else if (node.format === "lines") { | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "@node-red/nodes", |     "name": "@node-red/nodes", | ||||||
|     "version": "4.0.5", |     "version": "4.0.6", | ||||||
|     "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.6", | ||||||
|     "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.6", | ||||||
|         "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", | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "@node-red/runtime", |     "name": "@node-red/runtime", | ||||||
|     "version": "4.0.5", |     "version": "4.0.6", | ||||||
|     "license": "Apache-2.0", |     "license": "Apache-2.0", | ||||||
|     "main": "./lib/index.js", |     "main": "./lib/index.js", | ||||||
|     "repository": { |     "repository": { | ||||||
| @@ -16,11 +16,11 @@ | |||||||
|         } |         } | ||||||
|     ], |     ], | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|         "@node-red/registry": "4.0.5", |         "@node-red/registry": "4.0.6", | ||||||
|         "@node-red/util": "4.0.5", |         "@node-red/util": "4.0.6", | ||||||
|         "async-mutex": "0.5.0", |         "async-mutex": "0.5.0", | ||||||
|         "clone": "2.1.2", |         "clone": "2.1.2", | ||||||
|         "express": "4.21.1", |         "express": "4.21.2", | ||||||
|         "fs-extra": "11.2.0", |         "fs-extra": "11.2.0", | ||||||
|         "json-stringify-safe": "5.0.1", |         "json-stringify-safe": "5.0.1", | ||||||
|         "rfdc": "^1.3.1" |         "rfdc": "^1.3.1" | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "@node-red/util", |     "name": "@node-red/util", | ||||||
|     "version": "4.0.5", |     "version": "4.0.6", | ||||||
|     "license": "Apache-2.0", |     "license": "Apache-2.0", | ||||||
|     "repository": { |     "repository": { | ||||||
|         "type": "git", |         "type": "git", | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								packages/node_modules/node-red/package.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								packages/node_modules/node-red/package.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|     "name": "node-red", |     "name": "node-red", | ||||||
|     "version": "4.0.5", |     "version": "4.0.6", | ||||||
|     "description": "Low-code programming for event-driven applications", |     "description": "Low-code programming for event-driven applications", | ||||||
|     "homepage": "https://nodered.org", |     "homepage": "https://nodered.org", | ||||||
|     "license": "Apache-2.0", |     "license": "Apache-2.0", | ||||||
| @@ -31,14 +31,14 @@ | |||||||
|         "flow" |         "flow" | ||||||
|     ], |     ], | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|         "@node-red/editor-api": "4.0.5", |         "@node-red/editor-api": "4.0.6", | ||||||
|         "@node-red/runtime": "4.0.5", |         "@node-red/runtime": "4.0.6", | ||||||
|         "@node-red/util": "4.0.5", |         "@node-red/util": "4.0.6", | ||||||
|         "@node-red/nodes": "4.0.5", |         "@node-red/nodes": "4.0.6", | ||||||
|         "basic-auth": "2.0.1", |         "basic-auth": "2.0.1", | ||||||
|         "bcryptjs": "2.4.3", |         "bcryptjs": "2.4.3", | ||||||
|         "cors": "2.8.5", |         "cors": "2.8.5", | ||||||
|         "express": "4.21.1", |         "express": "4.21.2", | ||||||
|         "fs-extra": "11.2.0", |         "fs-extra": "11.2.0", | ||||||
|         "node-red-admin": "^4.0.1", |         "node-red-admin": "^4.0.1", | ||||||
|         "nopt": "5.0.0", |         "nopt": "5.0.0", | ||||||
|   | |||||||
| @@ -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