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 | ||||
|  | ||||
| Editor | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "node-red", | ||||
|     "version": "4.0.5", | ||||
|     "version": "4.0.6", | ||||
|     "description": "Low-code programming for event-driven applications", | ||||
|     "homepage": "https://nodered.org", | ||||
|     "license": "Apache-2.0", | ||||
| @@ -41,7 +41,7 @@ | ||||
|         "cors": "2.8.5", | ||||
|         "cronosjs": "1.7.1", | ||||
|         "denque": "2.1.0", | ||||
|         "express": "4.21.1", | ||||
|         "express": "4.21.2", | ||||
|         "express-session": "1.18.1", | ||||
|         "form-data": "4.0.0", | ||||
|         "fs-extra": "11.2.0", | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@node-red/editor-api", | ||||
|     "version": "4.0.5", | ||||
|     "version": "4.0.6", | ||||
|     "license": "Apache-2.0", | ||||
|     "main": "./lib/index.js", | ||||
|     "repository": { | ||||
| @@ -16,14 +16,14 @@ | ||||
|         } | ||||
|     ], | ||||
|     "dependencies": { | ||||
|         "@node-red/util": "4.0.5", | ||||
|         "@node-red/editor-client": "4.0.5", | ||||
|         "@node-red/util": "4.0.6", | ||||
|         "@node-red/editor-client": "4.0.6", | ||||
|         "bcryptjs": "2.4.3", | ||||
|         "body-parser": "1.20.3", | ||||
|         "clone": "2.1.2", | ||||
|         "cors": "2.8.5", | ||||
|         "express-session": "1.18.1", | ||||
|         "express": "4.21.1", | ||||
|         "express": "4.21.2", | ||||
|         "memorystore": "1.6.7", | ||||
|         "mime": "3.0.0", | ||||
|         "multer": "1.4.5-lts.1", | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@node-red/editor-client", | ||||
|     "version": "4.0.5", | ||||
|     "version": "4.0.6", | ||||
|     "license": "Apache-2.0", | ||||
|     "repository": { | ||||
|         "type": "git", | ||||
|   | ||||
| @@ -453,10 +453,61 @@ RED.history = (function() { | ||||
|                                     RED.events.emit("nodes:change",newConfigNode); | ||||
|                                 } | ||||
|                             }); | ||||
|                         } else if (i === "env" && ev.node.type.indexOf("subflow:") === 0) { | ||||
|                             // Subflow can have config node in node.env | ||||
|                             let nodeList = ev.node.env || []; | ||||
|                             nodeList = nodeList.reduce((list, prop) => { | ||||
|                                 if (prop.type === "conf-type" && prop.value) { | ||||
|                                     list.push(prop.value); | ||||
|                                 } | ||||
|                                 return list; | ||||
|                             }, []); | ||||
|  | ||||
|                             nodeList.forEach(function(id) { | ||||
|                                 const configNode = RED.nodes.node(id); | ||||
|                                 if (configNode) { | ||||
|                                     if (configNode.users.indexOf(ev.node) !== -1) { | ||||
|                                         configNode.users.splice(configNode.users.indexOf(ev.node), 1); | ||||
|                                         RED.events.emit("nodes:change", configNode); | ||||
|                                     } | ||||
|                                 } | ||||
|                             }); | ||||
|  | ||||
|                             nodeList = ev.changes.env || []; | ||||
|                             nodeList = nodeList.reduce((list, prop) => { | ||||
|                                 if (prop.type === "conf-type" && prop.value) { | ||||
|                                     list.push(prop.value); | ||||
|                                 } | ||||
|                                 return list; | ||||
|                             }, []); | ||||
|  | ||||
|                             nodeList.forEach(function(id) { | ||||
|                                 const configNode = RED.nodes.node(id); | ||||
|                                 if (configNode) { | ||||
|                                     if (configNode.users.indexOf(ev.node) === -1) { | ||||
|                                         configNode.users.push(ev.node); | ||||
|                                         RED.events.emit("nodes:change", configNode); | ||||
|                                     } | ||||
|                                 } | ||||
|                             }); | ||||
|                         } | ||||
|                         if (i === "credentials" && ev.changes[i]) { | ||||
|                             // Reset - Only want to keep the changes | ||||
|                             inverseEv.changes[i] = {}; | ||||
|                             for (const [key, value] of Object.entries(ev.changes[i])) { | ||||
|                                 // Edge case: node.credentials is cleared after a deploy, so we can't | ||||
|                                 // capture values for the inverse event when undoing past a deploy | ||||
|                                 if (ev.node.credentials) { | ||||
|                                     inverseEv.changes[i][key] = ev.node.credentials[key]; | ||||
|                                 } | ||||
|                                 ev.node.credentials[key] = value; | ||||
|                             } | ||||
|                         } else { | ||||
|                             ev.node[i] = ev.changes[i]; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 ev.node.dirty = true; | ||||
|                 ev.node.changed = ev.changed; | ||||
|  | ||||
| @@ -536,6 +587,24 @@ RED.history = (function() { | ||||
|                     RED.editor.updateNodeProperties(ev.node,outputMap); | ||||
|                     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) { | ||||
|                     inverseEv.createdLinks = []; | ||||
|                     for (i=0;i<ev.links.length;i++) { | ||||
|   | ||||
| @@ -708,11 +708,11 @@ RED.nodes = (function() { | ||||
|             n["_"] = RED._; | ||||
|         } | ||||
|         if (n._def.category == "config") { | ||||
|             configNodes[n.id] = n; | ||||
|             configNodes[n.id] = newNode; | ||||
|         } else { | ||||
|             if (n.wires && (n.wires.length > n.outputs)) { n.outputs = n.wires.length; } | ||||
|             n.dirty = true; | ||||
|             updateConfigNodeUsers(n); | ||||
|             updateConfigNodeUsers(newNode, { action: "add" }); | ||||
|             if (n._def.category == "subflows" && typeof n.i === "undefined") { | ||||
|                 var nextId = 0; | ||||
|                 RED.nodes.eachNode(function(node) { | ||||
| @@ -785,6 +785,7 @@ RED.nodes = (function() { | ||||
|             delete nodeLinks[id]; | ||||
|             removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); }); | ||||
|             removedLinks.forEach(removeLink); | ||||
|             updateConfigNodeUsers(node, { action: "remove" }); | ||||
|             var updatedConfigNode = false; | ||||
|             for (var d in node._def.defaults) { | ||||
|                 if (node._def.defaults.hasOwnProperty(d)) { | ||||
| @@ -798,10 +799,6 @@ RED.nodes = (function() { | ||||
|                                 if (configNode._def.exclusive) { | ||||
|                                     removeNode(node[d]); | ||||
|                                     removedNodes.push(configNode); | ||||
|                                 } else { | ||||
|                                     var users = configNode.users; | ||||
|                                     users.splice(users.indexOf(node),1); | ||||
|                                     RED.events.emit('nodes:change',configNode) | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
| @@ -1798,9 +1795,20 @@ RED.nodes = (function() { | ||||
|         // Replace config nodes | ||||
|         // | ||||
|         configNodeIds.forEach(function(id) { | ||||
|             removedNodes = removedNodes.concat(convertNode(getNode(id))); | ||||
|             const configNode = getNode(id); | ||||
|             const currentUserCount = configNode.users; | ||||
|  | ||||
|             // Add a snapshot of the Config Node | ||||
|             removedNodes = removedNodes.concat(convertNode(configNode)); | ||||
|  | ||||
|             // Remove the Config Node instance | ||||
|             removeNode(id); | ||||
|             importNodes([newConfigNodes[id]]) | ||||
|  | ||||
|             // Import the new one | ||||
|             importNodes([newConfigNodes[id]]); | ||||
|  | ||||
|             // Re-attributes the user count | ||||
|             getNode(id).users = currentUserCount; | ||||
|         }); | ||||
|  | ||||
|         return { | ||||
| @@ -2349,29 +2357,31 @@ RED.nodes = (function() { | ||||
|                             node.type = "unknown"; | ||||
|                         } | ||||
|                         if (node._def.category != "config") { | ||||
|                             if (n.hasOwnProperty('inputs')) { | ||||
|                                 node.inputs = n.inputs; | ||||
|                             if (n.hasOwnProperty('inputs') && def.defaults.hasOwnProperty("inputs")) { | ||||
|                                 node.inputs = parseInt(n.inputs, 10); | ||||
|                                 node._config.inputs = JSON.stringify(n.inputs); | ||||
|                             } else { | ||||
|                                 node.inputs = node._def.inputs; | ||||
|                             } | ||||
|                             if (n.hasOwnProperty('outputs')) { | ||||
|                                 node.outputs = n.outputs; | ||||
|                             if (n.hasOwnProperty('outputs') && def.defaults.hasOwnProperty("outputs")) { | ||||
|                                 node.outputs = parseInt(n.outputs, 10); | ||||
|                                 node._config.outputs = JSON.stringify(n.outputs); | ||||
|                             } else { | ||||
|                                 node.outputs = node._def.outputs; | ||||
|                             } | ||||
|                             if (node.hasOwnProperty('wires') && node.wires.length > node.outputs) { | ||||
|                                 if (!node._def.defaults.hasOwnProperty("outputs") || !isNaN(parseInt(n.outputs))) { | ||||
|                                     // If 'wires' is longer than outputs, clip wires | ||||
|                                     console.log("Warning: node.wires longer than node.outputs - trimming wires:",node.id," wires:",node.wires.length," outputs:",node.outputs); | ||||
|                                     node.wires = node.wires.slice(0,node.outputs); | ||||
|                                 } else { | ||||
|  | ||||
|                             // The node declares outputs in its defaults, but has not got a valid value | ||||
|                             // Defer to the length of the wires array | ||||
|                             if (node.hasOwnProperty('wires')) { | ||||
|                                 if (isNaN(node.outputs)) { | ||||
|                                     node.outputs = node.wires.length; | ||||
|                                 } else if (node.wires.length > node.outputs) { | ||||
|                                     // If 'wires' is longer than outputs, clip wires | ||||
|                                     console.log("Warning: node.wires longer than node.outputs - trimming wires:", node.id, " wires:", node.wires.length, " outputs:", node.outputs); | ||||
|                                     node.wires = node.wires.slice(0, node.outputs); | ||||
|                                 } | ||||
|                             } | ||||
|  | ||||
|                             for (d in node._def.defaults) { | ||||
|                                 if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') { | ||||
|                                     node[d] = n[d]; | ||||
| @@ -2468,11 +2478,6 @@ RED.nodes = (function() { | ||||
|                         nodeList = nodeList.map(function(id) { | ||||
|                             var node = node_map[id]; | ||||
|                             if (node) { | ||||
|                                 if (node._def.category === 'config') { | ||||
|                                     if (node.users.indexOf(n) === -1) { | ||||
|                                         node.users.push(n); | ||||
|                                     } | ||||
|                                 } | ||||
|                                 return node.id; | ||||
|                             } | ||||
|                             return id; | ||||
| @@ -2699,19 +2704,42 @@ RED.nodes = (function() { | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     // Update any config nodes referenced by the provided node to ensure their 'users' list is correct | ||||
|     function updateConfigNodeUsers(n) { | ||||
|         for (var d in n._def.defaults) { | ||||
|             if (n._def.defaults.hasOwnProperty(d)) { | ||||
|                 var property = n._def.defaults[d]; | ||||
|     /** | ||||
|      * Update any config nodes referenced by the provided node to ensure | ||||
|      * their 'users' list is correct. | ||||
|      * | ||||
|      * @param {object} node The node in which to check if it contains references | ||||
|      * @param {object} options Options to apply. | ||||
|      * @param {"add" | "remove"} [options.action] Add or remove the node from | ||||
|      * the Config Node users list. Default `add`. | ||||
|      * @param {boolean} [options.emitEvent] Emit the `nodes:changes` event. | ||||
|      * Default true. | ||||
|      */ | ||||
|     function updateConfigNodeUsers(node, options) { | ||||
|         const defaultOptions = { action: "add", emitEvent: true }; | ||||
|         options = Object.assign({}, defaultOptions, options); | ||||
|  | ||||
|         for (var d in node._def.defaults) { | ||||
|             if (node._def.defaults.hasOwnProperty(d)) { | ||||
|                 var property = node._def.defaults[d]; | ||||
|                 if (property.type) { | ||||
|                     var type = registry.getNodeType(property.type); | ||||
|                     if (type && type.category == "config") { | ||||
|                         var configNode = configNodes[n[d]]; | ||||
|                         var configNode = configNodes[node[d]]; | ||||
|                         if (configNode) { | ||||
|                             if (configNode.users.indexOf(n) === -1) { | ||||
|                                 configNode.users.push(n); | ||||
|                                 RED.events.emit('nodes:change',configNode) | ||||
|                             if (options.action === "add") { | ||||
|                                 if (configNode.users.indexOf(node) === -1) { | ||||
|                                     configNode.users.push(node); | ||||
|                                     if (options.emitEvent) { | ||||
|                                         RED.events.emit('nodes:change', configNode); | ||||
|                                     } | ||||
|                                 } | ||||
|                             } else if (options.action === "remove") { | ||||
|                                 if (configNode.users.indexOf(node) !== -1) { | ||||
|                                     const users = configNode.users; | ||||
|                                     users.splice(users.indexOf(node), 1); | ||||
|                                     if (options.emitEvent) { | ||||
|                                         RED.events.emit('nodes:change', configNode); | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
| @@ -2719,6 +2747,36 @@ RED.nodes = (function() { | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Subflows can have config node env | ||||
|         if (node.type.indexOf("subflow:") === 0) { | ||||
|             node.env?.forEach((prop) => { | ||||
|                 if (prop.type === "conf-type" && prop.value) { | ||||
|                     // Add the node to the config node users | ||||
|                     const configNode = getNode(prop.value); | ||||
|                     if (configNode) { | ||||
|                         if (options.action === "add") { | ||||
|                             if (configNode.users.indexOf(node) === -1) { | ||||
|                                 configNode.users.push(node); | ||||
|                                 if (options.emitEvent) { | ||||
|                                     RED.events.emit('nodes:change', configNode); | ||||
|                                 } | ||||
|                             } | ||||
|                         } else if (options.action === "remove") { | ||||
|                             if (configNode.users.indexOf(node) !== -1) { | ||||
|                                 const users = configNode.users; | ||||
|                                 users.splice(users.indexOf(node), 1); | ||||
|                                 if (options.emitEvent) { | ||||
|                                     RED.events.emit('nodes:change', configNode); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function flowVersion(version) { | ||||
|         if (version !== undefined) { | ||||
|   | ||||
| @@ -334,6 +334,30 @@ RED.clipboard = (function() { | ||||
|         },100); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validates if the provided string looks like valid flow json | ||||
|      * @param {string} flowString the string to validate | ||||
|      * @returns If valid, returns the node array | ||||
|      */ | ||||
|     function validateFlowString(flowString) { | ||||
|         const res = JSON.parse(flowString) | ||||
|         if (!Array.isArray(res)) { | ||||
|             throw new Error(RED._("clipboard.import.errors.notArray")); | ||||
|         } | ||||
|         for (let i = 0; i < res.length; i++) { | ||||
|             if (typeof res[i] !== "object") { | ||||
|                 throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i})); | ||||
|             } | ||||
|             if (!Object.hasOwn(res[i], 'id')) { | ||||
|                 throw new Error(RED._("clipboard.import.errors.missingId",{index:i})); | ||||
|             } | ||||
|             if (!Object.hasOwn(res[i], 'type')) { | ||||
|                 throw new Error(RED._("clipboard.import.errors.missingType",{index:i})); | ||||
|             } | ||||
|         } | ||||
|         return res | ||||
|     } | ||||
|  | ||||
|     var validateImportTimeout; | ||||
|     function validateImport() { | ||||
|         if (activeTab === "red-ui-clipboard-dialog-import-tab-clipboard") { | ||||
| @@ -351,21 +375,7 @@ RED.clipboard = (function() { | ||||
|                     return; | ||||
|                 } | ||||
|                 try { | ||||
|                     if (!/^\[[\s\S]*\]$/m.test(v)) { | ||||
|                         throw new Error(RED._("clipboard.import.errors.notArray")); | ||||
|                     } | ||||
|                     var res = JSON.parse(v); | ||||
|                     for (var i=0;i<res.length;i++) { | ||||
|                         if (typeof res[i] !== "object") { | ||||
|                             throw new Error(RED._("clipboard.import.errors.itemNotObject",{index:i})); | ||||
|                         } | ||||
|                         if (!res[i].hasOwnProperty('id')) { | ||||
|                             throw new Error(RED._("clipboard.import.errors.missingId",{index:i})); | ||||
|                         } | ||||
|                         if (!res[i].hasOwnProperty('type')) { | ||||
|                             throw new Error(RED._("clipboard.import.errors.missingType",{index:i})); | ||||
|                         } | ||||
|                     } | ||||
|                     validateFlowString(v) | ||||
|                     currentPopoverError = null; | ||||
|                     popover.close(true); | ||||
|                     importInput.removeClass("input-error"); | ||||
| @@ -998,16 +1008,16 @@ RED.clipboard = (function() { | ||||
|     } | ||||
|  | ||||
|     function importNodes(nodesStr,addFlow) { | ||||
|         var newNodes = nodesStr; | ||||
|         let newNodes = nodesStr; | ||||
|         if (typeof nodesStr === 'string') { | ||||
|             try { | ||||
|                 nodesStr = nodesStr.trim(); | ||||
|                 if (nodesStr.length === 0) { | ||||
|                     return; | ||||
|                 } | ||||
|                 newNodes = JSON.parse(nodesStr); | ||||
|                 newNodes = validateFlowString(nodesStr) | ||||
|             } catch(err) { | ||||
|                 var e = new Error(RED._("clipboard.invalidFlow",{message:err.message})); | ||||
|                 const e = new Error(RED._("clipboard.invalidFlow",{message:err.message})); | ||||
|                 e.code = "NODE_RED"; | ||||
|                 throw e; | ||||
|             } | ||||
| @@ -1342,6 +1352,7 @@ RED.clipboard = (function() { | ||||
|                             } | ||||
|                         } | ||||
|                     } catch(err) { | ||||
|                         console.warn('Import failed: ', err) | ||||
|                         // Ensure any errors throw above doesn't stop the drop target from | ||||
|                         // being hidden. | ||||
|                     } | ||||
|   | ||||
| @@ -54,15 +54,15 @@ RED.contextMenu = (function () { | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             const scale = RED.view.scale() | ||||
|             const offset = $("#red-ui-workspace-chart").offset() | ||||
|  | ||||
|             let addX = options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft() | ||||
|             let addY = options.y - offset.top + $("#red-ui-workspace-chart").scrollTop() | ||||
|             let addX = (options.x - offset.left + $("#red-ui-workspace-chart").scrollLeft()) / scale | ||||
|             let addY = (options.y - offset.top + $("#red-ui-workspace-chart").scrollTop()) / scale | ||||
|  | ||||
|             if (RED.view.snapGrid) { | ||||
|                 const gridSize = RED.view.gridSize() | ||||
|                 addX = gridSize * Math.floor(addX / gridSize) | ||||
|                 addY = gridSize * Math.floor(addY / gridSize) | ||||
|                 addX = gridSize * Math.round(addX / gridSize) | ||||
|                 addY = gridSize * Math.round(addY / gridSize) | ||||
|             } | ||||
|  | ||||
|             if (RED.settings.theme("menu.menu-item-action-list", true)) { | ||||
| @@ -87,7 +87,9 @@ RED.contextMenu = (function () { | ||||
|                 }, | ||||
|                 (hasLinks) ? { // has least 1 wire selected | ||||
|                     label: RED._("contextMenu.junction"), | ||||
|                     onselect: 'core:split-wires-with-junctions', | ||||
|                     onselect: function () { | ||||
|                         RED.actions.invoke('core:split-wires-with-junctions', { x: addX, y: addY }) | ||||
|                     }, | ||||
|                     disabled: !canEdit || !hasLinks | ||||
|                 } : { | ||||
|                     label: RED._("contextMenu.junction"), | ||||
|   | ||||
| @@ -808,6 +808,20 @@ RED.editor = (function() { | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             const oldCreds = {}; | ||||
|             if (editing_node._def.credentials) { | ||||
|                 for (const prop in editing_node._def.credentials) { | ||||
|                     if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) { | ||||
|                         if (editing_node._def.credentials[prop].type === 'password') { | ||||
|                             oldCreds['has_' + prop] = editing_node.credentials['has_' + prop]; | ||||
|                         } | ||||
|                         if (prop in editing_node.credentials) { | ||||
|                             oldCreds[prop] = editing_node.credentials[prop]; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             try { | ||||
|                 const rc = editing_node._def.oneditsave.call(editing_node); | ||||
|                 if (rc === true) { | ||||
| @@ -839,6 +853,25 @@ RED.editor = (function() { | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (editing_node._def.credentials) { | ||||
|                 for (const prop in editing_node._def.credentials) { | ||||
|                     if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) { | ||||
|                         if (oldCreds[prop] !== editing_node.credentials[prop]) { | ||||
|                             if (editing_node.credentials[prop] === '__PWRD__') { | ||||
|                                 // The password may not exist in oldCreds | ||||
|                                 // The value '__PWRD__' means the password exists, | ||||
|                                 // so ignore this change | ||||
|                                 continue; | ||||
|                             } | ||||
|                             editState.changes.credentials = editState.changes.credentials || {}; | ||||
|                             editState.changes.credentials['has_' + prop] = oldCreds['has_' + prop]; | ||||
|                             editState.changes.credentials[prop] = oldCreds[prop]; | ||||
|                             editState.changed = true; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -1481,134 +1514,181 @@ RED.editor = (function() { | ||||
|             }, | ||||
|             { | ||||
|                 id: "node-config-dialog-ok", | ||||
|                 text: adding?RED._("editor.configAdd"):RED._("editor.configUpdate"), | ||||
|                 text: adding ? RED._("editor.configAdd") : RED._("editor.configUpdate"), | ||||
|                 class: "primary", | ||||
|                 click: function() { | ||||
|                     var editState = { | ||||
|                     // TODO: Already defined | ||||
|                     const configProperty = name; | ||||
|                     const configType = type; | ||||
|                     const configTypeDef = RED.nodes.getType(configType); | ||||
|  | ||||
|                     const wasChanged = editing_config_node.changed; | ||||
|                     const editState = { | ||||
|                         changes: {}, | ||||
|                         changed: false, | ||||
|                         outputMap: null | ||||
|                     }; | ||||
|                     var configProperty = name; | ||||
|                     var configId = editing_config_node.id; | ||||
|                     var configType = type; | ||||
|                     var configAdding = adding; | ||||
|                     var configTypeDef = RED.nodes.getType(configType); | ||||
|                     var d; | ||||
|                     var input; | ||||
|                      | ||||
|                     if (configTypeDef.oneditsave) { | ||||
|                         try { | ||||
|                             configTypeDef.oneditsave.call(editing_config_node); | ||||
|                         } catch(err) { | ||||
|                             console.warn("oneditsave",editing_config_node.id,editing_config_node.type,err.toString()); | ||||
|                         } | ||||
|                     } | ||||
|                     // Call `oneditsave` and search for changes | ||||
|                     handleEditSave(editing_config_node, editState); | ||||
|  | ||||
|                     for (d in configTypeDef.defaults) { | ||||
|                         if (configTypeDef.defaults.hasOwnProperty(d)) { | ||||
|                             var newValue; | ||||
|                             input = $("#node-config-input-"+d); | ||||
|                             if (input.attr('type') === "checkbox") { | ||||
|                                 newValue = input.prop('checked'); | ||||
|                             } else if ("format" in configTypeDef.defaults[d] && configTypeDef.defaults[d].format !== "" && input[0].nodeName === "DIV") { | ||||
|                                 newValue = input.text(); | ||||
|                             } else { | ||||
|                                 newValue = input.val(); | ||||
|                             } | ||||
|                             if (newValue != null && newValue !== editing_config_node[d]) { | ||||
|                                 if (editing_config_node._def.defaults[d].type) { | ||||
|                                     if (newValue == "_ADD_") { | ||||
|                                         newValue = ""; | ||||
|                                     } | ||||
|                                     // Change to a related config node | ||||
|                                     var configNode = RED.nodes.node(editing_config_node[d]); | ||||
|                                     if (configNode) { | ||||
|                                         var users = configNode.users; | ||||
|                                         users.splice(users.indexOf(editing_config_node),1); | ||||
|                                         RED.events.emit("nodes:change",configNode); | ||||
|                                     } | ||||
|                                     configNode = RED.nodes.node(newValue); | ||||
|                                     if (configNode) { | ||||
|                                         configNode.users.push(editing_config_node); | ||||
|                                         RED.events.emit("nodes:change",configNode); | ||||
|                                     } | ||||
|                                 } | ||||
|                                 editing_config_node[d] = newValue; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     activeEditPanes.forEach(function(pane) { | ||||
|                     // Search for changes in the edit box (panes) | ||||
|                     activeEditPanes.forEach(function (pane) { | ||||
|                         if (pane.apply) { | ||||
|                             pane.apply.call(pane, editState); | ||||
|                         } | ||||
|                     }) | ||||
|                     }); | ||||
|  | ||||
|                     editing_config_node.label = configTypeDef.label; | ||||
|  | ||||
|                     var scope = $("#red-ui-editor-config-scope").val(); | ||||
|                     editing_config_node.z = scope; | ||||
|                     // TODO: Why? | ||||
|                     editing_config_node.label = configTypeDef.label | ||||
|  | ||||
|                     // Check if disabled has changed | ||||
|                     if ($("#node-config-input-node-disabled").prop('checked')) { | ||||
|                         if (editing_config_node.d !== true) { | ||||
|                             editState.changes.d = editing_config_node.d; | ||||
|                             editState.changed = true; | ||||
|                             editing_config_node.d = true; | ||||
|                         } | ||||
|                     } else { | ||||
|                         if (editing_config_node.d === true) { | ||||
|                             editState.changes.d = editing_config_node.d; | ||||
|                             editState.changed = true; | ||||
|                             delete editing_config_node.d; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     if (scope) { | ||||
|                         // Search for nodes that use this one that are no longer | ||||
|                     // 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 | ||||
|                         editing_config_node.users = editing_config_node.users.filter(function(n) { | ||||
|                             var keep = true; | ||||
|                             for (var d in n._def.defaults) { | ||||
|                                 if (n._def.defaults.hasOwnProperty(d)) { | ||||
|                                     if (n._def.defaults[d].type === editing_config_node.type && | ||||
|                                         n[d] === editing_config_node.id && | ||||
|                                         n.z !== scope) { | ||||
|                                             keep = false; | ||||
|                                             // Remove the reference to this node | ||||
|                                             // and revalidate | ||||
|                                             n[d] = null; | ||||
|                                             n.dirty = true; | ||||
|                                             n.changed = true; | ||||
|                                             validateNode(n); | ||||
|                     const historyEvents = []; | ||||
|                     if (scope) { | ||||
|                         const newUsers = editing_config_node.users.filter(function (node) { | ||||
|                             let keepNode = false; | ||||
|                             let nodeModified = null; | ||||
|  | ||||
|                             for (const d in node._def.defaults) { | ||||
|                                 if (node._def.defaults.hasOwnProperty(d)) { | ||||
|                                     if (node._def.defaults[d].type === editing_config_node.type) { | ||||
|                                         if (node[d] === editing_config_node.id) { | ||||
|                                             if (node.z === editing_config_node.z) { | ||||
|                                                 // The node is kept only if at least one property uses | ||||
|                                                 // this config node in the correct scope. | ||||
|                                                 keepNode = true; | ||||
|                                             } else { | ||||
|                                                 if (!nodeModified) { | ||||
|                                                     nodeModified = { | ||||
|                                                         t: "edit", | ||||
|                                                         node: node, | ||||
|                                                         changes: { [d]: node[d] }, | ||||
|                                                         changed: node.changed, | ||||
|                                                         dirty: node.dirty | ||||
|                                                     }; | ||||
|                                                 } else { | ||||
|                                                     nodeModified.changes[d] = node[d]; | ||||
|                                                 } | ||||
|  | ||||
|                                                 // Remove the reference to the config node | ||||
|                                                 node[d] = ""; | ||||
|                                             } | ||||
|                                         } | ||||
|                                     } | ||||
|                             return keep; | ||||
|                                 } | ||||
|                             } | ||||
|  | ||||
|                             // Add the node modified to the history | ||||
|                             if (nodeModified) { | ||||
|                                 historyEvents.push(nodeModified); | ||||
|                             } | ||||
|  | ||||
|                             // Mark as changed and revalidate this node | ||||
|                             if (!keepNode) { | ||||
|                                 node.changed = true; | ||||
|                                 node.dirty = true; | ||||
|                                 validateNode(node); | ||||
|                                 RED.events.emit("nodes:change", node); | ||||
|                             } | ||||
|  | ||||
|                             return keepNode; | ||||
|                         }); | ||||
|  | ||||
|                         // Check if users are changed | ||||
|                         if (editing_config_node.users.length !== newUsers.length) { | ||||
|                             editState.changes.users = editing_config_node.users; | ||||
|                             editState.changed = true; | ||||
|                             editing_config_node.users = newUsers; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     if (configAdding) { | ||||
|                         RED.nodes.add(editing_config_node); | ||||
|                     if (editState.changed) { | ||||
|                         // Set the congig node as changed | ||||
|                         editing_config_node.changed = true; | ||||
|                     } | ||||
|  | ||||
|                     // Now, validate the config node | ||||
|                     validateNode(editing_config_node); | ||||
|                     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); | ||||
|                     // 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(user); | ||||
|                             validateNode(node); | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     let historyEvent = { | ||||
|                         t: "edit", | ||||
|                         node: editing_config_node, | ||||
|                         changes: editState.changes, | ||||
|                         changed: wasChanged, | ||||
|                         dirty: RED.nodes.dirty() | ||||
|                     }; | ||||
|  | ||||
|                     if (historyEvents.length) { | ||||
|                         // Need a multi events | ||||
|                         historyEvent = { | ||||
|                             t: "multi", | ||||
|                             events: [historyEvent].concat(historyEvents), | ||||
|                             dirty: historyEvent.dirty | ||||
|                         }; | ||||
|                     } | ||||
|  | ||||
|                     if (!adding) { | ||||
|                         // This event is triggered when the edit box is saved, | ||||
|                         // regardless of whether there are any modifications. | ||||
|                         RED.events.emit("editor:save", editing_config_node); | ||||
|                     } | ||||
|  | ||||
|                     if (editState.changed) { | ||||
|                         if (adding) { | ||||
|                             RED.history.push({ t: "add", nodes: [editing_config_node.id], dirty: RED.nodes.dirty() }); | ||||
|                             // Add the new config node and trigger the `nodes:add` event | ||||
|                             RED.nodes.add(editing_config_node); | ||||
|                         } else { | ||||
|                             RED.history.push(historyEvent); | ||||
|                             RED.events.emit("nodes:change", editing_config_node); | ||||
|                         } | ||||
|  | ||||
|                         RED.nodes.dirty(true); | ||||
|                         RED.view.redraw(true); | ||||
|                     if (!configAdding) { | ||||
|                         RED.events.emit("editor:save",editing_config_node); | ||||
|                         RED.events.emit("nodes:change",editing_config_node); | ||||
|                     } | ||||
|  | ||||
|                     RED.tray.close(function() { | ||||
|                         var filter = null; | ||||
|                         // when editing a config via subflow edit panel, the `configProperty` will not | ||||
|   | ||||
| @@ -20,10 +20,31 @@ | ||||
|             apply: function(editState) { | ||||
|                 var old_env = node.env; | ||||
|                 var new_env = []; | ||||
|  | ||||
|                 if (/^subflow:/.test(node.type)) { | ||||
|                     // Get the list of environment variables from the node properties | ||||
|                     new_env = RED.subflow.exportSubflowInstanceEnv(node); | ||||
|                 } | ||||
|  | ||||
|                 if (old_env && old_env.length) { | ||||
|                     old_env.forEach(function (prop) { | ||||
|                         if (prop.type === "conf-type" && prop.value) { | ||||
|                             const stillInUse = new_env?.some((p) => p.type === "conf-type" && p.name === prop.name && p.value === prop.value); | ||||
|                             if (!stillInUse) { | ||||
|                                 // Remove the node from the config node users | ||||
|                                 // Only for empty value or modified | ||||
|                                 const configNode = RED.nodes.node(prop.value); | ||||
|                                 if (configNode) { | ||||
|                                     if (configNode.users.indexOf(node) !== -1) { | ||||
|                                         configNode.users.splice(configNode.users.indexOf(node), 1); | ||||
|                                         RED.events.emit('nodes:change', configNode) | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 // Get the values from the Properties table tab | ||||
|                 var items = this.list.editableList('items'); | ||||
|                 items.each(function (i,el) { | ||||
| @@ -41,7 +62,6 @@ | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|  | ||||
|                 if (new_env && new_env.length > 0) { | ||||
|                     new_env.forEach(function(prop) { | ||||
|                         if (prop.type === "cred") { | ||||
| @@ -52,6 +72,15 @@ | ||||
|                                 editState.changed = true; | ||||
|                             } | ||||
|                             delete prop.value; | ||||
|                         } else if (prop.type === "conf-type" && prop.value) {   | ||||
|                             const configNode = RED.nodes.node(prop.value); | ||||
|                             if (configNode) { | ||||
|                                 if (configNode.users.indexOf(node) === -1) { | ||||
|                                     // Add the node to the config node users | ||||
|                                     configNode.users.push(node); | ||||
|                                     RED.events.emit('nodes:change', configNode); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|   | ||||
| @@ -44,6 +44,7 @@ | ||||
|             apply: function(editState) { | ||||
|                 var newValue; | ||||
|                 var d; | ||||
|                 // If the node is a subflow, the node's properties (exepts name) are saved by `envProperties` | ||||
|                 if (node._def.defaults) { | ||||
|                     for (d in node._def.defaults) { | ||||
|                         if (node._def.defaults.hasOwnProperty(d)) { | ||||
| @@ -131,9 +132,16 @@ | ||||
|                     } | ||||
|                 } | ||||
|                 if (node._def.credentials) { | ||||
|                     var credDefinition = node._def.credentials; | ||||
|                     var credsChanged = updateNodeCredentials(node,credDefinition,this.inputClass); | ||||
|                     editState.changed = editState.changed || credsChanged; | ||||
|                     const credDefinition = node._def.credentials; | ||||
|                     const credChanges = updateNodeCredentials(node, credDefinition, this.inputClass); | ||||
|  | ||||
|                     if (Object.keys(credChanges).length) { | ||||
|                         editState.changed = true; | ||||
|                         editState.changes.credentials = { | ||||
|                             ...(editState.changes.credentials || {}), | ||||
|                             ...credChanges | ||||
|                         }; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @@ -161,10 +169,11 @@ | ||||
|      * @param node - the node containing the credentials | ||||
|      * @param credDefinition - definition of the credentials | ||||
|      * @param prefix - prefix of the input fields | ||||
|      * @return {boolean} whether anything has changed | ||||
|      * @return {object} an object containing the modified properties | ||||
|      */ | ||||
|     function updateNodeCredentials(node, credDefinition, prefix) { | ||||
|         var changed = false; | ||||
|         const changes = {}; | ||||
|  | ||||
|         if (!node.credentials) { | ||||
|             node.credentials = {_:{}}; | ||||
|         } else if (!node.credentials._) { | ||||
| @@ -177,22 +186,33 @@ | ||||
|                 if (input.length > 0) { | ||||
|                     var value = input.val(); | ||||
|                     if (credDefinition[cred].type == 'password') { | ||||
|                         node.credentials['has_' + cred] = (value !== ""); | ||||
|                         if (value == '__PWRD__') { | ||||
|                             continue; | ||||
|                         } | ||||
|                         changed = true; | ||||
|  | ||||
|                     } | ||||
|                         if (value === '__PWRD__') { | ||||
|                             // A cred value exists - no changes | ||||
|                         } else if (value === '' && node.credentials['has_' + cred] === false) { | ||||
|                             // Empty cred value exists - no changes | ||||
|                         } else if (value === node.credentials[cred]) { | ||||
|                             // A cred value exists locally in the editor - no changes | ||||
|                             // Like the user sets a value, saves the config, | ||||
|                             // reopens the config and save the config again | ||||
|                         } else { | ||||
|                             changes['has_' + cred] = node.credentials['has_' + cred]; | ||||
|                             changes[cred] = node.credentials[cred]; | ||||
|                             node.credentials[cred] = value; | ||||
|                     if (value != node.credentials._[cred]) { | ||||
|                         changed = true; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return changed; | ||||
|                         } | ||||
|  | ||||
|                         node.credentials['has_' + cred] = (value !== ''); | ||||
|                     } else { | ||||
|                         // Since these creds are loaded by the editor, | ||||
|                         // values can be directly compared | ||||
|                         if (value !== node.credentials[cred]) { | ||||
|                             changes[cred] = node.credentials[cred]; | ||||
|                             node.credentials[cred] = value; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return changes; | ||||
|     } | ||||
| })(); | ||||
|   | ||||
| @@ -1362,7 +1362,7 @@ RED.subflow = (function() { | ||||
|                         item.value = ""+input.prop("checked"); | ||||
|                         break; | ||||
|                     case "conf-types": | ||||
|                         item.value = input.val() | ||||
|                         item.value = input.val() === "_ADD_" ? "" : input.val(); | ||||
|                         item.type = "conf-type" | ||||
|                 } | ||||
|                 if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) { | ||||
|   | ||||
| @@ -11,7 +11,7 @@ RED.view.annotations = (function() { | ||||
|                 } | ||||
|                 let badgeRDX = 0; | ||||
|                 let badgeLDX = 0; | ||||
|                  | ||||
|                 const scale = RED.view.scale() | ||||
|                 for (let i=0,l=evt.el.__annotations__.length;i<l;i++) { | ||||
|                     const annotation = evt.el.__annotations__[i]; | ||||
|                     if (annotations.hasOwnProperty(annotation.id)) { | ||||
| @@ -42,15 +42,17 @@ RED.view.annotations = (function() { | ||||
|                         } | ||||
|                         if (isBadge) { | ||||
|                             if (showAnnotation) { | ||||
|                                 const rect = annotation.element.getBoundingClientRect(); | ||||
|                                 // getBoundingClientRect is in real-world scale so needs to be adjusted according to | ||||
|                                 // the current scale factor | ||||
|                                 const rectWidth = annotation.element.getBoundingClientRect().width / scale; | ||||
|                                 let annotationX | ||||
|                                 if (!opts.align || opts.align === 'right') { | ||||
|                                     annotationX = evt.node.w - 3 - badgeRDX - rect.width | ||||
|                                     badgeRDX += rect.width + 4; | ||||
|                                     annotationX = evt.node.w - 3 - badgeRDX - rectWidth | ||||
|                                     badgeRDX += rectWidth + 4; | ||||
|  | ||||
|                                 } else if (opts.align === 'left') { | ||||
|                                     annotationX = 3 + badgeLDX | ||||
|                                     badgeLDX += rect.width + 4; | ||||
|                                     badgeLDX += rectWidth + 4; | ||||
|                                 } | ||||
|                                 annotation.element.setAttribute("transform", "translate("+annotationX+", -8)"); | ||||
|                             } | ||||
|   | ||||
| @@ -1154,11 +1154,11 @@ RED.view.tools = (function() { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function addJunctionsToWires(wires) { | ||||
|     function addJunctionsToWires(options = {}) { | ||||
|         if (RED.workspaces.isLocked()) { | ||||
|             return | ||||
|         } | ||||
|         let wiresToSplit = wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link)); | ||||
|         let wiresToSplit = options.wires || (RED.view.selection().links && RED.view.selection().links.filter(e => !e.link)); | ||||
|         if (!wiresToSplit) { | ||||
|             return | ||||
|         } | ||||
| @@ -1206,6 +1206,10 @@ RED.view.tools = (function() { | ||||
|             if (links.length === 0) { | ||||
|                 return | ||||
|             } | ||||
|             if (addedJunctions.length === 0 && Object.hasOwn(options, 'x') && Object.hasOwn(options, 'y')) { | ||||
|                 junction.x = options.x | ||||
|                 junction.y = options.y | ||||
|             } else { | ||||
|                 let pointCount = 0 | ||||
|                 links.forEach(function(l) { | ||||
|                     if (l._sliceLocation) { | ||||
| @@ -1221,6 +1225,7 @@ RED.view.tools = (function() { | ||||
|                 }) | ||||
|                 junction.x = Math.round(junction.x/pointCount) | ||||
|                 junction.y = Math.round(junction.y/pointCount) | ||||
|             } | ||||
|             if (RED.view.snapGrid) { | ||||
|                 let gridSize = RED.view.gridSize() | ||||
|                 junction.x = (gridSize*Math.round(junction.x/gridSize)); | ||||
| @@ -1410,7 +1415,7 @@ RED.view.tools = (function() { | ||||
|             RED.actions.add("core:wire-multiple-to-node", function() { wireMultipleToNode() }) | ||||
|  | ||||
|             RED.actions.add("core:split-wire-with-link-nodes", function () { splitWiresWithLinkNodes() }); | ||||
|             RED.actions.add("core:split-wires-with-junctions", function () { addJunctionsToWires() }); | ||||
|             RED.actions.add("core:split-wires-with-junctions", function (options) { addJunctionsToWires(options) }); | ||||
|  | ||||
|             RED.actions.add("core:generate-node-names", generateNodeNames ) | ||||
|  | ||||
|   | ||||
| @@ -321,8 +321,8 @@ RED.view = (function() { | ||||
|             evt.stopPropagation() | ||||
|             RED.contextMenu.show({ | ||||
|                 type: 'workspace', | ||||
|                 x:evt.clientX-5, | ||||
|                 y:evt.clientY-5 | ||||
|                 x: evt.clientX, | ||||
|                 y: evt.clientY | ||||
|             }) | ||||
|             return false | ||||
|         }) | ||||
| @@ -5174,8 +5174,8 @@ RED.view = (function() { | ||||
|                                 var delta = Infinity; | ||||
|                                 for (var i = 0; i < lineLength; i++) { | ||||
|                                     var linePos = pathLine.getPointAtLength(i); | ||||
|                                     var posDeltaX = Math.abs(linePos.x-d3.event.offsetX) | ||||
|                                     var posDeltaY = Math.abs(linePos.y-d3.event.offsetY) | ||||
|                                     var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor)) | ||||
|                                     var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor)) | ||||
|                                     var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY | ||||
|                                     if (posDelta < delta) { | ||||
|                                         pos = linePos | ||||
|   | ||||
| @@ -291,26 +291,6 @@ module.exports = function(RED) { | ||||
|                     } | ||||
|                 } | ||||
|                 else if (!msg.hasOwnProperty("reset")) { | ||||
|                     if (maxKeptMsgsCount(node) > 0) { | ||||
|                         if (node.intervalID === -1) { | ||||
|                             node.send(msg); | ||||
|                             node.intervalID = setInterval(sendMsgFromBuffer, node.rate); | ||||
|                         } else { | ||||
|                             if (node.allowrate && msg.hasOwnProperty("rate") && !isNaN(parseFloat(msg.rate)) && node.rate !== msg.rate) { | ||||
|                                 node.rate = msg.rate; | ||||
|                                 clearInterval(node.intervalID); | ||||
|                                 node.intervalID = setInterval(sendMsgFromBuffer, node.rate); | ||||
|                             } | ||||
|                             if (node.buffer.length < _maxKeptMsgsCount) { | ||||
|                                 var m = RED.util.cloneMessage(msg); | ||||
|                                 node.buffer.push({msg: m, send: send, done: done}); | ||||
|                             } else { | ||||
|                                 node.trace("dropped due to buffer overflow. msg._msgid = " + msg._msgid); | ||||
|                                 node.droppedMsgs++; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     else { | ||||
|                     if (node.allowrate && msg.hasOwnProperty("rate") && !isNaN(parseFloat(msg.rate))) { | ||||
|                         node.rate = msg.rate; | ||||
|                     } | ||||
| @@ -325,9 +305,9 @@ module.exports = function(RED) { | ||||
|                     else if ( ( (timeSinceLast[0] * SECONDS_TO_NANOS) + timeSinceLast[1] ) > (node.rate * MILLIS_TO_NANOS) ) { | ||||
|                         node.lastSent = process.hrtime(); | ||||
|                         send(msg); | ||||
|                         } else if (node.outputs === 2) { | ||||
|                             send([null,msg]) | ||||
|                     } | ||||
|                     else if (node.outputs === 2) { | ||||
|                         send([null,msg]) | ||||
|                     } | ||||
|                     done(); | ||||
|                 } | ||||
|   | ||||
| @@ -367,20 +367,21 @@ module.exports = function(RED) { | ||||
|             const sendHeadersAlways = node.hdrout === "all" | ||||
|             const sendHeaders = !dontSendHeaders && (sendHeadersOnce || sendHeadersAlways) | ||||
|             const quoteables = [node.sep, node.quo, "\n", "\r"] | ||||
|             const templateQuoteables = [',', '"', "\n", "\r"] | ||||
|             const templateQuoteables = [node.sep, node.quo, "\n", "\r"] | ||||
|             const templateQuoteablesStrict = [',', '"', "\n", "\r"] | ||||
|             let badTemplateWarnOnce = true | ||||
|  | ||||
|             const columnStringToTemplateArray = function (col, sep) { | ||||
|                 // NOTE: enforce strict column template parsing in RFC4180 mode | ||||
|                 const parsed = csv.parse(col, { separator: sep, quote: node.quo, outputStyle: 'array', strict: true }) | ||||
|                 if (parsed.headers.length > 0) { node.goodtmpl = true } else { node.goodtmpl = false } | ||||
|                 return parsed.headers.length ? parsed.headers : null | ||||
|                 if (parsed.data?.length === 1) { node.goodtmpl = true } else { node.goodtmpl = false } | ||||
|                 return node.goodtmpl ? parsed.data[0] : null | ||||
|             } | ||||
|             const templateArrayToColumnString = function (template, keepEmptyColumns) { | ||||
|                 // NOTE: enforce strict column template parsing in RFC4180 mode | ||||
|                 const parsed = csv.parse('', {headers: template, headersOnly:true, separator: ',', quote: node.quo, outputStyle: 'array', strict: true }) | ||||
|             const templateArrayToColumnString = function (template, keepEmptyColumns, separator = ',', quotables = templateQuoteablesStrict) { | ||||
|                 // NOTE: defaults to strict column template parsing (commas and double quotes) | ||||
|                 const parsed = csv.parse('', {headers: template, headersOnly:true, separator, quote: node.quo, outputStyle: 'array', strict: true }) | ||||
|                 return keepEmptyColumns | ||||
|                     ? parsed.headers.map(e => addQuotes(e || '', { separator: ',', quoteables: templateQuoteables})) | ||||
|                     ? parsed.headers.map(e => addQuotes(e || '', { separator, quoteables: quotables })).join(separator) | ||||
|                     : parsed.header // exclues empty columns | ||||
|                     // TODO: resolve inconsistency between CSV->JSON and JSON->CSV | ||||
|                     // CSV->JSON: empty columns are excluded | ||||
| @@ -447,7 +448,7 @@ module.exports = function(RED) { | ||||
|                                         template = Object.keys(inputData[0]) || [''] | ||||
|                                     } | ||||
|                                 } | ||||
|                                 stringBuilder.push(templateArrayToColumnString(template, true)) | ||||
|                                 stringBuilder.push(templateArrayToColumnString(template, true, node.sep, templateQuoteables)) // use user set separator for output data. | ||||
|                                 if (sendHeadersOnce) { node.hdrSent = true } | ||||
|                             } | ||||
|  | ||||
| @@ -483,6 +484,7 @@ module.exports = function(RED) { | ||||
|                                             node.warn(RED._("csv.errors.obj_csv")) | ||||
|                                             badTemplateWarnOnce = false | ||||
|                                         } | ||||
|                                         template = Object.keys(row) || [''] | ||||
|                                         const rowData = [] | ||||
|                                         for (let header in inputData[0]) { | ||||
|                                             if (row.hasOwnProperty(header)) { | ||||
| @@ -518,7 +520,7 @@ module.exports = function(RED) { | ||||
|  | ||||
|                             // join lines, don't forget to add the last new line | ||||
|                             msg.payload = stringBuilder.join(node.ret) + node.ret | ||||
|                             msg.columns = templateArrayToColumnString(template) | ||||
|                             msg.columns = templateArrayToColumnString(template) // always strict commas + double quotes  for  | ||||
|                             if (msg.payload !== '') { send(msg) } | ||||
|                             done() | ||||
|                         } | ||||
| @@ -615,16 +617,15 @@ module.exports = function(RED) { | ||||
|                                     } | ||||
|                                     if (msg.parts.index + 1 === msg.parts.count) { | ||||
|                                         msg.payload = node.store | ||||
|                                         msg.columns = csvParseResult.header | ||||
|                                         // msg._mode = 'RFC4180 mode' | ||||
|                                         // msg.columns = csvParseResult.header | ||||
|                                         msg.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes  for msg.columns | ||||
|                                         delete msg.parts | ||||
|                                         send(msg) | ||||
|                                         node.store = [] | ||||
|                                     } | ||||
|                                 } | ||||
|                                 else { | ||||
|                                     msg.columns = csvParseResult.header | ||||
|                                     // msg._mode = 'RFC4180 mode' | ||||
|                                     msg.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes  for msg.columns | ||||
|                                     msg.payload = data | ||||
|                                     send(msg); // finally send the array | ||||
|                                 } | ||||
| @@ -633,7 +634,8 @@ module.exports = function(RED) { | ||||
|                                 const len = data.length | ||||
|                                 for (let row = 0; row < len; row++) { | ||||
|                                     const newMessage = RED.util.cloneMessage(msg) | ||||
|                                     newMessage.columns = csvParseResult.header | ||||
|                                     // newMessage.columns = csvParseResult.header | ||||
|                                     newMessage.columns = templateArrayToColumnString(csvParseResult.headers) // always strict commas + double quotes  for msg.columns | ||||
|                                     newMessage.payload = data[row] | ||||
|                                     if (!has_parts) { | ||||
|                                         newMessage.parts = { | ||||
|   | ||||
| @@ -339,7 +339,7 @@ module.exports = function(RED) { | ||||
|             } | ||||
|             else { | ||||
|                 msg.filename = filename; | ||||
|                 var lines = Buffer.from([]); | ||||
|                 const bufferArray = []; | ||||
|                 var spare = ""; | ||||
|                 var count = 0; | ||||
|                 var type = "buffer"; | ||||
| @@ -397,7 +397,7 @@ module.exports = function(RED) { | ||||
|                                 } | ||||
|                             } | ||||
|                             else { | ||||
|                                 lines = Buffer.concat([lines,chunk]); | ||||
|                                 bufferArray.push(chunk); | ||||
|                             } | ||||
|                         } | ||||
|                     }) | ||||
| @@ -413,10 +413,11 @@ module.exports = function(RED) { | ||||
|                     }) | ||||
|                     .on('end', function() { | ||||
|                         if (node.chunk === false) { | ||||
|                             const buffer = Buffer.concat(bufferArray); | ||||
|                             if (node.format === "utf8") { | ||||
|                                 msg.payload = decode(lines, node.encoding); | ||||
|                                 msg.payload = decode(buffer, node.encoding); | ||||
|                             } | ||||
|                             else { msg.payload = lines; } | ||||
|                             else { msg.payload = buffer; } | ||||
|                             nodeSend(msg); | ||||
|                         } | ||||
|                         else if (node.format === "lines") { | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@node-red/nodes", | ||||
|     "version": "4.0.5", | ||||
|     "version": "4.0.6", | ||||
|     "license": "Apache-2.0", | ||||
|     "repository": { | ||||
|         "type": "git", | ||||
|   | ||||
| @@ -144,7 +144,7 @@ async function installModule(module,version,url) { | ||||
|         if (url) { | ||||
|             if (pkgurlRe.test(url) || localtgzRe.test(url)) { | ||||
|                 // Git remote url or Tarball url - check the valid package url | ||||
|                 installName = url; | ||||
|                 installName = localtgzRe.test(url) && slashRe.test(url) ? `"${url}"` : url; | ||||
|                 isRegistryPackage = false; | ||||
|             } else { | ||||
|                 log.warn(log._("server.install.install-failed-url",{name:module,url:url})); | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@node-red/registry", | ||||
|     "version": "4.0.5", | ||||
|     "version": "4.0.6", | ||||
|     "license": "Apache-2.0", | ||||
|     "main": "./lib/index.js", | ||||
|     "repository": { | ||||
| @@ -16,7 +16,7 @@ | ||||
|         } | ||||
|     ], | ||||
|     "dependencies": { | ||||
|         "@node-red/util": "4.0.5", | ||||
|         "@node-red/util": "4.0.6", | ||||
|         "clone": "2.1.2", | ||||
|         "fs-extra": "11.2.0", | ||||
|         "semver": "7.6.3", | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@node-red/runtime", | ||||
|     "version": "4.0.5", | ||||
|     "version": "4.0.6", | ||||
|     "license": "Apache-2.0", | ||||
|     "main": "./lib/index.js", | ||||
|     "repository": { | ||||
| @@ -16,11 +16,11 @@ | ||||
|         } | ||||
|     ], | ||||
|     "dependencies": { | ||||
|         "@node-red/registry": "4.0.5", | ||||
|         "@node-red/util": "4.0.5", | ||||
|         "@node-red/registry": "4.0.6", | ||||
|         "@node-red/util": "4.0.6", | ||||
|         "async-mutex": "0.5.0", | ||||
|         "clone": "2.1.2", | ||||
|         "express": "4.21.1", | ||||
|         "express": "4.21.2", | ||||
|         "fs-extra": "11.2.0", | ||||
|         "json-stringify-safe": "5.0.1", | ||||
|         "rfdc": "^1.3.1" | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@node-red/util", | ||||
|     "version": "4.0.5", | ||||
|     "version": "4.0.6", | ||||
|     "license": "Apache-2.0", | ||||
|     "repository": { | ||||
|         "type": "git", | ||||
|   | ||||
							
								
								
									
										12
									
								
								packages/node_modules/node-red/package.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								packages/node_modules/node-red/package.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "node-red", | ||||
|     "version": "4.0.5", | ||||
|     "version": "4.0.6", | ||||
|     "description": "Low-code programming for event-driven applications", | ||||
|     "homepage": "https://nodered.org", | ||||
|     "license": "Apache-2.0", | ||||
| @@ -31,14 +31,14 @@ | ||||
|         "flow" | ||||
|     ], | ||||
|     "dependencies": { | ||||
|         "@node-red/editor-api": "4.0.5", | ||||
|         "@node-red/runtime": "4.0.5", | ||||
|         "@node-red/util": "4.0.5", | ||||
|         "@node-red/nodes": "4.0.5", | ||||
|         "@node-red/editor-api": "4.0.6", | ||||
|         "@node-red/runtime": "4.0.6", | ||||
|         "@node-red/util": "4.0.6", | ||||
|         "@node-red/nodes": "4.0.6", | ||||
|         "basic-auth": "2.0.1", | ||||
|         "bcryptjs": "2.4.3", | ||||
|         "cors": "2.8.5", | ||||
|         "express": "4.21.1", | ||||
|         "express": "4.21.2", | ||||
|         "fs-extra": "11.2.0", | ||||
|         "node-red-admin": "^4.0.1", | ||||
|         "nopt": "5.0.0", | ||||
|   | ||||
| @@ -2067,6 +2067,27 @@ describe('CSV node (RFC Mode)', function () { | ||||
|                 n2.on("input", function (msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property('payload', '1\tfoo\t"ba""r"\tdi,ng\n'); | ||||
|                         msg.should.have.property('columns', 'd,b,c,a'); // Strict RFC columns | ||||
|                         done(); | ||||
|                     } catch (e) { | ||||
|                         done(e); | ||||
|                     } | ||||
|                 }); | ||||
|                 const testJson = { d: 1, b: "foo", c: "ba\"r", a: "di,ng" }; | ||||
|                 n1.emit("input", { payload: testJson }); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('should convert a simple object back to a tsv with headers using a tab as a separator', function (done) { | ||||
|             const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", sep: "\t", ret: '\n', hdrout: "all", wires: [["n2"]] }, // RFC-vs-Legacy difference - use line separator \n to satisfy original test | ||||
|             { id: "n2", type: "helper" }]; | ||||
|             helper.load(csvNode, flow, function () { | ||||
|                 const n1 = helper.getNode("n1"); | ||||
|                 const n2 = helper.getNode("n2"); | ||||
|                 n2.on("input", function (msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property('payload', 'd\tb\tc\ta\n1\tfoo\t"ba""r"\tdi,ng\n'); | ||||
|                         msg.should.have.property('columns', 'd,b,c,a'); // Strict RFC columns | ||||
|                         done(); | ||||
|                     } catch (e) { | ||||
|                         done(e); | ||||
| @@ -2086,6 +2107,7 @@ describe('CSV node (RFC Mode)', function () { | ||||
|                 n2.on("input", function (msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property('payload', '4,foo,true,,0\n'); | ||||
|                         msg.should.have.property('columns', 'a,b o,c p,e'); // Strict RFC columns | ||||
|                         done(); | ||||
|                     } catch (e) { | ||||
|                         done(e); | ||||
| @@ -2106,6 +2128,7 @@ describe('CSV node (RFC Mode)', function () { | ||||
|                     try { | ||||
|                         //                       'payload', 'a"a,b\'b\nA1,B1\nA2,B2\n'); // Legacy | ||||
|                         msg.should.have.property('payload', '"a""a",b\'b\nA1,B1\nA2,B2\n'); // RFC-vs-Legacy difference - RFC4180 Section 2.6, 2.7 quote handling | ||||
|                         msg.should.have.property('columns', '"a""a",b\'b'); // RCF compliant column names | ||||
|                         done(); | ||||
|                     } catch (e) { | ||||
|                         done(e); | ||||
| @@ -2171,6 +2194,7 @@ describe('CSV node (RFC Mode)', function () { | ||||
|                 n2.on("input", function (msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property('payload', '1,3,2,4\n4,2,3,1\n'); | ||||
|                         msg.should.have.property('columns', 'd,b,c,a'); // Strict RFC columns | ||||
|                         done(); | ||||
|                     } | ||||
|                     catch (e) { done(e); } | ||||
| @@ -2189,6 +2213,7 @@ describe('CSV node (RFC Mode)', function () { | ||||
|                 n2.on("input", function (msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property('payload', 'd,b,c,a\n1,3,2,4\n4,"f\ng",3,1\n'); | ||||
|                         msg.should.have.property('columns', 'd,b,c,a'); // Strict RFC columns | ||||
|                         done(); | ||||
|                     } | ||||
|                     catch (e) { done(e); } | ||||
| @@ -2208,6 +2233,7 @@ describe('CSV node (RFC Mode)', function () { | ||||
|                     try { | ||||
|                         //                       'payload', ',0,1,foo,"ba""r","di,ng","fa\nba"\n'); | ||||
|                         msg.should.have.property('payload', ',0,1,foo\n'); // RFC-vs-Legacy difference - respect that user has specified a template with 4 columns | ||||
|                         msg.should.have.property('columns', 'a,b,c,d'); | ||||
|                         done(); | ||||
|                     } | ||||
|                     catch (e) { done(e); } | ||||
| @@ -2327,6 +2353,7 @@ describe('CSV node (RFC Mode)', function () { | ||||
|                 n2.on("input", function (msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property('payload', '{},"text,with,commas","This ""is"" a banana","{""sub"":""object""}"\n'); | ||||
|                         msg.should.have.property('columns', 'a,b,c,d'); | ||||
|                         done(); | ||||
|                     } | ||||
|                     catch (e) { done(e); } | ||||
|   | ||||
| @@ -258,6 +258,29 @@ describe('nodes/registry/installer', function() { | ||||
|             }).catch(done); | ||||
|         }); | ||||
|  | ||||
|         it("succeeds when file path is valid node-red module", function(done) { | ||||
|             var nodeInfo = {nodes:{module:"foo",types:["a"]}}; | ||||
|  | ||||
|             var res = { | ||||
|                 code: 0, | ||||
|                 stdout:"", | ||||
|                 stderr:"" | ||||
|             } | ||||
|             var p = Promise.resolve(res); | ||||
|             p.catch((err)=>{}); | ||||
|             execResponse = p; | ||||
|  | ||||
|             var addModule = sinon.stub(registry,"addModule").callsFake(function(md) { | ||||
|                 return Promise.resolve(nodeInfo); | ||||
|             }); | ||||
|  | ||||
|             installer.installModule("foo",null,"/example path/foo-0.1.1.tgz").then(function(info) { | ||||
|                 exec.run.lastCall.args[1].should.eql([ 'install', '--no-audit', '--no-update-notifier', '--no-fund', '--save', '--save-prefix=~', '--omit=dev', '--engine-strict', '"/example path/foo-0.1.1.tgz"' ]); | ||||
|                 info.should.eql(nodeInfo); | ||||
|                 done(); | ||||
|             }).catch(done); | ||||
|         }); | ||||
|  | ||||
|         it("triggers preInstall and postInstall hooks", function(done) { | ||||
|             let receivedPreEvent,receivedPostEvent; | ||||
|             hooks.add("preInstall", function(event) { event.args = ["a"]; receivedPreEvent = event; }) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user