mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	Merge branch 'dev' into theme-monaco-theme
This commit is contained in:
		
							
								
								
									
										46
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,3 +1,49 @@ | ||||
| #### 2.0.6: Maintenance Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Fix typo in ko editor.json Fixes #3119 | ||||
|  - Change fade color when hovering an inactive tab (#3106) @bonanitech | ||||
|  - Ensure treeList row has suitable min-height when no content Fixes #3109 | ||||
|  | ||||
| Runtime | ||||
|  | ||||
|  - Update tar to latest (#3128) @aksswami | ||||
|  - Give passport verify callback the same arity as the original callback (#3117) @dschmidt | ||||
|  - Handle HTTPS Key and certificate as string or buffer (#3115) @bartbutenaers | ||||
|  | ||||
| #### 2.0.5: Maintenance Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Remove default ctrl-enter keybinding from monaco editor Fixes #3093 | ||||
|  | ||||
| Runtime | ||||
|  | ||||
|  - Update tar dependency | ||||
|  - Add support for maintenance streams in generate-publish-script | ||||
|  | ||||
|  | ||||
| Nodes | ||||
|  | ||||
|  - Fix regression in Join node when manual joining array with msg.parts present Fixes #3096 | ||||
|  | ||||
| #### 2.0.4: Maintenance Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Fix tab fade CSS for when using themes (#3085) @bonanitech | ||||
|  - Handle just-copied-but-not-deployed node with credentials in editor Fixes #3090 | ||||
|  | ||||
| Nodes | ||||
|  | ||||
|  - Filter: Fix RBE node handling of default topi property Fixes #3087 | ||||
|  - HTTP Request: Handle partially encoded url query strings in request node | ||||
|  - HTTP Request: Fix support for supplied CA certs (#3089) @hardillb | ||||
|  - HTTP Request: Ensure TLS Cert is used (#3092) @hardillb | ||||
|  - Inject: Fix inject now button unable to send empty props | ||||
|  - Inject: Inject now button success notification should use label with updated props | ||||
|  | ||||
| #### 2.0.3: Maintenance Release | ||||
|  | ||||
| Nodes | ||||
|   | ||||
| @@ -162,7 +162,6 @@ module.exports = function(grunt) { | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/ui/common/stack.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/ui/common/toggleButton.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/ui/common/colorPicker.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/ui/actions.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/ui/diff.js", | ||||
| @@ -182,6 +181,7 @@ module.exports = function(grunt) { | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/ui/editor.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/*.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/ui/editors/*.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/*.js", | ||||
|                     "packages/node_modules/@node-red/editor-client/src/js/ui/event-log.js", | ||||
|   | ||||
							
								
								
									
										26
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "node-red", | ||||
|     "version": "2.0.3", | ||||
|     "version": "2.0.6", | ||||
|     "description": "Low-code programming for event-driven applications", | ||||
|     "homepage": "http://nodered.org", | ||||
|     "license": "Apache-2.0", | ||||
| @@ -28,8 +28,8 @@ | ||||
|     "dependencies": { | ||||
|         "acorn": "8.4.1", | ||||
|         "acorn-walk": "8.1.1", | ||||
|         "ajv": "8.6.0", | ||||
|         "async-mutex": "0.3.1", | ||||
|         "ajv": "8.6.2", | ||||
|         "async-mutex": "0.3.2", | ||||
|         "basic-auth": "2.0.1", | ||||
|         "bcryptjs": "2.4.3", | ||||
|         "body-parser": "1.19.0", | ||||
| @@ -55,14 +55,14 @@ | ||||
|         "is-utf8": "0.2.1", | ||||
|         "js-yaml": "3.14.1", | ||||
|         "json-stringify-safe": "5.0.1", | ||||
|         "jsonata": "1.8.4", | ||||
|         "jsonata": "1.8.5", | ||||
|         "lodash.clonedeep": "^4.5.0", | ||||
|         "media-typer": "1.1.0", | ||||
|         "memorystore": "1.6.6", | ||||
|         "mime": "2.5.2", | ||||
|         "moment-timezone": "0.5.33", | ||||
|         "mqtt": "4.2.8", | ||||
|         "multer": "1.4.2", | ||||
|         "multer": "1.4.3", | ||||
|         "mustache": "4.2.0", | ||||
|         "node-red-admin": "^2.2.0", | ||||
|         "nopt": "5.0.0", | ||||
| @@ -73,9 +73,9 @@ | ||||
|         "passport-oauth2-client-password": "0.1.2", | ||||
|         "raw-body": "2.4.1", | ||||
|         "semver": "7.3.5", | ||||
|         "tar": "6.1.0", | ||||
|         "tar": "6.1.11", | ||||
|         "tough-cookie": "4.0.0", | ||||
|         "uglify-js": "3.13.10", | ||||
|         "uglify-js": "3.14.1", | ||||
|         "uuid": "8.3.2", | ||||
|         "ws": "7.5.1", | ||||
|         "xml2js": "0.4.23" | ||||
| @@ -84,7 +84,7 @@ | ||||
|         "bcrypt": "5.0.1" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "dompurify": "2.2.9", | ||||
|         "dompurify": "2.3.1", | ||||
|         "grunt": "1.4.1", | ||||
|         "grunt-chmod": "~1.1.1", | ||||
|         "grunt-cli": "~1.4.3", | ||||
| @@ -109,15 +109,15 @@ | ||||
|         "jsdoc-nr-template": "github:node-red/jsdoc-nr-template", | ||||
|         "marked": "2.1.3", | ||||
|         "minami": "1.2.3", | ||||
|         "mocha": "9.0.1", | ||||
|         "mocha": "9.1.1", | ||||
|         "node-red-node-test-helper": "^0.2.7", | ||||
|         "nodemon": "2.0.8", | ||||
|         "nodemon": "2.0.12", | ||||
|         "proxy": "^1.0.2", | ||||
|         "sass": "1.35.1", | ||||
|         "sass": "1.39.0", | ||||
|         "should": "13.2.3", | ||||
|         "sinon": "11.1.1", | ||||
|         "sinon": "11.1.2", | ||||
|         "stoppable": "^1.1.0", | ||||
|         "supertest": "6.1.3" | ||||
|         "supertest": "6.1.6" | ||||
|     }, | ||||
|     "engines": { | ||||
|         "node": ">=12" | ||||
|   | ||||
| @@ -173,27 +173,30 @@ function genericStrategy(adminApp,strategy) { | ||||
|     adminApp.use(passport.session()); | ||||
|  | ||||
|     var options = strategy.options; | ||||
|     var verify = function() { | ||||
|         var originalDone = arguments[arguments.length-1]; | ||||
|         if (options.verify) { | ||||
|             var args = Array.from(arguments); | ||||
|             args[args.length-1] = function(err,profile) { | ||||
|                 if (err) { | ||||
|                     return originalDone(err); | ||||
|                 } else { | ||||
|                     return completeVerify(profile,originalDone); | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|     passport.use(new strategy.strategy(options, | ||||
|         function() { | ||||
|             var originalDone = arguments[arguments.length-1]; | ||||
|             if (options.verify) { | ||||
|                 var args = Array.from(arguments); | ||||
|                 args[args.length-1] = function(err,profile) { | ||||
|                     if (err) { | ||||
|                         return originalDone(err); | ||||
|                     } else { | ||||
|                         return completeVerify(profile,originalDone); | ||||
|                     } | ||||
|                 }; | ||||
|                 options.verify.apply(null,args); | ||||
|             } else { | ||||
|                 var profile = arguments[arguments.length - 2]; | ||||
|                 return completeVerify(profile,originalDone); | ||||
|             } | ||||
|  | ||||
|             options.verify.apply(null,args); | ||||
|         } else { | ||||
|             var profile = arguments[arguments.length - 2]; | ||||
|             return completeVerify(profile,originalDone); | ||||
|         } | ||||
|     )); | ||||
|     }; | ||||
|     // Give our callback the same arity as the original one from options | ||||
|     if (options.verify) { | ||||
|         Object.defineProperty(verify, "length", { value: options.verify.length }) | ||||
|     } | ||||
|  | ||||
|     passport.use(new strategy.strategy(options, verify)); | ||||
|  | ||||
|     adminApp.get('/auth/strategy', | ||||
|         passport.authenticate(strategy.name, {session:false, failureRedirect: settings.httpAdminRoot }), | ||||
|   | ||||
| @@ -24,7 +24,10 @@ var defaultContext = { | ||||
|     page: { | ||||
|         title: "Node-RED", | ||||
|         favicon: "favicon.ico", | ||||
|         tabicon: "red/images/node-red-icon-black.svg" | ||||
|         tabicon: { | ||||
|             icon: "red/images/node-red-icon-black.svg", | ||||
|             colour: "#8f0000" | ||||
|         } | ||||
|     }, | ||||
|     header: { | ||||
|         title: "Node-RED", | ||||
| @@ -123,9 +126,13 @@ module.exports = { | ||||
|             } | ||||
|  | ||||
|             if (theme.page.tabicon) { | ||||
|                 url = serveFile(themeApp,"/tabicon/",theme.page.tabicon) | ||||
|                 let icon = theme.page.tabicon.icon || theme.page.tabicon | ||||
|                 url = serveFile(themeApp,"/tabicon/", icon) | ||||
|                 if (url) { | ||||
|                     themeContext.page.tabicon = url; | ||||
|                     themeContext.page.tabicon.icon = url; | ||||
|                 } | ||||
|                 if (theme.page.tabicon.colour) { | ||||
|                     themeContext.page.tabicon.colour = theme.page.tabicon.colour | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@node-red/editor-api", | ||||
|     "version": "2.0.3", | ||||
|     "version": "2.0.6", | ||||
|     "license": "Apache-2.0", | ||||
|     "main": "./lib/index.js", | ||||
|     "repository": { | ||||
| @@ -16,8 +16,8 @@ | ||||
|         } | ||||
|     ], | ||||
|     "dependencies": { | ||||
|         "@node-red/util": "2.0.3", | ||||
|         "@node-red/editor-client": "2.0.3", | ||||
|         "@node-red/util": "2.0.6", | ||||
|         "@node-red/editor-client": "2.0.6", | ||||
|         "bcryptjs": "2.4.3", | ||||
|         "body-parser": "1.19.0", | ||||
|         "clone": "2.1.2", | ||||
| @@ -26,7 +26,7 @@ | ||||
|         "express": "4.17.1", | ||||
|         "memorystore": "1.6.6", | ||||
|         "mime": "2.5.2", | ||||
|         "multer": "1.4.2", | ||||
|         "multer": "1.4.3", | ||||
|         "mustache": "4.2.0", | ||||
|         "oauth2orize": "1.11.0", | ||||
|         "passport-http-bearer": "1.0.1", | ||||
|   | ||||
| @@ -116,7 +116,20 @@ | ||||
|             "groupSelection": "Group selection", | ||||
|             "ungroupSelection": "Ungroup selection", | ||||
|             "groupMergeSelection": "Merge selection", | ||||
|             "groupRemoveSelection": "Remove from group" | ||||
|             "groupRemoveSelection": "Remove from group", | ||||
|             "arrange":"Arrange", | ||||
|             "alignLeft":"Align to left", | ||||
|             "alignCenter":"Align to center", | ||||
|             "alignRight":"Align to right", | ||||
|             "alignTop":"Align to top", | ||||
|             "alignMiddle":"Align to middle", | ||||
|             "alignBottom":"Align to bottom", | ||||
|             "distributeHorizontally":"Distribute horizontally", | ||||
|             "distributeVertically":"Distribute vertically", | ||||
|             "moveToBack":"Move to back", | ||||
|             "moveToFront":"Move to front", | ||||
|             "moveBackwards":"Move backwards", | ||||
|             "moveForwards":"Move forwards" | ||||
|         } | ||||
|     }, | ||||
|     "actions": { | ||||
| @@ -450,8 +463,9 @@ | ||||
|         "unassigned": "Unassigned", | ||||
|         "global": "global", | ||||
|         "workspace": "workspace", | ||||
|         "selectAll": "Select all nodes", | ||||
|         "selectAllConnected": "Select all connected nodes", | ||||
|         "selectAll": "Select all", | ||||
|         "selectNone": "Select none", | ||||
|         "selectAllConnected": "Select connected", | ||||
|         "addRemoveNode": "Add/remove node from selection", | ||||
|         "editSelected": "Edit selected node", | ||||
|         "deleteSelected": "Delete selected nodes or link", | ||||
| @@ -464,7 +478,10 @@ | ||||
|         "copyNode": "Copy selected nodes", | ||||
|         "cutNode": "Cut selected nodes", | ||||
|         "pasteNode": "Paste nodes", | ||||
|         "undoChange": "Undo the last change performed", | ||||
|         "copyGroupStyle": "Copy group style", | ||||
|         "pasteGroupStyle": "Paste group style", | ||||
|         "undoChange": "Undo", | ||||
|         "redoChange": "Redo", | ||||
|         "searchBox": "Open search box", | ||||
|         "managePalette": "Manage palette", | ||||
|         "actionList":"Action list" | ||||
|   | ||||
| @@ -56,7 +56,7 @@ | ||||
|       "displayConfig": "설정노드 보기", | ||||
|       "import": "가져오기", | ||||
|       "export": "내보내기", | ||||
|       "search": "플로우 겅색", | ||||
|       "search": "플로우 검색", | ||||
|       "searchInput": "플로우 검색", | ||||
|       "subflows": "보조 플로우", | ||||
|       "createSubflow": "보조 플로우 생성", | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@node-red/editor-client", | ||||
|     "version": "2.0.3", | ||||
|     "version": "2.0.6", | ||||
|     "license": "Apache-2.0", | ||||
|     "repository": { | ||||
|         "type": "git", | ||||
|   | ||||
| @@ -558,11 +558,22 @@ RED.history = (function() { | ||||
|             } else if (ev.t == "reorder") { | ||||
|                 inverseEv = { | ||||
|                     t: 'reorder', | ||||
|                     order: RED.nodes.getWorkspaceOrder(), | ||||
|                     dirty: RED.nodes.dirty() | ||||
|                 }; | ||||
|                 if (ev.order) { | ||||
|                     RED.workspaces.order(ev.order); | ||||
|                 if (ev.workspaces) { | ||||
|                     inverseEv.workspaces = { | ||||
|                         from: ev.workspaces.to, | ||||
|                         to: ev.workspaces.from | ||||
|                     } | ||||
|                     RED.workspaces.order(ev.workspaces.from); | ||||
|                 } | ||||
|                 if (ev.nodes) { | ||||
|                     inverseEv.nodes = { | ||||
|                         z: ev.nodes.z, | ||||
|                         from: ev.nodes.to, | ||||
|                         to: ev.nodes.from | ||||
|                     } | ||||
|                     RED.nodes.setNodeOrder(ev.nodes.z,ev.nodes.from); | ||||
|                 } | ||||
|             } else if (ev.t == "createGroup") { | ||||
|                 inverseEv = { | ||||
| @@ -658,6 +669,8 @@ RED.history = (function() { | ||||
|         push: function(ev) { | ||||
|             undoHistory.push(ev); | ||||
|             redoHistory = []; | ||||
|             RED.menu.setDisabled("menu-item-edit-undo", false); | ||||
|             RED.menu.setDisabled("menu-item-edit-redo", true); | ||||
|         }, | ||||
|         pop: function() { | ||||
|             var ev = undoHistory.pop(); | ||||
| @@ -665,6 +678,8 @@ RED.history = (function() { | ||||
|             if (rev) { | ||||
|                 redoHistory.push(rev); | ||||
|             } | ||||
|             RED.menu.setDisabled("menu-item-edit-undo", undoHistory.length === 0); | ||||
|             RED.menu.setDisabled("menu-item-edit-redo", redoHistory.length === 0); | ||||
|         }, | ||||
|         peek: function() { | ||||
|             return undoHistory[undoHistory.length-1]; | ||||
| @@ -672,6 +687,8 @@ RED.history = (function() { | ||||
|         clear: function() { | ||||
|             undoHistory = []; | ||||
|             redoHistory = []; | ||||
|             RED.menu.setDisabled("menu-item-edit-undo", true); | ||||
|             RED.menu.setDisabled("menu-item-edit-redo", true); | ||||
|         }, | ||||
|         redo: function() { | ||||
|             var ev = redoHistory.pop(); | ||||
| @@ -681,6 +698,8 @@ RED.history = (function() { | ||||
|                     undoHistory.push(uev); | ||||
|                 } | ||||
|             } | ||||
|             RED.menu.setDisabled("menu-item-edit-undo", undoHistory.length === 0); | ||||
|             RED.menu.setDisabled("menu-item-edit-redo", redoHistory.length === 0); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -77,6 +77,15 @@ | ||||
|         "right": "core:go-to-nearest-node-on-right", | ||||
|         "left": "core:go-to-nearest-node-on-left", | ||||
|         "up": "core:go-to-nearest-node-above", | ||||
|         "down": "core:go-to-nearest-node-below" | ||||
|         "down": "core:go-to-nearest-node-below", | ||||
|         "alt-a g": "core:align-selection-to-grid", | ||||
|         "alt-a l": "core:align-selection-to-left", | ||||
|         "alt-a r": "core:align-selection-to-right", | ||||
|         "alt-a t": "core:align-selection-to-top", | ||||
|         "alt-a b": "core:align-selection-to-bottom", | ||||
|         "alt-a m": "core:align-selection-to-middle", | ||||
|         "alt-a c": "core:align-selection-to-center", | ||||
|         "alt-a h": "core:distribute-selection-horizontally", | ||||
|         "alt-a v": "core:distribute-selection-vertically" | ||||
|      } | ||||
| } | ||||
|   | ||||
| @@ -16,8 +16,6 @@ | ||||
| RED.nodes = (function() { | ||||
|  | ||||
|     var node_defs = {}; | ||||
|     var nodes = {}; | ||||
|     var nodeTabMap = {}; | ||||
|     var linkTabMap = {}; | ||||
|  | ||||
|     var configNodes = {}; | ||||
| @@ -41,6 +39,7 @@ RED.nodes = (function() { | ||||
|         RED.events.emit("workspace:dirty",{dirty:dirty}); | ||||
|     } | ||||
|  | ||||
|     // The registry holds information about all node types. | ||||
|     var registry = (function() { | ||||
|         var moduleList = {}; | ||||
|         var nodeList = []; | ||||
| @@ -53,7 +52,8 @@ RED.nodes = (function() { | ||||
|             defaults: { | ||||
|                 label: {value:""}, | ||||
|                 disabled: {value: false}, | ||||
|                 info: {value: ""} | ||||
|                 info: {value: ""}, | ||||
|                 env: {value: []} | ||||
|             } | ||||
|         }; | ||||
|  | ||||
| @@ -209,6 +209,279 @@ RED.nodes = (function() { | ||||
|         return exports; | ||||
|     })(); | ||||
|  | ||||
|     // allNodes holds information about the Flow nodes. | ||||
|     var allNodes = (function() { | ||||
|         var nodes = {}; | ||||
|         var tabMap = {}; | ||||
|         var api = { | ||||
|             addTab: function(id) { | ||||
|                 tabMap[id] = []; | ||||
|             }, | ||||
|             hasTab: function(z) { | ||||
|                 return tabMap.hasOwnProperty(z) | ||||
|             }, | ||||
|             removeTab: function(id) { | ||||
|                 delete tabMap[id]; | ||||
|             }, | ||||
|             addNode: function(n) { | ||||
|                 nodes[n.id] = n; | ||||
|                 if (tabMap.hasOwnProperty(n.z)) { | ||||
|                     tabMap[n.z].push(n); | ||||
|                 } else { | ||||
|                     console.warn("Node added to unknown tab/subflow:",n); | ||||
|                     tabMap["_"] = tabMap["_"] || []; | ||||
|                     tabMap["_"].push(n); | ||||
|                 } | ||||
|             }, | ||||
|             removeNode: function(n) { | ||||
|                 delete nodes[n.id] | ||||
|                 if (tabMap.hasOwnProperty(n.z)) { | ||||
|                     var i = tabMap[n.z].indexOf(n); | ||||
|                     if (i > -1) { | ||||
|                         tabMap[n.z].splice(i,1); | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             hasNode: function(id) { | ||||
|                 return nodes.hasOwnProperty(id); | ||||
|             }, | ||||
|             getNode: function(id) { | ||||
|                 return nodes[id] | ||||
|             }, | ||||
|             moveNode: function(n, newZ) { | ||||
|                 api.removeNode(n); | ||||
|                 tabMap[newZ] = tabMap[newZ] || []; | ||||
|                 tabMap[newZ].push(n); | ||||
|             }, | ||||
|             moveNodesForwards: function(nodes) { | ||||
|                 var result = []; | ||||
|                 if (!Array.isArray(nodes)) { | ||||
|                     nodes = [nodes] | ||||
|                 } | ||||
|                 // Can only do this for nodes on the same tab. | ||||
|                 // Use nodes[0] to get the z | ||||
|                 var tabNodes = tabMap[nodes[0].z]; | ||||
|                 var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); | ||||
|                 var moved = new Set(); | ||||
|                 for (var i = tabNodes.length-1; i >= 0; i--) { | ||||
|                     if (toMove.size === 0) { | ||||
|                         break; | ||||
|                     } | ||||
|                     var n = tabNodes[i]; | ||||
|                     if (toMove.has(n)) { | ||||
|                         // This is a node to move. | ||||
|                         if (i < tabNodes.length-1 && !moved.has(tabNodes[i+1])) { | ||||
|                             // Remove from current position | ||||
|                             tabNodes.splice(i,1); | ||||
|                             // Add it back one position higher | ||||
|                             tabNodes.splice(i+1,0,n); | ||||
|                             n._reordered = true; | ||||
|                             result.push(n); | ||||
|                         } | ||||
|                         toMove.delete(n); | ||||
|                         moved.add(n); | ||||
|                     } | ||||
|                 } | ||||
|                 if (result.length > 0) { | ||||
|                     RED.events.emit('nodes:reorder',{ | ||||
|                         z: nodes[0].z, | ||||
|                         nodes: result | ||||
|                     }); | ||||
|                 } | ||||
|                 return result; | ||||
|             }, | ||||
|             moveNodesBackwards: function(nodes) { | ||||
|                 var result = []; | ||||
|                 if (!Array.isArray(nodes)) { | ||||
|                     nodes = [nodes] | ||||
|                 } | ||||
|                 // Can only do this for nodes on the same tab. | ||||
|                 // Use nodes[0] to get the z | ||||
|                 var tabNodes = tabMap[nodes[0].z]; | ||||
|                 var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); | ||||
|                 var moved = new Set(); | ||||
|                 for (var i = 0; i < tabNodes.length; i++) { | ||||
|                     if (toMove.size === 0) { | ||||
|                         break; | ||||
|                     } | ||||
|                     var n = tabNodes[i]; | ||||
|                     if (toMove.has(n)) { | ||||
|                         // This is a node to move. | ||||
|                         if (i > 0 && !moved.has(tabNodes[i-1])) { | ||||
|                             // Remove from current position | ||||
|                             tabNodes.splice(i,1); | ||||
|                             // Add it back one position lower | ||||
|                             tabNodes.splice(i-1,0,n); | ||||
|                             n._reordered = true; | ||||
|                             result.push(n); | ||||
|                         } | ||||
|                         toMove.delete(n); | ||||
|                         moved.add(n); | ||||
|                     } | ||||
|                 } | ||||
|                 if (result.length > 0) { | ||||
|                     RED.events.emit('nodes:reorder',{ | ||||
|                         z: nodes[0].z, | ||||
|                         nodes: result | ||||
|                     }); | ||||
|                 } | ||||
|                 return result; | ||||
|             }, | ||||
|             moveNodesToFront: function(nodes) { | ||||
|                 var result = []; | ||||
|                 if (!Array.isArray(nodes)) { | ||||
|                     nodes = [nodes] | ||||
|                 } | ||||
|                 // Can only do this for nodes on the same tab. | ||||
|                 // Use nodes[0] to get the z | ||||
|                 var tabNodes = tabMap[nodes[0].z]; | ||||
|                 var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); | ||||
|                 var target = tabNodes.length-1; | ||||
|                 for (var i = tabNodes.length-1; i >= 0; i--) { | ||||
|                     if (toMove.size === 0) { | ||||
|                         break; | ||||
|                     } | ||||
|                     var n = tabNodes[i]; | ||||
|                     if (toMove.has(n)) { | ||||
|                         // This is a node to move. | ||||
|                         if (i < target) { | ||||
|                             // Remove from current position | ||||
|                             tabNodes.splice(i,1); | ||||
|                             tabNodes.splice(target,0,n); | ||||
|                             n._reordered = true; | ||||
|                             result.push(n); | ||||
|                         } | ||||
|                         target--; | ||||
|                         toMove.delete(n); | ||||
|                     } | ||||
|                 } | ||||
|                 if (result.length > 0) { | ||||
|                     RED.events.emit('nodes:reorder',{ | ||||
|                         z: nodes[0].z, | ||||
|                         nodes: result | ||||
|                     }); | ||||
|                 } | ||||
|                 return result; | ||||
|             }, | ||||
|             moveNodesToBack: function(nodes) { | ||||
|                 var result = []; | ||||
|                 if (!Array.isArray(nodes)) { | ||||
|                     nodes = [nodes] | ||||
|                 } | ||||
|                 // Can only do this for nodes on the same tab. | ||||
|                 // Use nodes[0] to get the z | ||||
|                 var tabNodes = tabMap[nodes[0].z]; | ||||
|                 var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); | ||||
|                 var target = 0; | ||||
|                 for (var i = 0; i < tabNodes.length; i++) { | ||||
|                     if (toMove.size === 0) { | ||||
|                         break; | ||||
|                     } | ||||
|                     var n = tabNodes[i]; | ||||
|                     if (toMove.has(n)) { | ||||
|                         // This is a node to move. | ||||
|                         if (i > target) { | ||||
|                             // Remove from current position | ||||
|                             tabNodes.splice(i,1); | ||||
|                             // Add it back one position lower | ||||
|                             tabNodes.splice(target,0,n); | ||||
|                             n._reordered = true; | ||||
|                             result.push(n); | ||||
|                         } | ||||
|                         target++; | ||||
|                         toMove.delete(n); | ||||
|                     } | ||||
|                 } | ||||
|                 if (result.length > 0) { | ||||
|                     RED.events.emit('nodes:reorder',{ | ||||
|                         z: nodes[0].z, | ||||
|                         nodes: result | ||||
|                     }); | ||||
|                 } | ||||
|                 return result; | ||||
|             }, | ||||
|             getNodes: function(z) { | ||||
|                 return tabMap[z]; | ||||
|             }, | ||||
|             clear: function() { | ||||
|                 nodes = {}; | ||||
|                 tabMap = {}; | ||||
|             }, | ||||
|             eachNode: function(cb) { | ||||
|                 var nodeList,i,j; | ||||
|                 for (i in subflows) { | ||||
|                     if (subflows.hasOwnProperty(i)) { | ||||
|                         nodeList = tabMap[i]; | ||||
|                         for (j = 0; j < nodeList.length; j++) { | ||||
|                             if (cb(nodeList[j]) === false) { | ||||
|                                 return; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 for (i = 0; i < workspacesOrder.length; i++) { | ||||
|                     nodeList = tabMap[workspacesOrder[i]]; | ||||
|                     for (j = 0; j < nodeList.length; j++) { | ||||
|                         if (cb(nodeList[j]) === false) { | ||||
|                             return; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 // Flow nodes that do not have a valid tab/subflow | ||||
|                 if (tabMap["_"]) { | ||||
|                     nodeList = tabMap["_"]; | ||||
|                     for (j = 0; j < nodeList.length; j++) { | ||||
|                         if (cb(nodeList[j]) === false) { | ||||
|                             return; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             filterNodes: function(filter) { | ||||
|                 var result = []; | ||||
|                 var searchSet = null; | ||||
|                 var doZFilter = false; | ||||
|                 if (filter.hasOwnProperty("z")) { | ||||
|                     if (tabMap.hasOwnProperty(filter.z)) { | ||||
|                         searchSet = tabMap[filter.z]; | ||||
|                     } else { | ||||
|                         doZFilter = true; | ||||
|                     } | ||||
|                 } | ||||
|                 if (searchSet === null) { | ||||
|                     searchSet = nodes; | ||||
|                 } | ||||
|  | ||||
|                 for (var n=0;n<searchSet.length;n++) { | ||||
|                     var node = searchSet[n]; | ||||
|                     if (filter.hasOwnProperty("type") && node.type !== filter.type) { | ||||
|                         continue; | ||||
|                     } | ||||
|                     if (doZFilter && node.z !== filter.z) { | ||||
|                         continue; | ||||
|                     } | ||||
|                     result.push(node); | ||||
|                 } | ||||
|                 return result; | ||||
|             }, | ||||
|             getNodeOrder: function(z) { | ||||
|                 return tabMap[z].map(function(n) { return n.id }) | ||||
|             }, | ||||
|             setNodeOrder: function(z, order) { | ||||
|                 var orderMap = {}; | ||||
|                 order.forEach(function(id,i) { | ||||
|                     orderMap[id] = i; | ||||
|                 }) | ||||
|                 tabMap[z].sort(function(A,B) { | ||||
|                     A._reordered = true; | ||||
|                     B._reordered = true; | ||||
|                     return orderMap[A.id] - orderMap[B.id]; | ||||
|                 }) | ||||
|             } | ||||
|         } | ||||
|         return api; | ||||
|     })() | ||||
|  | ||||
|     function getID() { | ||||
|         var bytes = []; | ||||
|         for (var i=0;i<8;i++) { | ||||
| @@ -294,15 +567,10 @@ RED.nodes = (function() { | ||||
|                 }); | ||||
|                 n.i = nextId+1; | ||||
|             } | ||||
|             nodes[n.id] = n; | ||||
|             allNodes.addNode(n); | ||||
|             if (!nodeLinks[n.id]) { | ||||
|                 nodeLinks[n.id] = {in:[],out:[]}; | ||||
|             } | ||||
|             if (nodeTabMap[n.z]) { | ||||
|                 nodeTabMap[n.z][n.id] = n; | ||||
|             } else { | ||||
|                 console.warn("Node added to unknown tab/subflow:",n); | ||||
|             } | ||||
|         } | ||||
|         RED.events.emit('nodes:add',n); | ||||
|     } | ||||
| @@ -330,10 +598,8 @@ RED.nodes = (function() { | ||||
|     function getNode(id) { | ||||
|         if (id in configNodes) { | ||||
|             return configNodes[id]; | ||||
|         } else if (id in nodes) { | ||||
|             return nodes[id]; | ||||
|         } | ||||
|         return null; | ||||
|         return allNodes.getNode(id); | ||||
|     } | ||||
|  | ||||
|     function removeNode(id) { | ||||
| @@ -345,13 +611,10 @@ RED.nodes = (function() { | ||||
|             delete configNodes[id]; | ||||
|             RED.events.emit('nodes:remove',node); | ||||
|             RED.workspaces.refresh(); | ||||
|         } else if (id in nodes) { | ||||
|             node = nodes[id]; | ||||
|             delete nodes[id] | ||||
|         } else if (allNodes.hasNode(id)) { | ||||
|             node = allNodes.getNode(id); | ||||
|             allNodes.removeNode(node); | ||||
|             delete nodeLinks[id]; | ||||
|             if (nodeTabMap[node.z]) { | ||||
|                 delete nodeTabMap[node.z][node.id]; | ||||
|             } | ||||
|             removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); }); | ||||
|             removedLinks.forEach(removeLink); | ||||
|             var updatedConfigNode = false; | ||||
| @@ -409,18 +672,32 @@ RED.nodes = (function() { | ||||
|         return {links:removedLinks,nodes:removedNodes}; | ||||
|     } | ||||
|  | ||||
|     function moveNodesForwards(nodes) { | ||||
|         return allNodes.moveNodesForwards(nodes); | ||||
|     } | ||||
|     function moveNodesBackwards(nodes) { | ||||
|         return allNodes.moveNodesBackwards(nodes); | ||||
|     } | ||||
|     function moveNodesToFront(nodes) { | ||||
|         return allNodes.moveNodesToFront(nodes); | ||||
|     } | ||||
|     function moveNodesToBack(nodes) { | ||||
|         return allNodes.moveNodesToBack(nodes); | ||||
|     } | ||||
|  | ||||
|     function getNodeOrder(z) { | ||||
|         return allNodes.getNodeOrder(z); | ||||
|     } | ||||
|     function setNodeOrder(z, order) { | ||||
|         allNodes.setNodeOrder(z,order); | ||||
|     } | ||||
|  | ||||
|     function moveNodeToTab(node, z) { | ||||
|         if (node.type === "group") { | ||||
|             moveGroupToTab(node,z); | ||||
|             return; | ||||
|         } | ||||
|         if (nodeTabMap[node.z]) { | ||||
|             delete nodeTabMap[node.z][node.id]; | ||||
|         } | ||||
|         if (!nodeTabMap[z]) { | ||||
|             nodeTabMap[z] = {}; | ||||
|         } | ||||
|         nodeTabMap[z][node.id] = node; | ||||
|         allNodes.moveNode(node,z); | ||||
|         var nl = nodeLinks[node.id]; | ||||
|         if (nl) { | ||||
|             nl.in.forEach(function(l) { | ||||
| @@ -482,7 +759,7 @@ RED.nodes = (function() { | ||||
|  | ||||
|     function addWorkspace(ws,targetIndex) { | ||||
|         workspaces[ws.id] = ws; | ||||
|         nodeTabMap[ws.id] = {}; | ||||
|         allNodes.addTab(ws.id); | ||||
|         linkTabMap[ws.id] = []; | ||||
|  | ||||
|         ws._def = RED.nodes.getType('tab'); | ||||
| @@ -506,21 +783,16 @@ RED.nodes = (function() { | ||||
|         var removedGroups = []; | ||||
|         if (ws) { | ||||
|             delete workspaces[id]; | ||||
|             delete nodeTabMap[id]; | ||||
|             allNodes.removeTab(id); | ||||
|             delete linkTabMap[id]; | ||||
|             workspacesOrder.splice(workspacesOrder.indexOf(id),1); | ||||
|             var i; | ||||
|             var node; | ||||
|             // TODO: this should use nodeTabMap | ||||
|             for (i in nodes) { | ||||
|                 if (nodes.hasOwnProperty(i)) { | ||||
|                     node = nodes[i]; | ||||
|                     if (node.z == id) { | ||||
|                         removedNodes.push(node); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|             if (allNodes.hasTab(id)) { | ||||
|                 removedNodes = allNodes.getNodes(id).slice() | ||||
|             } | ||||
|             for(i in configNodes) { | ||||
|             for (i in configNodes) { | ||||
|                 if (configNodes.hasOwnProperty(i)) { | ||||
|                     node = configNodes[i]; | ||||
|                     if (node.z == id) { | ||||
| @@ -572,7 +844,7 @@ RED.nodes = (function() { | ||||
|             sf.name = subflowName; | ||||
|         } | ||||
|         subflows[sf.id] = sf; | ||||
|         nodeTabMap[sf.id] = {}; | ||||
|         allNodes.addTab(sf.id); | ||||
|         linkTabMap[sf.id] = []; | ||||
|  | ||||
|         RED.nodes.registerType("subflow:"+sf.id, { | ||||
| @@ -591,18 +863,18 @@ RED.nodes = (function() { | ||||
|             inputLabels: function(i) { return sf.inputLabels?sf.inputLabels[i]:null }, | ||||
|             outputLabels: function(i) { return sf.outputLabels?sf.outputLabels[i]:null }, | ||||
|             oneditprepare: function() { | ||||
|                 RED.subflow.buildEditForm("subflow",this); | ||||
|                 RED.subflow.buildPropertiesForm(this); | ||||
|                 if (this.type !== 'subflow') { | ||||
|                     // A subflow instance node | ||||
|                     RED.subflow.buildEditForm("subflow",this); | ||||
|                 } else { | ||||
|                     // A subflow template node | ||||
|                     RED.subflow.buildEditForm("subflow-template", this); | ||||
|                 } | ||||
|             }, | ||||
|             oneditresize: function(size) { | ||||
|                 // var rows = $(".dialog-form>div:not(.node-input-env-container-row)"); | ||||
|                 var height = size.height; | ||||
|                 // for (var i=0; i<rows.size(); i++) { | ||||
|                 //     height -= $(rows[i]).outerHeight(true); | ||||
|                 // } | ||||
|                 // var editorRow = $("#dialog-form>div.node-input-env-container-row"); | ||||
|                 // height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom"))); | ||||
|                 $("ol.red-ui-editor-subflow-env-list").editableList('height',height); | ||||
|                 if (this.type === 'subflow') { | ||||
|                     $("#node-input-env-container").editableList('height',size.height - 80); | ||||
|                 } | ||||
|             }, | ||||
|             set:{ | ||||
|                 module: "node-red" | ||||
| @@ -618,27 +890,24 @@ RED.nodes = (function() { | ||||
|     function removeSubflow(sf) { | ||||
|         if (subflows[sf.id]) { | ||||
|             delete subflows[sf.id]; | ||||
|             delete nodeTabMap[sf.id]; | ||||
|             allNodes.removeTab(sf.id); | ||||
|             registry.removeNodeType("subflow:"+sf.id); | ||||
|             RED.events.emit("subflows:remove",sf); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function subflowContains(sfid,nodeid) { | ||||
|         for (var i in nodes) { | ||||
|             if (nodes.hasOwnProperty(i)) { | ||||
|                 var node = nodes[i]; | ||||
|                 if (node.z === sfid) { | ||||
|                     var m = /^subflow:(.+)$/.exec(node.type); | ||||
|                     if (m) { | ||||
|                         if (m[1] === nodeid) { | ||||
|                             return true; | ||||
|                         } else { | ||||
|                             var result = subflowContains(m[1],nodeid); | ||||
|                             if (result) { | ||||
|                                 return true; | ||||
|                             } | ||||
|                         } | ||||
|         var sfNodes = allNodes.getNodes(sfid); | ||||
|         for (var i = 0; i<sfNodes.length; i++) { | ||||
|             var node = sfNodes[i]; | ||||
|             var m = /^subflow:(.+)$/.exec(node.type); | ||||
|             if (m) { | ||||
|                 if (m[1] === nodeid) { | ||||
|                     return true; | ||||
|                 } else { | ||||
|                     var result = subflowContains(m[1],nodeid); | ||||
|                     if (result) { | ||||
|                         return true; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| @@ -681,7 +950,13 @@ RED.nodes = (function() { | ||||
|     } | ||||
|  | ||||
|  | ||||
|     function convertWorkspace(n) { | ||||
|     function convertWorkspace(n,opts) { | ||||
|         var exportCreds = true; | ||||
|         if (opts) { | ||||
|             if (opts.hasOwnProperty("credentials")) { | ||||
|                 exportCreds = opts.credentials; | ||||
|             } | ||||
|         } | ||||
|         var node = {}; | ||||
|         node.id = n.id; | ||||
|         node.type = n.type; | ||||
| @@ -690,6 +965,23 @@ RED.nodes = (function() { | ||||
|                 node[d] = n[d]; | ||||
|             } | ||||
|         } | ||||
|         if (exportCreds) { | ||||
|             var credentialSet = {}; | ||||
|             if (n.credentials) { | ||||
|                 for (var tabCred in n.credentials) { | ||||
|                     if (n.credentials.hasOwnProperty(tabCred)) { | ||||
|                         if (!n.credentials._ || | ||||
|                             n.credentials["has_"+tabCred] != n.credentials._["has_"+tabCred] || | ||||
|                             (n.credentials["has_"+tabCred] && n.credentials[tabCred])) { | ||||
|                             credentialSet[tabCred] = n.credentials[tabCred]; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 if (Object.keys(credentialSet).length > 0) { | ||||
|                     node.credentials = credentialSet; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return node; | ||||
|     } | ||||
|     /** | ||||
| @@ -710,7 +1002,7 @@ RED.nodes = (function() { | ||||
|         } | ||||
|  | ||||
|         if (n.type === 'tab') { | ||||
|             return convertWorkspace(n); | ||||
|             return convertWorkspace(n, { credentials: exportCreds }); | ||||
|         } | ||||
|         var node = {}; | ||||
|         node.id = n.id; | ||||
| @@ -739,8 +1031,10 @@ RED.nodes = (function() { | ||||
|             } | ||||
|             if (exportCreds) { | ||||
|                 var credentialSet = {}; | ||||
|                 if (/^subflow:/.test(node.type) && n.credentials) { | ||||
|                     // A subflow instance node can have arbitrary creds | ||||
|                 if ((/^subflow:/.test(node.type) || | ||||
|                      (node.type === "group")) && | ||||
|                     n.credentials) { | ||||
|                     // A subflow instance/group node can have arbitrary creds | ||||
|                     for (var sfCred in n.credentials) { | ||||
|                         if (n.credentials.hasOwnProperty(sfCred)) { | ||||
|                             if (!n.credentials._ || | ||||
| @@ -934,11 +1228,15 @@ RED.nodes = (function() { | ||||
|  | ||||
|     function createExportableSubflow(id) { | ||||
|         var sf = getSubflow(id); | ||||
|         var nodeSet = [sf]; | ||||
|         var sfNodeIds = Object.keys(nodeTabMap[sf.id]||{}); | ||||
|         for (var i=0, l=sfNodeIds.length; i<l; i++) { | ||||
|             nodeSet.push(nodeTabMap[sf.id][sfNodeIds[i]]); | ||||
|         var nodeSet; | ||||
|         var sfNodes = allNodes.getNodes(sf.id); | ||||
|         if (sfNodes) { | ||||
|             nodeSet = sfNodes.slice(); | ||||
|             nodeSet.unshift(sf); | ||||
|         } else { | ||||
|             nodeSet = [sf]; | ||||
|         } | ||||
|         console.log(nodeSet); | ||||
|         return createExportableNodeSet(nodeSet); | ||||
|     } | ||||
|     /** | ||||
| @@ -965,12 +1263,9 @@ RED.nodes = (function() { | ||||
|                 if (!exportedSubflows[subflowId]) { | ||||
|                     exportedSubflows[subflowId] = true; | ||||
|                     var subflow = getSubflow(subflowId); | ||||
|                     var subflowSet = [subflow]; | ||||
|                     RED.nodes.eachNode(function(n) { | ||||
|                         if (n.z == subflowId) { | ||||
|                             subflowSet.push(n); | ||||
|                         } | ||||
|                     }); | ||||
|                     var subflowSet = allNodes.getNodes(subflowId).slice(); | ||||
|                     subflowSet.unshift(subflow); | ||||
|  | ||||
|                     RED.nodes.eachConfig(function(n) { | ||||
|                         if (n.z == subflowId) { | ||||
|                             subflowSet.push(n); | ||||
| @@ -1030,7 +1325,7 @@ RED.nodes = (function() { | ||||
|         var i; | ||||
|         for (i=0;i<workspacesOrder.length;i++) { | ||||
|             if (workspaces[workspacesOrder[i]].type == "tab") { | ||||
|                 nns.push(convertWorkspace(workspaces[workspacesOrder[i]])); | ||||
|                 nns.push(convertWorkspace(workspaces[workspacesOrder[i]], opts)); | ||||
|             } | ||||
|         } | ||||
|         for (i in subflows) { | ||||
| @@ -1048,11 +1343,9 @@ RED.nodes = (function() { | ||||
|                 nns.push(convertNode(configNodes[i], opts)); | ||||
|             } | ||||
|         } | ||||
|         for (i in nodes) { | ||||
|             if (nodes.hasOwnProperty(i)) { | ||||
|                 nns.push(convertNode(nodes[i], opts)); | ||||
|             } | ||||
|         } | ||||
|         RED.nodes.eachNode(function(n) { | ||||
|             nns.push(convertNode(n, opts)); | ||||
|         }) | ||||
|         return nns; | ||||
|     } | ||||
|  | ||||
| @@ -1149,7 +1442,7 @@ RED.nodes = (function() { | ||||
|             var nodeZ = n.z || "__global__"; | ||||
|             imported.zMap[nodeZ] = imported.zMap[nodeZ] || []; | ||||
|             imported.zMap[nodeZ].push(n) | ||||
|             if (nodes[n.id] || configNodes[n.id] || workspaces[n.id] || subflows[n.id] || groups[n.id]) { | ||||
|             if (allNodes.hasNode(n.id) || configNodes[n.id] || workspaces[n.id] || subflows[n.id] || groups[n.id]) { | ||||
|                 imported.conflicted[n.id] = n; | ||||
|             } | ||||
|         }) | ||||
| @@ -1157,7 +1450,6 @@ RED.nodes = (function() { | ||||
|  | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Replace the provided nodes. | ||||
|      * This must contain complete Subflow defs or complete Flow Tabs. | ||||
| @@ -1316,7 +1608,7 @@ RED.nodes = (function() { | ||||
|             if (!options.generateIds) { | ||||
|                 if (!options.importMap[id]) { | ||||
|                     // No conflict resolution for this node | ||||
|                     var existing = nodes[id] || configNodes[id] || workspaces[id] || subflows[id] || groups[id]; | ||||
|                     var existing = allNodes.getNode(id) || configNodes[id] || workspaces[id] || subflows[id] || groups[id]; | ||||
|                     if (existing) { | ||||
|                         existingNodes.push({existing:existing, imported:n}); | ||||
|                     } | ||||
| @@ -1387,7 +1679,8 @@ RED.nodes = (function() { | ||||
|                         type: "tab", | ||||
|                         disabled: false, | ||||
|                         label: RED._("clipboard.recoveredNodes"), | ||||
|                         info: RED._("clipboard.recoveredNodesInfo") | ||||
|                         info: RED._("clipboard.recoveredNodesInfo"), | ||||
|                         env: [] | ||||
|                     } | ||||
|                     addWorkspace(recoveryWorkspace); | ||||
|                     RED.workspaces.add(recoveryWorkspace); | ||||
| @@ -1518,7 +1811,7 @@ RED.nodes = (function() { | ||||
|  | ||||
|         // Add a tab if there isn't one there already | ||||
|         if (defaultWorkspace == null) { | ||||
|             defaultWorkspace = { type:"tab", id:getID(), disabled: false, info:"",  label:RED._('workspace.defaultName',{number:1})}; | ||||
|             defaultWorkspace = { type:"tab", id:getID(), disabled: false, info:"",  label:RED._('workspace.defaultName',{number:1}), env:[]}; | ||||
|             addWorkspace(defaultWorkspace); | ||||
|             RED.workspaces.add(defaultWorkspace); | ||||
|             new_workspaces.push(defaultWorkspace); | ||||
| @@ -1979,32 +2272,9 @@ RED.nodes = (function() { | ||||
|  | ||||
|     // TODO: supports filter.z|type | ||||
|     function filterNodes(filter) { | ||||
|         var result = []; | ||||
|         var searchSet = null; | ||||
|         var doZFilter = false; | ||||
|         if (filter.hasOwnProperty("z")) { | ||||
|             if (nodeTabMap.hasOwnProperty(filter.z)) { | ||||
|                 searchSet = Object.keys(nodeTabMap[filter.z]); | ||||
|             } else { | ||||
|                 doZFilter = true; | ||||
|             } | ||||
|         } | ||||
|         if (searchSet === null) { | ||||
|             searchSet = Object.keys(nodes); | ||||
|         } | ||||
|  | ||||
|         for (var n=0;n<searchSet.length;n++) { | ||||
|             var node = nodes[searchSet[n]]; | ||||
|             if (filter.hasOwnProperty("type") && node.type !== filter.type) { | ||||
|                 continue; | ||||
|             } | ||||
|             if (doZFilter && node.z !== filter.z) { | ||||
|                 continue; | ||||
|             } | ||||
|             result.push(node); | ||||
|         } | ||||
|         return result; | ||||
|         return allNodes.filterNodes(filter); | ||||
|     } | ||||
|  | ||||
|     function filterLinks(filter) { | ||||
|         var result = []; | ||||
|         var candidateLinks = []; | ||||
| @@ -2092,9 +2362,8 @@ RED.nodes = (function() { | ||||
|     } | ||||
|  | ||||
|     function clear() { | ||||
|         nodes = {}; | ||||
|         allNodes.clear(); | ||||
|         links = []; | ||||
|         nodeTabMap = {}; | ||||
|         linkTabMap = {}; | ||||
|         nodeLinks = {}; | ||||
|         configNodes = {}; | ||||
| @@ -2186,10 +2455,7 @@ RED.nodes = (function() { | ||||
|                         if (configNodes.hasOwnProperty(n.id)) { | ||||
|                             delete configNodes[n.id]; | ||||
|                         } else { | ||||
|                             delete nodes[n.id]; | ||||
|                             if (nodeTabMap[n.z]) { | ||||
|                                 delete nodeTabMap[n.z][n.id]; | ||||
|                             } | ||||
|                             allNodes.removeNode(n); | ||||
|                         } | ||||
|                         reimportList.push(convertNode(n)); | ||||
|                         RED.events.emit('nodes:remove',n); | ||||
| @@ -2246,6 +2512,13 @@ RED.nodes = (function() { | ||||
|         remove: removeNode, | ||||
|         clear: clear, | ||||
|  | ||||
|         moveNodesForwards: moveNodesForwards, | ||||
|         moveNodesBackwards: moveNodesBackwards, | ||||
|         moveNodesToFront: moveNodesToFront, | ||||
|         moveNodesToBack: moveNodesToBack, | ||||
|         getNodeOrder: getNodeOrder, | ||||
|         setNodeOrder: setNodeOrder, | ||||
|  | ||||
|         moveNodeToTab: moveNodeToTab, | ||||
|  | ||||
|         addLink: addLink, | ||||
| @@ -2265,16 +2538,10 @@ RED.nodes = (function() { | ||||
|         addGroup: addGroup, | ||||
|         removeGroup: removeGroup, | ||||
|         group: function(id) { return groups[id] }, | ||||
|         groups: function(z) { return groupsByZ[z]||[] }, | ||||
|         groups: function(z) { return groupsByZ[z]?groupsByZ[z].slice():[] }, | ||||
|  | ||||
|         eachNode: function(cb) { | ||||
|             for (var id in nodes) { | ||||
|                 if (nodes.hasOwnProperty(id)) { | ||||
|                     if (cb(nodes[id]) === false) { | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             allNodes.eachNode(cb); | ||||
|         }, | ||||
|         eachLink: function(cb) { | ||||
|             for (var l=0;l<links.length;l++) { | ||||
|   | ||||
| @@ -559,6 +559,22 @@ var RED = (function() { | ||||
|                 {id:"menu-item-projects-settings",label:RED._("menu.label.projects-settings"),disabled:false,onselect:"core:show-project-settings"} | ||||
|             ]}); | ||||
|         } | ||||
|         menuOptions.push({id:"menu-item-edit-menu", label:"Edit", options: [ | ||||
|             {id: "menu-item-edit-undo", label:RED._("keyboard.undoChange"), disabled: true, onselect: "core:undo"}, | ||||
|             {id: "menu-item-edit-redo", label:RED._("keyboard.redoChange"), disabled: true, onselect: "core:redo"}, | ||||
|             null, | ||||
|             {id: "menu-item-edit-cut", label:RED._("keyboard.cutNode"), onselect: "core:cut-selection-to-internal-clipboard"}, | ||||
|             {id: "menu-item-edit-copy", label:RED._("keyboard.copyNode"), onselect: "core:copy-selection-to-internal-clipboard"}, | ||||
|             {id: "menu-item-edit-paste", label:RED._("keyboard.pasteNode"), disabled: true, onselect: "core:paste-from-internal-clipboard"}, | ||||
|             null, | ||||
|             {id: "menu-item-edit-copy-group-style", label:RED._("keyboard.copyGroupStyle"), onselect: "core:copy-group-style"}, | ||||
|             {id: "menu-item-edit-paste-group-style", label:RED._("keyboard.pasteGroupStyle"), disabled: true, onselect: "core:paste-group-style"}, | ||||
|             null, | ||||
|             {id: "menu-item-edit-select-all", label:RED._("keyboard.selectAll"), onselect: "core:select-all-nodes"}, | ||||
|             {id: "menu-item-edit-select-connected", label:RED._("keyboard.selectAllConnected"), onselect: "core:select-connected-nodes"}, | ||||
|             {id: "menu-item-edit-select-none", label:RED._("keyboard.selectNone"), onselect: "core:select-none"} | ||||
|         ]}); | ||||
|  | ||||
|         menuOptions.push({id:"menu-item-view-menu",label:RED._("menu.label.view.view"),options:[ | ||||
|             {id:"menu-item-palette",label:RED._("menu.label.palette.show"),toggle:true,onselect:"core:toggle-palette", selected: true}, | ||||
|             {id:"menu-item-sidebar",label:RED._("menu.label.sidebar.show"),toggle:true,onselect:"core:toggle-sidebar", selected: true}, | ||||
| @@ -566,6 +582,25 @@ var RED = (function() { | ||||
|             {id:"menu-item-action-list",label:RED._("keyboard.actionList"),onselect:"core:show-action-list"}, | ||||
|             null | ||||
|         ]}); | ||||
|  | ||||
|         menuOptions.push({id:"menu-item-arrange-menu", label:RED._("menu.label.arrange"), options: [ | ||||
|             {id: "menu-item-view-tools-move-to-back", label:RED._("menu.label.moveToBack"), disabled: true, onselect: "core:move-selection-to-back"}, | ||||
|             {id: "menu-item-view-tools-move-to-front", label:RED._("menu.label.moveToFront"), disabled: true, onselect: "core:move-selection-to-front"}, | ||||
|             {id: "menu-item-view-tools-move-backwards", label:RED._("menu.label.moveBackwards"), disabled: true, onselect: "core:move-selection-backwards"}, | ||||
|             {id: "menu-item-view-tools-move-forwards", label:RED._("menu.label.moveForwards"), disabled: true, onselect: "core:move-selection-forwards"}, | ||||
|             null, | ||||
|             {id: "menu-item-view-tools-align-left", label:RED._("menu.label.alignLeft"), disabled: true, onselect: "core:align-selection-to-left"}, | ||||
|             {id: "menu-item-view-tools-align-center", label:RED._("menu.label.alignCenter"), disabled: true, onselect: "core:align-selection-to-center"}, | ||||
|             {id: "menu-item-view-tools-align-right", label:RED._("menu.label.alignRight"), disabled: true, onselect: "core:align-selection-to-right"}, | ||||
|             null, | ||||
|             {id: "menu-item-view-tools-align-top", label:RED._("menu.label.alignTop"), disabled: true, onselect: "core:align-selection-to-top"}, | ||||
|             {id: "menu-item-view-tools-align-middle", label:RED._("menu.label.alignMiddle"), disabled: true, onselect: "core:align-selection-to-middle"}, | ||||
|             {id: "menu-item-view-tools-align-bottom", label:RED._("menu.label.alignBottom"), disabled: true, onselect: "core:align-selection-to-bottom"}, | ||||
|             null, | ||||
|             {id: "menu-item-view-tools-distribute-horizontally", label:RED._("menu.label.distributeHorizontally"), disabled: true, onselect: "core:distribute-selection-horizontally"}, | ||||
|             {id: "menu-item-view-tools-distribute-veritcally", label:RED._("menu.label.distributeVertically"), disabled: true, onselect: "core:distribute-selection-vertically"} | ||||
|         ]}); | ||||
|  | ||||
|         menuOptions.push(null); | ||||
|         if (RED.settings.theme("menu.menu-item-import-library", true)) { | ||||
|             menuOptions.push({id: "menu-item-import", label: RED._("menu.label.import"), onselect: "core:show-import-dialog"}); | ||||
| @@ -626,7 +661,6 @@ var RED = (function() { | ||||
|         RED.user.init(); | ||||
|         RED.notifications.init(); | ||||
|         RED.library.init(); | ||||
|         RED.keyboard.init(); | ||||
|         RED.palette.init(); | ||||
|         RED.eventLog.init(); | ||||
|  | ||||
| @@ -655,7 +689,7 @@ var RED = (function() { | ||||
|  | ||||
|         RED.deploy.init(RED.settings.theme("deployButton",null)); | ||||
|  | ||||
|         buildMainMenu(); | ||||
|         RED.keyboard.init(buildMainMenu); | ||||
|  | ||||
|         RED.nodes.init(); | ||||
|         RED.comms.connect(); | ||||
|   | ||||
| @@ -88,6 +88,13 @@ RED.menu = (function() { | ||||
|             linkContent += '</a>'; | ||||
|  | ||||
|             var link = $(linkContent).appendTo(item); | ||||
|             opt.link = link; | ||||
|             if (typeof opt.onselect === 'string') { | ||||
|                 var shortcut = RED.keyboard.getShortcut(opt.onselect); | ||||
|                 if (shortcut && shortcut.key) { | ||||
|                     opt.shortcutSpan = $('<span class="red-ui-popover-key">'+RED.keyboard.formatKey(shortcut.key, true)+'</span>').appendTo(link); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             menuItems[opt.id] = opt; | ||||
|  | ||||
| @@ -276,6 +283,22 @@ RED.menu = (function() { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function refreshShortcuts() { | ||||
|         for (var id in menuItems) { | ||||
|             if (menuItems.hasOwnProperty(id)) { | ||||
|                 var opt = menuItems[id]; | ||||
|                 if (typeof opt.onselect === "string" && opt.shortcutSpan) { | ||||
|                     opt.shortcutSpan.remove(); | ||||
|                     delete opt.shortcutSpan; | ||||
|                     var shortcut = RED.keyboard.getShortcut(opt.onselect); | ||||
|                     if (shortcut && shortcut.key) { | ||||
|                         opt.shortcutSpan = $('<span class="red-ui-popover-key">'+RED.keyboard.formatKey(shortcut.key, true)+'</span>').appendTo(opt.link); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         init: createMenu, | ||||
|         setSelected: setSelected, | ||||
| @@ -284,6 +307,7 @@ RED.menu = (function() { | ||||
|         setDisabled: setDisabled, | ||||
|         addItem: addItem, | ||||
|         removeItem: removeItem, | ||||
|         setAction: setAction | ||||
|         setAction: setAction, | ||||
|         refreshShortcuts: refreshShortcuts | ||||
|     } | ||||
| })(); | ||||
|   | ||||
| @@ -664,6 +664,8 @@ RED.tabs = (function() { | ||||
|                 link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); }) | ||||
|  | ||||
|  | ||||
|                 $('<span class="red-ui-tabs-fade"></span>').appendTo(li); | ||||
|  | ||||
|                 if (tab.closeable) { | ||||
|                     li.addClass("red-ui-tabs-closeable") | ||||
|                     var closeLink = $("<a/>",{href:"#",class:"red-ui-tab-close"}).appendTo(li); | ||||
| @@ -674,8 +676,6 @@ RED.tabs = (function() { | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 $('<span class="red-ui-tabs-fade"></span>').appendTo(li); | ||||
|  | ||||
|                 var badges = $('<span class="red-ui-tabs-badges"></span>').appendTo(li); | ||||
|                 if (options.onselect) { | ||||
|                     $('<i class="red-ui-tabs-badge-changed fa fa-circle"></i>').appendTo(badges); | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -31,7 +31,7 @@ | ||||
|         function setMode(mode, cb) | ||||
|         function getRange(); | ||||
|         function replace(range, text) | ||||
|         function selectAll  | ||||
|         function selectAll | ||||
|         function clearSelection | ||||
|         function getSelectedText() | ||||
|         function destroy() | ||||
| @@ -154,9 +154,9 @@ RED.editor.codeEditor.monaco = (function() { | ||||
|  | ||||
|     function init(options) { | ||||
|  | ||||
|         //Handles orphaned models  | ||||
|         //Handles orphaned models | ||||
|         //ensure loaded models that are not explicitly destroyed by a call to .destroy() are disposed | ||||
|         RED.events.on("editor:close",function() {  | ||||
|         RED.events.on("editor:close",function() { | ||||
|             let models = window.monaco ? monaco.editor.getModels() : null; | ||||
|             if(models && models.length) { | ||||
|                 console.warn("Cleaning up monaco models left behind. Any node that calls createEditor() should call .destroy().") | ||||
| @@ -765,10 +765,10 @@ RED.editor.codeEditor.monaco = (function() { | ||||
|  | ||||
|         //by default, set javascript editors to text mode. | ||||
|         //when element becomes visible, it will be (re) set to javascript mode | ||||
|         //this is to ensure multiple editors sharing the model dont present its  | ||||
|         //this is to ensure multiple editors sharing the model dont present its | ||||
|         //consts & lets to each other | ||||
|         if(editorOptions.language == "javascript") { | ||||
|             editorOptions._language = editorOptions.language;  | ||||
|             editorOptions._language = editorOptions.language; | ||||
|             editorOptions.language = "text" | ||||
|         } | ||||
|  | ||||
| @@ -942,6 +942,15 @@ RED.editor.codeEditor.monaco = (function() { | ||||
|         /*********** Create the monaco editor ***************/ | ||||
|         var ed = monaco.editor.create(el, editorOptions); | ||||
|  | ||||
|         //Unbind ctrl-Enter (default action is to insert a newline in editor) This permits the shortcut to close the tray. | ||||
|         try { | ||||
|             ed._standaloneKeybindingService.addDynamicKeybinding( | ||||
|                 '-editor.action.insertLineAfter', // command ID prefixed by '-' | ||||
|                 null, // keybinding | ||||
|                 () => {} // need to pass an empty handler | ||||
|             ); | ||||
|         } catch (error) { } | ||||
|  | ||||
|         ed.nodered = { | ||||
|             refreshModuleLibs: refreshModuleLibs //expose this for function node externalModules refresh | ||||
|         } | ||||
| @@ -988,7 +997,7 @@ RED.editor.codeEditor.monaco = (function() { | ||||
|             if (cb && typeof cb == "function") { | ||||
|                 cb(); | ||||
|             } | ||||
|             if(resize) {  | ||||
|             if(resize) { | ||||
|                 this.resize(); //cause a call to layout() | ||||
|             } | ||||
|         } | ||||
| @@ -1242,7 +1251,7 @@ RED.editor.codeEditor.monaco = (function() { | ||||
|         } | ||||
|         ed._mode = editorOptions.language; | ||||
|  | ||||
|         //as models are signleton, consts and let are avialable to other javascript instances  | ||||
|         //as models are signleton, consts and let are avialable to other javascript instances | ||||
|         //so when not focused, set editor mode to text temporarily to avoid multiple defs | ||||
|  | ||||
|         if(editorOptions._language) { | ||||
| @@ -1264,15 +1273,15 @@ RED.editor.codeEditor.monaco = (function() { | ||||
|             try { | ||||
|                 var options = { | ||||
|                     root: $(element).closest("div.red-ui-tray-content")[0] || document, | ||||
|                     attributes: true,  | ||||
|                     childList: true,  | ||||
|                     attributes: true, | ||||
|                     childList: true, | ||||
|                 }; | ||||
|                 var observer = new IntersectionObserver(function(entries, observer) { | ||||
|                     entries.forEach(function(entry) { | ||||
|                         callback(entry.intersectionRatio > 0, 5, entry.target); | ||||
|                     }); | ||||
|                 }, options); | ||||
|                 observer.observe(element);  | ||||
|                 observer.observe(element); | ||||
|             } catch (e1) { | ||||
|                 //browser not supporting IntersectionObserver? then fall back to polling! | ||||
|                 try { | ||||
| @@ -1291,7 +1300,7 @@ RED.editor.codeEditor.monaco = (function() { | ||||
|         function onVisibilityChange(visible, delay, element) { | ||||
|             if(visible) { | ||||
|                 if(ed._mode == "javascript" && ed._tempMode == "text") { | ||||
|                     ed._tempMode = "";  | ||||
|                     ed._tempMode = ""; | ||||
|                     setTimeout(function() { | ||||
|                         if(element.parentElement) { //ensure el is still in DOM | ||||
|                             ed.setMode('javascript', undefined, false); | ||||
| @@ -1301,7 +1310,7 @@ RED.editor.codeEditor.monaco = (function() { | ||||
|             } else if(ed._mode == "javascript" && ed._tempMode != "text") { | ||||
|                 if(element.parentElement) { //ensure el is still in DOM | ||||
|                     ed.setMode('text', undefined, false); | ||||
|                     ed._tempMode = "text";  | ||||
|                     ed._tempMode = "text"; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @@ -1377,4 +1386,4 @@ RED.editor.codeEditor.monaco = (function() { | ||||
|          */ | ||||
|         create: create | ||||
|     } | ||||
| })(); | ||||
| })(); | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| RED.colorPicker = (function() { | ||||
| RED.editor.colorPicker = RED.colorPicker = (function() { | ||||
| 
 | ||||
|     function create(options) { | ||||
|         var color = options.value; | ||||
							
								
								
									
										616
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/editors/envVarList.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										616
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/editors/envVarList.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,616 @@ | ||||
| RED.editor.envVarList = (function() { | ||||
|  | ||||
|     var currentLocale = 'en-US'; | ||||
|     var DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env']; | ||||
|     var DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred']; | ||||
|  | ||||
|     /** | ||||
|      * Create env var edit interface | ||||
|      * @param container - container | ||||
|      * @param node - subflow node | ||||
|      */ | ||||
|     function buildPropertiesList(envContainer, node) { | ||||
|  | ||||
|         var isTemplateNode = (node.type === "subflow"); | ||||
|  | ||||
|         envContainer | ||||
|             .css({ | ||||
|                 'min-height':'150px', | ||||
|                 'min-width':'450px' | ||||
|             }) | ||||
|             .editableList({ | ||||
|                 header: isTemplateNode?$('<div><div><div></div><div data-i18n="common.label.name"></div><div data-i18n="editor-tab.defaultValue"></div><div></div></div></div>'):undefined, | ||||
|                 addItem: function(container, i, opt) { | ||||
|                     // If this is an instance node, these are properties unique to | ||||
|                     // this instance - ie opt.parent will not be defined. | ||||
|  | ||||
|                     if (isTemplateNode) { | ||||
|                         container.addClass("red-ui-editor-subflow-env-editable") | ||||
|                     } | ||||
|  | ||||
|                     var envRow = $('<div/>').appendTo(container); | ||||
|                     var nameField = null; | ||||
|                     var valueField = null; | ||||
|  | ||||
|                     nameField = $('<input/>', { | ||||
|                         class: "node-input-env-name", | ||||
|                         type: "text", | ||||
|                         placeholder: RED._("common.label.name") | ||||
|                     }).attr("autocomplete","disable").appendTo(envRow).val(opt.name); | ||||
|                     valueField = $('<input/>',{ | ||||
|                         style: "width:100%", | ||||
|                         class: "node-input-env-value", | ||||
|                         type: "text", | ||||
|                     }).attr("autocomplete","disable").appendTo(envRow) | ||||
|                     valueField.typedInput({default:'str',types:isTemplateNode?DEFAULT_ENV_TYPE_LIST:DEFAULT_ENV_TYPE_LIST_INC_CRED}); | ||||
|                     valueField.typedInput('type', opt.type); | ||||
|                     if (opt.type === "cred") { | ||||
|                         if (opt.value) { | ||||
|                             valueField.typedInput('value', opt.value); | ||||
|                         } else if (node.credentials && node.credentials[opt.name]) { | ||||
|                             valueField.typedInput('value', node.credentials[opt.name]); | ||||
|                         } else if (node.credentials && node.credentials['has_'+opt.name]) { | ||||
|                             valueField.typedInput('value', "__PWRD__"); | ||||
|                         } else { | ||||
|                             valueField.typedInput('value', ""); | ||||
|                         } | ||||
|                     } else { | ||||
|                         valueField.typedInput('value', opt.value); | ||||
|                     } | ||||
|  | ||||
|  | ||||
|                     opt.nameField = nameField; | ||||
|                     opt.valueField = valueField; | ||||
|  | ||||
|                     var actionButton = $('<a/>',{href:"#",class:"red-ui-editableList-item-remove red-ui-button red-ui-button-small"}).appendTo(envRow); | ||||
|                     $('<i/>',{class:"fa "+(opt.parent?"fa-reply":"fa-remove")}).appendTo(actionButton); | ||||
|                     var removeTip = RED.popover.tooltip(actionButton,RED._("subflow.env.remove")); | ||||
|                     actionButton.on("click", function(evt) { | ||||
|                         evt.preventDefault(); | ||||
|                         removeTip.close(); | ||||
|                         container.parent().addClass("red-ui-editableList-item-deleting") | ||||
|                         container.fadeOut(300, function() { | ||||
|                             envContainer.editableList('removeItem',opt); | ||||
|                         }); | ||||
|                     }); | ||||
|  | ||||
|                     if (isTemplateNode) { | ||||
|                         // Add the UI customisation row | ||||
|                         // if `opt.ui` does not exist, then apply defaults. If these | ||||
|                         // defaults do not change then they will get stripped off | ||||
|                         // before saving. | ||||
|                         if (opt.type === 'cred') { | ||||
|                             opt.ui = opt.ui || { | ||||
|                                 icon: "", | ||||
|                                 type: "cred" | ||||
|                             } | ||||
|                             opt.ui.type = "cred"; | ||||
|                         } else { | ||||
|                             opt.ui = opt.ui || { | ||||
|                                 icon: "", | ||||
|                                 type: "input", | ||||
|                                 opts: {types:DEFAULT_ENV_TYPE_LIST} | ||||
|                             } | ||||
|                         } | ||||
|                         opt.ui.label = opt.ui.label || {}; | ||||
|                         opt.ui.type = opt.ui.type || "input"; | ||||
|  | ||||
|                         var uiRow = $('<div/>').appendTo(container).hide(); | ||||
|                         // save current info for reverting on cancel | ||||
|                         // var copy = $.extend(true, {}, ui); | ||||
|  | ||||
|                          $('<a href="#"><i class="fa fa-angle-right"></a>').prependTo(envRow).on("click", function (evt) { | ||||
|                             evt.preventDefault(); | ||||
|                             if ($(this).hasClass('expanded')) { | ||||
|                                 uiRow.slideUp(); | ||||
|                                 $(this).removeClass('expanded'); | ||||
|                             } else { | ||||
|                                 uiRow.slideDown(); | ||||
|                                 $(this).addClass('expanded'); | ||||
|                             } | ||||
|                         }); | ||||
|  | ||||
|                         buildEnvEditRow(uiRow, opt.ui, nameField, valueField); | ||||
|                         nameField.trigger('change'); | ||||
|                     } | ||||
|                 }, | ||||
|                 sortable: ".red-ui-editableList-item-handle", | ||||
|                 removable: false | ||||
|             }); | ||||
|         var parentEnv = {}; | ||||
|         var envList = []; | ||||
|         if (/^subflow:/.test(node.type)) { | ||||
|             var subflowDef = RED.nodes.subflow(node.type.substring(8)); | ||||
|             if (subflowDef.env) { | ||||
|                 subflowDef.env.forEach(function(env) { | ||||
|                     var item = { | ||||
|                         name:env.name, | ||||
|                         parent: { | ||||
|                             type: env.type, | ||||
|                             value: env.value, | ||||
|                             ui: env.ui | ||||
|                         } | ||||
|                     } | ||||
|                     envList.push(item); | ||||
|                     parentEnv[env.name] = item; | ||||
|                 }) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (node.env) { | ||||
|             for (var i = 0; i < node.env.length; i++) { | ||||
|                 var env = node.env[i]; | ||||
|                 if (parentEnv.hasOwnProperty(env.name)) { | ||||
|                     parentEnv[env.name].type = env.type; | ||||
|                     parentEnv[env.name].value = env.value; | ||||
|                 } else { | ||||
|                     envList.push({ | ||||
|                         name: env.name, | ||||
|                         type: env.type, | ||||
|                         value: env.value, | ||||
|                         ui: env.ui | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         envList.forEach(function(env) { | ||||
|             if (env.parent && env.parent.ui && env.parent.ui.type === 'hide') { | ||||
|                 return; | ||||
|             } | ||||
|             if (!isTemplateNode && env.parent) { | ||||
|                 return; | ||||
|             } | ||||
|             envContainer.editableList('addItem', JSON.parse(JSON.stringify(env))); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Create UI edit interface for environment variable | ||||
|      * @param container - container | ||||
|      * @param env - env var definition | ||||
|      * @param nameField - name field of env var | ||||
|      * @param valueField - value field of env var | ||||
|      */ | ||||
|      function buildEnvEditRow(container, ui, nameField, valueField) { | ||||
|          container.addClass("red-ui-editor-subflow-env-ui-row") | ||||
|          var topRow = $('<div></div>').appendTo(container); | ||||
|          $('<div></div>').appendTo(topRow); | ||||
|          $('<div>').text(RED._("editor.icon")).appendTo(topRow); | ||||
|          $('<div>').text(RED._("editor.label")).appendTo(topRow); | ||||
|          $('<div>').text(RED._("editor.inputType")).appendTo(topRow); | ||||
|  | ||||
|          var row = $('<div></div>').appendTo(container); | ||||
|          $('<div><i class="red-ui-editableList-item-handle fa fa-bars"></i></div>').appendTo(row); | ||||
|          var typeOptions = { | ||||
|              'input': {types:DEFAULT_ENV_TYPE_LIST}, | ||||
|              'select': {opts:[]}, | ||||
|              'spinner': {}, | ||||
|              'cred': {} | ||||
|          }; | ||||
|          if (ui.opts) { | ||||
|              typeOptions[ui.type] = ui.opts; | ||||
|          } else { | ||||
|              // Pick up the default values if not otherwise provided | ||||
|              ui.opts = typeOptions[ui.type]; | ||||
|          } | ||||
|          var iconCell = $('<div></div>').appendTo(row); | ||||
|  | ||||
|          var iconButton = $('<a href="#"></a>').appendTo(iconCell); | ||||
|          iconButton.on("click", function(evt) { | ||||
|              evt.preventDefault(); | ||||
|              var icon = ui.icon || ""; | ||||
|              var iconPath = (icon ? RED.utils.separateIconPath(icon) : {}); | ||||
|              RED.editor.iconPicker.show(iconButton, null, iconPath, true, function (newIcon) { | ||||
|                  iconButton.empty(); | ||||
|                  var path = newIcon || ""; | ||||
|                  var newPath = RED.utils.separateIconPath(path); | ||||
|                  if (newPath) { | ||||
|                      $('<i class="fa"></i>').addClass(newPath.file).appendTo(iconButton); | ||||
|                  } | ||||
|                  ui.icon = path; | ||||
|              }); | ||||
|          }) | ||||
|  | ||||
|          if (ui.icon) { | ||||
|              var newPath = RED.utils.separateIconPath(ui.icon); | ||||
|              $('<i class="fa '+newPath.file+'"></i>').appendTo(iconButton); | ||||
|          } | ||||
|  | ||||
|          var labelCell = $('<div></div>').appendTo(row); | ||||
|  | ||||
|          var label = ui.label && ui.label[currentLocale] || ""; | ||||
|          var labelInput = $('<input type="text">').val(label).appendTo(labelCell); | ||||
|          ui.labelField = labelInput; | ||||
|          labelInput.on('change', function(evt) { | ||||
|              ui.label = ui.label || {}; | ||||
|              var val = $(this).val().trim(); | ||||
|              if (val === "") { | ||||
|                  delete ui.label[currentLocale]; | ||||
|              } else { | ||||
|                  ui.label[currentLocale] = val; | ||||
|              } | ||||
|          }) | ||||
|          var labelIcon = $('<span class="red-ui-editor-subflow-env-lang-icon"><i class="fa fa-language"></i></span>').appendTo(labelCell); | ||||
|          RED.popover.tooltip(labelIcon,function() { | ||||
|              var langs = Object.keys(ui.label); | ||||
|              var content = $("<div>"); | ||||
|              if (langs.indexOf(currentLocale) === -1) { | ||||
|                  langs.push(currentLocale); | ||||
|                  langs.sort(); | ||||
|              } | ||||
|              langs.forEach(function(l) { | ||||
|                  var row = $('<div>').appendTo(content); | ||||
|                  $('<span>').css({display:"inline-block",width:"120px"}).text(RED._("languages."+l)+(l===currentLocale?"*":"")).appendTo(row); | ||||
|                  $('<span>').text(ui.label[l]||"").appendTo(row); | ||||
|              }); | ||||
|              return content; | ||||
|          }) | ||||
|  | ||||
|          nameField.on('change',function(evt) { | ||||
|             labelInput.attr("placeholder",$(this).val()) | ||||
|         }); | ||||
|  | ||||
|         var inputCell = $('<div></div>').appendTo(row); | ||||
|         var inputCellInput = $('<input type="text">').css("width","100%").appendTo(inputCell); | ||||
|         if (ui.type === "input") { | ||||
|             inputCellInput.val(ui.opts.types.join(",")); | ||||
|         } | ||||
|         var checkbox; | ||||
|         var selectBox; | ||||
|  | ||||
|         inputCellInput.typedInput({ | ||||
|             types: [ | ||||
|                 { | ||||
|                     value:"input", | ||||
|                     label:RED._("editor.inputs.input"), icon:"fa fa-i-cursor",showLabel:false,multiple:true,options:[ | ||||
|                         {value:"str",label:RED._("editor.types.str"),icon:"red/images/typedInput/az.svg"}, | ||||
|                         {value:"num",label:RED._("editor.types.num"),icon:"red/images/typedInput/09.svg"}, | ||||
|                         {value:"bool",label:RED._("editor.types.bool"),icon:"red/images/typedInput/bool.svg"}, | ||||
|                         {value:"json",label:RED._("editor.types.json"),icon:"red/images/typedInput/json.svg"}, | ||||
|                         {value: "bin",label: RED._("editor.types.bin"),icon: "red/images/typedInput/bin.svg"}, | ||||
|                         {value: "env",label: RED._("editor.types.env"),icon: "red/images/typedInput/env.svg"}, | ||||
|                         {value: "cred",label: RED._("editor.types.cred"),icon: "fa fa-lock"} | ||||
|                     ], | ||||
|                     default: DEFAULT_ENV_TYPE_LIST, | ||||
|                     valueLabel: function(container,value) { | ||||
|                         container.css("padding",0); | ||||
|                         var innerContainer = $('<div class="red-ui-editor-subflow-env-input-type"></div>').appendTo(container); | ||||
|  | ||||
|                         var input = $('<div class="placeholder-input">').appendTo(innerContainer); | ||||
|                         $('<span><i class="fa fa-i-cursor"></i></span>').appendTo(input); | ||||
|                         if (value.length) { | ||||
|                             value.forEach(function(v) { | ||||
|                                 if (!/^fa /.test(v.icon)) { | ||||
|                                     $('<img>',{src:v.icon,style:"max-width:14px; padding: 0 3px; margin-top:-4px; margin-left: 1px"}).appendTo(input); | ||||
|                                 } else { | ||||
|                                     var s = $('<span>',{style:"max-width:14px; padding: 0 3px; margin-top:-4px; margin-left: 1px"}).appendTo(input); | ||||
|                                     $("<i>",{class: v.icon}).appendTo(s); | ||||
|                                 } | ||||
|                             }) | ||||
|                         } else { | ||||
|                             $('<span class="red-ui-editor-subflow-env-input-type-placeholder"></span>').text(RED._("editor.selectType")).appendTo(input); | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     value: "cred", | ||||
|                     label: RED._("typedInput.type.cred"), icon:"fa fa-lock", showLabel: false, | ||||
|                     valueLabel: function(container,value) { | ||||
|                         container.css("padding",0); | ||||
|                         var innerContainer = $('<div class="red-ui-editor-subflow-env-input-type">').css({ | ||||
|                             "border-top-right-radius": "4px", | ||||
|                             "border-bottom-right-radius": "4px" | ||||
|                         }).appendTo(container); | ||||
|                         $('<div class="placeholder-input">').html("••••••••").appendTo(innerContainer); | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     value:"select", | ||||
|                     label:RED._("editor.inputs.select"), icon:"fa fa-tasks",showLabel:false, | ||||
|                     valueLabel: function(container,value) { | ||||
|                         container.css("padding","0"); | ||||
|  | ||||
|                         selectBox = $('<select></select>').appendTo(container); | ||||
|                         if (ui.opts && Array.isArray(ui.opts.opts)) { | ||||
|                             ui.opts.opts.forEach(function(o) { | ||||
|                                 var label = lookupLabel(o.l, o.l["en-US"]||o.v, currentLocale); | ||||
|                                 // $('<option>').val((o.t||'str')+":"+o.v).text(label).appendTo(selectBox); | ||||
|                                 $('<option>').val(o.v).text(label).appendTo(selectBox); | ||||
|                             }) | ||||
|                         } | ||||
|                         selectBox.on('change', function(evt) { | ||||
|                             var v = selectBox.val(); | ||||
|                             // var parts = v.split(":"); | ||||
|                             // var t = parts.shift(); | ||||
|                             // v = parts.join(":"); | ||||
|                             // | ||||
|                             // valueField.typedInput("type",'str') | ||||
|                             valueField.typedInput("value",v) | ||||
|                         }); | ||||
|                         selectBox.val(valueField.typedInput("value")); | ||||
|                         // selectBox.val(valueField.typedInput('type')+":"+valueField.typedInput("value")); | ||||
|                     }, | ||||
|                     expand: { | ||||
|                         icon: "fa-caret-down", | ||||
|                         minWidth: 400, | ||||
|                         content: function(container) { | ||||
|                             var content = $('<div class="red-ui-editor-subflow-ui-edit-panel">').appendTo(container); | ||||
|                             var optList = $('<ol>').appendTo(content).editableList({ | ||||
|                                 header:$("<div><div>"+RED._("editor.select.label")+"</div><div>"+RED._("editor.select.value")+"</div></div>"), | ||||
|                                 addItem: function(row,index,itemData) { | ||||
|                                     var labelDiv = $('<div>').appendTo(row); | ||||
|                                     var label = lookupLabel(itemData.l, "", currentLocale); | ||||
|                                     itemData.label = $('<input type="text">').val(label).appendTo(labelDiv); | ||||
|                                     itemData.label.on('keydown', function(evt) { | ||||
|                                         if (evt.keyCode === 13) { | ||||
|                                             itemData.input.focus(); | ||||
|                                             evt.preventDefault(); | ||||
|                                         } | ||||
|                                     }); | ||||
|                                     var labelIcon = $('<span class="red-ui-editor-subflow-env-lang-icon"><i class="fa fa-language"></i></span>').appendTo(labelDiv); | ||||
|                                     RED.popover.tooltip(labelIcon,function() { | ||||
|                                         return currentLocale; | ||||
|                                     }) | ||||
|                                     itemData.input = $('<input type="text">').val(itemData.v).appendTo(row); | ||||
|  | ||||
|                                     // Problem using a TI here: | ||||
|                                     //  - this is in a popout panel | ||||
|                                     //  - clicking the expand button in the TI will close the parent edit tray | ||||
|                                     //    and open the type editor. | ||||
|                                     //  - but it leaves the popout panel over the top. | ||||
|                                     //  - there is no way to get back to the popout panel after closing the type editor | ||||
|                                     //.typedInput({default:itemData.t||'str', types:DEFAULT_ENV_TYPE_LIST}); | ||||
|                                     itemData.input.on('keydown', function(evt) { | ||||
|                                         if (evt.keyCode === 13) { | ||||
|                                             // Enter or Tab | ||||
|                                             var index = optList.editableList('indexOf',itemData); | ||||
|                                             var length = optList.editableList('length'); | ||||
|                                             if (index + 1 === length) { | ||||
|                                                 var newItem = {}; | ||||
|                                                 optList.editableList('addItem',newItem); | ||||
|                                                 setTimeout(function() { | ||||
|                                                     if (newItem.label) { | ||||
|                                                         newItem.label.focus(); | ||||
|                                                     } | ||||
|                                                 },100) | ||||
|                                             } else { | ||||
|                                                 var nextItem = optList.editableList('getItemAt',index+1); | ||||
|                                                 if (nextItem.label) { | ||||
|                                                     nextItem.label.focus() | ||||
|                                                 } | ||||
|                                             } | ||||
|                                             evt.preventDefault(); | ||||
|                                         } | ||||
|                                     }); | ||||
|                                 }, | ||||
|                                 sortable: true, | ||||
|                                 removable: true, | ||||
|                                 height: 160 | ||||
|                             }) | ||||
|                             if (ui.opts.opts.length > 0) { | ||||
|                                 ui.opts.opts.forEach(function(o) { | ||||
|                                     optList.editableList('addItem',$.extend(true,{},o)) | ||||
|                                 }) | ||||
|                             } else { | ||||
|                                 optList.editableList('addItem',{}) | ||||
|                             } | ||||
|                             return { | ||||
|                                 onclose: function() { | ||||
|                                     var items = optList.editableList('items'); | ||||
|                                     var vals = []; | ||||
|                                     items.each(function (i,el) { | ||||
|                                         var data = el.data('data'); | ||||
|                                         var l = data.label.val().trim(); | ||||
|                                         var v = data.input.val(); | ||||
|                                         // var t = data.input.typedInput('type'); | ||||
|                                         // var v = data.input.typedInput('value'); | ||||
|                                         if (l.length > 0) { | ||||
|                                             data.l = data.l || {}; | ||||
|                                             data.l[currentLocale] = l; | ||||
|                                         } | ||||
|                                         data.v = v; | ||||
|  | ||||
|                                         if (l.length > 0 || v.length > 0) { | ||||
|                                             var val = {l:data.l,v:data.v}; | ||||
|                                             // if (t !== 'str') { | ||||
|                                             //     val.t = t; | ||||
|                                             // } | ||||
|                                             vals.push(val); | ||||
|                                         } | ||||
|                                     }); | ||||
|                                     ui.opts.opts = vals; | ||||
|                                     inputCellInput.typedInput('value',Date.now()) | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     value:"checkbox", | ||||
|                     label:RED._("editor.inputs.checkbox"), icon:"fa fa-check-square-o",showLabel:false, | ||||
|                     valueLabel: function(container,value) { | ||||
|                         container.css("padding",0); | ||||
|                         checkbox = $('<input type="checkbox">').appendTo(container); | ||||
|                         checkbox.on('change', function(evt) { | ||||
|                             valueField.typedInput('value',$(this).prop('checked')?"true":"false"); | ||||
|                         }) | ||||
|                         checkbox.prop('checked',valueField.typedInput('value')==="true"); | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     value:"spinner", | ||||
|                     label:RED._("editor.inputs.spinner"), icon:"fa fa-sort-numeric-asc", showLabel:false, | ||||
|                     valueLabel: function(container,value) { | ||||
|                         container.css("padding",0); | ||||
|                         var innerContainer = $('<div class="red-ui-editor-subflow-env-input-type"></div>').appendTo(container); | ||||
|  | ||||
|                         var input = $('<div class="placeholder-input">').appendTo(innerContainer); | ||||
|                         $('<span><i class="fa fa-sort-numeric-asc"></i></span>').appendTo(input); | ||||
|  | ||||
|                         var min = ui.opts && ui.opts.min; | ||||
|                         var max = ui.opts && ui.opts.max; | ||||
|                         var label = ""; | ||||
|                         if (min !== undefined && max !== undefined) { | ||||
|                             label = Math.min(min,max)+" - "+Math.max(min,max); | ||||
|                         } else if (min !== undefined) { | ||||
|                             label = "> "+min; | ||||
|                         } else if (max !== undefined) { | ||||
|                             label = "< "+max; | ||||
|                         } | ||||
|                         $('<span>').css("margin-left","15px").text(label).appendTo(input); | ||||
|                     }, | ||||
|                     expand: { | ||||
|                         icon: "fa-caret-down", | ||||
|                         content: function(container) { | ||||
|                             var content = $('<div class="red-ui-editor-subflow-ui-edit-panel">').appendTo(container); | ||||
|                             content.css("padding","8px 5px") | ||||
|                             var min = ui.opts.min; | ||||
|                             var max = ui.opts.max; | ||||
|                             var minInput = $('<input type="number" style="margin-bottom:0; width:60px">'); | ||||
|                             minInput.val(min); | ||||
|                             var maxInput = $('<input type="number" style="margin-bottom:0; width:60px">'); | ||||
|                             maxInput.val(max); | ||||
|                             $('<div class="form-row" style="margin-bottom:3px"><label>'+RED._("editor.spinner.min")+'</label></div>').append(minInput).appendTo(content); | ||||
|                             $('<div class="form-row" style="margin-bottom:0"><label>'+RED._("editor.spinner.max")+'</label></div>').append(maxInput).appendTo(content); | ||||
|                             return { | ||||
|                                 onclose: function() { | ||||
|                                     var min = minInput.val().trim(); | ||||
|                                     var max = maxInput.val().trim(); | ||||
|                                     if (min !== "") { | ||||
|                                         ui.opts.min = parseInt(min); | ||||
|                                     } else { | ||||
|                                         delete ui.opts.min; | ||||
|                                     } | ||||
|                                     if (max !== "") { | ||||
|                                         ui.opts.max = parseInt(max); | ||||
|                                     } else { | ||||
|                                         delete ui.opts.max; | ||||
|                                     } | ||||
|                                     inputCellInput.typedInput('value',Date.now()) | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     value:"none", | ||||
|                     label:RED._("editor.inputs.none"), icon:"fa fa-times",hasValue:false | ||||
|                 }, | ||||
|                 { | ||||
|                     value:"hide", | ||||
|                     label:RED._("editor.inputs.hidden"), icon:"fa fa-ban",hasValue:false | ||||
|                 } | ||||
|             ], | ||||
|             default: 'none' | ||||
|         }).on("typedinputtypechange", function(evt,type) { | ||||
|             ui.type = $(this).typedInput("type"); | ||||
|             ui.opts = typeOptions[ui.type]; | ||||
|             if (ui.type === 'input') { | ||||
|                 // In the case of 'input' type, the typedInput uses the multiple-option | ||||
|                 // mode. Its value needs to be set to a comma-separately list of the | ||||
|                 // selected options. | ||||
|                 inputCellInput.typedInput('value',ui.opts.types.join(",")) | ||||
|             } else { | ||||
|                 // No other type cares about `value`, but doing this will | ||||
|                 // force a refresh of the label now that `ui.opts` has | ||||
|                 // been updated. | ||||
|                 inputCellInput.typedInput('value',Date.now()) | ||||
|             } | ||||
|  | ||||
|             switch (ui.type) { | ||||
|                 case 'input': | ||||
|                     valueField.typedInput('types',ui.opts.types); | ||||
|                     break; | ||||
|                 case 'select': | ||||
|                     valueField.typedInput('types',['str']); | ||||
|                     break; | ||||
|                 case 'checkbox': | ||||
|                     valueField.typedInput('types',['bool']); | ||||
|                     break; | ||||
|                 case 'spinner': | ||||
|                     valueField.typedInput('types',['num']); | ||||
|                     break; | ||||
|                 case 'cred': | ||||
|                     valueField.typedInput('types',['cred']); | ||||
|                     break; | ||||
|                 default: | ||||
|                     valueField.typedInput('types',DEFAULT_ENV_TYPE_LIST) | ||||
|             } | ||||
|             if (ui.type === 'checkbox') { | ||||
|                 valueField.typedInput('type','bool'); | ||||
|             } else if (ui.type === 'spinner') { | ||||
|                 valueField.typedInput('type','num'); | ||||
|             } | ||||
|             if (ui.type !== 'checkbox') { | ||||
|                 checkbox = null; | ||||
|             } | ||||
|  | ||||
|         }).on("change", function(evt,type) { | ||||
|             if (ui.type === 'input') { | ||||
|                 var types = inputCellInput.typedInput('value'); | ||||
|                 ui.opts.types = (types === "") ? ["str"] : types.split(","); | ||||
|                 valueField.typedInput('types',ui.opts.types); | ||||
|             } | ||||
|         }); | ||||
|         valueField.on("change", function(evt) { | ||||
|             if (checkbox) { | ||||
|                 checkbox.prop('checked',$(this).typedInput('value')==="true") | ||||
|             } | ||||
|         }) | ||||
|         // Set the input to the right type. This will trigger the 'typedinputtypechange' | ||||
|         // event handler (just above ^^) to update the value if needed | ||||
|         inputCellInput.typedInput('type',ui.type) | ||||
|     } | ||||
|  | ||||
|     function setLocale(l, list) { | ||||
|         currentLocale = l; | ||||
|         if (list) { | ||||
|             var items = list.editableList("items"); | ||||
|             items.each(function (i, item) { | ||||
|                 var entry = $(this).data('data'); | ||||
|                 var labelField = entry.ui.labelField; | ||||
|                 labelField.val(lookupLabel(entry.ui.label, "", currentLocale)); | ||||
|                 if (labelField.timeout) { | ||||
|                     clearTimeout(labelField.timeout); | ||||
|                     delete labelField.timeout; | ||||
|                 } | ||||
|                 labelField.addClass("input-updated"); | ||||
|                 labelField.timeout = setTimeout(function() { | ||||
|                     delete labelField.timeout | ||||
|                     labelField.removeClass("input-updated"); | ||||
|                 },3000); | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Lookup text for specific locale | ||||
|      * @param labels - dict of labels | ||||
|      * @param defaultLabel - fallback label if not found | ||||
|      * @param locale - target locale | ||||
|      * @returns {string} text for specified locale | ||||
|      */ | ||||
|     function lookupLabel(labels, defaultLabel, locale) { | ||||
|         if (labels) { | ||||
|             if (labels[locale]) { | ||||
|                 return labels[locale]; | ||||
|             } | ||||
|             if (locale) { | ||||
|                 var lang = locale.substring(0, 2); | ||||
|                 if (labels[lang]) { | ||||
|                     return labels[lang]; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return defaultLabel; | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         create: buildPropertiesList, | ||||
|         setLocale: setLocale, | ||||
|         lookupLabel: lookupLabel, | ||||
|         DEFAULT_ENV_TYPE_LIST: DEFAULT_ENV_TYPE_LIST, | ||||
|         DEFAULT_ENV_TYPE_LIST_INC_CRED: DEFAULT_ENV_TYPE_LIST_INC_CRED | ||||
|     } | ||||
| })(); | ||||
							
								
								
									
										99
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/editors/iconPicker.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/editors/iconPicker.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| RED.editor.iconPicker = (function() { | ||||
|     function showIconPicker(container, backgroundColor, iconPath, faOnly, done) { | ||||
|         var picker = $('<div class="red-ui-icon-picker">'); | ||||
|         var searchDiv = $("<div>",{class:"red-ui-search-container"}).appendTo(picker); | ||||
|         searchInput = $('<input type="text">').attr("placeholder",RED._("editor.searchIcons")).appendTo(searchDiv).searchBox({ | ||||
|             delay: 50, | ||||
|             change: function() { | ||||
|                 var searchTerm = $(this).val().trim(); | ||||
|                 if (searchTerm === "") { | ||||
|                     iconList.find(".red-ui-icon-list-module").show(); | ||||
|                     iconList.find(".red-ui-icon-list-icon").show(); | ||||
|                 } else { | ||||
|                     iconList.find(".red-ui-icon-list-module").hide(); | ||||
|                     iconList.find(".red-ui-icon-list-icon").each(function(i,n) { | ||||
|                         if ($(n).data('icon').indexOf(searchTerm) === -1) { | ||||
|                             $(n).hide(); | ||||
|                         } else { | ||||
|                             $(n).show(); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         var row = $('<div>').appendTo(picker); | ||||
|         var iconList = $('<div class="red-ui-icon-list">').appendTo(picker); | ||||
|         var metaRow = $('<div class="red-ui-icon-meta"></div>').appendTo(picker); | ||||
|         var summary = $('<span>').appendTo(metaRow); | ||||
|         var resetButton = $('<button type="button" class="red-ui-button red-ui-button-small">'+RED._("editor.useDefault")+'</button>').appendTo(metaRow).on("click", function(e) { | ||||
|             e.preventDefault(); | ||||
|             iconPanel.hide(); | ||||
|             done(null); | ||||
|         }); | ||||
|         if (!backgroundColor && faOnly) { | ||||
|             iconList.addClass("red-ui-icon-list-dark"); | ||||
|         } | ||||
|         setTimeout(function() { | ||||
|             var iconSets = RED.nodes.getIconSets(); | ||||
|             Object.keys(iconSets).forEach(function(moduleName) { | ||||
|                 if (faOnly && (moduleName !== "font-awesome")) { | ||||
|                     return; | ||||
|                 } | ||||
|                 var icons = iconSets[moduleName]; | ||||
|                 if (icons.length > 0) { | ||||
|                     // selectIconModule.append($("<option></option>").val(moduleName).text(moduleName)); | ||||
|                     var header = $('<div class="red-ui-icon-list-module"></div>').text(moduleName).appendTo(iconList); | ||||
|                     $('<i class="fa fa-cube"></i>').prependTo(header); | ||||
|                     icons.forEach(function(icon) { | ||||
|                         var iconDiv = $('<div>',{class:"red-ui-icon-list-icon"}).appendTo(iconList); | ||||
|                         var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(iconDiv); | ||||
|                         var icon_url = RED.settings.apiRootUrl+"icons/"+moduleName+"/"+icon; | ||||
|                         iconDiv.data('icon',icon_url); | ||||
|                         if (backgroundColor) { | ||||
|                             nodeDiv.css({ | ||||
|                                 'backgroundColor': backgroundColor | ||||
|                             }); | ||||
|                             var borderColor = RED.utils.getDarkerColor(backgroundColor); | ||||
|                             if (borderColor !== backgroundColor) { | ||||
|                                 nodeDiv.css('border-color',borderColor) | ||||
|                             } | ||||
|  | ||||
|                         } | ||||
|                         var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv); | ||||
|                         RED.utils.createIconElement(icon_url, iconContainer, true); | ||||
|  | ||||
|                         if (iconPath.module === moduleName && iconPath.file === icon) { | ||||
|                             iconDiv.addClass("selected"); | ||||
|                         } | ||||
|                         iconDiv.on("mouseover", function() { | ||||
|                             summary.text(icon); | ||||
|                         }) | ||||
|                         iconDiv.on("mouseout", function() { | ||||
|                             summary.html(" "); | ||||
|                         }) | ||||
|                         iconDiv.on("click", function() { | ||||
|                             iconPanel.hide(); | ||||
|                             done(moduleName+"/"+icon); | ||||
|                         }) | ||||
|                     }) | ||||
|                 } | ||||
|             }); | ||||
|             setTimeout(function() { | ||||
|                 spinner.remove(); | ||||
|             },50); | ||||
|         },300); | ||||
|         var spinner = RED.utils.addSpinnerOverlay(iconList,true); | ||||
|         var iconPanel = RED.popover.panel(picker); | ||||
|         iconPanel.show({ | ||||
|             target: container | ||||
|         }) | ||||
|  | ||||
|  | ||||
|         picker.slideDown(100); | ||||
|         searchInput.trigger("focus"); | ||||
|     } | ||||
|     return { | ||||
|         show: showIconPicker | ||||
|     } | ||||
| })(); | ||||
							
								
								
									
										514
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/appearance.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										514
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/appearance.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,514 @@ | ||||
| ;(function() { | ||||
|  | ||||
|     RED.editor.registerEditPane("editor-tab-appearance", function(node) { | ||||
|         return { | ||||
|             label: RED._("editor-tab.appearance"), | ||||
|             name: RED._("editor-tab.appearance"), | ||||
|             iconClass: "fa fa-object-group", | ||||
|             create: function(container) { | ||||
|                 this.content = container; | ||||
|                 buildAppearanceForm(this.content,node); | ||||
|  | ||||
|                 if (node.type === 'subflow') { | ||||
|                     this.defaultIcon = "node-red/subflow.svg"; | ||||
|                 } else { | ||||
|                     var iconPath = RED.utils.getDefaultNodeIcon(node._def,node); | ||||
|                     this.defaultIcon = iconPath.module+"/"+iconPath.file; | ||||
|                     if (node.icon && node.icon !== this.defaultIcon) { | ||||
|                         this.isDefaultIcon = false; | ||||
|                     } else { | ||||
|                         this.isDefaultIcon = true; | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             resize: function(size) { | ||||
|  | ||||
|             }, | ||||
|             close: function() { | ||||
|  | ||||
|             }, | ||||
|             show: function() { | ||||
|                 refreshLabelForm(this.content, node); | ||||
|             }, | ||||
|             apply: function(editState) { | ||||
|                 if (updateLabels(node, editState.changes, editState.outputMap)) { | ||||
|                     editState.changed = true; | ||||
|                 } | ||||
|                 if (!node._def.defaults || !node._def.defaults.hasOwnProperty("icon")) { | ||||
|                     var icon = $("#red-ui-editor-node-icon").val()||"" | ||||
|                     if (!this.isDefaultIcon) { | ||||
|                         if (icon !== node.icon) { | ||||
|                             editState.changes.icon = node.icon; | ||||
|                             node.icon = icon; | ||||
|                             editState.changed = true; | ||||
|                         } | ||||
|                     } else { | ||||
|                         if (icon !== "" && icon !== this.defaultIcon) { | ||||
|                             editState.changes.icon = node.icon; | ||||
|                             node.icon = icon; | ||||
|                             editState.changed = true; | ||||
|                         } else { | ||||
|                             var iconPath = RED.utils.getDefaultNodeIcon(node._def, node); | ||||
|                             var currentDefaultIcon = iconPath.module+"/"+iconPath.file; | ||||
|                             if (this.defaultIcon !== currentDefaultIcon) { | ||||
|                                 editState.changes.icon = node.icon; | ||||
|                                 node.icon = currentDefaultIcon; | ||||
|                                 editState.changed = true; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 if (node.type === "subflow") { | ||||
|                     var newCategory = $("#subflow-appearance-input-category").val().trim(); | ||||
|                     if (newCategory === "_custom_") { | ||||
|                         newCategory = $("#subflow-appearance-input-custom-category").val().trim(); | ||||
|                         if (newCategory === "") { | ||||
|                             newCategory = node.category; | ||||
|                         } | ||||
|                     } | ||||
|                     if (newCategory === 'subflows') { | ||||
|                         newCategory = ''; | ||||
|                     } | ||||
|                     if (newCategory != node.category) { | ||||
|                         editState.changes['category'] = node.category; | ||||
|                         node.category = newCategory; | ||||
|                         editState.changed = true; | ||||
|                     } | ||||
|  | ||||
|                     var oldColor = node.color; | ||||
|                     var newColor =  $("#red-ui-editor-node-color").val(); | ||||
|                     if (oldColor !== newColor) { | ||||
|                         editState.changes.color = node.color; | ||||
|                         node.color = newColor; | ||||
|                         editState.changed = true; | ||||
|                         RED.utils.clearNodeColorCache(); | ||||
|                         if (node.type === "subflow") { | ||||
|                             var nodeDefinition = RED.nodes.getType( | ||||
|                                 "subflow:" + node.id | ||||
|                             ); | ||||
|                             nodeDefinition["color"] = newColor; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|  | ||||
|  | ||||
|                 } | ||||
|                 if (!$("#node-input-show-label").prop('checked')) { | ||||
|                     // Not checked - hide label | ||||
|                     if (!/^link (in|out)$/.test(node.type)) { | ||||
|                         // Not a link node - default state is true | ||||
|                         if (node.l !== false) { | ||||
|                             editState.changes.l = node.l | ||||
|                             editState.changed = true; | ||||
|                         } | ||||
|                         node.l = false; | ||||
|                     } else { | ||||
|                         // A link node - default state is false | ||||
|                         if (node.hasOwnProperty('l') && node.l) { | ||||
|                             editState.changes.l = node.l | ||||
|                             editState.changed = true; | ||||
|                         } | ||||
|                         delete node.l; | ||||
|                     } | ||||
|                 } else { | ||||
|                     // Checked - show label | ||||
|                     if (!/^link (in|out)$/.test(node.type)) { | ||||
|                         // Not a link node - default state is true | ||||
|                         if (node.hasOwnProperty('l') && !node.l) { | ||||
|                             editState.changes.l = node.l | ||||
|                             editState.changed = true; | ||||
|                         } | ||||
|                         delete node.l; | ||||
|                     } else { | ||||
|                         if (!node.l) { | ||||
|                             editState.changes.l = node.l | ||||
|                             editState.changed = true; | ||||
|                         } | ||||
|                         node.l = true; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     function buildAppearanceForm(container,node) { | ||||
|         var dialogForm = $('<form class="dialog-form form-horizontal" autocomplete="off"></form>').appendTo(container); | ||||
|  | ||||
|         var i,row; | ||||
|  | ||||
|         if (node.type === "subflow") { | ||||
|             var categoryRow = $("<div/>", { | ||||
|                 class: "form-row" | ||||
|             }).appendTo(dialogForm); | ||||
|             $("<label/>", { | ||||
|                 for: "subflow-appearance-input-category", | ||||
|                 "data-i18n": "editor:subflow.category" | ||||
|             }).appendTo(categoryRow); | ||||
|             var categorySelector = $("<select/>", { | ||||
|                 id: "subflow-appearance-input-category" | ||||
|             }).css({ | ||||
|                 width: "250px" | ||||
|             }).appendTo(categoryRow); | ||||
|             $("<input/>", { | ||||
|                 type: "text", | ||||
|                 id: "subflow-appearance-input-custom-category" | ||||
|             }).css({ | ||||
|                 display: "none", | ||||
|                 "margin-left": "10px", | ||||
|                 width: "calc(100% - 250px)" | ||||
|             }).appendTo(categoryRow); | ||||
|  | ||||
|             var categories = RED.palette.getCategories(); | ||||
|             categories.sort(function(A,B) { | ||||
|                 return A.label.localeCompare(B.label); | ||||
|             }) | ||||
|             categories.forEach(function(cat) { | ||||
|                 categorySelector.append($("<option/>").val(cat.id).text(cat.label)); | ||||
|             }) | ||||
|             categorySelector.append($("<option/>").attr('disabled',true).text("---")); | ||||
|             categorySelector.append($("<option/>").val("_custom_").text(RED._("palette.addCategory"))); | ||||
|  | ||||
|             $("#subflow-appearance-input-category").on("change", function() { | ||||
|                 var val = $(this).val(); | ||||
|                 if (val === "_custom_") { | ||||
|                     $("#subflow-appearance-input-category").width(120); | ||||
|                     $("#subflow-appearance-input-custom-category").show(); | ||||
|                 } else { | ||||
|                     $("#subflow-appearance-input-category").width(250); | ||||
|                     $("#subflow-appearance-input-custom-category").hide(); | ||||
|                 } | ||||
|             }) | ||||
|  | ||||
|             $("#subflow-appearance-input-category").val(node.category||"subflows"); | ||||
|             var userCount = 0; | ||||
|             var subflowType = "subflow:"+node.id; | ||||
|  | ||||
|             // RED.nodes.eachNode(function(n) { | ||||
|             //     if (n.type === subflowType) { | ||||
|             //         userCount++; | ||||
|             //     } | ||||
|             // }); | ||||
|             $("#red-ui-editor-subflow-user-count") | ||||
|                 .text(RED._("subflow.subflowInstances", {count:node.instances.length})).show(); | ||||
|         } | ||||
|  | ||||
|         $('<div class="form-row">'+ | ||||
|             '<label for="node-input-show-label-btn" data-i18n="editor.label"></label>'+ | ||||
|             '<span style="margin-right: 2px;"/>'+ | ||||
|             '<input type="checkbox" id="node-input-show-label"/>'+ | ||||
|         '</div>').appendTo(dialogForm); | ||||
|  | ||||
|         $("#node-input-show-label").toggleButton({ | ||||
|             enabledLabel: RED._("editor.show"), | ||||
|             disabledLabel: RED._("editor.hide") | ||||
|         }) | ||||
|  | ||||
|         if (!node.hasOwnProperty("l")) { | ||||
|             // Show label if type not link | ||||
|             node.l = !/^link (in|out)$/.test(node._def.type); | ||||
|         } | ||||
|         $("#node-input-show-label").prop("checked",node.l).trigger("change"); | ||||
|  | ||||
|         if (node.type === "subflow") { | ||||
|             // subflow template can select its color | ||||
|             var color = node.color ? node.color : "#DDAA99"; | ||||
|             var colorRow = $("<div/>", { | ||||
|                 class: "form-row" | ||||
|             }).appendTo(dialogForm); | ||||
|             $("<label/>").text(RED._("editor.color")).appendTo(colorRow); | ||||
|  | ||||
|             var recommendedColors = [ | ||||
|                 "#DDAA99", | ||||
|                 "#3FADB5", "#87A980", "#A6BBCF", | ||||
|                 "#AAAA66", "#C0C0C0", "#C0DEED", | ||||
|                 "#C7E9C0", "#D7D7A0", "#D8BFD8", | ||||
|                 "#DAC4B4", "#DEB887", "#DEBD5C", | ||||
|                 "#E2D96E", "#E6E0F8", "#E7E7AE", | ||||
|                 "#E9967A", "#F3B567", "#FDD0A2", | ||||
|                 "#FDF0C2", "#FFAAAA", "#FFCC66", | ||||
|                 "#FFF0F0", "#FFFFFF" | ||||
|             ] | ||||
|  | ||||
|             RED.editor.colorPicker.create({ | ||||
|                 id: "red-ui-editor-node-color", | ||||
|                 value: color, | ||||
|                 palette: recommendedColors, | ||||
|                 sortPalette: function (a, b) {return a.l - b.l;} | ||||
|             }).appendTo(colorRow); | ||||
|  | ||||
|             $("#red-ui-editor-node-color").on('change', function(ev) { | ||||
|                 // Horribly out of scope... | ||||
|                 var colour = $(this).val(); | ||||
|                 nodeDiv.css('backgroundColor',colour); | ||||
|                 var borderColor = RED.utils.getDarkerColor(colour); | ||||
|                 if (borderColor !== colour) { | ||||
|                     nodeDiv.css('border-color',borderColor) | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|  | ||||
|         // If a node has icon property in defaults, the icon of the node cannot be modified. (e.g, ui_button node in dashboard) | ||||
|         if ((!node._def.defaults || !node._def.defaults.hasOwnProperty("icon"))) { | ||||
|             var iconRow = $('<div class="form-row"></div>').appendTo(dialogForm); | ||||
|             $('<label data-i18n="editor.settingIcon">').appendTo(iconRow); | ||||
|  | ||||
|             var iconButton = $('<button type="button" class="red-ui-button red-ui-editor-node-appearance-button">').appendTo(iconRow); | ||||
|             $('<i class="fa fa-caret-down"></i>').appendTo(iconButton); | ||||
|             var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(iconButton); | ||||
|             var colour = RED.utils.getNodeColor(node.type, node._def); | ||||
|             var icon_url = RED.utils.getNodeIcon(node._def,node); | ||||
|             nodeDiv.css('backgroundColor',colour); | ||||
|             var borderColor = RED.utils.getDarkerColor(colour); | ||||
|             if (borderColor !== colour) { | ||||
|                 nodeDiv.css('border-color',borderColor) | ||||
|             } | ||||
|  | ||||
|             var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv); | ||||
|             RED.utils.createIconElement(icon_url, iconContainer, true); | ||||
|  | ||||
|             iconButton.on("click", function(e) { | ||||
|                 e.preventDefault(); | ||||
|                 var iconPath; | ||||
|                 var icon = $("#red-ui-editor-node-icon").val()||""; | ||||
|                 if (icon) { | ||||
|                     iconPath = RED.utils.separateIconPath(icon); | ||||
|                 } else { | ||||
|                     iconPath = RED.utils.getDefaultNodeIcon(node._def, node); | ||||
|                 } | ||||
|                 var backgroundColor = RED.utils.getNodeColor(node.type, node._def); | ||||
|                 if (node.type === "subflow") { | ||||
|                     backgroundColor = $("#red-ui-editor-node-color").val(); | ||||
|                 } | ||||
|                 RED.editor.iconPicker.show(iconButton,backgroundColor,iconPath,false,function(newIcon) { | ||||
|                     $("#red-ui-editor-node-icon").val(newIcon||""); | ||||
|                     var icon_url = RED.utils.getNodeIcon(node._def,{type:node.type,icon:newIcon}); | ||||
|                     RED.utils.createIconElement(icon_url, iconContainer, true); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             RED.popover.tooltip(iconButton, function() { | ||||
|                 return $("#red-ui-editor-node-icon").val() || RED._("editor.default"); | ||||
|             }) | ||||
|             $('<input type="hidden" id="red-ui-editor-node-icon">').val(node.icon).appendTo(iconRow); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         $('<div class="form-row"><span data-i18n="editor.portLabels"></span></div>').appendTo(dialogForm); | ||||
|  | ||||
|         var inputCount = node.inputs || node._def.inputs || 0; | ||||
|         var outputCount = node.outputs || node._def.outputs || 0; | ||||
|         if (node.type === 'subflow') { | ||||
|             inputCount = node.in.length; | ||||
|             outputCount = node.out.length; | ||||
|         } | ||||
|  | ||||
|         var inputLabels = node.inputLabels || []; | ||||
|         var outputLabels = node.outputLabels || []; | ||||
|  | ||||
|         var inputPlaceholder = node._def.inputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel"); | ||||
|         var outputPlaceholder = node._def.outputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel"); | ||||
|  | ||||
|         $('<div class="form-row"><span style="margin-left: 50px;" data-i18n="editor.labelInputs"></span><div id="red-ui-editor-node-label-form-inputs"></div></div>').appendTo(dialogForm); | ||||
|         var inputsDiv = $("#red-ui-editor-node-label-form-inputs"); | ||||
|         if (inputCount > 0) { | ||||
|             for (i=0;i<inputCount;i++) { | ||||
|                 buildLabelRow("input",i,inputLabels[i],inputPlaceholder).appendTo(inputsDiv); | ||||
|             } | ||||
|         } else { | ||||
|             buildLabelRow().appendTo(inputsDiv); | ||||
|         } | ||||
|         $('<div class="form-row"><span style="margin-left: 50px;" data-i18n="editor.labelOutputs"></span><div id="red-ui-editor-node-label-form-outputs"></div></div>').appendTo(dialogForm); | ||||
|         var outputsDiv = $("#red-ui-editor-node-label-form-outputs"); | ||||
|         if (outputCount > 0) { | ||||
|             for (i=0;i<outputCount;i++) { | ||||
|                 buildLabelRow("output",i,outputLabels[i],outputPlaceholder).appendTo(outputsDiv); | ||||
|             } | ||||
|         } else { | ||||
|             buildLabelRow().appendTo(outputsDiv); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function refreshLabelForm(container,node) { | ||||
|  | ||||
|         var inputPlaceholder = node._def.inputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel"); | ||||
|         var outputPlaceholder = node._def.outputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel"); | ||||
|  | ||||
|         var inputsDiv = $("#red-ui-editor-node-label-form-inputs"); | ||||
|         var outputsDiv = $("#red-ui-editor-node-label-form-outputs"); | ||||
|  | ||||
|         var inputCount; | ||||
|         var formInputs = $("#node-input-inputs").val(); | ||||
|         if (formInputs === undefined) { | ||||
|             if (node.type === 'subflow') { | ||||
|                 inputCount = node.in.length; | ||||
|             } else { | ||||
|                 inputCount = node.inputs || node._def.inputs || 0; | ||||
|             } | ||||
|         } else { | ||||
|             inputCount = Math.min(1,Math.max(0,parseInt(formInputs))); | ||||
|             if (isNaN(inputCount)) { | ||||
|                 inputCount = 0; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         var children = inputsDiv.children(); | ||||
|         var childCount = children.length; | ||||
|         if (childCount === 1 && $(children[0]).hasClass('red-ui-editor-node-label-form-none')) { | ||||
|             childCount--; | ||||
|         } | ||||
|  | ||||
|         if (childCount < inputCount) { | ||||
|             if (childCount === 0) { | ||||
|                 // remove the 'none' placeholder | ||||
|                 $(children[0]).remove(); | ||||
|             } | ||||
|             for (i = childCount;i<inputCount;i++) { | ||||
|                 buildLabelRow("input",i,"",inputPlaceholder).appendTo(inputsDiv); | ||||
|             } | ||||
|         } else if (childCount > inputCount) { | ||||
|             for (i=inputCount;i<childCount;i++) { | ||||
|                 $(children[i]).remove(); | ||||
|             } | ||||
|             if (inputCount === 0) { | ||||
|                 buildLabelRow().appendTo(inputsDiv); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         var outputCount; | ||||
|         var i; | ||||
|         var formOutputs = $("#node-input-outputs").val(); | ||||
|  | ||||
|         if (formOutputs === undefined) { | ||||
|             if (node.type === 'subflow') { | ||||
|                 outputCount = node.out.length; | ||||
|             } else { | ||||
|                 inputCount = node.outputs || node._def.outputs || 0; | ||||
|             } | ||||
|         } else if (isNaN(formOutputs)) { | ||||
|             var outputMap = JSON.parse(formOutputs); | ||||
|             var keys = Object.keys(outputMap); | ||||
|             children = outputsDiv.children(); | ||||
|             childCount = children.length; | ||||
|             if (childCount === 1 && $(children[0]).hasClass('red-ui-editor-node-label-form-none')) { | ||||
|                 childCount--; | ||||
|             } | ||||
|  | ||||
|             outputCount = 0; | ||||
|             var rows = []; | ||||
|             keys.forEach(function(p) { | ||||
|                 var row = $("#red-ui-editor-node-label-form-output-"+p).parent(); | ||||
|                 if (row.length === 0 && outputMap[p] !== -1) { | ||||
|                     if (childCount === 0) { | ||||
|                         $(children[0]).remove(); | ||||
|                         childCount = -1; | ||||
|                     } | ||||
|                     row = buildLabelRow("output",p,"",outputPlaceholder); | ||||
|                 } else { | ||||
|                     row.detach(); | ||||
|                 } | ||||
|                 if (outputMap[p] !== -1) { | ||||
|                     outputCount++; | ||||
|                     rows.push({i:parseInt(outputMap[p]),r:row}); | ||||
|                 } | ||||
|             }); | ||||
|             rows.sort(function(A,B) { | ||||
|                 return A.i-B.i; | ||||
|             }) | ||||
|             rows.forEach(function(r,i) { | ||||
|                 r.r.find("label").text((i+1)+"."); | ||||
|                 r.r.appendTo(outputsDiv); | ||||
|             }) | ||||
|             if (rows.length === 0) { | ||||
|                 buildLabelRow("output",i,"").appendTo(outputsDiv); | ||||
|             } else { | ||||
|  | ||||
|             } | ||||
|         } else { | ||||
|             outputCount = Math.max(0,parseInt(formOutputs)); | ||||
|         } | ||||
|         children = outputsDiv.children(); | ||||
|         childCount = children.length; | ||||
|         if (childCount === 1 && $(children[0]).hasClass('red-ui-editor-node-label-form-none')) { | ||||
|             childCount--; | ||||
|         } | ||||
|         if (childCount < outputCount) { | ||||
|             if (childCount === 0) { | ||||
|                 // remove the 'none' placeholder | ||||
|                 $(children[0]).remove(); | ||||
|             } | ||||
|             for (i = childCount;i<outputCount;i++) { | ||||
|                 buildLabelRow("output",i,"").appendTo(outputsDiv); | ||||
|             } | ||||
|         } else if (childCount > outputCount) { | ||||
|             for (i=outputCount;i<childCount;i++) { | ||||
|                 $(children[i]).remove(); | ||||
|             } | ||||
|             if (outputCount === 0) { | ||||
|                 buildLabelRow().appendTo(outputsDiv); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function buildLabelRow(type, index, value, placeHolder) { | ||||
|         var result = $('<div>',{class:"red-ui-editor-node-label-form-row"}); | ||||
|         if (type === undefined) { | ||||
|             $('<span>').text(RED._("editor.noDefaultLabel")).appendTo(result); | ||||
|             result.addClass("red-ui-editor-node-label-form-none"); | ||||
|         } else { | ||||
|             result.addClass(""); | ||||
|             var id = "red-ui-editor-node-label-form-"+type+"-"+index; | ||||
|             $('<label>',{for:id}).text((index+1)+".").appendTo(result); | ||||
|             var input = $('<input>',{type:"text",id:id, placeholder: placeHolder}).val(value).appendTo(result); | ||||
|             var clear = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-times"></i></button>').appendTo(result); | ||||
|             clear.on("click", function(evt) { | ||||
|                 evt.preventDefault(); | ||||
|                 input.val(""); | ||||
|             }) | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     function updateLabels(node, changes, outputMap) { | ||||
|         var inputLabels = $("#red-ui-editor-node-label-form-inputs").children().find("input"); | ||||
|         var outputLabels = $("#red-ui-editor-node-label-form-outputs").children().find("input"); | ||||
|  | ||||
|         var hasNonBlankLabel = false; | ||||
|         var changed = false; | ||||
|         var newValue = inputLabels.map(function() { | ||||
|             var v = $(this).val(); | ||||
|             hasNonBlankLabel = hasNonBlankLabel || v!== ""; | ||||
|             return v; | ||||
|         }).toArray().slice(0,node.inputs); | ||||
|         if ((node.inputLabels === undefined && hasNonBlankLabel) || | ||||
|             (node.inputLabels !== undefined && JSON.stringify(newValue) !== JSON.stringify(node.inputLabels))) { | ||||
|             changes.inputLabels = node.inputLabels; | ||||
|             node.inputLabels = newValue; | ||||
|             changed = true; | ||||
|         } | ||||
|         hasNonBlankLabel = false; | ||||
|         newValue = new Array(node.outputs); | ||||
|         outputLabels.each(function() { | ||||
|             var index = $(this).attr('id').substring("red-ui-editor-node-label-form-output-".length); | ||||
|             if (outputMap && outputMap.hasOwnProperty(index)) { | ||||
|                 index = parseInt(outputMap[index]); | ||||
|                 if (index === -1) { | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|             var v = $(this).val(); | ||||
|             hasNonBlankLabel = hasNonBlankLabel || v!== ""; | ||||
|             newValue[index] = v; | ||||
|         }); | ||||
|  | ||||
|         if ((node.outputLabels === undefined && hasNonBlankLabel) || | ||||
|             (node.outputLabels !== undefined && JSON.stringify(newValue) !== JSON.stringify(node.outputLabels))) { | ||||
|             changes.outputLabels = node.outputLabels; | ||||
|             node.outputLabels = newValue; | ||||
|             changed = true; | ||||
|         } | ||||
|         return changed; | ||||
|     } | ||||
|  | ||||
|  | ||||
| })(); | ||||
							
								
								
									
										70
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/description.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/description.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| ;(function() { | ||||
|  | ||||
|     RED.editor.registerEditPane("editor-tab-description", function(node) { | ||||
|         return { | ||||
|             label: RED._("editor-tab.description"), | ||||
|             name: RED._("editor-tab.description"), | ||||
|             iconClass: "fa fa-file-text-o", | ||||
|  | ||||
|             create: function(container) { | ||||
|                 this.editor = buildDescriptionForm(container,node); | ||||
|                 RED.e = this.editor; | ||||
|             }, | ||||
|             resize: function(size) { | ||||
|                 this.editor.resize(); | ||||
|             }, | ||||
|             close: function() { | ||||
|                 this.editor.destroy(); | ||||
|                 this.editor = null; | ||||
|             }, | ||||
|             show: function() { | ||||
|                 this.editor.focus(); | ||||
|             }, | ||||
|             apply: function(editState) { | ||||
|                 var oldInfo = node.info; | ||||
|                 var newInfo = this.editor.getValue(); | ||||
|                 if (!!oldInfo) { | ||||
|                     // Has existing info property | ||||
|                     if (newInfo.trim() === "") { | ||||
|                         // New value is blank - remove the property | ||||
|                         editState.changed = true; | ||||
|                         editState.changes.info = oldInfo; | ||||
|                         delete node.info; | ||||
|                     } else if (newInfo !== oldInfo) { | ||||
|                         // New value is different | ||||
|                         editState.changed = true; | ||||
|                         editState.changes.info = oldInfo; | ||||
|                         node.info = newInfo; | ||||
|                     } | ||||
|                 } else { | ||||
|                     // No existing info | ||||
|                     if (newInfo.trim() !== "") { | ||||
|                         // New value is not blank | ||||
|                         editState.changed = true; | ||||
|                         editState.changes.info = undefined; | ||||
|                         node.info = newInfo; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     function buildDescriptionForm(container,node) { | ||||
|         var dialogForm = $('<form class="dialog-form form-horizontal" autocomplete="off"></form>').appendTo(container); | ||||
|         var toolbarRow = $('<div></div>').appendTo(dialogForm); | ||||
|         var row = $('<div class="form-row node-text-editor-row" style="position:relative; padding-top: 4px; height: 100%"></div>').appendTo(dialogForm); | ||||
|         var editorId = "node-info-input-info-editor-"+Math.floor(1000*Math.random()); | ||||
|         $('<div style="height: 100%" class="node-text-editor" id="'+editorId+'" ></div>').appendTo(row); | ||||
|         var nodeInfoEditor = RED.editor.createEditor({ | ||||
|             id: editorId, | ||||
|             mode: 'ace/mode/markdown', | ||||
|             value: "" | ||||
|         }); | ||||
|         if (node.info) { | ||||
|             nodeInfoEditor.getSession().setValue(node.info, -1); | ||||
|         } | ||||
|         node.infoEditor = nodeInfoEditor; | ||||
|         return nodeInfoEditor; | ||||
|     } | ||||
|  | ||||
| })(); | ||||
							
								
								
									
										69
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/envVarProperties.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/envVarProperties.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| ;(function() { | ||||
|  | ||||
|     RED.editor.registerEditPane("editor-tab-envProperties", function(node) { | ||||
|         return { | ||||
|             label: RED._("editor-tab.envProperties"), | ||||
|             name: RED._("editor-tab.envProperties"), | ||||
|             iconClass: "fa fa-list", | ||||
|             create: function(container) { | ||||
|                 var form = $('<form class="dialog-form form-horizontal"></form>').appendTo(container); | ||||
|                 var listContainer = $('<div class="form-row node-input-env-container-row"></div>').appendTo(form); | ||||
|                 this.list = $('<ol></ol>').appendTo(listContainer); | ||||
|                 RED.editor.envVarList.create(this.list, node); | ||||
|             }, | ||||
|             resize: function(size) { | ||||
|                 this.list.editableList('height',size.height); | ||||
|             }, | ||||
|             close: function() { | ||||
|  | ||||
|             }, | ||||
|             apply: function(editState) { | ||||
|                 var old_env = node.env; | ||||
|                 var new_env = []; | ||||
|                 if (/^subflow:/.test(node.type)) { | ||||
|                     new_env = RED.subflow.exportSubflowInstanceEnv(node); | ||||
|                 } | ||||
|  | ||||
|                 // Get the values from the Properties table tab | ||||
|                 var items = this.list.editableList('items'); | ||||
|                 items.each(function (i,el) { | ||||
|                     var data = el.data('data'); | ||||
|                     var item; | ||||
|                     if (data.nameField && data.valueField) { | ||||
|                         item = { | ||||
|                             name: data.nameField.val(), | ||||
|                             value: data.valueField.typedInput("value"), | ||||
|                             type: data.valueField.typedInput("type") | ||||
|                         } | ||||
|                         if (item.name.trim() !== "") { | ||||
|                             new_env.push(item); | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|  | ||||
|                 if (new_env && new_env.length > 0) { | ||||
|                     new_env.forEach(function(prop) { | ||||
|                         if (prop.type === "cred") { | ||||
|                             node.credentials = node.credentials || {_:{}}; | ||||
|                             node.credentials[prop.name] = prop.value; | ||||
|                             node.credentials['has_'+prop.name] = (prop.value !== ""); | ||||
|                             if (prop.value !== '__PWRD__') { | ||||
|                                 editState.changed = true; | ||||
|                             } | ||||
|                             delete prop.value; | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|                 if (!isSameObj(old_env, new_env)) { | ||||
|                     node.env = new_env; | ||||
|                     editState.changes.env = node.env; | ||||
|                     editState.changed = true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|     function isSameObj(env0, env1) { | ||||
|         return (JSON.stringify(env0) === JSON.stringify(env1)); | ||||
|     } | ||||
| })(); | ||||
							
								
								
									
										60
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/flowProperties.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/flowProperties.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| ;(function() { | ||||
|  | ||||
|     RED.editor.registerEditPane("editor-tab-flow-properties", function(node) { | ||||
|         return { | ||||
|             label: RED._("editor-tab.properties"), | ||||
|             name: RED._("editor-tab.properties"), | ||||
|             iconClass: "fa fa-cog", | ||||
|             create: function(container) { | ||||
|                 var dialogForm = $('<form id="dialog-form" class="form-horizontal"></form>').appendTo(container); | ||||
|                 $('<div class="form-row">'+ | ||||
|                   '<label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label>'+ | ||||
|                   '<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">'+ | ||||
|                   '</div>').appendTo(dialogForm); | ||||
|  | ||||
|                 var row = $('<div class="form-row node-text-editor-row">'+ | ||||
|                             '<label for="node-input-info" data-i18n="editor:workspace.info" style="width:300px;"></label>'+ | ||||
|                             '<div style="min-height:150px;" class="node-text-editor" id="node-input-info"></div>'+ | ||||
|                             '</div>').appendTo(dialogForm); | ||||
|                 this.tabflowEditor = RED.editor.createEditor({ | ||||
|                     id: 'node-input-info', | ||||
|                     mode: 'ace/mode/markdown', | ||||
|                     value: "" | ||||
|                 }); | ||||
|  | ||||
|                 $('<input type="text" style="display: none;" />').prependTo(dialogForm); | ||||
|                 dialogForm.on("submit", function(e) { e.preventDefault();}); | ||||
|  | ||||
|                 $("#node-input-name").val(node.label); | ||||
|                 RED.text.bidi.prepareInput($("#node-input-name")); | ||||
|                 this.tabflowEditor.getSession().setValue(node.info || "", -1); | ||||
|             }, | ||||
|             resize: function(size) { | ||||
|                 $("#node-input-info").css("height", (size.height-70)+"px"); | ||||
|                 this.tabflowEditor.resize(); | ||||
|             }, | ||||
|             close: function() { | ||||
|                 this.tabflowEditor.destroy(); | ||||
|             }, | ||||
|             apply: function(editState) { | ||||
|                 var label = $( "#node-input-name" ).val(); | ||||
|  | ||||
|                 if (node.label != label) { | ||||
|                     editState.changes.label = node.label; | ||||
|                     editState.changed = true; | ||||
|                     node.label = label; | ||||
|                 } | ||||
|  | ||||
|                 var info = this.tabflowEditor.getValue(); | ||||
|                 if (node.info !== info) { | ||||
|                     editState.changes.info = node.info; | ||||
|                     editState.changed = true; | ||||
|                     node.info = info; | ||||
|                 } | ||||
|                 $("#red-ui-tab-"+(node.id.replace(".","-"))).toggleClass('red-ui-workspace-disabled',!!node.disabled); | ||||
|                 $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!node.disabled); | ||||
|  | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| })(); | ||||
							
								
								
									
										181
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | ||||
| ;(function() { | ||||
|  | ||||
|     RED.editor.registerEditPane("editor-tab-properties", function(node) { | ||||
|         return { | ||||
|             label: RED._("editor-tab.properties"), | ||||
|             name: RED._("editor-tab.properties"), | ||||
|             iconClass: "fa fa-cog", | ||||
|             create: function(container) { | ||||
|  | ||||
|                 var nodeType = node.type; | ||||
|                 if (node.type === "subflow") { | ||||
|                     nodeType = "subflow-template"; | ||||
|                 } else if (node.type.substring(0,8) == "subflow:") { | ||||
|                     nodeType = "subflow"; | ||||
|                 } | ||||
|  | ||||
|                 var i18nNamespace; | ||||
|                 if (node._def.set.module === "node-red") { | ||||
|                     i18nNamespace = "node-red"; | ||||
|                 } else { | ||||
|                     i18nNamespace = node._def.set.id; | ||||
|                 } | ||||
|  | ||||
|                 var formStyle = "dialog-form"; | ||||
|                 this.inputClass = "node-input"; | ||||
|                 if (node._def.category === "config" && nodeType !== "group") { | ||||
|                     this.inputClass = "node-config-input"; | ||||
|                     formStyle = "node-config-dialog-edit-form"; | ||||
|                 } | ||||
|                 RED.editor.buildEditForm(container,formStyle,nodeType,i18nNamespace,node); | ||||
|             }, | ||||
|             resize: function(size) { | ||||
|                 if (node && node._def.oneditresize) { | ||||
|                     try { | ||||
|                         node._def.oneditresize.call(node,size); | ||||
|                     } catch(err) { | ||||
|                         console.log("oneditresize",node.id,node.type,err.toString()); | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             close: function() { | ||||
|  | ||||
|             }, | ||||
|             apply: function(editState) { | ||||
|                 var newValue; | ||||
|                 var d; | ||||
|                 if (node._def.defaults) { | ||||
|                     for (d in node._def.defaults) { | ||||
|                         if (node._def.defaults.hasOwnProperty(d)) { | ||||
|                             var input = $("#"+this.inputClass+"-"+d); | ||||
|                             if (input.attr('type') === "checkbox") { | ||||
|                                 newValue = input.prop('checked'); | ||||
|                             } else if (input.prop("nodeName") === "select" && input.attr("multiple") === "multiple") { | ||||
|                                 // An empty select-multiple box returns null. | ||||
|                                 // Need to treat that as an empty array. | ||||
|                                 newValue = input.val(); | ||||
|                                 if (newValue == null) { | ||||
|                                     newValue = []; | ||||
|                                 } | ||||
|                             } else if ("format" in node._def.defaults[d] && node._def.defaults[d].format !== "" && input[0].nodeName === "DIV") { | ||||
|                                 newValue = input.text(); | ||||
|                             } else { | ||||
|                                 newValue = input.val(); | ||||
|                             } | ||||
|                             if (newValue != null) { | ||||
|                                 if (d === "outputs") { | ||||
|                                     if  (newValue.trim() === "") { | ||||
|                                         continue; | ||||
|                                     } | ||||
|                                     if (isNaN(newValue)) { | ||||
|                                         editState.outputMap = JSON.parse(newValue); | ||||
|                                         var outputCount = 0; | ||||
|                                         var outputsChanged = false; | ||||
|                                         var keys = Object.keys(editState.outputMap); | ||||
|                                         keys.forEach(function(p) { | ||||
|                                             if (isNaN(p)) { | ||||
|                                                 // New output; | ||||
|                                                 outputCount ++; | ||||
|                                                 delete editState.outputMap[p]; | ||||
|                                             } else { | ||||
|                                                 editState.outputMap[p] = editState.outputMap[p]+""; | ||||
|                                                 if (editState.outputMap[p] !== "-1") { | ||||
|                                                     outputCount++; | ||||
|                                                     if (editState.outputMap[p] !== p) { | ||||
|                                                         // Output moved | ||||
|                                                         outputsChanged = true; | ||||
|                                                     } else { | ||||
|                                                         delete editState.outputMap[p]; | ||||
|                                                     } | ||||
|                                                 } else { | ||||
|                                                     // Output removed | ||||
|                                                     outputsChanged = true; | ||||
|                                                 } | ||||
|                                             } | ||||
|                                         }); | ||||
|  | ||||
|                                         newValue = outputCount; | ||||
|                                         if (outputsChanged) { | ||||
|                                             editState.changed = true; | ||||
|                                         } | ||||
|                                     } else { | ||||
|                                         newValue = parseInt(newValue); | ||||
|                                     } | ||||
|                                 } | ||||
|                                 if (node._def.defaults[d].type) { | ||||
|                                     if (newValue == "_ADD_") { | ||||
|                                         newValue = ""; | ||||
|                                     } | ||||
|                                 } | ||||
|                                 if (node[d] != newValue) { | ||||
|                                     if (node._def.defaults[d].type) { | ||||
|                                         // Change to a related config node | ||||
|                                         var configNode = RED.nodes.node(node[d]); | ||||
|                                         if (configNode) { | ||||
|                                             var users = configNode.users; | ||||
|                                             users.splice(users.indexOf(node),1); | ||||
|                                             RED.events.emit("nodes:change",configNode); | ||||
|                                         } | ||||
|                                         configNode = RED.nodes.node(newValue); | ||||
|                                         if (configNode) { | ||||
|                                             configNode.users.push(node); | ||||
|                                             RED.events.emit("nodes:change",configNode); | ||||
|                                         } | ||||
|                                     } | ||||
|                                     editState.changes[d] = node[d]; | ||||
|                                     node[d] = newValue; | ||||
|                                     editState.changed = true; | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 if (node._def.credentials) { | ||||
|                     var credDefinition = node._def.credentials; | ||||
|                     var credsChanged = updateNodeCredentials(node,credDefinition,this.inputClass); | ||||
|                     editState.changed = editState.changed || credsChanged; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     /** | ||||
|      * Update the node credentials from the edit form | ||||
|      * @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 | ||||
|      */ | ||||
|     function updateNodeCredentials(node, credDefinition, prefix) { | ||||
|         var changed = false; | ||||
|         if (!node.credentials) { | ||||
|             node.credentials = {_:{}}; | ||||
|         } else if (!node.credentials._) { | ||||
|             node.credentials._ = {}; | ||||
|         } | ||||
|  | ||||
|         for (var cred in credDefinition) { | ||||
|             if (credDefinition.hasOwnProperty(cred)) { | ||||
|                 var input = $("#" + prefix + '-' + cred); | ||||
|                 if (input.length > 0) { | ||||
|                     var value = input.val(); | ||||
|                     if (credDefinition[cred].type == 'password') { | ||||
|                         node.credentials['has_' + cred] = (value !== ""); | ||||
|                         if (value == '__PWRD__') { | ||||
|                             continue; | ||||
|                         } | ||||
|                         changed = true; | ||||
|  | ||||
|                     } | ||||
|                     node.credentials[cred] = value; | ||||
|                     if (value != node.credentials._[cred]) { | ||||
|                         changed = true; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return changed; | ||||
|     } | ||||
|  | ||||
|  | ||||
| })(); | ||||
							
								
								
									
										179
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/subflowModule.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/subflowModule.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,179 @@ | ||||
| (function() { | ||||
|     var _subflowModulePaneTemplate = '<form class="dialog-form form-horizontal" autocomplete="off">'+ | ||||
|         '<div class="form-row">'+ | ||||
|             '<label for="subflow-input-module-module" data-i18n="[append]editor:subflow.module"><i class="fa fa-cube"></i> </label>'+ | ||||
|             '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-module" data-i18n="[placeholder]common.label.name">'+ | ||||
|         '</div>'+ | ||||
|         '<div class="form-row">'+ | ||||
|             '<label for="subflow-input-module-type" data-i18n="[append]editor:subflow.type"> </label>'+ | ||||
|             '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-type">'+ | ||||
|         '</div>'+ | ||||
|         '<div class="form-row">'+ | ||||
|             '<label for="subflow-input-module-version" data-i18n="[append]editor:subflow.version"></label>'+ | ||||
|             '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-version" data-i18n="[placeholder]editor:subflow.versionPlaceholder">'+ | ||||
|         '</div>'+ | ||||
|         '<div class="form-row">'+ | ||||
|             '<label for="subflow-input-module-desc" data-i18n="[append]editor:subflow.desc"></label>'+ | ||||
|             '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-desc">'+ | ||||
|         '</div>'+ | ||||
|         '<div class="form-row">'+ | ||||
|             '<label for="subflow-input-module-license" data-i18n="[append]editor:subflow.license"></label>'+ | ||||
|             '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-license">'+ | ||||
|         '</div>'+ | ||||
|         '<div class="form-row">'+ | ||||
|             '<label for="subflow-input-module-author" data-i18n="[append]editor:subflow.author"></label>'+ | ||||
|             '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-author" data-i18n="[placeholder]editor:subflow.authorPlaceholder">'+ | ||||
|         '</div>'+ | ||||
|         '<div class="form-row">'+ | ||||
|             '<label for="subflow-input-module-keywords" data-i18n="[append]editor:subflow.keys"></label>'+ | ||||
|             '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-keywords" data-i18n="[placeholder]editor:subflow.keysPlaceholder">'+ | ||||
|         '</div>'+ | ||||
|     '</form>'; | ||||
|  | ||||
|     RED.editor.registerEditPane("editor-tab-subflow-module", function(node) { | ||||
|         return { | ||||
|             label: RED._("editor-tab.module"), | ||||
|             name: RED._("editor-tab.module"), | ||||
|             iconClass: "fa fa-cube", | ||||
|             create: function(container) { | ||||
|                 buildModuleForm(container, node); | ||||
|             }, | ||||
|             resize: function(size) { | ||||
|             }, | ||||
|             close: function() { | ||||
|  | ||||
|             }, | ||||
|             apply: function(editState) { | ||||
|                 var newMeta = exportSubflowModuleProperties(node); | ||||
|                 if (!isSameObj(node.meta,newMeta)) { | ||||
|                     editState.changes.meta = node.meta; | ||||
|                     node.meta = newMeta; | ||||
|                     editState.changed = true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     function isSameObj(env0, env1) { | ||||
|         return (JSON.stringify(env0) === JSON.stringify(env1)); | ||||
|     } | ||||
|  | ||||
|     function setupInputValidation(input,validator) { | ||||
|         var errorTip; | ||||
|         var validateTimeout; | ||||
|  | ||||
|         var validateFunction = function() { | ||||
|             if (validateTimeout) { | ||||
|                 return; | ||||
|             } | ||||
|             validateTimeout = setTimeout(function() { | ||||
|                 var error = validator(input.val()); | ||||
|                 // if (!error && errorTip) { | ||||
|                 //     errorTip.close(); | ||||
|                 //     errorTip = null; | ||||
|                 // } else if (error && !errorTip) { | ||||
|                 //     errorTip = RED.popover.create({ | ||||
|                 //         tooltip: true, | ||||
|                 //         target:input, | ||||
|                 //         size: "small", | ||||
|                 //         direction: "bottom", | ||||
|                 //         content: error, | ||||
|                 //     }).open(); | ||||
|                 // } | ||||
|                 input.toggleClass("input-error",!!error); | ||||
|                 validateTimeout = null; | ||||
|             }) | ||||
|         } | ||||
|         input.on("change keyup paste", validateFunction); | ||||
|     } | ||||
|  | ||||
|     function buildModuleForm(container, node) { | ||||
|         $(_subflowModulePaneTemplate).appendTo(container); | ||||
|         var moduleProps = node.meta || {}; | ||||
|         [ | ||||
|             'module', | ||||
|             'type', | ||||
|             'version', | ||||
|             'author', | ||||
|             'desc', | ||||
|             'keywords', | ||||
|             'license' | ||||
|         ].forEach(function(property) { | ||||
|             $("#subflow-input-module-"+property).val(moduleProps[property]||"") | ||||
|         }) | ||||
|         $("#subflow-input-module-type").attr("placeholder",node.id); | ||||
|  | ||||
|         setupInputValidation($("#subflow-input-module-module"), function(newValue) { | ||||
|             newValue = newValue.trim(); | ||||
|             var isValid = newValue.length < 215; | ||||
|             isValid = isValid && !/^[._]/.test(newValue); | ||||
|             isValid = isValid && !/[A-Z]/.test(newValue); | ||||
|             if (newValue !== encodeURIComponent(newValue)) { | ||||
|                 var m = /^@([^\/]+)\/([^\/]+)$/.exec(newValue); | ||||
|                 if (m) { | ||||
|                     isValid = isValid && (m[1] === encodeURIComponent(m[1]) && m[2] === encodeURIComponent(m[2])) | ||||
|                 } else { | ||||
|                     isValid = false; | ||||
|                 } | ||||
|             } | ||||
|             return isValid?"":"Invalid module name" | ||||
|         }) | ||||
|         setupInputValidation($("#subflow-input-module-version"), function(newValue) { | ||||
|             newValue = newValue.trim(); | ||||
|             var isValid = newValue === "" || | ||||
|                           /^(\d|[1-9]\d*)\.(\d|[1-9]\d*)\.(\d|[1-9]\d*)(-(0|[1-9A-Za-z-][0-9A-Za-z-]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9A-Za-z-][0-9A-Za-z-]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/.test(newValue); | ||||
|             return isValid?"":"Invalid version number" | ||||
|         }) | ||||
|  | ||||
|         var licenses = ["none", "Apache-2.0", "BSD-3-Clause", "BSD-2-Clause", "GPL-2.0", "GPL-3.0", "MIT", "MPL-2.0", "CDDL-1.0", "EPL-2.0"]; | ||||
|         var typedLicenses = { | ||||
|             types: licenses.map(function(l) { | ||||
|                 return { | ||||
|                     value: l, | ||||
|                     label: l === "none" ? RED._("editor:subflow.licenseNone") : l, | ||||
|                     hasValue: false | ||||
|                 }; | ||||
|             }) | ||||
|         } | ||||
|         typedLicenses.types.push({ | ||||
|             value:"_custom_", label:RED._("editor:subflow.licenseOther"), icon:"red/images/typedInput/az.svg" | ||||
|         }) | ||||
|         if (!moduleProps.license) { | ||||
|             typedLicenses.default = "none"; | ||||
|         } else if (licenses.indexOf(moduleProps.license) > -1) { | ||||
|             typedLicenses.default = moduleProps.license; | ||||
|         } else { | ||||
|             typedLicenses.default = "_custom_"; | ||||
|         } | ||||
|         $("#subflow-input-module-license").typedInput(typedLicenses) | ||||
|     } | ||||
|     function exportSubflowModuleProperties(node) { | ||||
|         var value; | ||||
|         var moduleProps = {}; | ||||
|         [ | ||||
|             'module', | ||||
|             'type', | ||||
|             'version', | ||||
|             'author', | ||||
|             'desc', | ||||
|             'keywords' | ||||
|         ].forEach(function(property) { | ||||
|             value = $("#subflow-input-module-"+property).val().trim(); | ||||
|             if (value) { | ||||
|                 moduleProps[property] = value; | ||||
|             } | ||||
|         }) | ||||
|         var selectedLicenseType = $("#subflow-input-module-license").typedInput("type"); | ||||
|  | ||||
|         if (selectedLicenseType === '_custom_') { | ||||
|             value = $("#subflow-input-module-license").val(); | ||||
|             if (value) { | ||||
|                 moduleProps.license = value; | ||||
|             } | ||||
|         } else if (selectedLicenseType !== "none") { | ||||
|             moduleProps.license = selectedLicenseType; | ||||
|         } | ||||
|         return moduleProps; | ||||
|     } | ||||
|  | ||||
| })(); | ||||
| @@ -87,16 +87,18 @@ RED.group = (function() { | ||||
|         "label-position": "nw" | ||||
|     }; | ||||
|  | ||||
|  | ||||
|     var groupDef = { | ||||
|         defaults:{ | ||||
|             name:{value:""}, | ||||
|             style:{value:{label:true}}, | ||||
|             nodes:{value:[]} | ||||
|             nodes:{value:[]}, | ||||
|             env: {value:[]}, | ||||
|         }, | ||||
|         category: "config", | ||||
|         oneditprepare: function() { | ||||
|             var style = this.style || {}; | ||||
|             RED.colorPicker.create({ | ||||
|             RED.editor.colorPicker.create({ | ||||
|                 id:"node-input-style-stroke", | ||||
|                 value: style.stroke || defaultGroupStyle.stroke || "#a4a4a4", | ||||
|                 palette: colorPalette, | ||||
| @@ -107,7 +109,7 @@ RED.group = (function() { | ||||
|                 none: true, | ||||
|                 opacity: style.hasOwnProperty('stroke-opacity')?style['stroke-opacity']:(defaultGroupStyle.hasOwnProperty('stroke-opacity')?defaultGroupStyle['stroke-opacity']:1.0) | ||||
|             }).appendTo("#node-input-row-style-stroke"); | ||||
|             RED.colorPicker.create({ | ||||
|             RED.editor.colorPicker.create({ | ||||
|                 id:"node-input-style-fill", | ||||
|                 value: style.fill || defaultGroupStyle.fill ||"none", | ||||
|                 palette: colorPalette, | ||||
| @@ -124,7 +126,7 @@ RED.group = (function() { | ||||
|                 value:style["label-position"] || "nw" | ||||
|             }).appendTo("#node-input-row-style-label-position"); | ||||
|  | ||||
|             RED.colorPicker.create({ | ||||
|             RED.editor.colorPicker.create({ | ||||
|                 id:"node-input-style-color", | ||||
|                 value: style.color || defaultGroupStyle.color ||"#a4a4a4", | ||||
|                 palette: colorPalette, | ||||
| @@ -144,7 +146,6 @@ RED.group = (function() { | ||||
|             }) | ||||
|             $("#node-input-style-label").prop("checked", this.style.label) | ||||
|             $("#node-input-style-label").trigger("change"); | ||||
|  | ||||
|         }, | ||||
|         oneditresize: function(size) { | ||||
|         }, | ||||
| @@ -183,7 +184,9 @@ RED.group = (function() { | ||||
|             var activateUngroup = false; | ||||
|             var activateMerge = false; | ||||
|             var activateRemove = false; | ||||
|             var singleGroupSelected = false; | ||||
|             if (activateGroup) { | ||||
|                 singleGroupSelected = selection.nodes.length === 1 && selection.nodes[0].type === 'group'; | ||||
|                 selection.nodes.forEach(function (n) { | ||||
|                     if (n.type === "group") { | ||||
|                         activateUngroup = true; | ||||
| @@ -200,6 +203,8 @@ RED.group = (function() { | ||||
|             RED.menu.setDisabled("menu-item-group-ungroup", !activateUngroup); | ||||
|             RED.menu.setDisabled("menu-item-group-merge", !activateMerge); | ||||
|             RED.menu.setDisabled("menu-item-group-remove", !activateRemove); | ||||
|             RED.menu.setDisabled("menu-item-edit-copy-group-style", !singleGroupSelected); | ||||
|             RED.menu.setDisabled("menu-item-edit-paste-group-style", !activateUngroup); | ||||
|         }); | ||||
|  | ||||
|         RED.actions.add("core:group-selection", function() { groupSelection() }) | ||||
| @@ -252,6 +257,7 @@ RED.group = (function() { | ||||
|         if (selection.nodes && selection.nodes.length === 1 && selection.nodes[0].type === 'group') { | ||||
|             groupStyleClipboard = JSON.parse(JSON.stringify(selection.nodes[0].style)); | ||||
|             RED.notify(RED._("clipboard.groupStyleCopied"),{id:"clipboard"}) | ||||
|             RED.menu.setDisabled("menu-item-edit-paste-group-style", false) | ||||
|         } | ||||
|     } | ||||
|     function pasteGroupStyle() { | ||||
|   | ||||
| @@ -131,7 +131,7 @@ RED.keyboard = (function() { | ||||
|         return mergedKeymap; | ||||
|     } | ||||
|  | ||||
|     function init() { | ||||
|     function init(done) { | ||||
|         // Migrate from pre-0.18 | ||||
|         migrateOldKeymap(); | ||||
|  | ||||
| @@ -164,6 +164,7 @@ RED.keyboard = (function() { | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             done(); | ||||
|         }); | ||||
|  | ||||
|         RED.userSettings.add({ | ||||
| @@ -174,6 +175,9 @@ RED.keyboard = (function() { | ||||
|                 setTimeout(function() { | ||||
|                     $("#red-ui-settings-tab-keyboard-filter").trigger("focus"); | ||||
|                 },200); | ||||
|             }, | ||||
|             close: function() { | ||||
|                 RED.menu.refreshShortcuts(); | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|   | ||||
| @@ -536,7 +536,7 @@ RED.library = (function() { | ||||
|     //             evt.preventDefault(); | ||||
|     //             var icon = libraryFields['icon'].input.val() || ""; | ||||
|     //             var iconPath = (icon ? RED.utils.separateIconPath(icon) : {}); | ||||
|     //             RED.editor.showIconPicker(iconButton, null, iconPath, true, function (newIcon) { | ||||
|     //             RED.editor.iconPicker.show(iconButton, null, iconPath, true, function (newIcon) { | ||||
|     //                 iconButton.empty(); | ||||
|     //                 var path = newIcon || ""; | ||||
|     //                 var newPath = RED.utils.separateIconPath(path); | ||||
|   | ||||
| @@ -16,8 +16,6 @@ | ||||
|  | ||||
| RED.subflow = (function() { | ||||
|  | ||||
|     var currentLocale = "en-US"; | ||||
|  | ||||
|     var _subflowEditTemplate = '<script type="text/x-red" data-template-name="subflow">'+ | ||||
|         '<div class="form-row">'+ | ||||
|             '<label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label>'+ | ||||
| @@ -37,7 +35,7 @@ RED.subflow = (function() { | ||||
|         '<div id="subflow-env-tabs-content">'+ | ||||
|             '<div id="subflow-env-tab-edit">'+ | ||||
|                 '<div class="form-row node-input-env-container-row" id="subflow-input-edit-ui">'+ | ||||
|                     '<ol class="red-ui-editor-subflow-env-list" id="node-input-env-container"></ol>'+ | ||||
|                     '<ol id="node-input-env-container"></ol>'+ | ||||
|                     '<div class="node-input-env-locales-row"><i class="fa fa-language"></i> <select id="subflow-input-env-locale"></select></div>'+ | ||||
|                 '</div>'+ | ||||
|             '</div>'+ | ||||
| @@ -47,37 +45,6 @@ RED.subflow = (function() { | ||||
|         '</div>'+ | ||||
|         '</script>'; | ||||
|  | ||||
|     var _subflowModulePaneTemplate = '<form class="dialog-form form-horizontal" autocomplete="off">'+ | ||||
|         '<div class="form-row">'+ | ||||
|             '<label for="subflow-input-module-module" data-i18n="[append]editor:subflow.module"><i class="fa fa-cube"></i> </label>'+ | ||||
|             '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-module" data-i18n="[placeholder]common.label.name">'+ | ||||
|         '</div>'+ | ||||
|         '<div class="form-row">'+ | ||||
|             '<label for="subflow-input-module-type" data-i18n="[append]editor:subflow.type"> </label>'+ | ||||
|             '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-type">'+ | ||||
|         '</div>'+ | ||||
|         '<div class="form-row">'+ | ||||
|             '<label for="subflow-input-module-version" data-i18n="[append]editor:subflow.version"></label>'+ | ||||
|             '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-version" data-i18n="[placeholder]editor:subflow.versionPlaceholder">'+ | ||||
|         '</div>'+ | ||||
|         '<div class="form-row">'+ | ||||
|             '<label for="subflow-input-module-desc" data-i18n="[append]editor:subflow.desc"></label>'+ | ||||
|             '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-desc">'+ | ||||
|         '</div>'+ | ||||
|         '<div class="form-row">'+ | ||||
|             '<label for="subflow-input-module-license" data-i18n="[append]editor:subflow.license"></label>'+ | ||||
|             '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-license">'+ | ||||
|         '</div>'+ | ||||
|         '<div class="form-row">'+ | ||||
|             '<label for="subflow-input-module-author" data-i18n="[append]editor:subflow.author"></label>'+ | ||||
|             '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-author" data-i18n="[placeholder]editor:subflow.authorPlaceholder">'+ | ||||
|         '</div>'+ | ||||
|         '<div class="form-row">'+ | ||||
|             '<label for="subflow-input-module-keywords" data-i18n="[append]editor:subflow.keys"></label>'+ | ||||
|             '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-keywords" data-i18n="[placeholder]editor:subflow.keysPlaceholder">'+ | ||||
|         '</div>'+ | ||||
|     '</form>'; | ||||
|  | ||||
|     function findAvailableSubflowIOPosition(subflow,isInput) { | ||||
|         var pos = {x:50,y:30}; | ||||
|         if (!isInput) { | ||||
| @@ -909,7 +876,6 @@ RED.subflow = (function() { | ||||
|      * Create interface for controlling env var UI definition | ||||
|      */ | ||||
|     function buildEnvControl(envList,node) { | ||||
|  | ||||
|         var tabs = RED.tabs.create({ | ||||
|             id: "subflow-env-tabs", | ||||
|             onchange: function(tab) { | ||||
| @@ -950,588 +916,11 @@ RED.subflow = (function() { | ||||
|         locales.val(locale); | ||||
|  | ||||
|         locales.on("change", function() { | ||||
|             currentLocale = $(this).val(); | ||||
|             var items = $("#node-input-env-container").editableList("items"); | ||||
|             items.each(function (i, item) { | ||||
|                 var entry = $(this).data('data'); | ||||
|                 var labelField = entry.ui.labelField; | ||||
|                 labelField.val(lookupLabel(entry.ui.label, "", currentLocale)); | ||||
|                 if (labelField.timeout) { | ||||
|                     clearTimeout(labelField.timeout); | ||||
|                     delete labelField.timeout; | ||||
|                 } | ||||
|                 labelField.addClass("input-updated"); | ||||
|                 labelField.timeout = setTimeout(function() { | ||||
|                     delete labelField.timeout | ||||
|                     labelField.removeClass("input-updated"); | ||||
|                 },3000); | ||||
|             }); | ||||
|             RED.editor.envVarList.setLocale($(this).val(), $("#node-input-env-container")); | ||||
|         }); | ||||
|         RED.editor.envVarList.setLocale(locale); | ||||
|     } | ||||
|  | ||||
|     var DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env']; | ||||
|     var DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred']; | ||||
|  | ||||
|     /** | ||||
|      * Create env var edit interface | ||||
|      * @param container - container | ||||
|      * @param node - subflow node | ||||
|      */ | ||||
|     function buildPropertiesList(envContainer, node) { | ||||
|  | ||||
|         var isTemplateNode = (node.type === "subflow"); | ||||
|  | ||||
|         if (isTemplateNode) { | ||||
|             buildEnvControl(envContainer, node); | ||||
|         } | ||||
|         envContainer | ||||
|             .css({ | ||||
|                 'min-height':'150px', | ||||
|                 'min-width':'450px' | ||||
|             }) | ||||
|             .editableList({ | ||||
|                 header: isTemplateNode?$('<div><div><div></div><div data-i18n="common.label.name"></div><div data-i18n="editor-tab.defaultValue"></div><div></div></div></div>'):undefined, | ||||
|                 addItem: function(container, i, opt) { | ||||
|                     // If this is an instance node, these are properties unique to | ||||
|                     // this instance - ie opt.parent will not be defined. | ||||
|  | ||||
|                     if (isTemplateNode) { | ||||
|                         container.addClass("red-ui-editor-subflow-env-editable") | ||||
|                     } | ||||
|  | ||||
|                     var envRow = $('<div/>').appendTo(container); | ||||
|                     var nameField = null; | ||||
|                     var valueField = null; | ||||
|  | ||||
|                     nameField = $('<input/>', { | ||||
|                         class: "node-input-env-name", | ||||
|                         type: "text", | ||||
|                         placeholder: RED._("common.label.name") | ||||
|                     }).attr("autocomplete","disable").appendTo(envRow).val(opt.name); | ||||
|                     valueField = $('<input/>',{ | ||||
|                         style: "width:100%", | ||||
|                         class: "node-input-env-value", | ||||
|                         type: "text", | ||||
|                     }).attr("autocomplete","disable").appendTo(envRow) | ||||
|                     valueField.typedInput({default:'str',types:isTemplateNode?DEFAULT_ENV_TYPE_LIST:DEFAULT_ENV_TYPE_LIST_INC_CRED}); | ||||
|                     valueField.typedInput('type', opt.type); | ||||
|                     if (opt.type === "cred") { | ||||
|                         if (opt.value) { | ||||
|                             valueField.typedInput('value', opt.value); | ||||
|                         } else if (node.credentials && node.credentials[opt.name]) { | ||||
|                             valueField.typedInput('value', node.credentials[opt.name]); | ||||
|                         } else if (node.credentials && node.credentials['has_'+opt.name]) { | ||||
|                             valueField.typedInput('value', "__PWRD__"); | ||||
|                         } else { | ||||
|                             valueField.typedInput('value', ""); | ||||
|                         } | ||||
|                     } else { | ||||
|                         valueField.typedInput('value', opt.value); | ||||
|                     } | ||||
|  | ||||
|  | ||||
|                     opt.nameField = nameField; | ||||
|                     opt.valueField = valueField; | ||||
|  | ||||
|                     var actionButton = $('<a/>',{href:"#",class:"red-ui-editableList-item-remove red-ui-button red-ui-button-small"}).appendTo(envRow); | ||||
|                     $('<i/>',{class:"fa "+(opt.parent?"fa-reply":"fa-remove")}).appendTo(actionButton); | ||||
|                     var removeTip = RED.popover.tooltip(actionButton,RED._("subflow.env.remove")); | ||||
|                     actionButton.on("click", function(evt) { | ||||
|                         evt.preventDefault(); | ||||
|                         removeTip.close(); | ||||
|                         container.parent().addClass("red-ui-editableList-item-deleting") | ||||
|                         container.fadeOut(300, function() { | ||||
|                             envContainer.editableList('removeItem',opt); | ||||
|                         }); | ||||
|                     }); | ||||
|  | ||||
|                     if (isTemplateNode) { | ||||
|                         // Add the UI customisation row | ||||
|                         // if `opt.ui` does not exist, then apply defaults. If these | ||||
|                         // defaults do not change then they will get stripped off | ||||
|                         // before saving. | ||||
|                         if (opt.type === 'cred') { | ||||
|                             opt.ui = opt.ui || { | ||||
|                                 icon: "", | ||||
|                                 type: "cred" | ||||
|                             } | ||||
|                             opt.ui.type = "cred"; | ||||
|                         } else { | ||||
|                             opt.ui = opt.ui || { | ||||
|                                 icon: "", | ||||
|                                 type: "input", | ||||
|                                 opts: {types:DEFAULT_ENV_TYPE_LIST} | ||||
|                             } | ||||
|                         } | ||||
|                         opt.ui.label = opt.ui.label || {}; | ||||
|                         opt.ui.type = opt.ui.type || "input"; | ||||
|  | ||||
|                         var uiRow = $('<div/>').appendTo(container).hide(); | ||||
|                         // save current info for reverting on cancel | ||||
|                         // var copy = $.extend(true, {}, ui); | ||||
|  | ||||
|                          $('<a href="#"><i class="fa fa-angle-right"></a>').prependTo(envRow).on("click", function (evt) { | ||||
|                             evt.preventDefault(); | ||||
|                             if ($(this).hasClass('expanded')) { | ||||
|                                 uiRow.slideUp(); | ||||
|                                 $(this).removeClass('expanded'); | ||||
|                             } else { | ||||
|                                 uiRow.slideDown(); | ||||
|                                 $(this).addClass('expanded'); | ||||
|                             } | ||||
|                         }); | ||||
|  | ||||
|                         buildEnvEditRow(uiRow, opt.ui, nameField, valueField); | ||||
|                         nameField.trigger('change'); | ||||
|                     } | ||||
|                 }, | ||||
|                 sortable: ".red-ui-editableList-item-handle", | ||||
|                 removable: false | ||||
|             }); | ||||
|         var parentEnv = {}; | ||||
|         var envList = []; | ||||
|         if (/^subflow:/.test(node.type)) { | ||||
|             var subflowDef = RED.nodes.subflow(node.type.substring(8)); | ||||
|             if (subflowDef.env) { | ||||
|                 subflowDef.env.forEach(function(env) { | ||||
|                     var item = { | ||||
|                         name:env.name, | ||||
|                         parent: { | ||||
|                             type: env.type, | ||||
|                             value: env.value, | ||||
|                             ui: env.ui | ||||
|                         } | ||||
|                     } | ||||
|                     envList.push(item); | ||||
|                     parentEnv[env.name] = item; | ||||
|                 }) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (node.env) { | ||||
|             for (var i = 0; i < node.env.length; i++) { | ||||
|                 var env = node.env[i]; | ||||
|                 if (parentEnv.hasOwnProperty(env.name)) { | ||||
|                     parentEnv[env.name].type = env.type; | ||||
|                     parentEnv[env.name].value = env.value; | ||||
|                 } else { | ||||
|                     envList.push({ | ||||
|                         name: env.name, | ||||
|                         type: env.type, | ||||
|                         value: env.value, | ||||
|                         ui: env.ui | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         envList.forEach(function(env) { | ||||
|             if (env.parent && env.parent.ui && env.parent.ui.type === 'hide') { | ||||
|                 return; | ||||
|             } | ||||
|             if (!isTemplateNode && env.parent) { | ||||
|                 return; | ||||
|             } | ||||
|             envContainer.editableList('addItem', JSON.parse(JSON.stringify(env))); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create UI edit interface for environment variable | ||||
|      * @param container - container | ||||
|      * @param env - env var definition | ||||
|      * @param nameField - name field of env var | ||||
|      * @param valueField - value field of env var | ||||
|      */ | ||||
|      function buildEnvEditRow(container, ui, nameField, valueField) { | ||||
|          container.addClass("red-ui-editor-subflow-env-ui-row") | ||||
|          var topRow = $('<div></div>').appendTo(container); | ||||
|          $('<div></div>').appendTo(topRow); | ||||
|          $('<div>').text(RED._("editor.icon")).appendTo(topRow); | ||||
|          $('<div>').text(RED._("editor.label")).appendTo(topRow); | ||||
|          $('<div>').text(RED._("editor.inputType")).appendTo(topRow); | ||||
|  | ||||
|          var row = $('<div></div>').appendTo(container); | ||||
|          $('<div><i class="red-ui-editableList-item-handle fa fa-bars"></i></div>').appendTo(row); | ||||
|          var typeOptions = { | ||||
|              'input': {types:DEFAULT_ENV_TYPE_LIST}, | ||||
|              'select': {opts:[]}, | ||||
|              'spinner': {}, | ||||
|              'cred': {} | ||||
|          }; | ||||
|          if (ui.opts) { | ||||
|              typeOptions[ui.type] = ui.opts; | ||||
|          } else { | ||||
|              // Pick up the default values if not otherwise provided | ||||
|              ui.opts = typeOptions[ui.type]; | ||||
|          } | ||||
|          var iconCell = $('<div></div>').appendTo(row); | ||||
|  | ||||
|          var iconButton = $('<a href="#"></a>').appendTo(iconCell); | ||||
|          iconButton.on("click", function(evt) { | ||||
|              evt.preventDefault(); | ||||
|              var icon = ui.icon || ""; | ||||
|              var iconPath = (icon ? RED.utils.separateIconPath(icon) : {}); | ||||
|              RED.editor.showIconPicker(iconButton, null, iconPath, true, function (newIcon) { | ||||
|                  iconButton.empty(); | ||||
|                  var path = newIcon || ""; | ||||
|                  var newPath = RED.utils.separateIconPath(path); | ||||
|                  if (newPath) { | ||||
|                      $('<i class="fa"></i>').addClass(newPath.file).appendTo(iconButton); | ||||
|                  } | ||||
|                  ui.icon = path; | ||||
|              }); | ||||
|          }) | ||||
|  | ||||
|          if (ui.icon) { | ||||
|              var newPath = RED.utils.separateIconPath(ui.icon); | ||||
|              $('<i class="fa '+newPath.file+'"></i>').appendTo(iconButton); | ||||
|          } | ||||
|  | ||||
|          var labelCell = $('<div></div>').appendTo(row); | ||||
|  | ||||
|          var label = ui.label && ui.label[currentLocale] || ""; | ||||
|          var labelInput = $('<input type="text">').val(label).appendTo(labelCell); | ||||
|          ui.labelField = labelInput; | ||||
|          labelInput.on('change', function(evt) { | ||||
|              ui.label = ui.label || {}; | ||||
|              var val = $(this).val().trim(); | ||||
|              if (val === "") { | ||||
|                  delete ui.label[currentLocale]; | ||||
|              } else { | ||||
|                  ui.label[currentLocale] = val; | ||||
|              } | ||||
|          }) | ||||
|          var labelIcon = $('<span class="red-ui-editor-subflow-env-lang-icon"><i class="fa fa-language"></i></span>').appendTo(labelCell); | ||||
|          RED.popover.tooltip(labelIcon,function() { | ||||
|              var langs = Object.keys(ui.label); | ||||
|              var content = $("<div>"); | ||||
|              if (langs.indexOf(currentLocale) === -1) { | ||||
|                  langs.push(currentLocale); | ||||
|                  langs.sort(); | ||||
|              } | ||||
|              langs.forEach(function(l) { | ||||
|                  var row = $('<div>').appendTo(content); | ||||
|                  $('<span>').css({display:"inline-block",width:"120px"}).text(RED._("languages."+l)+(l===currentLocale?"*":"")).appendTo(row); | ||||
|                  $('<span>').text(ui.label[l]||"").appendTo(row); | ||||
|              }); | ||||
|              return content; | ||||
|          }) | ||||
|  | ||||
|          nameField.on('change',function(evt) { | ||||
|             labelInput.attr("placeholder",$(this).val()) | ||||
|         }); | ||||
|  | ||||
|         var inputCell = $('<div></div>').appendTo(row); | ||||
|         var inputCellInput = $('<input type="text">').css("width","100%").appendTo(inputCell); | ||||
|         if (ui.type === "input") { | ||||
|             inputCellInput.val(ui.opts.types.join(",")); | ||||
|         } | ||||
|         var checkbox; | ||||
|         var selectBox; | ||||
|  | ||||
|         inputCellInput.typedInput({ | ||||
|             types: [ | ||||
|                 { | ||||
|                     value:"input", | ||||
|                     label:RED._("editor.inputs.input"), icon:"fa fa-i-cursor",showLabel:false,multiple:true,options:[ | ||||
|                         {value:"str",label:RED._("editor.types.str"),icon:"red/images/typedInput/az.svg"}, | ||||
|                         {value:"num",label:RED._("editor.types.num"),icon:"red/images/typedInput/09.svg"}, | ||||
|                         {value:"bool",label:RED._("editor.types.bool"),icon:"red/images/typedInput/bool.svg"}, | ||||
|                         {value:"json",label:RED._("editor.types.json"),icon:"red/images/typedInput/json.svg"}, | ||||
|                         {value: "bin",label: RED._("editor.types.bin"),icon: "red/images/typedInput/bin.svg"}, | ||||
|                         {value: "env",label: RED._("editor.types.env"),icon: "red/images/typedInput/env.svg"}, | ||||
|                         {value: "cred",label: RED._("editor.types.cred"),icon: "fa fa-lock"} | ||||
|                     ], | ||||
|                     default: DEFAULT_ENV_TYPE_LIST, | ||||
|                     valueLabel: function(container,value) { | ||||
|                         container.css("padding",0); | ||||
|                         var innerContainer = $('<div class="red-ui-editor-subflow-env-input-type"></div>').appendTo(container); | ||||
|  | ||||
|                         var input = $('<div class="placeholder-input">').appendTo(innerContainer); | ||||
|                         $('<span><i class="fa fa-i-cursor"></i></span>').appendTo(input); | ||||
|                         if (value.length) { | ||||
|                             value.forEach(function(v) { | ||||
|                                 if (!/^fa /.test(v.icon)) { | ||||
|                                     $('<img>',{src:v.icon,style:"max-width:14px; padding: 0 3px; margin-top:-4px; margin-left: 1px"}).appendTo(input); | ||||
|                                 } else { | ||||
|                                     var s = $('<span>',{style:"max-width:14px; padding: 0 3px; margin-top:-4px; margin-left: 1px"}).appendTo(input); | ||||
|                                     $("<i>",{class: v.icon}).appendTo(s); | ||||
|                                 } | ||||
|                             }) | ||||
|                         } else { | ||||
|                             $('<span class="red-ui-editor-subflow-env-input-type-placeholder"></span>').text(RED._("editor.selectType")).appendTo(input); | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     value: "cred", | ||||
|                     label: RED._("typedInput.type.cred"), icon:"fa fa-lock", showLabel: false, | ||||
|                     valueLabel: function(container,value) { | ||||
|                         container.css("padding",0); | ||||
|                         var innerContainer = $('<div class="red-ui-editor-subflow-env-input-type">').css({ | ||||
|                             "border-top-right-radius": "4px", | ||||
|                             "border-bottom-right-radius": "4px" | ||||
|                         }).appendTo(container); | ||||
|                         $('<div class="placeholder-input">').html("••••••••").appendTo(innerContainer); | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     value:"select", | ||||
|                     label:RED._("editor.inputs.select"), icon:"fa fa-tasks",showLabel:false, | ||||
|                     valueLabel: function(container,value) { | ||||
|                         container.css("padding","0"); | ||||
|  | ||||
|                         selectBox = $('<select></select>').appendTo(container); | ||||
|                         if (ui.opts && Array.isArray(ui.opts.opts)) { | ||||
|                             ui.opts.opts.forEach(function(o) { | ||||
|                                 var label = lookupLabel(o.l, o.l["en-US"]||o.v, currentLocale); | ||||
|                                 // $('<option>').val((o.t||'str')+":"+o.v).text(label).appendTo(selectBox); | ||||
|                                 $('<option>').val(o.v).text(label).appendTo(selectBox); | ||||
|                             }) | ||||
|                         } | ||||
|                         selectBox.on('change', function(evt) { | ||||
|                             var v = selectBox.val(); | ||||
|                             // var parts = v.split(":"); | ||||
|                             // var t = parts.shift(); | ||||
|                             // v = parts.join(":"); | ||||
|                             // | ||||
|                             // valueField.typedInput("type",'str') | ||||
|                             valueField.typedInput("value",v) | ||||
|                         }); | ||||
|                         selectBox.val(valueField.typedInput("value")); | ||||
|                         // selectBox.val(valueField.typedInput('type')+":"+valueField.typedInput("value")); | ||||
|                     }, | ||||
|                     expand: { | ||||
|                         icon: "fa-caret-down", | ||||
|                         minWidth: 400, | ||||
|                         content: function(container) { | ||||
|                             var content = $('<div class="red-ui-editor-subflow-ui-edit-panel">').appendTo(container); | ||||
|                             var optList = $('<ol>').appendTo(content).editableList({ | ||||
|                                 header:$("<div><div>"+RED._("editor.select.label")+"</div><div>"+RED._("editor.select.value")+"</div></div>"), | ||||
|                                 addItem: function(row,index,itemData) { | ||||
|                                     var labelDiv = $('<div>').appendTo(row); | ||||
|                                     var label = lookupLabel(itemData.l, "", currentLocale); | ||||
|                                     itemData.label = $('<input type="text">').val(label).appendTo(labelDiv); | ||||
|                                     itemData.label.on('keydown', function(evt) { | ||||
|                                         if (evt.keyCode === 13) { | ||||
|                                             itemData.input.focus(); | ||||
|                                             evt.preventDefault(); | ||||
|                                         } | ||||
|                                     }); | ||||
|                                     var labelIcon = $('<span class="red-ui-editor-subflow-env-lang-icon"><i class="fa fa-language"></i></span>').appendTo(labelDiv); | ||||
|                                     RED.popover.tooltip(labelIcon,function() { | ||||
|                                         return currentLocale; | ||||
|                                     }) | ||||
|                                     itemData.input = $('<input type="text">').val(itemData.v).appendTo(row); | ||||
|  | ||||
|                                     // Problem using a TI here: | ||||
|                                     //  - this is in a popout panel | ||||
|                                     //  - clicking the expand button in the TI will close the parent edit tray | ||||
|                                     //    and open the type editor. | ||||
|                                     //  - but it leaves the popout panel over the top. | ||||
|                                     //  - there is no way to get back to the popout panel after closing the type editor | ||||
|                                     //.typedInput({default:itemData.t||'str', types:DEFAULT_ENV_TYPE_LIST}); | ||||
|                                     itemData.input.on('keydown', function(evt) { | ||||
|                                         if (evt.keyCode === 13) { | ||||
|                                             // Enter or Tab | ||||
|                                             var index = optList.editableList('indexOf',itemData); | ||||
|                                             var length = optList.editableList('length'); | ||||
|                                             if (index + 1 === length) { | ||||
|                                                 var newItem = {}; | ||||
|                                                 optList.editableList('addItem',newItem); | ||||
|                                                 setTimeout(function() { | ||||
|                                                     if (newItem.label) { | ||||
|                                                         newItem.label.focus(); | ||||
|                                                     } | ||||
|                                                 },100) | ||||
|                                             } else { | ||||
|                                                 var nextItem = optList.editableList('getItemAt',index+1); | ||||
|                                                 if (nextItem.label) { | ||||
|                                                     nextItem.label.focus() | ||||
|                                                 } | ||||
|                                             } | ||||
|                                             evt.preventDefault(); | ||||
|                                         } | ||||
|                                     }); | ||||
|                                 }, | ||||
|                                 sortable: true, | ||||
|                                 removable: true, | ||||
|                                 height: 160 | ||||
|                             }) | ||||
|                             if (ui.opts.opts.length > 0) { | ||||
|                                 ui.opts.opts.forEach(function(o) { | ||||
|                                     optList.editableList('addItem',$.extend(true,{},o)) | ||||
|                                 }) | ||||
|                             } else { | ||||
|                                 optList.editableList('addItem',{}) | ||||
|                             } | ||||
|                             return { | ||||
|                                 onclose: function() { | ||||
|                                     var items = optList.editableList('items'); | ||||
|                                     var vals = []; | ||||
|                                     items.each(function (i,el) { | ||||
|                                         var data = el.data('data'); | ||||
|                                         var l = data.label.val().trim(); | ||||
|                                         var v = data.input.val(); | ||||
|                                         // var t = data.input.typedInput('type'); | ||||
|                                         // var v = data.input.typedInput('value'); | ||||
|                                         if (l.length > 0) { | ||||
|                                             data.l = data.l || {}; | ||||
|                                             data.l[currentLocale] = l; | ||||
|                                         } | ||||
|                                         data.v = v; | ||||
|  | ||||
|                                         if (l.length > 0 || v.length > 0) { | ||||
|                                             var val = {l:data.l,v:data.v}; | ||||
|                                             // if (t !== 'str') { | ||||
|                                             //     val.t = t; | ||||
|                                             // } | ||||
|                                             vals.push(val); | ||||
|                                         } | ||||
|                                     }); | ||||
|                                     ui.opts.opts = vals; | ||||
|                                     inputCellInput.typedInput('value',Date.now()) | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     value:"checkbox", | ||||
|                     label:RED._("editor.inputs.checkbox"), icon:"fa fa-check-square-o",showLabel:false, | ||||
|                     valueLabel: function(container,value) { | ||||
|                         container.css("padding",0); | ||||
|                         checkbox = $('<input type="checkbox">').appendTo(container); | ||||
|                         checkbox.on('change', function(evt) { | ||||
|                             valueField.typedInput('value',$(this).prop('checked')?"true":"false"); | ||||
|                         }) | ||||
|                         checkbox.prop('checked',valueField.typedInput('value')==="true"); | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     value:"spinner", | ||||
|                     label:RED._("editor.inputs.spinner"), icon:"fa fa-sort-numeric-asc", showLabel:false, | ||||
|                     valueLabel: function(container,value) { | ||||
|                         container.css("padding",0); | ||||
|                         var innerContainer = $('<div class="red-ui-editor-subflow-env-input-type"></div>').appendTo(container); | ||||
|  | ||||
|                         var input = $('<div class="placeholder-input">').appendTo(innerContainer); | ||||
|                         $('<span><i class="fa fa-sort-numeric-asc"></i></span>').appendTo(input); | ||||
|  | ||||
|                         var min = ui.opts && ui.opts.min; | ||||
|                         var max = ui.opts && ui.opts.max; | ||||
|                         var label = ""; | ||||
|                         if (min !== undefined && max !== undefined) { | ||||
|                             label = Math.min(min,max)+" - "+Math.max(min,max); | ||||
|                         } else if (min !== undefined) { | ||||
|                             label = "> "+min; | ||||
|                         } else if (max !== undefined) { | ||||
|                             label = "< "+max; | ||||
|                         } | ||||
|                         $('<span>').css("margin-left","15px").text(label).appendTo(input); | ||||
|                     }, | ||||
|                     expand: { | ||||
|                         icon: "fa-caret-down", | ||||
|                         content: function(container) { | ||||
|                             var content = $('<div class="red-ui-editor-subflow-ui-edit-panel">').appendTo(container); | ||||
|                             content.css("padding","8px 5px") | ||||
|                             var min = ui.opts.min; | ||||
|                             var max = ui.opts.max; | ||||
|                             var minInput = $('<input type="number" style="margin-bottom:0; width:60px">'); | ||||
|                             minInput.val(min); | ||||
|                             var maxInput = $('<input type="number" style="margin-bottom:0; width:60px">'); | ||||
|                             maxInput.val(max); | ||||
|                             $('<div class="form-row" style="margin-bottom:3px"><label>'+RED._("editor.spinner.min")+'</label></div>').append(minInput).appendTo(content); | ||||
|                             $('<div class="form-row" style="margin-bottom:0"><label>'+RED._("editor.spinner.max")+'</label></div>').append(maxInput).appendTo(content); | ||||
|                             return { | ||||
|                                 onclose: function() { | ||||
|                                     var min = minInput.val().trim(); | ||||
|                                     var max = maxInput.val().trim(); | ||||
|                                     if (min !== "") { | ||||
|                                         ui.opts.min = parseInt(min); | ||||
|                                     } else { | ||||
|                                         delete ui.opts.min; | ||||
|                                     } | ||||
|                                     if (max !== "") { | ||||
|                                         ui.opts.max = parseInt(max); | ||||
|                                     } else { | ||||
|                                         delete ui.opts.max; | ||||
|                                     } | ||||
|                                     inputCellInput.typedInput('value',Date.now()) | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     value:"none", | ||||
|                     label:RED._("editor.inputs.none"), icon:"fa fa-times",hasValue:false | ||||
|                 }, | ||||
|                 { | ||||
|                     value:"hide", | ||||
|                     label:RED._("editor.inputs.hidden"), icon:"fa fa-ban",hasValue:false | ||||
|                 } | ||||
|             ], | ||||
|             default: 'none' | ||||
|         }).on("typedinputtypechange", function(evt,type) { | ||||
|             ui.type = $(this).typedInput("type"); | ||||
|             ui.opts = typeOptions[ui.type]; | ||||
|             if (ui.type === 'input') { | ||||
|                 // In the case of 'input' type, the typedInput uses the multiple-option | ||||
|                 // mode. Its value needs to be set to a comma-separately list of the | ||||
|                 // selected options. | ||||
|                 inputCellInput.typedInput('value',ui.opts.types.join(",")) | ||||
|             } else { | ||||
|                 // No other type cares about `value`, but doing this will | ||||
|                 // force a refresh of the label now that `ui.opts` has | ||||
|                 // been updated. | ||||
|                 inputCellInput.typedInput('value',Date.now()) | ||||
|             } | ||||
|  | ||||
|             switch (ui.type) { | ||||
|                 case 'input': | ||||
|                     valueField.typedInput('types',ui.opts.types); | ||||
|                     break; | ||||
|                 case 'select': | ||||
|                     valueField.typedInput('types',['str']); | ||||
|                     break; | ||||
|                 case 'checkbox': | ||||
|                     valueField.typedInput('types',['bool']); | ||||
|                     break; | ||||
|                 case 'spinner': | ||||
|                     valueField.typedInput('types',['num']); | ||||
|                     break; | ||||
|                 case 'cred': | ||||
|                     valueField.typedInput('types',['cred']); | ||||
|                     break; | ||||
|                 default: | ||||
|                     valueField.typedInput('types',DEFAULT_ENV_TYPE_LIST) | ||||
|             } | ||||
|             if (ui.type === 'checkbox') { | ||||
|                 valueField.typedInput('type','bool'); | ||||
|             } else if (ui.type === 'spinner') { | ||||
|                 valueField.typedInput('type','num'); | ||||
|             } | ||||
|             if (ui.type !== 'checkbox') { | ||||
|                 checkbox = null; | ||||
|             } | ||||
|  | ||||
|         }).on("change", function(evt,type) { | ||||
|             if (ui.type === 'input') { | ||||
|                 var types = inputCellInput.typedInput('value'); | ||||
|                 ui.opts.types = (types === "") ? ["str"] : types.split(","); | ||||
|                 valueField.typedInput('types',ui.opts.types); | ||||
|             } | ||||
|         }); | ||||
|         valueField.on("change", function(evt) { | ||||
|             if (checkbox) { | ||||
|                 checkbox.prop('checked',$(this).typedInput('value')==="true") | ||||
|             } | ||||
|         }) | ||||
|         // Set the input to the right type. This will trigger the 'typedinputtypechange' | ||||
|         // event handler (just above ^^) to update the value if needed | ||||
|         inputCellInput.typedInput('type',ui.type) | ||||
|     } | ||||
|  | ||||
|     function buildEnvUIRow(row, tenv, ui, node) { | ||||
|         ui.label = ui.label||{}; | ||||
| @@ -1540,7 +929,7 @@ RED.subflow = (function() { | ||||
|             ui.opts = {}; | ||||
|         } else if (!ui.type) { | ||||
|             ui.type = "input"; | ||||
|             ui.opts = {types:DEFAULT_ENV_TYPE_LIST} | ||||
|             ui.opts = { types: RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST } | ||||
|         } else { | ||||
|             if (!ui.opts) { | ||||
|                 ui.opts = (ui.type === "select") ? {opts:[]} : {}; | ||||
| @@ -1549,7 +938,7 @@ RED.subflow = (function() { | ||||
|  | ||||
|         var labels = ui.label || {}; | ||||
|         var locale = RED.i18n.lang(); | ||||
|         var labelText = lookupLabel(labels, labels["en-US"]||tenv.name, locale); | ||||
|         var labelText = RED.editor.envVarList.lookupLabel(labels, labels["en-US"]||tenv.name, locale); | ||||
|         var label = $('<label>').appendTo(row); | ||||
|         $('<span> </span>').appendTo(row); | ||||
|         var labelContainer = $('<span></span>').appendTo(label); | ||||
| @@ -1602,7 +991,7 @@ RED.subflow = (function() { | ||||
|                 input = $('<select>').css('width','70%').appendTo(row); | ||||
|                 if (ui.opts.opts) { | ||||
|                     ui.opts.opts.forEach(function(o) { | ||||
|                         $('<option>').val(o.v).text(lookupLabel(o.l, o.l['en-US']||o.v, locale)).appendTo(input); | ||||
|                         $('<option>').val(o.v).text(RED.editor.envVarList.lookupLabel(o.l, o.l['en-US']||o.v, locale)).appendTo(input); | ||||
|                     }) | ||||
|                 } | ||||
|                 input.val(val.value); | ||||
| @@ -1668,9 +1057,8 @@ RED.subflow = (function() { | ||||
|      * @param uiContainer - container for UI | ||||
|      * @param envList - env var definitions of template | ||||
|      */ | ||||
|     function buildEnvUI(uiContainer, envList,node) { | ||||
|     function buildEnvUI(uiContainer, envList, node) { | ||||
|         uiContainer.empty(); | ||||
|         var elementID = 0; | ||||
|         for (var i = 0; i < envList.length; i++) { | ||||
|             var tenv = envList[i]; | ||||
|             if (tenv.ui && tenv.ui.type === 'hide') { | ||||
| @@ -1678,8 +1066,6 @@ RED.subflow = (function() { | ||||
|             } | ||||
|             var row = $("<div/>", { class: "form-row" }).appendTo(uiContainer); | ||||
|             buildEnvUIRow(row,tenv, tenv.ui || {}, node); | ||||
|  | ||||
|             // console.log(ui); | ||||
|         } | ||||
|     } | ||||
|     // buildEnvUI | ||||
| @@ -1715,7 +1101,7 @@ RED.subflow = (function() { | ||||
|                             //     icon: "", | ||||
|                             //     label: {}, | ||||
|                             //     type: "input", | ||||
|                             //     opts: {types:DEFAULT_ENV_TYPE_LIST} | ||||
|                             //     opts: {types:RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST} | ||||
|                             // } | ||||
|                             if (!ui.icon) { | ||||
|                                 delete ui.icon; | ||||
| @@ -1725,7 +1111,7 @@ RED.subflow = (function() { | ||||
|                             } | ||||
|                             switch (ui.type) { | ||||
|                                 case "input": | ||||
|                                     if (JSON.stringify(ui.opts) === JSON.stringify({types:DEFAULT_ENV_TYPE_LIST})) { | ||||
|                                     if (JSON.stringify(ui.opts) === JSON.stringify({types:RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST})) { | ||||
|                                         // This is the default input config. Delete it as it will | ||||
|                                         // be applied automatically | ||||
|                                         delete ui.type; | ||||
| @@ -1841,7 +1227,6 @@ RED.subflow = (function() { | ||||
|  | ||||
|     function exportSubflowInstanceEnv(node) { | ||||
|         var env = []; | ||||
|  | ||||
|         // First, get the values for the SubflowTemplate defined properties | ||||
|         //  - these are the ones with custom UI elements | ||||
|         var parentEnv = getSubflowInstanceParentEnv(node); | ||||
| @@ -1853,7 +1238,7 @@ RED.subflow = (function() { | ||||
|                     ui.type = "cred"; | ||||
|                 } else { | ||||
|                     ui.type = "input"; | ||||
|                     ui.opts = {types:DEFAULT_ENV_TYPE_LIST} | ||||
|                     ui.opts = {types:RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST} | ||||
|                 } | ||||
|             } else { | ||||
|                 ui.opts = ui.opts || {}; | ||||
| @@ -1893,22 +1278,6 @@ RED.subflow = (function() { | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|         // Second, get the values from the Properties table tab | ||||
|         var items = $('#red-ui-editor-subflow-env-list').editableList('items'); | ||||
|         items.each(function (i,el) { | ||||
|             var data = el.data('data'); | ||||
|             var item; | ||||
|             if (data.nameField && data.valueField) { | ||||
|                 item = { | ||||
|                     name: data.nameField.val(), | ||||
|                     value: data.valueField.typedInput("value"), | ||||
|                     type: data.valueField.typedInput("type") | ||||
|                 } | ||||
|                 if (item.name.trim() !== "") { | ||||
|                     env.push(item); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|         return env; | ||||
|     } | ||||
|  | ||||
| @@ -1916,164 +1285,18 @@ RED.subflow = (function() { | ||||
|         return 'node-input-subflow-env-'+name.replace(/[^a-z0-9-_]/ig,"_"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Lookup text for specific locale | ||||
|      * @param labels - dict of labels | ||||
|      * @param defaultLabel - fallback label if not found | ||||
|      * @param locale - target locale | ||||
|      * @returns {string} text for specified locale | ||||
|      */ | ||||
|     function lookupLabel(labels, defaultLabel, locale) { | ||||
|         if (labels) { | ||||
|             if (labels[locale]) { | ||||
|                 return labels[locale]; | ||||
|             } | ||||
|             if (locale) { | ||||
|                 var lang = locale.substring(0, 2); | ||||
|                 if (labels[lang]) { | ||||
|                     return labels[lang]; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return defaultLabel; | ||||
|     } | ||||
|  | ||||
|     // Called by subflow.oneditprepare for both instances and templates | ||||
|     function buildEditForm(type,node) { | ||||
|         if (type === "subflow-template") { | ||||
|             buildPropertiesList($('#node-input-env-container'), node); | ||||
|             // This is the tabbed UI that offers the env list - with UI options | ||||
|             // plus the preview tab | ||||
|             buildEnvControl($('#node-input-env-container'), node); | ||||
|             RED.editor.envVarList.create($('#node-input-env-container'), node); | ||||
|         } else  if (type === "subflow") { | ||||
|             // This gets called by the subflow type `oneditprepare` function | ||||
|             // registered in nodes.js#addSubflow() | ||||
|             // This is the rendered version of the subflow env var list | ||||
|             buildEnvUI($("#subflow-input-ui"), getSubflowInstanceParentEnv(node), node); | ||||
|         } | ||||
|     } | ||||
|     function buildPropertiesForm(node) { | ||||
|         var container = $('#editor-subflow-envProperties-content'); | ||||
|         var form = $('<form class="dialog-form form-horizontal"></form>').appendTo(container); | ||||
|         var listContainer = $('<div class="form-row node-input-env-container-row"></div>').appendTo(form); | ||||
|         var list = $('<ol id="red-ui-editor-subflow-env-list" class="red-ui-editor-subflow-env-list"></ol>').appendTo(listContainer); | ||||
|         buildPropertiesList(list, node); | ||||
|     } | ||||
|  | ||||
|     function setupInputValidation(input,validator) { | ||||
|         var errorTip; | ||||
|         var validateTimeout; | ||||
|  | ||||
|         var validateFunction = function() { | ||||
|             if (validateTimeout) { | ||||
|                 return; | ||||
|             } | ||||
|             validateTimeout = setTimeout(function() { | ||||
|                 var error = validator(input.val()); | ||||
|                 // if (!error && errorTip) { | ||||
|                 //     errorTip.close(); | ||||
|                 //     errorTip = null; | ||||
|                 // } else if (error && !errorTip) { | ||||
|                 //     errorTip = RED.popover.create({ | ||||
|                 //         tooltip: true, | ||||
|                 //         target:input, | ||||
|                 //         size: "small", | ||||
|                 //         direction: "bottom", | ||||
|                 //         content: error, | ||||
|                 //     }).open(); | ||||
|                 // } | ||||
|                 input.toggleClass("input-error",!!error); | ||||
|                 validateTimeout = null; | ||||
|             }) | ||||
|         } | ||||
|         input.on("change keyup paste", validateFunction); | ||||
|     } | ||||
|  | ||||
|     function buildModuleForm(container, node) { | ||||
|         $(_subflowModulePaneTemplate).appendTo(container); | ||||
|         var moduleProps = node.meta || {}; | ||||
|         [ | ||||
|             'module', | ||||
|             'type', | ||||
|             'version', | ||||
|             'author', | ||||
|             'desc', | ||||
|             'keywords', | ||||
|             'license' | ||||
|         ].forEach(function(property) { | ||||
|             $("#subflow-input-module-"+property).val(moduleProps[property]||"") | ||||
|         }) | ||||
|         $("#subflow-input-module-type").attr("placeholder",node.id); | ||||
|  | ||||
|         setupInputValidation($("#subflow-input-module-module"), function(newValue) { | ||||
|             newValue = newValue.trim(); | ||||
|             var isValid = newValue.length < 215; | ||||
|             isValid = isValid && !/^[._]/.test(newValue); | ||||
|             isValid = isValid && !/[A-Z]/.test(newValue); | ||||
|             if (newValue !== encodeURIComponent(newValue)) { | ||||
|                 var m = /^@([^\/]+)\/([^\/]+)$/.exec(newValue); | ||||
|                 if (m) { | ||||
|                     isValid = isValid && (m[1] === encodeURIComponent(m[1]) && m[2] === encodeURIComponent(m[2])) | ||||
|                 } else { | ||||
|                     isValid = false; | ||||
|                 } | ||||
|             } | ||||
|             return isValid?"":"Invalid module name" | ||||
|         }) | ||||
|         setupInputValidation($("#subflow-input-module-version"), function(newValue) { | ||||
|             newValue = newValue.trim(); | ||||
|             var isValid = newValue === "" || | ||||
|                           /^(\d|[1-9]\d*)\.(\d|[1-9]\d*)\.(\d|[1-9]\d*)(-(0|[1-9A-Za-z-][0-9A-Za-z-]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9A-Za-z-][0-9A-Za-z-]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/.test(newValue); | ||||
|             return isValid?"":"Invalid version number" | ||||
|         }) | ||||
|  | ||||
|         var licenses = ["none", "Apache-2.0", "BSD-3-Clause", "BSD-2-Clause", "GPL-2.0", "GPL-3.0", "MIT", "MPL-2.0", "CDDL-1.0", "EPL-2.0"]; | ||||
|         var typedLicenses = { | ||||
|             types: licenses.map(function(l) { | ||||
|                 return { | ||||
|                     value: l, | ||||
|                     label: l === "none" ? RED._("editor:subflow.licenseNone") : l, | ||||
|                     hasValue: false | ||||
|                 }; | ||||
|             }) | ||||
|         } | ||||
|         typedLicenses.types.push({ | ||||
|             value:"_custom_", label:RED._("editor:subflow.licenseOther"), icon:"red/images/typedInput/az.svg" | ||||
|         }) | ||||
|         if (!moduleProps.license) { | ||||
|             typedLicenses.default = "none"; | ||||
|         } else if (licenses.indexOf(moduleProps.license) > -1) { | ||||
|             typedLicenses.default = moduleProps.license; | ||||
|         } else { | ||||
|             typedLicenses.default = "_custom_"; | ||||
|         } | ||||
|         $("#subflow-input-module-license").typedInput(typedLicenses) | ||||
|     } | ||||
|  | ||||
|     function exportSubflowModuleProperties(node) { | ||||
|         var value; | ||||
|         var moduleProps = {}; | ||||
|         [ | ||||
|             'module', | ||||
|             'type', | ||||
|             'version', | ||||
|             'author', | ||||
|             'desc', | ||||
|             'keywords' | ||||
|         ].forEach(function(property) { | ||||
|             value = $("#subflow-input-module-"+property).val().trim(); | ||||
|             if (value) { | ||||
|                 moduleProps[property] = value; | ||||
|             } | ||||
|         }) | ||||
|         var selectedLicenseType = $("#subflow-input-module-license").typedInput("type"); | ||||
|  | ||||
|         if (selectedLicenseType === '_custom_') { | ||||
|             value = $("#subflow-input-module-license").val(); | ||||
|             if (value) { | ||||
|                 moduleProps.license = value; | ||||
|             } | ||||
|         } else if (selectedLicenseType !== "none") { | ||||
|             moduleProps.license = selectedLicenseType; | ||||
|         } | ||||
|         return moduleProps; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     return { | ||||
|         init: init, | ||||
| @@ -2085,14 +1308,9 @@ RED.subflow = (function() { | ||||
|         removeOutput: removeSubflowOutput, | ||||
|         removeStatus: removeSubflowStatus, | ||||
|  | ||||
|  | ||||
|         buildEditForm: buildEditForm, | ||||
|         buildPropertiesForm: buildPropertiesForm, | ||||
|         buildModuleForm: buildModuleForm, | ||||
|  | ||||
|         exportSubflowTemplateEnv: exportEnvList, | ||||
|         exportSubflowInstanceEnv: exportSubflowInstanceEnv, | ||||
|         exportSubflowModuleProperties: exportSubflowModuleProperties | ||||
|  | ||||
|         exportSubflowInstanceEnv: exportSubflowInstanceEnv | ||||
|     } | ||||
| })(); | ||||
|   | ||||
| @@ -311,6 +311,7 @@ RED.sidebar.info.outliner = (function() { | ||||
|         RED.events.on("nodes:add",onNodeAdd); | ||||
|         RED.events.on("nodes:remove",onObjectRemove); | ||||
|         RED.events.on("nodes:change",onNodeChange); | ||||
|         // RED.events.on("nodes:reorder",onNodesReorder); | ||||
|  | ||||
|         RED.events.on("groups:add",onNodeAdd); | ||||
|         RED.events.on("groups:remove",onObjectRemove); | ||||
| @@ -369,6 +370,21 @@ RED.sidebar.info.outliner = (function() { | ||||
|             return indexMap[A.id] - indexMap[B.id] | ||||
|         }) | ||||
|     } | ||||
|     // function onNodesReorder(event) { | ||||
|     //     // | ||||
|     //     var nodes = RED.nodes.getNodeOrder(event.z); | ||||
|     //     var indexMap = {}; | ||||
|     //     nodes.forEach(function(id,index) { | ||||
|     //         indexMap[id] = index; | ||||
|     //     }) | ||||
|     //     var existingObject = objects[event.z]; | ||||
|     //     existingObject.treeList.sortChildren(function(A,B) { | ||||
|     //         if (A.children && !B.children) { return -1 } | ||||
|     //         if (!A.children && B.children) { return 1 } | ||||
|     //         if (A.children && B.children) { return -1 } | ||||
|     //         return indexMap[A.id] - indexMap[B.id] | ||||
|     //     }) | ||||
|     // } | ||||
|     function onSubflowAdd(sf) { | ||||
|         objects[sf.id] = { | ||||
|             id: sf.id, | ||||
|   | ||||
| @@ -427,18 +427,309 @@ RED.view.tools = (function() { | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|  | ||||
|     } | ||||
|  | ||||
|     function alignSelectionToEdge(direction) { | ||||
|         var selection = RED.view.selection(); | ||||
|  | ||||
|         if (selection.nodes && selection.nodes.length > 1) { | ||||
|             var changedNodes = []; | ||||
|             var bounds = { | ||||
|                 minX: Number.MAX_SAFE_INTEGER, | ||||
|                 minY: Number.MAX_SAFE_INTEGER, | ||||
|                 maxX: Number.MIN_SAFE_INTEGER, | ||||
|                 maxY: Number.MIN_SAFE_INTEGER | ||||
|             } | ||||
|             selection.nodes.forEach(function(n) { | ||||
|                 if (n.type === "group") { | ||||
|                     bounds.minX = Math.min(bounds.minX, n.x); | ||||
|                     bounds.minY = Math.min(bounds.minY, n.y); | ||||
|                     bounds.maxX = Math.max(bounds.maxX, n.x + n.w); | ||||
|                     bounds.maxY = Math.max(bounds.maxY, n.y + n.h); | ||||
|                 } else { | ||||
|                     bounds.minX = Math.min(bounds.minX, n.x - n.w/2); | ||||
|                     bounds.minY = Math.min(bounds.minY, n.y - n.h/2); | ||||
|                     bounds.maxX = Math.max(bounds.maxX, n.x + n.w/2); | ||||
|                     bounds.maxY = Math.max(bounds.maxY, n.y + n.h/2); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             bounds.midX = bounds.minX + (bounds.maxX - bounds.minX)/2; | ||||
|             bounds.midY = bounds.minY + (bounds.maxY - bounds.minY)/2; | ||||
|  | ||||
|             selection.nodes.forEach(function(n) { | ||||
|                 var targetX; | ||||
|                 var targetY; | ||||
|                 var isGroup = n.type==="group"; | ||||
|                 switch(direction) { | ||||
|                     case 'top': | ||||
|                         targetX = n.x; | ||||
|                         targetY = bounds.minY + (isGroup?0:(n.h/2)); | ||||
|                         break; | ||||
|                     case 'bottom': | ||||
|                         targetX = n.x; | ||||
|                         targetY = bounds.maxY - (isGroup?n.h:(n.h/2)); | ||||
|                         break; | ||||
|                     case 'left': | ||||
|                         targetX = bounds.minX + (isGroup?0:(n.w/2)); | ||||
|                         targetY = n.y; | ||||
|                         break; | ||||
|                     case 'right': | ||||
|                         targetX = bounds.maxX - (isGroup?n.w:(n.w/2)); | ||||
|                         targetY = n.y; | ||||
|                         break; | ||||
|                     case 'middle': | ||||
|                         targetX = n.x; | ||||
|                         targetY = bounds.midY - (isGroup?n.h/2:0) | ||||
|                         break; | ||||
|                     case 'center': | ||||
|                         targetX = bounds.midX - (isGroup?n.w/2:0) | ||||
|                         targetY = n.y; | ||||
|                         break; | ||||
|                 } | ||||
|  | ||||
|                 if (n.x !== targetX || n.y !== targetY) { | ||||
|                     if (!isGroup) { | ||||
|                         changedNodes.push({ | ||||
|                             n:n, | ||||
|                             ox: n.x, | ||||
|                             oy: n.y, | ||||
|                             moved: n.moved | ||||
|                         }); | ||||
|                         n.x = targetX; | ||||
|                         n.y = targetY; | ||||
|                         n.dirty = true; | ||||
|                         n.moved = true; | ||||
|                     } else { | ||||
|                         var groupNodes = RED.group.getNodes(n, true); | ||||
|                         var deltaX = n.x - targetX; | ||||
|                         var deltaY = n.y - targetY; | ||||
|                         groupNodes.forEach(function(gn) { | ||||
|                             if (gn.type !== "group" ) { | ||||
|                                 changedNodes.push({ | ||||
|                                     n:gn, | ||||
|                                     ox: gn.x, | ||||
|                                     oy: gn.y, | ||||
|                                     moved: gn.moved | ||||
|                                 }); | ||||
|                                 gn.x = gn.x - deltaX; | ||||
|                                 gn.y = gn.y - deltaY; | ||||
|                                 gn.dirty = true; | ||||
|                                 gn.moved = true; | ||||
|                             } | ||||
|                         }) | ||||
|  | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|             if (changedNodes.length > 0) { | ||||
|                 RED.history.push({t:"move",nodes:changedNodes,dirty:RED.nodes.dirty()}); | ||||
|                 RED.nodes.dirty(true); | ||||
|                 RED.view.redraw(true); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     function distributeSelection(direction) { | ||||
|         var selection = RED.view.selection(); | ||||
|  | ||||
|         if (selection.nodes && selection.nodes.length > 2) { | ||||
|             var changedNodes = []; | ||||
|             var bounds = { | ||||
|                 minX: Number.MAX_SAFE_INTEGER, | ||||
|                 minY: Number.MAX_SAFE_INTEGER, | ||||
|                 maxX: Number.MIN_SAFE_INTEGER, | ||||
|                 maxY: Number.MIN_SAFE_INTEGER | ||||
|             } | ||||
|             var startAnchors = []; | ||||
|             var endAnchors = []; | ||||
|  | ||||
|             selection.nodes.forEach(function(n) { | ||||
|                 var nx,ny; | ||||
|                 if (n.type === "group") { | ||||
|                     nx = n.x + n.w/2; | ||||
|                     ny = n.y + n.h/2; | ||||
|                 } else { | ||||
|                     nx = n.x; | ||||
|                     ny = n.y; | ||||
|                 } | ||||
|                 if (direction === "h") { | ||||
|                     if (nx < bounds.minX) { | ||||
|                         startAnchors = []; | ||||
|                         bounds.minX = nx; | ||||
|                     } | ||||
|                     if (nx === bounds.minX) { | ||||
|                         startAnchors.push(n); | ||||
|                     } | ||||
|                     if (nx > bounds.maxX) { | ||||
|                         endAnchors = []; | ||||
|                         bounds.maxX = nx; | ||||
|                     } | ||||
|                     if (nx === bounds.maxX) { | ||||
|                         endAnchors.push(n); | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (ny < bounds.minY) { | ||||
|                         startAnchors = []; | ||||
|                         bounds.minY = ny; | ||||
|                     } | ||||
|                     if (ny === bounds.minY) { | ||||
|                         startAnchors.push(n); | ||||
|                     } | ||||
|                     if (ny > bounds.maxY) { | ||||
|                         endAnchors = []; | ||||
|                         bounds.maxY = ny; | ||||
|                     } | ||||
|                     if (ny === bounds.maxY) { | ||||
|                         endAnchors.push(n); | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             var startAnchor = startAnchors[0]; | ||||
|             var endAnchor = endAnchors[0]; | ||||
|  | ||||
|             var nodeSpace = 0; | ||||
|             var nodesToMove = selection.nodes.filter(function(n) { | ||||
|                 if (n.id !== startAnchor.id && n.id !== endAnchor.id) { | ||||
|                     nodeSpace += direction === 'h'?n.w:n.h; | ||||
|                     return true; | ||||
|                 } | ||||
|                 return false; | ||||
|             }).sort(function(A,B) { | ||||
|                 if (direction === 'h') { | ||||
|                     return A.x - B.x | ||||
|                 } else { | ||||
|                     return A.y - B.y | ||||
|                 } | ||||
|             }) | ||||
|  | ||||
|             var saX = startAnchor.x + startAnchor.w/2; | ||||
|             var saY = startAnchor.y + startAnchor.h/2; | ||||
|             if (startAnchor.type === "group") { | ||||
|                 saX = startAnchor.x + startAnchor.w; | ||||
|                 saY = startAnchor.y + startAnchor.h; | ||||
|             } | ||||
|             var eaX = endAnchor.x; | ||||
|             var eaY = endAnchor.y; | ||||
|             if (endAnchor.type !== "group") { | ||||
|                 eaX -= endAnchor.w/2; | ||||
|                 eaY -= endAnchor.h/2; | ||||
|             } | ||||
|             var spaceToFill = direction === 'h'?(eaX - saX - nodeSpace): (eaY - saY - nodeSpace); | ||||
|             var spaceBetweenNodes = spaceToFill / (nodesToMove.length + 1); | ||||
|  | ||||
|             var tx = saX; | ||||
|             var ty = saY; | ||||
|             while(nodesToMove.length > 0) { | ||||
|                 if (direction === 'h') { | ||||
|                     tx += spaceBetweenNodes; | ||||
|                 } else { | ||||
|                     ty += spaceBetweenNodes; | ||||
|                 } | ||||
|                 var nextNode = nodesToMove.shift(); | ||||
|                 var isGroup = nextNode.type==="group"; | ||||
|  | ||||
|                 var nx = nextNode.x; | ||||
|                 var ny = nextNode.y; | ||||
|                 if (!isGroup) { | ||||
|                     tx += nextNode.w/2; | ||||
|                     ty += nextNode.h/2; | ||||
|                 } | ||||
|                 if ((direction === 'h' && nx !== tx) || (direction === 'v' && ny !== ty)) { | ||||
|                     if (!isGroup) { | ||||
|                         changedNodes.push({ | ||||
|                             n:nextNode, | ||||
|                             ox: nextNode.x, | ||||
|                             oy: nextNode.y, | ||||
|                             moved: nextNode.moved | ||||
|                         }); | ||||
|                         if (direction === 'h') { | ||||
|                             nextNode.x = tx; | ||||
|                         } else { | ||||
|                             nextNode.y = ty; | ||||
|                         } | ||||
|                         nextNode.dirty = true; | ||||
|                         nextNode.moved = true; | ||||
|                     } else { | ||||
|                         var groupNodes = RED.group.getNodes(nextNode, true); | ||||
|                         var deltaX = direction === 'h'? nx - tx : 0; | ||||
|                         var deltaY = direction === 'v'? ny - ty : 0; | ||||
|                         groupNodes.forEach(function(gn) { | ||||
|                             if (gn.type !== "group" ) { | ||||
|                                 changedNodes.push({ | ||||
|                                     n:gn, | ||||
|                                     ox: gn.x, | ||||
|                                     oy: gn.y, | ||||
|                                     moved: gn.moved | ||||
|                                 }); | ||||
|                                 gn.x = gn.x - deltaX; | ||||
|                                 gn.y = gn.y - deltaY; | ||||
|                                 gn.dirty = true; | ||||
|                                 gn.moved = true; | ||||
|                             } | ||||
|                         }) | ||||
|                     } | ||||
|                 } | ||||
|                 if (isGroup) { | ||||
|                     tx += nextNode.w; | ||||
|                     ty += nextNode.h; | ||||
|                 } else { | ||||
|                     tx += nextNode.w/2; | ||||
|                     ty += nextNode.h/2; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (changedNodes.length > 0) { | ||||
|                 RED.history.push({t:"move",nodes:changedNodes,dirty:RED.nodes.dirty()}); | ||||
|                 RED.nodes.dirty(true); | ||||
|                 RED.view.redraw(true); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function reorderSelection(dir) { | ||||
|         var selection = RED.view.selection(); | ||||
|         if (selection.nodes) { | ||||
|             var nodesToMove = []; | ||||
|             selection.nodes.forEach(function(n) { | ||||
|                 if (n.type === "group") { | ||||
|                     nodesToMove = nodesToMove.concat(RED.group.getNodes(n, true).filter(function(n) { | ||||
|                         return n.type !== "group"; | ||||
|                     })) | ||||
|                 } else if (n.type !== "subflow"){ | ||||
|                     nodesToMove.push(n); | ||||
|                 } | ||||
|             }) | ||||
|             if (nodesToMove.length > 0) { | ||||
|                 var z = nodesToMove[0].z; | ||||
|                 var existingOrder = RED.nodes.getNodeOrder(z); | ||||
|                 var movedNodes; | ||||
|                 if (dir === "forwards") { | ||||
|                     movedNodes = RED.nodes.moveNodesForwards(nodesToMove); | ||||
|                 } else if (dir === "backwards") { | ||||
|                     movedNodes = RED.nodes.moveNodesBackwards(nodesToMove); | ||||
|                 } else if (dir === "front") { | ||||
|                     movedNodes = RED.nodes.moveNodesToFront(nodesToMove); | ||||
|                 } else if (dir === "back") { | ||||
|                     movedNodes = RED.nodes.moveNodesToBack(nodesToMove); | ||||
|                 } | ||||
|                 if (movedNodes.length > 0) { | ||||
|                     var newOrder = RED.nodes.getNodeOrder(z); | ||||
|                     RED.history.push({t:"reorder",nodes:{z:z,from:existingOrder,to:newOrder},dirty:RED.nodes.dirty()}); | ||||
|                     RED.nodes.dirty(true); | ||||
|                     RED.view.redraw(true); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         init: function() { | ||||
|             RED.actions.add("core:show-selected-node-labels", function() { setSelectedNodeLabelState(true); }) | ||||
|             RED.actions.add("core:hide-selected-node-labels", function() { setSelectedNodeLabelState(false); }) | ||||
|  | ||||
|             RED.actions.add("core:align-selection-to-grid", alignToGrid); | ||||
|  | ||||
|             RED.actions.add("core:scroll-view-up", function() { RED.view.scroll(0,-RED.view.gridSize());}); | ||||
|             RED.actions.add("core:scroll-view-right", function() { RED.view.scroll(RED.view.gridSize(),0);}); | ||||
|             RED.actions.add("core:scroll-view-down", function() { RED.view.scroll(0,RED.view.gridSize());}); | ||||
| @@ -454,6 +745,12 @@ RED.view.tools = (function() { | ||||
|             RED.actions.add("core:move-selection-down", function() { moveSelection(0,1);}); | ||||
|             RED.actions.add("core:move-selection-left", function() { moveSelection(-1,0);}); | ||||
|  | ||||
|             RED.actions.add("core:move-selection-forwards", function() { reorderSelection('forwards') }) | ||||
|             RED.actions.add("core:move-selection-backwards", function() { reorderSelection('backwards') }) | ||||
|             RED.actions.add("core:move-selection-to-front", function() { reorderSelection('front') }) | ||||
|             RED.actions.add("core:move-selection-to-back", function() { reorderSelection('back') }) | ||||
|  | ||||
|  | ||||
|             RED.actions.add("core:step-selection-up", function() { moveSelection(0,-RED.view.gridSize());}); | ||||
|             RED.actions.add("core:step-selection-right", function() { moveSelection(RED.view.gridSize(),0);}); | ||||
|             RED.actions.add("core:step-selection-down", function() { moveSelection(0,RED.view.gridSize());}); | ||||
| @@ -474,6 +771,20 @@ RED.view.tools = (function() { | ||||
|             RED.actions.add("core:go-to-nearest-node-on-right", function() { gotoNearestNode('right')}) | ||||
|             RED.actions.add("core:go-to-nearest-node-above", function() { gotoNearestNode('up') }) | ||||
|             RED.actions.add("core:go-to-nearest-node-below", function() { gotoNearestNode('down') }) | ||||
|  | ||||
|             RED.actions.add("core:align-selection-to-grid", alignToGrid); | ||||
|             RED.actions.add("core:align-selection-to-left", function() { alignSelectionToEdge('left') }) | ||||
|             RED.actions.add("core:align-selection-to-right", function() { alignSelectionToEdge('right') }) | ||||
|             RED.actions.add("core:align-selection-to-top", function() { alignSelectionToEdge('top') }) | ||||
|             RED.actions.add("core:align-selection-to-bottom", function() { alignSelectionToEdge('bottom') }) | ||||
|             RED.actions.add("core:align-selection-to-middle", function() { alignSelectionToEdge('middle') }) | ||||
|             RED.actions.add("core:align-selection-to-center", function() { alignSelectionToEdge('center') }) | ||||
|  | ||||
|             RED.actions.add("core:distribute-selection-horizontally", function() { distributeSelection('h') }) | ||||
|             RED.actions.add("core:distribute-selection-vertically", function() { distributeSelection('v') }) | ||||
|  | ||||
|  | ||||
|  | ||||
|             // RED.actions.add("core:add-node", function() { addNode() }) | ||||
|         }, | ||||
|         /** | ||||
|   | ||||
| @@ -501,6 +501,28 @@ RED.view = (function() { | ||||
|         RED.actions.add("core:copy-selection-to-internal-clipboard",copySelection); | ||||
|         RED.actions.add("core:cut-selection-to-internal-clipboard",function(){copySelection();deleteSelection();}); | ||||
|         RED.actions.add("core:paste-from-internal-clipboard",function(){importNodes(clipboard,{generateIds: true});}); | ||||
|  | ||||
|         RED.events.on("view:selection-changed", function(selection) { | ||||
|             var hasSelection = (selection.nodes && selection.nodes.length > 0); | ||||
|             var hasMultipleSelection = hasSelection && selection.nodes.length > 1; | ||||
|             RED.menu.setDisabled("menu-item-edit-cut",!hasSelection); | ||||
|             RED.menu.setDisabled("menu-item-edit-copy",!hasSelection); | ||||
|             RED.menu.setDisabled("menu-item-edit-select-connected",!hasSelection); | ||||
|             RED.menu.setDisabled("menu-item-view-tools-move-to-back",!hasSelection); | ||||
|             RED.menu.setDisabled("menu-item-view-tools-move-to-front",!hasSelection); | ||||
|             RED.menu.setDisabled("menu-item-view-tools-move-backwards",!hasSelection); | ||||
|             RED.menu.setDisabled("menu-item-view-tools-move-forwards",!hasSelection); | ||||
|  | ||||
|             RED.menu.setDisabled("menu-item-view-tools-align-left",!hasMultipleSelection); | ||||
|             RED.menu.setDisabled("menu-item-view-tools-align-center",!hasMultipleSelection); | ||||
|             RED.menu.setDisabled("menu-item-view-tools-align-right",!hasMultipleSelection); | ||||
|             RED.menu.setDisabled("menu-item-view-tools-align-top",!hasMultipleSelection); | ||||
|             RED.menu.setDisabled("menu-item-view-tools-align-middle",!hasMultipleSelection); | ||||
|             RED.menu.setDisabled("menu-item-view-tools-align-bottom",!hasMultipleSelection); | ||||
|             RED.menu.setDisabled("menu-item-view-tools-distribute-horizontally",!hasMultipleSelection); | ||||
|             RED.menu.setDisabled("menu-item-view-tools-distribute-veritcally",!hasMultipleSelection); | ||||
|         }) | ||||
|  | ||||
|         RED.actions.add("core:delete-selection",deleteSelection); | ||||
|         RED.actions.add("core:edit-selected-node",editSelection); | ||||
|         RED.actions.add("core:go-to-selection",function() { | ||||
| @@ -655,14 +677,17 @@ RED.view = (function() { | ||||
|         var activeWorkspace = RED.workspaces.active(); | ||||
|  | ||||
|         activeNodes = RED.nodes.filterNodes({z:activeWorkspace}); | ||||
|  | ||||
|         activeNodes.forEach(function(n,i) { | ||||
|             n._index = i; | ||||
|         }) | ||||
|         activeLinks = RED.nodes.filterLinks({ | ||||
|             source:{z:activeWorkspace}, | ||||
|             target:{z:activeWorkspace} | ||||
|         }); | ||||
|  | ||||
|         activeGroups = RED.nodes.groups(activeWorkspace)||[]; | ||||
|         activeGroups.forEach(function(g) { | ||||
|         activeGroups.forEach(function(g,i) { | ||||
|             g._index = i; | ||||
|             if (g.g) { | ||||
|                 g._root = g.g; | ||||
|                 g._depth = 1; | ||||
| @@ -695,7 +720,8 @@ RED.view = (function() { | ||||
|             if (a._root === b._root) { | ||||
|                 return a._depth - b._depth; | ||||
|             } else { | ||||
|                 return a._root.localeCompare(b._root); | ||||
|                 // return a._root.localeCompare(b._root); | ||||
|                 return a._index - b._index; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
| @@ -704,7 +730,8 @@ RED.view = (function() { | ||||
|             if (a._root === b._root) { | ||||
|                 return a._depth - b._depth; | ||||
|             } else { | ||||
|                 return a._root.localeCompare(b._root); | ||||
|                 return a._index - b._index; | ||||
|                 // return a._root.localeCompare(b._root); | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| @@ -2332,6 +2359,7 @@ RED.view = (function() { | ||||
|                 } | ||||
|             } | ||||
|             clipboard = JSON.stringify(nns); | ||||
|             RED.menu.setDisabled("menu-item-edit-paste", false); | ||||
|             if (nodeCount > 0) { | ||||
|                 RED.notify(RED._("clipboard.nodeCopied",{count:nodeCount}),{id:"clipboard"}); | ||||
|             } else if (groupCount > 0) { | ||||
| @@ -3807,7 +3835,6 @@ RED.view = (function() { | ||||
|                 .attr("class", "red-ui-flow-node red-ui-flow-node-group") | ||||
|                 .classed("red-ui-flow-subflow", activeSubflow != null); | ||||
|  | ||||
|  | ||||
|             nodeEnter.each(function(d,i) { | ||||
|                 this.__outputs__ = []; | ||||
|                 this.__inputs__ = []; | ||||
| @@ -3953,7 +3980,12 @@ RED.view = (function() { | ||||
|                 RED.hooks.trigger("viewAddNode",{node:d,el:this}) | ||||
|             }); | ||||
|  | ||||
|             var nodesReordered = false; | ||||
|             node.each(function(d,i) { | ||||
|                 if (d._reordered) { | ||||
|                     nodesReordered = true; | ||||
|                     delete d._reordered; | ||||
|                 } | ||||
|                 if (d.dirty) { | ||||
|                     var self = this; | ||||
|                     var thisNode = d3.select(this); | ||||
| @@ -4261,6 +4293,13 @@ RED.view = (function() { | ||||
|  | ||||
|                 RED.hooks.trigger("viewRedrawNode",{node:d,el:this}) | ||||
|             }); | ||||
|  | ||||
|             if (nodesReordered) { | ||||
|                 node.sort(function(a,b) { | ||||
|                     return a._index - b._index; | ||||
|                 }) | ||||
|             } | ||||
|  | ||||
|             var link = linkLayer.selectAll(".red-ui-flow-link").data( | ||||
|                 activeLinks, | ||||
|                 function(d) { | ||||
| @@ -4499,7 +4538,7 @@ RED.view = (function() { | ||||
|                     if (a._root === b._root) { | ||||
|                         return a._depth - b._depth; | ||||
|                     } else { | ||||
|                         return a._root.localeCompare(b._root); | ||||
|                         return a._index - b._index; | ||||
|                     } | ||||
|                 }) | ||||
|             } | ||||
|   | ||||
| @@ -23,6 +23,9 @@ RED.workspaces = (function() { | ||||
|     var viewStack = []; | ||||
|     var viewStackPos = 0; | ||||
|  | ||||
|     function isSameObj(env0, env1) { | ||||
|         return (JSON.stringify(env0) === JSON.stringify(env1)); | ||||
|     } | ||||
|  | ||||
|     function addToViewStack(id) { | ||||
|         if (viewStackPos !== viewStack.length) { | ||||
| @@ -43,7 +46,7 @@ RED.workspaces = (function() { | ||||
|                 workspaceIndex += 1; | ||||
|             } while ($("#red-ui-workspace-tabs a[title='"+RED._('workspace.defaultName',{number:workspaceIndex})+"']").size() !== 0); | ||||
|  | ||||
|             ws = {type:"tab",id:tabId,disabled: false,info:"",label:RED._('workspace.defaultName',{number:workspaceIndex})}; | ||||
|             ws = {type:"tab",id:tabId,disabled: false,info:"",label:RED._('workspace.defaultName',{number:workspaceIndex}),env:[]}; | ||||
|             RED.nodes.addWorkspace(ws,targetIndex); | ||||
|             workspace_tabs.addTab(ws,targetIndex); | ||||
|             workspace_tabs.activateTab(tabId); | ||||
| @@ -55,6 +58,7 @@ RED.workspaces = (function() { | ||||
|         RED.view.focus(); | ||||
|         return ws; | ||||
|     } | ||||
|  | ||||
|     function deleteWorkspace(ws) { | ||||
|         if (workspaceTabCount === 1) { | ||||
|             return; | ||||
| @@ -78,165 +82,9 @@ RED.workspaces = (function() { | ||||
|             if (subflow) { | ||||
|                 RED.editor.editSubflow(subflow); | ||||
|             } | ||||
|             return; | ||||
|         } else { | ||||
|             RED.editor.editFlow(workspace); | ||||
|         } | ||||
|         RED.view.state(RED.state.EDITING); | ||||
|         var tabflowEditor; | ||||
|         var trayOptions = { | ||||
|             title: RED._("workspace.editFlow",{name:RED.utils.sanitize(workspace.label)}), | ||||
|             buttons: [ | ||||
|                 { | ||||
|                     id: "node-dialog-delete", | ||||
|                     class: 'leftButton'+((workspaceTabCount === 1)?" disabled":""), | ||||
|                     text: RED._("common.label.delete"), //'<i class="fa fa-trash"></i>', | ||||
|                     click: function() { | ||||
|                         deleteWorkspace(workspace); | ||||
|                         RED.tray.close(); | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     id: "node-dialog-cancel", | ||||
|                     text: RED._("common.label.cancel"), | ||||
|                     click: function() { | ||||
|                         RED.tray.close(); | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     id: "node-dialog-ok", | ||||
|                     class: "primary", | ||||
|                     text: RED._("common.label.done"), | ||||
|                     click: function() { | ||||
|                         var label = $( "#node-input-name" ).val(); | ||||
|                         var changed = false; | ||||
|                         var changes = {}; | ||||
|                         if (workspace.label != label) { | ||||
|                             changes.label = workspace.label; | ||||
|                             changed = true; | ||||
|                             workspace.label = label; | ||||
|                             workspace_tabs.renameTab(workspace.id,label); | ||||
|                         } | ||||
|                         var disabled = $("#node-input-disabled").prop("checked"); | ||||
|                         if (workspace.disabled !== disabled) { | ||||
|                             changes.disabled = workspace.disabled; | ||||
|                             changed = true; | ||||
|                             workspace.disabled = disabled; | ||||
|                         } | ||||
|                         var info = tabflowEditor.getValue(); | ||||
|                         if (workspace.info !== info) { | ||||
|                             changes.info = workspace.info; | ||||
|                             changed = true; | ||||
|                             workspace.info = info; | ||||
|                         } | ||||
|                         $("#red-ui-tab-"+(workspace.id.replace(".","-"))).toggleClass('red-ui-workspace-disabled',!!workspace.disabled); | ||||
|                         $("#red-ui-workspace").toggleClass("red-ui-workspace-disabled",!!workspace.disabled); | ||||
|  | ||||
|                         if (changed) { | ||||
|                             var historyEvent = { | ||||
|                                 t: "edit", | ||||
|                                 changes:changes, | ||||
|                                 node: workspace, | ||||
|                                 dirty: RED.nodes.dirty() | ||||
|                             } | ||||
|                             workspace.changed = true; | ||||
|                             RED.history.push(historyEvent); | ||||
|                             RED.nodes.dirty(true); | ||||
|                             RED.sidebar.config.refresh(); | ||||
|                             if (changes.hasOwnProperty('disabled')) { | ||||
|                                 RED.nodes.eachNode(function(n) { | ||||
|                                     if (n.z === workspace.id) { | ||||
|                                         n.dirty = true; | ||||
|                                     } | ||||
|                                 }); | ||||
|                                 RED.view.redraw(); | ||||
|                             } | ||||
|                             RED.events.emit("flows:change",workspace); | ||||
|                         } | ||||
|                         RED.tray.close(); | ||||
|                     } | ||||
|                 } | ||||
|             ], | ||||
|             resize: function(dimensions) { | ||||
|                 var rows = $("#dialog-form>div:not(.node-text-editor-row)"); | ||||
|                 var editorRow = $("#dialog-form>div.node-text-editor-row"); | ||||
|                 var height = $("#dialog-form").height(); | ||||
|                 for (var i=0; i<rows.size(); i++) { | ||||
|                     height -= $(rows[i]).outerHeight(true); | ||||
|                 } | ||||
|                 height -= (parseInt($("#dialog-form").css("marginTop"))+parseInt($("#dialog-form").css("marginBottom"))); | ||||
|                 $(".node-text-editor").css("height",height+"px"); | ||||
|                 tabflowEditor.resize(); | ||||
|             }, | ||||
|             open: function(tray) { | ||||
|                 var trayFooter = tray.find(".red-ui-tray-footer"); | ||||
|                 var trayBody = tray.find('.red-ui-tray-body'); | ||||
|                 var trayFooterLeft = $('<div class="red-ui-tray-footer-left"></div>').appendTo(trayFooter) | ||||
|  | ||||
|                 var dialogForm = $('<form id="dialog-form" class="form-horizontal"></form>').appendTo(trayBody); | ||||
|                 $('<div class="form-row">'+ | ||||
|                     '<label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label>'+ | ||||
|                     '<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">'+ | ||||
|                 '</div>').appendTo(dialogForm); | ||||
|  | ||||
|  | ||||
|                 if (!workspace.hasOwnProperty("disabled")) { | ||||
|                     workspace.disabled = false; | ||||
|                 } | ||||
|  | ||||
|                 $('<input id="node-input-disabled" type="checkbox">').prop("checked",workspace.disabled).appendTo(trayFooterLeft).toggleButton({ | ||||
|                     enabledIcon: "fa-circle-thin", | ||||
|                     disabledIcon: "fa-ban", | ||||
|                     invertState: true | ||||
|                 }) | ||||
|  | ||||
|  | ||||
|                 var row = $('<div class="form-row node-text-editor-row">'+ | ||||
|                     '<label for="node-input-info" data-i18n="editor:workspace.info" style="width:300px;"></label>'+ | ||||
|                     '<div style="min-height:250px;" class="node-text-editor" id="node-input-info"></div>'+ | ||||
|                 '</div>').appendTo(dialogForm); | ||||
|                 tabflowEditor = RED.editor.createEditor({ | ||||
|                     id: 'node-input-info', | ||||
|                     mode: 'ace/mode/markdown', | ||||
|                     value: "" | ||||
|                 }); | ||||
|  | ||||
|                 $('#node-info-input-info-expand').on("click", function(e) { | ||||
|                     e.preventDefault(); | ||||
|                     var value = tabflowEditor.getValue(); | ||||
|                     RED.editor.editMarkdown({ | ||||
|                         value: value, | ||||
|                         width: "Infinity", | ||||
|                         cursor: tabflowEditor.getCursorPosition(), | ||||
|                         complete: function(v,cursor) { | ||||
|                             tabflowEditor.setValue(v, -1); | ||||
|                             tabflowEditor.gotoLine(cursor.row+1,cursor.column,false); | ||||
|                             setTimeout(function() { | ||||
|                                 tabflowEditor.focus(); | ||||
|                             },300); | ||||
|                         } | ||||
|                     }) | ||||
|                 }); | ||||
|  | ||||
|  | ||||
|  | ||||
|                 $('<input type="text" style="display: none;" />').prependTo(dialogForm); | ||||
|                 dialogForm.on("submit", function(e) { e.preventDefault();}); | ||||
|                 $("#node-input-name").val(workspace.label); | ||||
|                 RED.text.bidi.prepareInput($("#node-input-name")); | ||||
|                 tabflowEditor.getSession().setValue(workspace.info || "", -1); | ||||
|                 dialogForm.i18n(); | ||||
|             }, | ||||
|             close: function() { | ||||
|                 if (RED.view.state() != RED.state.IMPORT_DRAGGING) { | ||||
|                     RED.view.state(RED.state.DEFAULT); | ||||
|                 } | ||||
|                 var selection = RED.view.selection(); | ||||
|                 if (!selection.nodes && !selection.links && workspace.id === activeWorkspace) { | ||||
|                     RED.sidebar.info.refresh(workspace); | ||||
|                 } | ||||
|                 tabflowEditor.destroy(); | ||||
|             } | ||||
|         } | ||||
|         RED.tray.show(trayOptions); | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -293,7 +141,14 @@ RED.workspaces = (function() { | ||||
|                 } | ||||
|             }, | ||||
|             onreorder: function(oldOrder, newOrder) { | ||||
|                 RED.history.push({t:'reorder',order:oldOrder,dirty:RED.nodes.dirty()}); | ||||
|                 RED.history.push({ | ||||
|                     t:'reorder', | ||||
|                     workspaces: { | ||||
|                         from:oldOrder, | ||||
|                         to:newOrder | ||||
|                     }, | ||||
|                     dirty:RED.nodes.dirty() | ||||
|                 }); | ||||
|                 RED.nodes.dirty(true); | ||||
|                 setWorkspaceOrder(newOrder); | ||||
|             }, | ||||
| @@ -445,7 +300,6 @@ RED.workspaces = (function() { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     function removeWorkspace(ws) { | ||||
|         if (!ws) { | ||||
|             deleteWorkspace(RED.nodes.workspace(activeWorkspace)); | ||||
| @@ -474,7 +328,10 @@ RED.workspaces = (function() { | ||||
|     return { | ||||
|         init: init, | ||||
|         add: addWorkspace, | ||||
|         // remove: remove workspace without editor history etc | ||||
|         remove: removeWorkspace, | ||||
|         // delete: remove workspace and update editor history | ||||
|         delete: deleteWorkspace, | ||||
|         order: setWorkspaceOrder, | ||||
|         edit: editWorkspace, | ||||
|         contains: function(id) { | ||||
|   | ||||
| @@ -68,6 +68,10 @@ | ||||
|     & > .disabled > a:hover, | ||||
|     & > .disabled > a:focus { | ||||
|         color: $menuDisabledColor; | ||||
|         .red-ui-popover-key { | ||||
|             color: $menuDisabledColor; | ||||
|             border-color: $menuDisabledColor; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     & > .disabled > a:hover, | ||||
| @@ -102,6 +106,14 @@ | ||||
|                 display: none; | ||||
|             } | ||||
|         } | ||||
|         .red-ui-popover-key { | ||||
|             border: none; | ||||
|             padding: 0; | ||||
|             font-size: 13px; | ||||
|             float: right; | ||||
|             color: $menuColor; | ||||
|             border-color: $menuColor; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -209,4 +221,4 @@ ul.red-ui-menu:not(.red-ui-menu-dropdown) { | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -191,14 +191,17 @@ | ||||
|         margin-top: 0; | ||||
|         li a { | ||||
|             color: $header-menu-color; | ||||
|             padding: 3px 40px; | ||||
|             padding: 3px 10px 3px 40px; | ||||
|             img { | ||||
|                 max-width: 100%; | ||||
|                 margin-right: 10px; | ||||
|                 padding: 4px; | ||||
|                 border: 3px solid transparent; | ||||
|             } | ||||
|  | ||||
|             .red-ui-popover-key { | ||||
|                 color: $header-menu-color-disabled !important; | ||||
|                 border-color: $header-menu-color-disabled !important; | ||||
|             } | ||||
|             &.active img { | ||||
|                 border: 3px solid $header-menu-item-border-active; | ||||
|             } | ||||
|   | ||||
| @@ -104,7 +104,7 @@ | ||||
|                 } | ||||
|  | ||||
|                 .red-ui-tabs-fade { | ||||
|                     background-image: linear-gradient(to right, rgba(255,255,255,0.001), $tab-background-active); | ||||
|                     background-image: linear-gradient(to right, change-color($tab-background-active, $alpha: 0.001), $tab-background-active); | ||||
|                 } | ||||
|  | ||||
|             } | ||||
| @@ -112,7 +112,7 @@ | ||||
|                 &:not(.active) { | ||||
|                     background: $tab-background-selected; | ||||
|                     .red-ui-tabs-fade { | ||||
|                         background-image: linear-gradient(to right, rgba(255,255,255,0.001), $tab-background-selected); | ||||
|                         background-image: linear-gradient(to right, change-color($tab-background-selected, $alpha: 0.001), $tab-background-selected); | ||||
|                     } | ||||
|                     .red-ui-tabs-badge-selected { | ||||
|                         background: $tab-background-selected; | ||||
| @@ -131,6 +131,9 @@ | ||||
|             &:not(.active) a:hover { | ||||
|                 color: $workspace-button-color-hover; | ||||
|                 background: $tab-background-hover; | ||||
|                 &+.red-ui-tabs-fade { | ||||
|                     background-image: linear-gradient(to right, change-color($tab-background-hover, $alpha: 0.001), $tab-background-hover); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -308,7 +311,7 @@ | ||||
|     top: 0; | ||||
|     right: 0; | ||||
|     width: 15px; | ||||
|     background-image: linear-gradient(to right, rgba(255,255,255,0.001), $tab-background-inactive); | ||||
|     background-image: linear-gradient(to right, change-color($tab-background-inactive, $alpha: 0.001), $tab-background-inactive); | ||||
|     pointer-events: none; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -101,6 +101,9 @@ | ||||
| } | ||||
| .red-ui-treeList-label-text { | ||||
|     margin-left: 4px; | ||||
|     &:empty { | ||||
|         min-height: 20px; | ||||
|     } | ||||
| } | ||||
| .red-ui-treeList-sublabel-text { | ||||
|     top: 0; | ||||
|   | ||||
| @@ -23,7 +23,7 @@ | ||||
| --> | ||||
| <title>{{ page.title }}</title> | ||||
| <link rel="icon" type="image/png" href="{{ page.favicon }}"> | ||||
| <link rel="mask-icon" href="{{ page.tabicon }}" color="#8f0000"> | ||||
| <link rel="mask-icon" href="{{ page.tabicon.icon }}" color="{{ page.tabicon.colour }}"> | ||||
| <link rel="stylesheet" href="vendor/jquery/css/base/jquery-ui.min.css"> | ||||
| <link rel="stylesheet" href="vendor/font-awesome/css/font-awesome.min.css"> | ||||
| <link rel="stylesheet" href="red/style.min.css"> | ||||
|   | ||||
| @@ -193,7 +193,7 @@ | ||||
|     } | ||||
|     /** Perform inject, optionally sending a custom msg (refactored for re-use in the form inject button)*/ | ||||
|     function doInject(node, customMsg) { | ||||
|         var label = node._def.label.call(node); | ||||
|         var label = node._def.label.call(node,customMsg?customMsg.__user_inject_props__:undefined); | ||||
|         if (label.length > 30) { | ||||
|             label = label.substring(0, 50) + "..."; | ||||
|         } | ||||
| @@ -201,7 +201,8 @@ | ||||
|         $.ajax({ | ||||
|             url: "inject/" + node.id, | ||||
|             type: "POST", | ||||
|             data: customMsg, | ||||
|             data: JSON.stringify(customMsg||{}), | ||||
|             contentType: "application/json; charset=utf-8", | ||||
|             success: function (resp) { | ||||
|                 RED.notify(node._("inject.success", { label: label }), { type: "success", id: "inject", timeout: 2000 }); | ||||
|             }, | ||||
| @@ -291,7 +292,7 @@ | ||||
|             } | ||||
|             return lab; | ||||
|         }, | ||||
|         label: function() { | ||||
|         label: function(customProps) { | ||||
|             var suffix = ""; | ||||
|             // if fire once then add small indication | ||||
|             if (this.once) { | ||||
| @@ -304,11 +305,23 @@ | ||||
|             if (this.name) { | ||||
|                 return this.name+suffix; | ||||
|             } | ||||
|  | ||||
|             var payload = this.payload || ""; | ||||
|             var payloadType = this.payloadType || "str"; | ||||
|             var topic = this.topic || ""; | ||||
|  | ||||
|             var payload = ""; | ||||
|             var payloadType = "str"; | ||||
|             var topic = ""; | ||||
|             if (customProps) { | ||||
|                 for (var i=0;i<customProps.length;i++) { | ||||
|                     if (customProps[i].p === "payload") { | ||||
|                         payload = customProps[i].v; | ||||
|                         payloadType = customProps[i].vt; | ||||
|                     } else if (customProps[i].p === "topic") { | ||||
|                         topic = customProps[i].v; | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 payload = this.payload || ""; | ||||
|                 payloadType = this.payloadType || "str"; | ||||
|                 topic = this.topic || ""; | ||||
|             } | ||||
|             if (payloadType === "string" || | ||||
|                     payloadType === "str" || | ||||
|                     payloadType === "num" || | ||||
| @@ -496,11 +509,8 @@ | ||||
|                         label: node._("inject.injectNow"), | ||||
|                         click: function(e) { | ||||
|                             var items = eList.editableList('items'); | ||||
|                             var result = getProps(items); | ||||
|                             var m = {__user_inject_props__: []}; | ||||
|                             if (result && result.props && result.props.length) { | ||||
|                                 m.__user_inject_props__ = result.props; | ||||
|                             } | ||||
|                             var props = getProps(items); | ||||
|                             var m = {__user_inject_props__: props.props}; | ||||
|                             doInject(node, m); | ||||
|                         } | ||||
|                     } | ||||
|   | ||||
| @@ -101,7 +101,7 @@ module.exports = function(RED) { | ||||
|         this.on("input", function(msg, send, done) { | ||||
|             var errors = []; | ||||
|             var props = this.props; | ||||
|             if(msg.__user_inject_props__ && Array.isArray(msg.__user_inject_props__)) { | ||||
|             if (msg.__user_inject_props__ && Array.isArray(msg.__user_inject_props__)) { | ||||
|                 props = msg.__user_inject_props__; | ||||
|             } | ||||
|             delete msg.__user_inject_props__; | ||||
|   | ||||
| @@ -27,10 +27,14 @@ | ||||
|         <label for="node-input-property"><i class="fa fa-ellipsis-h"></i> <span data-i18n="node-red:common.label.property"></span></label> | ||||
|         <input type="text" id="node-input-property" style="width:70%;"/> | ||||
|     </div> | ||||
|     <div class="form-row"> | ||||
|     <div class="form-row" style="margin-bottom: 0px;"> | ||||
|         <label> </label> | ||||
|         <input type="checkbox" id="node-input-septopics" style="display:inline-block; width:20px; vertical-align:baseline;"> | ||||
|         <span data-i18n="rbe.label.septopics"></span> <input type="text" id="node-input-topi" style="width:27%;"/> | ||||
|         <label style="width: auto" for="node-input-septopics" data-i18n="rbe.label.septopics"></label> | ||||
|     </div> | ||||
|     <div class="form-row"> | ||||
|         <label> </label> | ||||
|         <input type="text" id="node-input-topi"  style="width:70%;"/> | ||||
|     </div> | ||||
|     <div class="form-row"> | ||||
|         <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="rbe.label.name"></span></label> | ||||
| @@ -70,6 +74,10 @@ | ||||
|             if (this.septopics === undefined) { | ||||
|                 $("#node-input-septopics").prop('checked', true); | ||||
|             } | ||||
|             if (this.topi === undefined) { | ||||
|                 $("#node-input-topi").val("topic"); | ||||
|             } | ||||
|  | ||||
|             $("#node-input-property").typedInput({default:'msg',types:['msg']}); | ||||
|             $("#node-input-topi").typedInput({default:'msg',types:['msg']}); | ||||
|             //$( "#node-input-gap" ).spinner({min:0}); | ||||
| @@ -88,6 +96,11 @@ | ||||
|                     $("#node-startvalue").hide(); | ||||
|                 } | ||||
|             }); | ||||
|             $("#node-input-septopics").on("change", function() { | ||||
|                 $("#node-input-topi").typedInput("disable",!this.checked); | ||||
|             }) | ||||
|             $("#node-input-topi").typedInput("disable",!!!this.septopics); | ||||
|  | ||||
|         } | ||||
|     }); | ||||
| </script> | ||||
|   | ||||
| @@ -134,7 +134,29 @@ in your Node-RED user directory (${RED.settings.userDir}). | ||||
|                     url = "http://"+url; | ||||
|                 } | ||||
|             } | ||||
|             url = encodeURI(url); | ||||
|  | ||||
|             // The Request module used in Node-RED 1.x was tolerant of query strings that | ||||
|             // were partially encoded. For example - "?a=hello%20there&b=20%" | ||||
|             // The GOT module doesn't like that. | ||||
|             // The following is an attempt to normalise the url to ensure it is properly | ||||
|             // encoded. We cannot just encode it directly as we don't want any valid | ||||
|             // encoded entity to end up doubly encoded. | ||||
|             if (url.indexOf("?") > -1) { | ||||
|                 // Only do this if there is a query string to deal with | ||||
|                 const [hostPath, ...queryString] = url.split("?") | ||||
|                 const query = queryString.join("?"); | ||||
|                 if (query) { | ||||
|                     // Look for any instance of % not followed by two hex chars. | ||||
|                     // Replace any we find with %25. | ||||
|                     const escapedQueryString = query.replace(/(%.?.?)/g, function(v) { | ||||
|                         if (/^%[a-f0-9]{2}/i.test(v)) { | ||||
|                             return v; | ||||
|                         } | ||||
|                         return v.replace(/%/,"%25") | ||||
|                     }) | ||||
|                     url = hostPath+"?"+escapedQueryString; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             var method = nodeMethod.toUpperCase() || "GET"; | ||||
|             if (msg.method && n.method && (n.method !== "use")) {     // warn if override option not set | ||||
| @@ -449,6 +471,14 @@ in your Node-RED user directory (${RED.settings.userDir}). | ||||
|             if (tlsNode) { | ||||
|                 opts.https = {}; | ||||
|                 tlsNode.addTLSOptions(opts.https); | ||||
|                 if (opts.https.ca) { | ||||
|                     opts.https.certificateAuthority = opts.https.ca; | ||||
|                     delete opts.https.ca; | ||||
|                 } | ||||
|                 if (opts.https.cert) { | ||||
|                     opts.https.certificate = opts.https.cert; | ||||
|                     delete opts.https.cert; | ||||
|                 } | ||||
|             } else { | ||||
|                 if (msg.hasOwnProperty('rejectUnauthorized')) { | ||||
|                     opts.https = { rejectUnauthorized: msg.rejectUnauthorized }; | ||||
|   | ||||
| @@ -629,9 +629,6 @@ module.exports = function(RED) { | ||||
|                     if (node.build === 'object') { | ||||
|                         propertyKey = RED.util.getMessageProperty(msg,node.key); | ||||
|                     } | ||||
|                     if (msg.hasOwnProperty("parts")) { | ||||
|                         propertyIndex = msg.parts.index; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (msg.hasOwnProperty("reset")) { | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@node-red/nodes", | ||||
|     "version": "2.0.3", | ||||
|     "version": "2.0.6", | ||||
|     "license": "Apache-2.0", | ||||
|     "repository": { | ||||
|         "type": "git", | ||||
| @@ -17,7 +17,7 @@ | ||||
|     "dependencies": { | ||||
|         "acorn": "8.4.1", | ||||
|         "acorn-walk": "8.1.1", | ||||
|         "ajv": "8.6.0", | ||||
|         "ajv": "8.6.2", | ||||
|         "body-parser": "1.19.0", | ||||
|         "cheerio": "1.0.0-rc.10", | ||||
|         "content-type": "1.0.4", | ||||
| @@ -37,7 +37,7 @@ | ||||
|         "js-yaml": "3.14.1", | ||||
|         "media-typer": "1.1.0", | ||||
|         "mqtt": "4.2.8", | ||||
|         "multer": "1.4.2", | ||||
|         "multer": "1.4.3", | ||||
|         "mustache": "4.2.0", | ||||
|         "on-headers": "1.0.2", | ||||
|         "raw-body": "2.4.1", | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@node-red/registry", | ||||
|     "version": "2.0.3", | ||||
|     "version": "2.0.6", | ||||
|     "license": "Apache-2.0", | ||||
|     "main": "./lib/index.js", | ||||
|     "repository": { | ||||
| @@ -16,11 +16,11 @@ | ||||
|         } | ||||
|     ], | ||||
|     "dependencies": { | ||||
|         "@node-red/util": "2.0.3", | ||||
|         "@node-red/util": "2.0.6", | ||||
|         "clone": "2.1.2", | ||||
|         "fs-extra": "10.0.0", | ||||
|         "semver": "7.3.5", | ||||
|         "tar": "6.1.0", | ||||
|         "uglify-js": "3.13.10" | ||||
|         "tar": "6.1.11", | ||||
|         "uglify-js": "3.14.1" | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -233,7 +233,9 @@ var api = module.exports = { | ||||
|         } | ||||
|         var sendCredentials = {}; | ||||
|         var cred; | ||||
|         if (/^subflow(:|$)/.test(opts.type)) { | ||||
|         if (/^subflow(:|$)/.test(opts.type) || | ||||
|             (opts.type === "tab") || | ||||
|             (opts.type === "group")) { | ||||
|             for (cred in credentials) { | ||||
|                 if (credentials.hasOwnProperty(cred)) { | ||||
|                     sendCredentials['has_'+cred] = credentials[cred] != null && credentials[cred] !== ''; | ||||
|   | ||||
| @@ -20,6 +20,7 @@ const events = require("@node-red/util").events; | ||||
| var flowUtil = require("./util"); | ||||
| const context = require('../nodes/context'); | ||||
| const hooks = require("@node-red/util").hooks; | ||||
| const credentials = require("../nodes/credentials"); | ||||
|  | ||||
| var Subflow; | ||||
| var Log; | ||||
| @@ -396,6 +397,17 @@ class Flow { | ||||
|         return undefined; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Get a group node instance | ||||
|      * @param  {String} id | ||||
|      * @return {Node}   group node | ||||
|      */ | ||||
|     getGroupNode(id) { | ||||
|         const groups = this.global.groups; | ||||
|         return groups[id]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get all of the nodes instantiated within this flow | ||||
|      * @return {[type]} [description] | ||||
| @@ -404,6 +416,82 @@ class Flow { | ||||
|         return this.activeNodes; | ||||
|     } | ||||
|  | ||||
|     /*! | ||||
|      * Get value of environment variable defined in group node. | ||||
|      * @param {String} group - group node | ||||
|      * @param {String} name - name of variable | ||||
|      * @return {Object} object containing the value in val property or null if not defined | ||||
|      */ | ||||
|     getGroupEnvSetting(node, group, name) { | ||||
|         if (group) { | ||||
|             if (group.credentials === undefined) { | ||||
|                 group.credentials = credentials.get(group.id) || {}; | ||||
|             } | ||||
|             if (!name.startsWith("$parent.")) { | ||||
|                 if (group.env) { | ||||
|                     if (!group._env) { | ||||
|                         const envs = group.env; | ||||
|                         const entries = envs.map((env) => { | ||||
|                             if (env.type === "cred") { | ||||
|                                 const cred = group.credentials; | ||||
|                                 if (cred.hasOwnProperty(env.name)) { | ||||
|                                     env.value = cred[env.name]; | ||||
|                                 } | ||||
|                             } | ||||
|                             return [env.name, env]; | ||||
|                              | ||||
|                             return [env.name, env]; | ||||
|                         }); | ||||
|                         group._env = Object.fromEntries(entries); | ||||
|                     } | ||||
|                     const env = group._env[name]; | ||||
|                     if (env) { | ||||
|                         let value = env.value; | ||||
|                         const type = env.type; | ||||
|                         if ((type !== "env") || | ||||
|                             (value !== name)) { | ||||
|                             if (type === "env") { | ||||
|                                 value = value.replace(new RegExp("\\${"+name+"}","g"),"${$parent."+name+"}"); | ||||
|                             } | ||||
|                             if (type === "bool") { | ||||
|                                 const val | ||||
|                                       = ((value === "true") || | ||||
|                                          (value === true)); | ||||
|                                 return { | ||||
|                                     val: val | ||||
|                                 }; | ||||
|                             } | ||||
|                             if (type === "cred") { | ||||
|                                 return { | ||||
|                                     val: value | ||||
|                                 }; | ||||
|                             } | ||||
|                             try { | ||||
|                                 var val = redUtil.evaluateNodeProperty(value, type, node, null, null); | ||||
|                                 return { | ||||
|                                     val: val | ||||
|                                 }; | ||||
|                             } | ||||
|                             catch (e) { | ||||
|                                 this.error(e); | ||||
|                                 return null; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             else { | ||||
|                 name = name.substring(8); | ||||
|             } | ||||
|             if (group.g) { | ||||
|                 const parent = this.getGroupNode(group.g); | ||||
|                 return this.getGroupEnvSetting(node, parent, name); | ||||
|             } | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Get a flow setting value. This currently automatically defers to the parent | ||||
|      * flow which, as defined in ./index.js returns `process.env[key]`. | ||||
| @@ -412,6 +500,52 @@ class Flow { | ||||
|      * @return {[type]}     [description] | ||||
|      */ | ||||
|     getSetting(key) { | ||||
|         const flow = this.flow;  | ||||
|         if (flow.credentials === undefined) { | ||||
|             flow.credentials = credentials.get(flow.id) || {}; | ||||
|         } | ||||
|         if (flow.env) { | ||||
|             if (!key.startsWith("$parent.")) { | ||||
|                 if (!flow._env) { | ||||
|                     const envs = flow.env; | ||||
|                     const entries = envs.map((env) => { | ||||
|                         if (env.type === "cred") { | ||||
|                             const cred = flow.credentials; | ||||
|                             if (cred.hasOwnProperty(env.name)) { | ||||
|                                 env.value = cred[env.name]; | ||||
|                             } | ||||
|                         } | ||||
|                         return [env.name, env] | ||||
|                     }); | ||||
|                     flow._env = Object.fromEntries(entries); | ||||
|                 } | ||||
|                 const env = flow._env[key]; | ||||
|                 if (env) { | ||||
|                     let value = env.value; | ||||
|                     const type = env.type; | ||||
|                     if ((type !== "env") || (value !== key)) { | ||||
|                         if (type === "env") { | ||||
|                             value = value.replace(new RegExp("\\${"+key+"}","g"),"${$parent."+key+"}"); | ||||
|                         } | ||||
|                         try { | ||||
|                             if (type === "bool") { | ||||
|                                 const val = ((value === "true") || | ||||
|                                              (value === true)); | ||||
|                                 return val; | ||||
|                             } | ||||
|                             if (type === "cred") { | ||||
|                                 return value; | ||||
|                             } | ||||
|                             var val = redUtil.evaluateNodeProperty(value, type, null, null, null); | ||||
|                             return val; | ||||
|                         } | ||||
|                         catch (e) { | ||||
|                             this.error(e); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return this.parent.getSetting(key); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -370,6 +370,16 @@ class Subflow extends Flow { | ||||
|             // name starts $parent. ... so delegate to parent automatically | ||||
|             name = name.substring(8); | ||||
|         } | ||||
|         const node = this.subflowInstance; | ||||
|         if (node.g) { | ||||
|             const group = this.getGroupNode(node.g); | ||||
|             const result = this.getGroupEnvSetting(node, group, name); | ||||
|             if (result) { | ||||
|                 return result.val; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|  | ||||
|         var parent = this.parent; | ||||
|         if (parent) { | ||||
|             var val = parent.getSetting(name); | ||||
|   | ||||
| @@ -539,6 +539,9 @@ async function addFlow(flow, user) { | ||||
|     if (flow.hasOwnProperty('disabled')) { | ||||
|         tabNode.disabled = flow.disabled; | ||||
|     } | ||||
|     if (flow.hasOwnProperty('env')) { | ||||
|         tabNode.env = flow.env; | ||||
|     } | ||||
|  | ||||
|     var nodes = [tabNode]; | ||||
|  | ||||
| @@ -599,6 +602,9 @@ function getFlow(id) { | ||||
|     if (flow.hasOwnProperty('info')) { | ||||
|         result.info = flow.info; | ||||
|     } | ||||
|     if (flow.hasOwnProperty('env')) { | ||||
|         result.env = flow.env; | ||||
|     } | ||||
|     if (id !== 'global') { | ||||
|         result.nodes = []; | ||||
|     } | ||||
| @@ -694,6 +700,12 @@ async function updateFlow(id,newFlow, user) { | ||||
|         if (newFlow.hasOwnProperty('disabled')) { | ||||
|             tabNode.disabled = newFlow.disabled; | ||||
|         } | ||||
|         if (newFlow.hasOwnProperty('env')) { | ||||
|             tabNode.env = newFlow.env; | ||||
|         } | ||||
|         if (newFlow.hasOwnProperty('credentials')) { | ||||
|             tabNode.credentials = newFlow.credentials; | ||||
|         } | ||||
|  | ||||
|         nodes = [tabNode].concat(newFlow.nodes||[]).concat(newFlow.configs||[]); | ||||
|         nodes.forEach(function(n) { | ||||
|   | ||||
| @@ -44,24 +44,25 @@ function diffNodes(oldNode,newNode) { | ||||
| var EnvVarPropertyRE_old = /^\$\((\S+)\)$/; | ||||
| var EnvVarPropertyRE = /^\${(\S+)}$/; | ||||
|  | ||||
| function mapEnvVarProperties(obj,prop,flow) { | ||||
|  | ||||
| function mapEnvVarProperties(obj,prop,flow,config) { | ||||
|     var v = obj[prop]; | ||||
|     if (Buffer.isBuffer(v)) { | ||||
|         return; | ||||
|     } else if (Array.isArray(v)) { | ||||
|         for (var i=0;i<v.length;i++) { | ||||
|             mapEnvVarProperties(v,i,flow); | ||||
|             mapEnvVarProperties(v,i,flow,config); | ||||
|         } | ||||
|     } else if (typeof obj[prop] === 'string') { | ||||
|         if (obj[prop][0] === "$" && (EnvVarPropertyRE_old.test(v) || EnvVarPropertyRE.test(v)) ) { | ||||
|             var envVar = v.substring(2,v.length-1); | ||||
|             var r = flow.getSetting(envVar); | ||||
|             obj[prop] = r!==undefined?r:obj[prop]; | ||||
|             var r = redUtil.getSetting(config, envVar, flow); | ||||
|             obj[prop] = r ? r : obj[prop]; | ||||
|         } | ||||
|     } else { | ||||
|         for (var p in v) { | ||||
|             if (v.hasOwnProperty(p)) { | ||||
|                 mapEnvVarProperties(v,p,flow); | ||||
|                 mapEnvVarProperties(v,p,flow,config); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -78,7 +79,7 @@ function createNode(flow,config) { | ||||
|             delete conf.credentials; | ||||
|             for (var p in conf) { | ||||
|                 if (conf.hasOwnProperty(p)) { | ||||
|                     mapEnvVarProperties(conf,p,flow); | ||||
|                     mapEnvVarProperties(conf,p,flow,conf); | ||||
|                 } | ||||
|             } | ||||
|             try { | ||||
|   | ||||
| @@ -38,6 +38,7 @@ function Node(n) { | ||||
|     this.id = n.id; | ||||
|     this.type = n.type; | ||||
|     this.z = n.z; | ||||
|     this.g = n.g; | ||||
|     this._closeCallbacks = []; | ||||
|     this._inputCallback = null; | ||||
|     this._inputCallbacks = null; | ||||
|   | ||||
| @@ -103,7 +103,7 @@ function createNode(node,def) { | ||||
|         // allow $(foo) syntax to substitute env variables for credentials also... | ||||
|         for (var p in creds) { | ||||
|             if (creds.hasOwnProperty(p)) { | ||||
|                 flowUtil.mapEnvVarProperties(creds,p,node._flow); | ||||
|                 flowUtil.mapEnvVarProperties(creds,p,node._flow,node); | ||||
|             } | ||||
|         } | ||||
|         node.credentials = creds; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@node-red/runtime", | ||||
|     "version": "2.0.3", | ||||
|     "version": "2.0.6", | ||||
|     "license": "Apache-2.0", | ||||
|     "main": "./lib/index.js", | ||||
|     "repository": { | ||||
| @@ -16,9 +16,9 @@ | ||||
|         } | ||||
|     ], | ||||
|     "dependencies": { | ||||
|         "@node-red/registry": "2.0.3", | ||||
|         "@node-red/util": "2.0.3", | ||||
|         "async-mutex": "0.3.1", | ||||
|         "@node-red/registry": "2.0.6", | ||||
|         "@node-red/util": "2.0.6", | ||||
|         "async-mutex": "0.3.2", | ||||
|         "clone": "2.1.2", | ||||
|         "express": "4.17.1", | ||||
|         "fs-extra": "10.0.0", | ||||
|   | ||||
							
								
								
									
										27
									
								
								packages/node_modules/@node-red/util/lib/util.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										27
									
								
								packages/node_modules/@node-red/util/lib/util.js
									
									
									
									
										vendored
									
									
								
							| @@ -521,12 +521,17 @@ function setObjectProperty(msg,prop,value,createMissing) { | ||||
|  * @param {String} name - name of variable | ||||
|  * @return {String} value of env var | ||||
|  */ | ||||
| function getSetting(node, name) { | ||||
|     if (node && node._flow) { | ||||
|         var flow = node._flow; | ||||
|         if (flow) { | ||||
|             return flow.getSetting(name); | ||||
| function getSetting(node, name, flow_) { | ||||
|     var flow = (flow_ ? flow_ : (node ? node._flow : null)); | ||||
|     if (flow) { | ||||
|         if (node && node.g) { | ||||
|             const group = flow.getGroupNode(node.g); | ||||
|             const result = flow.getGroupEnvSetting(node, group, name); | ||||
|             if (result) { | ||||
|                 return result.val; | ||||
|             } | ||||
|         } | ||||
|         return flow.getSetting(name); | ||||
|     } | ||||
|     return process.env[name]; | ||||
| } | ||||
| @@ -544,18 +549,19 @@ function getSetting(node, name) { | ||||
|  * @memberof @node-red/util_util | ||||
|  */ | ||||
| function evaluateEnvProperty(value, node) { | ||||
|     var flow = (node && node.hasOwnProperty("_flow")) ? node._flow : null; | ||||
|     var result; | ||||
|     if (/^\${[^}]+}$/.test(value)) { | ||||
|         // ${ENV_VAR} | ||||
|         var name = value.substring(2,value.length-1); | ||||
|         result = getSetting(node, name); | ||||
|         result = getSetting(node, name, flow); | ||||
|     } else if (!/\${\S+}/.test(value)) { | ||||
|         // ENV_VAR | ||||
|         result = getSetting(node, value); | ||||
|         result = getSetting(node, value, flow); | ||||
|     } else { | ||||
|         // FOO${ENV_VAR}BAR | ||||
|         return value.replace(/\${([^}]+)}/g, function(match, name) { | ||||
|             var val = getSetting(node, name); | ||||
|             var val = getSetting(node, name, flow); | ||||
|             return (val === undefined)?"":val; | ||||
|         }); | ||||
|     } | ||||
| @@ -668,7 +674,7 @@ function prepareJSONataExpression(value,node) { | ||||
|         return node.context().global.get(val, store); | ||||
|     }); | ||||
|     expr.assign('env', function(name) { | ||||
|         var val = getSetting(node, name); | ||||
|         var val = getSetting(node, name, node._flow); | ||||
|         if (typeof val !== 'undefined') { | ||||
|             return val; | ||||
|         } else { | ||||
| @@ -976,5 +982,6 @@ module.exports = { | ||||
|     normaliseNodeTypeName: normaliseNodeTypeName, | ||||
|     prepareJSONataExpression: prepareJSONataExpression, | ||||
|     evaluateJSONataExpression: evaluateJSONataExpression, | ||||
|     parseContextStore: parseContextStore | ||||
|     parseContextStore: parseContextStore, | ||||
|     getSetting: getSetting | ||||
| }; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "@node-red/util", | ||||
|     "version": "2.0.3", | ||||
|     "version": "2.0.6", | ||||
|     "license": "Apache-2.0", | ||||
|     "repository": { | ||||
|         "type": "git", | ||||
| @@ -18,7 +18,7 @@ | ||||
|         "fs-extra": "10.0.0", | ||||
|         "i18next": "20.3.2", | ||||
|         "json-stringify-safe": "5.0.1", | ||||
|         "jsonata": "1.8.4", | ||||
|         "jsonata": "1.8.5", | ||||
|         "lodash.clonedeep": "^4.5.0", | ||||
|         "moment-timezone": "0.5.33" | ||||
|     } | ||||
|   | ||||
							
								
								
									
										10
									
								
								packages/node_modules/node-red/package.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								packages/node_modules/node-red/package.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "node-red", | ||||
|     "version": "2.0.3", | ||||
|     "version": "2.0.6", | ||||
|     "description": "Low-code programming for event-driven applications", | ||||
|     "homepage": "http://nodered.org", | ||||
|     "license": "Apache-2.0", | ||||
| @@ -31,10 +31,10 @@ | ||||
|         "flow" | ||||
|     ], | ||||
|     "dependencies": { | ||||
|         "@node-red/editor-api": "2.0.3", | ||||
|         "@node-red/runtime": "2.0.3", | ||||
|         "@node-red/util": "2.0.3", | ||||
|         "@node-red/nodes": "2.0.3", | ||||
|         "@node-red/editor-api": "2.0.6", | ||||
|         "@node-red/runtime": "2.0.6", | ||||
|         "@node-red/util": "2.0.6", | ||||
|         "@node-red/nodes": "2.0.6", | ||||
|         "basic-auth": "2.0.1", | ||||
|         "bcryptjs": "2.4.3", | ||||
|         "express": "4.17.1", | ||||
|   | ||||
							
								
								
									
										7
									
								
								packages/node_modules/node-red/red.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								packages/node_modules/node-red/red.js
									
									
									
									
										vendored
									
									
								
							| @@ -234,8 +234,13 @@ httpsPromise.then(function(startupHttps) { | ||||
|                             // Get the result of the function, because createServer doesn't accept functions as input | ||||
|                             Promise.resolve(settings.https()).then(function(refreshedHttps) { | ||||
|                                 if (refreshedHttps) { | ||||
|                                     // The key/cert needs to be updated in the NodeJs http(s) server, when no key/cert is yet available or when the key/cert has changed.  | ||||
|                                     // Note that the refreshed key/cert can be supplied as a string or a buffer. | ||||
|                                     var updateKey = (server.key == undefined || (Buffer.isBuffer(server.key) && !server.key.equals(refreshedHttps.key)) || (typeof server.key == "string" && server.key != refreshedHttps.key)); | ||||
|                                     var updateCert = (server.cert == undefined || (Buffer.isBuffer(server.cert) && !server.cert.equals(refreshedHttps.cert)) || (typeof server.cert == "string" && server.cert != refreshedHttps.cert)); | ||||
|                                      | ||||
|                                     // Only update the credentials in the server when key or cert has changed | ||||
|                                     if(!server.key || !server.cert || !server.key.equals(refreshedHttps.key) || !server.cert.equals(refreshedHttps.cert)) { | ||||
|                                     if(updateKey || updateCert) { | ||||
|                                         server.setSecureContext(refreshedHttps); | ||||
|                                         RED.log.info(RED.log._("server.https.settings-refreshed")); | ||||
|                                     } | ||||
|   | ||||
| @@ -4,6 +4,8 @@ const path = require("path"); | ||||
| const fs = require("fs-extra"); | ||||
| const should = require("should"); | ||||
|  | ||||
| const LATEST = "2"; | ||||
|  | ||||
| function generateScript() { | ||||
|     return new Promise((resolve, reject) => { | ||||
|         const packages = [ | ||||
| @@ -18,7 +20,13 @@ function generateScript() { | ||||
|         const rootPackage = require(path.join(__dirname,"..","package.json")); | ||||
|         const version = rootPackage.version; | ||||
|  | ||||
|         const tagArg = /-/.test(version) ? "--tag next" : "" | ||||
|         const versionParts = version.split("."); | ||||
|         let tagArg = ""; | ||||
|         if (versionParts[0] !== LATEST) { | ||||
|             tagArg = `--tag v${versionParts[0]}-maintenance` | ||||
|         } else if (/-/.test(version))  { | ||||
|             tagArg = "--tag next" | ||||
|         } | ||||
|  | ||||
|         const lines = []; | ||||
|  | ||||
|   | ||||
| @@ -542,6 +542,77 @@ describe('change Node', function() { | ||||
|                     changeNode1.receive({payload:"123",topic:"ABC"}); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it('sets the value using env property from tab', function(done) { | ||||
|                 var flow = [ | ||||
|                     {"id":"tab1","type":"tab","env":[ | ||||
|                         {"name":"NR_TEST_A", "value":"bar", "type": "str"} | ||||
|                     ]}, | ||||
|                     {"id":"changeNode1","type":"change","z":"tab1",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]]}, | ||||
|                     {id:"helperNode1", type:"helper", wires:[]} | ||||
|                 ]; | ||||
|                 helper.load(changeNode, flow, function() { | ||||
|                     var changeNode1 = helper.getNode("changeNode1"); | ||||
|                     var helperNode1 = helper.getNode("helperNode1"); | ||||
|                     helperNode1.on("input", function(msg) { | ||||
|                         try { | ||||
|                             msg.payload.should.equal("bar"); | ||||
|                             done(); | ||||
|                         } catch(err) { | ||||
|                             done(err); | ||||
|                         } | ||||
|                     }); | ||||
|                     changeNode1.receive({payload:"123",topic:"ABC"}); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it('sets the value using env property from group', function(done) { | ||||
|                 var flow = [ | ||||
|                     {"id":"group1","type":"group","env":[ | ||||
|                         {"name":"NR_TEST_A", "value":"bar", "type": "str"} | ||||
|                     ]}, | ||||
|                     {"id":"changeNode1","type":"change","g":"group1",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]]}, | ||||
|                     {id:"helperNode1", type:"helper", wires:[]} | ||||
|                 ]; | ||||
|                 helper.load(changeNode, flow, function() { | ||||
|                     var changeNode1 = helper.getNode("changeNode1"); | ||||
|                     var helperNode1 = helper.getNode("helperNode1"); | ||||
|                     helperNode1.on("input", function(msg) { | ||||
|                         try { | ||||
|                             msg.payload.should.equal("bar"); | ||||
|                             done(); | ||||
|                         } catch(err) { | ||||
|                             done(err); | ||||
|                         } | ||||
|                     }); | ||||
|                     changeNode1.receive({payload:"123",topic:"ABC"}); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it('sets the value using env property from nested group', function(done) { | ||||
|                 var flow = [ | ||||
|                     {"id":"group1","type":"group","env":[ | ||||
|                         {"name":"NR_TEST_A", "value":"bar", "type": "str"} | ||||
|                     ]}, | ||||
|                     {"id":"group2","type":"group","g":"group1","env":[]}, | ||||
|                     {"id":"changeNode1","type":"change","g":"group2",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]]}, | ||||
|                     {id:"helperNode1", type:"helper", wires:[]} | ||||
|                 ]; | ||||
|                 helper.load(changeNode, flow, function() { | ||||
|                     var changeNode1 = helper.getNode("changeNode1"); | ||||
|                     var helperNode1 = helper.getNode("helperNode1"); | ||||
|                     helperNode1.on("input", function(msg) { | ||||
|                         try { | ||||
|                             msg.payload.should.equal("bar"); | ||||
|                             done(); | ||||
|                         } catch(err) { | ||||
|                             done(err); | ||||
|                         } | ||||
|                     }); | ||||
|                     changeNode1.receive({payload:"123",topic:"ABC"}); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -42,6 +42,8 @@ describe('HTTP Request Node', function() { | ||||
|     var testProxyPort = 10444; | ||||
|     var testProxyServerAuth; | ||||
|     var testProxyAuthPort = 10554; | ||||
|     var testSslClientServer; | ||||
|     var testSslClientPort = 10664; | ||||
|  | ||||
|     //save environment variables | ||||
|     var preEnvHttpProxyLowerCase; | ||||
| @@ -57,6 +59,7 @@ describe('HTTP Request Node', function() { | ||||
|         testServer = stoppable(http.createServer(testApp)); | ||||
|         testServer.listen(testPort,function(err) { | ||||
|             testSslPort += 1; | ||||
|             console.log("ssl port", testSslPort); | ||||
|             var sslOptions = { | ||||
|                 key:  fs.readFileSync('test/resources/ssl/server.key'), | ||||
|                 cert: fs.readFileSync('test/resources/ssl/server.crt') | ||||
| @@ -75,7 +78,25 @@ describe('HTTP Request Node', function() { | ||||
|                 */ | ||||
|             }; | ||||
|             testSslServer = stoppable(https.createServer(sslOptions,testApp)); | ||||
|             testSslServer.listen(testSslPort); | ||||
|             testSslServer.listen(testSslPort, function(err){ | ||||
|                 if (err) { | ||||
|                     console.log(err); | ||||
|                 } else { | ||||
|                     console.log("started testSslServer"); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             testSslClientPort += 1; | ||||
|             var sslClientOptions = { | ||||
|                 key:  fs.readFileSync('test/resources/ssl/server.key'), | ||||
|                 cert: fs.readFileSync('test/resources/ssl/server.crt'), | ||||
|                 ca: fs.readFileSync('test/resources/ssl/server.crt'), | ||||
|                 requestCert: true | ||||
|             }; | ||||
|             testSslClientServer = stoppable(https.createServer(sslClientOptions, testApp)); | ||||
|             testSslClientServer.listen(testSslClientPort, function(err){ | ||||
|                 console.log("ssl-client", err) | ||||
|             }); | ||||
|  | ||||
|             testProxyPort += 1; | ||||
|             testProxyServer = stoppable(httpProxy(http.createServer())) | ||||
| @@ -121,6 +142,10 @@ describe('HTTP Request Node', function() { | ||||
|         return "https://localhost:"+testSslPort+url; | ||||
|     } | ||||
|  | ||||
|     function getSslClientTestURL(url) { | ||||
|         return "https://localhost:"+testSslClientPort+url; | ||||
|     } | ||||
|  | ||||
|     function getDifferentTestURL(url) { | ||||
|         return "http://127.0.0.1:"+testPort+url; | ||||
|     } | ||||
| @@ -280,6 +305,14 @@ describe('HTTP Request Node', function() { | ||||
|                 headers:result | ||||
|             }); | ||||
|         }) | ||||
|  | ||||
|         testApp.get('/getClientCert', function(req,res) { | ||||
|             if (req.client.authorized) { | ||||
|                 res.send('hello'); | ||||
|             } else { | ||||
|                 res.status(401).send(); | ||||
|             } | ||||
|         }) | ||||
|         startServer(function(err) { | ||||
|             if (err) { | ||||
|                 done(err); | ||||
| @@ -293,7 +326,9 @@ describe('HTTP Request Node', function() { | ||||
|             testProxyServer.stop(() => { | ||||
|                 testProxyServerAuth.stop(() => { | ||||
|                     testSslServer.stop(() => { | ||||
|                         helper.stopServer(done); | ||||
|                         testSslClientServer.stop(() => { | ||||
|                             helper.stopServer(done); | ||||
|                         }) | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
| @@ -1145,7 +1180,7 @@ describe('HTTP Request Node', function() { | ||||
|                     try { | ||||
|                         msg.should.have.property('payload',{ | ||||
|                             query:{ a: 'b', c:[ 'T24,0°|H80%|W S8,3m/s' ] }, | ||||
|                             url: '/getQueryParams?a=b&c%5B0%5D.Text=T24,0%C2%B0%7CH80%25%7CW%20S8,3m/s' | ||||
|                             url: '/getQueryParams?a=b&c[0].Text=T24,0%C2%B0|H80%25|W%20S8,3m/s' | ||||
|                         }); | ||||
|                         msg.should.have.property('statusCode',200); | ||||
|                         msg.should.have.property('headers'); | ||||
| @@ -1154,7 +1189,7 @@ describe('HTTP Request Node', function() { | ||||
|                         done(err); | ||||
|                     } | ||||
|                 }); | ||||
|                 n1.receive({url: getTestURL('/getQueryParams')+"?a=b&c[0].Text=T24,0°|H80%|W S8,3m/s"}); | ||||
|                 n1.receive({url: getTestURL('/getQueryParams')+"?a=b&c[0].Text=T24,0°|H80%|W%20S8,3m/s"}); | ||||
|             }); | ||||
|         }) | ||||
|     }); | ||||
| @@ -1518,6 +1553,60 @@ describe('HTTP Request Node', function() { | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('should use tls-config and verify serverCert', function(done) { | ||||
|             var flow = [ | ||||
|                 {id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getSslTestURL('/text'),tls:"n3"}, | ||||
|                 {id:"n2", type:"helper"}, | ||||
|                 {id:"n3", type:"tls-config", cert:"test/resources/ssl/server.crt", key:"test/resources/ssl/server.key", ca:"test/resources/ssl/server.crt", verifyservercert:true}]; | ||||
|             var testNodes = [httpRequestNode, tlsNode]; | ||||
|             helper.load(testNodes, flow, function() { | ||||
|                 var n3 = helper.getNode("n3"); | ||||
|                 var n2 = helper.getNode("n2"); | ||||
|                 var n1 = helper.getNode("n1"); | ||||
|                 n2.on("input", function(msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property('payload','hello'); | ||||
|                         msg.should.have.property('statusCode',200); | ||||
|                         msg.should.have.property('headers'); | ||||
|                         msg.headers.should.have.property('content-length',''+('hello'.length)); | ||||
|                         msg.headers.should.have.property('content-type').which.startWith('text/html'); | ||||
|                         msg.should.have.property('responseUrl').which.startWith('https://'); | ||||
|                         done(); | ||||
|                     } catch(err) { | ||||
|                         done(err); | ||||
|                     } | ||||
|                 }); | ||||
|                 n1.receive({payload:"foo"}); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('should use tls-config and send client cert', function(done) { | ||||
|             var flow = [ | ||||
|                 {id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getSslClientTestURL('/getClientCert'),tls:"n3"}, | ||||
|                 {id:"n2", type:"helper"}, | ||||
|                 {id:"n3", type:"tls-config", cert:"test/resources/ssl/server.crt", key:"test/resources/ssl/server.key", ca:"test/resources/ssl/server.crt", verifyservercert:false}]; | ||||
|             var testNodes = [httpRequestNode,tlsNode]; | ||||
|             helper.load(testNodes, flow, function() { | ||||
|                 var n3 = helper.getNode("n3"); | ||||
|                 var n2 = helper.getNode("n2"); | ||||
|                 var n1 = helper.getNode("n1"); | ||||
|                 n2.on("input", function(msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property('payload','hello'); | ||||
|                         msg.should.have.property('statusCode',200); | ||||
|                         msg.should.have.property('headers'); | ||||
|                         msg.headers.should.have.property('content-length',''+('hello'.length)); | ||||
|                         msg.headers.should.have.property('content-type').which.startWith('text/html'); | ||||
|                         msg.should.have.property('responseUrl').which.startWith('https://'); | ||||
|                         done(); | ||||
|                     } catch(err) { | ||||
|                         done(err); | ||||
|                     } | ||||
|                 }); | ||||
|                 n1.receive({payload:"foo"}); | ||||
|             }) | ||||
|         }); | ||||
|  | ||||
|         //Removing HTTP Proxy testcases as GOT + Proxy_Agent doesn't work with mock'd proxy | ||||
|         /* */ | ||||
|         it('should use http_proxy', function(done) { | ||||
|   | ||||
| @@ -516,6 +516,28 @@ describe('JOIN node', function() { | ||||
|             n1.receive({payload:{a:1}}); | ||||
|         }); | ||||
|     }); | ||||
|     it('should join things into an array ignoring msg.parts.index in manual mode', function(done) { | ||||
|         var flow = [{id:"n1", type:"join", wires:[["n2"]], count:3, joiner:",",mode:"custom"}, | ||||
|                     {id:"n2", type:"helper"}]; | ||||
|         helper.load(joinNode, flow, function() { | ||||
|             var n1 = helper.getNode("n1"); | ||||
|             var n2 = helper.getNode("n2"); | ||||
|             n2.on("input", function(msg) { | ||||
|                 try { | ||||
|                     msg.should.have.property("payload"); | ||||
|                     msg.payload.should.be.an.Array(); | ||||
|                     msg.payload[0].should.equal(1); | ||||
|                     msg.payload[1].should.equal(true); | ||||
|                     //msg.payload[2].a.should.equal(1); | ||||
|                     done(); | ||||
|                 } | ||||
|                 catch(e) {done(e);} | ||||
|             }); | ||||
|             n1.receive({payload:1, parts: {index: 3}}); | ||||
|             n1.receive({payload:true, parts: {index: 0}}); | ||||
|             n1.receive({payload:{a:1}, parts: {index: 9}}); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     it('should join things into an array after a count with a buffer join set', function(done) { | ||||
|         var flow = [{id:"n1", type:"join", wires:[["n2"]], count:3, joinerType:"bin", joiner:"" ,mode:"custom"}, | ||||
| @@ -1646,7 +1668,7 @@ describe('JOIN node', function() { | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     it('should handle join an array when using msg.parts and duplicate indexed parts arrive', function (done) { | ||||
|     it('should handle join an array when mode is auto and duplicate indexed parts arrive', function (done) { | ||||
|         var flow = [{ id: "n1", type: "join", wires: [["n2"]], joiner: "[44]", joinerType: "bin", build: "array", mode: "auto" }, | ||||
|                     { id: "n2", type: "helper" }]; | ||||
|         helper.load(joinNode, flow, function () { | ||||
|   | ||||
| @@ -447,4 +447,124 @@ describe('subflow', function() { | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     it('should access env var of tab', function(done) { | ||||
|         var flow = [ | ||||
|             {id:"t0", type:"tab", label:"", disabled:false, info:"", env: [ | ||||
|                 {name: "K", type: "str", value: "V"} | ||||
|             ]}, | ||||
|             {id:"n1", x:10, y:10, z:"t0", type:"subflow:s1", wires:[["n2"]]}, | ||||
|             {id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]}, | ||||
|             // Subflow | ||||
|             {id:"s1", type:"subflow", name:"Subflow", info:"", env: [], | ||||
|              in:[{ | ||||
|                  x:10, y:10, | ||||
|                  wires:[ {id:"s1-n1"} ] | ||||
|              }], | ||||
|              out:[{ | ||||
|                  x:10, y:10, | ||||
|                  wires:[ {id:"s1-n1", port:0} ] | ||||
|              }] | ||||
|             }, | ||||
|             {id:"s1-n1", x:10, y:10, z:"s1", type:"function", | ||||
|              func:"msg.V = env.get('K'); return msg;", | ||||
|              wires:[]} | ||||
|         ]; | ||||
|         helper.load(functionNode, flow, function() { | ||||
|             var n1 = helper.getNode("n1"); | ||||
|             var n2 = helper.getNode("n2"); | ||||
|             n2.on("input", function(msg) { | ||||
|                 try { | ||||
|                     msg.should.have.property("V", "V"); | ||||
|                     done(); | ||||
|                 } | ||||
|                 catch (e) { | ||||
|                     console.log(e); | ||||
|                     done(e); | ||||
|                 } | ||||
|             }); | ||||
|             n1.receive({payload:"foo"}); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     it('should access env var of group', function(done) { | ||||
|         var flow = [ | ||||
|             {id:"t0", type:"tab", label:"", disabled:false, info:""}, | ||||
|             {id:"g1", z:"t0", type:"group", env:[ | ||||
|                 {name: "K", type: "str", value: "V"} | ||||
|             ]}, | ||||
|             {id:"n1", x:10, y:10, z:"t0", g:"g1", type:"subflow:s1", wires:[["n2"]]}, | ||||
|             {id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]}, | ||||
|             // Subflow | ||||
|             {id:"s1", type:"subflow", name:"Subflow", info:"", env: [], | ||||
|              in:[{ | ||||
|                  x:10, y:10, | ||||
|                  wires:[ {id:"s1-n1"} ] | ||||
|              }], | ||||
|              out:[{ | ||||
|                  x:10, y:10, | ||||
|                  wires:[ {id:"s1-n1", port:0} ] | ||||
|              }] | ||||
|             }, | ||||
|             {id:"s1-n1", x:10, y:10, z:"s1", type:"function", | ||||
|              func:"msg.V = env.get('K'); return msg;", | ||||
|              wires:[]} | ||||
|         ]; | ||||
|         helper.load(functionNode, flow, function() { | ||||
|             var n1 = helper.getNode("n1"); | ||||
|             var n2 = helper.getNode("n2"); | ||||
|             n2.on("input", function(msg) { | ||||
|                 try { | ||||
|                     msg.should.have.property("V", "V"); | ||||
|                     done(); | ||||
|                 } | ||||
|                 catch (e) { | ||||
|                     console.log(e); | ||||
|                     done(e); | ||||
|                 } | ||||
|             }); | ||||
|             n1.receive({payload:"foo"}); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     it('should access env var of nested group', function(done) { | ||||
|         var flow = [ | ||||
|             {id:"t0", type:"tab", label:"", disabled:false, info:""}, | ||||
|             {id:"g1", z:"t0", type:"group", env:[ | ||||
|                 {name: "K", type: "str", value: "V"} | ||||
|             ]}, | ||||
|             {id:"g2", z:"t0", g:"g1", type:"group", env:[]}, | ||||
|             {id:"n1", x:10, y:10, z:"t0", g:"g2", type:"subflow:s1", wires:[["n2"]]}, | ||||
|             {id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]}, | ||||
|             // Subflow | ||||
|             {id:"s1", type:"subflow", name:"Subflow", info:"", env: [], | ||||
|              in:[{ | ||||
|                  x:10, y:10, | ||||
|                  wires:[ {id:"s1-n1"} ] | ||||
|              }], | ||||
|              out:[{ | ||||
|                  x:10, y:10, | ||||
|                  wires:[ {id:"s1-n1", port:0} ] | ||||
|              }] | ||||
|             }, | ||||
|             {id:"s1-n1", x:10, y:10, z:"s1", type:"function", | ||||
|              func:"msg.V = env.get('K'); return msg;", | ||||
|              wires:[]} | ||||
|         ]; | ||||
|         helper.load(functionNode, flow, function() { | ||||
|             var n1 = helper.getNode("n1"); | ||||
|             var n2 = helper.getNode("n2"); | ||||
|             n2.on("input", function(msg) { | ||||
|                 try { | ||||
|                     msg.should.have.property("V", "V"); | ||||
|                     done(); | ||||
|                 } | ||||
|                 catch (e) { | ||||
|                     console.log(e); | ||||
|                     done(e); | ||||
|                 } | ||||
|             }); | ||||
|             n1.receive({payload:"foo"}); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|   | ||||
| @@ -41,7 +41,9 @@ describe("api/editor/theme", function () { | ||||
|         context.should.have.a.property("page"); | ||||
|         context.page.should.have.a.property("title", "Node-RED"); | ||||
|         context.page.should.have.a.property("favicon", "favicon.ico"); | ||||
|         context.page.should.have.a.property("tabicon", "red/images/node-red-icon-black.svg"); | ||||
|         context.page.should.have.a.property("tabicon"); | ||||
|         context.page.tabicon.should.have.a.property("icon", "red/images/node-red-icon-black.svg"); | ||||
|         context.page.tabicon.should.have.a.property("colour", "#8f0000"); | ||||
|         context.should.have.a.property("header"); | ||||
|         context.header.should.have.a.property("title", "Node-RED"); | ||||
|         context.header.should.have.a.property("image", "red/images/node-red.svg"); | ||||
| @@ -58,7 +60,10 @@ describe("api/editor/theme", function () { | ||||
|                 page: { | ||||
|                     title: "Test Page Title", | ||||
|                     favicon: "/absolute/path/to/theme/favicon", | ||||
|                     tabicon: "/absolute/path/to/theme/tabicon", | ||||
|                     tabicon: { | ||||
|                         icon: "/absolute/path/to/theme/tabicon", | ||||
|                         colour: "#8f008f" | ||||
|                     }, | ||||
|                     css: "/absolute/path/to/custom/css/file.css", | ||||
|                     scripts: "/absolute/path/to/script.js" | ||||
|                 }, | ||||
| @@ -108,7 +113,9 @@ describe("api/editor/theme", function () { | ||||
|         context.should.have.a.property("page"); | ||||
|         context.page.should.have.a.property("title", "Test Page Title"); | ||||
|         context.page.should.have.a.property("favicon", "theme/favicon/favicon"); | ||||
|         context.page.should.have.a.property("tabicon", "theme/tabicon/tabicon"); | ||||
|         context.page.should.have.a.property("tabicon") | ||||
|         context.page.tabicon.should.have.a.property("icon", "theme/tabicon/tabicon"); | ||||
|         context.page.tabicon.should.have.a.property("colour", "#8f008f") | ||||
|         context.should.have.a.property("header"); | ||||
|         context.header.should.have.a.property("title", "Test Header Title"); | ||||
|         context.header.should.have.a.property("url", "http://nodered.org"); | ||||
| @@ -142,6 +149,27 @@ describe("api/editor/theme", function () { | ||||
|         settings.projects.should.have.a.property("enabled", false); | ||||
|     }); | ||||
|  | ||||
|     it("picks up backwards compatible tabicon setting", async function () { | ||||
|         theme.init({ | ||||
|             editorTheme: { | ||||
|                 page: { | ||||
|                     tabicon: "/absolute/path/to/theme/tabicon", | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         theme.app(); | ||||
|  | ||||
|         var context = await theme.context(); | ||||
|         context.should.have.a.property("page"); | ||||
|         context.page.should.have.a.property("tabicon"); | ||||
|         context.page.tabicon.should.have.a.property("icon", "theme/tabicon/tabicon"); | ||||
|         // The colour property should remain as default in this case as the | ||||
|         // legacy format for defining tabicon doesn't allow specifying a colour | ||||
|         context.page.tabicon.should.have.a.property("colour", "#8f0000"); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it("test explicit userMenu set to true in theme setting", function () { | ||||
|       theme.init({ | ||||
|           editorTheme: { | ||||
|   | ||||
| @@ -1228,4 +1228,54 @@ describe('Flow', function() { | ||||
|         }) | ||||
|     }) | ||||
|  | ||||
|     describe("#env", function () { | ||||
|         it("can instantiate a node with environment variable property values of group and tab", function (done) { | ||||
|             try { | ||||
|                 after(function() { | ||||
|                     delete process.env.V0; | ||||
|                     delete process.env.V1; | ||||
|                 }) | ||||
|                 process.env.V0 = "gv0"; | ||||
|                 process.env.V1 = "gv1"; | ||||
|                 var config = flowUtils.parseConfig([ | ||||
|                     {id:"t1",type:"tab",env:[ | ||||
|                         {"name": "V0", value: "v0", type: "str"} | ||||
|                     ]}, | ||||
|                     {id:"g1",type:"group",z:"t1",env:[ | ||||
|                         {"name": "V0", value: "v1", type: "str"}, | ||||
|                         {"name": "V1", value: "v2", type: "str"} | ||||
|                     ]}, | ||||
|                     {id:"g2",type:"group",z:"t1",g:"g1",env:[ | ||||
|                         {"name": "V1", value: "v3", type: "str"} | ||||
|                     ]}, | ||||
|                     {id:"1",x:10,y:10,z:"t1",type:"test",foo:"$(V0)",wires:[]}, | ||||
|                     {id:"2",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V0)",wires:[]}, | ||||
|                     {id:"3",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V1)",wires:[]}, | ||||
|                     {id:"4",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"$(V1)",wires:[]}, | ||||
|                     {id:"5",x:10,y:10,z:"t1",type:"test",foo:"$(V1)",wires:[]}, | ||||
|                 ]); | ||||
|                 var flow = Flow.create({getSetting:v=>process.env[v]},config,config.flows["t1"]); | ||||
|                 flow.start(); | ||||
|  | ||||
|                 var activeNodes = flow.getActiveNodes(); | ||||
|  | ||||
|                 activeNodes["1"].foo.should.equal("v0"); | ||||
|                 activeNodes["2"].foo.should.equal("v1"); | ||||
|                 activeNodes["3"].foo.should.equal("v2"); | ||||
|                 activeNodes["4"].foo.should.equal("v3"); | ||||
|                 activeNodes["5"].foo.should.equal("gv1"); | ||||
|  | ||||
|                 flow.stop().then(function() { | ||||
|                     done(); | ||||
|                 }); | ||||
|             } | ||||
|             catch (e) { | ||||
|                 console.log(e.stack); | ||||
|                 done(e); | ||||
|             } | ||||
|                  | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user