mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	Compare commits
	
		
			361 Commits
		
	
	
		
			move-out-s
			...
			0.19.5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | c685a31056 | ||
|  | c32ce3bb7b | ||
|  | 4f87ebdf0a | ||
|  | 19b6cba398 | ||
|  | 5cf9c07b73 | ||
|  | 211eeea05d | ||
|  | 51a3521834 | ||
|  | 6a9575e9f4 | ||
|  | 62088259ae | ||
|  | 925ebcc06e | ||
|  | 673a6bbe2c | ||
|  | cf32a33984 | ||
|  | 98c1bc276d | ||
|  | 629536b562 | ||
|  | 1441042458 | ||
|  | 58c8311d56 | ||
|  | e03a0fffa9 | ||
|  | 8e2c12f8d9 | ||
|  | 7cec7ae608 | ||
|  | c49f722e4f | ||
|  | d4d95a43b6 | ||
|  | a345089c8b | ||
|  | 67c268e13d | ||
|  | ce85c8d986 | ||
|  | cb35604ef5 | ||
|  | 61681bb1d6 | ||
|  | 1a226c4dc6 | ||
|  | 08fccc4e77 | ||
|  | c1d50e82e1 | ||
|  | 9777af7cb5 | ||
|  | fd86035865 | ||
|  | a8ec032553 | ||
|  | 66ee27c5fa | ||
|  | 17a737ca88 | ||
|  | 75e7c0e50d | ||
|  | fc0cf1ff51 | ||
|  | 0f4d46671f | ||
|  | 048f9c0294 | ||
|  | ca77842b5b | ||
|  | 6fa8b7f5f1 | ||
|  | a2d03c14ae | ||
|  | c667a0e74c | ||
|  | 8123828113 | ||
|  | 72b8dbb45b | ||
|  | 7703875740 | ||
|  | 6442bb8a13 | ||
|  | f29d7c9252 | ||
|  | 2f7f53ed96 | ||
|  | 94031a52a5 | ||
|  | d67f91e7ed | ||
|  | f37697c4fb | ||
|  | 69448c7329 | ||
|  | 8e9815fb91 | ||
|  | 4cdd7978cf | ||
|  | 40d81358f4 | ||
|  | c7b62aed91 | ||
|  | c0e7d6d826 | ||
|  | f809377de8 | ||
|  | 9767bd9697 | ||
|  | 3a55528552 | ||
|  | 56197ffe3a | ||
|  | 0f0d0c046c | ||
|  | ecc4973645 | ||
|  | 3169f93cc2 | ||
|  | c1a1a73599 | ||
|  | db1b0ccb79 | ||
|  | df161ce672 | ||
|  | 72fe30892e | ||
|  | d373105b32 | ||
|  | 28b311b7ed | ||
|  | dcda513901 | ||
|  | 72c400794c | ||
|  | 042409f870 | ||
|  | 5b8f4f4069 | ||
|  | d132d63c1d | ||
|  | 4374506981 | ||
|  | ef8b936069 | ||
|  | 36e3bfffb4 | ||
|  | 91a38bdb60 | ||
|  | f169a68319 | ||
|  | ee886f98dd | ||
|  | a3826cc6a7 | ||
|  | ba33b832ba | ||
|  | f6c017176b | ||
|  | 7a01b115bb | ||
|  | 1dc021e871 | ||
|  | c9f916ebab | ||
|  | ff627fd128 | ||
|  | 695873d35a | ||
|  | 15da19dcea | ||
|  | d5bdc1600b | ||
|  | dfa077fd5f | ||
|  | 5155770213 | ||
|  | f1d5bbb036 | ||
|  | 69ed0aebc3 | ||
|  | 549e56e220 | ||
|  | 450f4d9a5a | ||
|  | 6533a9793c | ||
|  | 2000cadb17 | ||
|  | 083c321efa | ||
|  | f64c4a981f | ||
|  | 3ac8ce03bf | ||
|  | 66fca8710e | ||
|  | 1e245ece46 | ||
|  | 81efce03ba | ||
|  | 4e549dd426 | ||
|  | 52f74ff7e0 | ||
|  | 3c71b815f5 | ||
|  | 9efd48fe51 | ||
|  | 4609ee75b6 | ||
|  | 963ea4177e | ||
|  | 17e6940a42 | ||
|  | 315a9ceba3 | ||
|  | a2bdeedb09 | ||
|  | da5700d2d7 | ||
|  | 90e7f30247 | ||
|  | 3ccf6ba892 | ||
|  | e50cd5b745 | ||
|  | db77be5d72 | ||
|  | c36870c23e | ||
|  | e9be007040 | ||
|  | 9e400d9aa6 | ||
|  | 490c8dae75 | ||
|  | 3bcffe375d | ||
|  | 9f81a591e1 | ||
|  | 3db5306c70 | ||
|  | 45029dd084 | ||
|  | b01cd30339 | ||
|  | 09329e1104 | ||
|  | ab0fc2ecfa | ||
|  | bf5d36d6bd | ||
|  | a29527ec96 | ||
|  | 45e7ad8049 | ||
|  | 4d54663efd | ||
|  | 29d386cc51 | ||
|  | ba1a67969b | ||
|  | 390ea5419e | ||
|  | 0fdeec7cc4 | ||
|  | e34d883e50 | ||
|  | 507871687b | ||
|  | ed58f62cd1 | ||
|  | 94bc4e7125 | ||
|  | 5832f7930d | ||
|  | 0066a20c22 | ||
|  | 774e4bfced | ||
|  | 054c7a76a4 | ||
|  | c7f3b77aac | ||
|  | a6a4620374 | ||
|  | 5148c62d1c | ||
|  | 6fc863a91e | ||
|  | e066a154a1 | ||
|  | d432edaed2 | ||
|  | 8f34f4e80b | ||
|  | 39b751acf5 | ||
|  | bd5e8ba961 | ||
|  | ed20327c41 | ||
|  | 991c68c394 | ||
|  | 2acc31a4e7 | ||
|  | b9733e3dfa | ||
|  | bb106bfce7 | ||
|  | daf1388a6a | ||
|  | 8226f1fa75 | ||
|  | e675512fa3 | ||
|  | 65e67b6c3e | ||
|  | 7612481570 | ||
|  | f6c7cb5804 | ||
|  | 2201c9062f | ||
|  | ca3da262da | ||
|  | 5847f92bef | ||
|  | 31ee1be81e | ||
|  | 6bccdd015f | ||
|  | cecea318da | ||
|  | 8663ec6880 | ||
|  | 9b03f128aa | ||
|  | 8a51f97616 | ||
|  | ee74ed9ce9 | ||
|  | 0f947c756e | ||
|  | cae7949a48 | ||
|  | be58b614e1 | ||
|  | b0a01fa4b2 | ||
|  | 9734228001 | ||
|  | 9df1d44bc4 | ||
|  | 13d887028a | ||
|  | 83a8979309 | ||
|  | 75c29f1cb7 | ||
|  | d9d15e41c7 | ||
|  | d3598d5854 | ||
|  | 3a8aaee5d7 | ||
|  | 4fcf57d42c | ||
|  | adb0891335 | ||
|  | d21e719cc1 | ||
|  | 5807ab82c1 | ||
|  | 65cb04da63 | ||
|  | 312e3611b1 | ||
|  | 46acc62279 | ||
|  | 529b358c9b | ||
|  | 7fca04404e | ||
|  | 3a1cc6a2be | ||
|  | 5b76c91004 | ||
|  | cf87837f7d | ||
|  | 5a0a7b907b | ||
|  | 6a2b1669b3 | ||
|  | ca8264b3f4 | ||
|  | b44ecd8819 | ||
|  | 987942959e | ||
|  | 91992b48c1 | ||
|  | c9a335a6f9 | ||
|  | b7ed159b50 | ||
|  | c72961a52a | ||
|  | afe6afca36 | ||
|  | 050acd239c | ||
|  | 24505ee4f5 | ||
|  | 63a249aba3 | ||
|  | 7bd94df2a0 | ||
|  | 761161a8e5 | ||
|  | 513579a7ee | ||
|  | 7165483d83 | ||
|  | f8bcf219cb | ||
|  | 590506e306 | ||
|  | 9a5439c580 | ||
|  | 6b2f5fbb19 | ||
|  | 051c147b41 | ||
|  | 9111adf15f | ||
|  | ba18b27371 | ||
|  | 2a287b2ae6 | ||
|  | fc9040f715 | ||
|  | 0029022ef6 | ||
|  | 94f728d3fd | ||
|  | d53ced7830 | ||
|  | 053d8c44c2 | ||
|  | 9f5767ea16 | ||
|  | e8d76b0555 | ||
|  | c2675600f6 | ||
|  | 6f087b4ec1 | ||
|  | e94708606d | ||
|  | c248f1a762 | ||
|  | 28402b0894 | ||
|  | 7dd98e99f9 | ||
|  | dba195b396 | ||
|  | 88b153bc12 | ||
|  | d4a47dc974 | ||
|  | fe3ea6edfd | ||
|  | 6c8fc4846b | ||
|  | 54d9656f09 | ||
|  | 49da324c5d | ||
|  | 9bf87697fd | ||
|  | f368f5a9c4 | ||
|  | eea85485e6 | ||
|  | 1a544b3b82 | ||
|  | 8b38fe9fe0 | ||
|  | 1bf4addf63 | ||
|  | 407e16e900 | ||
|  | 6e9fe3248a | ||
|  | d8cf86fd6f | ||
|  | f8aa4a9588 | ||
|  | c249907846 | ||
|  | 57c1524a9a | ||
|  | d8d82e2ba3 | ||
|  | 807b512ef7 | ||
|  | b2f06b6777 | ||
|  | d7adff9a65 | ||
|  | b0d7e11d48 | ||
|  | fc9cdb61f2 | ||
|  | 9c00492dc2 | ||
|  | 1a6babd199 | ||
|  | 1b693eed37 | ||
|  | afb566b6b4 | ||
|  | f870e9ed3e | ||
|  | 4bcf13cb58 | ||
|  | 946a6d6041 | ||
|  | 372c213c2c | ||
|  | a5a79d3ab7 | ||
|  | e6c5cfb703 | ||
|  | 7843eccae8 | ||
|  | 7ca153abd0 | ||
|  | 33b4774c49 | ||
|  | c243481432 | ||
|  | 80873e4ea9 | ||
|  | 4e4a1f11e6 | ||
|  | 9bbe405cd0 | ||
|  | c440a4c730 | ||
|  | a1251371d7 | ||
|  | 7d702e8332 | ||
|  | 43d7c8d48c | ||
|  | 7423583508 | ||
|  | 08b0838f9a | ||
|  | 038d821a7c | ||
|  | 6a218814d3 | ||
|  | 905f89b0f5 | ||
|  | 14882bda78 | ||
|  | 781fa4634b | ||
|  | cdb173fd6e | ||
|  | 466cb4be89 | ||
|  | c39e2ffd56 | ||
|  | 17bf09e276 | ||
|  | bc01f9f8fd | ||
|  | c0870c5694 | ||
|  | 3b5174a2ea | ||
|  | af6885f3e8 | ||
|  | e01996095f | ||
|  | 5d86f7b6ba | ||
|  | 8d6ac6406d | ||
|  | 40ff54f67e | ||
|  | cce7ac09d0 | ||
|  | 73a18891c5 | ||
|  | fe22cedc1d | ||
|  | fa09c7c8b2 | ||
|  | 15e28e3cc0 | ||
|  | 2cb4f6b1fc | ||
|  | b17a483b85 | ||
|  | 7c3e5443ab | ||
|  | f95a2851c8 | ||
|  | 2b2eee352f | ||
|  | 11569d8056 | ||
|  | bdf87452b6 | ||
|  | 0c6bf81c24 | ||
|  | f2fa26fb07 | ||
|  | f5e212ff1e | ||
|  | 461e6562ca | ||
|  | fd67d08402 | ||
|  | e6411d11b1 | ||
|  | dd81d947fc | ||
|  | 23b887c30e | ||
|  | c4eae3f130 | ||
|  | 41a04a2849 | ||
|  | ed1d34e678 | ||
|  | f44487338d | ||
|  | 7aced85a31 | ||
|  | fbe0e2d6eb | ||
|  | 6e34f0697c | ||
|  | a835f9f0cb | ||
|  | 16715673c3 | ||
|  | c48c74f173 | ||
|  | f262348497 | ||
|  | 7185bcd51f | ||
|  | 28d05e2449 | ||
|  | 7fafa21a1b | ||
|  | 84f598e143 | ||
|  | e30f8628db | ||
|  | 0be9c88106 | ||
|  | e046fc1ac5 | ||
|  | 3a476ac493 | ||
|  | e33ec0cf50 | ||
|  | b4b70a988e | ||
|  | e66b381070 | ||
|  | 771b598c09 | ||
|  | cd44f13171 | ||
|  | aa6b72ac87 | ||
|  | a467fe5ed7 | ||
|  | 467411c6c3 | ||
|  | 2648b7ca54 | ||
|  | de35c7024a | ||
|  | 9d219c163d | ||
|  | f7434b5ec8 | ||
|  | 5ed3360c0b | ||
|  | 6f5974f875 | ||
|  | fef71f29c4 | ||
|  | dcf44fed58 | ||
|  | b204b183de | ||
|  | 6e2e36e7a0 | ||
|  | 9994df9601 | 
							
								
								
									
										139
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										139
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,3 +1,142 @@ | ||||
| #### 0.19.5: Maintenance Release | ||||
|  | ||||
|  - Recognize pip installs of RPi.GPIO (#1934) | ||||
|  - Merge pull request #1941 from node-red-hitachi/master-batch | ||||
|  - Merge pull request #1931 from node-red-hitachi/master-typedinput | ||||
|  - Set min value of properties and spinners for batch | ||||
|  - Fix that unnecessary optionMenu remains | ||||
|  - Merge pull request #1894 from node-red-hitachi/fix-overlapping-file-node-execution | ||||
|  - Merge pull request #1924 from imZack/patch-1 | ||||
|  - Add missing comma | ||||
|  - Do not disable context sidebar during node edit Fixes #1921 | ||||
|  - Don't allow virtual links to be spliced Fixes #1920 | ||||
|  - Merge project package changes to avoid overwritten changes | ||||
|  - Handle manually added project deps that are unused Fixes #1908 | ||||
|  - update close & input handling of File node | ||||
|  - make close handler argument only one | ||||
|  - Merge pull request #1907 from amilajack/patch-2 | ||||
|  - Change repo badge to point to master branch | ||||
|  - invoke callbacks if async handler is specified | ||||
|  - Merge pull request #1891 from camlow325/resolve-example-path-for-windows-support | ||||
|  - Merge pull request #1900 from kazuhitoyokoi/master-addtestcases4settings.js | ||||
|  - wait closing while pending messages exist | ||||
|  - Add test cases for red/api/editor/settings.js | ||||
|  - Ensure all palette categories are opened properly Closes #1893 | ||||
|  - Resolve path when sending example file for Windows support | ||||
|  - fix multiple input message processing of file node | ||||
|  | ||||
| #### 0.19.4: Maintenance Release | ||||
|  | ||||
|  - Fix race condition in non-cache lfs context Fixes #1888 | ||||
|  - LocalFileSystem Context: Remove extra flush code | ||||
|  - Prevent race condition in caching mode of lfs context (#1889) | ||||
|  - Allow context store name to be provided in the key | ||||
|  - Switch node: only use promises when absolutely necessary | ||||
|  - Fix dbl-click handling on webkit-based browsers | ||||
|  - Ensure context.flow/global cannot be deleted or enumerated | ||||
|  - Handle context.get with multiple levels of unknown key Fixes #1883 | ||||
|  - Fix global.get("foo.bar") for functionGlobalContext set values | ||||
|  - Fix node color bug (#1877) | ||||
|  - Merge pull request #1857 from cclauss/patch-1 | ||||
|  - Define raw_input() in Python 3 & fix time.sleep() | ||||
|  | ||||
| #### 0.19.3: Maintenance Release | ||||
|  | ||||
|  - Split node - fix complete to send msg for k/v object | ||||
|  - Remove unused Join node merged object key typed input | ||||
|  - Set the JavaScript editor to full-screen | ||||
|  - Filter global modules installed locally | ||||
|  - Add svg to permitted icon extension list | ||||
|  - Debug node - indicate status all the time if selected to do so | ||||
|  - pi nodes - increase test coverage slightly | ||||
|  - TCP-request node - only write payload | ||||
|  - JSON schema: perform validation when obj -> obj or str -> str | ||||
|  - JSON schema: add draft-06 support (via $schema keyword) | ||||
|  - Mqtt proxy configuration for websocket connection, #1651. | ||||
|  - Allows MQTT Shared Subscriptions for MQTT-In core node | ||||
|  - Fix use of HTML tag or CSS class specification as icon of typedInput | ||||
|  | ||||
| #### 0.19.2: Maintenance Release | ||||
|  | ||||
|  - Ensure node default colour is used if palette.theme has no match | ||||
|  - fix lost messages / properties in TCPRequest Node; closes #1863 (#1864) | ||||
|  - Fix typo in template.html | ||||
|  - Improve error reporting from context plugin loading | ||||
|  - Prevent no-op edit of node marking as changed due to icon | ||||
|  - Change node must handle empty rule set | ||||
|  | ||||
| #### 0.19.1: Maintenance Release | ||||
|  | ||||
|  - Pull in latest twitter node | ||||
|  - Handle windows paths for context storage | ||||
|  - Handle persisting objects with circular refs in context | ||||
|  - Ensure js editor can expand to fill available space | ||||
|  - Add example localfilesystem contextStorage to settings | ||||
|  - Fix template node handling of nested context tags | ||||
|  | ||||
| #### 0.19: Milestone Release | ||||
|  | ||||
| Editor | ||||
|  | ||||
|  - Add editorTheme.palette.theme to allow overriding colours | ||||
|  - Index all node properties when searching Fixes #1446 | ||||
|  - Handle NaN and Infinity properly in debug sidebar Fixes #1778 #1779 | ||||
|  - Prevent horizontal scroll when palette name cannot wrap | ||||
|  - Ignore middle-click on node/ports to enable panning | ||||
|  - Better wire layout when looping back | ||||
|  - fix appearence of retry button of remote branch management dialog | ||||
|  - Handle releasing ctrl when using quick-add node dialog | ||||
|  - Add $env function to JSONata expressions | ||||
|  - Widen support for env var to use ${} or $() syntax | ||||
|  - Add env-var support to TypedInput | ||||
|  - Show unknown node properties in info tab | ||||
|  - Add node icon picker widget | ||||
|  - Only edit nodes on dbl click on primary button with no modifiers | ||||
|  - Allow subflows to be put in any palette category | ||||
|  - Add flow navigator widget | ||||
|  - Cache flow library result to improve response time Fixes #1753 | ||||
|  - Add middle-button-drag to pan the workspace | ||||
|  - allow multi-line category name in editor | ||||
|  - Redesign sidebar tabs | ||||
|  - Do not disable the export-clipboard menu option with empty selection | ||||
|  | ||||
| Nodes | ||||
|  | ||||
|  - Change: Ensure runtime errors in Change node can be caught Fixes #1769 | ||||
|  - File: Add output to File Out node | ||||
|  - Function: add expandable JavaScript editor pane | ||||
|  - Function: allow id and name reference in function node code (#1731) | ||||
|  - HTTP Request: Move to request module | ||||
|  - HTTP: Ensure apiMaxLength applies to HTTP Nodes Fixes #1278 | ||||
|  - Join: accumulate top level properties | ||||
|  - Join: allow environment variable as reduce init value | ||||
|  - JSON: add JSON schema validation via msg.schema | ||||
|  - Pi: Let nrgpio code work with python 3 | ||||
|  - Pi: let Pi nodes be visible/editable on all platforms | ||||
|  - Switch: add isEmpty rule | ||||
|  - TCP: queue messages while connecting; closes #1414 | ||||
|  - TLS: Add servername option to TLS config node for SNI Fixes #1805 | ||||
|  - UDP: Don't accidentally re-use udp port when set to not do so | ||||
|  | ||||
| Persistent Context | ||||
|  | ||||
|  - Add Context data sidebar | ||||
|  - Add persistable context option | ||||
|  - Add default memory store | ||||
|  - Add file-based context store | ||||
|  - Add async mode to evaluateJSONataExpression | ||||
|  - Update RED.util.evaluateNodeProperty to support context stores | ||||
|  | ||||
| Runtime | ||||
|  | ||||
|  - Support flow.disabled and .info in /flow API | ||||
|  - Node errors should be Strings not Errors Fixes #1781 | ||||
|  - Add detection of connection timeout in git communication Fixes #1770 | ||||
|  - Handle loading empty nodesDir | ||||
|  - Add 'private' property to userDir generated package.json | ||||
|  - Add RED.require to allow nodes to access other modules | ||||
|  - Ensure add/remove modules are run sequentially | ||||
|  | ||||
| #### 0.18.7: Maintenance Release | ||||
|  | ||||
| Editor Fixes | ||||
|   | ||||
| @@ -24,6 +24,10 @@ module.exports = function(grunt) { | ||||
|         nodemonArgs.push(flowFile); | ||||
|     } | ||||
|  | ||||
|     var nonHeadless = grunt.option('non-headless'); | ||||
|     if (nonHeadless) { | ||||
|         process.env.NODE_RED_NON_HEADLESS = 'true'; | ||||
|     } | ||||
|     grunt.initConfig({ | ||||
|         pkg: grunt.file.readJSON('package.json'), | ||||
|         paths: { | ||||
| @@ -149,8 +153,10 @@ module.exports = function(grunt) { | ||||
|                     "editor/js/ui/palette.js", | ||||
|                     "editor/js/ui/tab-info.js", | ||||
|                     "editor/js/ui/tab-config.js", | ||||
|                     "editor/js/ui/tab-context.js", | ||||
|                     "editor/js/ui/palette-editor.js", | ||||
|                     "editor/js/ui/editor.js", | ||||
|                     "editor/js/ui/editors/*.js", | ||||
|                     "editor/js/ui/tray.js", | ||||
|                     "editor/js/ui/clipboard.js", | ||||
|                     "editor/js/ui/library.js", | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|  | ||||
| http://nodered.org | ||||
|  | ||||
| [](https://travis-ci.org/node-red/node-red) | ||||
| [](https://travis-ci.org/node-red/node-red) | ||||
| [](https://coveralls.io/r/node-red/node-red?branch=master) | ||||
|  | ||||
| A visual tool for wiring the Internet of Things. | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								editor/icons/file-in.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								editor/icons/file-in.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 542 B | 
							
								
								
									
										
											BIN
										
									
								
								editor/icons/file-out.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								editor/icons/file-out.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 503 B | 
| @@ -10,6 +10,7 @@ | ||||
|         "ctrl-g i": "core:show-info-tab", | ||||
|         "ctrl-g d": "core:show-debug-tab", | ||||
|         "ctrl-g c": "core:show-config-tab", | ||||
|         "ctrl-g x": "core:show-context-tab", | ||||
|         "ctrl-e": "core:show-export-dialog", | ||||
|         "ctrl-i": "core:show-import-dialog", | ||||
|         "ctrl-space": "core:toggle-sidebar", | ||||
|   | ||||
| @@ -276,9 +276,20 @@ RED.clipboard = (function() { | ||||
|         if (typeof value !== "string" ) { | ||||
|             value = JSON.stringify(value, function(key,value) { | ||||
|                 if (value !== null && typeof value === 'object') { | ||||
|                     if (value.__encoded__ && value.hasOwnProperty('data') && value.hasOwnProperty('length')) { | ||||
|                         truncated = value.data.length !== value.length; | ||||
|                         return value.data; | ||||
|                     if (value.__enc__) { | ||||
|                         if (value.hasOwnProperty('data') && value.hasOwnProperty('length')) { | ||||
|                             truncated = value.data.length !== value.length; | ||||
|                             return value.data; | ||||
|                         } | ||||
|                         if (value.type === 'function' || value.type === 'internal') { | ||||
|                             return undefined | ||||
|                         } | ||||
|                         if (value.type === 'number') { | ||||
|                             // Handle NaN and Infinity - they are not permitted | ||||
|                             // in JSON. We can either substitute with a String | ||||
|                             // representation or null | ||||
|                             return null; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 return value; | ||||
|   | ||||
| @@ -48,23 +48,31 @@ RED.popover = (function() { | ||||
|  | ||||
|         var openPopup = function(instant) { | ||||
|             if (active) { | ||||
|                 div = $('<div class="red-ui-popover red-ui-popover-'+direction+'"></div>').appendTo("body"); | ||||
|                 div = $('<div class="red-ui-popover red-ui-popover-'+direction+'"></div>'); | ||||
|                 if (size !== "default") { | ||||
|                     div.addClass("red-ui-popover-size-"+size); | ||||
|                 } | ||||
|                 if (typeof content === 'function') { | ||||
|                     content.call(res).appendTo(div); | ||||
|                     var result = content.call(res); | ||||
|                     if (result === null) { | ||||
|                         return; | ||||
|                     } | ||||
|                     if (typeof result === 'string') { | ||||
|                         div.text(result); | ||||
|                     } else { | ||||
|                         div.append(result); | ||||
|                     } | ||||
|                 } else { | ||||
|                     div.html(content); | ||||
|                 } | ||||
|                 if (width !== "auto") { | ||||
|                     div.width(width); | ||||
|                 } | ||||
|  | ||||
|                 div.appendTo("body"); | ||||
|  | ||||
|                 var targetPos = target.offset(); | ||||
|                 var targetWidth = target.width(); | ||||
|                 var targetHeight = target.height(); | ||||
|                 var targetWidth = target.outerWidth(); | ||||
|                 var targetHeight = target.outerHeight(); | ||||
|                 var divHeight = div.height(); | ||||
|                 var divWidth = div.width(); | ||||
|                 if (direction === 'right') { | ||||
| @@ -147,7 +155,17 @@ RED.popover = (function() { | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         create: createPopover | ||||
|         create: createPopover, | ||||
|         tooltip: function(target,content) { | ||||
|             RED.popover.create({ | ||||
|                 target:target, | ||||
|                 trigger: "hover", | ||||
|                 size: "small", | ||||
|                 direction: "bottom", | ||||
|                 content: content, | ||||
|                 delay: { show: 550, hide: 10 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| })(); | ||||
|   | ||||
| @@ -17,6 +17,9 @@ | ||||
|  | ||||
|  | ||||
| RED.tabs = (function() { | ||||
|  | ||||
|     var defaultTabIcon = "fa fa-lemon-o"; | ||||
|  | ||||
|     function createTabs(options) { | ||||
|         var tabs = {}; | ||||
|         var pinnedTabsCount = 0; | ||||
| @@ -71,6 +74,7 @@ RED.tabs = (function() { | ||||
|                         var id = $(el).data('tabId'); | ||||
|                         var opt = { | ||||
|                             id:"red-ui-tabs-menu-option-"+id, | ||||
|                             icon: tabs[id].iconClass || defaultTabIcon, | ||||
|                             label: tabs[id].name, | ||||
|                             onselect: function() { | ||||
|                                 activateTab(id); | ||||
| @@ -90,12 +94,12 @@ RED.tabs = (function() { | ||||
|                     collapsibleMenu.on('mouseleave', function(){ $(this).hide() }); | ||||
|                     collapsibleMenu.on('mouseup', function() { $(this).hide() }); | ||||
|                     collapsibleMenu.appendTo("body"); | ||||
|                     var elementPos = selectButton.offset(); | ||||
|                     collapsibleMenu.css({ | ||||
|                         top: (elementPos.top+selectButton.height()-20)+"px", | ||||
|                         left: (elementPos.left - collapsibleMenu.width() + selectButton.width())+"px" | ||||
|                     }) | ||||
|                 } | ||||
|                 var elementPos = selectButton.offset(); | ||||
|                 collapsibleMenu.css({ | ||||
|                     top: (elementPos.top+selectButton.height()-20)+"px", | ||||
|                     left: (elementPos.left - collapsibleMenu.width() + selectButton.width())+"px" | ||||
|                 }) | ||||
|                 collapsibleMenu.toggle(); | ||||
|             }) | ||||
|  | ||||
| @@ -170,8 +174,8 @@ RED.tabs = (function() { | ||||
|                 ul.children().css({"transition": "width 100ms"}); | ||||
|                 link.parent().addClass("active"); | ||||
|                 var parentId = link.parent().attr('id'); | ||||
|                 wrapper.find(".red-ui-tab-link-button").removeClass("active"); | ||||
|                 $("#"+parentId+"-link-button").addClass("active"); | ||||
|                 wrapper.find(".red-ui-tab-link-button").removeClass("active selected"); | ||||
|                 $("#"+parentId+"-link-button").addClass("active selected"); | ||||
|                 if (options.scrollable) { | ||||
|                     var pos = link.parent().position().left; | ||||
|                     if (pos-21 < 0) { | ||||
| @@ -339,7 +343,7 @@ RED.tabs = (function() { | ||||
|                     if (tab.iconClass) { | ||||
|                         $('<i>',{class:tab.iconClass}).appendTo(pinnedLink); | ||||
|                     } else { | ||||
|                         $('<i>',{class:"fa fa-lemon-o"}).appendTo(pinnedLink); | ||||
|                         $('<i>',{class:defaultTabIcon}).appendTo(pinnedLink); | ||||
|                     } | ||||
|                     pinnedLink.click(function(evt) { | ||||
|                         evt.preventDefault(); | ||||
| @@ -349,14 +353,7 @@ RED.tabs = (function() { | ||||
|                         pinnedLink.addClass("red-ui-tab-link-button-pinned"); | ||||
|                         pinnedTabsCount++; | ||||
|                     } | ||||
|                     RED.popover.create({ | ||||
|                         target:$(pinnedLink), | ||||
|                         trigger: "hover", | ||||
|                         size: "small", | ||||
|                         direction: "bottom", | ||||
|                         content: tab.name, | ||||
|                         delay: { show: 550, hide: 10 } | ||||
|                     }); | ||||
|                     RED.popover.tooltip($(pinnedLink), tab.name); | ||||
|  | ||||
|                 } | ||||
|                 link.on("click",onTabClick); | ||||
| @@ -370,7 +367,6 @@ RED.tabs = (function() { | ||||
|                         removeTab(tab.id); | ||||
|                     }); | ||||
|                 } | ||||
|                 updateTabWidths(); | ||||
|                 if (options.onadd) { | ||||
|                     options.onadd(tab); | ||||
|                 } | ||||
| @@ -455,6 +451,9 @@ RED.tabs = (function() { | ||||
|                         } | ||||
|                     }) | ||||
|                 } | ||||
|                 setTimeout(function() { | ||||
|                     updateTabWidths(); | ||||
|                 },10); | ||||
|                 collapsibleMenu = null; | ||||
|             }, | ||||
|             removeTab: removeTab, | ||||
|   | ||||
| @@ -14,10 +14,38 @@ | ||||
|  * limitations under the License. | ||||
|  **/ | ||||
| (function($) { | ||||
|     var contextParse = function(v) { | ||||
|         var parts = RED.utils.parseContextKey(v); | ||||
|         return { | ||||
|             option: parts.store, | ||||
|             value: parts.key | ||||
|         } | ||||
|     } | ||||
|     var contextExport = function(v,opt) { | ||||
|         if (!opt) { | ||||
|             return v; | ||||
|         } | ||||
|         var store = ((typeof opt === "string")?opt:opt.value) | ||||
|         if (store !== RED.settings.context.default) { | ||||
|             return "#:("+store+")::"+v; | ||||
|         } else { | ||||
|             return v; | ||||
|         } | ||||
|     } | ||||
|     var allOptions = { | ||||
|         msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression}, | ||||
|         flow: {value:"flow",label:"flow.",validate:RED.utils.validatePropertyExpression}, | ||||
|         global: {value:"global",label:"global.",validate:RED.utils.validatePropertyExpression}, | ||||
|         flow: {value:"flow",label:"flow.",hasValue:true, | ||||
|             options:[], | ||||
|             validate:RED.utils.validatePropertyExpression, | ||||
|             parse: contextParse, | ||||
|             export: contextExport | ||||
|         }, | ||||
|         global: {value:"global",label:"global.",hasValue:true, | ||||
|             options:[], | ||||
|             validate:RED.utils.validatePropertyExpression, | ||||
|             parse: contextParse, | ||||
|             export: contextExport | ||||
|         }, | ||||
|         str: {value:"str",label:"string",icon:"red/images/typedInput/az.png"}, | ||||
|         num: {value:"num",label:"number",icon:"red/images/typedInput/09.png",validate:/^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/}, | ||||
|         bool: {value:"bool",label:"boolean",icon:"red/images/typedInput/bool.png",options:["true","false"]}, | ||||
| @@ -87,25 +115,40 @@ | ||||
|  | ||||
|     $.widget( "nodered.typedInput", { | ||||
|         _create: function() { | ||||
|             try { | ||||
|             if (!nlsd && RED && RED._) { | ||||
|                 for (var i in allOptions) { | ||||
|                     if (allOptions.hasOwnProperty(i)) { | ||||
|                         allOptions[i].label = RED._("typedInput.type."+i,{defaultValue:allOptions[i].label}); | ||||
|                     } | ||||
|                 } | ||||
|                 var contextStores = RED.settings.context.stores; | ||||
|                 var contextOptions = contextStores.map(function(store) { | ||||
|                     return {value:store,label: store, icon:'<i class="red-ui-typedInput-icon fa fa-database" style="color: #'+(store==='memory'?'ddd':'777')+'"></i>'} | ||||
|                 }) | ||||
|                 if (contextOptions.length < 2) { | ||||
|                     allOptions.flow.options = []; | ||||
|                     allOptions.global.options = []; | ||||
|                 } else { | ||||
|                     allOptions.flow.options = contextOptions; | ||||
|                     allOptions.global.options = contextOptions; | ||||
|                 } | ||||
|             } | ||||
|             nlsd = true; | ||||
|             var that = this; | ||||
|  | ||||
|             this.disarmClick = false; | ||||
|             this.input = $('<input type="text"></input>'); | ||||
|             this.input.insertAfter(this.element); | ||||
|             this.input.val(this.element.val()); | ||||
|             this.element.addClass('red-ui-typedInput'); | ||||
|             this.uiWidth = this.element.outerWidth(); | ||||
|             this.elementDiv = this.element.wrap("<div>").parent().addClass('red-ui-typedInput-input'); | ||||
|             this.elementDiv = this.input.wrap("<div>").parent().addClass('red-ui-typedInput-input'); | ||||
|             this.uiSelect = this.elementDiv.wrap( "<div>" ).parent(); | ||||
|             var attrStyle = this.element.attr('style'); | ||||
|             var m; | ||||
|             if ((m = /width\s*:\s*(calc\s*\(.*\)|\d+(%|px))/i.exec(attrStyle)) !== null) { | ||||
|                 this.element.css('width','100%'); | ||||
|                 this.input.css('width','100%'); | ||||
|                 this.uiSelect.width(m[1]); | ||||
|                 this.uiWidth = null; | ||||
|             } else { | ||||
| @@ -114,17 +157,19 @@ | ||||
|             ["Right","Left"].forEach(function(d) { | ||||
|                 var m = that.element.css("margin"+d); | ||||
|                 that.uiSelect.css("margin"+d,m); | ||||
|                 that.element.css("margin"+d,0); | ||||
|                 that.input.css("margin"+d,0); | ||||
|             }); | ||||
|  | ||||
|             this.uiSelect.addClass("red-ui-typedInput-container"); | ||||
|  | ||||
|             this.element.attr('type','hidden'); | ||||
|  | ||||
|             this.options.types = this.options.types||Object.keys(allOptions); | ||||
|  | ||||
|             this.selectTrigger = $('<button tabindex="0"></button>').prependTo(this.uiSelect); | ||||
|             if (this.options.types.length > 1) { | ||||
|                 $('<i class="fa fa-sort-desc"></i>').appendTo(this.selectTrigger); | ||||
|             } | ||||
|             this.selectLabel = $('<span></span>').appendTo(this.selectTrigger); | ||||
|             $('<i class="red-ui-typedInput-icon fa fa-sort-desc"></i>').toggle(this.options.types.length > 1).appendTo(this.selectTrigger); | ||||
|  | ||||
|             this.selectLabel = $('<span class="red-ui-typedInput-type-label"></span>').appendTo(this.selectTrigger); | ||||
|  | ||||
|             this.types(this.options.types); | ||||
|  | ||||
| @@ -138,14 +183,16 @@ | ||||
|                 this.typeField = $("<input>",{type:'hidden'}).appendTo(this.uiSelect); | ||||
|             } | ||||
|  | ||||
|             this.element.on('focus', function() { | ||||
|             this.input.on('focus', function() { | ||||
|                 that.uiSelect.addClass('red-ui-typedInput-focus'); | ||||
|             }); | ||||
|             this.element.on('blur', function() { | ||||
|             this.input.on('blur', function() { | ||||
|                 that.uiSelect.removeClass('red-ui-typedInput-focus'); | ||||
|             }); | ||||
|             this.element.on('change', function() { | ||||
|             this.input.on('change', function() { | ||||
|                 that.validate(); | ||||
|                 that.element.val(that.value()); | ||||
|                 that.element.trigger('change',that.propertyType,that.value()); | ||||
|             }) | ||||
|             this.selectTrigger.click(function(event) { | ||||
|                 event.preventDefault(); | ||||
| @@ -161,8 +208,11 @@ | ||||
|             }) | ||||
|  | ||||
|             // explicitly set optionSelectTrigger display to inline-block otherwise jQ sets it to 'inline' | ||||
|             this.optionSelectTrigger = $('<button tabindex="0" class="red-ui-typedInput-option-trigger" style="display:inline-block"><span class="red-ui-typedInput-option-caret"><i class="fa fa-sort-desc"></i></span></button>').appendTo(this.uiSelect); | ||||
|             this.optionSelectTrigger = $('<button tabindex="0" class="red-ui-typedInput-option-trigger" style="display:inline-block"><span class="red-ui-typedInput-option-caret"><i class="red-ui-typedInput-icon fa fa-sort-desc"></i></span></button>').appendTo(this.uiSelect); | ||||
|             this.optionSelectLabel = $('<span class="red-ui-typedInput-option-label"></span>').prependTo(this.optionSelectTrigger); | ||||
|             RED.popover.tooltip(this.optionSelectLabel,function() { | ||||
|                 return that.optionValue; | ||||
|             }); | ||||
|             this.optionSelectTrigger.click(function(event) { | ||||
|                 event.preventDefault(); | ||||
|                 that._showOptionSelectMenu(); | ||||
| @@ -177,17 +227,18 @@ | ||||
|                 that.uiSelect.addClass('red-ui-typedInput-focus'); | ||||
|             }); | ||||
|  | ||||
|             this.optionExpandButton = $('<button tabindex="0" class="red-ui-typedInput-option-expand" style="display:inline-block"><i class="fa fa-ellipsis-h"></i></button>').appendTo(this.uiSelect); | ||||
|  | ||||
|  | ||||
|             this.optionExpandButton = $('<button tabindex="0" class="red-ui-typedInput-option-expand" style="display:inline-block"><i class="red-ui-typedInput-icon fa fa-ellipsis-h"></i></button>').appendTo(this.uiSelect); | ||||
|             this.type(this.options.default||this.typeList[0].value); | ||||
|         }catch(err) { | ||||
|             console.log(err.stack); | ||||
|         } | ||||
|         }, | ||||
|         _showTypeMenu: function() { | ||||
|             if (this.typeList.length > 1) { | ||||
|                 this._showMenu(this.menu,this.selectTrigger); | ||||
|                 this.menu.find("[value='"+this.propertyType+"']").focus(); | ||||
|             } else { | ||||
|                 this.element.focus(); | ||||
|                 this.input.focus(); | ||||
|             } | ||||
|         }, | ||||
|         _showOptionSelectMenu: function() { | ||||
| @@ -196,8 +247,8 @@ | ||||
|                     minWidth:this.optionSelectLabel.width() | ||||
|                 }); | ||||
|  | ||||
|                 this._showMenu(this.optionMenu,this.optionSelectLabel); | ||||
|                 var selectedOption = this.optionMenu.find("[value='"+this.value()+"']"); | ||||
|                 this._showMenu(this.optionMenu,this.optionSelectTrigger); | ||||
|                 var selectedOption = this.optionMenu.find("[value='"+this.optionValue+"']"); | ||||
|                 if (selectedOption.length === 0) { | ||||
|                     selectedOption = this.optionMenu.children(":first"); | ||||
|                 } | ||||
| @@ -209,7 +260,7 @@ | ||||
|             $(document).off("mousedown.close-property-select"); | ||||
|             menu.hide(); | ||||
|             if (this.elementDiv.is(":visible")) { | ||||
|                 this.element.focus(); | ||||
|                 this.input.focus(); | ||||
|             } else if (this.optionSelectTrigger.is(":visible")){ | ||||
|                 this.optionSelectTrigger.focus(); | ||||
|             } else { | ||||
| @@ -228,10 +279,19 @@ | ||||
|                     op.text(opt.label); | ||||
|                 } | ||||
|                 if (opt.icon) { | ||||
|                     $('<img>',{src:opt.icon,style:"margin-right: 4px; height: 18px;"}).prependTo(op); | ||||
|                     if (opt.icon.indexOf("<") === 0) { | ||||
|                         $(opt.icon).prependTo(op); | ||||
|                     } else if (opt.icon.indexOf("/") !== -1) { | ||||
|                         $('<img>',{src:opt.icon,style:"margin-right: 4px; height: 18px;"}).prependTo(op); | ||||
|                     } else { | ||||
|                         $('<i>',{class:"red-ui-typedInput-icon "+opt.icon}).prependTo(op); | ||||
|                     } | ||||
|                 } else { | ||||
|                     op.css({paddingLeft: "18px"}); | ||||
|                 } | ||||
|                 if (!opt.icon && !opt.label) { | ||||
|                     op.text(opt.value); | ||||
|                 } | ||||
|  | ||||
|                 op.click(function(event) { | ||||
|                     event.preventDefault(); | ||||
| @@ -310,7 +370,8 @@ | ||||
|             if (this.uiWidth !== null) { | ||||
|                 this.uiSelect.width(this.uiWidth); | ||||
|             } | ||||
|             if (this.typeMap[this.propertyType] && this.typeMap[this.propertyType].hasValue === false) { | ||||
|             var type = this.typeMap[this.propertyType]; | ||||
|             if (type && type.hasValue === false) { | ||||
|                 this.selectTrigger.addClass("red-ui-typedInput-full-width"); | ||||
|             } else { | ||||
|                 this.selectTrigger.removeClass("red-ui-typedInput-full-width"); | ||||
| @@ -320,13 +381,68 @@ | ||||
|                     this.elementDiv.css('right',"22px"); | ||||
|                 } else { | ||||
|                     this.elementDiv.css('right','0'); | ||||
|                     this.input.css({ | ||||
|                         'border-top-right-radius': '4px', | ||||
|                         'border-bottom-right-radius': '4px' | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 // if (this.optionSelectTrigger) { | ||||
|                 //     this.optionSelectTrigger.css({'left':(labelWidth)+"px",'width':'calc( 100% - '+labelWidth+'px )'}); | ||||
|                 // } | ||||
|  | ||||
|                 if (this.optionSelectTrigger) { | ||||
|                     this.optionSelectTrigger.css({'left':(labelWidth)+"px",'width':'calc( 100% - '+labelWidth+'px )'}); | ||||
|                     if (type && type.options && type.hasValue === true) { | ||||
|                         this.optionSelectLabel.css({'left':'auto'}) | ||||
|                         var lw = this._getLabelWidth(this.optionSelectLabel); | ||||
|                         this.optionSelectTrigger.css({'width':(23+lw)+"px"}); | ||||
|                         this.elementDiv.css('right',(23+lw)+"px"); | ||||
|                         this.input.css({ | ||||
|                             'border-top-right-radius': 0, | ||||
|                             'border-bottom-right-radius': 0 | ||||
|                         }); | ||||
|                     } else { | ||||
|                         this.optionSelectLabel.css({'left':'0'}) | ||||
|                         this.optionSelectTrigger.css({'width':'calc( 100% - '+labelWidth+'px )'}); | ||||
|                         if (!this.optionExpandButton.is(":visible")) { | ||||
|                             this.elementDiv.css({'right':0}); | ||||
|                             this.input.css({ | ||||
|                                 'border-top-right-radius': '4px', | ||||
|                                 'border-bottom-right-radius': '4px' | ||||
|                             }); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         _updateOptionSelectLabel: function(o) { | ||||
|             var opt = this.typeMap[this.propertyType]; | ||||
|             this.optionSelectLabel.empty(); | ||||
|             if (o.icon) { | ||||
|                 if (o.icon.indexOf("<") === 0) { | ||||
|                     $(o.icon).prependTo(this.optionSelectLabel); | ||||
|                 } else if (o.icon.indexOf("/") !== -1) { | ||||
|                     // url | ||||
|                     $('<img>',{src:o.icon,style:"height: 18px;"}).prependTo(this.optionSelectLabel); | ||||
|                 } else { | ||||
|                     // icon class | ||||
|                     $('<i>',{class:"red-ui-typedInput-icon "+o.icon}).prependTo(this.optionSelectLabel); | ||||
|                 } | ||||
|             } else if (o.label) { | ||||
|                 this.optionSelectLabel.text(o.label); | ||||
|             } else { | ||||
|                 this.optionSelectLabel.text(o.value); | ||||
|             } | ||||
|             if (opt.hasValue) { | ||||
|                 this.optionValue = o.value; | ||||
|                 this._resize(); | ||||
|                 this.input.trigger('change',this.propertyType,this.value()); | ||||
|             } | ||||
|         }, | ||||
|         _destroy: function() { | ||||
|             if (this.optionMenu) { | ||||
|                 this.optionMenu.remove(); | ||||
|             } | ||||
|             this.menu.remove(); | ||||
|         }, | ||||
|         types: function(types) { | ||||
| @@ -344,13 +460,18 @@ | ||||
|                 return result; | ||||
|             }); | ||||
|             this.selectTrigger.toggleClass("disabled", this.typeList.length === 1); | ||||
|             this.selectTrigger.find(".fa-sort-desc").toggle(this.typeList.length > 1) | ||||
|             if (this.menu) { | ||||
|                 this.menu.remove(); | ||||
|             } | ||||
|             this.menu = this._createMenu(this.typeList, function(v) { that.type(v) }); | ||||
|             if (currentType && !this.typeMap.hasOwnProperty(currentType)) { | ||||
|                 this.type(this.typeList[0].value); | ||||
|             } else { | ||||
|                 this.propertyType = null; | ||||
|                 this.type(currentType); | ||||
|             } | ||||
|             setTimeout(function() {that._resize();},0); | ||||
|         }, | ||||
|         width: function(desiredWidth) { | ||||
|             this.uiWidth = desiredWidth; | ||||
| @@ -358,33 +479,33 @@ | ||||
|         }, | ||||
|         value: function(value) { | ||||
|             if (!arguments.length) { | ||||
|                 return this.element.val(); | ||||
|                 var v = this.input.val(); | ||||
|                 if (this.typeMap[this.propertyType].export) { | ||||
|                     v = this.typeMap[this.propertyType].export(v,this.optionValue) | ||||
|                 } | ||||
|                 return v; | ||||
|             } else { | ||||
|                 var selectedOption; | ||||
|                 if (this.typeMap[this.propertyType].options) { | ||||
|                     var validValue = false; | ||||
|                     var label; | ||||
|                     for (var i=0;i<this.typeMap[this.propertyType].options.length;i++) { | ||||
|                         var op = this.typeMap[this.propertyType].options[i]; | ||||
|                         if (typeof op === "string") { | ||||
|                             if (op === value) { | ||||
|                                 label = value; | ||||
|                                 validValue = true; | ||||
|                                 selectedOption = this.activeOptions[op]; | ||||
|                                 break; | ||||
|                             } | ||||
|                         } else if (op.value === value) { | ||||
|                             label = op.label||op.value; | ||||
|                             validValue = true; | ||||
|                             selectedOption = op; | ||||
|                             break; | ||||
|                         } | ||||
|                     } | ||||
|                     if (!validValue) { | ||||
|                         value = ""; | ||||
|                         label = ""; | ||||
|                     if (!selectedOption) { | ||||
|                         selectedOption = {value:""} | ||||
|                     } | ||||
|                     this.optionSelectLabel.text(label); | ||||
|                     this._updateOptionSelectLabel(selectedOption) | ||||
|                 } | ||||
|                 this.element.val(value); | ||||
|                 this.element.trigger('change',this.type(),value); | ||||
|                 this.input.val(value); | ||||
|                 this.input.trigger('change',this.type(),value); | ||||
|             } | ||||
|         }, | ||||
|         type: function(type) { | ||||
| @@ -395,85 +516,148 @@ | ||||
|                 var opt = this.typeMap[type]; | ||||
|                 if (opt && this.propertyType !== type) { | ||||
|                     this.propertyType = type; | ||||
|                     this.typeField.val(type); | ||||
|                     if (this.typeField) { | ||||
|                         this.typeField.val(type); | ||||
|                     } | ||||
|                     this.selectLabel.empty(); | ||||
|                     var image; | ||||
|                     if (opt.icon) { | ||||
|                         image = new Image(); | ||||
|                         image.name = opt.icon; | ||||
|                         image.src = opt.icon; | ||||
|                         $('<img>',{src:opt.icon,style:"margin-right: 4px;height: 18px;"}).prependTo(this.selectLabel); | ||||
|                         if (opt.icon.indexOf("<") === 0) { | ||||
|                             $(opt.icon).prependTo(this.selectLabel); | ||||
|                         } | ||||
|                         else if (opt.icon.indexOf("/") !== -1) { | ||||
|                             image = new Image(); | ||||
|                             image.name = opt.icon; | ||||
|                             image.src = opt.icon; | ||||
|                             $('<img>',{src:opt.icon,style:"margin-right: 4px;height: 18px;"}).prependTo(this.selectLabel); | ||||
|                         } | ||||
|                         else { | ||||
|                             $('<i>',{class:"red-ui-typedInput-icon "+opt.icon}).prependTo(this.selectLabel); | ||||
|                         } | ||||
|                     } else { | ||||
|                         this.selectLabel.text(opt.label); | ||||
|                     } | ||||
|                     if (this.optionMenu) { | ||||
|                         this.optionMenu.remove(); | ||||
|                         this.optionMenu = null; | ||||
|                     } | ||||
|                     if (opt.options) { | ||||
|                         if (this.optionExpandButton) { | ||||
|                             this.optionExpandButton.hide(); | ||||
|                         } | ||||
|                         if (this.optionSelectTrigger) { | ||||
|                             this.optionSelectTrigger.show(); | ||||
|                             this.elementDiv.hide(); | ||||
|                             this.optionMenu = this._createMenu(opt.options,function(v){ | ||||
|                                 that.optionSelectLabel.text(v); | ||||
|                                 that.value(v); | ||||
|                             if (!opt.hasValue) { | ||||
|                                 this.elementDiv.hide(); | ||||
|                             } else { | ||||
|                                 this.elementDiv.show(); | ||||
|                             } | ||||
|                             this.activeOptions = {}; | ||||
|                             opt.options.forEach(function(o) { | ||||
|                                 if (typeof o === 'string') { | ||||
|                                     that.activeOptions[o] = {label:o,value:o}; | ||||
|                                 } else { | ||||
|                                     that.activeOptions[o.value] = o; | ||||
|                                 } | ||||
|                             }); | ||||
|  | ||||
|                             if (!that.activeOptions.hasOwnProperty(that.optionValue)) { | ||||
|                                 that.optionValue = null; | ||||
|                             } | ||||
|                             this.optionMenu = this._createMenu(opt.options,function(v){ | ||||
|                                 that._updateOptionSelectLabel(that.activeOptions[v]); | ||||
|                                 if (!opt.hasValue) { | ||||
|                                     that.value(that.activeOptions[v].value) | ||||
|                                 } | ||||
|                             }); | ||||
|                             var currentVal = this.element.val(); | ||||
|                             var validValue = false; | ||||
|                             var op; | ||||
|                             for (var i=0;i<opt.options.length;i++) { | ||||
|                                 op = opt.options[i]; | ||||
|                                 if (typeof op === "string") { | ||||
|                                     if (op === currentVal) { | ||||
|                                         this.optionSelectLabel.text(currentVal); | ||||
|                             if (!opt.hasValue) { | ||||
|                                 var currentVal = this.input.val(); | ||||
|                                 var validValue = false; | ||||
|                                 for (var i=0;i<opt.options.length;i++) { | ||||
|                                     op = opt.options[i]; | ||||
|                                     if (typeof op === "string" && op === currentVal) { | ||||
|                                         that._updateOptionSelectLabel({value:currentVal}); | ||||
|                                         validValue = true; | ||||
|                                         break; | ||||
|                                     } else if (op.value === currentVal) { | ||||
|                                         that._updateOptionSelectLabel(op); | ||||
|                                         validValue = true; | ||||
|                                         break; | ||||
|                                     } | ||||
|                                 } else if (op.value === currentVal) { | ||||
|                                     this.optionSelectLabel.text(op.label||op.value); | ||||
|                                     validValue = true; | ||||
|                                     break; | ||||
|                                 } | ||||
|                             } | ||||
|                             if (!validValue) { | ||||
|                                 op = opt.options[0]; | ||||
|                                 if (typeof op === "string") { | ||||
|                                     this.value(op); | ||||
|                                 if (!validValue) { | ||||
|                                     op = opt.options[0]; | ||||
|                                     if (typeof op === "string") { | ||||
|                                         this.value(op); | ||||
|                                         that._updateOptionSelectLabel({value:op}); | ||||
|                                     } else { | ||||
|                                         this.value(op.value); | ||||
|                                         that._updateOptionSelectLabel(op); | ||||
|                                     } | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 var selectedOption = this.optionValue||opt.options[0]; | ||||
|                                 if (opt.parse) { | ||||
|                                     var parts = opt.parse(this.input.val()); | ||||
|                                     if (parts.option) { | ||||
|                                         selectedOption = parts.option; | ||||
|                                         if (!this.activeOptions.hasOwnProperty(selectedOption)) { | ||||
|                                             parts.option = Object.keys(this.activeOptions)[0]; | ||||
|                                             selectedOption = parts.option | ||||
|                                         } | ||||
|                                     } | ||||
|                                     this.input.val(parts.value); | ||||
|                                     if (opt.export) { | ||||
|                                         this.element.val(opt.export(parts.value,parts.option||selectedOption)); | ||||
|                                     } | ||||
|                                 } | ||||
|                                 if (typeof selectedOption === "string") { | ||||
|                                     this.optionValue = selectedOption; | ||||
|                                     if (!this.activeOptions.hasOwnProperty(selectedOption)) { | ||||
|                                         selectedOption = Object.keys(this.activeOptions)[0]; | ||||
|                                     } | ||||
|                                     if (!selectedOption) { | ||||
|                                         this.optionSelectTrigger.hide(); | ||||
|                                     } else { | ||||
|                                         this._updateOptionSelectLabel(this.activeOptions[selectedOption]); | ||||
|                                     } | ||||
|                                 } else if (selectedOption) { | ||||
|                                     this.optionValue = selectedOption.value; | ||||
|                                     this._updateOptionSelectLabel(selectedOption); | ||||
|                                 } else { | ||||
|                                     this.value(op.value); | ||||
|                                     this.optionSelectTrigger.hide(); | ||||
|                                 } | ||||
|                             } | ||||
|                             console.log(validValue); | ||||
|                         } | ||||
|                     } else { | ||||
|                         if (this.optionMenu) { | ||||
|                             this.optionMenu.remove(); | ||||
|                             this.optionMenu = null; | ||||
|                         } | ||||
|                         if (this.optionSelectTrigger) { | ||||
|                             this.optionSelectTrigger.hide(); | ||||
|                         } | ||||
|                         if (opt.hasValue === false) { | ||||
|                             this.oldValue = this.element.val(); | ||||
|                             this.element.val(""); | ||||
|                             this.oldValue = this.input.val(); | ||||
|                             this.input.val(""); | ||||
|                             this.elementDiv.hide(); | ||||
|                         } else { | ||||
|                             if (this.oldValue !== undefined) { | ||||
|                                 this.element.val(this.oldValue); | ||||
|                                 this.input.val(this.oldValue); | ||||
|                                 delete this.oldValue; | ||||
|                             } | ||||
|                             this.elementDiv.show(); | ||||
|                         } | ||||
|                         if (opt.expand && typeof opt.expand === 'function') { | ||||
|                             this.optionExpandButton.show(); | ||||
|                             this.optionExpandButton.off('click'); | ||||
|                             this.optionExpandButton.on('click',function(evt) { | ||||
|                                 evt.preventDefault(); | ||||
|                                 opt.expand.call(that); | ||||
|                             }) | ||||
|                         } else { | ||||
|                             this.optionExpandButton.hide(); | ||||
|                         if (this.optionExpandButton) { | ||||
|                             if (opt.expand && typeof opt.expand === 'function') { | ||||
|                                 this.optionExpandButton.show(); | ||||
|                                 this.optionExpandButton.off('click'); | ||||
|                                 this.optionExpandButton.on('click',function(evt) { | ||||
|                                     evt.preventDefault(); | ||||
|                                     opt.expand.call(that); | ||||
|                                 }) | ||||
|                             } else { | ||||
|                                 this.optionExpandButton.hide(); | ||||
|                             } | ||||
|                         } | ||||
|                         this.element.trigger('change',this.propertyType,this.value()); | ||||
|                         this.input.trigger('change',this.propertyType,this.value()); | ||||
|                     } | ||||
|                     if (image) { | ||||
|                         image.onload = function() { that._resize(); } | ||||
|   | ||||
| @@ -490,7 +490,7 @@ RED.diff = (function() { | ||||
|     } | ||||
|     function createNodeIcon(node,def) { | ||||
|         var nodeDiv = $("<div>",{class:"node-diff-node-entry-node"}); | ||||
|         var colour = def.color; | ||||
|         var colour = RED.utils.getNodeColor(node.type,def); | ||||
|         var icon_url = RED.utils.getNodeIcon(def,node); | ||||
|         if (node.type === 'tab') { | ||||
|             colour = "#C0DEED"; | ||||
| @@ -881,7 +881,6 @@ RED.diff = (function() { | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         var properties = Object.keys(node).filter(function(p) { return p!='inputLabels'&&p!='outputLabels'&&p!='z'&&p!='wires'&&p!=='x'&&p!=='y'&&p!=='id'&&p!=='type'&&(!def.defaults||!def.defaults.hasOwnProperty(p))}); | ||||
|         if (def.defaults) { | ||||
|             properties = properties.concat(Object.keys(def.defaults)); | ||||
| @@ -889,6 +888,13 @@ RED.diff = (function() { | ||||
|         if (node.type !== 'tab') { | ||||
|             properties = properties.concat(['inputLabels','outputLabels']); | ||||
|         } | ||||
|         if ( ((localNode && localNode.hasOwnProperty('icon')) || (remoteNode && remoteNode.hasOwnProperty('icon'))) && | ||||
|             properties.indexOf('icon') === -1 | ||||
|         ) { | ||||
|             properties.unshift('icon'); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         properties.forEach(function(d) { | ||||
|             localChanged = false; | ||||
|             remoteChanged = false; | ||||
|   | ||||
| @@ -496,6 +496,8 @@ RED.editor = (function() { | ||||
|             label = node.type; | ||||
|             if (node.type === '_expression') { | ||||
|                 label = RED._("expressionEditor.title"); | ||||
|             } else if (node.type === '_js') { | ||||
|                 label = RED._("jsEditor.title"); | ||||
|             } else if (node.type === '_json') { | ||||
|                 label = RED._("jsonEditor.title"); | ||||
|             } else if (node.type === '_markdown') { | ||||
| @@ -704,7 +706,7 @@ RED.editor = (function() { | ||||
|         pickerBackground.on("mousedown", hide); | ||||
|  | ||||
|         var searchDiv = $("<div>",{class:"red-ui-search-container"}).appendTo(picker); | ||||
|         searchInput = $('<input type="text">').attr("placeholder","Search icons").appendTo(searchDiv).searchBox({ | ||||
|         searchInput = $('<input type="text">').attr("placeholder",RED._("editor.searchIcons")).appendTo(searchDiv).searchBox({ | ||||
|             delay: 50, | ||||
|             change: function() { | ||||
|                 var searchTerm = $(this).val().trim(); | ||||
| @@ -728,7 +730,7 @@ RED.editor = (function() { | ||||
|         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 class="editor-button editor-button-small">use default</button>').appendTo(metaRow).click(function(e) { | ||||
|         var resetButton = $('<button class="editor-button editor-button-small">'+RED._("editor.useDefault")+'</button>').appendTo(metaRow).click(function(e) { | ||||
|             e.preventDefault(); | ||||
|             hide(); | ||||
|             done(null); | ||||
| @@ -743,7 +745,7 @@ RED.editor = (function() { | ||||
|                 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 colour = node._def.color; | ||||
|                     var colour = RED.utils.getNodeColor(node.type, node._def); | ||||
|                     var icon_url = "icons/"+moduleName+"/"+icon; | ||||
|                     iconDiv.data('icon',icon_url) | ||||
|                     nodeDiv.css('backgroundColor',colour); | ||||
| @@ -814,7 +816,7 @@ RED.editor = (function() { | ||||
|             var iconButton = $('<button class="editor-button">').appendTo(iconRow); | ||||
|  | ||||
|             var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(iconButton); | ||||
|             var colour = node._def.color; | ||||
|             var colour = RED.utils.getNodeColor(node.type, node._def); | ||||
|             var icon_url = RED.utils.getNodeIcon(node._def,node); | ||||
|             nodeDiv.css('backgroundColor',colour); | ||||
|             var iconContainer = $('<div/>',{class:"palette_icon_container"}).appendTo(nodeDiv); | ||||
| @@ -1108,7 +1110,7 @@ RED.editor = (function() { | ||||
|                                     changed = true; | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 if (icon !== defaultIcon) { | ||||
|                                 if (icon !== "" && icon !== defaultIcon) { | ||||
|                                     changes.icon = editing_node.icon; | ||||
|                                     editing_node.icon = icon; | ||||
|                                     changed = true; | ||||
| @@ -1867,658 +1869,22 @@ RED.editor = (function() { | ||||
|         RED.tray.show(trayOptions); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     var expressionTestCache = {}; | ||||
|  | ||||
|     function editExpression(options) { | ||||
|         var expressionTestCacheId = "_"; | ||||
|         if (editStack.length > 0) { | ||||
|             expressionTestCacheId = editStack[editStack.length-1].id; | ||||
|         } | ||||
|  | ||||
|         var value = options.value; | ||||
|         var onComplete = options.complete; | ||||
|         var type = "_expression" | ||||
|         editStack.push({type:type}); | ||||
|         RED.view.state(RED.state.EDITING); | ||||
|         var expressionEditor; | ||||
|         var testDataEditor; | ||||
|         var testResultEditor | ||||
|         var panels; | ||||
|  | ||||
|         var trayOptions = { | ||||
|             title: getEditStackTitle(), | ||||
|             width: "inherit", | ||||
|             buttons: [ | ||||
|                 { | ||||
|                     id: "node-dialog-cancel", | ||||
|                     text: RED._("common.label.cancel"), | ||||
|                     click: function() { | ||||
|                         RED.tray.close(); | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     id: "node-dialog-ok", | ||||
|                     text: RED._("common.label.done"), | ||||
|                     class: "primary", | ||||
|                     click: function() { | ||||
|                         $("#node-input-expression-help").text(""); | ||||
|                         onComplete(expressionEditor.getValue()); | ||||
|                         RED.tray.close(); | ||||
|                     } | ||||
|                 } | ||||
|             ], | ||||
|             resize: function(dimensions) { | ||||
|                 if (dimensions) { | ||||
|                     editTrayWidthCache[type] = dimensions.width; | ||||
|                 } | ||||
|                 var height = $("#dialog-form").height(); | ||||
|                 if (panels) { | ||||
|                     panels.resize(height); | ||||
|                 } | ||||
|  | ||||
|             }, | ||||
|             open: function(tray) { | ||||
|                 var trayBody = tray.find('.editor-tray-body'); | ||||
|                 trayBody.addClass("node-input-expression-editor") | ||||
|                 var dialogForm = buildEditForm(tray.find('.editor-tray-body'),'dialog-form','_expression','editor'); | ||||
|                 var funcSelect = $("#node-input-expression-func"); | ||||
|                 Object.keys(jsonata.functions).forEach(function(f) { | ||||
|                     funcSelect.append($("<option></option>").val(f).text(f)); | ||||
|                 }) | ||||
|                 funcSelect.change(function(e) { | ||||
|                     var f = $(this).val(); | ||||
|                     var args = RED._('jsonata:'+f+".args",{defaultValue:''}); | ||||
|                     var title = "<h5>"+f+"("+args+")</h5>"; | ||||
|                     var body = marked(RED._('jsonata:'+f+'.desc',{defaultValue:''})); | ||||
|                     $("#node-input-expression-help").html(title+"<p>"+body+"</p>"); | ||||
|  | ||||
|                 }) | ||||
|                 expressionEditor = RED.editor.createEditor({ | ||||
|                     id: 'node-input-expression', | ||||
|                     value: "", | ||||
|                     mode:"ace/mode/jsonata", | ||||
|                     options: { | ||||
|                         enableBasicAutocompletion:true, | ||||
|                         enableSnippets:true, | ||||
|                         enableLiveAutocompletion: true | ||||
|                     } | ||||
|                 }); | ||||
|                 var currentToken = null; | ||||
|                 var currentTokenPos = -1; | ||||
|                 var currentFunctionMarker = null; | ||||
|  | ||||
|                 expressionEditor.getSession().setValue(value||"",-1); | ||||
|                 expressionEditor.on("changeSelection", function() { | ||||
|                     var c = expressionEditor.getCursorPosition(); | ||||
|                     var token = expressionEditor.getSession().getTokenAt(c.row,c.column); | ||||
|                     if (token !== currentToken || (token && /paren/.test(token.type) && c.column !== currentTokenPos)) { | ||||
|                         currentToken = token; | ||||
|                         var r,p; | ||||
|                         var scopedFunction = null; | ||||
|                         if (token && token.type === 'keyword') { | ||||
|                             r = c.row; | ||||
|                             scopedFunction = token; | ||||
|                         } else { | ||||
|                             var depth = 0; | ||||
|                             var next = false; | ||||
|                             if (token) { | ||||
|                                 if (token.type === 'paren.rparen') { | ||||
|                                     // If this is a block of parens ')))', set | ||||
|                                     // depth to offset against the cursor position | ||||
|                                     // within the block | ||||
|                                     currentTokenPos = c.column; | ||||
|                                     depth = c.column - (token.start + token.value.length); | ||||
|                                 } | ||||
|                                 r = c.row; | ||||
|                                 p = token.index; | ||||
|                             } else { | ||||
|                                 r = c.row-1; | ||||
|                                 p = -1; | ||||
|                             } | ||||
|                             while ( scopedFunction === null && r > -1) { | ||||
|                                 var rowTokens = expressionEditor.getSession().getTokens(r); | ||||
|                                 if (p === -1) { | ||||
|                                     p = rowTokens.length-1; | ||||
|                                 } | ||||
|                                 while (p > -1) { | ||||
|                                     var type = rowTokens[p].type; | ||||
|                                     if (next) { | ||||
|                                         if (type === 'keyword') { | ||||
|                                             scopedFunction = rowTokens[p]; | ||||
|                                             // console.log("HIT",scopedFunction); | ||||
|                                             break; | ||||
|                                         } | ||||
|                                         next = false; | ||||
|                                     } | ||||
|                                     if (type === 'paren.lparen') { | ||||
|                                         depth-=rowTokens[p].value.length; | ||||
|                                     } else if (type === 'paren.rparen') { | ||||
|                                         depth+=rowTokens[p].value.length; | ||||
|                                     } | ||||
|                                     if (depth < 0) { | ||||
|                                         next = true; | ||||
|                                         depth = 0; | ||||
|                                     } | ||||
|                                     // console.log(r,p,depth,next,rowTokens[p]); | ||||
|                                     p--; | ||||
|                                 } | ||||
|                                 if (!scopedFunction) { | ||||
|                                     r--; | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         expressionEditor.session.removeMarker(currentFunctionMarker); | ||||
|                         if (scopedFunction) { | ||||
|                         //console.log(token,.map(function(t) { return t.type})); | ||||
|                             funcSelect.val(scopedFunction.value).change(); | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 dialogForm.i18n(); | ||||
|                 $("#node-input-expression-func-insert").click(function(e) { | ||||
|                     e.preventDefault(); | ||||
|                     var pos = expressionEditor.getCursorPosition(); | ||||
|                     var f = funcSelect.val(); | ||||
|                     var snippet = jsonata.getFunctionSnippet(f); | ||||
|                     expressionEditor.insertSnippet(snippet); | ||||
|                     expressionEditor.focus(); | ||||
|                 }); | ||||
|                 $("#node-input-expression-reformat").click(function(evt) { | ||||
|                     evt.preventDefault(); | ||||
|                     var v = expressionEditor.getValue()||""; | ||||
|                     try { | ||||
|                         v = jsonata.format(v); | ||||
|                     } catch(err) { | ||||
|                         // TODO: do an optimistic auto-format | ||||
|                     } | ||||
|                     expressionEditor.getSession().setValue(v||"",-1); | ||||
|                 }); | ||||
|  | ||||
|                 var tabs = RED.tabs.create({ | ||||
|                     element: $("#node-input-expression-tabs"), | ||||
|                     onchange:function(tab) { | ||||
|                         $(".node-input-expression-tab-content").hide(); | ||||
|                         tab.content.show(); | ||||
|                         trayOptions.resize(); | ||||
|                     } | ||||
|                 }) | ||||
|  | ||||
|                 tabs.addTab({ | ||||
|                     id: 'expression-help', | ||||
|                     label: RED._('expressionEditor.functionReference'), | ||||
|                     content: $("#node-input-expression-tab-help") | ||||
|                 }); | ||||
|                 tabs.addTab({ | ||||
|                     id: 'expression-tests', | ||||
|                     label: RED._('expressionEditor.test'), | ||||
|                     content: $("#node-input-expression-tab-test") | ||||
|                 }); | ||||
|                 testDataEditor = RED.editor.createEditor({ | ||||
|                     id: 'node-input-expression-test-data', | ||||
|                     value: expressionTestCache[expressionTestCacheId] || '{\n    "payload": "hello world"\n}', | ||||
|                     mode:"ace/mode/json", | ||||
|                     lineNumbers: false | ||||
|                 }); | ||||
|                 var changeTimer; | ||||
|                 $(".node-input-expression-legacy").click(function(e) { | ||||
|                     e.preventDefault(); | ||||
|                     RED.sidebar.info.set(RED._("expressionEditor.compatModeDesc")); | ||||
|                     RED.sidebar.info.show(); | ||||
|                 }) | ||||
|                 var testExpression = function() { | ||||
|                     var value = testDataEditor.getValue(); | ||||
|                     var parsedData; | ||||
|                     var currentExpression = expressionEditor.getValue(); | ||||
|                     var expr; | ||||
|                     var usesContext = false; | ||||
|                     var legacyMode = /(^|[^a-zA-Z0-9_'"])msg([^a-zA-Z0-9_'"]|$)/.test(currentExpression); | ||||
|                     $(".node-input-expression-legacy").toggle(legacyMode); | ||||
|                     try { | ||||
|                         expr = jsonata(currentExpression); | ||||
|                         expr.assign('flowContext',function(val) { | ||||
|                             usesContext = true; | ||||
|                             return null; | ||||
|                         }); | ||||
|                         expr.assign('globalContext',function(val) { | ||||
|                             usesContext = true; | ||||
|                             return null; | ||||
|                         }); | ||||
|                     } catch(err) { | ||||
|                         testResultEditor.setValue(RED._("expressionEditor.errors.invalid-expr",{message:err.message}),-1); | ||||
|                         return; | ||||
|                     } | ||||
|                     try { | ||||
|                         parsedData = JSON.parse(value); | ||||
|                     } catch(err) { | ||||
|                         testResultEditor.setValue(RED._("expressionEditor.errors.invalid-msg",{message:err.toString()})) | ||||
|                         return; | ||||
|                     } | ||||
|  | ||||
|                     try { | ||||
|                         var result = expr.evaluate(legacyMode?{msg:parsedData}:parsedData); | ||||
|                         if (usesContext) { | ||||
|                             testResultEditor.setValue(RED._("expressionEditor.errors.context-unsupported"),-1); | ||||
|                             return; | ||||
|                         } | ||||
|  | ||||
|                         var formattedResult; | ||||
|                         if (result !== undefined) { | ||||
|                             formattedResult = JSON.stringify(result,null,4); | ||||
|                         } else { | ||||
|                             formattedResult = RED._("expressionEditor.noMatch"); | ||||
|                         } | ||||
|                         testResultEditor.setValue(formattedResult,-1); | ||||
|                     } catch(err) { | ||||
|                         testResultEditor.setValue(RED._("expressionEditor.errors.eval",{message:err.message}),-1); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 testDataEditor.getSession().on('change', function() { | ||||
|                     clearTimeout(changeTimer); | ||||
|                     changeTimer = setTimeout(testExpression,200); | ||||
|                     expressionTestCache[expressionTestCacheId] = testDataEditor.getValue(); | ||||
|                 }); | ||||
|                 expressionEditor.getSession().on('change', function() { | ||||
|                     clearTimeout(changeTimer); | ||||
|                     changeTimer = setTimeout(testExpression,200); | ||||
|                 }); | ||||
|  | ||||
|                 testResultEditor = RED.editor.createEditor({ | ||||
|                     id: 'node-input-expression-test-result', | ||||
|                     value: "", | ||||
|                     mode:"ace/mode/json", | ||||
|                     lineNumbers: false, | ||||
|                     readOnly: true | ||||
|                 }); | ||||
|                 panels = RED.panels.create({ | ||||
|                     id:"node-input-expression-panels", | ||||
|                     resize: function(p1Height,p2Height) { | ||||
|                         var p1 = $("#node-input-expression-panel-expr"); | ||||
|                         p1Height -= $(p1.children()[0]).outerHeight(true); | ||||
|                         var editorRow = $(p1.children()[1]); | ||||
|                         p1Height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom"))); | ||||
|                         $("#node-input-expression").css("height",(p1Height-5)+"px"); | ||||
|                         expressionEditor.resize(); | ||||
|  | ||||
|                         var p2 = $("#node-input-expression-panel-info > .form-row > div:first-child"); | ||||
|                         p2Height -= p2.outerHeight(true) + 20; | ||||
|                         $(".node-input-expression-tab-content").height(p2Height); | ||||
|                         $("#node-input-expression-test-data").css("height",(p2Height-5)+"px"); | ||||
|                         testDataEditor.resize(); | ||||
|                         $("#node-input-expression-test-result").css("height",(p2Height-5)+"px"); | ||||
|                         testResultEditor.resize(); | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 $("#node-input-example-reformat").click(function(evt) { | ||||
|                     evt.preventDefault(); | ||||
|                     var v = testDataEditor.getValue()||""; | ||||
|                     try { | ||||
|                         v = JSON.stringify(JSON.parse(v),null,4); | ||||
|                     } catch(err) { | ||||
|                         // TODO: do an optimistic auto-format | ||||
|                     } | ||||
|                     testDataEditor.getSession().setValue(v||"",-1); | ||||
|                 }); | ||||
|  | ||||
|                 testExpression(); | ||||
|             }, | ||||
|             close: function() { | ||||
|                 editStack.pop(); | ||||
|                 expressionEditor.destroy(); | ||||
|                 testDataEditor.destroy(); | ||||
|             }, | ||||
|             show: function() {} | ||||
|         } | ||||
|         RED.tray.show(trayOptions); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     function editJSON(options) { | ||||
|         var value = options.value; | ||||
|         var onComplete = options.complete; | ||||
|         var type = "_json" | ||||
|         editStack.push({type:type}); | ||||
|         RED.view.state(RED.state.EDITING); | ||||
|         var expressionEditor; | ||||
|         var changeTimer; | ||||
|  | ||||
|         var checkValid = function() { | ||||
|             var v = expressionEditor.getValue(); | ||||
|             try { | ||||
|                 JSON.parse(v); | ||||
|                 $("#node-dialog-ok").removeClass('disabled'); | ||||
|                 return true; | ||||
|             } catch(err) { | ||||
|                 $("#node-dialog-ok").addClass('disabled'); | ||||
|                 return false; | ||||
|     function showTypeEditor(type, options) { | ||||
|         if (RED.editor.types.hasOwnProperty(type)) { | ||||
|             if (editStack.length > 0) { | ||||
|                 options.parent = editStack[editStack.length-1].id; | ||||
|             } | ||||
|         } | ||||
|         var trayOptions = { | ||||
|             title: options.title || getEditStackTitle(), | ||||
|             width: "inherit", | ||||
|             buttons: [ | ||||
|                 { | ||||
|                     id: "node-dialog-cancel", | ||||
|                     text: RED._("common.label.cancel"), | ||||
|                     click: function() { | ||||
|                         RED.tray.close(); | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     id: "node-dialog-ok", | ||||
|                     text: RED._("common.label.done"), | ||||
|                     class: "primary", | ||||
|                     click: function() { | ||||
|                         if (options.requireValid && !checkValid()) { | ||||
|                             return; | ||||
|                         } | ||||
|                         onComplete(expressionEditor.getValue()); | ||||
|                         RED.tray.close(); | ||||
|                     } | ||||
|                 } | ||||
|             ], | ||||
|             resize: function(dimensions) { | ||||
|                 editTrayWidthCache[type] = dimensions.width; | ||||
|  | ||||
|                 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"); | ||||
|                 expressionEditor.resize(); | ||||
|             }, | ||||
|             open: function(tray) { | ||||
|                 var trayBody = tray.find('.editor-tray-body'); | ||||
|                 var dialogForm = buildEditForm(tray.find('.editor-tray-body'),'dialog-form',type,'editor'); | ||||
|                 expressionEditor = RED.editor.createEditor({ | ||||
|                     id: 'node-input-json', | ||||
|                     value: "", | ||||
|                     mode:"ace/mode/json" | ||||
|                 }); | ||||
|                 expressionEditor.getSession().setValue(value||"",-1); | ||||
|                 if (options.requireValid) { | ||||
|                     expressionEditor.getSession().on('change', function() { | ||||
|                         clearTimeout(changeTimer); | ||||
|                         changeTimer = setTimeout(checkValid,200); | ||||
|                     }); | ||||
|                     checkValid(); | ||||
|                 } | ||||
|                 $("#node-input-json-reformat").click(function(evt) { | ||||
|                     evt.preventDefault(); | ||||
|                     var v = expressionEditor.getValue()||""; | ||||
|                     try { | ||||
|                         v = JSON.stringify(JSON.parse(v),null,4); | ||||
|                     } catch(err) { | ||||
|                         // TODO: do an optimistic auto-format | ||||
|                     } | ||||
|                     expressionEditor.getSession().setValue(v||"",-1); | ||||
|                 }); | ||||
|                 dialogForm.i18n(); | ||||
|             }, | ||||
|             close: function() { | ||||
|             editStack.push({type:type}); | ||||
|             options.title = options.title || getEditStackTitle(); | ||||
|             options.onclose = function() { | ||||
|                 editStack.pop(); | ||||
|                 expressionEditor.destroy(); | ||||
|             }, | ||||
|             show: function() {} | ||||
|         } | ||||
|         RED.tray.show(trayOptions); | ||||
|     } | ||||
|  | ||||
|     function editMarkdown(options) { | ||||
|         var value = options.value; | ||||
|         var onComplete = options.complete; | ||||
|         var type = "_markdown" | ||||
|         editStack.push({type:type}); | ||||
|         RED.view.state(RED.state.EDITING); | ||||
|         var expressionEditor; | ||||
|  | ||||
|         var trayOptions = { | ||||
|             title: options.title || getEditStackTitle(), | ||||
|             width: "inherit", | ||||
|             buttons: [ | ||||
|                 { | ||||
|                     id: "node-dialog-cancel", | ||||
|                     text: RED._("common.label.cancel"), | ||||
|                     click: function() { | ||||
|                         RED.tray.close(); | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     id: "node-dialog-ok", | ||||
|                     text: RED._("common.label.done"), | ||||
|                     class: "primary", | ||||
|                     click: function() { | ||||
|                         onComplete(expressionEditor.getValue()); | ||||
|                         RED.tray.close(); | ||||
|                     } | ||||
|                 } | ||||
|             ], | ||||
|             resize: function(dimensions) { | ||||
|                 editTrayWidthCache[type] = dimensions.width; | ||||
|  | ||||
|                 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"); | ||||
|                 expressionEditor.resize(); | ||||
|             }, | ||||
|             open: function(tray) { | ||||
|                 var trayBody = tray.find('.editor-tray-body'); | ||||
|                 var dialogForm = buildEditForm(tray.find('.editor-tray-body'),'dialog-form',type,'editor'); | ||||
|                 expressionEditor = RED.editor.createEditor({ | ||||
|                     id: 'node-input-markdown', | ||||
|                     value: value, | ||||
|                     mode:"ace/mode/markdown" | ||||
|                 }); | ||||
|                 if (options.header) { | ||||
|                     options.header.appendTo(tray.find('#node-input-markdown-title')); | ||||
|                 } | ||||
|  | ||||
|                 dialogForm.i18n(); | ||||
|             }, | ||||
|             close: function() { | ||||
|                 editStack.pop(); | ||||
|                 expressionEditor.destroy(); | ||||
|             }, | ||||
|             show: function() {} | ||||
|         } | ||||
|         RED.tray.show(trayOptions); | ||||
|     } | ||||
|  | ||||
|     function stringToUTF8Array(str) { | ||||
|         var data = []; | ||||
|         var i=0, l = str.length; | ||||
|         for (i=0; i<l; i++) { | ||||
|             var char = str.charCodeAt(i); | ||||
|             if (char < 0x80) { | ||||
|                 data.push(char); | ||||
|             } else if (char < 0x800) { | ||||
|                 data.push(0xc0 | (char >> 6)); | ||||
|                 data.push(0x80 | (char & 0x3f)); | ||||
|             } else if (char < 0xd800 || char >= 0xe000) { | ||||
|                 data.push(0xe0 | (char >> 12)); | ||||
|                 data.push(0x80 | ((char>>6) & 0x3f)); | ||||
|                 data.push(0x80 | (char & 0x3f)); | ||||
|             } else { | ||||
|                 i++; | ||||
|                 char = 0x10000 + (((char & 0x3ff)<<10) | (str.charAt(i) & 0x3ff)); | ||||
|                 data.push(0xf0 | (char >>18)); | ||||
|                 data.push(0x80 | ((char>>12) & 0x3f)); | ||||
|                 data.push(0x80 | ((char>>6) & 0x3f)); | ||||
|                 data.push(0x80 | (char & 0x3f)); | ||||
|             } | ||||
|             RED.editor.types[type].show(options); | ||||
|         } else { | ||||
|             console.log("Unknown type editor:",type); | ||||
|         } | ||||
|         return data; | ||||
|     } | ||||
|  | ||||
|     function editBuffer(options) { | ||||
|         var value = options.value; | ||||
|         var onComplete = options.complete; | ||||
|         var type = "_buffer" | ||||
|         editStack.push({type:type}); | ||||
|         RED.view.state(RED.state.EDITING); | ||||
|         var bufferStringEditor = []; | ||||
|         var bufferBinValue; | ||||
|  | ||||
|         var panels; | ||||
|  | ||||
|         var trayOptions = { | ||||
|             title: getEditStackTitle(), | ||||
|             width: "inherit", | ||||
|             buttons: [ | ||||
|                 { | ||||
|                     id: "node-dialog-cancel", | ||||
|                     text: RED._("common.label.cancel"), | ||||
|                     click: function() { | ||||
|                         RED.tray.close(); | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     id: "node-dialog-ok", | ||||
|                     text: RED._("common.label.done"), | ||||
|                     class: "primary", | ||||
|                     click: function() { | ||||
|                         onComplete(JSON.stringify(bufferBinValue)); | ||||
|                         RED.tray.close(); | ||||
|                     } | ||||
|                 } | ||||
|             ], | ||||
|             resize: function(dimensions) { | ||||
|                 if (dimensions) { | ||||
|                     editTrayWidthCache[type] = dimensions.width; | ||||
|                 } | ||||
|                 var height = $("#dialog-form").height(); | ||||
|                 if (panels) { | ||||
|                     panels.resize(height); | ||||
|                 } | ||||
|             }, | ||||
|             open: function(tray) { | ||||
|                 var trayBody = tray.find('.editor-tray-body'); | ||||
|                 var dialogForm = buildEditForm(tray.find('.editor-tray-body'),'dialog-form',type,'editor'); | ||||
|  | ||||
|                 bufferStringEditor = RED.editor.createEditor({ | ||||
|                     id: 'node-input-buffer-str', | ||||
|                     value: "", | ||||
|                     mode:"ace/mode/text" | ||||
|                 }); | ||||
|                 bufferStringEditor.getSession().setValue(value||"",-1); | ||||
|  | ||||
|                 bufferBinEditor = RED.editor.createEditor({ | ||||
|                     id: 'node-input-buffer-bin', | ||||
|                     value: "", | ||||
|                     mode:"ace/mode/text", | ||||
|                     readOnly: true | ||||
|                 }); | ||||
|  | ||||
|                 var changeTimer; | ||||
|                 var buildBuffer = function(data) { | ||||
|                     var valid = true; | ||||
|                     var isString = typeof data === 'string'; | ||||
|                     var binBuffer = []; | ||||
|                     if (isString) { | ||||
|                         bufferBinValue = stringToUTF8Array(data); | ||||
|                     } else { | ||||
|                         bufferBinValue = data; | ||||
|                     } | ||||
|                     var i=0,l=bufferBinValue.length; | ||||
|                     var c = 0; | ||||
|                     for(i=0;i<l;i++) { | ||||
|                         var d = parseInt(bufferBinValue[i]); | ||||
|                         if (!isString && (isNaN(d) || d < 0 || d > 255)) { | ||||
|                             valid = false; | ||||
|                             break; | ||||
|                         } | ||||
|                         if (i>0) { | ||||
|                             if (i%8 === 0) { | ||||
|                                 if (i%16 === 0) { | ||||
|                                     binBuffer.push("\n"); | ||||
|                                 } else { | ||||
|                                     binBuffer.push("  "); | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 binBuffer.push(" "); | ||||
|                             } | ||||
|                         } | ||||
|                         binBuffer.push((d<16?"0":"")+d.toString(16).toUpperCase()); | ||||
|                     } | ||||
|                     if (valid) { | ||||
|                         $("#node-input-buffer-type-string").toggle(isString); | ||||
|                         $("#node-input-buffer-type-array").toggle(!isString); | ||||
|                         bufferBinEditor.setValue(binBuffer.join(""),1); | ||||
|                     } | ||||
|                     return valid; | ||||
|                 } | ||||
|                 var bufferStringUpdate = function() { | ||||
|                     var value = bufferStringEditor.getValue(); | ||||
|                     var isValidArray = false; | ||||
|                     if (/^[\s]*\[[\s\S]*\][\s]*$/.test(value)) { | ||||
|                         isValidArray = true; | ||||
|                         try { | ||||
|                             var data = JSON.parse(value); | ||||
|                             isValidArray = buildBuffer(data); | ||||
|                         } catch(err) { | ||||
|                             isValidArray = false; | ||||
|                         } | ||||
|                     } | ||||
|                     if (!isValidArray) { | ||||
|                         buildBuffer(value); | ||||
|                     } | ||||
|  | ||||
|                 } | ||||
|                 bufferStringEditor.getSession().on('change', function() { | ||||
|                     clearTimeout(changeTimer); | ||||
|                     changeTimer = setTimeout(bufferStringUpdate,200); | ||||
|                 }); | ||||
|  | ||||
|                 bufferStringUpdate(); | ||||
|  | ||||
|                 dialogForm.i18n(); | ||||
|  | ||||
|                 panels = RED.panels.create({ | ||||
|                     id:"node-input-buffer-panels", | ||||
|                     resize: function(p1Height,p2Height) { | ||||
|                         var p1 = $("#node-input-buffer-panel-str"); | ||||
|                         p1Height -= $(p1.children()[0]).outerHeight(true); | ||||
|                         var editorRow = $(p1.children()[1]); | ||||
|                         p1Height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom"))); | ||||
|                         $("#node-input-buffer-str").css("height",(p1Height-5)+"px"); | ||||
|                         bufferStringEditor.resize(); | ||||
|  | ||||
|                         var p2 = $("#node-input-buffer-panel-bin"); | ||||
|                         editorRow = $(p2.children()[0]); | ||||
|                         p2Height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom"))); | ||||
|                         $("#node-input-buffer-bin").css("height",(p2Height-5)+"px"); | ||||
|                         bufferBinEditor.resize(); | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 $(".node-input-buffer-type").click(function(e) { | ||||
|                     e.preventDefault(); | ||||
|                     RED.sidebar.info.set(RED._("bufferEditor.modeDesc")); | ||||
|                     RED.sidebar.info.show(); | ||||
|                 }) | ||||
|  | ||||
|  | ||||
|             }, | ||||
|             close: function() { | ||||
|                 editStack.pop(); | ||||
|                 bufferStringEditor.destroy(); | ||||
|                 bufferBinEditor.destroy(); | ||||
|             }, | ||||
|             show: function() {} | ||||
|         } | ||||
|         RED.tray.show(trayOptions); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         init: function() { | ||||
| @@ -2531,14 +1897,23 @@ RED.editor = (function() { | ||||
|                 $("#node-dialog-cancel").click(); | ||||
|                 $("#node-config-dialog-cancel").click(); | ||||
|             }); | ||||
|  | ||||
|             for (var type in RED.editor.types) { | ||||
|                 if (RED.editor.types.hasOwnProperty(type)) { | ||||
|                     RED.editor.types[type].init(); | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         types: {}, | ||||
|         edit: showEditDialog, | ||||
|         editConfig: showEditConfigNodeDialog, | ||||
|         editSubflow: showEditSubflowDialog, | ||||
|         editExpression: editExpression, | ||||
|         editJSON: editJSON, | ||||
|         editMarkdown: editMarkdown, | ||||
|         editBuffer: editBuffer, | ||||
|         editJavaScript: function(options) { showTypeEditor("_js",options) }, | ||||
|         editExpression: function(options) { showTypeEditor("_expression", options) }, | ||||
|         editJSON: function(options) { showTypeEditor("_json", options) }, | ||||
|         editMarkdown: function(options) { showTypeEditor("_markdown", options) }, | ||||
|         editBuffer: function(options) { showTypeEditor("_buffer", options) }, | ||||
|         buildEditForm: buildEditForm, | ||||
|         validateNode: validateNode, | ||||
|         updateNodeProperties: updateNodeProperties, // TODO: only exposed for edit-undo | ||||
|  | ||||
|   | ||||
							
								
								
									
										209
									
								
								editor/js/ui/editors/buffer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								editor/js/ui/editors/buffer.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | ||||
| /** | ||||
|  * Copyright JS Foundation and other contributors, http://js.foundation | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  **/ | ||||
| RED.editor.types._buffer = (function() { | ||||
|  | ||||
|  | ||||
|     var template = '<script type="text/x-red" data-template-name="_buffer"><div id="node-input-buffer-panels"><div id="node-input-buffer-panel-str" class="red-ui-panel"><div class="form-row" style="margin-bottom: 3px; text-align: right;"><span class="node-input-buffer-type"><i class="fa fa-exclamation-circle"></i> <span id="node-input-buffer-type-string" data-i18n="bufferEditor.modeString"></span><span id="node-input-buffer-type-array" data-i18n="bufferEditor.modeArray"></span></span></div><div class="form-row node-text-editor-row"><div class="node-text-editor" id="node-input-buffer-str"></div></div></div><div id="node-input-buffer-panel-bin" class="red-ui-panel"><div class="form-row node-text-editor-row" style="margin-top: 10px"><div class="node-text-editor" id="node-input-buffer-bin"></div></div></div></div></script>'; | ||||
|  | ||||
|     function stringToUTF8Array(str) { | ||||
|         var data = []; | ||||
|         var i=0, l = str.length; | ||||
|         for (i=0; i<l; i++) { | ||||
|             var char = str.charCodeAt(i); | ||||
|             if (char < 0x80) { | ||||
|                 data.push(char); | ||||
|             } else if (char < 0x800) { | ||||
|                 data.push(0xc0 | (char >> 6)); | ||||
|                 data.push(0x80 | (char & 0x3f)); | ||||
|             } else if (char < 0xd800 || char >= 0xe000) { | ||||
|                 data.push(0xe0 | (char >> 12)); | ||||
|                 data.push(0x80 | ((char>>6) & 0x3f)); | ||||
|                 data.push(0x80 | (char & 0x3f)); | ||||
|             } else { | ||||
|                 i++; | ||||
|                 char = 0x10000 + (((char & 0x3ff)<<10) | (str.charAt(i) & 0x3ff)); | ||||
|                 data.push(0xf0 | (char >>18)); | ||||
|                 data.push(0x80 | ((char>>12) & 0x3f)); | ||||
|                 data.push(0x80 | ((char>>6) & 0x3f)); | ||||
|                 data.push(0x80 | (char & 0x3f)); | ||||
|             } | ||||
|         } | ||||
|         return data; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     return { | ||||
|         init: function() { | ||||
|             $(template).appendTo(document.body); | ||||
|         }, | ||||
|         show: function(options) { | ||||
|             var value = options.value; | ||||
|             var onComplete = options.complete; | ||||
|             var type = "_buffer" | ||||
|             RED.view.state(RED.state.EDITING); | ||||
|             var bufferStringEditor = []; | ||||
|             var bufferBinValue; | ||||
|  | ||||
|             var panels; | ||||
|  | ||||
|             var trayOptions = { | ||||
|                 title: options.title, | ||||
|                 width: "inherit", | ||||
|                 buttons: [ | ||||
|                     { | ||||
|                         id: "node-dialog-cancel", | ||||
|                         text: RED._("common.label.cancel"), | ||||
|                         click: function() { | ||||
|                             RED.tray.close(); | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         id: "node-dialog-ok", | ||||
|                         text: RED._("common.label.done"), | ||||
|                         class: "primary", | ||||
|                         click: function() { | ||||
|                             onComplete(JSON.stringify(bufferBinValue)); | ||||
|                             RED.tray.close(); | ||||
|                         } | ||||
|                     } | ||||
|                 ], | ||||
|                 resize: function(dimensions) { | ||||
|                     var height = $("#dialog-form").height(); | ||||
|                     if (panels) { | ||||
|                         panels.resize(height); | ||||
|                     } | ||||
|                 }, | ||||
|                 open: function(tray) { | ||||
|                     var trayBody = tray.find('.editor-tray-body'); | ||||
|                     var dialogForm = RED.editor.buildEditForm(tray.find('.editor-tray-body'),'dialog-form',type,'editor'); | ||||
|  | ||||
|                     bufferStringEditor = RED.editor.createEditor({ | ||||
|                         id: 'node-input-buffer-str', | ||||
|                         value: "", | ||||
|                         mode:"ace/mode/text" | ||||
|                     }); | ||||
|                     bufferStringEditor.getSession().setValue(value||"",-1); | ||||
|  | ||||
|                     bufferBinEditor = RED.editor.createEditor({ | ||||
|                         id: 'node-input-buffer-bin', | ||||
|                         value: "", | ||||
|                         mode:"ace/mode/text", | ||||
|                         readOnly: true | ||||
|                     }); | ||||
|  | ||||
|                     var changeTimer; | ||||
|                     var buildBuffer = function(data) { | ||||
|                         var valid = true; | ||||
|                         var isString = typeof data === 'string'; | ||||
|                         var binBuffer = []; | ||||
|                         if (isString) { | ||||
|                             bufferBinValue = stringToUTF8Array(data); | ||||
|                         } else { | ||||
|                             bufferBinValue = data; | ||||
|                         } | ||||
|                         var i=0,l=bufferBinValue.length; | ||||
|                         var c = 0; | ||||
|                         for(i=0;i<l;i++) { | ||||
|                             var d = parseInt(bufferBinValue[i]); | ||||
|                             if (!isString && (isNaN(d) || d < 0 || d > 255)) { | ||||
|                                 valid = false; | ||||
|                                 break; | ||||
|                             } | ||||
|                             if (i>0) { | ||||
|                                 if (i%8 === 0) { | ||||
|                                     if (i%16 === 0) { | ||||
|                                         binBuffer.push("\n"); | ||||
|                                     } else { | ||||
|                                         binBuffer.push("  "); | ||||
|                                     } | ||||
|                                 } else { | ||||
|                                     binBuffer.push(" "); | ||||
|                                 } | ||||
|                             } | ||||
|                             binBuffer.push((d<16?"0":"")+d.toString(16).toUpperCase()); | ||||
|                         } | ||||
|                         if (valid) { | ||||
|                             $("#node-input-buffer-type-string").toggle(isString); | ||||
|                             $("#node-input-buffer-type-array").toggle(!isString); | ||||
|                             bufferBinEditor.setValue(binBuffer.join(""),1); | ||||
|                         } | ||||
|                         return valid; | ||||
|                     } | ||||
|                     var bufferStringUpdate = function() { | ||||
|                         var value = bufferStringEditor.getValue(); | ||||
|                         var isValidArray = false; | ||||
|                         if (/^[\s]*\[[\s\S]*\][\s]*$/.test(value)) { | ||||
|                             isValidArray = true; | ||||
|                             try { | ||||
|                                 var data = JSON.parse(value); | ||||
|                                 isValidArray = buildBuffer(data); | ||||
|                             } catch(err) { | ||||
|                                 isValidArray = false; | ||||
|                             } | ||||
|                         } | ||||
|                         if (!isValidArray) { | ||||
|                             buildBuffer(value); | ||||
|                         } | ||||
|  | ||||
|                     } | ||||
|                     bufferStringEditor.getSession().on('change', function() { | ||||
|                         clearTimeout(changeTimer); | ||||
|                         changeTimer = setTimeout(bufferStringUpdate,200); | ||||
|                     }); | ||||
|  | ||||
|                     bufferStringUpdate(); | ||||
|  | ||||
|                     dialogForm.i18n(); | ||||
|  | ||||
|                     panels = RED.panels.create({ | ||||
|                         id:"node-input-buffer-panels", | ||||
|                         resize: function(p1Height,p2Height) { | ||||
|                             var p1 = $("#node-input-buffer-panel-str"); | ||||
|                             p1Height -= $(p1.children()[0]).outerHeight(true); | ||||
|                             var editorRow = $(p1.children()[1]); | ||||
|                             p1Height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom"))); | ||||
|                             $("#node-input-buffer-str").css("height",(p1Height-5)+"px"); | ||||
|                             bufferStringEditor.resize(); | ||||
|  | ||||
|                             var p2 = $("#node-input-buffer-panel-bin"); | ||||
|                             editorRow = $(p2.children()[0]); | ||||
|                             p2Height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom"))); | ||||
|                             $("#node-input-buffer-bin").css("height",(p2Height-5)+"px"); | ||||
|                             bufferBinEditor.resize(); | ||||
|                         } | ||||
|                     }); | ||||
|  | ||||
|                     $(".node-input-buffer-type").click(function(e) { | ||||
|                         e.preventDefault(); | ||||
|                         RED.sidebar.info.set(RED._("bufferEditor.modeDesc")); | ||||
|                         RED.sidebar.info.show(); | ||||
|                     }) | ||||
|  | ||||
|  | ||||
|                 }, | ||||
|                 close: function() { | ||||
|                     if (options.onclose) { | ||||
|                         options.onclose(); | ||||
|                     } | ||||
|                     bufferStringEditor.destroy(); | ||||
|                     bufferBinEditor.destroy(); | ||||
|                 }, | ||||
|                 show: function() {} | ||||
|             } | ||||
|             RED.tray.show(trayOptions); | ||||
|         } | ||||
|     } | ||||
| })(); | ||||
							
								
								
									
										325
									
								
								editor/js/ui/editors/expression.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										325
									
								
								editor/js/ui/editors/expression.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,325 @@ | ||||
| /** | ||||
|  * Copyright JS Foundation and other contributors, http://js.foundation | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  **/ | ||||
| RED.editor.types._expression = (function() { | ||||
|  | ||||
|  | ||||
|     var template = '<script type="text/x-red" data-template-name="_expression"><div id="node-input-expression-panels"><div id="node-input-expression-panel-expr" class="red-ui-panel"><div class="form-row" style="margin-bottom: 3px; text-align: right;"><span class="node-input-expression-legacy"><i class="fa fa-exclamation-circle"></i> <span data-i18n="expressionEditor.compatMode"></span></span><button id="node-input-expression-reformat" class="editor-button editor-button-small"><span data-i18n="expressionEditor.format"></span></button></div><div class="form-row node-text-editor-row"><div class="node-text-editor" id="node-input-expression"></div></div></div><div id="node-input-expression-panel-info" class="red-ui-panel"><div class="form-row"><ul id="node-input-expression-tabs"></ul><div id="node-input-expression-tab-help" class="node-input-expression-tab-content hide"><div><select id="node-input-expression-func"></select><button id="node-input-expression-func-insert" class="editor-button" data-i18n="expressionEditor.insert"></button></div><div id="node-input-expression-help"></div></div><div id="node-input-expression-tab-test" class="node-input-expression-tab-content hide"><div><span style="display: inline-block; width: calc(50% - 5px);"><span data-i18n="expressionEditor.data"></span><button style="float: right; margin-right: 5px;" id="node-input-example-reformat" class="editor-button editor-button-small"><span data-i18n="jsonEditor.format"></span></button></span><span style="display: inline-block; width: calc(50% - 5px);" data-i18n="expressionEditor.result"></span></div><div style="display: inline-block; width: calc(50% - 5px);" class="node-text-editor" id="node-input-expression-test-data"></div><div style="display: inline-block; width: calc(50% - 5px);" class="node-text-editor" id="node-input-expression-test-result"></div></div></div></div></div></script>'; | ||||
|     var expressionTestCache = {}; | ||||
|  | ||||
|     return { | ||||
|         init: function() { | ||||
|             $(template).appendTo(document.body); | ||||
|         }, | ||||
|         show: function(options) { | ||||
|             var expressionTestCacheId = options.parent||"_"; | ||||
|             var value = options.value; | ||||
|             var onComplete = options.complete; | ||||
|             var type = "_expression" | ||||
|             RED.view.state(RED.state.EDITING); | ||||
|             var expressionEditor; | ||||
|             var testDataEditor; | ||||
|             var testResultEditor | ||||
|             var panels; | ||||
|  | ||||
|             var trayOptions = { | ||||
|                 title: options.title, | ||||
|                 width: "inherit", | ||||
|                 buttons: [ | ||||
|                     { | ||||
|                         id: "node-dialog-cancel", | ||||
|                         text: RED._("common.label.cancel"), | ||||
|                         click: function() { | ||||
|                             RED.tray.close(); | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         id: "node-dialog-ok", | ||||
|                         text: RED._("common.label.done"), | ||||
|                         class: "primary", | ||||
|                         click: function() { | ||||
|                             $("#node-input-expression-help").text(""); | ||||
|                             onComplete(expressionEditor.getValue()); | ||||
|                             RED.tray.close(); | ||||
|                         } | ||||
|                     } | ||||
|                 ], | ||||
|                 resize: function(dimensions) { | ||||
|                     var height = $("#dialog-form").height(); | ||||
|                     if (panels) { | ||||
|                         panels.resize(height); | ||||
|                     } | ||||
|  | ||||
|                 }, | ||||
|                 open: function(tray) { | ||||
|                     var trayBody = tray.find('.editor-tray-body'); | ||||
|                     trayBody.addClass("node-input-expression-editor") | ||||
|                     var dialogForm = RED.editor.buildEditForm(tray.find('.editor-tray-body'),'dialog-form','_expression','editor'); | ||||
|                     var funcSelect = $("#node-input-expression-func"); | ||||
|                     Object.keys(jsonata.functions).forEach(function(f) { | ||||
|                         funcSelect.append($("<option></option>").val(f).text(f)); | ||||
|                     }) | ||||
|                     funcSelect.change(function(e) { | ||||
|                         var f = $(this).val(); | ||||
|                         var args = RED._('jsonata:'+f+".args",{defaultValue:''}); | ||||
|                         var title = "<h5>"+f+"("+args+")</h5>"; | ||||
|                         var body = marked(RED._('jsonata:'+f+'.desc',{defaultValue:''})); | ||||
|                         $("#node-input-expression-help").html(title+"<p>"+body+"</p>"); | ||||
|  | ||||
|                     }) | ||||
|                     expressionEditor = RED.editor.createEditor({ | ||||
|                         id: 'node-input-expression', | ||||
|                         value: "", | ||||
|                         mode:"ace/mode/jsonata", | ||||
|                         options: { | ||||
|                             enableBasicAutocompletion:true, | ||||
|                             enableSnippets:true, | ||||
|                             enableLiveAutocompletion: true | ||||
|                         } | ||||
|                     }); | ||||
|                     var currentToken = null; | ||||
|                     var currentTokenPos = -1; | ||||
|                     var currentFunctionMarker = null; | ||||
|  | ||||
|                     expressionEditor.getSession().setValue(value||"",-1); | ||||
|                     expressionEditor.on("changeSelection", function() { | ||||
|                         var c = expressionEditor.getCursorPosition(); | ||||
|                         var token = expressionEditor.getSession().getTokenAt(c.row,c.column); | ||||
|                         if (token !== currentToken || (token && /paren/.test(token.type) && c.column !== currentTokenPos)) { | ||||
|                             currentToken = token; | ||||
|                             var r,p; | ||||
|                             var scopedFunction = null; | ||||
|                             if (token && token.type === 'keyword') { | ||||
|                                 r = c.row; | ||||
|                                 scopedFunction = token; | ||||
|                             } else { | ||||
|                                 var depth = 0; | ||||
|                                 var next = false; | ||||
|                                 if (token) { | ||||
|                                     if (token.type === 'paren.rparen') { | ||||
|                                         // If this is a block of parens ')))', set | ||||
|                                         // depth to offset against the cursor position | ||||
|                                         // within the block | ||||
|                                         currentTokenPos = c.column; | ||||
|                                         depth = c.column - (token.start + token.value.length); | ||||
|                                     } | ||||
|                                     r = c.row; | ||||
|                                     p = token.index; | ||||
|                                 } else { | ||||
|                                     r = c.row-1; | ||||
|                                     p = -1; | ||||
|                                 } | ||||
|                                 while ( scopedFunction === null && r > -1) { | ||||
|                                     var rowTokens = expressionEditor.getSession().getTokens(r); | ||||
|                                     if (p === -1) { | ||||
|                                         p = rowTokens.length-1; | ||||
|                                     } | ||||
|                                     while (p > -1) { | ||||
|                                         var type = rowTokens[p].type; | ||||
|                                         if (next) { | ||||
|                                             if (type === 'keyword') { | ||||
|                                                 scopedFunction = rowTokens[p]; | ||||
|                                                 // console.log("HIT",scopedFunction); | ||||
|                                                 break; | ||||
|                                             } | ||||
|                                             next = false; | ||||
|                                         } | ||||
|                                         if (type === 'paren.lparen') { | ||||
|                                             depth-=rowTokens[p].value.length; | ||||
|                                         } else if (type === 'paren.rparen') { | ||||
|                                             depth+=rowTokens[p].value.length; | ||||
|                                         } | ||||
|                                         if (depth < 0) { | ||||
|                                             next = true; | ||||
|                                             depth = 0; | ||||
|                                         } | ||||
|                                         // console.log(r,p,depth,next,rowTokens[p]); | ||||
|                                         p--; | ||||
|                                     } | ||||
|                                     if (!scopedFunction) { | ||||
|                                         r--; | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                             expressionEditor.session.removeMarker(currentFunctionMarker); | ||||
|                             if (scopedFunction) { | ||||
|                             //console.log(token,.map(function(t) { return t.type})); | ||||
|                                 funcSelect.val(scopedFunction.value).change(); | ||||
|                             } | ||||
|                         } | ||||
|                     }); | ||||
|  | ||||
|                     dialogForm.i18n(); | ||||
|                     $("#node-input-expression-func-insert").click(function(e) { | ||||
|                         e.preventDefault(); | ||||
|                         var pos = expressionEditor.getCursorPosition(); | ||||
|                         var f = funcSelect.val(); | ||||
|                         var snippet = jsonata.getFunctionSnippet(f); | ||||
|                         expressionEditor.insertSnippet(snippet); | ||||
|                         expressionEditor.focus(); | ||||
|                     }); | ||||
|                     $("#node-input-expression-reformat").click(function(evt) { | ||||
|                         evt.preventDefault(); | ||||
|                         var v = expressionEditor.getValue()||""; | ||||
|                         try { | ||||
|                             v = jsonata.format(v); | ||||
|                         } catch(err) { | ||||
|                             // TODO: do an optimistic auto-format | ||||
|                         } | ||||
|                         expressionEditor.getSession().setValue(v||"",-1); | ||||
|                     }); | ||||
|  | ||||
|                     var tabs = RED.tabs.create({ | ||||
|                         element: $("#node-input-expression-tabs"), | ||||
|                         onchange:function(tab) { | ||||
|                             $(".node-input-expression-tab-content").hide(); | ||||
|                             tab.content.show(); | ||||
|                             trayOptions.resize(); | ||||
|                         } | ||||
|                     }) | ||||
|  | ||||
|                     tabs.addTab({ | ||||
|                         id: 'expression-help', | ||||
|                         label: RED._('expressionEditor.functionReference'), | ||||
|                         content: $("#node-input-expression-tab-help") | ||||
|                     }); | ||||
|                     tabs.addTab({ | ||||
|                         id: 'expression-tests', | ||||
|                         label: RED._('expressionEditor.test'), | ||||
|                         content: $("#node-input-expression-tab-test") | ||||
|                     }); | ||||
|                     testDataEditor = RED.editor.createEditor({ | ||||
|                         id: 'node-input-expression-test-data', | ||||
|                         value: expressionTestCache[expressionTestCacheId] || '{\n    "payload": "hello world"\n}', | ||||
|                         mode:"ace/mode/json", | ||||
|                         lineNumbers: false | ||||
|                     }); | ||||
|                     var changeTimer; | ||||
|                     $(".node-input-expression-legacy").click(function(e) { | ||||
|                         e.preventDefault(); | ||||
|                         RED.sidebar.info.set(RED._("expressionEditor.compatModeDesc")); | ||||
|                         RED.sidebar.info.show(); | ||||
|                     }) | ||||
|                     var testExpression = function() { | ||||
|                         var value = testDataEditor.getValue(); | ||||
|                         var parsedData; | ||||
|                         var currentExpression = expressionEditor.getValue(); | ||||
|                         var expr; | ||||
|                         var usesContext = false; | ||||
|                         var legacyMode = /(^|[^a-zA-Z0-9_'"])msg([^a-zA-Z0-9_'"]|$)/.test(currentExpression); | ||||
|                         $(".node-input-expression-legacy").toggle(legacyMode); | ||||
|                         try { | ||||
|                             expr = jsonata(currentExpression); | ||||
|                             expr.assign('flowContext',function(val) { | ||||
|                                 usesContext = true; | ||||
|                                 return null; | ||||
|                             }); | ||||
|                             expr.assign('globalContext',function(val) { | ||||
|                                 usesContext = true; | ||||
|                                 return null; | ||||
|                             }); | ||||
|                         } catch(err) { | ||||
|                             testResultEditor.setValue(RED._("expressionEditor.errors.invalid-expr",{message:err.message}),-1); | ||||
|                             return; | ||||
|                         } | ||||
|                         try { | ||||
|                             parsedData = JSON.parse(value); | ||||
|                         } catch(err) { | ||||
|                             testResultEditor.setValue(RED._("expressionEditor.errors.invalid-msg",{message:err.toString()})) | ||||
|                             return; | ||||
|                         } | ||||
|  | ||||
|                         try { | ||||
|                             var result = expr.evaluate(legacyMode?{msg:parsedData}:parsedData); | ||||
|                             if (usesContext) { | ||||
|                                 testResultEditor.setValue(RED._("expressionEditor.errors.context-unsupported"),-1); | ||||
|                                 return; | ||||
|                             } | ||||
|  | ||||
|                             var formattedResult; | ||||
|                             if (result !== undefined) { | ||||
|                                 formattedResult = JSON.stringify(result,null,4); | ||||
|                             } else { | ||||
|                                 formattedResult = RED._("expressionEditor.noMatch"); | ||||
|                             } | ||||
|                             testResultEditor.setValue(formattedResult,-1); | ||||
|                         } catch(err) { | ||||
|                             testResultEditor.setValue(RED._("expressionEditor.errors.eval",{message:err.message}),-1); | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     testDataEditor.getSession().on('change', function() { | ||||
|                         clearTimeout(changeTimer); | ||||
|                         changeTimer = setTimeout(testExpression,200); | ||||
|                         expressionTestCache[expressionTestCacheId] = testDataEditor.getValue(); | ||||
|                     }); | ||||
|                     expressionEditor.getSession().on('change', function() { | ||||
|                         clearTimeout(changeTimer); | ||||
|                         changeTimer = setTimeout(testExpression,200); | ||||
|                     }); | ||||
|  | ||||
|                     testResultEditor = RED.editor.createEditor({ | ||||
|                         id: 'node-input-expression-test-result', | ||||
|                         value: "", | ||||
|                         mode:"ace/mode/json", | ||||
|                         lineNumbers: false, | ||||
|                         readOnly: true | ||||
|                     }); | ||||
|                     panels = RED.panels.create({ | ||||
|                         id:"node-input-expression-panels", | ||||
|                         resize: function(p1Height,p2Height) { | ||||
|                             var p1 = $("#node-input-expression-panel-expr"); | ||||
|                             p1Height -= $(p1.children()[0]).outerHeight(true); | ||||
|                             var editorRow = $(p1.children()[1]); | ||||
|                             p1Height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom"))); | ||||
|                             $("#node-input-expression").css("height",(p1Height-5)+"px"); | ||||
|                             expressionEditor.resize(); | ||||
|  | ||||
|                             var p2 = $("#node-input-expression-panel-info > .form-row > div:first-child"); | ||||
|                             p2Height -= p2.outerHeight(true) + 20; | ||||
|                             $(".node-input-expression-tab-content").height(p2Height); | ||||
|                             $("#node-input-expression-test-data").css("height",(p2Height-5)+"px"); | ||||
|                             testDataEditor.resize(); | ||||
|                             $("#node-input-expression-test-result").css("height",(p2Height-5)+"px"); | ||||
|                             testResultEditor.resize(); | ||||
|                         } | ||||
|                     }); | ||||
|  | ||||
|                     $("#node-input-example-reformat").click(function(evt) { | ||||
|                         evt.preventDefault(); | ||||
|                         var v = testDataEditor.getValue()||""; | ||||
|                         try { | ||||
|                             v = JSON.stringify(JSON.parse(v),null,4); | ||||
|                         } catch(err) { | ||||
|                             // TODO: do an optimistic auto-format | ||||
|                         } | ||||
|                         testDataEditor.getSession().setValue(v||"",-1); | ||||
|                     }); | ||||
|  | ||||
|                     testExpression(); | ||||
|                 }, | ||||
|                 close: function() { | ||||
|                     if (options.onclose) { | ||||
|                         options.onclose(); | ||||
|                     } | ||||
|                     expressionEditor.destroy(); | ||||
|                     testDataEditor.destroy(); | ||||
|                 }, | ||||
|                 show: function() {} | ||||
|             } | ||||
|             RED.tray.show(trayOptions); | ||||
|         } | ||||
|     } | ||||
| })(); | ||||
							
								
								
									
										102
									
								
								editor/js/ui/editors/js.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								editor/js/ui/editors/js.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| /** | ||||
|  * Copyright JS Foundation and other contributors, http://js.foundation | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  **/ | ||||
| RED.editor.types._js = (function() { | ||||
|  | ||||
|  | ||||
|     var template = '<script type="text/x-red" data-template-name="_js"><div class="form-row node-text-editor-row"><div style="height: 200px;min-height: 150px;" class="node-text-editor" id="node-input-js"></div></div></script>'; | ||||
|  | ||||
|     return { | ||||
|         init: function() { | ||||
|             $(template).appendTo(document.body); | ||||
|         }, | ||||
|         show: function(options) { | ||||
|             var value = options.value; | ||||
|             var onComplete = options.complete; | ||||
|             var type = "_js" | ||||
|             RED.view.state(RED.state.EDITING); | ||||
|             var expressionEditor; | ||||
|             var changeTimer; | ||||
|  | ||||
|             var trayOptions = { | ||||
|                 title: options.title, | ||||
|                 width: options.width||"inherit", | ||||
|                 buttons: [ | ||||
|                     { | ||||
|                         id: "node-dialog-cancel", | ||||
|                         text: RED._("common.label.cancel"), | ||||
|                         click: function() { | ||||
|                             RED.tray.close(); | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         id: "node-dialog-ok", | ||||
|                         text: RED._("common.label.done"), | ||||
|                         class: "primary", | ||||
|                         click: function() { | ||||
|                             onComplete(expressionEditor.getValue(),expressionEditor.getCursorPosition()); | ||||
|                             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); | ||||
|                     } | ||||
|                     $(".node-text-editor").css("height",height+"px"); | ||||
|                     expressionEditor.resize(); | ||||
|                 }, | ||||
|                 open: function(tray) { | ||||
|                     var trayBody = tray.find('.editor-tray-body'); | ||||
|                     var dialogForm = RED.editor.buildEditForm(tray.find('.editor-tray-body'),'dialog-form',type,'editor'); | ||||
|                     expressionEditor = RED.editor.createEditor({ | ||||
|                         id: 'node-input-js', | ||||
|                         mode: 'ace/mode/javascript', | ||||
|                         value: value, | ||||
|                         globals: { | ||||
|                             msg:true, | ||||
|                             context:true, | ||||
|                             RED: true, | ||||
|                             util: true, | ||||
|                             flow: true, | ||||
|                             global: true, | ||||
|                             console: true, | ||||
|                             Buffer: true, | ||||
|                             setTimeout: true, | ||||
|                             clearTimeout: true, | ||||
|                             setInterval: true, | ||||
|                             clearInterval: true | ||||
|                         } | ||||
|                     }); | ||||
|                     if (options.cursor) { | ||||
|                         expressionEditor.gotoLine(options.cursor.row+1,options.cursor.column,false); | ||||
|                     } | ||||
|                     dialogForm.i18n(); | ||||
|                 }, | ||||
|                 close: function() { | ||||
|                     expressionEditor.destroy(); | ||||
|                     if (options.onclose) { | ||||
|                         options.onclose(); | ||||
|                     } | ||||
|                 }, | ||||
|                 show: function() {} | ||||
|             } | ||||
|             RED.tray.show(trayOptions); | ||||
|         } | ||||
|     } | ||||
| })(); | ||||
							
								
								
									
										118
									
								
								editor/js/ui/editors/json.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								editor/js/ui/editors/json.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| /** | ||||
|  * Copyright JS Foundation and other contributors, http://js.foundation | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  **/ | ||||
| RED.editor.types._json = (function() { | ||||
|  | ||||
|  | ||||
|     var template = '<script type="text/x-red" data-template-name="_json"><div class="form-row" style="margin-bottom: 3px; text-align: right;"><button id="node-input-json-reformat" class="editor-button editor-button-small"><span data-i18n="jsonEditor.format"></span></button></div><div class="form-row node-text-editor-row"><div style="height: 200px;min-height: 150px;" class="node-text-editor" id="node-input-json"></div></div></script>'; | ||||
|  | ||||
|     return { | ||||
|         init: function() { | ||||
|             $(template).appendTo(document.body); | ||||
|         }, | ||||
|         show: function(options) { | ||||
|             var value = options.value; | ||||
|             var onComplete = options.complete; | ||||
|             var type = "_json" | ||||
|             RED.view.state(RED.state.EDITING); | ||||
|             var expressionEditor; | ||||
|             var changeTimer; | ||||
|  | ||||
|             var checkValid = function() { | ||||
|                 var v = expressionEditor.getValue(); | ||||
|                 try { | ||||
|                     JSON.parse(v); | ||||
|                     $("#node-dialog-ok").removeClass('disabled'); | ||||
|                     return true; | ||||
|                 } catch(err) { | ||||
|                     $("#node-dialog-ok").addClass('disabled'); | ||||
|                     return false; | ||||
|                 } | ||||
|             } | ||||
|             var trayOptions = { | ||||
|                 title: options.title, | ||||
|                 width: "inherit", | ||||
|                 buttons: [ | ||||
|                     { | ||||
|                         id: "node-dialog-cancel", | ||||
|                         text: RED._("common.label.cancel"), | ||||
|                         click: function() { | ||||
|                             RED.tray.close(); | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         id: "node-dialog-ok", | ||||
|                         text: RED._("common.label.done"), | ||||
|                         class: "primary", | ||||
|                         click: function() { | ||||
|                             if (options.requireValid && !checkValid()) { | ||||
|                                 return; | ||||
|                             } | ||||
|                             onComplete(expressionEditor.getValue()); | ||||
|                             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"); | ||||
|                     expressionEditor.resize(); | ||||
|                 }, | ||||
|                 open: function(tray) { | ||||
|                     var trayBody = tray.find('.editor-tray-body'); | ||||
|                     var dialogForm = RED.editor.buildEditForm(tray.find('.editor-tray-body'),'dialog-form',type,'editor'); | ||||
|                     expressionEditor = RED.editor.createEditor({ | ||||
|                         id: 'node-input-json', | ||||
|                         value: "", | ||||
|                         mode:"ace/mode/json" | ||||
|                     }); | ||||
|                     expressionEditor.getSession().setValue(value||"",-1); | ||||
|                     if (options.requireValid) { | ||||
|                         expressionEditor.getSession().on('change', function() { | ||||
|                             clearTimeout(changeTimer); | ||||
|                             changeTimer = setTimeout(checkValid,200); | ||||
|                         }); | ||||
|                         checkValid(); | ||||
|                     } | ||||
|                     $("#node-input-json-reformat").click(function(evt) { | ||||
|                         evt.preventDefault(); | ||||
|                         var v = expressionEditor.getValue()||""; | ||||
|                         try { | ||||
|                             v = JSON.stringify(JSON.parse(v),null,4); | ||||
|                         } catch(err) { | ||||
|                             // TODO: do an optimistic auto-format | ||||
|                         } | ||||
|                         expressionEditor.getSession().setValue(v||"",-1); | ||||
|                     }); | ||||
|                     dialogForm.i18n(); | ||||
|                 }, | ||||
|                 close: function() { | ||||
|                     expressionEditor.destroy(); | ||||
|                     if (options.onclose) { | ||||
|                         options.onclose(); | ||||
|                     } | ||||
|                 }, | ||||
|                 show: function() {} | ||||
|             } | ||||
|             RED.tray.show(trayOptions); | ||||
|         } | ||||
|     } | ||||
| })(); | ||||
							
								
								
									
										90
									
								
								editor/js/ui/editors/markdown.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								editor/js/ui/editors/markdown.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| /** | ||||
|  * Copyright JS Foundation and other contributors, http://js.foundation | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  **/ | ||||
| RED.editor.types._markdown = (function() { | ||||
|  | ||||
|  | ||||
|     var template = '<script type="text/x-red" data-template-name="_markdown"><div class="form-row" id="node-input-markdown-title" style="margin-bottom: 3px; text-align: right;"></div><div class="form-row node-text-editor-row"><div style="height: 200px;min-height: 150px;" class="node-text-editor" id="node-input-markdown"></div></div></script>'; | ||||
|  | ||||
|     return { | ||||
|         init: function() { | ||||
|             $(template).appendTo(document.body); | ||||
|         }, | ||||
|         show: function(options) { | ||||
|             var value = options.value; | ||||
|             var onComplete = options.complete; | ||||
|             var type = "_markdown" | ||||
|             RED.view.state(RED.state.EDITING); | ||||
|             var expressionEditor; | ||||
|  | ||||
|             var trayOptions = { | ||||
|                 title: options.title, | ||||
|                 width: "inherit", | ||||
|                 buttons: [ | ||||
|                     { | ||||
|                         id: "node-dialog-cancel", | ||||
|                         text: RED._("common.label.cancel"), | ||||
|                         click: function() { | ||||
|                             RED.tray.close(); | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         id: "node-dialog-ok", | ||||
|                         text: RED._("common.label.done"), | ||||
|                         class: "primary", | ||||
|                         click: function() { | ||||
|                             onComplete(expressionEditor.getValue()); | ||||
|                             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"); | ||||
|                     expressionEditor.resize(); | ||||
|                 }, | ||||
|                 open: function(tray) { | ||||
|                     var trayBody = tray.find('.editor-tray-body'); | ||||
|                     var dialogForm = RED.editor.buildEditForm(tray.find('.editor-tray-body'),'dialog-form',type,'editor'); | ||||
|                     expressionEditor = RED.editor.createEditor({ | ||||
|                         id: 'node-input-markdown', | ||||
|                         value: value, | ||||
|                         mode:"ace/mode/markdown" | ||||
|                     }); | ||||
|                     if (options.header) { | ||||
|                         options.header.appendTo(tray.find('#node-input-markdown-title')); | ||||
|                     } | ||||
|  | ||||
|                     dialogForm.i18n(); | ||||
|                 }, | ||||
|                 close: function() { | ||||
|                     expressionEditor.destroy(); | ||||
|                     if (options.onclose) { | ||||
|                         options.onclose(); | ||||
|                     } | ||||
|                 }, | ||||
|                 show: function() {} | ||||
|             } | ||||
|             RED.tray.show(trayOptions); | ||||
|  | ||||
|         } | ||||
|     } | ||||
| })(); | ||||
| @@ -96,7 +96,7 @@ RED.notifications = (function() { | ||||
|         if (options.buttons) { | ||||
|             var buttonSet = $('<div style="margin-top: 20px;" class="ui-dialog-buttonset"></div>').appendTo(n) | ||||
|             options.buttons.forEach(function(buttonDef) { | ||||
|                 var b = $('<button>').text(buttonDef.text).click(buttonDef.click).appendTo(buttonSet); | ||||
|                 var b = $('<button>').html(buttonDef.text).click(buttonDef.click).appendTo(buttonSet); | ||||
|                 if (buttonDef.id) { | ||||
|                     b.attr('id',buttonDef.id); | ||||
|                 } | ||||
|   | ||||
| @@ -233,7 +233,7 @@ RED.palette.editor = (function() { | ||||
|                             if (set.enabled) { | ||||
|                                 var def = RED.nodes.getType(t); | ||||
|                                 if (def && def.color) { | ||||
|                                     swatch.css({background:def.color}); | ||||
|                                     swatch.css({background:RED.utils.getNodeColor(t,def)}); | ||||
|                                     swatch.css({border: "1px solid "+getContrastingBorder(swatch.css('backgroundColor'))}) | ||||
|  | ||||
|                                 } else { | ||||
|   | ||||
| @@ -172,7 +172,7 @@ RED.palette = (function() { | ||||
|                 $('<div/>',{class:"palette_icon",style:"background-image: url("+icon_url+")"}).appendTo(iconContainer); | ||||
|             } | ||||
|  | ||||
|             d.style.backgroundColor = def.color; | ||||
|             d.style.backgroundColor = RED.utils.getNodeColor(nt,def); | ||||
|  | ||||
|             if (def.outputs > 0) { | ||||
|                 var portOut = document.createElement("div"); | ||||
| @@ -275,7 +275,8 @@ RED.palette = (function() { | ||||
|                                 } | ||||
|  | ||||
|                                 for (var i=0;i<nodes.length;i++) { | ||||
|                                     if (d3.select(nodes[i]).classed('link_background')) { | ||||
|                                     var node = d3.select(nodes[i]); | ||||
|                                     if (node.classed('link_background') && !node.classed('link_link')) { | ||||
|                                         var length = nodes[i].getTotalLength(); | ||||
|                                         for (var j=0;j<length;j+=10) { | ||||
|                                             var p = nodes[i].getPointAtLength(j); | ||||
| @@ -320,9 +321,9 @@ RED.palette = (function() { | ||||
|             } | ||||
|             setLabel(nt,$(d),label,nodeInfo); | ||||
|  | ||||
|             var categoryNode = $("#palette-container-"+category); | ||||
|             var categoryNode = $("#palette-container-"+rootCategory); | ||||
|             if (categoryNode.find(".palette_node").length === 1) { | ||||
|                 categoryContainers[category].open(); | ||||
|                 categoryContainers[rootCategory].open(); | ||||
|             } | ||||
|  | ||||
|         } | ||||
| @@ -459,7 +460,6 @@ RED.palette = (function() { | ||||
|             } | ||||
|         }); | ||||
|         RED.events.on('registry:node-set-disabled', function(nodeSet) { | ||||
|             console.log(nodeSet); | ||||
|             for (var j=0;j<nodeSet.types.length;j++) { | ||||
|                 hideNodeType(nodeSet.types[j]); | ||||
|                 var def = RED.nodes.getType(nodeSet.types[j]); | ||||
|   | ||||
| @@ -461,7 +461,11 @@ RED.projects.settings = (function() { | ||||
|                                             setTimeout(function() { | ||||
|                                                 depsList.editableList('removeItem',entry); | ||||
|                                                 refreshModuleInUseCounts(); | ||||
|                                                 entry.count = modulesInUse[entry.id].count; | ||||
|                                                 if (modulesInUse.hasOwnProperty(entry.id)) { | ||||
|                                                     entry.count = modulesInUse[entry.id].count; | ||||
|                                                 } else { | ||||
|                                                     entry.count = 0; | ||||
|                                                 } | ||||
|                                                 depsList.editableList('addItem',entry); | ||||
|                                             },500); | ||||
|                                         } | ||||
|   | ||||
| @@ -622,7 +622,7 @@ RED.projects = (function() { | ||||
|                                             }, | ||||
|                                             400: { | ||||
|                                                 'project_exists': function(error) { | ||||
|                                                     console.log(RED._("projects.clone-project.already-exists")); | ||||
|                                                     console.log(RED._("projects.clone-project.already-exists2")); | ||||
|                                                 }, | ||||
|                                                 'git_error': function(error) { | ||||
|                                                     console.log(RED._("projects.clone-project.git-error"),error); | ||||
| @@ -1983,7 +1983,7 @@ RED.projects = (function() { | ||||
|                                     notification.close(); | ||||
|                                 } | ||||
|                             },{ | ||||
|                                 text: $('<span><i class="fa fa-refresh"></i> Retry</span>'), | ||||
|                                 text: '<span><i class="fa fa-refresh"></i> ' +RED._("projects.send-req.retry") +'</span>', | ||||
|                                 click: function() { | ||||
|                                     body = body || {}; | ||||
|                                     var authBody = {}; | ||||
|   | ||||
| @@ -26,6 +26,24 @@ RED.search = (function() { | ||||
|     var keys = []; | ||||
|     var results = []; | ||||
|  | ||||
|  | ||||
|     function indexProperty(node,label,property) { | ||||
|         if (typeof property === 'string' || typeof property === 'number') { | ||||
|             property = (""+property).toLowerCase(); | ||||
|             index[property] = index[property] || {}; | ||||
|             index[property][node.id] = {node:node,label:label}; | ||||
|         } else if (Array.isArray(property)) { | ||||
|             property.forEach(function(prop) { | ||||
|                 indexProperty(node,label,prop); | ||||
|             }) | ||||
|         } else if (typeof property === 'object') { | ||||
|             for (var prop in property) { | ||||
|                 if (property.hasOwnProperty(prop)) { | ||||
|                     indexProperty(node,label,property[prop]) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     function indexNode(n) { | ||||
|         var l = RED.utils.getNodeLabel(n); | ||||
|         if (l) { | ||||
| @@ -42,17 +60,11 @@ RED.search = (function() { | ||||
|         } | ||||
|         for (var i=0;i<properties.length;i++) { | ||||
|             if (n.hasOwnProperty(properties[i])) { | ||||
|                 var v = n[properties[i]]; | ||||
|                 if (typeof v === 'string' || typeof v === 'number') { | ||||
|                     v = (""+v).toLowerCase(); | ||||
|                     index[v] = index[v] || {}; | ||||
|                     index[v][n.id] = {node:n,label:l}; | ||||
|                 } | ||||
|                 indexProperty(n, l, n[properties[i]]); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|  | ||||
|     } | ||||
|  | ||||
|     function indexWorkspace() { | ||||
|         index = {}; | ||||
|         RED.nodes.eachWorkspace(indexNode); | ||||
| @@ -181,7 +193,7 @@ RED.search = (function() { | ||||
|                     var div = $('<a>',{href:'#',class:"red-ui-search-result"}).appendTo(container); | ||||
|  | ||||
|                     var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(div); | ||||
|                     var colour = def.color; | ||||
|                     var colour = RED.utils.getNodeColor(node.type,def); | ||||
|                     var icon_url = RED.utils.getNodeIcon(def,node); | ||||
|                     if (node.type === 'tab') { | ||||
|                         colour = "#C0DEED"; | ||||
|   | ||||
| @@ -61,7 +61,7 @@ RED.sidebar = (function() { | ||||
|         } | ||||
|  | ||||
|         delete options.closeable; | ||||
|          | ||||
|  | ||||
|         options.wrapper = $('<div>',{style:"height:100%"}).appendTo("#sidebar-content") | ||||
|         options.wrapper.append(options.content); | ||||
|         options.wrapper.hide(); | ||||
| @@ -218,6 +218,7 @@ RED.sidebar = (function() { | ||||
|         showSidebar(); | ||||
|         RED.sidebar.info.init(); | ||||
|         RED.sidebar.config.init(); | ||||
|         RED.sidebar.context.init(); | ||||
|         // hide info bar at start if screen rather narrow... | ||||
|         if ($(window).width() < 600) { RED.menu.setSelected("menu-item-sidebar",false); } | ||||
|     } | ||||
|   | ||||
							
								
								
									
										292
									
								
								editor/js/ui/tab-context.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										292
									
								
								editor/js/ui/tab-context.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,292 @@ | ||||
| /** | ||||
|  * Copyright JS Foundation and other contributors, http://js.foundation | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  **/ | ||||
| RED.sidebar.context = (function() { | ||||
|  | ||||
|     var content; | ||||
|     var sections; | ||||
|  | ||||
|     var localCache = {}; | ||||
|  | ||||
|  | ||||
|     var nodeSection; | ||||
|     // var subflowSection; | ||||
|     var flowSection; | ||||
|     var globalSection; | ||||
|  | ||||
|     var currentNode; | ||||
|     var currentFlow; | ||||
|  | ||||
|     function init() { | ||||
|  | ||||
|         content = $("<div>").css({"position":"relative","height":"100%"}); | ||||
|         content.className = "sidebar-context" | ||||
|         // var toolbar = $('<div class="sidebar-header">'+ | ||||
|         //     '</div>').appendTo(content); | ||||
|  | ||||
|         var footerToolbar = $('<div>'+ | ||||
|             // '<span class="button-group"><a class="sidebar-footer-button" href="#" data-i18n="[title]node-red:debug.sidebar.openWindow"><i class="fa fa-desktop"></i></a></span> ' + | ||||
|             '</div>'); | ||||
|  | ||||
|  | ||||
|  | ||||
|         var stackContainer = $("<div>",{class:"sidebar-context-stack"}).appendTo(content); | ||||
|         sections = RED.stack.create({ | ||||
|             container: stackContainer | ||||
|         }); | ||||
|  | ||||
|         nodeSection = sections.add({ | ||||
|             title: RED._("sidebar.context.node"), | ||||
|             collapsible: true, | ||||
|             // onexpand: function() { | ||||
|             //     updateNode(currentNode,true); | ||||
|             // } | ||||
|         }); | ||||
|         nodeSection.expand(); | ||||
|         nodeSection.content.css({height:"100%"}); | ||||
|         nodeSection.timestamp = $('<div class="sidebar-context-updated"> </div>').appendTo(nodeSection.content); | ||||
|         var table = $('<table class="node-info"></table>').appendTo(nodeSection.content); | ||||
|         nodeSection.table = $('<tbody>').appendTo(table); | ||||
|         var bg = $('<div style="float: right"></div>').appendTo(nodeSection.header); | ||||
|         $('<button class="editor-button editor-button-small"><i class="fa fa-refresh"></i></button>') | ||||
|             .appendTo(bg) | ||||
|             .click(function(evt) { | ||||
|                 evt.stopPropagation(); | ||||
|                 evt.preventDefault(); | ||||
|                 updateNode(currentNode, true); | ||||
|             }) | ||||
|  | ||||
|         // subflowSection  = sections.add({ | ||||
|         //     title: "Subflow", | ||||
|         //     collapsible: true | ||||
|         // }); | ||||
|         // subflowSection.expand(); | ||||
|         // subflowSection.content.css({height:"100%"}); | ||||
|         // bg = $('<div style="float: right"></div>').appendTo(subflowSection.header); | ||||
|         // $('<button class="editor-button editor-button-small"><i class="fa fa-refresh"></i></button>') | ||||
|         //     .appendTo(bg) | ||||
|         //     .click(function(evt) { | ||||
|         //         evt.stopPropagation(); | ||||
|         //         evt.preventDefault(); | ||||
|         //     }) | ||||
|         // | ||||
|         // subflowSection.container.hide(); | ||||
|  | ||||
|         flowSection = sections.add({ | ||||
|             title: RED._("sidebar.context.flow"), | ||||
|             collapsible: true | ||||
|         }); | ||||
|         flowSection.expand(); | ||||
|         flowSection.content.css({height:"100%"}); | ||||
|         flowSection.timestamp = $('<div class="sidebar-context-updated"> </div>').appendTo(flowSection.content); | ||||
|         var table = $('<table class="node-info"></table>').appendTo(flowSection.content); | ||||
|         flowSection.table = $('<tbody>').appendTo(table); | ||||
|         bg = $('<div style="float: right"></div>').appendTo(flowSection.header); | ||||
|         $('<button class="editor-button editor-button-small"><i class="fa fa-refresh"></i></button>') | ||||
|             .appendTo(bg) | ||||
|             .click(function(evt) { | ||||
|                 evt.stopPropagation(); | ||||
|                 evt.preventDefault(); | ||||
|                 updateFlow(currentFlow); | ||||
|             }) | ||||
|  | ||||
|         globalSection = sections.add({ | ||||
|             title: RED._("sidebar.context.global"), | ||||
|             collapsible: true | ||||
|         }); | ||||
|         globalSection.expand(); | ||||
|         globalSection.content.css({height:"100%"}); | ||||
|         globalSection.timestamp = $('<div class="sidebar-context-updated"> </div>').appendTo(globalSection.content); | ||||
|         var table = $('<table class="node-info"></table>').appendTo(globalSection.content); | ||||
|         globalSection.table = $('<tbody>').appendTo(table); | ||||
|  | ||||
|         bg = $('<div style="float: right"></div>').appendTo(globalSection.header); | ||||
|         $('<button class="editor-button editor-button-small"><i class="fa fa-refresh"></i></button>') | ||||
|             .appendTo(bg) | ||||
|             .click(function(evt) { | ||||
|                 evt.stopPropagation(); | ||||
|                 evt.preventDefault(); | ||||
|                 updateEntry(globalSection,"context/global","global"); | ||||
|             }) | ||||
|  | ||||
|  | ||||
|         RED.actions.add("core:show-context-tab",show); | ||||
|  | ||||
|         RED.sidebar.addTab({ | ||||
|             id: "context", | ||||
|             label: RED._("sidebar.context.label"), | ||||
|             name: RED._("sidebar.context.name"), | ||||
|             iconClass: "fa fa-database", | ||||
|             content: content, | ||||
|             toolbar: footerToolbar, | ||||
|             // pinned: true, | ||||
|             enableOnEdit: true | ||||
|         }); | ||||
|  | ||||
|         // var toggleLiveButton = $("#sidebar-context-toggle-live"); | ||||
|         // toggleLiveButton.click(function(evt) { | ||||
|         //     evt.preventDefault(); | ||||
|         //     if ($(this).hasClass("selected")) { | ||||
|         //         $(this).removeClass("selected"); | ||||
|         //         $(this).find("i").removeClass("fa-pause"); | ||||
|         //         $(this).find("i").addClass("fa-play"); | ||||
|         //     } else { | ||||
|         //         $(this).addClass("selected"); | ||||
|         //         $(this).find("i").removeClass("fa-play"); | ||||
|         //         $(this).find("i").addClass("fa-pause"); | ||||
|         //     } | ||||
|         // }); | ||||
|         // RED.popover.tooltip(toggleLiveButton, function() { | ||||
|         //     if (toggleLiveButton.hasClass("selected")) { | ||||
|         //         return "Pause live updates" | ||||
|         //     } else { | ||||
|         //         return "Start live updates" | ||||
|         //     } | ||||
|         // }); | ||||
|  | ||||
|  | ||||
|         RED.events.on("view:selection-changed", function(event) { | ||||
|             var selectedNode = event.nodes && event.nodes.length === 1 && event.nodes[0]; | ||||
|             updateNode(selectedNode); | ||||
|         }) | ||||
|  | ||||
|         RED.events.on("workspace:change", function(event) { | ||||
|             updateFlow(RED.nodes.workspace(event.workspace)); | ||||
|         }) | ||||
|  | ||||
|         updateEntry(globalSection,"context/global","global"); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     function updateNode(node,force) { | ||||
|         currentNode = node; | ||||
|         if (force) { | ||||
|             if (node) { | ||||
|                 updateEntry(nodeSection,"context/node/"+node.id,node.id); | ||||
|                 // if (/^subflow:/.test(node.type)) { | ||||
|                 //     subflowSection.container.show(); | ||||
|                 //     updateEntry(subflowSection,"context/flow/"+node.id,node.id); | ||||
|                 // } else { | ||||
|                 //     subflowSection.container.hide(); | ||||
|                 // } | ||||
|             } else { | ||||
|                 // subflowSection.container.hide(); | ||||
|                 updateEntry(nodeSection) | ||||
|             } | ||||
|         } else { | ||||
|             $(nodeSection.table).empty(); | ||||
|             if (node) { | ||||
|                 $('<tr class="node-info-node-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.refresh"></td></tr>').appendTo(nodeSection.table).i18n(); | ||||
|             } else { | ||||
|                 $('<tr class="node-info-node-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.none"></td></tr>').appendTo(nodeSection.table).i18n(); | ||||
|             } | ||||
|             nodeSection.timestamp.html(" "); | ||||
|  | ||||
|         } | ||||
|     } | ||||
|     function updateFlow(flow) { | ||||
|         currentFlow = flow; | ||||
|         if (flow) { | ||||
|             updateEntry(flowSection,"context/flow/"+flow.id,flow.id); | ||||
|         } else { | ||||
|             updateEntry(flowSection) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function refreshEntry(section,baseUrl,id) { | ||||
|  | ||||
|         var contextStores = RED.settings.context.stores; | ||||
|         var container = section.table; | ||||
|  | ||||
|         $.getJSON(baseUrl, function(data) { | ||||
|             $(container).empty(); | ||||
|             var sortedData = {}; | ||||
|             for (var store in data) { | ||||
|                 if (data.hasOwnProperty(store)) { | ||||
|                     for (var key in data[store]) { | ||||
|                         if (data[store].hasOwnProperty(key)) { | ||||
|                             if (!sortedData.hasOwnProperty(key)) { | ||||
|                                 sortedData[key] = []; | ||||
|                             } | ||||
|                             data[store][key].store = store; | ||||
|                             sortedData[key].push(data[store][key]) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             var keys = Object.keys(sortedData); | ||||
|             keys.sort(); | ||||
|             var l = keys.length; | ||||
|             for (var i = 0; i < l; i++) { | ||||
|                 sortedData[keys[i]].forEach(function(v) { | ||||
|                     var k = keys[i]; | ||||
|                     var l2 = sortedData[k].length; | ||||
|                     var propRow = $('<tr class="node-info-node-row"><td class="sidebar-context-property"></td><td></td></tr>').appendTo(container); | ||||
|                     var obj = $(propRow.children()[0]); | ||||
|                     obj.text(k); | ||||
|                     var tools = $('<span class="debug-message-tools button-group"></span>').appendTo(obj); | ||||
|                     var refreshItem = $('<button class="editor-button editor-button-small"><i class="fa fa-refresh"></i></button>').appendTo(tools).click(function(e) { | ||||
|                         e.preventDefault(); | ||||
|                         e.stopPropagation(); | ||||
|                         $.getJSON(baseUrl+"/"+k+"?store="+v.store, function(data) { | ||||
|                             $(propRow.children()[1]).empty(); | ||||
|                             var payload = data.msg; | ||||
|                             var format = data.format; | ||||
|                             payload = RED.utils.decodeObject(payload,format); | ||||
|                             RED.utils.createObjectElement(payload, { | ||||
|                                 typeHint: data.format, | ||||
|                                 sourceId: id+"."+k | ||||
|                             }).appendTo(propRow.children()[1]); | ||||
|                         }) | ||||
|                     }); | ||||
|  | ||||
|  | ||||
|                     var payload = v.msg; | ||||
|                     var format = v.format; | ||||
|                     payload = RED.utils.decodeObject(payload,format); | ||||
|                     RED.utils.createObjectElement(payload, { | ||||
|                         typeHint: v.format, | ||||
|                         sourceId: id+"."+k | ||||
|                     }).appendTo(propRow.children()[1]); | ||||
|                     if (contextStores.length > 1) { | ||||
|                         $("<span>",{class:"sidebar-context-property-storename"}).text(v.store).appendTo($(propRow.children()[0])) | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|             if (l === 0) { | ||||
|                 $('<tr class="node-info-node-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.empty"></td></tr>').appendTo(container).i18n(); | ||||
|             } | ||||
|             $(section.timestamp).text(new Date().toLocaleString()); | ||||
|         }); | ||||
|     } | ||||
|     function updateEntry(section,baseUrl,id) { | ||||
|         var container = section.table; | ||||
|         if (id) { | ||||
|             refreshEntry(section,baseUrl,id); | ||||
|         } else { | ||||
|             $(container).empty(); | ||||
|             $('<tr class="node-info-node-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.none"></td></tr>').appendTo(container).i18n(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|     function show() { | ||||
|         RED.sidebar.show("context"); | ||||
|     } | ||||
|     return { | ||||
|         init: init | ||||
|     } | ||||
| })(); | ||||
| @@ -221,7 +221,7 @@ RED.sidebar.info = (function() { | ||||
|  | ||||
|                                         var div = $('<span>',{class:""}).appendTo(container); | ||||
|                                         var nodeDiv = $('<div>',{class:"palette_node palette_node_small"}).appendTo(div); | ||||
|                                         var colour = configNode._def.color; | ||||
|                                         var colour = RED.utils.getNodeColor(configNode.type,configNode._def); | ||||
|                                         var icon_url = RED.utils.getNodeIcon(configNode._def); | ||||
|                                         nodeDiv.css({'backgroundColor':colour, "cursor":"pointer"}); | ||||
|                                         var iconContainer = $('<div/>',{class:"palette_icon_container"}).appendTo(nodeDiv); | ||||
| @@ -280,8 +280,7 @@ RED.sidebar.info = (function() { | ||||
|             if (infoText) { | ||||
|                 setInfoText(infoText); | ||||
|             } | ||||
|  | ||||
|  | ||||
|             $(".sidebar-node-info-stack").scrollTop(0); | ||||
|             $(".node-info-property-header").click(function(e) { | ||||
|                 e.preventDefault(); | ||||
|                 expandedSections["property"] = !expandedSections["property"]; | ||||
| @@ -395,10 +394,9 @@ RED.sidebar.info = (function() { | ||||
|     function set(html,title) { | ||||
|         // tips.stop(); | ||||
|         // sections.show(); | ||||
|         // nodeSection.container.hide(); | ||||
|         infoSection.title.text(title||""); | ||||
|         refresh(null); | ||||
|         $(infoSection.content).empty(); | ||||
|         nodeSection.container.hide(); | ||||
|         infoSection.title.text(title||RED._("sidebar.info.info")); | ||||
|         setInfoText(html); | ||||
|         $(".sidebar-node-info-stack").scrollTop(0); | ||||
|     } | ||||
|   | ||||
| @@ -128,7 +128,7 @@ RED.typeSearch = (function() { | ||||
|                 var div = $('<a>',{href:'#',class:"red-ui-search-result"}).appendTo(container); | ||||
|  | ||||
|                 var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(div); | ||||
|                 var colour = def.color; | ||||
|                 var colour = RED.utils.getNodeColor(object.type,def); | ||||
|                 var icon_url = RED.utils.getNodeIcon(def); | ||||
|                 nodeDiv.css('backgroundColor',colour); | ||||
|  | ||||
|   | ||||
| @@ -34,6 +34,10 @@ RED.utils = (function() { | ||||
|                 result = $('<span class="debug-message-object-value debug-message-type-meta"></span>').text('buffer['+value.length+']'); | ||||
|             } else if (value.hasOwnProperty('type') && value.type === 'array' && value.hasOwnProperty('data')) { | ||||
|                 result = $('<span class="debug-message-object-value debug-message-type-meta"></span>').text('array['+value.length+']'); | ||||
|             } else if (value.hasOwnProperty('type') && value.type === 'function') { | ||||
|                 result = $('<span class="debug-message-object-value debug-message-type-meta"></span>').text('function'); | ||||
|             } else if (value.hasOwnProperty('type') && value.type === 'number') { | ||||
|                 result = $('<span class="debug-message-object-value debug-message-type-number"></span>').text(value.data); | ||||
|             } else { | ||||
|                 result = $('<span class="debug-message-object-value debug-message-type-meta">object</span>'); | ||||
|             } | ||||
| @@ -45,6 +49,8 @@ RED.utils = (function() { | ||||
|                 subvalue = sanitize(value); | ||||
|             } | ||||
|             result = $('<span class="debug-message-object-value debug-message-type-string"></span>').html('"'+formatString(subvalue)+'"'); | ||||
|         } else if (typeof value === 'number') { | ||||
|             result = $('<span class="debug-message-object-value debug-message-type-number"></span>').text(""+value); | ||||
|         } else { | ||||
|             result = $('<span class="debug-message-object-value debug-message-type-other"></span>').text(""+value); | ||||
|         } | ||||
| @@ -125,7 +131,7 @@ RED.utils = (function() { | ||||
|             e.stopPropagation(); | ||||
|             RED.clipboard.copyText(msg,copyPayload,"clipboard.copyMessageValue"); | ||||
|         }) | ||||
|         if (strippedKey !== '') { | ||||
|         if (strippedKey !== undefined && strippedKey !== '') { | ||||
|             var isPinned = pinnedPaths[sourceId].hasOwnProperty(strippedKey); | ||||
|  | ||||
|             var pinPath = $('<button class="editor-button editor-button-small debug-message-tools-pin"><i class="fa fa-map-pin"></i></button>').appendTo(tools).click(function(e) { | ||||
| @@ -292,13 +298,18 @@ RED.utils = (function() { | ||||
|  | ||||
|         var isArray = Array.isArray(obj); | ||||
|         var isArrayObject = false; | ||||
|         if (obj && typeof obj === 'object' && obj.hasOwnProperty('type') && obj.hasOwnProperty('data') && ((obj.__encoded__ && obj.type === 'array') || obj.type === 'Buffer')) { | ||||
|         if (obj && typeof obj === 'object' && obj.hasOwnProperty('type') && obj.hasOwnProperty('data') && ((obj.__enc__ && obj.type === 'array') || obj.type === 'Buffer')) { | ||||
|             isArray = true; | ||||
|             isArrayObject = true; | ||||
|         } | ||||
|  | ||||
|         if (obj === null || obj === undefined) { | ||||
|             $('<span class="debug-message-type-null">'+obj+'</span>').appendTo(entryObj); | ||||
|         } else if (obj.__enc__ && obj.type === 'number') { | ||||
|             e = $('<span class="debug-message-type-number debug-message-object-header"></span>').text(obj.data).appendTo(entryObj); | ||||
|         } else if (typeHint === "function" || (obj.__enc__ && obj.type === 'function')) { | ||||
|             e = $('<span class="debug-message-type-meta debug-message-object-header"></span>').text("function").appendTo(entryObj); | ||||
|         } else if (typeHint === "internal" || (obj.__enc__ && obj.type === 'internal')) { | ||||
|             e = $('<span class="debug-message-type-meta debug-message-object-header"></span>').text("[internal]").appendTo(entryObj); | ||||
|         } else if (typeof obj === 'string') { | ||||
|             if (/[\t\n\r]/.test(obj)) { | ||||
|                 element.addClass('collapsed'); | ||||
| @@ -343,7 +354,7 @@ RED.utils = (function() { | ||||
|                 if (originalLength === undefined) { | ||||
|                     originalLength = data.length; | ||||
|                 } | ||||
|                 if (data.__encoded__) { | ||||
|                 if (data.__enc__) { | ||||
|                     data = data.data; | ||||
|                 } | ||||
|                 type = obj.type.toLowerCase(); | ||||
| @@ -783,6 +794,45 @@ RED.utils = (function() { | ||||
|         return RED.text.bidi.enforceTextDirectionWithUCC(l); | ||||
|     } | ||||
|  | ||||
|     var nodeColorCache = {}; | ||||
|     function getNodeColor(type, def) { | ||||
|         var result = def.color; | ||||
|         var paletteTheme = RED.settings.theme('palette.theme') || []; | ||||
|         if (paletteTheme.length > 0) { | ||||
|             if (!nodeColorCache.hasOwnProperty(type)) { | ||||
|                 nodeColorCache[type] = def.color; | ||||
|                 var l = paletteTheme.length; | ||||
|                 for (var i = 0; i < l; i++ ){ | ||||
|                     var themeRule = paletteTheme[i]; | ||||
|                     if (themeRule.hasOwnProperty('category')) { | ||||
|                         if (!themeRule.hasOwnProperty('_category')) { | ||||
|                             themeRule._category = new RegExp(themeRule.category); | ||||
|                         } | ||||
|                         if (!themeRule._category.test(def.category)) { | ||||
|                             continue; | ||||
|                         } | ||||
|                     } | ||||
|                     if (themeRule.hasOwnProperty('type')) { | ||||
|                         if (!themeRule.hasOwnProperty('_type')) { | ||||
|                             themeRule._type = new RegExp(themeRule.type); | ||||
|                         } | ||||
|                         if (!themeRule._type.test(type)) { | ||||
|                             continue; | ||||
|                         } | ||||
|                     } | ||||
|                     nodeColorCache[type] = themeRule.color || def.color; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|             result = nodeColorCache[type]; | ||||
|         } | ||||
|         if (result) { | ||||
|             return result; | ||||
|         } else { | ||||
|             return "#ddd"; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function addSpinnerOverlay(container,contain) { | ||||
|         var spinner = $('<div class="projects-dialog-spinner "><img src="red/images/spin.svg"/></div>').appendTo(container); | ||||
|         if (contain) { | ||||
| @@ -791,6 +841,47 @@ RED.utils = (function() { | ||||
|         return spinner; | ||||
|     } | ||||
|  | ||||
|     function decodeObject(payload,format) { | ||||
|         if ((format === 'number') && (payload === "NaN")) { | ||||
|             payload = Number.NaN; | ||||
|         } else if ((format === 'number') && (payload === "Infinity")) { | ||||
|             payload = Infinity; | ||||
|         } else if ((format === 'number') && (payload === "-Infinity")) { | ||||
|             payload = -Infinity; | ||||
|         } else if (format === 'Object' || /^array/.test(format) || format === 'boolean' || format === 'number' ) { | ||||
|             payload = JSON.parse(payload); | ||||
|         } else if (/error/i.test(format)) { | ||||
|             payload = JSON.parse(payload); | ||||
|             payload = (payload.name?payload.name+": ":"")+payload.message; | ||||
|         } else if (format === 'null') { | ||||
|             payload = null; | ||||
|         } else if (format === 'undefined') { | ||||
|             payload = undefined; | ||||
|         } else if (/^buffer/.test(format)) { | ||||
|             var buffer = payload; | ||||
|             payload = []; | ||||
|             for (var c = 0; c < buffer.length; c += 2) { | ||||
|                 payload.push(parseInt(buffer.substr(c, 2), 16)); | ||||
|             } | ||||
|         } | ||||
|         return payload; | ||||
|     } | ||||
|  | ||||
|     function parseContextKey(key) { | ||||
|         var parts = {}; | ||||
|         var m = /^#:\((\S+?)\)::(.*)$/.exec(key); | ||||
|         if (m) { | ||||
|             parts.store = m[1]; | ||||
|             parts.key = m[2]; | ||||
|         } else { | ||||
|             parts.key = key; | ||||
|             if (RED.settings.context) { | ||||
|                 parts.store = RED.settings.context.default; | ||||
|             } | ||||
|         } | ||||
|         return parts; | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         createObjectElement: buildMessageElement, | ||||
|         getMessageProperty: getMessageProperty, | ||||
| @@ -800,6 +891,9 @@ RED.utils = (function() { | ||||
|         getDefaultNodeIcon: getDefaultNodeIcon, | ||||
|         getNodeIcon: getNodeIcon, | ||||
|         getNodeLabel: getNodeLabel, | ||||
|         addSpinnerOverlay: addSpinnerOverlay | ||||
|         getNodeColor: getNodeColor, | ||||
|         addSpinnerOverlay: addSpinnerOverlay, | ||||
|         decodeObject: decodeObject, | ||||
|         parseContextKey: parseContextKey | ||||
|     } | ||||
| })(); | ||||
|   | ||||
| @@ -46,7 +46,7 @@ | ||||
|              .attr("y",function(d) { return (d.y-d.h/2)/nav_scale }) | ||||
|              .attr("width",function(d) { return Math.max(9,d.w/nav_scale) }) | ||||
|              .attr("height",function(d) { return Math.max(3,d.h/nav_scale) }) | ||||
|              .attr("fill",function(d) { return d._def.color;}) | ||||
|              .attr("fill",function(d) { return RED.utils.getNodeColor(d.type,d._def);}) | ||||
|          }); | ||||
|      } | ||||
|      function onScroll() { | ||||
| @@ -65,13 +65,28 @@ | ||||
|                       .attr('height',chartSize[1]/nav_scale/scaleFactor) | ||||
|          } | ||||
|      } | ||||
|      function toggle() { | ||||
|          if (!isShowing) { | ||||
|              isShowing = true; | ||||
|              $("#btn-navigate").addClass("selected"); | ||||
|              resizeNavBorder(); | ||||
|              refreshNodes(); | ||||
|              $("#chart").on("scroll",onScroll); | ||||
|              navContainer.fadeIn(200); | ||||
|          } else { | ||||
|              isShowing = false; | ||||
|              navContainer.fadeOut(100); | ||||
|              $("#chart").off("scroll",onScroll); | ||||
|              $("#btn-navigate").removeClass("selected"); | ||||
|          } | ||||
|      } | ||||
|  | ||||
|      return { | ||||
|          init: function() { | ||||
|  | ||||
|              $(window).resize(resizeNavBorder); | ||||
|              RED.events.on("sidebar:resize",resizeNavBorder); | ||||
|  | ||||
|              RED.actions.add("core:toggle-navigator",toggle); | ||||
|              var hideTimeout; | ||||
|  | ||||
|              navContainer = $('<div>').css({ | ||||
| @@ -141,23 +156,12 @@ | ||||
|  | ||||
|             $("#btn-navigate").click(function(evt) { | ||||
|                 evt.preventDefault(); | ||||
|                 if (!isShowing) { | ||||
|                     isShowing = true; | ||||
|                     $("#btn-navigate").addClass("selected"); | ||||
|                     resizeNavBorder(); | ||||
|                     refreshNodes(); | ||||
|                     $("#chart").on("scroll",onScroll); | ||||
|                     navContainer.fadeIn(200); | ||||
|                 } else { | ||||
|                     isShowing = false; | ||||
|                     navContainer.fadeOut(100); | ||||
|                     $("#chart").off("scroll",onScroll); | ||||
|                     $("#btn-navigate").removeClass("selected"); | ||||
|                 } | ||||
|                 toggle(); | ||||
|             }) | ||||
|         }, | ||||
|         refresh: refreshNodes, | ||||
|         resize: resizeNavBorder | ||||
|         resize: resizeNavBorder, | ||||
|         toggle: toggle | ||||
|     } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -59,7 +59,9 @@ RED.view = (function() { | ||||
|         dblClickPrimed = null, | ||||
|         clickTime = 0, | ||||
|         clickElapsed = 0, | ||||
|         scroll_position; | ||||
|         scroll_position = [], | ||||
|         quickAddActive = false, | ||||
|         quickAddLink = null; | ||||
|  | ||||
|     var clipboard = ""; | ||||
|  | ||||
| @@ -462,6 +464,82 @@ RED.view = (function() { | ||||
|         RED.actions.add("core:step-selection-left", function() { moveSelection(-20,0);}); | ||||
|     } | ||||
|  | ||||
|     function generateLinkPath(origX,origY, destX, destY, sc) { | ||||
|         var dy = destY-origY; | ||||
|         var dx = destX-origX; | ||||
|         var delta = Math.sqrt(dy*dy+dx*dx); | ||||
|         var scale = lineCurveScale; | ||||
|         var scaleY = 0; | ||||
|         if (dx*sc > 0) { | ||||
|             if (delta < node_width) { | ||||
|                 scale = 0.75-0.75*((node_width-delta)/node_width); | ||||
|                 // scale += 2*(Math.min(5*node_width,Math.abs(dx))/(5*node_width)); | ||||
|                 // if (Math.abs(dy) < 3*node_height) { | ||||
|                 //     scaleY = ((dy>0)?0.5:-0.5)*(((3*node_height)-Math.abs(dy))/(3*node_height))*(Math.min(node_width,Math.abs(dx))/(node_width)) ; | ||||
|                 // } | ||||
|             } | ||||
|         } else { | ||||
|             scale = 0.4-0.2*(Math.max(0,(node_width-Math.min(Math.abs(dx),Math.abs(dy)))/node_width)); | ||||
|         } | ||||
|         if (dx*sc > 0) { | ||||
|             return "M "+origX+" "+origY+ | ||||
|                 " C "+(origX+sc*(node_width*scale))+" "+(origY+scaleY*node_height)+" "+ | ||||
|                 (destX-sc*(scale)*node_width)+" "+(destY-scaleY*node_height)+" "+ | ||||
|                 destX+" "+destY | ||||
|         } else { | ||||
|  | ||||
|             var midX = Math.floor(destX-dx/2); | ||||
|             var midY = Math.floor(destY-dy/2); | ||||
|             // | ||||
|             if (dy === 0) { | ||||
|                 midY = destY + node_height; | ||||
|             } | ||||
|             var cp_height = node_height/2; | ||||
|             var y1 = (destY + midY)/2 | ||||
|             var topX =origX + sc*node_width*scale; | ||||
|             var topY = dy>0?Math.min(y1 - dy/2 , origY+cp_height):Math.max(y1 - dy/2 , origY-cp_height); | ||||
|             var bottomX = destX - sc*node_width*scale; | ||||
|             var bottomY = dy>0?Math.max(y1, destY-cp_height):Math.min(y1, destY+cp_height); | ||||
|             var x1 = (origX+topX)/2; | ||||
|             var scy = dy>0?1:-1; | ||||
|             var cp = [ | ||||
|                 // Orig -> Top | ||||
|                 [x1,origY], | ||||
|                 [topX,dy>0?Math.max(origY, topY-cp_height):Math.min(origY, topY+cp_height)], | ||||
|                 // Top -> Mid | ||||
|                 // [Mirror previous cp] | ||||
|                 [x1,dy>0?Math.min(midY, topY+cp_height):Math.max(midY, topY-cp_height)], | ||||
|                 // Mid -> Bottom | ||||
|                 // [Mirror previous cp] | ||||
|                 [bottomX,dy>0?Math.max(midY, bottomY-cp_height):Math.min(midY, bottomY+cp_height)], | ||||
|                 // Bottom -> Dest | ||||
|                 // [Mirror previous cp] | ||||
|                 [(destX+bottomX)/2,destY] | ||||
|             ]; | ||||
|             if (cp[2][1] === topY+scy*cp_height) { | ||||
|                 if (Math.abs(dy) < cp_height*10) { | ||||
|                     cp[1][1] = topY-scy*cp_height/2; | ||||
|                     cp[3][1] = bottomY-scy*cp_height/2; | ||||
|                 } | ||||
|                 cp[2][0] = topX; | ||||
|             } | ||||
|             return "M "+origX+" "+origY+ | ||||
|                 " C "+ | ||||
|                    cp[0][0]+" "+cp[0][1]+" "+ | ||||
|                    cp[1][0]+" "+cp[1][1]+" "+ | ||||
|                    topX+" "+topY+ | ||||
|                 " S "+ | ||||
|                    cp[2][0]+" "+cp[2][1]+" "+ | ||||
|                    midX+" "+midY+ | ||||
|                " S "+ | ||||
|                   cp[3][0]+" "+cp[3][1]+" "+ | ||||
|                   bottomX+" "+bottomY+ | ||||
|                 " S "+ | ||||
|                     cp[4][0]+" "+cp[4][1]+" "+ | ||||
|                     destX+" "+destY | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     function addNode(type,x,y) { | ||||
|         var m = /^subflow:(.+)$/.exec(type); | ||||
| @@ -569,14 +647,16 @@ RED.view = (function() { | ||||
|                     mouse_mode = RED.state.QUICK_JOINING; | ||||
|                     $(window).on('keyup',disableQuickJoinEventHandler); | ||||
|                 } | ||||
|  | ||||
|                 quickAddActive = true; | ||||
|                 RED.typeSearch.show({ | ||||
|                     x:d3.event.clientX-mainPos.left-node_width/2, | ||||
|                     y:d3.event.clientY-mainPos.top-node_height/2, | ||||
|                     cancel: function() { | ||||
|                         quickAddActive = false; | ||||
|                         resetMouseVars(); | ||||
|                     }, | ||||
|                     add: function(type) { | ||||
|                         quickAddActive = false; | ||||
|                         var result = addNode(type); | ||||
|                         if (!result) { | ||||
|                             return; | ||||
| @@ -585,11 +665,10 @@ RED.view = (function() { | ||||
|                         var historyEvent = result.historyEvent; | ||||
|                         nn.x = point[0]; | ||||
|                         nn.y = point[1]; | ||||
|                         if (mouse_mode === RED.state.QUICK_JOINING) { | ||||
|                             if (drag_lines.length > 0) { | ||||
|                                 var drag_line = drag_lines[0]; | ||||
|                         if (mouse_mode === RED.state.QUICK_JOINING || quickAddLink) { | ||||
|                             if (quickAddLink || drag_lines.length > 0) { | ||||
|                                 var drag_line = quickAddLink||drag_lines[0]; | ||||
|                                 var src = null,dst,src_port; | ||||
|  | ||||
|                                 if (drag_line.portType === PORT_TYPE_OUTPUT && nn.inputs > 0) { | ||||
|                                     src = drag_line.node; | ||||
|                                     src_port = drag_line.port; | ||||
| @@ -604,9 +683,9 @@ RED.view = (function() { | ||||
|                                     RED.nodes.addLink(link); | ||||
|                                     historyEvent.links = [link]; | ||||
|                                     hideDragLines(); | ||||
|                                     if (drag_line.portType === PORT_TYPE_OUTPUT && nn.outputs > 0) { | ||||
|                                     if (!quickAddLink && drag_line.portType === PORT_TYPE_OUTPUT && nn.outputs > 0) { | ||||
|                                         showDragLines([{node:nn,port:0,portType:PORT_TYPE_OUTPUT}]); | ||||
|                                     } else if (drag_line.portType === PORT_TYPE_INPUT && nn.inputs > 0) { | ||||
|                                     } else if (!quickAddLink && drag_line.portType === PORT_TYPE_INPUT && nn.inputs > 0) { | ||||
|                                         showDragLines([{node:nn,port:0,portType:PORT_TYPE_INPUT}]); | ||||
|                                     } else { | ||||
|                                         resetMouseVars(); | ||||
| @@ -624,9 +703,9 @@ RED.view = (function() { | ||||
|                                     resetMouseVars(); | ||||
|                                 } | ||||
|                             } | ||||
|                             quickAddLink = null; | ||||
|                         } | ||||
|  | ||||
|  | ||||
|                         RED.history.push(historyEvent); | ||||
|                         RED.nodes.add(nn); | ||||
|                         RED.editor.validateNode(nn); | ||||
| @@ -791,28 +870,7 @@ RED.view = (function() { | ||||
|  | ||||
|                 var sc = (drag_line.portType === PORT_TYPE_OUTPUT)?1:-1; | ||||
|  | ||||
|                 var dy = mousePos[1]-(drag_line.node.y+portY); | ||||
|                 var dx = mousePos[0]-(drag_line.node.x+sc*drag_line.node.w/2); | ||||
|                 var delta = Math.sqrt(dy*dy+dx*dx); | ||||
|                 var scale = lineCurveScale; | ||||
|                 var scaleY = 0; | ||||
|  | ||||
|                 if (delta < node_width) { | ||||
|                     scale = 0.75-0.75*((node_width-delta)/node_width); | ||||
|                 } | ||||
|                 if (dx*sc < 0) { | ||||
|                     scale += 2*(Math.min(5*node_width,Math.abs(dx))/(5*node_width)); | ||||
|                     if (Math.abs(dy) < 3*node_height) { | ||||
|                         scaleY = ((dy>0)?0.5:-0.5)*(((3*node_height)-Math.abs(dy))/(3*node_height))*(Math.min(node_width,Math.abs(dx))/(node_width)) ; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 drag_line.el.attr("d", | ||||
|                     "M "+(drag_line.node.x+sc*drag_line.node.w/2)+" "+(drag_line.node.y+portY)+ | ||||
|                     " C "+(drag_line.node.x+sc*(drag_line.node.w/2+node_width*scale))+" "+(drag_line.node.y+portY+scaleY*node_height)+" "+ | ||||
|                     (mousePos[0]-sc*(scale)*node_width)+" "+(mousePos[1]-scaleY*node_height)+" "+ | ||||
|                     mousePos[0]+" "+mousePos[1] | ||||
|                     ); | ||||
|                 drag_line.el.attr("d",generateLinkPath(drag_line.node.x+sc*drag_line.node.w/2,drag_line.node.y+portY,mousePos[0],mousePos[1],sc)); | ||||
|             } | ||||
|             d3.event.preventDefault(); | ||||
|         } else if (mouse_mode == RED.state.MOVING) { | ||||
| @@ -1412,6 +1470,9 @@ RED.view = (function() { | ||||
|     function disableQuickJoinEventHandler(evt) { | ||||
|         // Check for ctrl (all browsers), "Meta" (Chrome/FF), keyCode 91 (Safari) | ||||
|         if (evt.keyCode === 17 || evt.key === "Meta" || evt.keyCode === 91) { | ||||
|             if (quickAddActive && drag_lines.length > 0) { | ||||
|                 quickAddLink = drag_lines[0]; | ||||
|             } | ||||
|             resetMouseVars(); | ||||
|             hideDragLines(); | ||||
|             redraw(); | ||||
| @@ -1423,6 +1484,10 @@ RED.view = (function() { | ||||
|         //console.log(d,portType,portIndex); | ||||
|         // disable zoom | ||||
|         //vis.call(d3.behavior.zoom().on("zoom"), null); | ||||
|         if (d3.event.button === 1) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         mousedown_node = d; | ||||
|         mousedown_port_type = portType; | ||||
|         mousedown_port_index = portIndex || 0; | ||||
| @@ -1654,6 +1719,9 @@ RED.view = (function() { | ||||
|  | ||||
|     function nodeMouseDown(d) { | ||||
|         focusView(); | ||||
|         if (d3.event.button === 1) { | ||||
|             return; | ||||
|         } | ||||
|         //var touch0 = d3.event; | ||||
|         //var pos = [touch0.pageX,touch0.pageY]; | ||||
|         //RED.touch.radialMenu.show(d3.select(this),pos); | ||||
| @@ -1698,7 +1766,7 @@ RED.view = (function() { | ||||
|         clickTime = now; | ||||
|  | ||||
|         dblClickPrimed = (lastClickNode == mousedown_node && | ||||
|             d3.event.buttons === 1 && | ||||
|             d3.event.button === 0 && | ||||
|             !d3.event.shiftKey && !d3.event.metaKey && !d3.event.altKey && !d3.event.ctrlKey); | ||||
|         lastClickNode = mousedown_node; | ||||
|  | ||||
| @@ -1970,7 +2038,7 @@ RED.view = (function() { | ||||
|                             .attr("ry",4) | ||||
|                             .attr("width",16) | ||||
|                             .attr("height",node_height-12) | ||||
|                             .attr("fill",function(d) { return d._def.color;}) | ||||
|                             .attr("fill",function(d) { return RED.utils.getNodeColor(d.type,d._def); /*d._def.color;*/}) | ||||
|                             .attr("cursor","pointer") | ||||
|                             .on("mousedown",function(d) {if (!lasso && isButtonEnabled(d)) {focusView();d3.select(this).attr("fill-opacity",0.2);d3.event.preventDefault(); d3.event.stopPropagation();}}) | ||||
|                             .on("mouseup",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);d3.event.preventDefault();d3.event.stopPropagation();}}) | ||||
| @@ -1991,7 +2059,7 @@ RED.view = (function() { | ||||
|                         .classed("node_unknown",function(d) { return d.type == "unknown"; }) | ||||
|                         .attr("rx", 5) | ||||
|                         .attr("ry", 5) | ||||
|                         .attr("fill",function(d) { return d._def.color;}) | ||||
|                         .attr("fill",function(d) { return RED.utils.getNodeColor(d.type,d._def); /*d._def.color;*/}) | ||||
|                         .on("mouseup",nodeMouseUp) | ||||
|                         .on("mousedown",nodeMouseDown) | ||||
|                         .on("touchstart",function(d) { | ||||
| @@ -2347,6 +2415,7 @@ RED.view = (function() { | ||||
|                 var l = d3.select(this); | ||||
|                 d.added = true; | ||||
|                 l.append("svg:path").attr("class","link_background link_path") | ||||
|                    .classed("link_link", function(d) { return d.link }) | ||||
|                    .on("mousedown",function(d) { | ||||
|                         mousedown_link = d; | ||||
|                         clearSelection(); | ||||
| @@ -2388,32 +2457,17 @@ RED.view = (function() { | ||||
|                         var numOutputs = d.source.outputs || 1; | ||||
|                         var sourcePort = d.sourcePort || 0; | ||||
|                         var y = -((numOutputs-1)/2)*13 +13*sourcePort; | ||||
|  | ||||
|                         var dy = d.target.y-(d.source.y+y); | ||||
|                         var dx = (d.target.x-d.target.w/2)-(d.source.x+d.source.w/2); | ||||
|                         var delta = Math.sqrt(dy*dy+dx*dx); | ||||
|                         var scale = lineCurveScale; | ||||
|                         var scaleY = 0; | ||||
|                         if (delta < node_width) { | ||||
|                             scale = 0.75-0.75*((node_width-delta)/node_width); | ||||
|                         } | ||||
|  | ||||
|                         if (dx < 0) { | ||||
|                             scale += 2*(Math.min(5*node_width,Math.abs(dx))/(5*node_width)); | ||||
|                             if (Math.abs(dy) < 3*node_height) { | ||||
|                                 scaleY = ((dy>0)?0.5:-0.5)*(((3*node_height)-Math.abs(dy))/(3*node_height))*(Math.min(node_width,Math.abs(dx))/(node_width)) ; | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         d.x1 = d.source.x+d.source.w/2; | ||||
|                         d.y1 = d.source.y+y; | ||||
|                         d.x2 = d.target.x-d.target.w/2; | ||||
|                         d.y2 = d.target.y; | ||||
|  | ||||
|                         return "M "+d.x1+" "+d.y1+ | ||||
|                             " C "+(d.x1+scale*node_width)+" "+(d.y1+scaleY*node_height)+" "+ | ||||
|                             (d.x2-scale*node_width)+" "+(d.y2-scaleY*node_height)+" "+ | ||||
|                             d.x2+" "+d.y2; | ||||
|                         // return "M "+d.x1+" "+d.y1+ | ||||
|                         //     " C "+(d.x1+scale*node_width)+" "+(d.y1+scaleY*node_height)+" "+ | ||||
|                         //     (d.x2-scale*node_width)+" "+(d.y2-scaleY*node_height)+" "+ | ||||
|                         //     d.x2+" "+d.y2; | ||||
|  | ||||
|                         return generateLinkPath(d.x1,d.y1,d.x2,d.y2,1); | ||||
|                     }); | ||||
|                 } | ||||
|             }) | ||||
|   | ||||
							
								
								
									
										4
									
								
								editor/sass/bootstrap.scss
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								editor/sass/bootstrap.scss
									
									
									
									
										vendored
									
									
								
							| @@ -19,3 +19,7 @@ | ||||
| div.btn-group, a.btn { | ||||
|    @include disable-selection; | ||||
| } | ||||
|  | ||||
| .dropdown-menu>li>a { | ||||
|     color: #444; | ||||
| } | ||||
|   | ||||
| @@ -64,22 +64,22 @@ | ||||
|             display: inline-block; | ||||
|         } | ||||
|     } | ||||
|     .debug-message-row { | ||||
|         .debug-message-tools-pin { | ||||
|             display: none; | ||||
|         } | ||||
|         &.debug-message-row-pinned .debug-message-tools-pin { | ||||
|             display: inline-block; | ||||
|         } | ||||
|         &:hover { | ||||
|             background: #f3f3f3; | ||||
|             &>.debug-message-tools { | ||||
|                 .debug-message-tools-copy { | ||||
|                     display: inline-block; | ||||
|                 } | ||||
|                 .debug-message-tools-pin { | ||||
|                     display: inline-block; | ||||
|                 } | ||||
| } | ||||
| .debug-message-row { | ||||
|     .debug-message-tools-pin { | ||||
|         display: none; | ||||
|     } | ||||
|     &.debug-message-row-pinned .debug-message-tools-pin { | ||||
|         display: inline-block; | ||||
|     } | ||||
|     &:hover { | ||||
|         background: #f3f3f3; | ||||
|         &>.debug-message-tools { | ||||
|             .debug-message-tools-copy { | ||||
|                 display: inline-block; | ||||
|             } | ||||
|             .debug-message-tools-pin { | ||||
|                 display: inline-block; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -248,7 +248,7 @@ | ||||
|  | ||||
| .link_outline { | ||||
|     stroke: #fff; | ||||
|     stroke-width: 4; | ||||
|     stroke-width: 5; | ||||
|     cursor: crosshair; | ||||
|     fill: none; | ||||
|     pointer-events: none; | ||||
|   | ||||
| @@ -134,15 +134,18 @@ | ||||
|     color: $workspace-button-toggle-color !important; | ||||
|     background:$workspace-button-background-active; | ||||
|     margin-bottom: 1px; | ||||
|     &.selected:not(.disabled) { | ||||
|  | ||||
|     &.selected:not(.disabled):not(:disabled) { | ||||
|         color: $workspace-button-toggle-color-selected !important; | ||||
|         background: $workspace-button-background; | ||||
|         border-bottom-width: 2px; | ||||
|         border-bottom-color: $form-input-border-selected-color; | ||||
|         margin-bottom: 0; | ||||
|         cursor: default; | ||||
|         &:not(.single) { | ||||
|             cursor: default; | ||||
|         } | ||||
|     } | ||||
|     &.disabled { | ||||
|     &.disabled,&:disabled { | ||||
|         color: $workspace-button-toggle-color-disabled !important; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -90,13 +90,14 @@ | ||||
|     text-align: left; | ||||
|     padding: 9px; | ||||
|     font-weight: bold; | ||||
|     padding-left: 30px; | ||||
|     overflow: hidden; | ||||
|     white-space: nowrap; | ||||
|     text-overflow: ellipsis; | ||||
|  | ||||
|     user-select: none; | ||||
| } | ||||
| .palette-header > i { | ||||
|     margin: 3px 10px 3px 3px; | ||||
|     position: absolute; | ||||
|     left: 11px; | ||||
|     top: 12px; | ||||
|     -webkit-transition: all 0.2s ease-in-out; | ||||
|     -moz-transition: all 0.2s ease-in-out; | ||||
|     -o-transition: all 0.2s ease-in-out; | ||||
|   | ||||
| @@ -40,6 +40,7 @@ | ||||
| @import "panels"; | ||||
| @import "tabs"; | ||||
| @import "tab-config"; | ||||
| @import "tab-context"; | ||||
| @import "tab-info"; | ||||
| @import "popover"; | ||||
| @import "flow"; | ||||
|   | ||||
							
								
								
									
										54
									
								
								editor/sass/tab-context.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								editor/sass/tab-context.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| /** | ||||
|  * Copyright JS Foundation and other contributors, http://js.foundation | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  **/ | ||||
|  | ||||
| .sidebar-context-stack { | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     bottom: 0; | ||||
|     left: 0; | ||||
|     right: 0; | ||||
|     overflow-y: scroll; | ||||
|  | ||||
|     .palette-category { | ||||
|         &:not(.palette-category-expanded) button { | ||||
|             display: none; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .sidebar-context-property { | ||||
|     position: relative; | ||||
|     .debug-message-tools { | ||||
|         right: 0px; | ||||
|         margin-right: 5px; | ||||
|         display: none; | ||||
|     } | ||||
|     &:hover .debug-message-tools { | ||||
|         display: inline-block; | ||||
|     } | ||||
| } | ||||
| .sidebar-context-updated { | ||||
|     text-align: right; | ||||
|     font-size: 11px; | ||||
|     color: #bbb; | ||||
|     padding: 1px 3px; | ||||
| } | ||||
| .sidebar-context-property-storename { | ||||
|     display: block; | ||||
|     font-size: 0.8em; | ||||
|     font-style: italic; | ||||
|     color: #aaa; | ||||
| } | ||||
| @@ -214,12 +214,11 @@ | ||||
|     border-bottom: 1px solid $primary-border-color; | ||||
|     z-index: 2; | ||||
|     a { | ||||
|         @include workspace-button; | ||||
|         @include workspace-button-toggle; | ||||
|         line-height: 26px; | ||||
|         height: 28px; | ||||
|         width: 28px; | ||||
|         margin: 4px 3px 3px; | ||||
|         border: 1px solid $primary-border-color; | ||||
|         z-index: 2; | ||||
|         &.red-ui-tab-link-button { | ||||
|             &:not(.active) { | ||||
|   | ||||
| @@ -23,7 +23,7 @@ | ||||
|     margin: 0; | ||||
|     vertical-align: middle; | ||||
|     box-sizing: border-box; | ||||
|     overflow:hidden; | ||||
|     overflow:visible; | ||||
|     position: relative; | ||||
|     .red-ui-typedInput-input { | ||||
|         position: absolute; | ||||
| @@ -43,6 +43,7 @@ | ||||
|         border-bottom-left-radius: 0; | ||||
|         box-shadow: none; | ||||
|         vertical-align: middle; | ||||
|         // backgroun/d: #f0fff0; | ||||
|     } | ||||
|  | ||||
|     &.red-ui-typedInput-focus:not(.input-error) { | ||||
| @@ -63,7 +64,7 @@ | ||||
|         line-height: 32px; | ||||
|         vertical-align: middle; | ||||
|         color: #555; | ||||
|         i { | ||||
|         i.red-ui-typedInput-icon { | ||||
|             position: relative; | ||||
|             top: -3px; | ||||
|             margin-left: 1px; | ||||
| @@ -76,11 +77,11 @@ | ||||
|         } | ||||
|         &.disabled { | ||||
|             cursor: default; | ||||
|             i { | ||||
|             i.red-ui-typedInput-icon { | ||||
|                 color: #bbb; | ||||
|             } | ||||
|         } | ||||
|         span { | ||||
|         .red-ui-typedInput-type-label,.red-ui-typedInput-option-label { | ||||
|             display: inline-block; | ||||
|             height: 100%; | ||||
|             padding: 0 1px 0 5px; | ||||
| @@ -121,26 +122,25 @@ | ||||
|         border-bottom-right-radius: 4px; | ||||
|         padding: 0 0 0 0; | ||||
|         position:absolute; | ||||
|         width: calc( 100% ); | ||||
|  | ||||
|         i { | ||||
|             position:absolute; | ||||
|             right: 4px; | ||||
|             top: 7px; | ||||
|         } | ||||
|         right: 0; | ||||
|         .red-ui-typedInput-option-label { | ||||
|             background:#fff; | ||||
|             background:$typedInput-button-background; | ||||
|             position:absolute; | ||||
|             left:0; | ||||
|             right:23px; | ||||
|             top: 0; | ||||
|             padding: 0 5px 0 5px; | ||||
|             padding: 0 5px 0 8px; | ||||
|             i.red-ui-typedInput-icon { | ||||
|                 margin-right: 4px; | ||||
|                 margin-top: 4px; | ||||
|             } | ||||
|         } | ||||
|         .red-ui-typedInput-option-caret { | ||||
|             top: 0; | ||||
|             position: absolute; | ||||
|             right: 0; | ||||
|             width: 17px; | ||||
|             padding-left: 6px; | ||||
|         } | ||||
|         &:focus { | ||||
|             box-shadow: none; | ||||
| @@ -175,4 +175,7 @@ | ||||
|             background: $typedInput-button-background-active; | ||||
|         } | ||||
|     } | ||||
|     .red-ui-typedInput-icon { | ||||
|         margin-right: 4px; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -61,7 +61,13 @@ | ||||
| } | ||||
|  | ||||
| #user-settings-tab-view { | ||||
|     position: absolute; | ||||
|     top:0; | ||||
|     right: 0; | ||||
|     left: 0; | ||||
|     bottom: 0; | ||||
|     padding: 8px 20px 20px; | ||||
|     overflow-y: scroll; | ||||
| } | ||||
| .user-settings-row { | ||||
|     padding: 5px 10px 2px; | ||||
|   | ||||
| @@ -51,7 +51,7 @@ | ||||
|             <a class="workspace-footer-button" id="btn-zoom-out" href="#"><i class="fa fa-minus"></i></a> | ||||
|             <a class="workspace-footer-button" id="btn-zoom-zero" href="#"><i class="fa fa-circle-o"></i></a> | ||||
|             <a class="workspace-footer-button" id="btn-zoom-in" href="#"><i class="fa fa-plus"></i></a> | ||||
|             <a class="workspace-footer-button-toggle" id="btn-navigate" href="#"><i class="fa fa-map-o"></i></a> | ||||
|             <a class="workspace-footer-button-toggle single" id="btn-navigate" href="#"><i class="fa fa-map-o"></i></a> | ||||
|         </div> | ||||
|         <div id="editor-shade" class="hide"></div> | ||||
|     </div> | ||||
| @@ -145,76 +145,6 @@ | ||||
|     </div> | ||||
|     <div class="form-row form-tips" id="subflow-dialog-user-count"></div> | ||||
| </script> | ||||
|  | ||||
| <script type="text/x-red" data-template-name="_expression"> | ||||
|     <div id="node-input-expression-panels"> | ||||
|         <div id="node-input-expression-panel-expr" class="red-ui-panel"> | ||||
|             <div class="form-row" style="margin-bottom: 3px; text-align: right;"> | ||||
|                 <span class="node-input-expression-legacy"><i class="fa fa-exclamation-circle"></i> <span data-i18n="expressionEditor.compatMode"></span></span> | ||||
|                 <button id="node-input-expression-reformat" class="editor-button editor-button-small"><span data-i18n="expressionEditor.format"></span></button> | ||||
|             </div> | ||||
|             <div class="form-row node-text-editor-row"> | ||||
|                 <div class="node-text-editor" id="node-input-expression"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div id="node-input-expression-panel-info" class="red-ui-panel"> | ||||
|             <div class="form-row"> | ||||
|                 <ul id="node-input-expression-tabs"></ul> | ||||
|                 <div id="node-input-expression-tab-help" class="node-input-expression-tab-content hide"> | ||||
|                     <div> | ||||
|                         <select id="node-input-expression-func"></select> | ||||
|                         <button id="node-input-expression-func-insert" class="editor-button" data-i18n="expressionEditor.insert"></button> | ||||
|                     </div> | ||||
|                     <div id="node-input-expression-help"></div> | ||||
|                 </div> | ||||
|                 <div id="node-input-expression-tab-test" class="node-input-expression-tab-content hide"> | ||||
|                     <div> | ||||
|                         <span style="display: inline-block; width: calc(50% - 5px);"> | ||||
|                             <span data-i18n="expressionEditor.data"></span> | ||||
|                             <button style="float: right; margin-right: 5px;" id="node-input-example-reformat" class="editor-button editor-button-small"><span data-i18n="jsonEditor.format"></span></button> | ||||
|                         </span> | ||||
|                         <span style="display: inline-block; width: calc(50% - 5px);" data-i18n="expressionEditor.result"></span> | ||||
|                     </div> | ||||
|                     <div style="display: inline-block; width: calc(50% - 5px);" class="node-text-editor" id="node-input-expression-test-data"></div> | ||||
|                     <div style="display: inline-block; width: calc(50% - 5px);" class="node-text-editor" id="node-input-expression-test-result"></div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </script> | ||||
| <script type="text/x-red" data-template-name="_json"> | ||||
|     <div class="form-row" style="margin-bottom: 3px; text-align: right;"> | ||||
|         <button id="node-input-json-reformat" class="editor-button editor-button-small"><span data-i18n="jsonEditor.format"></span></button> | ||||
|     </div> | ||||
|     <div class="form-row node-text-editor-row"> | ||||
|         <div style="height: 200px;min-height: 150px;" class="node-text-editor" id="node-input-json"></div> | ||||
|     </div> | ||||
| </script> | ||||
| <script type="text/x-red" data-template-name="_markdown"> | ||||
|     <div class="form-row" id="node-input-markdown-title" style="margin-bottom: 3px; text-align: right;"> | ||||
|  | ||||
|     </div> | ||||
|     <div class="form-row node-text-editor-row"> | ||||
|         <div style="height: 200px;min-height: 150px;" class="node-text-editor" id="node-input-markdown"></div> | ||||
|     </div> | ||||
| </script> | ||||
| <script type="text/x-red" data-template-name="_buffer"> | ||||
|     <div id="node-input-buffer-panels"> | ||||
|         <div id="node-input-buffer-panel-str" class="red-ui-panel"> | ||||
|             <div class="form-row" style="margin-bottom: 3px; text-align: right;"> | ||||
|                 <span class="node-input-buffer-type"><i class="fa fa-exclamation-circle"></i> <span id="node-input-buffer-type-string" data-i18n="bufferEditor.modeString"></span><span id="node-input-buffer-type-array" data-i18n="bufferEditor.modeArray"></span></span> | ||||
|             </div> | ||||
|             <div class="form-row node-text-editor-row"> | ||||
|                 <div class="node-text-editor" id="node-input-buffer-str"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div id="node-input-buffer-panel-bin" class="red-ui-panel"> | ||||
|             <div class="form-row node-text-editor-row" style="margin-top: 10px"> | ||||
|                 <div class="node-text-editor" id="node-input-buffer-bin"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </script> | ||||
| <script src="vendor/vendor.js"></script> | ||||
| <script src="vendor/jsonata/jsonata.min.js"></script> | ||||
| <script src="vendor/ace/ace.js"></script> | ||||
|   | ||||
| @@ -67,6 +67,7 @@ | ||||
|         }, | ||||
|         inputs:1,               // set the number of inputs - only 0 or 1 | ||||
|         outputs:1,              // set the number of outputs - 0 to n | ||||
|         color: "#ddd",          // set icon color | ||||
|         // set the icon (held in icons dir below where you save the node) | ||||
|         icon: "myicon.png",     // saved in  icons/myicon.png | ||||
|         label: function() {     // sets the default label contents | ||||
|   | ||||
| @@ -237,10 +237,9 @@ If you want every 20 minutes from now - use the <i>"interval"</i> option.</p> | ||||
|                 } else { | ||||
|                     return this._("inject.timestamp")+suffix; | ||||
|                 } | ||||
|             } else if (this.payloadType === 'flow' && this.payload.length < 19) { | ||||
|                 return 'flow.'+this.payload+suffix; | ||||
|             } else if (this.payloadType === 'global' && this.payload.length < 17) { | ||||
|                 return 'global.'+this.payload+suffix; | ||||
|             } else if (this.payloadType === 'flow' || this.payloadType === 'global') { | ||||
|                 var key = RED.utils.parseContextKey(this.payload); | ||||
|                 return this.payloadType+"."+key.key+suffix; | ||||
|             } else { | ||||
|                 return this._("inject.inject")+suffix; | ||||
|             } | ||||
| @@ -502,7 +501,13 @@ If you want every 20 minutes from now - use the <i>"interval"</i> option.</p> | ||||
|                 if (this.changed) { | ||||
|                     return RED.notify(RED._("notification.warning", {message:RED._("notification.warnings.undeployedChanges")}),"warning"); | ||||
|                 } | ||||
|                 var label = (this.name||this.payload); | ||||
|                 var payload = this.payload; | ||||
|                 if ((this.payloadType === 'flow') || | ||||
|                     (this.payloadType === 'global')) { | ||||
|                     var key = RED.utils.parseContextKey(payload); | ||||
|                     payload = this.payloadType+"."+key.key; | ||||
|                 } | ||||
|                 var label = (this.name||payload); | ||||
|                 if (label.length > 30) { | ||||
|                     label = label.substring(0,50)+"..."; | ||||
|                 } | ||||
|   | ||||
| @@ -63,21 +63,33 @@ module.exports = function(RED) { | ||||
|         } | ||||
|  | ||||
|         this.on("input",function(msg) { | ||||
|             try { | ||||
|                 msg.topic = this.topic; | ||||
|                 if ( (this.payloadType == null && this.payload === "") || this.payloadType === "date") { | ||||
|                     msg.payload = Date.now(); | ||||
|                 } else if (this.payloadType == null) { | ||||
|                     msg.payload = this.payload; | ||||
|                 } else if (this.payloadType === 'none') { | ||||
|                     msg.payload = ""; | ||||
|                 } else { | ||||
|                     msg.payload = RED.util.evaluateNodeProperty(this.payload,this.payloadType,this,msg); | ||||
|             msg.topic = this.topic; | ||||
|             if (this.payloadType !== 'flow' && this.payloadType !== 'global') { | ||||
|                 try { | ||||
|                     if ( (this.payloadType == null && this.payload === "") || this.payloadType === "date") { | ||||
|                         msg.payload = Date.now(); | ||||
|                     } else if (this.payloadType == null) { | ||||
|                         msg.payload = this.payload; | ||||
|                     } else if (this.payloadType === 'none') { | ||||
|                         msg.payload = ""; | ||||
|                     } else { | ||||
|                         msg.payload = RED.util.evaluateNodeProperty(this.payload,this.payloadType,this,msg); | ||||
|                     } | ||||
|                     this.send(msg); | ||||
|                     msg = null; | ||||
|                 } catch(err) { | ||||
|                     this.error(err,msg); | ||||
|                 } | ||||
|                 this.send(msg); | ||||
|                 msg = null; | ||||
|             } catch(err) { | ||||
|                 this.error(err,msg); | ||||
|             } else { | ||||
|                 RED.util.evaluateNodeProperty(this.payload,this.payloadType,this,msg, function(err,res) { | ||||
|                     if (err) { | ||||
|                         node.error(err,msg); | ||||
|                     } else { | ||||
|                         msg.payload = res; | ||||
|                         node.send(msg); | ||||
|                     } | ||||
|  | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|   | ||||
| @@ -156,7 +156,7 @@ | ||||
|                 toolbar: uiComponents.footer, | ||||
|                 enableOnEdit: true, | ||||
|                 pinned: true, | ||||
|                 iconClass: "fa fa-list-alt" | ||||
|                 iconClass: "fa fa-bug" | ||||
|             }); | ||||
|             RED.actions.add("core:show-debug-tab",function() { RED.sidebar.show('debug'); }); | ||||
|  | ||||
|   | ||||
| @@ -4,7 +4,6 @@ module.exports = function(RED) { | ||||
|     var util = require("util"); | ||||
|     var events = require("events"); | ||||
|     var path = require("path"); | ||||
|     var safeJSONStringify = require("json-stringify-safe"); | ||||
|     var debuglength = RED.settings.debugMaxLength || 1000; | ||||
|     var useColors = RED.settings.debugUseColors || false; | ||||
|     util.inspect.styles.boolean = "red"; | ||||
| @@ -20,7 +19,11 @@ module.exports = function(RED) { | ||||
|         if (this.tosidebar === undefined) { this.tosidebar = true; } | ||||
|         this.severity = n.severity || 40; | ||||
|         this.active = (n.active === null || typeof n.active === "undefined") || n.active; | ||||
|         this.status({}); | ||||
|         if (this.tostatus) { | ||||
|             this.oldStatus = {fill:"grey", shape:"ring"}; | ||||
|             this.status(this.oldStatus); | ||||
|         } | ||||
|         else { this.status({}); } | ||||
|  | ||||
|         var node = this; | ||||
|         var levels = { | ||||
| @@ -104,111 +107,7 @@ module.exports = function(RED) { | ||||
|     function sendDebug(msg) { | ||||
|         // don't put blank errors in sidebar (but do add to logs) | ||||
|         //if ((msg.msg === "") && (msg.hasOwnProperty("level")) && (msg.level === 20)) { return; } | ||||
|         if (msg.msg instanceof Error) { | ||||
|             msg.format = "error"; | ||||
|             var errorMsg = {}; | ||||
|             if (msg.msg.name) { | ||||
|                 errorMsg.name = msg.msg.name; | ||||
|             } | ||||
|             if (msg.msg.hasOwnProperty('message')) { | ||||
|                 errorMsg.message = msg.msg.message; | ||||
|             } else { | ||||
|                 errorMsg.message = msg.msg.toString(); | ||||
|             } | ||||
|             msg.msg = JSON.stringify(errorMsg); | ||||
|         } else if (msg.msg instanceof Buffer) { | ||||
|             msg.format = "buffer["+msg.msg.length+"]"; | ||||
|             msg.msg = msg.msg.toString('hex'); | ||||
|             if (msg.msg.length > debuglength) { | ||||
|                 msg.msg = msg.msg.substring(0,debuglength); | ||||
|             } | ||||
|         } else if (msg.msg && typeof msg.msg === 'object') { | ||||
|             try { | ||||
|                 msg.format = msg.msg.constructor.name || "Object"; | ||||
|                 // Handle special case of msg.req/res objects from HTTP In node | ||||
|                 if (msg.format === "IncomingMessage" || msg.format === "ServerResponse") { | ||||
|                     msg.format = "Object"; | ||||
|                 } | ||||
|             } catch(err) { | ||||
|                 msg.format = "Object"; | ||||
|             } | ||||
|             if (/error/i.test(msg.format)) { | ||||
|                 msg.msg = JSON.stringify({ | ||||
|                     name: msg.msg.name, | ||||
|                     message: msg.msg.message | ||||
|                 }); | ||||
|             } else { | ||||
|                 var isArray = util.isArray(msg.msg); | ||||
|                 if (isArray) { | ||||
|                     msg.format = "array["+msg.msg.length+"]"; | ||||
|                     if (msg.msg.length > debuglength) { | ||||
|                         // msg.msg = msg.msg.slice(0,debuglength); | ||||
|                         msg.msg = { | ||||
|                             __encoded__: true, | ||||
|                             type: "array", | ||||
|                             data: msg.msg.slice(0,debuglength), | ||||
|                             length: msg.msg.length | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 if (isArray || (msg.format === "Object")) { | ||||
|                     msg.msg = safeJSONStringify(msg.msg, function(key, value) { | ||||
|                         if (key === '_req' || key === '_res') { | ||||
|                             value = "[internal]" | ||||
|                         } else if (value instanceof Error) { | ||||
|                             value = value.toString() | ||||
|                         } else if (util.isArray(value) && value.length > debuglength) { | ||||
|                             value = { | ||||
|                                 __encoded__: true, | ||||
|                                 type: "array", | ||||
|                                 data: value.slice(0,debuglength), | ||||
|                                 length: value.length | ||||
|                             } | ||||
|                         } else if (typeof value === 'string') { | ||||
|                             if (value.length > debuglength) { | ||||
|                                 value = value.substring(0,debuglength)+"..."; | ||||
|                             } | ||||
|                         } else if (value && value.constructor) { | ||||
|                             if (value.type === "Buffer") { | ||||
|                                 value.__encoded__ = true; | ||||
|                                 value.length = value.data.length; | ||||
|                                 if (value.length > debuglength) { | ||||
|                                     value.data = value.data.slice(0,debuglength); | ||||
|                                 } | ||||
|                             } else if (value.constructor.name === "ServerResponse") { | ||||
|                                 value = "[internal]" | ||||
|                             } else if (value.constructor.name === "Socket") { | ||||
|                                 value = "[internal]" | ||||
|                             } | ||||
|                         } | ||||
|                         return value; | ||||
|                     }," "); | ||||
|                 } else { | ||||
|                     try { msg.msg = msg.msg.toString(); } | ||||
|                     catch(e) { msg.msg = "[Type not printable]"; } | ||||
|                 } | ||||
|             } | ||||
|         } else if (typeof msg.msg === "boolean") { | ||||
|             msg.format = "boolean"; | ||||
|             msg.msg = msg.msg.toString(); | ||||
|         } else if (typeof msg.msg === "number") { | ||||
|             msg.format = "number"; | ||||
|             msg.msg = msg.msg.toString(); | ||||
|         } else if (msg.msg === 0) { | ||||
|             msg.format = "number"; | ||||
|             msg.msg = "0"; | ||||
|         } else if (msg.msg === null || typeof msg.msg === "undefined") { | ||||
|             msg.format = (msg.msg === null)?"null":"undefined"; | ||||
|             msg.msg = "(undefined)"; | ||||
|         } else { | ||||
|             msg.format = "string["+msg.msg.length+"]"; | ||||
|             if (msg.msg.length > debuglength) { | ||||
|                 msg.msg = msg.msg.substring(0,debuglength)+"..."; | ||||
|             } | ||||
|         } | ||||
|         // if (msg.msg.length > debuglength) { | ||||
|         //     msg.msg = msg.msg.substr(0,debuglength) +" ...."; | ||||
|         // } | ||||
|         msg = RED.util.encodeObject(msg,{maxLength:debuglength}); | ||||
|         RED.comms.publish("debug",msg); | ||||
|     } | ||||
|  | ||||
| @@ -227,12 +126,12 @@ module.exports = function(RED) { | ||||
|             if (state === "enable") { | ||||
|                 node.active = true; | ||||
|                 res.sendStatus(200); | ||||
|                 if (node.tostatus) { node.status({}); } | ||||
|                 if (node.tostatus) { node.status({fill:"grey", shape:"dot"}); } | ||||
|             } else if (state === "disable") { | ||||
|                 node.active = false; | ||||
|                 res.sendStatus(201); | ||||
|                 if (node.tostatus && node.hasOwnProperty("oldStatus")) { | ||||
|                     node.oldStatus.shape = "ring"; | ||||
|                     node.oldStatus.shape = "dot"; | ||||
|                     node.status(node.oldStatus); | ||||
|                 } | ||||
|             } else { | ||||
|   | ||||
| @@ -9,7 +9,8 @@ | ||||
|         <input type="hidden" id="node-input-func" autofocus="autofocus"> | ||||
|         <input type="hidden" id="node-input-noerr"> | ||||
|     </div> | ||||
|     <div class="form-row node-text-editor-row"> | ||||
|     <div class="form-row node-text-editor-row" style="position:relative"> | ||||
|         <div style="position: absolute; right:0; bottom:calc(100% + 3px);"><button id="node-function-expand-js" class="editor-button editor-button-small"><i class="fa fa-expand"></i></button></div> | ||||
|         <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-func-editor" ></div> | ||||
|     </div> | ||||
|     <div class="form-row"> | ||||
| @@ -119,6 +120,23 @@ | ||||
|                 fields:['name','outputs'] | ||||
|             }); | ||||
|             this.editor.focus(); | ||||
|  | ||||
|             $("#node-function-expand-js").click(function(e) { | ||||
|                 e.preventDefault(); | ||||
|                 var value = that.editor.getValue(); | ||||
|                 RED.editor.editJavaScript({ | ||||
|                     value: value, | ||||
|                     width: "Infinity", | ||||
|                     cursor: that.editor.getCursorPosition(), | ||||
|                     complete: function(v,cursor) { | ||||
|                         that.editor.setValue(v, -1); | ||||
|                         that.editor.gotoLine(cursor.row+1,cursor.column,false); | ||||
|                         setTimeout(function() { | ||||
|                             that.editor.focus(); | ||||
|                         },300); | ||||
|                     } | ||||
|                 }) | ||||
|             }) | ||||
|         }, | ||||
|         oneditsave: function() { | ||||
|             var annot = this.editor.getSession().getAnnotations(); | ||||
|   | ||||
| @@ -42,7 +42,7 @@ module.exports = function(RED) { | ||||
|                             if (type === 'object') { | ||||
|                                 type = Buffer.isBuffer(msg)?'Buffer':(util.isArray(msg)?'Array':'Date'); | ||||
|                             } | ||||
|                             node.error(RED._("function.error.non-message-returned",{ type: type })) | ||||
|                             node.error(RED._("function.error.non-message-returned",{ type: type })); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
| @@ -203,9 +203,9 @@ module.exports = function(RED) { | ||||
|         if (util.hasOwnProperty('promisify')) { | ||||
|             sandbox.setTimeout[util.promisify.custom] = function(after, value) { | ||||
|                 return new Promise(function(resolve, reject) { | ||||
|                     sandbox.setTimeout(function(){ resolve(value) }, after); | ||||
|                     sandbox.setTimeout(function(){ resolve(value); }, after); | ||||
|                 }); | ||||
|             } | ||||
|             }; | ||||
|         } | ||||
|         var context = vm.createContext(sandbox); | ||||
|         try { | ||||
| @@ -241,7 +241,6 @@ module.exports = function(RED) { | ||||
|  | ||||
|                     var line = 0; | ||||
|                     var errorMessage; | ||||
|                     var stack = err.stack.split(/\r?\n/); | ||||
|                     if (stack.length > 0) { | ||||
|                         while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) { | ||||
|                             line++; | ||||
| @@ -265,13 +264,13 @@ module.exports = function(RED) { | ||||
|             }); | ||||
|             this.on("close", function() { | ||||
|                 while (node.outstandingTimers.length > 0) { | ||||
|                     clearTimeout(node.outstandingTimers.pop()) | ||||
|                     clearTimeout(node.outstandingTimers.pop()); | ||||
|                 } | ||||
|                 while (node.outstandingIntervals.length > 0) { | ||||
|                     clearInterval(node.outstandingIntervals.pop()) | ||||
|                     clearInterval(node.outstandingIntervals.pop()); | ||||
|                 } | ||||
|                 this.status({}); | ||||
|             }) | ||||
|             }); | ||||
|         } catch(err) { | ||||
|             // eg SyntaxError - which v8 doesn't include line number information | ||||
|             // so we can't do better than this | ||||
| @@ -280,4 +279,4 @@ module.exports = function(RED) { | ||||
|     } | ||||
|     RED.nodes.registerType("function",FunctionNode); | ||||
|     RED.library.register("functions"); | ||||
| } | ||||
| }; | ||||
|   | ||||
| @@ -77,7 +77,9 @@ | ||||
| }</pre> | ||||
|     <p>The resulting property will be: | ||||
|     <pre>Hello Fred. Today is Monday</pre> | ||||
|     <p>It is possible to use a property from the flow context or global context. Just use <code>{{flow.name}}</code> or <code>{{global.name}}</code>. | ||||
|     <p>It is possible to use a property from the flow context or global context. Just use <code>{{flow.name}}</code> or | ||||
|     <code>{{global.name}}</code>, or for persistable store <code>store</code> use <code>{{flow[store].name}}</code> or | ||||
|     <code>{{global[store].name}}</code>. | ||||
|     <p><b>Note: </b>By default, <i>mustache</i> will escape any HTML entities in the values it substitutes. | ||||
|        To prevent this, use <code>{{{triple}}}</code> braces. | ||||
| </script> | ||||
|   | ||||
| @@ -19,15 +19,41 @@ module.exports = function(RED) { | ||||
|     var mustache = require("mustache"); | ||||
|     var yaml = require("js-yaml"); | ||||
|  | ||||
|     function extractTokens(tokens,set) { | ||||
|         set = set || new Set(); | ||||
|         tokens.forEach(function(token) { | ||||
|             if (token[0] !== 'text') { | ||||
|                 set.add(token[1]); | ||||
|                 if (token.length > 4) { | ||||
|                     extractTokens(token[4],set); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|         return set; | ||||
|     } | ||||
|  | ||||
|     function parseContext(key) { | ||||
|         var match = /^(flow|global)(\[(\w+)\])?\.(.+)/.exec(key); | ||||
|         if (match) { | ||||
|             var parts = {}; | ||||
|             parts.type = match[1]; | ||||
|             parts.store = (match[3] === '') ? "default" : match[3]; | ||||
|             parts.field = match[4]; | ||||
|             return parts; | ||||
|         } | ||||
|         return undefined; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Custom Mustache Context capable to resolve message property and node | ||||
|      * Custom Mustache Context capable to collect message property and node | ||||
|      * flow and global context | ||||
|      */ | ||||
|     function NodeContext(msg, nodeContext, parent, escapeStrings) { | ||||
|  | ||||
|     function NodeContext(msg, nodeContext, parent, escapeStrings, cachedContextTokens) { | ||||
|         this.msgContext = new mustache.Context(msg,parent); | ||||
|         this.nodeContext = nodeContext; | ||||
|         this.escapeStrings = escapeStrings; | ||||
|         this.cachedContextTokens = cachedContextTokens; | ||||
|     } | ||||
|  | ||||
|     NodeContext.prototype = new mustache.Context(); | ||||
| @@ -48,20 +74,18 @@ module.exports = function(RED) { | ||||
|                 return value; | ||||
|             } | ||||
|  | ||||
|             // try node context: | ||||
|             var dot = name.indexOf("."); | ||||
|             /* istanbul ignore else  */ | ||||
|             if (dot > 0) { | ||||
|                 var contextName = name.substr(0, dot); | ||||
|                 var variableName = name.substr(dot + 1); | ||||
|  | ||||
|                 if (contextName === "flow" && this.nodeContext.flow) { | ||||
|                     return this.nodeContext.flow.get(variableName); | ||||
|                 } | ||||
|                 else if (contextName === "global" && this.nodeContext.global) { | ||||
|                     return this.nodeContext.global.get(variableName); | ||||
|             // try flow/global context: | ||||
|             var context = parseContext(name); | ||||
|             if (context) { | ||||
|                 var type = context.type; | ||||
|                 var store = context.store; | ||||
|                 var field = context.field; | ||||
|                 var target = this.nodeContext[type]; | ||||
|                 if (target) { | ||||
|                     return this.cachedContextTokens[name]; | ||||
|                 } | ||||
|             } | ||||
|             return ''; | ||||
|         } | ||||
|         catch(err) { | ||||
|             throw err; | ||||
| @@ -69,7 +93,7 @@ module.exports = function(RED) { | ||||
|     } | ||||
|  | ||||
|     NodeContext.prototype.push = function push (view) { | ||||
|         return new NodeContext(view, this.nodeContext,this.msgContext); | ||||
|         return new NodeContext(view, this.nodeContext, this.msgContext, undefined, this.cachedContextTokens); | ||||
|     }; | ||||
|  | ||||
|     function TemplateNode(n) { | ||||
| @@ -82,9 +106,37 @@ module.exports = function(RED) { | ||||
|         this.outputFormat = n.output || "str"; | ||||
|  | ||||
|         var node = this; | ||||
|  | ||||
|         function output(msg,value) { | ||||
|             /* istanbul ignore else  */ | ||||
|             if (node.outputFormat === "json") { | ||||
|                 value = JSON.parse(value); | ||||
|             } | ||||
|             /* istanbul ignore else  */ | ||||
|             if (node.outputFormat === "yaml") { | ||||
|                 value = yaml.load(value); | ||||
|             } | ||||
|  | ||||
|             if (node.fieldType === 'msg') { | ||||
|                 RED.util.setMessageProperty(msg, node.field, value); | ||||
|                 node.send(msg); | ||||
|             } else if ((node.fieldType === 'flow') || | ||||
|                        (node.fieldType === 'global')) { | ||||
|                 var context = RED.util.parseContextStore(node.field); | ||||
|                 var target = node.context()[node.fieldType]; | ||||
|                 target.set(context.key, value, context.store, function (err) { | ||||
|                     if (err) { | ||||
|                         node.error(err, msg); | ||||
|                     } else { | ||||
|                         node.send(msg); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         node.on("input", function(msg) { | ||||
|  | ||||
|             try { | ||||
|                 var value; | ||||
|                 /*** | ||||
|                 * Allow template contents to be defined externally | ||||
|                 * through inbound msg.template IFF node.template empty | ||||
| @@ -97,34 +149,46 @@ module.exports = function(RED) { | ||||
|                 } | ||||
|  | ||||
|                 if (node.syntax === "mustache") { | ||||
|                     if (node.outputFormat === "json") { | ||||
|                         value = mustache.render(template,new NodeContext(msg, node.context(), null, true)); | ||||
|                     } else { | ||||
|                         value = mustache.render(template,new NodeContext(msg, node.context(), null, false)); | ||||
|                     } | ||||
|                 } else { | ||||
|                     value = template; | ||||
|                 } | ||||
|                 /* istanbul ignore else  */ | ||||
|                 if (node.outputFormat === "json") { | ||||
|                     value = JSON.parse(value); | ||||
|                 } | ||||
|                 /* istanbul ignore else  */ | ||||
|                 if (node.outputFormat === "yaml") { | ||||
|                     value = yaml.load(value); | ||||
|                 } | ||||
|                     var is_json = (node.outputFormat === "json"); | ||||
|                     var promises = []; | ||||
|                     var tokens = extractTokens(mustache.parse(template)); | ||||
|                     var resolvedTokens = {}; | ||||
|                     tokens.forEach(function(name) { | ||||
|                         var context = parseContext(name); | ||||
|                         if (context) { | ||||
|                             var type = context.type; | ||||
|                             var store = context.store; | ||||
|                             var field = context.field; | ||||
|                             var target = node.context()[type]; | ||||
|                             if (target) { | ||||
|                                 var promise = new Promise((resolve, reject) => { | ||||
|                                     target.get(field, store, (err, val) => { | ||||
|                                         if (err) { | ||||
|                                             reject(err); | ||||
|                                         } else { | ||||
|                                             resolvedTokens[name] = val; | ||||
|                                             resolve(); | ||||
|                                         } | ||||
|                                     }); | ||||
|                                 }); | ||||
|                                 promises.push(promise); | ||||
|                                 return; | ||||
|                             } | ||||
|                         } | ||||
|                     }); | ||||
|  | ||||
|                 if (node.fieldType === 'msg') { | ||||
|                     RED.util.setMessageProperty(msg,node.field,value); | ||||
|                 } else if (node.fieldType === 'flow') { | ||||
|                     node.context().flow.set(node.field,value); | ||||
|                 } else if (node.fieldType === 'global') { | ||||
|                     node.context().global.set(node.field,value); | ||||
|                     Promise.all(promises).then(function() { | ||||
|                         var value = mustache.render(template, new NodeContext(msg, node.context(), null, is_json, resolvedTokens)); | ||||
|                         output(msg, value); | ||||
|                     }).catch(function (err) { | ||||
|                         node.error(err.message,msg); | ||||
|                     }); | ||||
|                 } else { | ||||
|                     output(msg, template); | ||||
|                 } | ||||
|                 node.send(msg); | ||||
|             } | ||||
|             catch(err) { | ||||
|                 node.error(err.message); | ||||
|                 node.error(err.message, msg); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|   | ||||
| @@ -162,7 +162,7 @@ | ||||
|             $("#node-input-op1").typedInput({ | ||||
|                 default: 'str', | ||||
|                 typeField: $("#node-input-op1type"), | ||||
|                 types:['flow','global','str','num','bool','json', | ||||
|                 types:['flow','global','str','num','bool','json','bin','date','env', | ||||
|                     optionPayload, | ||||
|                     optionNothing | ||||
|                 ] | ||||
| @@ -170,7 +170,7 @@ | ||||
|             $("#node-input-op2").typedInput({ | ||||
|                 default: 'str', | ||||
|                 typeField: $("#node-input-op2type"), | ||||
|                 types:['flow','global','str','num','bool','json', | ||||
|                 types:['flow','global','str','num','bool','json','bin','date','env', | ||||
|                     optionOriginalPayload, | ||||
|                     optionLatestPayload, | ||||
|                     optionNothing | ||||
|   | ||||
| @@ -76,8 +76,43 @@ module.exports = function(RED) { | ||||
|         var node = this; | ||||
|         node.topics = {}; | ||||
|  | ||||
|         this.on("input", function(msg) { | ||||
|         var pendingMessages = []; | ||||
|         var activeMessagePromise = null; | ||||
|         var processMessageQueue = function(msg) { | ||||
|             if (msg) { | ||||
|                 // A new message has arrived - add it to the message queue | ||||
|                 pendingMessages.push(msg); | ||||
|                 if (activeMessagePromise !== null) { | ||||
|                     // The node is currently processing a message, so do nothing | ||||
|                     // more with this message | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|             if (pendingMessages.length === 0) { | ||||
|                 // There are no more messages to process, clear the active flag | ||||
|                 // and return | ||||
|                 activeMessagePromise = null; | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // There are more messages to process. Get the next message and | ||||
|             // start processing it. Recurse back in to check for any more | ||||
|             var nextMsg = pendingMessages.shift(); | ||||
|             activeMessagePromise = processMessage(nextMsg) | ||||
|                 .then(processMessageQueue) | ||||
|                 .catch((err) => { | ||||
|                     node.error(err,nextMsg); | ||||
|                     return processMessageQueue(); | ||||
|                 }); | ||||
|         } | ||||
|  | ||||
|         this.on('input', function(msg) { | ||||
|             processMessageQueue(msg); | ||||
|         }); | ||||
|  | ||||
|         var processMessage = function(msg) { | ||||
|             var topic = msg.topic || "_none"; | ||||
|             var promise; | ||||
|             if (node.bytopic === "all") { topic = "_none"; } | ||||
|             node.topics[topic] = node.topics[topic] || {}; | ||||
|             if (msg.hasOwnProperty("reset") || ((node.reset !== '') && msg.hasOwnProperty("payload") && (msg.payload !== null) && msg.payload.toString && (msg.payload.toString() == node.reset)) ) { | ||||
| @@ -88,48 +123,88 @@ module.exports = function(RED) { | ||||
|             } | ||||
|             else { | ||||
|                 if (((!node.topics[topic].tout) && (node.topics[topic].tout !== 0)) || (node.loop === true)) { | ||||
|                     promise = Promise.resolve(); | ||||
|                     if (node.op2type === "pay" || node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); } | ||||
|                     else if (node.op2Templated) { node.topics[topic].m2 = mustache.render(node.op2,msg); } | ||||
|                     else if (node.op2type !== "nul") { | ||||
|                         node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg); | ||||
|                     } | ||||
|  | ||||
|                     if (node.op1type === "pay") { } | ||||
|                     else if (node.op1Templated) { msg.payload = mustache.render(node.op1,msg); } | ||||
|                     else if (node.op1type !== "nul") { | ||||
|                         msg.payload = RED.util.evaluateNodeProperty(node.op1,node.op1type,node,msg); | ||||
|                     } | ||||
|  | ||||
|                     if (node.duration === 0) { node.topics[topic].tout = 0; } | ||||
|                     else if (node.loop === true) { | ||||
|                         /* istanbul ignore else  */ | ||||
|                         if (node.topics[topic].tout) { clearInterval(node.topics[topic].tout); } | ||||
|                         /* istanbul ignore else  */ | ||||
|                         if (node.op1type !== "nul") { | ||||
|                             var msg2 = RED.util.cloneMessage(msg); | ||||
|                             node.topics[topic].tout = setInterval(function() { node.send(RED.util.cloneMessage(msg2)); }, node.duration); | ||||
|                         } | ||||
|                     } | ||||
|                     else { | ||||
|                         if (!node.topics[topic].tout) { | ||||
|                             node.topics[topic].tout = setTimeout(function() { | ||||
|                                 var msg2 = null; | ||||
|                                 if (node.op2type !== "nul") { | ||||
|                                     msg2 = RED.util.cloneMessage(msg); | ||||
|                                     if (node.op2type === "flow" || node.op2type === "global") { | ||||
|                                         node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg); | ||||
|                                     } | ||||
|                                     msg2.payload = node.topics[topic].m2; | ||||
|                                     delete node.topics[topic]; | ||||
|                                     node.send(msg2); | ||||
|                         promise = new Promise((resolve,reject) => { | ||||
|                             RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => { | ||||
|                                 if (err) { | ||||
|                                     reject(err); | ||||
|                                 } else { | ||||
|                                     node.topics[topic].m2 = value; | ||||
|                                     resolve(); | ||||
|                                 } | ||||
|                                 else { delete node.topics[topic]; } | ||||
|                                 node.status({}); | ||||
|                             }, node.duration); | ||||
|                         } | ||||
|                             }); | ||||
|                         }); | ||||
|                     } | ||||
|                     node.status({fill:"blue",shape:"dot",text:" "}); | ||||
|                     if (node.op1type !== "nul") { node.send(RED.util.cloneMessage(msg)); } | ||||
|  | ||||
|                     return promise.then(() => { | ||||
|                         promise = Promise.resolve(); | ||||
|                         if (node.op1type === "pay") { } | ||||
|                         else if (node.op1Templated) { msg.payload = mustache.render(node.op1,msg); } | ||||
|                         else if (node.op1type !== "nul") { | ||||
|                             promise = new Promise((resolve,reject) => { | ||||
|                                 RED.util.evaluateNodeProperty(node.op1,node.op1type,node,msg,(err,value) => { | ||||
|                                     if (err) { | ||||
|                                         reject(err); | ||||
|                                     } else { | ||||
|                                         msg.payload = value; | ||||
|                                         resolve(); | ||||
|                                     } | ||||
|                                 }); | ||||
|                             }); | ||||
|                         } | ||||
|                         return promise.then(() => { | ||||
|                             if (node.duration === 0) { node.topics[topic].tout = 0; } | ||||
|                             else if (node.loop === true) { | ||||
|                                 /* istanbul ignore else  */ | ||||
|                                 if (node.topics[topic].tout) { clearInterval(node.topics[topic].tout); } | ||||
|                                 /* istanbul ignore else  */ | ||||
|                                 if (node.op1type !== "nul") { | ||||
|                                     var msg2 = RED.util.cloneMessage(msg); | ||||
|                                     node.topics[topic].tout = setInterval(function() { node.send(RED.util.cloneMessage(msg2)); }, node.duration); | ||||
|                                 } | ||||
|                             } | ||||
|                             else { | ||||
|                                 if (!node.topics[topic].tout) { | ||||
|                                     node.topics[topic].tout = setTimeout(function() { | ||||
|                                         var msg2 = null; | ||||
|                                         if (node.op2type !== "nul") { | ||||
|                                             var promise = Promise.resolve(); | ||||
|                                             msg2 = RED.util.cloneMessage(msg); | ||||
|                                             if (node.op2type === "flow" || node.op2type === "global") { | ||||
|                                                 promise = new Promise((resolve,reject) => { | ||||
|                                                     RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => { | ||||
|                                                         if (err) { | ||||
|                                                             reject(err); | ||||
|                                                         } else { | ||||
|                                                             node.topics[topic].m2 = value; | ||||
|                                                             resolve(); | ||||
|                                                         } | ||||
|                                                     }); | ||||
|                                                 }); | ||||
|                                             } | ||||
|                                             promise.then(() => { | ||||
|                                                 msg2.payload = node.topics[topic].m2; | ||||
|                                                 delete node.topics[topic]; | ||||
|                                                 node.send(msg2); | ||||
|                                                 node.status({}); | ||||
|                                             }).catch(err => { | ||||
|                                                 node.error(err); | ||||
|                                             }); | ||||
|                                         } else { | ||||
|                                             delete node.topics[topic]; | ||||
|                                             node.status({}); | ||||
|                                         } | ||||
|  | ||||
|                                     }, node.duration); | ||||
|                                 } | ||||
|                             } | ||||
|                             node.status({fill:"blue",shape:"dot",text:" "}); | ||||
|                             if (node.op1type !== "nul") { node.send(RED.util.cloneMessage(msg)); } | ||||
|                         }); | ||||
|                     }); | ||||
|                 } | ||||
|                 else if ((node.extend === "true" || node.extend === true) && (node.duration > 0)) { | ||||
|                     /* istanbul ignore else  */ | ||||
| @@ -138,25 +213,43 @@ module.exports = function(RED) { | ||||
|                     if (node.topics[topic].tout) { clearTimeout(node.topics[topic].tout); } | ||||
|                     node.topics[topic].tout = setTimeout(function() { | ||||
|                         var msg2 = null; | ||||
|                         var promise = Promise.resolve(); | ||||
|  | ||||
|                         if (node.op2type !== "nul") { | ||||
|                             if (node.op2type === "flow" || node.op2type === "global") { | ||||
|                                 node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg); | ||||
|                             } | ||||
|                             if (node.topics[topic] !== undefined) { | ||||
|                                 msg2 = RED.util.cloneMessage(msg); | ||||
|                                 msg2.payload = node.topics[topic].m2; | ||||
|                                 promise = new Promise((resolve,reject) => { | ||||
|                                     RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => { | ||||
|                                         if (err) { | ||||
|                                             reject(err); | ||||
|                                         } else { | ||||
|                                             node.topics[topic].m2 = value; | ||||
|                                             resolve(); | ||||
|                                         } | ||||
|                                     }); | ||||
|                                 }); | ||||
|                             } | ||||
|                         } | ||||
|                         delete node.topics[topic]; | ||||
|                         node.status({}); | ||||
|                         node.send(msg2); | ||||
|                         promise.then(() => { | ||||
|                             if (node.op2type !== "nul") { | ||||
|                                 if (node.topics[topic] !== undefined) { | ||||
|                                     msg2 = RED.util.cloneMessage(msg); | ||||
|                                     msg2.payload = node.topics[topic].m2; | ||||
|                                 } | ||||
|                             } | ||||
|                             delete node.topics[topic]; | ||||
|                             node.status({}); | ||||
|                             node.send(msg2); | ||||
|                         }).catch(err => { | ||||
|                             node.error(err); | ||||
|                         }); | ||||
|                     }, node.duration); | ||||
|                 } | ||||
|                 else { | ||||
|                     if (node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); } | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|             return Promise.resolve(); | ||||
|         } | ||||
|         this.on("close", function() { | ||||
|             for (var t in node.topics) { | ||||
|                 /* istanbul ignore else  */ | ||||
|   | ||||
| @@ -455,24 +455,8 @@ RED.debug = (function() { | ||||
|             $('<span class="debug-message-name">'+name+'</span>').appendTo(metaRow); | ||||
|         } | ||||
|  | ||||
|         if ((format === 'number') && (payload === "NaN")) { | ||||
|             payload = Number.NaN; | ||||
|         } else if (format === 'Object' || /^array/.test(format) || format === 'boolean' || format === 'number' ) { | ||||
|             payload = JSON.parse(payload); | ||||
|         } else if (/error/i.test(format)) { | ||||
|             payload = JSON.parse(payload); | ||||
|             payload = (payload.name?payload.name+": ":"")+payload.message; | ||||
|         } else if (format === 'null') { | ||||
|             payload = null; | ||||
|         } else if (format === 'undefined') { | ||||
|             payload = undefined; | ||||
|         } else if (/^buffer/.test(format)) { | ||||
|             var buffer = payload; | ||||
|             payload = []; | ||||
|             for (var c = 0; c < buffer.length; c += 2) { | ||||
|                 payload.push(parseInt(buffer.substr(c, 2), 16)); | ||||
|             } | ||||
|         } | ||||
|         payload = RED.utils.decodeObject(payload,format); | ||||
|  | ||||
|         var el = $('<span class="debug-message-payload"></span>').appendTo(msg); | ||||
|         var path = o.property||''; | ||||
|         var debugMessage = RED.utils.createObjectElement(payload, { | ||||
|   | ||||
| @@ -24,8 +24,12 @@ module.exports = function(RED) { | ||||
|                 try { | ||||
|                     fs.statSync("/usr/lib/python2.7/dist-packages/RPi/GPIO"); // test on Hypriot | ||||
|                 } catch(err) { | ||||
|                     RED.log.warn("rpi-gpio : "+RED._("rpi-gpio.errors.libnotfound")); | ||||
|                     allOK = false; | ||||
|                     try { | ||||
|                         fs.statSync("/usr/local/lib/python2.7/dist-packages/RPi/GPIO"); // installed with pip | ||||
|                     } catch(err) { | ||||
|                         RED.log.warn("rpi-gpio : "+RED._("rpi-gpio.errors.libnotfound")); | ||||
|                         allOK = false; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -21,11 +21,12 @@ import os | ||||
| import subprocess | ||||
| from time import sleep | ||||
|  | ||||
| bounce = 25; | ||||
| try: | ||||
|     raw_input          # Python 2 | ||||
| except NameError: | ||||
|     raw_input = input  # Python 3 | ||||
|  | ||||
| if sys.version_info >= (3,0): | ||||
|     print("Sorry - currently only configured to work with python 2.x") | ||||
|     sys.exit(1) | ||||
| bounce = 25 | ||||
|  | ||||
| if len(sys.argv) > 2: | ||||
|     cmd = sys.argv[1].lower() | ||||
| @@ -34,7 +35,7 @@ if len(sys.argv) > 2: | ||||
|     GPIO.setwarnings(False) | ||||
|  | ||||
|     if cmd == "pwm": | ||||
|         #print "Initialised pin "+str(pin)+" to PWM" | ||||
|         #print("Initialised pin "+str(pin)+" to PWM") | ||||
|         try: | ||||
|             freq = int(sys.argv[3]) | ||||
|         except: | ||||
| @@ -54,10 +55,10 @@ if len(sys.argv) > 2: | ||||
|                 GPIO.cleanup(pin) | ||||
|                 sys.exit(0) | ||||
|             except Exception as ex: | ||||
|                 print "bad data: "+data | ||||
|                 print("bad data: "+data) | ||||
|  | ||||
|     elif cmd == "buzz": | ||||
|         #print "Initialised pin "+str(pin)+" to Buzz" | ||||
|         #print("Initialised pin "+str(pin)+" to Buzz") | ||||
|         GPIO.setup(pin,GPIO.OUT) | ||||
|         p = GPIO.PWM(pin, 100) | ||||
|         p.stop() | ||||
| @@ -76,10 +77,10 @@ if len(sys.argv) > 2: | ||||
|                 GPIO.cleanup(pin) | ||||
|                 sys.exit(0) | ||||
|             except Exception as ex: | ||||
|                 print "bad data: "+data | ||||
|                 print("bad data: "+data) | ||||
|  | ||||
|     elif cmd == "out": | ||||
|         #print "Initialised pin "+str(pin)+" to OUT" | ||||
|         #print("Initialised pin "+str(pin)+" to OUT") | ||||
|         GPIO.setup(pin,GPIO.OUT) | ||||
|         if len(sys.argv) == 4: | ||||
|             GPIO.output(pin,int(sys.argv[3])) | ||||
| @@ -103,11 +104,11 @@ if len(sys.argv) > 2: | ||||
|             GPIO.output(pin,data) | ||||
|  | ||||
|     elif cmd == "in": | ||||
|         #print "Initialised pin "+str(pin)+" to IN" | ||||
|         #print("Initialised pin "+str(pin)+" to IN") | ||||
|         bounce = float(sys.argv[4]) | ||||
|         def handle_callback(chan): | ||||
|             sleep(bounce/1000.0) | ||||
|             print GPIO.input(chan) | ||||
|             print(GPIO.input(chan)) | ||||
|  | ||||
|         if sys.argv[3].lower() == "up": | ||||
|             GPIO.setup(pin,GPIO.IN,GPIO.PUD_UP) | ||||
| @@ -116,7 +117,7 @@ if len(sys.argv) > 2: | ||||
|         else: | ||||
|             GPIO.setup(pin,GPIO.IN) | ||||
|  | ||||
|         print GPIO.input(pin) | ||||
|         print(GPIO.input(pin)) | ||||
|         GPIO.add_event_detect(pin, GPIO.BOTH, callback=handle_callback, bouncetime=int(bounce)) | ||||
|  | ||||
|         while True: | ||||
| @@ -129,7 +130,7 @@ if len(sys.argv) > 2: | ||||
|                 sys.exit(0) | ||||
|  | ||||
|     elif cmd == "byte": | ||||
|         #print "Initialised BYTE mode - "+str(pin)+ | ||||
|         #print("Initialised BYTE mode - "+str(pin)+) | ||||
|         list = [7,11,13,12,15,16,18,22] | ||||
|         GPIO.setup(list,GPIO.OUT) | ||||
|  | ||||
| @@ -152,7 +153,7 @@ if len(sys.argv) > 2: | ||||
|                 GPIO.output(list[bit], data & mask) | ||||
|  | ||||
|     elif cmd == "borg": | ||||
|         #print "Initialised BORG mode - "+str(pin)+ | ||||
|         #print("Initialised BORG mode - "+str(pin)+) | ||||
|         GPIO.setup(11,GPIO.OUT) | ||||
|         GPIO.setup(13,GPIO.OUT) | ||||
|         GPIO.setup(15,GPIO.OUT) | ||||
| @@ -190,7 +191,7 @@ if len(sys.argv) > 2: | ||||
|           button = ord( buf[0] ) & pin # mask out just the required button(s) | ||||
|           if button != oldbutt:  # only send if changed | ||||
|               oldbutt = button | ||||
|               print button | ||||
|               print(button) | ||||
|  | ||||
|         while True: | ||||
|             try: | ||||
| @@ -202,7 +203,7 @@ if len(sys.argv) > 2: | ||||
|     elif cmd == "kbd":  # catch keyboard button events | ||||
|         try: | ||||
|             while not os.path.isdir("/dev/input/by-path"): | ||||
|                 time.sleep(10) | ||||
|                 sleep(10) | ||||
|             infile = subprocess.check_output("ls /dev/input/by-path/ | grep -m 1 'kbd'", shell=True).strip() | ||||
|             infile_path = "/dev/input/by-path/" + infile | ||||
|             EVENT_SIZE = struct.calcsize('llHHI') | ||||
| @@ -215,7 +216,7 @@ if len(sys.argv) > 2: | ||||
|                     # type,code,value | ||||
|                     print("%u,%u" % (code, value)) | ||||
|                 event = file.read(EVENT_SIZE) | ||||
|             print "0,0" | ||||
|             print("0,0") | ||||
|             file.close() | ||||
|             sys.exit(0) | ||||
|         except: | ||||
| @@ -225,14 +226,14 @@ if len(sys.argv) > 2: | ||||
| elif len(sys.argv) > 1: | ||||
|     cmd = sys.argv[1].lower() | ||||
|     if cmd == "rev": | ||||
|         print GPIO.RPI_REVISION | ||||
|         print(GPIO.RPI_REVISION) | ||||
|     elif cmd == "ver": | ||||
|         print GPIO.VERSION | ||||
|         print(GPIO.VERSION) | ||||
|     elif cmd == "info": | ||||
|         print GPIO.RPI_INFO | ||||
|         print(GPIO.RPI_INFO) | ||||
|     else: | ||||
|         print "Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}" | ||||
|         print "  only ver (gpio version) and info (board information) accept no pin parameter." | ||||
|         print("Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}") | ||||
|         print("  only ver (gpio version) and info (board information) accept no pin parameter.") | ||||
|  | ||||
| else: | ||||
|     print "Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}" | ||||
|     print("Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}") | ||||
|   | ||||
| @@ -63,6 +63,11 @@ | ||||
|         <input type="checkbox" id="node-config-input-verifyservercert" style="display: inline-block; width: auto; vertical-align: top;"> | ||||
|         <label for="node-config-input-verifyservercert" style="width: calc(100% - 170px);" data-i18n="tls.label.verify-server-cert"></label> | ||||
|     </div> | ||||
|     <div class="form-row"> | ||||
|         <label style="width: 120px;" for="node-config-input-servername"><i class="fa fa-server"></i> <span data-i18n="tls.label.servername"></span></label> | ||||
|         <input style="width: calc(100% - 170px);" type="text" id="node-config-input-servername" data-i18n="[placeholder]tls.placeholder.servername"> | ||||
|     </div> | ||||
|     <hr> | ||||
|     <div class="form-row"> | ||||
|         <label style="width: 120px;" for="node-config-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label> | ||||
|         <input style="width: calc(100% - 170px);" type="text" id="node-config-input-name" data-i18n="[placeholder]common.label.name"> | ||||
| @@ -96,6 +101,7 @@ | ||||
|             certname: {value:""}, | ||||
|             keyname: {value:""}, | ||||
|             caname: {value:""}, | ||||
|             servername: {value:""}, | ||||
|             verifyservercert: {value: true} | ||||
|         }, | ||||
|         credentials: { | ||||
|   | ||||
| @@ -25,6 +25,7 @@ module.exports = function(RED) { | ||||
|         var certPath = n.cert.trim(); | ||||
|         var keyPath = n.key.trim(); | ||||
|         var caPath = n.ca.trim(); | ||||
|         this.servername = (n.servername||"").trim(); | ||||
|  | ||||
|         if ((certPath.length > 0) || (keyPath.length > 0)) { | ||||
|  | ||||
| @@ -102,6 +103,9 @@ module.exports = function(RED) { | ||||
|             if (this.credentials && this.credentials.passphrase) { | ||||
|                 opts.passphrase = this.credentials.passphrase; | ||||
|             } | ||||
|             if (this.servername) { | ||||
|                 opts.servername = this.servername; | ||||
|             } | ||||
|             opts.rejectUnauthorized = this.verifyservercert; | ||||
|         } | ||||
|         return opts; | ||||
|   | ||||
| @@ -19,11 +19,26 @@ module.exports = function(RED) { | ||||
|     var mqtt = require("mqtt"); | ||||
|     var util = require("util"); | ||||
|     var isUtf8 = require('is-utf8'); | ||||
|     var HttpsProxyAgent = require('https-proxy-agent'); | ||||
|     var url = require('url'); | ||||
|  | ||||
|     function matchTopic(ts,t) { | ||||
|         if (ts == "#") { | ||||
|             return true; | ||||
|         } | ||||
|         /* The following allows shared subscriptions (as in MQTT v5) | ||||
|            http://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html#_Toc514345522 | ||||
|             | ||||
|            4.8.2 describes shares like: | ||||
|            $share/{ShareName}/{filter} | ||||
|            $share is a literal string that marks the Topic Filter as being a Shared Subscription Topic Filter. | ||||
|            {ShareName} is a character string that does not include "/", "+" or "#" | ||||
|            {filter} The remainder of the string has the same syntax and semantics as a Topic Filter in a non-shared subscription. Refer to section 4.7. | ||||
|         */ | ||||
|         else if(ts.startsWith("$share")){ | ||||
|             ts = ts.replace(/^\$share\/[^#+/]+\/(.*)/g,"$1"); | ||||
|              | ||||
|         } | ||||
|         var re = new RegExp("^"+ts.replace(/([\[\]\?\(\)\\\\$\^\*\.|])/g,"\\$1").replace(/\+/g,"[^/]+").replace(/\/#$/,"(\/.*)?")+"$"); | ||||
|         return re.test(t); | ||||
|     } | ||||
| @@ -96,12 +111,29 @@ module.exports = function(RED) { | ||||
|         if (typeof this.cleansession === 'undefined') { | ||||
|             this.cleansession = true; | ||||
|         } | ||||
|         var prox; | ||||
|         if (process.env.http_proxy != null) { prox = process.env.http_proxy; } | ||||
|         if (process.env.HTTP_PROXY != null) { prox = process.env.HTTP_PROXY; } | ||||
|  | ||||
|         // Create the URL to pass in to the MQTT.js library | ||||
|         if (this.brokerurl === "") { | ||||
|             // if the broker may be ws:// or wss:// or even tcp:// | ||||
|             if (this.broker.indexOf("://") > -1) { | ||||
|                 this.brokerurl = this.broker; | ||||
|                 // Only for ws or wss, check if proxy env var for additional configuration | ||||
|                 if (this.brokerurl.indexOf("wss://") > -1 || this.brokerurl.indexOf("ws://") > -1 ) | ||||
|                 // check if proxy is set in env | ||||
|                     if (prox) { | ||||
|                         var parsedUrl = url.parse(this.brokerurl); | ||||
|                         var proxyOpts = url.parse(prox); | ||||
|                         // true for wss | ||||
|                         proxyOpts.secureEndpoint = parsedUrl.protocol ? parsedUrl.protocol === 'wss:' : true; | ||||
|                         // Set Agent for wsOption in MQTT | ||||
|                         var agent = new HttpsProxyAgent(proxyOpts); | ||||
|                         this.options.wsOptions = { | ||||
|                             agent: agent | ||||
|                         } | ||||
|                     } | ||||
|             } else { | ||||
|                 // construct the std mqtt:// url | ||||
|                 if (this.usetls) { | ||||
| @@ -435,4 +467,4 @@ module.exports = function(RED) { | ||||
|         } | ||||
|     } | ||||
|     RED.nodes.registerType("mqtt out",MQTTOutNode); | ||||
| }; | ||||
| }; | ||||
| @@ -212,11 +212,11 @@ module.exports = function(RED) { | ||||
|         if (this.serverConfig) { | ||||
|             this.serverConfig.registerInputNode(this); | ||||
|             // TODO: nls | ||||
|             this.serverConfig.on('opened', function(n) { node.status({fill:"green",shape:"dot",text:"connected "+n}); }); | ||||
|             this.serverConfig.on('erro', function() { node.status({fill:"red",shape:"ring",text:"error"}); }); | ||||
|             this.serverConfig.on('opened', function(n) { node.status({fill:"green",shape:"dot",text:RED._("websocket.status.connected",{count:n})}); }); | ||||
|             this.serverConfig.on('erro', function() { node.status({fill:"red",shape:"ring",text:"common.status.error"}); }); | ||||
|             this.serverConfig.on('closed', function(n) { | ||||
|                 if (n > 0) { node.status({fill:"green",shape:"dot",text:"connected "+n}); } | ||||
|                 else { node.status({fill:"red",shape:"ring",text:"disconnected"}); } | ||||
|                 if (n > 0) { node.status({fill:"green",shape:"dot",text:RED._("websocket.status.connected",{count:n})}); } | ||||
|                 else { node.status({fill:"red",shape:"ring",text:"common.status.disconnected"}); } | ||||
|             }); | ||||
|         } else { | ||||
|             this.error(RED._("websocket.errors.missing-conf")); | ||||
| @@ -240,11 +240,11 @@ module.exports = function(RED) { | ||||
|         } | ||||
|         else { | ||||
|             // TODO: nls | ||||
|             this.serverConfig.on('opened', function(n) { node.status({fill:"green",shape:"dot",text:"connected "+n}); }); | ||||
|             this.serverConfig.on('erro', function() { node.status({fill:"red",shape:"ring",text:"error"}); }); | ||||
|             this.serverConfig.on('opened', function(n) { node.status({fill:"green",shape:"dot",text:RED._("websocket.status.connected",{count:n})}); }); | ||||
|             this.serverConfig.on('erro', function() { node.status({fill:"red",shape:"ring",text:"common.status.error"}); }); | ||||
|             this.serverConfig.on('closed', function(n) { | ||||
|                 if (n > 0) { node.status({fill:"green",shape:"dot",text:"connected "+n}); } | ||||
|                 else { node.status({fill:"red",shape:"ring",text:"disconnected"}); } | ||||
|                 if (n > 0) { node.status({fill:"green",shape:"dot",text:RED._("websocket.status.connected",{count:n})}); } | ||||
|                 else { node.status({fill:"red",shape:"ring",text:"common.status.disconnected"}); } | ||||
|             }); | ||||
|         } | ||||
|         this.on("input", function(msg) { | ||||
|   | ||||
| @@ -18,10 +18,34 @@ module.exports = function(RED) { | ||||
|     "use strict"; | ||||
|     var reconnectTime = RED.settings.socketReconnectTime||10000; | ||||
|     var socketTimeout = RED.settings.socketTimeout||null; | ||||
|     const msgQueueSize = RED.settings.tcpMsgQueueSize || 1000; | ||||
|     const Denque = require('denque'); | ||||
|     var net = require('net'); | ||||
|  | ||||
|     var connectionPool = {}; | ||||
|  | ||||
|     /** | ||||
|      * Enqueue `item` in `queue` | ||||
|      * @param {Denque} queue - Queue | ||||
|      * @param {*} item - Item to enqueue | ||||
|      * @private | ||||
|      * @returns {Denque} `queue` | ||||
|      */ | ||||
|     const enqueue = (queue, item) => { | ||||
|         // drop msgs from front of queue if size is going to be exceeded | ||||
|         if (queue.size() === msgQueueSize) { queue.shift(); } | ||||
|         queue.push(item); | ||||
|         return queue; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Shifts item off front of queue | ||||
|      * @param {Deque} queue - Queue | ||||
|      * @private | ||||
|      * @returns {*} Item previously at front of queue | ||||
|      */ | ||||
|     const dequeue = queue => queue.shift(); | ||||
|  | ||||
|     function TcpIn(n) { | ||||
|         RED.nodes.createNode(this,n); | ||||
|         this.host = n.host; | ||||
| @@ -435,11 +459,15 @@ module.exports = function(RED) { | ||||
|             // the clients object will have: | ||||
|             // clients[id].client, clients[id].msg, clients[id].timeout | ||||
|             var connection_id = host + ":" + port; | ||||
|             clients[connection_id] = clients[connection_id] || {}; | ||||
|             clients[connection_id].msg = msg; | ||||
|             clients[connection_id].connected = clients[connection_id].connected || false; | ||||
|             clients[connection_id] = clients[connection_id] || { | ||||
|                 msgQueue: new Denque(), | ||||
|                 connected: false, | ||||
|                 connecting: false | ||||
|             }; | ||||
|             enqueue(clients[connection_id].msgQueue, msg); | ||||
|             clients[connection_id].lastMsg = msg; | ||||
|  | ||||
|             if (!clients[connection_id].connected) { | ||||
|             if (!clients[connection_id].connecting && !clients[connection_id].connected) { | ||||
|                 var buf; | ||||
|                 if (this.out == "count") { | ||||
|                     if (this.splitc === 0) { buf = Buffer.alloc(1); } | ||||
| @@ -451,14 +479,19 @@ module.exports = function(RED) { | ||||
|                 if (socketTimeout !== null) { clients[connection_id].client.setTimeout(socketTimeout);} | ||||
|  | ||||
|                 if (host && port) { | ||||
|                     clients[connection_id].connecting = true; | ||||
|                     clients[connection_id].client.connect(port, host, function() { | ||||
|                         //node.log(RED._("tcpin.errors.client-connected")); | ||||
|                         node.status({fill:"green",shape:"dot",text:"common.status.connected"}); | ||||
|                         if (clients[connection_id] && clients[connection_id].client) { | ||||
|                             clients[connection_id].connected = true; | ||||
|                             clients[connection_id].client.write(clients[connection_id].msg.payload); | ||||
|                             clients[connection_id].connecting = false; | ||||
|                             let msg; | ||||
|                             while (msg = dequeue(clients[connection_id].msgQueue)) { | ||||
|                                 clients[connection_id].client.write(msg.payload); | ||||
|                             } | ||||
|                             if (node.out === "time" && node.splitc < 0) { | ||||
|                                 clients[connection_id].connected = false; | ||||
|                                 clients[connection_id].connected = clients[connection_id].connecting = false; | ||||
|                                 clients[connection_id].client.end(); | ||||
|                                 delete clients[connection_id]; | ||||
|                                 node.status({}); | ||||
| @@ -473,9 +506,9 @@ module.exports = function(RED) { | ||||
|                 clients[connection_id].client.on('data', function(data) { | ||||
|                     if (node.out === "sit") { // if we are staying connected just send the buffer | ||||
|                         if (clients[connection_id]) { | ||||
|                             if (!clients[connection_id].hasOwnProperty("msg")) { clients[connection_id].msg = {}; } | ||||
|                             clients[connection_id].msg.payload = data; | ||||
|                             node.send(RED.util.cloneMessage(clients[connection_id].msg)); | ||||
|                             const msg = clients[connection_id].lastMsg || {}; | ||||
|                             msg.payload = data; | ||||
|                             node.send(RED.util.cloneMessage(msg)); | ||||
|                         } | ||||
|                     } | ||||
|                     // else if (node.splitc === 0) { | ||||
| @@ -495,9 +528,10 @@ module.exports = function(RED) { | ||||
|                                         clients[connection_id].timeout = setTimeout(function () { | ||||
|                                             if (clients[connection_id]) { | ||||
|                                                 clients[connection_id].timeout = null; | ||||
|                                                 clients[connection_id].msg.payload = Buffer.alloc(i+1); | ||||
|                                                 buf.copy(clients[connection_id].msg.payload,0,0,i+1); | ||||
|                                                 node.send(clients[connection_id].msg); | ||||
|                                                 const msg = clients[connection_id].lastMsg || {}; | ||||
|                                                 msg.payload = Buffer.alloc(i+1); | ||||
|                                                 buf.copy(msg.payload,0,0,i+1); | ||||
|                                                 node.send(msg); | ||||
|                                                 if (clients[connection_id].client) { | ||||
|                                                     node.status({}); | ||||
|                                                     clients[connection_id].client.destroy(); | ||||
| @@ -516,9 +550,10 @@ module.exports = function(RED) { | ||||
|                                 i += 1; | ||||
|                                 if ( i >= node.splitc) { | ||||
|                                     if (clients[connection_id]) { | ||||
|                                         clients[connection_id].msg.payload = Buffer.alloc(i); | ||||
|                                         buf.copy(clients[connection_id].msg.payload,0,0,i); | ||||
|                                         node.send(clients[connection_id].msg); | ||||
|                                         const msg = clients[connection_id].lastMsg || {}; | ||||
|                                         msg.payload = Buffer.alloc(i); | ||||
|                                         buf.copy(msg.payload,0,0,i); | ||||
|                                         node.send(msg); | ||||
|                                         if (clients[connection_id].client) { | ||||
|                                             node.status({}); | ||||
|                                             clients[connection_id].client.destroy(); | ||||
| @@ -534,9 +569,10 @@ module.exports = function(RED) { | ||||
|                                 i += 1; | ||||
|                                 if (data[j] == node.splitc) { | ||||
|                                     if (clients[connection_id]) { | ||||
|                                         clients[connection_id].msg.payload = Buffer.alloc(i); | ||||
|                                         buf.copy(clients[connection_id].msg.payload,0,0,i); | ||||
|                                         node.send(clients[connection_id].msg); | ||||
|                                         const msg = clients[connection_id].lastMsg || {}; | ||||
|                                         msg.payload = Buffer.alloc(i); | ||||
|                                         buf.copy(msg.payload,0,0,i); | ||||
|                                         node.send(msg); | ||||
|                                         if (clients[connection_id].client) { | ||||
|                                             node.status({}); | ||||
|                                             clients[connection_id].client.destroy(); | ||||
| @@ -554,7 +590,7 @@ module.exports = function(RED) { | ||||
|                     //console.log("END"); | ||||
|                     node.status({fill:"grey",shape:"ring",text:"common.status.disconnected"}); | ||||
|                     if (clients[connection_id] && clients[connection_id].client) { | ||||
|                         clients[connection_id].connected = false; | ||||
|                         clients[connection_id].connected = clients[connection_id].connecting = false; | ||||
|                         clients[connection_id].client = null; | ||||
|                     } | ||||
|                 }); | ||||
| @@ -562,7 +598,7 @@ module.exports = function(RED) { | ||||
|                 clients[connection_id].client.on('close', function() { | ||||
|                     //console.log("CLOSE"); | ||||
|                     if (clients[connection_id]) { | ||||
|                         clients[connection_id].connected = false; | ||||
|                         clients[connection_id].connected = clients[connection_id].connecting = false; | ||||
|                     } | ||||
|  | ||||
|                     var anyConnected = false; | ||||
| @@ -592,21 +628,23 @@ module.exports = function(RED) { | ||||
|                 clients[connection_id].client.on('timeout',function() { | ||||
|                     //console.log("TIMEOUT"); | ||||
|                     if (clients[connection_id]) { | ||||
|                         clients[connection_id].connected = false; | ||||
|                         clients[connection_id].connected = clients[connection_id].connecting = false; | ||||
|                         node.status({fill:"grey",shape:"dot",text:"tcpin.errors.connect-timeout"}); | ||||
|                         //node.warn(RED._("tcpin.errors.connect-timeout")); | ||||
|                         if (clients[connection_id].client) { | ||||
|                             clients[connection_id].connecting = true; | ||||
|                             clients[connection_id].client.connect(port, host, function() { | ||||
|                                 clients[connection_id].connected = true; | ||||
|                                 clients[connection_id].connecting = false; | ||||
|                                 node.status({fill:"green",shape:"dot",text:"common.status.connected"}); | ||||
|                             }); | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|             else { | ||||
|             else if (!clients[connection_id].connecting && clients[connection_id].connected) { | ||||
|                 if (clients[connection_id] && clients[connection_id].client) { | ||||
|                     clients[connection_id].client.write(clients[connection_id].msg.payload); | ||||
|                     clients[connection_id].client.write(dequeue(clients[connection_id].msgQueue).payload); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|   | ||||
| @@ -63,7 +63,7 @@ module.exports = function(RED) { | ||||
|             udpInputPortsInUse[this.port] = server; | ||||
|         } | ||||
|         else { | ||||
|             node.warn(RED._("udp.errors.alreadyused",{port:node.port})); | ||||
|             node.log(RED._("udp.errors.alreadyused",{port:node.port})); | ||||
|             server = udpInputPortsInUse[this.port];  // re-use existing | ||||
|         } | ||||
|  | ||||
| @@ -172,8 +172,7 @@ module.exports = function(RED) { | ||||
|         if (process.version.indexOf("v0.10") === 0) { opts = node.ipv; } | ||||
|  | ||||
|         var sock; | ||||
|         var p = this.port; | ||||
|         if (node.multicast != "false") { p = this.outport||"0"; } | ||||
|         var p = this.outport || this.port || "0"; | ||||
|         if (udpInputPortsInUse[p]) { | ||||
|             sock = udpInputPortsInUse[p]; | ||||
|             node.log(RED._("udp.status.re-use",{outport:node.outport,host:node.addr,port:node.port})); | ||||
|   | ||||
| @@ -153,13 +153,15 @@ | ||||
|             "key": "Private Key", | ||||
|             "passphrase": "Passphrase", | ||||
|             "ca": "CA Certificate", | ||||
|             "verify-server-cert":"Verify server certificate" | ||||
|             "verify-server-cert":"Verify server certificate", | ||||
|             "servername": "Server Name" | ||||
|         }, | ||||
|         "placeholder": { | ||||
|             "cert":"path to certificate (PEM format)", | ||||
|             "key":"path to private key (PEM format)", | ||||
|             "ca":"path to CA certificate (PEM format)", | ||||
|             "passphrase":"private key passphrase (optional)" | ||||
|             "passphrase":"private key passphrase (optional)", | ||||
|             "servername":"for use with SNI" | ||||
|         }, | ||||
|         "error": { | ||||
|             "missing-file": "No certificate/key file provided" | ||||
| @@ -420,6 +422,10 @@ | ||||
|             "url1": "URL should use ws:// or wss:// scheme and point to an existing websocket listener.", | ||||
|             "url2": "By default, <code>payload</code> will contain the data to be sent over, or received from a websocket. The client can be configured to send or receive the entire message object as a JSON formatted string." | ||||
|         }, | ||||
|         "status": { | ||||
|             "connected": "connected __count__", | ||||
|             "connected_plural": "connected __count__" | ||||
|         }, | ||||
|         "errors": { | ||||
|             "connect-error": "An error occured on the ws connection: ", | ||||
|             "send-error": "An error occurred while sending: ", | ||||
| @@ -575,6 +581,8 @@ | ||||
|             "null":"is null", | ||||
|             "nnull":"is not null", | ||||
|             "istype":"is of type", | ||||
|             "empty":"is empty", | ||||
|             "nempty":"is not empty", | ||||
|             "head":"head", | ||||
|             "tail":"tail", | ||||
|             "index":"index between", | ||||
| @@ -697,7 +705,9 @@ | ||||
|         "errors": { | ||||
|             "dropped-object": "Ignored non-object payload", | ||||
|             "dropped": "Ignored unsupported payload type", | ||||
|             "dropped-error": "Failed to convert payload" | ||||
|             "dropped-error": "Failed to convert payload", | ||||
|             "schema-error": "JSON Schema error", | ||||
|             "schema-error-compile": "JSON Schema error: failed to compile schema" | ||||
|         }, | ||||
|         "label": { | ||||
|             "o2j": "Object to JSON options", | ||||
| @@ -924,8 +934,8 @@ | ||||
|         "ascending" : "ascending", | ||||
|         "descending" : "descending", | ||||
|         "as-number" : "as number", | ||||
|         "invalid-exp" : "invalid JSONata expression in sort node", | ||||
|         "too-many" : "too many pending messages in sort node", | ||||
|         "invalid-exp" : "Invalid JSONata expression in sort node: __message__", | ||||
|         "too-many" : "Too many pending messages in sort node", | ||||
|         "clear" : "clear pending message in sort node" | ||||
|     }, | ||||
|     "batch" : { | ||||
|   | ||||
| @@ -26,7 +26,7 @@ | ||||
|         <dt class="optional">kill <span class="property-type">文字列</span></dt> | ||||
|         <dd>execノードのプロセスに対して送るシグナルの種別を指定します</dd> | ||||
|         <dt class="optional">pid <span class="property-type">数値|文字列</span></dt> | ||||
|         <dd>シグナル送信対象のexecノードのプロセスID</dd> | ||||
|         <dd>シグナル送信対象のexecノードのプロセスIDを指定します</dd> | ||||
|     </dl> | ||||
|  | ||||
|     <h3>出力</h3> | ||||
| @@ -60,13 +60,12 @@ | ||||
|     </ol> | ||||
|     <h3>詳細</h3> | ||||
|     <p>デフォルトでは、<code>exec</code>システムコールを用いてコマンドを呼び出してその完了を待ち、出力を返します。例えば、コマンドの実行が成功した場合には、<code>{ code: 0 }</code>と言う返却値を返します。</p> | ||||
|     <p><code>spawn</code>を使ってコマンドを実行し、 | ||||
| 標準出力および標準エラー出力へ出力を返すようにすることもできます。この場合、通常1行毎に値を返します。コマンドの実行が完了すると、3番目の端子にオブジェクトを出力します。例えば、コマンドの実行が成功した場合には、<code>{ code: 0 }</code>と言う返却値を返します。</p> | ||||
|     <p><code>spawn</code>を使ってコマンドを実行し、標準出力および標準エラー出力へ出力を返すようにすることもできます。この場合、通常1行毎に値を返します。コマンドの実行が完了すると、3番目の端子にオブジェクトを出力します。例えば、コマンドの実行が成功した場合には、<code>{ code: 0 }</code>という返却値を返します。</p> | ||||
|     <p>エラー発生時には、3番目の端子の<code>msg.payload</code>に<code>message</code>、<code>signal</code>など付加情報を返します。</p> | ||||
|     <p>実行対象のコマンドはノード設定で定義します。<code>msg.payload</code>や追加引数をコマンドに追加することもできます。</p> | ||||
|     <p>コマンドもしくはパラメータが空白を含む場合には、引用符で囲みます。- <code>"This is a single parameter"</code></p> | ||||
|     <p>コマンドもしくはパラメータが空白を含む場合には、引用符で囲みます。- <code>"これは一つのパラメータです"</code></p> | ||||
|     <p>返却する<code>payload</code>は通常<i>文字列</i>ですが、UTF8文字以外が存在すると<i>バッファ</i>となります。</p> | ||||
|     <p>ノードが実行中の場合、ステータスアイコンとPIDを表示します。この状態変化は<code>status</code>ノードで検知できます。</p> | ||||
|     <p>ノードが実行中の場合、ステータスアイコンとPIDを表示します。この状態変化は<code>Status</code>ノードで検知できます。</p> | ||||
|     <h4>プロセスの停止</h4> | ||||
|     <p><code>msg.kill</code>を受信すると、実行中のプロセスを停止することができます。<code>msg.kill</code>には送出するシグナルの種別を指定します。例えば、<code>SIGINT</code>、<code>SIGQUIT</code>、<code>SIGHUP</code>などです。空の文字列を指定した場合には、<code>SIGTERM</code>を指定したものとみなします。</p> | ||||
|     <p>ノードが1つ以上のプロセスを実行している場合、<code>msg.pid</code>に停止対象のPIDを指定しなければなりません。</p> | ||||
|   | ||||
| @@ -18,7 +18,7 @@ | ||||
|     <p>受信メッセージに対して処理を行うJavaScriptコード(関数の本体)を定義します。</p> | ||||
|     <p>入力メッセージは<code>msg</code>という名称のJavaScriptオブジェクトで受け渡されます。</p> | ||||
|     <p><code>msg</code>オブジェクトは<code>msg.payload</code>プロパティにメッセージ本体を保持するのが慣例です。</p> | ||||
|     <p>通常、コードはメッセージオブジェクト(もしくは複数のメッセージオブジェクト)を返却します。何も返却しない場合には、フロー実行を停止します。</p> | ||||
|     <p>通常、コードはメッセージオブジェクト(もしくは複数のメッセージオブジェクト)を返却します。後続フローの実行を停止したい場合は、オブジェクトを返却しなくてもかまいません。</p> | ||||
|     <h3>詳細</h3> | ||||
|     <p>コードの書き方の詳細については、<a target="_blank" href="http://nodered.org/docs/writing-functions.html">オンラインドキュメント</a>を参照してください。</p> | ||||
|     <h4>メッセージの送信</h4> | ||||
| @@ -40,4 +40,10 @@ | ||||
|     </p> | ||||
|     <p>catchノードを用いてエラー処理が可能です。catchノードで処理させるためには、<code>msg</code>を<code>node.error</code>の第二引数として渡します:</p> | ||||
|     <pre>node.error("エラー",msg);</pre> | ||||
|     <h4>ノード情報の参照</h4> | ||||
|     <p>コード中ではノードのIDおよび名前を以下のプロパティで参照できます:</p> | ||||
|     <ul> | ||||
|         <li><code>node.id</code> - ノードのID</li> | ||||
|         <li><code>node.name</code> - ノードの名称</li> | ||||
|     </ul> | ||||
| </script> | ||||
|   | ||||
| @@ -63,6 +63,8 @@ | ||||
|     <p>ノードにクライアントIDを設定しておらずセッションの初期化を設定している場合、ランダムなクライアントIDを生成します。クライアントIDを設定する場合、接続先のブローカで一意となるようにしてください。</p> | ||||
|     <h4>Birthメッセージ</h4> | ||||
|     <p>接続を確立した際に、設定したトピックに対して発行するメッセージ</p> | ||||
|     <h4>Closeメッセージ</h4> | ||||
|     <p>接続が正常に終了する前に、ノードの再デプロイまたはシャットダウンした場合に、設定したトピックに対して発行するメッセージ</p> | ||||
|     <h4>Willメッセージ</h4> | ||||
|     <p>予期せず接続が切断された場合にブローカが発行するメッセージ</p> | ||||
|     <h4>WebSocket</h4> | ||||
|   | ||||
| @@ -30,7 +30,9 @@ | ||||
|         <dt class="optional">payload</dt> | ||||
|         <dd>リクエストボディとして送るデータ</dd> | ||||
|         <dt class="optional">rejectUnauthorized</dt> | ||||
|         <dd><code>true</code>をセットすると、自己署名証明書を使用するhttpsサイトへのリクエストを許可します。</dd> | ||||
|         <dd><code>false</code>をセットすると、自己署名証明書を使用するhttpsサイトへのリクエストを許可します。</dd> | ||||
|         <dt class="optional">followRedirects</dt> | ||||
|         <dd><code>false</code>をセットすると、リダイレクトを行いません。デフォルトは<code>true</code>です。</dd> | ||||
|     </dl> | ||||
|     <h3>出力</h3> | ||||
|     <dl class="message-properties"> | ||||
|   | ||||
| @@ -29,7 +29,11 @@ | ||||
|         <li><b>その他</b> - これより前のルールにマッチするものがなかった場合に適用</li> | ||||
|     </ol> | ||||
|  | ||||
|     <h3>注釈</h3> | ||||
|     <p><code>is true/false</code>と<code>is null</code>のルールは、型に対して厳密な比較を行います。型変換した上での比較はしません。</p> | ||||
|     <p><code>is empty</code>のルールは、長さ0の文字列・配列・バッファ、またはプロパティを持たないオブジェクトを出力します。<code>null</code>や<code>undefined</code>は出力しません。</p> | ||||
|  | ||||
|     <h3>メッセージ列の扱い</h3> | ||||
|     <p>switchノードは入力メッセージの列に関する情報を保持する<code>msg.parts</code>をデフォルトでは変更しません。</p> | ||||
|     <p>「<b>メッセージ列の補正</b>」オプションを指定すると、マッチした各ルールに対して新しいメッセージ列を生成します。このモードでは、switchノードは新たなメッセージ列を送信する前に、入力メッセージ列全体を内部に蓄積します。<code>nodeMessageBufferMaxLength</code>を設定すると、蓄積するメッセージ数を制限できます。</p> | ||||
|     <p>「<b>メッセージ列の補正</b>」オプションを指定すると、マッチした各ルールに対して新しいメッセージ列を生成します。このモードでは、switchノードは新たなメッセージ列を送信する前に、入力メッセージ列全体を内部に蓄積します。<b>settings.js</b>の<code>nodeMessageBufferMaxLength</code>を設定すると、蓄積するメッセージ数を制限できます。</p> | ||||
| </script> | ||||
|   | ||||
| @@ -30,5 +30,5 @@ | ||||
|         </dd> | ||||
|     </dl> | ||||
|     <h4>メッセージの蓄積</h4> | ||||
|     <p>このノードの処理ではメッセージ列の処理のためメッセージを内部に蓄積します。<code>nodeMessageBufferMaxLength</code>を指定することで蓄積するメッセージの最大値を制限することができます。</p> | ||||
|     <p>このノードの処理ではメッセージ列の処理のためメッセージを内部に蓄積します。<b>settings.js</b>の<code>nodeMessageBufferMaxLength</code>を指定することで蓄積するメッセージの最大値を制限することができます。</p> | ||||
| </script> | ||||
|   | ||||
| @@ -153,19 +153,23 @@ | ||||
|             "key": "秘密鍵", | ||||
|             "passphrase": "パスフレーズ", | ||||
|             "ca": "CA証明書", | ||||
|             "verify-server-cert": "サーバ証明書を確認" | ||||
|             "verify-server-cert": "サーバ証明書を確認", | ||||
|             "servername": "サーバ名" | ||||
|         }, | ||||
|         "placeholder": { | ||||
|             "cert": "証明書(PEM形式)のパス", | ||||
|             "key": "秘密鍵(PEM形式)のパス", | ||||
|             "ca": "CA証明書(PEM形式)のパス", | ||||
|             "passphrase":"秘密鍵のパスフレーズ (任意)" | ||||
|             "passphrase": "秘密鍵のパスフレーズ (任意)", | ||||
|             "servername": "SNIで使用" | ||||
|         }, | ||||
|         "error": { | ||||
|             "missing-file": "証明書と秘密鍵のファイルが設定されていません" | ||||
|         } | ||||
|     }, | ||||
|     "exec": { | ||||
|         "exec": "exec", | ||||
|         "spawn": "spawn", | ||||
|         "label": { | ||||
|             "command": "コマンド", | ||||
|             "append": "引数", | ||||
| @@ -184,6 +188,7 @@ | ||||
|         "oldrc": "旧型式の出力を使用(互換モード)" | ||||
|     }, | ||||
|     "function": { | ||||
|         "function": "", | ||||
|         "label": { | ||||
|             "function": "コード", | ||||
|             "outputs": "出力数" | ||||
| @@ -195,6 +200,7 @@ | ||||
|         "tip": "コードの記述方法はノードの「情報」を参照してください。" | ||||
|     }, | ||||
|     "template": { | ||||
|         "template": "template", | ||||
|         "label": { | ||||
|             "template": "テンプレート", | ||||
|             "property": "設定先", | ||||
| @@ -301,6 +307,7 @@ | ||||
|         } | ||||
|     }, | ||||
|     "comment": { | ||||
|         "comment": "comment", | ||||
|         "label": { | ||||
|             "title": "タイトル", | ||||
|             "body": "本文" | ||||
| @@ -318,6 +325,7 @@ | ||||
|             "broker": "サーバ", | ||||
|             "example": "例) localhost", | ||||
|             "qos": "QoS", | ||||
|             "retain": "保持", | ||||
|             "clientid": "クライアント", | ||||
|             "port": "ポート", | ||||
|             "keepalive": "キープアライブ時間", | ||||
| @@ -327,10 +335,10 @@ | ||||
|             "verify-server-cert": "サーバの証明書を確認", | ||||
|             "compatmode": "旧MQTT 3.1のサポート" | ||||
|         }, | ||||
|         "sections-label":{ | ||||
|         "sections-label": { | ||||
|             "birth-message": "接続時の送信メッセージ(Birthメッセージ)", | ||||
|             "will-message":"予期しない切断時の送信メッセージ(Willメッセージ)", | ||||
|             "close-message":"切断前の送信メッセージ(Closeメッセージ)" | ||||
|             "will-message": "予期しない切断時の送信メッセージ(Willメッセージ)", | ||||
|             "close-message": "切断前の送信メッセージ(Closeメッセージ)" | ||||
|         }, | ||||
|         "tabs-label": { | ||||
|             "connection": "接続", | ||||
| @@ -414,6 +422,10 @@ | ||||
|             "url1": "URLには ws:// または wss:// スキーマを使用して、存在するwebsocketリスナを設定してください。", | ||||
|             "url2": "標準では <code>payload</code> がwebsocketから送信、受信されるデータを持ちます。クライアントはJSON形式の文字列としてメッセージ全体を送信、受信するよう設定できます。" | ||||
|         }, | ||||
|         "status": { | ||||
|             "connected": "接続数 __count__", | ||||
|             "connected_plural": "接続数 __count__" | ||||
|         }, | ||||
|         "errors": { | ||||
|             "connect-error": "ws接続でエラーが発生しました: ", | ||||
|             "send-error": "送信中にエラーが発生しました: ", | ||||
| @@ -421,6 +433,7 @@ | ||||
|         } | ||||
|     }, | ||||
|     "watch": { | ||||
|         "watch": "watch", | ||||
|         "label": { | ||||
|             "files": "ファイル", | ||||
|             "recursive": "サブディレクトリを再帰的に監視" | ||||
| @@ -543,15 +556,15 @@ | ||||
|             "port-notset": "udp: ポートが設定されていません", | ||||
|             "port-invalid": "udp: ポート番号が不正です", | ||||
|             "alreadyused": "udp: 既に__port__番ポートが使用されています", | ||||
|             "ifnotfound": "udp: インターフェイス __iface__ がありません", | ||||
|             "alreadyused": "udp: 既にポートが使用されています" | ||||
|             "ifnotfound": "udp: インターフェイス __iface__ がありません" | ||||
|         } | ||||
|     }, | ||||
|     "switch": { | ||||
|         "switch": "switch", | ||||
|         "label": { | ||||
|             "property": "プロパティ", | ||||
|             "rule": "条件", | ||||
|             "repair" :  "メッセージ列の補正" | ||||
|             "repair": "メッセージ列の補正" | ||||
|         }, | ||||
|         "and": "~", | ||||
|         "checkall": "全ての条件を適用", | ||||
| @@ -565,15 +578,18 @@ | ||||
|             "false": "is false", | ||||
|             "null": "is null", | ||||
|             "nnull": "is not null", | ||||
|             "head":"head", | ||||
|             "tail":"tail", | ||||
|             "index":"index between", | ||||
|             "exp":"JSONata式", | ||||
|             "istype": "is of type", | ||||
|             "empty": "is empty", | ||||
|             "nempty": "is not empty", | ||||
|             "head": "head", | ||||
|             "tail": "tail", | ||||
|             "index": "index between", | ||||
|             "exp": "JSONata式", | ||||
|             "else": "その他" | ||||
|         }, | ||||
|         "errors": { | ||||
|             "invalid-expr": "不正な表現: __error__", | ||||
|             "too-many" : "switchノード内で保持しているメッセージが多すぎます" | ||||
|             "too-many": "switchノード内で保持しているメッセージが多すぎます" | ||||
|         } | ||||
|     }, | ||||
|     "change": { | ||||
| @@ -603,6 +619,7 @@ | ||||
|         } | ||||
|     }, | ||||
|     "range": { | ||||
|         "range": "range", | ||||
|         "label": { | ||||
|             "action": "動作", | ||||
|             "inputrange": "入力値の範囲", | ||||
| @@ -670,7 +687,7 @@ | ||||
|         "label": { | ||||
|             "select": "抽出する要素", | ||||
|             "output": "出力", | ||||
| 	    "in": "対象:" | ||||
|             "in": "対象:" | ||||
|         }, | ||||
|         "output": { | ||||
|             "html": "要素内のHTML", | ||||
| @@ -686,7 +703,9 @@ | ||||
|         "errors": { | ||||
|             "dropped-object": "オブジェクト形式でないペイロードを無視しました", | ||||
|             "dropped": "対応していない形式のペイロードを無視しました", | ||||
|             "dropped-error": "ペイロードの変換処理が失敗しました" | ||||
|             "dropped-error": "ペイロードの変換処理が失敗しました", | ||||
|             "schema-error": "JSONスキーマエラー", | ||||
|             "schema-error-compile": "JSONスキーマエラー: スキーマのコンパイルが失敗しました" | ||||
|         }, | ||||
|         "label": { | ||||
|             "o2j": "オブジェクトからJSONへ変換", | ||||
| @@ -695,8 +714,8 @@ | ||||
|             "property": "プロパティ", | ||||
|             "actions": { | ||||
|                 "toggle": "JSON文字列とオブジェクト間の相互変換", | ||||
|                 "str":"常にJSON文字列に変換", | ||||
|                 "obj":"常にJavaScriptオブジェクトに変換" | ||||
|                 "str": "常にJSON文字列に変換", | ||||
|                 "obj": "常にJavaScriptオブジェクトに変換" | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
| @@ -791,6 +810,7 @@ | ||||
|         } | ||||
|     }, | ||||
|     "tail": { | ||||
|         "tail": "tail", | ||||
|         "label": { | ||||
|             "filename": "ファイル名", | ||||
|             "type": "ファイル形式", | ||||
| @@ -844,6 +864,7 @@ | ||||
|         "tip": "注釈: 「ファイル名」はフルパスを設定する必要があります。" | ||||
|     }, | ||||
|     "split": { | ||||
|         "split": "split", | ||||
|         "intro": "型に基づいて <code>msg.payload</code> を分割:", | ||||
|         "object": "<b>オブジェクト</b>", | ||||
|         "objectSend": "各key/valueペアのメッセージを送信", | ||||
| @@ -855,11 +876,12 @@ | ||||
|         "addname": " keyのコピー先" | ||||
|     }, | ||||
|     "join": { | ||||
|         "join": "join", | ||||
|         "mode": { | ||||
|             "mode": "動作", | ||||
|             "auto": "自動", | ||||
|             "merge":"列のマージ", | ||||
|             "reduce":"列の集約", | ||||
|             "merge": "列のマージ", | ||||
|             "reduce": "列の集約", | ||||
|             "custom": "手動" | ||||
|         }, | ||||
|         "combine": "結合", | ||||
| @@ -882,12 +904,12 @@ | ||||
|         "seconds": "秒", | ||||
|         "complete": "<code>msg.complete</code> プロパティが設定されたメッセージ受信後", | ||||
|         "tip": "このモードでは、本ノードが <i>split</i> ノードと組となるか、 <code>msg.parts</code> プロパティが設定されたメッセージを受け取ることが前提となります。", | ||||
|         "too-many" : "joinノード内部で保持しているメッセージが多すぎます", | ||||
|         "too-many": "joinノード内部で保持しているメッセージが多すぎます", | ||||
|         "merge": { | ||||
|             "topics-label":"対象トピック", | ||||
|             "topics":"トピック", | ||||
|             "topic" : "トピック", | ||||
|             "on-change":"新規トピックを受け取るとメッセージを送信する" | ||||
|             "topics-label": "対象トピック", | ||||
|             "topics": "トピック", | ||||
|             "topic": "トピック", | ||||
|             "on-change": "新規トピックを受け取るとメッセージを送信する" | ||||
|         }, | ||||
|         "reduce": { | ||||
|             "exp": "集約式", | ||||
| @@ -900,43 +922,45 @@ | ||||
|             "invalid-expr": "JSONata式が不正: __error__" | ||||
|         } | ||||
|     }, | ||||
|     "sort" : { | ||||
|         "target" : "対象", | ||||
|         "seq" : "メッセージ列", | ||||
|         "key" : "キー", | ||||
|         "elem" : "要素の値", | ||||
|         "order" : "順序", | ||||
|         "ascending" : "昇順", | ||||
|         "descending" : "降順", | ||||
|         "as-number" : "数値として比較", | ||||
|         "invalid-exp" : "sortノードで不正なJSONata式が指定されました", | ||||
|         "too-many" : "sortノードの未処理メッセージの数が許容数を超えました", | ||||
|         "clear" : "sortノードの未処理メッセージを破棄しました" | ||||
|     "sort": { | ||||
|         "sort": "sort", | ||||
|         "target": "対象", | ||||
|         "seq": "メッセージ列", | ||||
|         "key": "キー", | ||||
|         "elem": "要素の値", | ||||
|         "order": "順序", | ||||
|         "ascending": "昇順", | ||||
|         "descending": "降順", | ||||
|         "as-number": "数値として比較", | ||||
|         "invalid-exp": "sortノードで不正なJSONata式が指定されました", | ||||
|         "too-many": "sortノードの未処理メッセージの数が許容数を超えました", | ||||
|         "clear": "sortノードの未処理メッセージを破棄しました" | ||||
|     }, | ||||
|     "batch" : { | ||||
|     "batch": { | ||||
|         "batch": "batch", | ||||
|         "mode": { | ||||
|             "label" : "モード", | ||||
|             "num-msgs" : "メッセージ数でグループ化", | ||||
|             "interval" : "時間間隔でグループ化", | ||||
|             "concat" : "列の結合" | ||||
|             "label": "モード", | ||||
|             "num-msgs": "メッセージ数でグループ化", | ||||
|             "interval": "時間間隔でグループ化", | ||||
|             "concat": "列の結合" | ||||
|         }, | ||||
|         "count": { | ||||
|             "label" : "メッセージ数", | ||||
|             "overlap" : "オーバラップ", | ||||
|             "count" : "数", | ||||
|             "invalid" : "メッセージ数とオーバラップ数が不正" | ||||
|             "label": "メッセージ数", | ||||
|             "overlap": "オーバラップ", | ||||
|             "count": "数", | ||||
|             "invalid": "メッセージ数とオーバラップ数が不正" | ||||
|         }, | ||||
|         "interval": { | ||||
|             "label" : "時間間隔", | ||||
|             "seconds" : "秒", | ||||
|             "empty" : "メッセージを受信しない場合、空のメッセージを送信" | ||||
|             "label": "時間間隔", | ||||
|             "seconds": "秒", | ||||
|             "empty": "メッセージを受信しない場合、空のメッセージを送信" | ||||
|         }, | ||||
|         "concat": { | ||||
|             "topics-label": "トピック", | ||||
|             "topic" : "トピック" | ||||
|             "topic": "トピック" | ||||
|         }, | ||||
|         "too-many" : "batchノード内で保持しているメッセージが多すぎます", | ||||
|         "unexpected" : "想定外のモード", | ||||
|         "no-parts" : "メッセージにpartsプロパティがありません" | ||||
|         "too-many": "batchノード内で保持しているメッセージが多すぎます", | ||||
|         "unexpected": "想定外のモード", | ||||
|         "no-parts": "メッセージにpartsプロパティがありません" | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -20,6 +20,8 @@ | ||||
|     <dl class="message-properties"> | ||||
|         <dt>payload<span class="property-type">オブジェクト | 文字列</span></dt> | ||||
|         <dd>JavaScriptオブジェクトもしくはJSON文字列</dd> | ||||
|         <dt>schema<span class="property-type">オブジェクト</span></dt> | ||||
|         <dd>JSONの検証に利用するJSONスキーマ。設定されていない場合は検証を行いません。</dd> | ||||
|     </dl> | ||||
|     <h3>出力</h3> | ||||
|     <dl class="message-properties"> | ||||
| @@ -30,9 +32,12 @@ | ||||
|                 <li>入力がJavaScriptオブジェクトの場合、JSON文字列に変換します。JSON文字列は整形することも可能です。</li> | ||||
|             </ul> | ||||
|         </dd> | ||||
|         <dt>schemaError<span class="property-type">配列</span></dt> | ||||
|         <dd>JSONの検証でエラーが発生した場合、Catchノードを利用し、エラーを配列として<code>schemaError</code>プロパティから取得することができます。</dd> | ||||
|     </dl> | ||||
|     <h3>詳細</h3> | ||||
|     <p>デフォルトの変換対象は<code>msg.payload</code>ですが、他のメッセージプロパティを変換対象とすることも可能です。</p> | ||||
|     <p>双方向の変換を自動選択するのではなく、特定の変換のみ行うように設定できます。この機能は、例えば、<code>HTTP In</code>ノードに対するリクエストがcontent-typeを正しく設定していない場合であっても、JSONノードによる変換結果がJavaScriptオブジェクトであることを保証するために利用します。</p> | ||||
|     <p>JSON文字列への変換が指定されている場合、受信した文字列に対してさらなるチェックは行いません。すなわち、文字列がJSONとして正しいかどうかの検査や、整形オプションを指定していたとしても整形処理を実施しません。</p> | ||||
|     <p>JSONスキーマの詳細については、<a href="http://json-schema.org/latest/json-schema-validation.html">こちら</a>を参照してください。</p> | ||||
| </script> | ||||
|   | ||||
| @@ -21,6 +21,8 @@ | ||||
|         <dt class="optional">filename <span class="property-type">文字列</span></dt> | ||||
|         <dd>対象ファイル名をノードに設定していない場合、このプロパティでファイルを指定できます</dd> | ||||
|     </dl> | ||||
|     <h3>出力</h3> | ||||
|     <p>書き込みの完了時、入力メッセージを出力端子に送出します。</p> | ||||
|     <h3>詳細</h3> | ||||
|     <p>入力メッセージのペイロードをファイルの最後に追記します。改行(\n)を各データの最後に追加することもできます。</p> | ||||
|     <p><code>msg.filename</code>を使う場合、書き込みを行う毎にファイルをクローズします。より良い性能を得るためにはファイル名をノードに設定してください。</p> | ||||
|   | ||||
| @@ -60,6 +60,12 @@ | ||||
|         <li>An <b>Otherwise</b> rule can be used to match if none of the preceeding | ||||
|             rules have matched.</li> | ||||
|     </ol> | ||||
|     <h4>Notes</h4> | ||||
|     <p>The <code>is true/false</code> and <code>is null</code> rules perform strict | ||||
|        comparisons against those types. They do not convert between types.</p> | ||||
|     <p>The <code>is empty</code> rule passes for Strings, Arrays and Buffers that have | ||||
|        a length of 0, or Objects that have no properties. It does not pass for <code>null</code> | ||||
|        or <code>undefined</code> values.</p> | ||||
|     <h4>Handling message sequences</h4> | ||||
|     <p>By default, the node does not modify the <code>msg.parts</code> property of messages | ||||
|        that are part of a sequence.</p> | ||||
| @@ -86,6 +92,8 @@ | ||||
|         {v:"null",t:"switch.rules.null",kind:'V'}, | ||||
|         {v:"nnull",t:"switch.rules.nnull",kind:'V'}, | ||||
|         {v:"istype",t:"switch.rules.istype",kind:'V'}, | ||||
|         {v:"empty",t:"switch.rules.empty",kind:'V'}, | ||||
|         {v:"nempty",t:"switch.rules.nempty",kind:'V'}, | ||||
|         {v:"head",t:"switch.rules.head",kind:'S'}, | ||||
|         {v:"index",t:"switch.rules.index",kind:'S'}, | ||||
|         {v:"tail",t:"switch.rules.tail",kind:'S'}, | ||||
| @@ -99,11 +107,17 @@ | ||||
|         } | ||||
|         return v; | ||||
|     } | ||||
|     function prop2name(key) { | ||||
|         var result = RED.utils.parseContextKey(key); | ||||
|         return result.key; | ||||
|     } | ||||
|     function getValueLabel(t,v) { | ||||
|         if (t === 'str') { | ||||
|             return '"'+clipValueLength(v)+'"'; | ||||
|         } else if (t === 'msg' || t==='flow' || t==='global') { | ||||
|         } else if (t === 'msg') { | ||||
|             return t+"."+clipValueLength(v); | ||||
|         } else if (t === 'flow' || t === 'global') { | ||||
|             return t+"."+clipValueLength(prop2name(v)); | ||||
|         } | ||||
|         return clipValueLength(v); | ||||
|     } | ||||
| @@ -133,7 +147,7 @@ | ||||
|                 } | ||||
|                 if ((rule.t === 'btwn') || (rule.t === 'index')) { | ||||
|                     label += " "+getValueLabel(rule.vt,rule.v)+" & "+getValueLabel(rule.v2t,rule.v2); | ||||
|                 } else if (rule.t !== 'true' && rule.t !== 'false' && rule.t !== 'null' && rule.t !== 'nnull' && rule.t !== 'else' ) { | ||||
|                 } else if (rule.t !== 'true' && rule.t !== 'false' && rule.t !== 'null' && rule.t !== 'nnull' && rule.t !== 'empty' && rule.t !== 'nempty' && rule.t !== 'else' ) { | ||||
|                     label += " "+getValueLabel(rule.vt,rule.v); | ||||
|                 } | ||||
|                 return label; | ||||
| @@ -185,7 +199,7 @@ | ||||
|                 } else if (type === "istype") { | ||||
|                     typeField.typedInput("width",(newWidth-selectWidth-70)); | ||||
|                 } else { | ||||
|                     if (type === "true" || type === "false" || type === "null" || type === "nnull" || type === "else") { | ||||
|                     if (type === "true" || type === "false" || type === "null" || type === "nnull" || type === "empty" || type === "nempty" || type === "else") { | ||||
|                         // valueField.hide(); | ||||
|                     } else { | ||||
|                         valueField.typedInput("width",(newWidth-selectWidth-70)); | ||||
| @@ -281,7 +295,7 @@ | ||||
|                             numValueField.typedInput('hide'); | ||||
|                             typeValueField.typedInput('hide'); | ||||
|                             valueField.typedInput('hide'); | ||||
|                             if (type === "true" || type === "false" || type === "null" || type === "nnull" || type === "else") { | ||||
|                             if (type === "true" || type === "false" || type === "null" || type === "nnull" || type === "empty" || type === "nempty" || type === "else") { | ||||
|                                 valueField.typedInput('hide'); | ||||
|                                 typeValueField.typedInput('hide'); | ||||
|                             } | ||||
| @@ -382,7 +396,7 @@ | ||||
|                 var rule = $(this); | ||||
|                 var type = rule.find("select").val(); | ||||
|                 var r = {t:type}; | ||||
|                 if (!(type === "true" || type === "false" || type === "null" || type === "nnull" || type === "else")) { | ||||
|                 if (!(type === "true" || type === "false" || type === "null" || type === "nnull" || type === "empty" || type === "nempty" || type === "else")) { | ||||
|                     if ((type === "btwn") || (type === "index")) { | ||||
|                         r.v = rule.find(".node-input-rule-btwn-value").typedInput('value'); | ||||
|                         r.vt = rule.find(".node-input-rule-btwn-value").typedInput('type'); | ||||
|   | ||||
| @@ -31,6 +31,23 @@ module.exports = function(RED) { | ||||
|         'false': function(a) { return a === false; }, | ||||
|         'null': function(a) { return (typeof a == "undefined" || a === null); }, | ||||
|         'nnull': function(a) { return (typeof a != "undefined" && a !== null); }, | ||||
|         'empty': function(a) { | ||||
|             if (typeof a === 'string' || Array.isArray(a) || Buffer.isBuffer(a)) { | ||||
|                 return a.length === 0; | ||||
|             } else if (typeof a === 'object' && a !== null) { | ||||
|                 return Object.keys(a).length === 0; | ||||
|             } | ||||
|             return false; | ||||
|         }, | ||||
|         'nempty': function(a) { | ||||
|             if (typeof a === 'string' || Array.isArray(a) || Buffer.isBuffer(a)) { | ||||
|                 return a.length !== 0; | ||||
|             } else if (typeof a === 'object' && a !== null) { | ||||
|                 return Object.keys(a).length !== 0; | ||||
|             } | ||||
|             return false; | ||||
|         }, | ||||
|  | ||||
|         'istype': function(a, b) { | ||||
|             if (b === "array") { return Array.isArray(a); } | ||||
|             else if (b === "buffer") { return Buffer.isBuffer(a); } | ||||
| @@ -59,21 +76,254 @@ module.exports = function(RED) { | ||||
|         'else': function(a) { return a === true; } | ||||
|     }; | ||||
|  | ||||
|     var _max_kept_msgs_count = undefined; | ||||
|     var _maxKeptCount; | ||||
|  | ||||
|     function max_kept_msgs_count(node) { | ||||
|         if (_max_kept_msgs_count === undefined) { | ||||
|     function getMaxKeptCount() { | ||||
|         if (_maxKeptCount === undefined) { | ||||
|             var name = "nodeMessageBufferMaxLength"; | ||||
|             if (RED.settings.hasOwnProperty(name)) { | ||||
|                 _max_kept_msgs_count = RED.settings[name]; | ||||
|                 _maxKeptCount = RED.settings[name]; | ||||
|             } | ||||
|             else { | ||||
|                 _max_kept_msgs_count = 0; | ||||
|                 _maxKeptCount = 0; | ||||
|             } | ||||
|         } | ||||
|         return _max_kept_msgs_count; | ||||
|         return _maxKeptCount; | ||||
|     } | ||||
|  | ||||
|     function getProperty(node,msg) { | ||||
|         if (node.useAsyncRules) { | ||||
|             return new Promise((resolve,reject) => { | ||||
|                 if (node.propertyType === 'jsonata') { | ||||
|                     RED.util.evaluateJSONataExpression(node.property,msg,(err,value) => { | ||||
|                         if (err) { | ||||
|                             reject(RED._("switch.errors.invalid-expr",{error:err.message})); | ||||
|                         } else { | ||||
|                             resolve(value); | ||||
|                         } | ||||
|                     }); | ||||
|                 } else { | ||||
|                     RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg,(err,value) => { | ||||
|                         if (err) { | ||||
|                             resolve(undefined); | ||||
|                         } else { | ||||
|                             resolve(value); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|         } else { | ||||
|             if (node.propertyType === 'jsonata') { | ||||
|                 try { | ||||
|                     return RED.util.evaluateJSONataExpression(node.property,msg); | ||||
|                 } catch(err) { | ||||
|                     throw new Error(RED._("switch.errors.invalid-expr",{error:err.message})) | ||||
|                 } | ||||
|             } else { | ||||
|                 try { | ||||
|                     return RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg); | ||||
|                 } catch(err) { | ||||
|                     return undefined; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function getV1(node,msg,rule,hasParts) { | ||||
|         if (node.useAsyncRules) { | ||||
|             return new Promise( (resolve,reject) => { | ||||
|                 if (rule.vt === 'prev') { | ||||
|                     resolve(node.previousValue); | ||||
|                 } else if (rule.vt === 'jsonata') { | ||||
|                     var exp = rule.v; | ||||
|                     if (rule.t === 'jsonata_exp') { | ||||
|                         if (hasParts) { | ||||
|                             exp.assign("I", msg.parts.index); | ||||
|                             exp.assign("N", msg.parts.count); | ||||
|                         } | ||||
|                     } | ||||
|                     RED.util.evaluateJSONataExpression(exp,msg,(err,value) => { | ||||
|                         if (err) { | ||||
|                             reject(RED._("switch.errors.invalid-expr",{error:err.message})); | ||||
|                         } else { | ||||
|                             resolve(value); | ||||
|                         } | ||||
|                     }); | ||||
|                 } else if (rule.vt === 'json') { | ||||
|                     resolve("json"); // TODO: ?! invalid case | ||||
|                 } else if (rule.vt === 'null') { | ||||
|                     resolve("null"); | ||||
|                 } else { | ||||
|                     RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg, function(err,value) { | ||||
|                         if (err) { | ||||
|                             resolve(undefined); | ||||
|                         } else { | ||||
|                             resolve(value); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|         } else { | ||||
|             if (rule.vt === 'prev') { | ||||
|                 return node.previousValue; | ||||
|             } else if (rule.vt === 'jsonata') { | ||||
|                 var exp = rule.v; | ||||
|                 if (rule.t === 'jsonata_exp') { | ||||
|                     if (hasParts) { | ||||
|                         exp.assign("I", msg.parts.index); | ||||
|                         exp.assign("N", msg.parts.count); | ||||
|                     } | ||||
|                 } | ||||
|                 try { | ||||
|                     return RED.util.evaluateJSONataExpression(exp,msg); | ||||
|                 } catch(err) { | ||||
|                     throw new Error(RED._("switch.errors.invalid-expr",{error:err.message})) | ||||
|                 } | ||||
|             } else if (rule.vt === 'json') { | ||||
|                 return "json"; // TODO: ?! invalid case | ||||
|             } else if (rule.vt === 'null') { | ||||
|                 return "null"; | ||||
|             } else { | ||||
|                 try { | ||||
|                     return RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg); | ||||
|                 } catch(err) { | ||||
|                     return undefined; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function getV2(node,msg,rule) { | ||||
|         if (node.useAsyncRules) { | ||||
|             return new Promise((resolve,reject) => { | ||||
|                 var v2 = rule.v2; | ||||
|                 if (rule.v2t === 'prev') { | ||||
|                     resolve(node.previousValue); | ||||
|                 } else if (rule.v2t === 'jsonata') { | ||||
|                     RED.util.evaluateJSONataExpression(rule.v2,msg,(err,value) => { | ||||
|                         if (err) { | ||||
|                             reject(RED._("switch.errors.invalid-expr",{error:err.message})); | ||||
|                         } else { | ||||
|                             resolve(value); | ||||
|                         } | ||||
|                     }); | ||||
|                 } else if (typeof v2 !== 'undefined') { | ||||
|                     RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg, function(err,value) { | ||||
|                         if (err) { | ||||
|                             resolve(undefined); | ||||
|                         } else { | ||||
|                             resolve(value); | ||||
|                         } | ||||
|                     }); | ||||
|                 } else { | ||||
|                     resolve(v2); | ||||
|                 } | ||||
|             }) | ||||
|         } else { | ||||
|             var v2 = rule.v2; | ||||
|             if (rule.v2t === 'prev') { | ||||
|                 return node.previousValue; | ||||
|             } else if (rule.v2t === 'jsonata') { | ||||
|                 try { | ||||
|                     return RED.util.evaluateJSONataExpression(rule.v2,msg); | ||||
|                 } catch(err) { | ||||
|                     throw new Error(RED._("switch.errors.invalid-expr",{error:err.message})) | ||||
|                 } | ||||
|             } else if (typeof v2 !== 'undefined') { | ||||
|                 try { | ||||
|                     return RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg); | ||||
|                 } catch(err) { | ||||
|                     return undefined; | ||||
|                 } | ||||
|             } else { | ||||
|                 return v2; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function applyRule(node, msg, property, state) { | ||||
|         if (node.useAsyncRules) { | ||||
|             return new Promise((resolve,reject) => { | ||||
|  | ||||
|                 var rule = node.rules[state.currentRule]; | ||||
|                 var v1,v2; | ||||
|  | ||||
|                 getV1(node,msg,rule,state.hasParts).then(value => { | ||||
|                     v1 = value; | ||||
|                 }).then(()=>getV2(node,msg,rule)).then(value => { | ||||
|                     v2 = value; | ||||
|                 }).then(() => { | ||||
|                     if (rule.t == "else") { | ||||
|                         property = state.elseflag; | ||||
|                         state.elseflag = true; | ||||
|                     } | ||||
|                     if (operators[rule.t](property,v1,v2,rule.case,msg.parts)) { | ||||
|                         state.onward.push(msg); | ||||
|                         state.elseflag = false; | ||||
|                         if (node.checkall == "false") { | ||||
|                             return resolve(false); | ||||
|                         } | ||||
|                     } else { | ||||
|                         state.onward.push(null); | ||||
|                     } | ||||
|                     resolve(state.currentRule < node.rules.length - 1); | ||||
|                 }); | ||||
|             }) | ||||
|         } else { | ||||
|             var rule = node.rules[state.currentRule]; | ||||
|             var v1 = getV1(node,msg,rule,state.hasParts); | ||||
|             var v2 = getV2(node,msg,rule); | ||||
|             if (rule.t == "else") { | ||||
|                 property = state.elseflag; | ||||
|                 state.elseflag = true; | ||||
|             } | ||||
|             if (operators[rule.t](property,v1,v2,rule.case,msg.parts)) { | ||||
|                 state.onward.push(msg); | ||||
|                 state.elseflag = false; | ||||
|                 if (node.checkall == "false") { | ||||
|                     return false; | ||||
|                 } | ||||
|             } else { | ||||
|                 state.onward.push(null); | ||||
|             } | ||||
|             return state.currentRule < node.rules.length - 1 | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function applyRules(node, msg, property,state) { | ||||
|         if (!state) { | ||||
|             state = { | ||||
|                 currentRule: 0, | ||||
|                 elseflag: true, | ||||
|                 onward: [], | ||||
|                 hasParts: msg.hasOwnProperty("parts") && | ||||
|                                 msg.parts.hasOwnProperty("id") && | ||||
|                                 msg.parts.hasOwnProperty("index") | ||||
|             } | ||||
|         } | ||||
|         if (node.useAsyncRules) { | ||||
|             return applyRule(node,msg,property,state).then(hasMore => { | ||||
|                 if (hasMore) { | ||||
|                     state.currentRule++; | ||||
|                     return applyRules(node,msg,property,state); | ||||
|                 } else { | ||||
|                     node.previousValue = property; | ||||
|                     return state.onward; | ||||
|                 } | ||||
|             }); | ||||
|         } else { | ||||
|             var hasMore = applyRule(node,msg,property,state); | ||||
|             if (hasMore) { | ||||
|                 state.currentRule++; | ||||
|                 return applyRules(node,msg,property,state); | ||||
|             } else { | ||||
|                 node.previousValue = property; | ||||
|                 return state.onward; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     function SwitchNode(n) { | ||||
|         RED.nodes.createNode(this, n); | ||||
|         this.rules = n.rules || []; | ||||
| @@ -94,10 +344,18 @@ module.exports = function(RED) { | ||||
|         var node = this; | ||||
|         var valid = true; | ||||
|         var repair = n.repair; | ||||
|         var needs_count = repair; | ||||
|         var needsCount = repair; | ||||
|         this.useAsyncRules = ( | ||||
|             this.propertyType === 'flow' || | ||||
|             this.propertyType === 'global' || ( | ||||
|                 this.propertyType === 'jsonata' && | ||||
|                 /\$(flow|global)Context/.test(this.property) | ||||
|             ) | ||||
|         ); | ||||
|  | ||||
|         for (var i=0; i<this.rules.length; i+=1) { | ||||
|             var rule = this.rules[i]; | ||||
|             needs_count = needs_count || ((rule.t === "tail") || (rule.t === "jsonata_exp")); | ||||
|             needsCount = needsCount || ((rule.t === "tail") || (rule.t === "jsonata_exp")); | ||||
|             if (!rule.vt) { | ||||
|                 if (!isNaN(Number(rule.v))) { | ||||
|                     rule.vt = 'num'; | ||||
| @@ -105,6 +363,13 @@ module.exports = function(RED) { | ||||
|                     rule.vt = 'str'; | ||||
|                 } | ||||
|             } | ||||
|             this.useAsyncRules = this.useAsyncRules || ( | ||||
|                 rule.vt === 'flow' || | ||||
|                 rule.vt === 'global' || ( | ||||
|                     rule.vt === 'jsonata' && | ||||
|                     /\$(flow|global)Context/.test(rule.v) | ||||
|                 ) | ||||
|             ); | ||||
|             if (rule.vt === 'num') { | ||||
|                 if (!isNaN(Number(rule.v))) { | ||||
|                     rule.v = Number(rule.v); | ||||
| @@ -117,6 +382,9 @@ module.exports = function(RED) { | ||||
|                     valid = false; | ||||
|                 } | ||||
|             } | ||||
|             if (rule.vt === 'flow' || rule.vt === 'global' || rule.vt === 'jsonata') { | ||||
|                 this.useAsyncRules = true; | ||||
|             } | ||||
|             if (typeof rule.v2 !== 'undefined') { | ||||
|                 if (!rule.v2t) { | ||||
|                     if (!isNaN(Number(rule.v2))) { | ||||
| @@ -125,6 +393,13 @@ module.exports = function(RED) { | ||||
|                         rule.v2t = 'str'; | ||||
|                     } | ||||
|                 } | ||||
|                 this.useAsyncRules = this.useAsyncRules || ( | ||||
|                     rule.v2t === 'flow' || | ||||
|                     rule.v2t === 'global' || ( | ||||
|                         rule.v2t === 'jsonata' && | ||||
|                         /\$(flow|global)Context/.test(rule.v2) | ||||
|                     ) | ||||
|                 ); | ||||
|                 if (rule.v2t === 'num') { | ||||
|                     rule.v2 = Number(rule.v2); | ||||
|                 } else if (rule.v2t === 'jsonata') { | ||||
| @@ -137,31 +412,30 @@ module.exports = function(RED) { | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (!valid) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         var pending_count = 0; | ||||
|         var pending_id = 0; | ||||
|         var pending_in = {}; | ||||
|         var pending_out = {}; | ||||
|         var pendingCount = 0; | ||||
|         var pendingId = 0; | ||||
|         var pendingIn = {}; | ||||
|         var pendingOut = {}; | ||||
|         var received = {}; | ||||
|  | ||||
|         function add2group_in(id, msg, parts) { | ||||
|             if (!(id in pending_in)) { | ||||
|                 pending_in[id] = { | ||||
|         function addMessageToGroup(id, msg, parts) { | ||||
|             if (!(id in pendingIn)) { | ||||
|                 pendingIn[id] = { | ||||
|                     count: undefined, | ||||
|                     msgs: [], | ||||
|                     seq_no: pending_id++ | ||||
|                     seq_no: pendingId++ | ||||
|                 }; | ||||
|             } | ||||
|             var group = pending_in[id]; | ||||
|             var group = pendingIn[id]; | ||||
|             group.msgs.push(msg); | ||||
|             pending_count++; | ||||
|             var max_msgs = max_kept_msgs_count(node); | ||||
|             if ((max_msgs > 0) && (pending_count > max_msgs)) { | ||||
|                 clear_pending(); | ||||
|             pendingCount++; | ||||
|             var max_msgs = getMaxKeptCount(); | ||||
|             if ((max_msgs > 0) && (pendingCount > max_msgs)) { | ||||
|                 clearPending(); | ||||
|                 node.error(RED._("switch.errors.too-many"), msg); | ||||
|             } | ||||
|             if (parts.hasOwnProperty("count")) { | ||||
| @@ -170,32 +444,29 @@ module.exports = function(RED) { | ||||
|             return group; | ||||
|         } | ||||
|  | ||||
|         function del_group_in(id, group) { | ||||
|             pending_count -= group.msgs.length; | ||||
|             delete pending_in[id]; | ||||
|         } | ||||
|  | ||||
|         function add2pending_in(msg) { | ||||
|         function addMessageToPending(msg) { | ||||
|             var parts = msg.parts; | ||||
|             if (parts.hasOwnProperty("id") && | ||||
|                 parts.hasOwnProperty("index")) { | ||||
|                 var group = add2group_in(parts.id, msg, parts); | ||||
|                 var msgs = group.msgs; | ||||
|                 var count = group.count; | ||||
|                 if (count === msgs.length) { | ||||
|                     for (var i = 0; i < msgs.length; i++) { | ||||
|                         var msg = msgs[i]; | ||||
|             // We've already checked the msg.parts has the require bits | ||||
|             var group = addMessageToGroup(parts.id, msg, parts); | ||||
|             var msgs = group.msgs; | ||||
|             var count = group.count; | ||||
|             if (count === msgs.length) { | ||||
|                 // We have a complete group - send the individual parts | ||||
|                 return msgs.reduce((promise, msg) => { | ||||
|                     return promise.then((result) => { | ||||
|                         msg.parts.count = count; | ||||
|                         process_msg(msg, false); | ||||
|                     } | ||||
|                     del_group_in(parts.id, group); | ||||
|                 } | ||||
|                 return true; | ||||
|                         return processMessage(msg, false); | ||||
|                     }) | ||||
|                 }, Promise.resolve()).then( () => { | ||||
|                     pendingCount -= group.msgs.length; | ||||
|                     delete pendingIn[parts.id]; | ||||
|                 }); | ||||
|             } | ||||
|             return false; | ||||
|             return Promise.resolve(); | ||||
|         } | ||||
|  | ||||
|         function send_group(onwards, port_count) { | ||||
|         function sendGroup(onwards, port_count) { | ||||
|             var counts = new Array(port_count).fill(0); | ||||
|             for (var i = 0; i < onwards.length; i++) { | ||||
|                 var onward = onwards[i]; | ||||
| @@ -230,141 +501,122 @@ module.exports = function(RED) { | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         function send2ports(onward, msg) { | ||||
|         function sendGroupMessages(onward, msg) { | ||||
|             var parts = msg.parts; | ||||
|             var gid = parts.id; | ||||
|             received[gid] = ((gid in received) ? received[gid] : 0) +1; | ||||
|             var send_ok = (received[gid] === parts.count); | ||||
|  | ||||
|             if (!(gid in pending_out)) { | ||||
|                 pending_out[gid] = { | ||||
|             if (!(gid in pendingOut)) { | ||||
|                 pendingOut[gid] = { | ||||
|                     onwards: [] | ||||
|                 }; | ||||
|             } | ||||
|             var group = pending_out[gid]; | ||||
|             var group = pendingOut[gid]; | ||||
|             var onwards = group.onwards; | ||||
|             onwards.push(onward); | ||||
|             pending_count++; | ||||
|             pendingCount++; | ||||
|             if (send_ok) { | ||||
|                 send_group(onwards, onward.length, msg); | ||||
|                 pending_count -= onward.length; | ||||
|                 delete pending_out[gid]; | ||||
|                 sendGroup(onwards, onward.length, msg); | ||||
|                 pendingCount -= onward.length; | ||||
|                 delete pendingOut[gid]; | ||||
|                 delete received[gid]; | ||||
|             } | ||||
|             var max_msgs = max_kept_msgs_count(node); | ||||
|             if ((max_msgs > 0) && (pending_count > max_msgs)) { | ||||
|                 clear_pending(); | ||||
|             var max_msgs = getMaxKeptCount(); | ||||
|             if ((max_msgs > 0) && (pendingCount > max_msgs)) { | ||||
|                 clearPending(); | ||||
|                 node.error(RED._("switch.errors.too-many"), msg); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         function msg_has_parts(msg) { | ||||
|             if (msg.hasOwnProperty("parts")) { | ||||
|                 var parts = msg.parts; | ||||
|                 return (parts.hasOwnProperty("id") && | ||||
|                         parts.hasOwnProperty("index")); | ||||
|             } | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         function process_msg(msg, check_parts) { | ||||
|             var has_parts = msg_has_parts(msg); | ||||
|             if (needs_count && check_parts && has_parts && | ||||
|                 add2pending_in(msg)) { | ||||
|                 return; | ||||
|  | ||||
|  | ||||
|  | ||||
|         function processMessage(msg, checkParts) { | ||||
|             var hasParts = msg.hasOwnProperty("parts") && | ||||
|                             msg.parts.hasOwnProperty("id") && | ||||
|                             msg.parts.hasOwnProperty("index"); | ||||
|  | ||||
|             if (needsCount && checkParts && hasParts) { | ||||
|                 return addMessageToPending(msg); | ||||
|             } | ||||
|             var onward = []; | ||||
|             try { | ||||
|                 var prop; | ||||
|                 if (node.propertyType === 'jsonata') { | ||||
|                     prop = RED.util.evaluateJSONataExpression(node.property,msg); | ||||
|                 } else { | ||||
|                     prop = RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg); | ||||
|                 } | ||||
|                 var elseflag = true; | ||||
|                 for (var i=0; i<node.rules.length; i+=1) { | ||||
|                     var rule = node.rules[i]; | ||||
|                     var test = prop; | ||||
|                     var v1,v2; | ||||
|                     if (rule.vt === 'prev') { | ||||
|                         v1 = node.previousValue; | ||||
|                     } else if (rule.vt === 'jsonata') { | ||||
|                         try { | ||||
|                             var exp = rule.v; | ||||
|                             if (rule.t === 'jsonata_exp') { | ||||
|                                 if (has_parts) { | ||||
|                                     exp.assign("I", msg.parts.index); | ||||
|                                     exp.assign("N", msg.parts.count); | ||||
|                                 } | ||||
|             if (node.useAsyncRules) { | ||||
|                 return getProperty(node,msg) | ||||
|                         .then(property => applyRules(node,msg,property)) | ||||
|                         .then(onward => { | ||||
|                             if (!repair || !hasParts) { | ||||
|                                 node.send(onward); | ||||
|                             } | ||||
|                             v1 = RED.util.evaluateJSONataExpression(exp,msg); | ||||
|                         } catch(err) { | ||||
|                             node.error(RED._("switch.errors.invalid-expr",{error:err.message})); | ||||
|                             return; | ||||
|                         } | ||||
|                     } else if (rule.vt === 'json') { | ||||
|                         v1 = "json"; | ||||
|                     } else if (rule.vt === 'null') { | ||||
|                         v1 = "null"; | ||||
|                             else { | ||||
|                                 sendGroupMessages(onward, msg); | ||||
|                             } | ||||
|                         }).catch(err => { | ||||
|                             node.warn(err); | ||||
|                         }); | ||||
|             } else { | ||||
|                 try { | ||||
|                     var property = getProperty(node,msg); | ||||
|                     var onward = applyRules(node,msg,property); | ||||
|                     if (!repair || !hasParts) { | ||||
|                         node.send(onward); | ||||
|                     } else { | ||||
|                         try { | ||||
|                             v1 = RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg); | ||||
|                         } catch(err) { | ||||
|                             v1 = undefined; | ||||
|                         } | ||||
|                     } | ||||
|                     v2 = rule.v2; | ||||
|                     if (rule.v2t === 'prev') { | ||||
|                         v2 = node.previousValue; | ||||
|                     } else if (rule.v2t === 'jsonata') { | ||||
|                         try { | ||||
|                             v2 = RED.util.evaluateJSONataExpression(rule.v2,msg); | ||||
|                         } catch(err) { | ||||
|                             node.error(RED._("switch.errors.invalid-expr",{error:err.message})); | ||||
|                             return; | ||||
|                         } | ||||
|                     } else if (typeof v2 !== 'undefined') { | ||||
|                         try { | ||||
|                             v2 = RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg); | ||||
|                         } catch(err) { | ||||
|                             v2 = undefined; | ||||
|                         } | ||||
|                     } | ||||
|                     if (rule.t == "else") { test = elseflag; elseflag = true; } | ||||
|                     if (operators[rule.t](test,v1,v2,rule.case,msg.parts)) { | ||||
|                         onward.push(msg); | ||||
|                         elseflag = false; | ||||
|                         if (node.checkall == "false") { break; } | ||||
|                     } else { | ||||
|                         onward.push(null); | ||||
|                         sendGroupMessages(onward, msg); | ||||
|                     } | ||||
|                 } catch(err) { | ||||
|                     node.warn(err); | ||||
|                 } | ||||
|                 node.previousValue = prop; | ||||
|                 if (!repair || !has_parts) { | ||||
|                     node.send(onward); | ||||
|                 } | ||||
|                 else { | ||||
|                     send2ports(onward, msg); | ||||
|                 } | ||||
|             } catch(err) { | ||||
|                 node.warn(err); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         function clear_pending() { | ||||
|             pending_count = 0; | ||||
|             pending_id = 0; | ||||
|             pending_in = {}; | ||||
|             pending_out = {}; | ||||
|         function clearPending() { | ||||
|             pendingCount = 0; | ||||
|             pendingId = 0; | ||||
|             pendingIn = {}; | ||||
|             pendingOut = {}; | ||||
|             received = {}; | ||||
|         } | ||||
|  | ||||
|         var pendingMessages = []; | ||||
|         var activeMessagePromise = null; | ||||
|         var processMessageQueue = function(msg) { | ||||
|             if (msg) { | ||||
|                 // A new message has arrived - add it to the message queue | ||||
|                 pendingMessages.push(msg); | ||||
|                 if (activeMessagePromise !== null) { | ||||
|                     // The node is currently processing a message, so do nothing | ||||
|                     // more with this message | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|             if (pendingMessages.length === 0) { | ||||
|                 // There are no more messages to process, clear the active flag | ||||
|                 // and return | ||||
|                 activeMessagePromise = null; | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // There are more messages to process. Get the next message and | ||||
|             // start processing it. Recurse back in to check for any more | ||||
|             var nextMsg = pendingMessages.shift(); | ||||
|             activeMessagePromise = processMessage(nextMsg,true) | ||||
|                 .then(processMessageQueue) | ||||
|                 .catch((err) => { | ||||
|                     node.error(err,nextMsg); | ||||
|                     return processMessageQueue(); | ||||
|                 }); | ||||
|         } | ||||
|  | ||||
|         this.on('input', function(msg) { | ||||
|             process_msg(msg, true); | ||||
|             if (node.useAsyncRules) { | ||||
|                 processMessageQueue(msg); | ||||
|             } else { | ||||
|                 processMessage(msg,true); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         this.on('close', function() { | ||||
|             clear_pending(); | ||||
|             clearPending(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -54,6 +54,10 @@ | ||||
|         outputs: 1, | ||||
|         icon: "swap.png", | ||||
|         label: function() { | ||||
|             function prop2name(type, key) { | ||||
|                 var result = RED.utils.parseContextKey(key); | ||||
|                 return type +"." +result.key; | ||||
|             } | ||||
|             if (this.name) { | ||||
|                 return this.name; | ||||
|             } | ||||
| @@ -70,13 +74,13 @@ | ||||
|             } else { | ||||
|                 if (this.rules.length == 1) { | ||||
|                     if (this.rules[0].t === "set") { | ||||
|                         return this._("change.label.set",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p}); | ||||
|                         return this._("change.label.set",{property:prop2name((this.rules[0].pt||"msg"), this.rules[0].p)}); | ||||
|                     } else if (this.rules[0].t === "change") { | ||||
|                         return this._("change.label.change",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p}); | ||||
|                         return this._("change.label.change",{property:prop2name((this.rules[0].pt||"msg"), this.rules[0].p)}); | ||||
|                     } else if (this.rules[0].t === "move") { | ||||
|                         return this._("change.label.move",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p}); | ||||
|                         return this._("change.label.move",{property:prop2name((this.rules[0].pt||"msg"), this.rules[0].p)}); | ||||
|                     } else { | ||||
|                         return this._("change.label.delete",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p}); | ||||
|                         return this._("change.label.delete",{property:prop2name((this.rules[0].pt||"msg"), this.rules[0].p)}); | ||||
|                     } | ||||
|                 } else { | ||||
|                     return this._("change.label.changeCount",{count:this.rules.length}); | ||||
|   | ||||
| @@ -98,44 +98,61 @@ module.exports = function(RED) { | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         function applyRule(msg,rule) { | ||||
|             try { | ||||
|                 var property = rule.p; | ||||
|                 var value = rule.to; | ||||
|                 if (rule.tot === 'json') { | ||||
|                     value = JSON.parse(rule.to); | ||||
|                 } else if (rule.tot === 'bin') { | ||||
|                     value = Buffer.from(JSON.parse(rule.to)) | ||||
|                 } | ||||
|                 var current; | ||||
|                 var fromValue; | ||||
|                 var fromType; | ||||
|                 var fromRE; | ||||
|                 if (rule.tot === "msg") { | ||||
|                     value = RED.util.getMessageProperty(msg,rule.to); | ||||
|                 } else if (rule.tot === 'flow') { | ||||
|                     value = node.context().flow.get(rule.to); | ||||
|                 } else if (rule.tot === 'global') { | ||||
|                     value = node.context().global.get(rule.to); | ||||
|                 } else if (rule.tot === 'date') { | ||||
|                     value = Date.now(); | ||||
|                 } else if (rule.tot === 'jsonata') { | ||||
|                     try{ | ||||
|                         value = RED.util.evaluateJSONataExpression(rule.to,msg); | ||||
|                     } catch(err) { | ||||
|                         node.error(RED._("change.errors.invalid-expr",{error:err.message})); | ||||
|                         return; | ||||
|                     } | ||||
|                 } | ||||
|                 if (rule.t === 'change') { | ||||
|                     if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') { | ||||
|                         if (rule.fromt === "msg") { | ||||
|                             fromValue = RED.util.getMessageProperty(msg,rule.from); | ||||
|                         } else if (rule.fromt === 'flow') { | ||||
|                             fromValue = node.context().flow.get(rule.from); | ||||
|                         } else if (rule.fromt === 'global') { | ||||
|                             fromValue = node.context().global.get(rule.from); | ||||
|         function getToValue(msg,rule) { | ||||
|             var value = rule.to; | ||||
|             if (rule.tot === 'json') { | ||||
|                 value = JSON.parse(rule.to); | ||||
|             } else if (rule.tot === 'bin') { | ||||
|                 value = Buffer.from(JSON.parse(rule.to)) | ||||
|             } | ||||
|             if (rule.tot === "msg") { | ||||
|                 value = RED.util.getMessageProperty(msg,rule.to); | ||||
|             } else if ((rule.tot === 'flow') || | ||||
|                        (rule.tot === 'global')) { | ||||
|                 return new Promise((resolve,reject) => { | ||||
|                     RED.util.evaluateNodeProperty(rule.to, rule.tot, node, msg, (err,value) => { | ||||
|                         if (err) { | ||||
|                             resolve(undefined); | ||||
|                         } else { | ||||
|                             resolve(value); | ||||
|                         } | ||||
|                     }); | ||||
|                 }); | ||||
|             } else if (rule.tot === 'date') { | ||||
|                 value = Date.now(); | ||||
|             } else if (rule.tot === 'jsonata') { | ||||
|                 return new Promise((resolve,reject) => { | ||||
|                     RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => { | ||||
|                         if (err) { | ||||
|                             reject(RED._("change.errors.invalid-expr",{error:err.message})) | ||||
|                         } else { | ||||
|                             resolve(value); | ||||
|                         } | ||||
|                     }); | ||||
|                 }); | ||||
|             } | ||||
|             return Promise.resolve(value); | ||||
|         } | ||||
|         function getFromValue(msg,rule) { | ||||
|             var fromValue; | ||||
|             var fromType; | ||||
|             var fromRE; | ||||
|             if (rule.t === 'change') { | ||||
|                 if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') { | ||||
|                     return new Promise((resolve,reject) => { | ||||
|                         if (rule.fromt === "msg") { | ||||
|                             resolve(RED.util.getMessageProperty(msg,rule.from)); | ||||
|                         } else if (rule.fromt === 'flow' || rule.fromt === 'global') { | ||||
|                             var contextKey = RED.util.parseContextStore(rule.from); | ||||
|                             node.context()[rule.fromt].get(contextKey.key, contextKey.store, (err,fromValue) => { | ||||
|                                 if (err) { | ||||
|                                     reject(err); | ||||
|                                 } else { | ||||
|                                     resolve(fromValue); | ||||
|                                 } | ||||
|                             }); | ||||
|                         } | ||||
|                     }).then(fromValue => { | ||||
|                         if (typeof fromValue === 'number' || fromValue instanceof Number) { | ||||
|                             fromType = 'num'; | ||||
|                         } else if (typeof fromValue === 'boolean') { | ||||
| @@ -149,108 +166,163 @@ module.exports = function(RED) { | ||||
|                             try { | ||||
|                                 fromRE = new RegExp(fromRE, "g"); | ||||
|                             } catch (e) { | ||||
|                                 valid = false; | ||||
|                                 node.error(RED._("change.errors.invalid-from",{error:e.message})); | ||||
|                                 return; | ||||
|                                 return Promise.reject(new Error(RED._("change.errors.invalid-from",{error:e.message}))); | ||||
|                             } | ||||
|                         } else { | ||||
|                             node.error(RED._("change.errors.invalid-from",{error:"unsupported type: "+(typeof fromValue)})); | ||||
|                             return | ||||
|                             return Promise.reject(new Error(RED._("change.errors.invalid-from",{error:"unsupported type: "+(typeof fromValue)}))); | ||||
|                         } | ||||
|                         return { | ||||
|                             fromType, | ||||
|                             fromValue, | ||||
|                             fromRE | ||||
|                         } | ||||
|                     }); | ||||
|                 } else { | ||||
|                     fromType = rule.fromt; | ||||
|                     fromValue = rule.from; | ||||
|                     fromRE = rule.fromRE; | ||||
|                 } | ||||
|             } | ||||
|             return Promise.resolve({ | ||||
|                 fromType, | ||||
|                 fromValue, | ||||
|                 fromRE | ||||
|             }); | ||||
|         } | ||||
|         function applyRule(msg,rule) { | ||||
|                 var property = rule.p; | ||||
|                 var current; | ||||
|                 var fromValue; | ||||
|                 var fromType; | ||||
|                 var fromRE; | ||||
|                 try { | ||||
|                 return getToValue(msg,rule).then(value => { | ||||
|                     return getFromValue(msg,rule).then(fromParts => { | ||||
|                         fromValue = fromParts.fromValue; | ||||
|                         fromType = fromParts.fromType; | ||||
|                         fromRE = fromParts.fromRE; | ||||
|                         if (rule.pt === 'msg') { | ||||
|                             try { | ||||
|                                 if (rule.t === 'delete') { | ||||
|                                     RED.util.setMessageProperty(msg,property,undefined); | ||||
|                                 } else if (rule.t === 'set') { | ||||
|                                     RED.util.setMessageProperty(msg,property,value); | ||||
|                                 } else if (rule.t === 'change') { | ||||
|                                     current = RED.util.getMessageProperty(msg,property); | ||||
|                                     if (typeof current === 'string') { | ||||
|                                         if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) { | ||||
|                                             // str representation of exact from number/boolean | ||||
|                                             // only replace if they match exactly | ||||
|                                             RED.util.setMessageProperty(msg,property,value); | ||||
|                                         } else { | ||||
|                                             current = current.replace(fromRE,value); | ||||
|                                             RED.util.setMessageProperty(msg,property,current); | ||||
|                                         } | ||||
|                                     } else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') { | ||||
|                                         if (current == Number(fromValue)) { | ||||
|                                             RED.util.setMessageProperty(msg,property,value); | ||||
|                                         } | ||||
|                                     } else if (typeof current === 'boolean' && fromType === 'bool') { | ||||
|                                         if (current.toString() === fromValue) { | ||||
|                                             RED.util.setMessageProperty(msg,property,value); | ||||
|                                         } | ||||
|                                     } | ||||
|                                 } | ||||
|                             } catch(err) {} | ||||
|                             return msg; | ||||
|                         } else if (rule.pt === 'flow' || rule.pt === 'global') { | ||||
|                             var contextKey = RED.util.parseContextStore(property); | ||||
|                             return new Promise((resolve,reject) => { | ||||
|                                 var target = node.context()[rule.pt]; | ||||
|                                 var callback = err => { | ||||
|                                     if (err) { | ||||
|                                         reject(err); | ||||
|                                     } else { | ||||
|                                         resolve(msg); | ||||
|                                     } | ||||
|                                 } | ||||
|                                 if (rule.t === 'delete') { | ||||
|                                     target.set(contextKey.key,undefined,contextKey.store,callback); | ||||
|                                 } else if (rule.t === 'set') { | ||||
|                                     target.set(contextKey.key,value,contextKey.store,callback); | ||||
|                                 } else if (rule.t === 'change') { | ||||
|                                     target.get(contextKey.key,contextKey.store,(err,current) => { | ||||
|                                         if (err) { | ||||
|                                             reject(err); | ||||
|                                             return; | ||||
|                                         } | ||||
|                                         if (typeof current === 'string') { | ||||
|                                             if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) { | ||||
|                                                 // str representation of exact from number/boolean | ||||
|                                                 // only replace if they match exactly | ||||
|                                                 target.set(contextKey.key,value,contextKey.store,callback); | ||||
|                                             } else { | ||||
|                                                 current = current.replace(fromRE,value); | ||||
|                                                 target.set(contextKey.key,current,contextKey.store,callback); | ||||
|                                             } | ||||
|                                         } else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') { | ||||
|                                             if (current == Number(fromValue)) { | ||||
|                                                 target.set(contextKey.key,value,contextKey.store,callback); | ||||
|                                             } | ||||
|                                         } else if (typeof current === 'boolean' && fromType === 'bool') { | ||||
|                                             if (current.toString() === fromValue) { | ||||
|                                                 target.set(contextKey.key,value,contextKey.store,callback); | ||||
|                                             } | ||||
|                                         } | ||||
|                                     }); | ||||
|                                 } | ||||
|                             }); | ||||
|                         } | ||||
|                     }); | ||||
|                 }).catch(err => { | ||||
|                     node.error(err, msg); | ||||
|                     return null; | ||||
|                 }); | ||||
|             } catch(err) { | ||||
|                 return Promise.resolve(msg); | ||||
|             } | ||||
|         } | ||||
|         function applyRules(msg, currentRule) { | ||||
|             if (currentRule >= node.rules.length) { | ||||
|                 return Promise.resolve(msg); | ||||
|             } | ||||
|             var r = node.rules[currentRule]; | ||||
|             var rulePromise; | ||||
|             if (r.t === "move") { | ||||
|                 if ((r.tot !== r.pt) || (r.p.indexOf(r.to) !== -1)) { | ||||
|                     rulePromise = applyRule(msg,{t:"set", p:r.to, pt:r.tot, to:r.p, tot:r.pt}).then( | ||||
|                         msg => applyRule(msg,{t:"delete", p:r.p, pt:r.pt}) | ||||
|                     ); | ||||
|                 } | ||||
|                 else { // 2 step move if we are moving from a child | ||||
|                     rulePromise = applyRule(msg,{t:"set", p:"_temp_move", pt:r.tot, to:r.p, tot:r.pt}).then( | ||||
|                         msg => applyRule(msg,{t:"delete", p:r.p, pt:r.pt}) | ||||
|                     ).then( | ||||
|                         msg => applyRule(msg,{t:"set", p:r.to, pt:r.tot, to:"_temp_move", tot:r.pt}) | ||||
|                     ).then( | ||||
|                         msg => applyRule(msg,{t:"delete", p:"_temp_move", pt:r.pt}) | ||||
|                     ) | ||||
|                 } | ||||
|             } else { | ||||
|                 rulePromise = applyRule(msg,r); | ||||
|             } | ||||
|             return rulePromise.then( | ||||
|                 msg => { | ||||
|                     if (!msg) { | ||||
|                         return | ||||
|                     } else if (currentRule === node.rules.length - 1) { | ||||
|                         return msg; | ||||
|                     } else { | ||||
|                         fromType = rule.fromt; | ||||
|                         fromValue = rule.from; | ||||
|                         fromRE = rule.fromRE; | ||||
|                         return applyRules(msg, currentRule+1); | ||||
|                     } | ||||
|                 } | ||||
|                 if (rule.pt === 'msg') { | ||||
|                     if (rule.t === 'delete') { | ||||
|                         RED.util.setMessageProperty(msg,property,undefined); | ||||
|                     } else if (rule.t === 'set') { | ||||
|                         RED.util.setMessageProperty(msg,property,value); | ||||
|                     } else if (rule.t === 'change') { | ||||
|                         current = RED.util.getMessageProperty(msg,property); | ||||
|                         if (typeof current === 'string') { | ||||
|                             if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) { | ||||
|                                 // str representation of exact from number/boolean | ||||
|                                 // only replace if they match exactly | ||||
|                                 RED.util.setMessageProperty(msg,property,value); | ||||
|                             } else { | ||||
|                                 current = current.replace(fromRE,value); | ||||
|                                 RED.util.setMessageProperty(msg,property,current); | ||||
|                             } | ||||
|                         } else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') { | ||||
|                             if (current == Number(fromValue)) { | ||||
|                                 RED.util.setMessageProperty(msg,property,value); | ||||
|                             } | ||||
|                         } else if (typeof current === 'boolean' && fromType === 'bool') { | ||||
|                             if (current.toString() === fromValue) { | ||||
|                                 RED.util.setMessageProperty(msg,property,value); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 else { | ||||
|                     var target; | ||||
|                     if (rule.pt === 'flow') { | ||||
|                         target = node.context().flow; | ||||
|                     } else if (rule.pt === 'global') { | ||||
|                         target = node.context().global; | ||||
|                     } | ||||
|                     if (target) { | ||||
|                         if (rule.t === 'delete') { | ||||
|                             target.set(property,undefined); | ||||
|                         } else if (rule.t === 'set') { | ||||
|                             target.set(property,value); | ||||
|                         } else if (rule.t === 'change') { | ||||
|                             current = target.get(property); | ||||
|                             if (typeof current === 'string') { | ||||
|                                 if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) { | ||||
|                                     // str representation of exact from number/boolean | ||||
|                                     // only replace if they match exactly | ||||
|                                     target.set(property,value); | ||||
|                                 } else { | ||||
|                                     current = current.replace(fromRE,value); | ||||
|                                     target.set(property,current); | ||||
|                                 } | ||||
|                             } else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') { | ||||
|                                 if (current == Number(fromValue)) { | ||||
|                                     target.set(property,value); | ||||
|                                 } | ||||
|                             } else if (typeof current === 'boolean' && fromType === 'bool') { | ||||
|                                 if (current.toString() === fromValue) { | ||||
|                                     target.set(property,value); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } catch(err) {/*console.log(err.stack)*/} | ||||
|             return msg; | ||||
|             ); | ||||
|         } | ||||
|         if (valid) { | ||||
|             this.on('input', function(msg) { | ||||
|                 for (var i=0; i<this.rules.length; i++) { | ||||
|                     if (this.rules[i].t === "move") { | ||||
|                         var r = this.rules[i]; | ||||
|                         if ((r.tot !== r.pt) || (r.p.indexOf(r.to) !== -1)) { | ||||
|                             msg = applyRule(msg,{t:"set", p:r.to, pt:r.tot, to:r.p, tot:r.pt}); | ||||
|                             applyRule(msg,{t:"delete", p:r.p, pt:r.pt}); | ||||
|                         } | ||||
|                         else { // 2 step move if we are moving from a child | ||||
|                             msg = applyRule(msg,{t:"set", p:"_temp_move", pt:r.tot, to:r.p, tot:r.pt}); | ||||
|                             applyRule(msg,{t:"delete", p:r.p, pt:r.pt}); | ||||
|                             msg = applyRule(msg,{t:"set", p:r.to, pt:r.tot, to:"_temp_move", tot:r.pt}); | ||||
|                             applyRule(msg,{t:"delete", p:"_temp_move", pt:r.pt}); | ||||
|                         } | ||||
|                     } else { | ||||
|                         msg = applyRule(msg,this.rules[i]); | ||||
|                     } | ||||
|                     if (msg === null) { | ||||
|                         return; | ||||
|                     } | ||||
|                 } | ||||
|                 node.send(msg); | ||||
|                 applyRules(msg, 0) | ||||
|                     .then( msg => { if (msg) { node.send(msg) }} ) | ||||
|                     .catch( err => node.error(err, msg)) | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -295,7 +295,8 @@ | ||||
|     For object outputs, once this count has been reached, the node can be configured to send a message for each subsequent message | ||||
|     received.</p> | ||||
|     <p>A <i>timeout</i> can be set to trigger sending the new message using whatever has been received so far.</p> | ||||
|     <p>If a message is received with the <b>msg.complete</b> property set, the output message is sent.</p> | ||||
|     <p>If a message is received with the <b>msg.complete</b> property set, the output message is finalised and sent. | ||||
|     This resets any part counts.</p> | ||||
|  | ||||
|     <h4>Reduce Sequence mode</h4> | ||||
|     <p>When configured to join in reduce mode, an expression is applied to each | ||||
| @@ -415,7 +416,7 @@ | ||||
|                     $("#node-input-reduceExp").typedInput({types:[jsonata_or_empty]}); | ||||
|                     $("#node-input-reduceInit").typedInput({ | ||||
|                         default: 'num', | ||||
|                         types:['flow','global','str','num','bool','json','bin','date','jsonata'], | ||||
|                         types:['flow','global','str','num','bool','json','bin','date','jsonata','env'], | ||||
|                         typeField: $("#node-input-reduceInitType") | ||||
|                     }); | ||||
|                     $("#node-input-reduceFixup").typedInput({types:[jsonata_or_empty]}); | ||||
| @@ -439,10 +440,7 @@ | ||||
|             $("#node-input-joiner").typedInput({ | ||||
|                 default: 'str', | ||||
|                 typeField: $("#node-input-joinerType"), | ||||
|                 types:[ | ||||
|                     'str', | ||||
|                     'bin' | ||||
|                 ] | ||||
|                 types:['str', 'bin'] | ||||
|             }); | ||||
|  | ||||
|             $("#node-input-property").typedInput({ | ||||
| @@ -451,7 +449,7 @@ | ||||
|             }); | ||||
|  | ||||
|             $("#node-input-key").typedInput({ | ||||
|                 types:['msg', {value:"merge", label:"", hasValue:false}] | ||||
|                 types:['msg'] | ||||
|             }); | ||||
|  | ||||
|             $("#node-input-build").change(); | ||||
|   | ||||
| @@ -233,7 +233,7 @@ module.exports = function(RED) { | ||||
|     RED.nodes.registerType("split",SplitNode); | ||||
|  | ||||
|  | ||||
|     var _max_kept_msgs_count = undefined; | ||||
|     var _max_kept_msgs_count; | ||||
|  | ||||
|     function max_kept_msgs_count(node) { | ||||
|         if (_max_kept_msgs_count === undefined) { | ||||
| @@ -252,13 +252,29 @@ module.exports = function(RED) { | ||||
|         exp.assign("I", index); | ||||
|         exp.assign("N", count); | ||||
|         exp.assign("A", accum); | ||||
|         return RED.util.evaluateJSONataExpression(exp, msg); | ||||
|         return new Promise((resolve,reject) => { | ||||
|             RED.util.evaluateJSONataExpression(exp, msg, (err, result) => { | ||||
|                 if (err) { | ||||
|                     reject(err); | ||||
|                 } else { | ||||
|                     resolve(result); | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function apply_f(exp, accum, count) { | ||||
|         exp.assign("N", count); | ||||
|         exp.assign("A", accum); | ||||
|         return RED.util.evaluateJSONataExpression(exp, {}); | ||||
|         return new Promise((resolve,reject) => { | ||||
|             return RED.util.evaluateJSONataExpression(exp, {}, (err, result) => { | ||||
|                 if (err) { | ||||
|                     reject(err); | ||||
|                 } else { | ||||
|                     resolve(result); | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function exp_or_undefined(exp) { | ||||
| @@ -269,32 +285,40 @@ module.exports = function(RED) { | ||||
|         return exp | ||||
|     } | ||||
|  | ||||
|     function reduce_and_send_group(node, group) { | ||||
|     function reduceAndSendGroup(node, group) { | ||||
|         var is_right = node.reduce_right; | ||||
|         var flag = is_right ? -1 : 1; | ||||
|         var msgs = group.msgs; | ||||
|         var accum = eval_exp(node, node.exp_init, node.exp_init_type); | ||||
|         var reduce_exp = node.reduce_exp; | ||||
|         var reduce_fixup = node.reduce_fixup; | ||||
|         var count = group.count; | ||||
|         msgs.sort(function(x,y) { | ||||
|             var ix = x.parts.index; | ||||
|             var iy = y.parts.index; | ||||
|             if (ix < iy) return -flag; | ||||
|             if (ix > iy) return flag; | ||||
|             return 0; | ||||
|         return getInitialReduceValue(node, node.exp_init, node.exp_init_type).then(accum => { | ||||
|             var reduce_exp = node.reduce_exp; | ||||
|             var reduce_fixup = node.reduce_fixup; | ||||
|             var count = group.count; | ||||
|             msgs.sort(function(x,y) { | ||||
|                 var ix = x.parts.index; | ||||
|                 var iy = y.parts.index; | ||||
|                 if (ix < iy) {return -flag;} | ||||
|                 if (ix > iy) {return flag;} | ||||
|                 return 0; | ||||
|             }); | ||||
|  | ||||
|             return msgs.reduce((promise, msg) => promise.then(accum => apply_r(reduce_exp, accum, msg, msg.parts.index, count)), Promise.resolve(accum)) | ||||
|                 .then(accum => { | ||||
|                     if(reduce_fixup !== undefined) { | ||||
|                         return apply_f(reduce_fixup, accum, count).then(accum => { | ||||
|                             node.send({payload: accum}); | ||||
|                         }); | ||||
|                     } else { | ||||
|                         node.send({payload: accum}); | ||||
|                     } | ||||
|                 }); | ||||
|         }).catch(err => { | ||||
|             throw new Error(RED._("join.errors.invalid-expr",{error:err.message})); | ||||
|         }); | ||||
|         for(var msg of msgs) { | ||||
|             accum = apply_r(reduce_exp, accum, msg, msg.parts.index, count); | ||||
|         } | ||||
|         if(reduce_fixup !== undefined) { | ||||
|             accum = apply_f(reduce_fixup, accum, count); | ||||
|         } | ||||
|         node.send({payload: accum}); | ||||
|     } | ||||
|  | ||||
|     function reduce_msg(node, msg) { | ||||
|         if(msg.hasOwnProperty('parts')) { | ||||
|         var promise; | ||||
|         if (msg.hasOwnProperty('parts')) { | ||||
|             var parts = msg.parts; | ||||
|             var pending = node.pending; | ||||
|             var pending_count = node.pending_count; | ||||
| @@ -311,66 +335,51 @@ module.exports = function(RED) { | ||||
|             } | ||||
|             var group = pending[gid]; | ||||
|             var msgs = group.msgs; | ||||
|             if(parts.hasOwnProperty('count') && | ||||
|                (group.count === undefined)) { | ||||
|                 group.count = count; | ||||
|             if(parts.hasOwnProperty('count') && (group.count === undefined)) { | ||||
|                 group.count = parts.count; | ||||
|             } | ||||
|             msgs.push(msg); | ||||
|             pending_count++; | ||||
|             var completeProcess = function() { | ||||
|                 node.pending_count = pending_count; | ||||
|                 var max_msgs = max_kept_msgs_count(node); | ||||
|                 if ((max_msgs > 0) && (pending_count > max_msgs)) { | ||||
|                     node.pending = {}; | ||||
|                     node.pending_count = 0; | ||||
|                     var promise = Promise.reject(RED._("join.too-many")); | ||||
|                     promise.catch(()=>{}); | ||||
|                     return promise; | ||||
|                 } | ||||
|                 return Promise.resolve(); | ||||
|             } | ||||
|             if(msgs.length === group.count) { | ||||
|                 delete pending[gid]; | ||||
|                 try { | ||||
|                     pending_count -= msgs.length; | ||||
|                     reduce_and_send_group(node, group); | ||||
|                 } catch(e) { | ||||
|                     node.error(RED._("join.errors.invalid-expr",{error:e.message}));            } | ||||
|                 pending_count -= msgs.length; | ||||
|                 promise = reduceAndSendGroup(node, group).then(completeProcess); | ||||
|             } else { | ||||
|                 promise = completeProcess(); | ||||
|             } | ||||
|             node.pending_count = pending_count; | ||||
|             var max_msgs = max_kept_msgs_count(node); | ||||
|             if ((max_msgs > 0) && (pending_count > max_msgs)) { | ||||
|                 node.pending = {}; | ||||
|                 node.pending_count = 0; | ||||
|                 node.error(RED._("join.too-many"), msg); | ||||
|             } | ||||
|         } | ||||
|         else { | ||||
|         } else { | ||||
|             node.send(msg); | ||||
|         } | ||||
|         if (!promise) { | ||||
|             promise = Promise.resolve(); | ||||
|         } | ||||
|         return promise; | ||||
|     } | ||||
|  | ||||
|     function eval_exp(node, exp, exp_type) { | ||||
|         if(exp_type === "flow") { | ||||
|             return node.context().flow.get(exp); | ||||
|         } | ||||
|         else if(exp_type === "global") { | ||||
|             return node.context().global.get(exp); | ||||
|         } | ||||
|         else if(exp_type === "str") { | ||||
|             return exp; | ||||
|         } | ||||
|         else if(exp_type === "num") { | ||||
|             return Number(exp); | ||||
|         } | ||||
|         else if(exp_type === "bool") { | ||||
|             if (exp === 'true') { | ||||
|                 return true; | ||||
|             } | ||||
|             else if (exp === 'false') { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|         else if ((exp_type === "bin") || | ||||
|                  (exp_type === "json")) { | ||||
|             return JSON.parse(exp); | ||||
|         } | ||||
|         else if(exp_type === "date") { | ||||
|             return Date.now(); | ||||
|         } | ||||
|         else if(exp_type === "jsonata") { | ||||
|             var jexp = RED.util.prepareJSONataExpression(exp, node); | ||||
|             return RED.util.evaluateJSONataExpression(jexp, {}); | ||||
|         } | ||||
|         throw new Error("unexpected initial value type"); | ||||
|     function getInitialReduceValue(node, exp, exp_type) { | ||||
|         return new Promise((resolve, reject) => { | ||||
|             RED.util.evaluateNodeProperty(exp, exp_type, node, {}, | ||||
|                 (err, result) => { | ||||
|                     if(err) { | ||||
|                         return reject(err); | ||||
|                     } | ||||
|                     else { | ||||
|                         return resolve(result); | ||||
|                     } | ||||
|                 }); | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     function JoinNode(n) { | ||||
| @@ -399,6 +408,7 @@ module.exports = function(RED) { | ||||
|                 this.reduce_fixup = (exp_fixup !== undefined) ? RED.util.prepareJSONataExpression(exp_fixup, this) : undefined; | ||||
|             } catch(e) { | ||||
|                 this.error(RED._("join.errors.invalid-expr",{error:e.message})); | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -437,7 +447,8 @@ module.exports = function(RED) { | ||||
|                     newArray = newArray.concat(n); | ||||
|                 }) | ||||
|                 group.payload = newArray; | ||||
|             } else if (group.type === 'buffer') { | ||||
|             } | ||||
|             else if (group.type === 'buffer') { | ||||
|                 var buffers = []; | ||||
|                 var bufferLen = 0; | ||||
|                 if (group.joinChar !== undefined) { | ||||
| @@ -450,7 +461,8 @@ module.exports = function(RED) { | ||||
|                         buffers.push(group.payload[i]); | ||||
|                         bufferLen += group.payload[i].length; | ||||
|                     } | ||||
|                 } else { | ||||
|                 } | ||||
|                 else { | ||||
|                     bufferLen = group.bufferLen; | ||||
|                     buffers = group.payload; | ||||
|                 } | ||||
| @@ -463,7 +475,8 @@ module.exports = function(RED) { | ||||
|                     groupJoinChar = group.joinChar.toString(); | ||||
|                 } | ||||
|                 RED.util.setMessageProperty(group.msg,node.property,group.payload.join(groupJoinChar)); | ||||
|             } else { | ||||
|             } | ||||
|             else { | ||||
|                 if (node.propertyType === 'full') { | ||||
|                     group.msg = RED.util.cloneMessage(group.msg); | ||||
|                 } | ||||
| @@ -471,13 +484,48 @@ module.exports = function(RED) { | ||||
|             } | ||||
|             if (group.msg.hasOwnProperty('parts') && group.msg.parts.hasOwnProperty('parts')) { | ||||
|                 group.msg.parts = group.msg.parts.parts; | ||||
|             } else { | ||||
|             } | ||||
|             else { | ||||
|                 delete group.msg.parts; | ||||
|             } | ||||
|             delete group.msg.complete; | ||||
|             node.send(group.msg); | ||||
|         } | ||||
|  | ||||
|         var pendingMessages = []; | ||||
|         var activeMessagePromise = null; | ||||
|         // In reduce mode, we must process messages fully in order otherwise | ||||
|         // groups may overlap and cause unexpected results. The use of JSONata | ||||
|         // means some async processing *might* occur if flow/global context is | ||||
|         // accessed. | ||||
|         var processReduceMessageQueue = function(msg) { | ||||
|             if (msg) { | ||||
|                 // A new message has arrived - add it to the message queue | ||||
|                 pendingMessages.push(msg); | ||||
|                 if (activeMessagePromise !== null) { | ||||
|                     // The node is currently processing a message, so do nothing | ||||
|                     // more with this message | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|             if (pendingMessages.length === 0) { | ||||
|                 // There are no more messages to process, clear the active flag | ||||
|                 // and return | ||||
|                 activeMessagePromise = null; | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // There are more messages to process. Get the next message and | ||||
|             // start processing it. Recurse back in to check for any more | ||||
|             var nextMsg = pendingMessages.shift(); | ||||
|             activeMessagePromise = reduce_msg(node, nextMsg) | ||||
|                 .then(processReduceMessageQueue) | ||||
|                 .catch((err) => { | ||||
|                     node.error(err,nextMsg); | ||||
|                     return processReduceMessageQueue(); | ||||
|                 }); | ||||
|         } | ||||
|  | ||||
|         this.on("input", function(msg) { | ||||
|             try { | ||||
|                 var property; | ||||
| @@ -516,8 +564,7 @@ module.exports = function(RED) { | ||||
|                     propertyIndex = msg.parts.index; | ||||
|                 } | ||||
|                 else if (node.mode === 'reduce') { | ||||
|                     reduce_msg(node, msg); | ||||
|                     return; | ||||
|                     return processReduceMessageQueue(msg); | ||||
|                 } | ||||
|                 else { | ||||
|                     // Use the node configuration to identify all of the group information | ||||
| @@ -525,7 +572,7 @@ module.exports = function(RED) { | ||||
|                     payloadType = node.build; | ||||
|                     targetCount = node.count; | ||||
|                     joinChar = node.joiner; | ||||
|                     if (targetCount === 0 && msg.hasOwnProperty('parts')) { | ||||
|                     if (n.count === "" && msg.hasOwnProperty('parts')) { | ||||
|                         targetCount = msg.parts.count || 0; | ||||
|                     } | ||||
|                     if (node.build === 'object') { | ||||
| @@ -539,7 +586,10 @@ module.exports = function(RED) { | ||||
|                     } | ||||
|                     else { | ||||
|                         if (msg.hasOwnProperty('complete')) { | ||||
|                             completeSend(partId); | ||||
|                             if (inflight[partId]) { | ||||
|                                 inflight[partId].msg.complete = msg.complete; | ||||
|                                 completeSend(partId); | ||||
|                             } | ||||
|                         } | ||||
|                         else { | ||||
|                             node.warn("Message missing key property 'msg."+node.key+"' - cannot add to object") | ||||
| @@ -547,6 +597,7 @@ module.exports = function(RED) { | ||||
|                     } | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 if (!inflight.hasOwnProperty(partId)) { | ||||
|                     if (payloadType === 'object' || payloadType === 'merged') { | ||||
|                         inflight[partId] = { | ||||
| @@ -554,29 +605,16 @@ module.exports = function(RED) { | ||||
|                             payload:{}, | ||||
|                             targetCount:targetCount, | ||||
|                             type:"object", | ||||
|                             msg:msg | ||||
|                             msg:RED.util.cloneMessage(msg) | ||||
|                         }; | ||||
|                     } | ||||
|                     else if (node.accumulate === true) { | ||||
|                         if (msg.hasOwnProperty("reset")) { delete inflight[partId]; } | ||||
|                         inflight[partId] = inflight[partId] || { | ||||
|                             currentCount:0, | ||||
|                             payload:{}, | ||||
|                             targetCount:targetCount, | ||||
|                             type:payloadType, | ||||
|                             msg:msg | ||||
|                         } | ||||
|                         if (payloadType === 'string' || payloadType === 'array' || payloadType === 'buffer') { | ||||
|                             inflight[partId].payload = []; | ||||
|                         } | ||||
|                     } | ||||
|                     else { | ||||
|                         inflight[partId] = { | ||||
|                             currentCount:0, | ||||
|                             payload:[], | ||||
|                             targetCount:targetCount, | ||||
|                             type:payloadType, | ||||
|                             msg:msg | ||||
|                             msg:RED.util.cloneMessage(msg) | ||||
|                         }; | ||||
|                         if (payloadType === 'string') { | ||||
|                             inflight[partId].joinChar = joinChar; | ||||
| @@ -619,19 +657,22 @@ module.exports = function(RED) { | ||||
|                 } else { | ||||
|                     if (!isNaN(propertyIndex)) { | ||||
|                         group.payload[propertyIndex] = property; | ||||
|                         group.currentCount++; | ||||
|                     } else { | ||||
|                         group.payload.push(property); | ||||
|                         if (property !== undefined) { | ||||
|                             group.payload.push(property); | ||||
|                             group.currentCount++; | ||||
|                         } | ||||
|                     } | ||||
|                     group.currentCount++; | ||||
|                 } | ||||
|                 // TODO: currently reuse the last received - add option to pick first received | ||||
|                 group.msg = msg; | ||||
|                 group.msg = Object.assign(group.msg, msg); | ||||
|                 var tcnt = group.targetCount; | ||||
|                 if (msg.hasOwnProperty("parts")) { tcnt = group.targetCount || msg.parts.count; } | ||||
|                 if ((tcnt > 0 && group.currentCount >= tcnt) || msg.hasOwnProperty('complete')) { | ||||
|                     completeSend(partId); | ||||
|                 } | ||||
|             } catch(err) { | ||||
|             } | ||||
|             catch(err) { | ||||
|                 console.log(err.stack); | ||||
|             } | ||||
|         }); | ||||
|   | ||||
| @@ -17,7 +17,7 @@ | ||||
| module.exports = function(RED) { | ||||
|     "use strict"; | ||||
|  | ||||
|     var _max_kept_msgs_count = undefined; | ||||
|     var _max_kept_msgs_count; | ||||
|  | ||||
|     function max_kept_msgs_count(node) { | ||||
|         if (_max_kept_msgs_count === undefined) { | ||||
| @@ -32,30 +32,20 @@ module.exports = function(RED) { | ||||
|         return _max_kept_msgs_count; | ||||
|     } | ||||
|  | ||||
|     function eval_jsonata(node, code, val) { | ||||
|         try { | ||||
|             return RED.util.evaluateJSONataExpression(code, val); | ||||
|         } | ||||
|         catch (e) { | ||||
|             node.error(RED._("sort.invalid-exp")); | ||||
|             throw e; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function get_context_val(node, name, dval) { | ||||
|         var context = node.context(); | ||||
|         var val = context.get(name); | ||||
|         if (val === undefined) { | ||||
|             context.set(name, dval); | ||||
|             return dval; | ||||
|         } | ||||
|         return val; | ||||
|     } | ||||
|     // function get_context_val(node, name, dval) { | ||||
|     //     var context = node.context(); | ||||
|     //     var val = context.get(name); | ||||
|     //     if (val === undefined) { | ||||
|     //         context.set(name, dval); | ||||
|     //         return dval; | ||||
|     //     } | ||||
|     //     return val; | ||||
|     // } | ||||
|  | ||||
|     function SortNode(n) { | ||||
|         RED.nodes.createNode(this, n); | ||||
|         var node = this; | ||||
|         var pending = get_context_val(node, 'pending', {}) | ||||
|         var pending = {};//get_context_val(node, 'pending', {}) | ||||
|         var pending_count = 0; | ||||
|         var pending_id = 0; | ||||
|         var order = n.order || "ascending"; | ||||
| @@ -71,16 +61,15 @@ module.exports = function(RED) { | ||||
|                 key_exp = RED.util.prepareJSONataExpression(key_exp, this); | ||||
|             } | ||||
|             catch (e) { | ||||
|                 node.error(RED._("sort.invalid-exp")); | ||||
|                 node.error(RED._("sort.invalid-exp",{message:e.toString()})); | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|         var dir = (order === "ascending") ? 1 : -1; | ||||
|         var conv = as_num | ||||
|             ? function(x) { return Number(x); } | ||||
|             : function(x) { return x; }; | ||||
|         var conv = as_num ? function(x) { return Number(x); } | ||||
|                           : function(x) { return x; }; | ||||
|  | ||||
|         function gen_comp(key) { | ||||
|         function generateComparisonFunction(key) { | ||||
|             return function(x, y) { | ||||
|                 var xp = conv(key(x)); | ||||
|                 var yp = conv(key(y)); | ||||
| @@ -90,74 +79,105 @@ module.exports = function(RED) { | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         function send_group(group) { | ||||
|             var key = key_is_exp | ||||
|                 ? function(msg) { | ||||
|                     return eval_jsonata(node, key_exp, msg); | ||||
|                 } | ||||
|                 : function(msg) { | ||||
|                     return RED.util.getMessageProperty(msg, key_prop); | ||||
|                 }; | ||||
|             var comp = gen_comp(key); | ||||
|         function sortMessageGroup(group) { | ||||
|             var promise; | ||||
|             var msgs = group.msgs; | ||||
|             try { | ||||
|                 msgs.sort(comp); | ||||
|             } | ||||
|             catch (e) { | ||||
|                 return; // not send when error | ||||
|             } | ||||
|             for (var i = 0; i < msgs.length; i++) { | ||||
|                 var msg = msgs[i]; | ||||
|                 msg.parts.index = i; | ||||
|                 node.send(msg); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         function sort_payload(msg) { | ||||
|             var data = RED.util.getMessageProperty(msg, target_prop); | ||||
|             if (Array.isArray(data)) { | ||||
|                 var key = key_is_exp | ||||
|                     ? function(elem) { | ||||
|                         return eval_jsonata(node, key_exp, elem); | ||||
|                     } | ||||
|                     : function(elem) { return elem; }; | ||||
|                 var comp = gen_comp(key); | ||||
|             if (key_is_exp) { | ||||
|                 var evaluatedDataPromises = msgs.map(msg => { | ||||
|                     return new Promise((resolve,reject) => { | ||||
|                         RED.util.evaluateJSONataExpression(key_exp, msg, (err, result) => { | ||||
|                             if (err) { | ||||
|                                 reject(RED._("sort.invalid-exp",{message:err.toString()})); | ||||
|                             } else { | ||||
|                                 resolve({ | ||||
|                                     item: msg, | ||||
|                                     sortValue: result | ||||
|                                 }) | ||||
|                             } | ||||
|                         }); | ||||
|                     }) | ||||
|                 }); | ||||
|                 promise = Promise.all(evaluatedDataPromises).then(evaluatedElements => { | ||||
|                     // Once all of the sort keys are evaluated, sort by them | ||||
|                     var comp = generateComparisonFunction(elem=>elem.sortValue); | ||||
|                     return evaluatedElements.sort(comp).map(elem=>elem.item); | ||||
|                 }); | ||||
|             } else { | ||||
|                 var key = function(msg) { | ||||
|                     return ; | ||||
|                 } | ||||
|                 var comp = generateComparisonFunction(msg => RED.util.getMessageProperty(msg, key_prop)); | ||||
|                 try { | ||||
|                     data.sort(comp); | ||||
|                     msgs.sort(comp); | ||||
|                 } | ||||
|                 catch (e) { | ||||
|                     return false; | ||||
|                     return; // not send when error | ||||
|                 } | ||||
|                 return true; | ||||
|                 promise = Promise.resolve(msgs); | ||||
|             } | ||||
|             return false; | ||||
|             return promise.then(msgs => { | ||||
|                 for (var i = 0; i < msgs.length; i++) { | ||||
|                     var msg = msgs[i]; | ||||
|                     msg.parts.index = i; | ||||
|                     node.send(msg); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         function check_parts(parts) { | ||||
|             if (parts.hasOwnProperty("id") && | ||||
|                 parts.hasOwnProperty("index")) { | ||||
|                 return true; | ||||
|         function sortMessageProperty(msg) { | ||||
|             var data = RED.util.getMessageProperty(msg, target_prop); | ||||
|             if (Array.isArray(data)) { | ||||
|                 if (key_is_exp) { | ||||
|                     // key is an expression. Evaluated the expression for each item | ||||
|                     // to get its sort value. As this could be async, need to do | ||||
|                     // it first. | ||||
|                     var evaluatedDataPromises = data.map(elem => { | ||||
|                         return new Promise((resolve,reject) => { | ||||
|                             RED.util.evaluateJSONataExpression(key_exp, elem, (err, result) => { | ||||
|                                 if (err) { | ||||
|                                     reject(RED._("sort.invalid-exp",{message:err.toString()})); | ||||
|                                 } else { | ||||
|                                     resolve({ | ||||
|                                         item: elem, | ||||
|                                         sortValue: result | ||||
|                                     }) | ||||
|                                 } | ||||
|                             }); | ||||
|                         }) | ||||
|                     }) | ||||
|                     return Promise.all(evaluatedDataPromises).then(evaluatedElements => { | ||||
|                         // Once all of the sort keys are evaluated, sort by them | ||||
|                         // and reconstruct the original message item with the newly | ||||
|                         // sorted values. | ||||
|                         var comp = generateComparisonFunction(elem=>elem.sortValue); | ||||
|                         data = evaluatedElements.sort(comp).map(elem=>elem.item); | ||||
|                         RED.util.setMessageProperty(msg, target_prop,data); | ||||
|                         return true; | ||||
|                     }) | ||||
|                 } else { | ||||
|                     var comp = generateComparisonFunction(elem=>elem); | ||||
|                     try { | ||||
|                         data.sort(comp); | ||||
|                     } catch (e) { | ||||
|                         return Promise.resolve(false); | ||||
|                     } | ||||
|                     return Promise.resolve(true); | ||||
|                 } | ||||
|             } | ||||
|             return false; | ||||
|             return Promise.resolve(false); | ||||
|         } | ||||
|  | ||||
|         function clear_pending() { | ||||
|         function removeOldestPending() { | ||||
|             var oldest; | ||||
|             var oldest_key; | ||||
|             for(var key in pending) { | ||||
|                 node.log(RED._("sort.clear"), pending[key].msgs[0]); | ||||
|                 delete pending[key]; | ||||
|             } | ||||
|             pending_count = 0; | ||||
|         } | ||||
|  | ||||
|         function remove_oldest_pending() { | ||||
|             var oldest = undefined; | ||||
|             var oldest_key = undefined; | ||||
|             for(var key in pending) { | ||||
|                 var item = pending[key]; | ||||
|                 if((oldest === undefined) || | ||||
|                    (oldest.seq_no > item.seq_no)) { | ||||
|                     oldest = item; | ||||
|                     oldest_key = key; | ||||
|                 if (pending.hasOwnProperty(key)) { | ||||
|                     var item = pending[key]; | ||||
|                     if((oldest === undefined) || | ||||
|                        (oldest.seq_no > item.seq_no)) { | ||||
|                         oldest = item; | ||||
|                         oldest_key = key; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             if(oldest !== undefined) { | ||||
| @@ -166,16 +186,20 @@ module.exports = function(RED) { | ||||
|             } | ||||
|             return 0; | ||||
|         } | ||||
|          | ||||
|         function process_msg(msg) { | ||||
|  | ||||
|         function processMessage(msg) { | ||||
|             if (target_is_prop) { | ||||
|                 if (sort_payload(msg)) { | ||||
|                     node.send(msg); | ||||
|                 } | ||||
|                 sortMessageProperty(msg).then(send => { | ||||
|                     if (send) { | ||||
|                         node.send(msg); | ||||
|                     } | ||||
|                 }).catch(err => { | ||||
|                     node.error(err,msg); | ||||
|                 }); | ||||
|                 return; | ||||
|             } | ||||
|             var parts = msg.parts; | ||||
|             if (!check_parts(parts)) { | ||||
|             if (!parts || !parts.hasOwnProperty("id") || !parts.hasOwnProperty("index")) { | ||||
|                 return; | ||||
|             } | ||||
|             var gid = parts.id; | ||||
| @@ -195,23 +219,32 @@ module.exports = function(RED) { | ||||
|             pending_count++; | ||||
|             if (group.count === msgs.length) { | ||||
|                 delete pending[gid] | ||||
|                 send_group(group); | ||||
|                 sortMessageGroup(group).catch(err => { | ||||
|                     node.error(err,msg); | ||||
|                 }); | ||||
|                 pending_count -= msgs.length; | ||||
|             } | ||||
|             var max_msgs = max_kept_msgs_count(node); | ||||
|             if ((max_msgs > 0) && (pending_count > max_msgs)) { | ||||
|                 pending_count -= remove_oldest_pending(); | ||||
|                 node.error(RED._("sort.too-many"), msg); | ||||
|             } else { | ||||
|                 var max_msgs = max_kept_msgs_count(node); | ||||
|                 if ((max_msgs > 0) && (pending_count > max_msgs)) { | ||||
|                     pending_count -= removeOldestPending(); | ||||
|                     node.error(RED._("sort.too-many"), msg); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|          | ||||
|  | ||||
|         this.on("input", function(msg) { | ||||
|             process_msg(msg); | ||||
|             processMessage(msg); | ||||
|         }); | ||||
|  | ||||
|         this.on("close", function() { | ||||
|             clear_pending(); | ||||
|         }) | ||||
|             for(var key in pending) { | ||||
|                 if (pending.hasOwnProperty(key)) { | ||||
|                     node.log(RED._("sort.clear"), pending[key].msgs[0]); | ||||
|                     delete pending[key]; | ||||
|                 } | ||||
|             } | ||||
|             pending_count = 0; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     RED.nodes.registerType("sort", SortNode); | ||||
|   | ||||
| @@ -100,9 +100,9 @@ | ||||
|         defaults: { | ||||
|             name: {value:""}, | ||||
|             mode: {value:"count"}, | ||||
|             count: {value:10}, | ||||
|             overlap: {value:0}, | ||||
|             interval: {value:10}, | ||||
|             count: {value:10,validate:function(v) { return RED.validators.number(v) && (v >= 1); }}, | ||||
|             overlap: {value:0,validate:function(v) { return RED.validators.number(v) && (v >= 0); }}, | ||||
|             interval: {value:10,validate:function(v) { return RED.validators.number(v) && (v >= 1); }}, | ||||
|             allowEmptySequence: {value:false}, | ||||
|             topics: {value:[{topic:""}]} | ||||
|         }, | ||||
| @@ -149,12 +149,9 @@ | ||||
|                     removable: true | ||||
|                 }); | ||||
|  | ||||
|             $("#node-input-count").spinner({ | ||||
|             }); | ||||
|             $("#node-input-overlap").spinner({ | ||||
|             }); | ||||
|             $("#node-input-interval").spinner({ | ||||
|             }); | ||||
|             $("#node-input-count").spinner({min:1}); | ||||
|             $("#node-input-overlap").spinner({min:0}); | ||||
|             $("#node-input-interval").spinner({min:1}); | ||||
|             $("#node-input-mode").change(function(e) { | ||||
|                 var val = $(this).val(); | ||||
|                 $(".node-row-msg-count").toggle(val==="count"); | ||||
|   | ||||
| @@ -31,6 +31,8 @@ | ||||
|     <dl class="message-properties"> | ||||
|         <dt>payload<span class="property-type">object | string</span></dt> | ||||
|         <dd>A JavaScript object or JSON string.</dd> | ||||
|         <dt>schema<span class="property-type">object</span></dt> | ||||
|         <dd>An optional JSON Schema object to validate the payload against.</dd> | ||||
|     </dl> | ||||
|     <h3>Outputs</h3> | ||||
|     <dl class="message-properties"> | ||||
| @@ -41,6 +43,9 @@ | ||||
|                 <li>If the input is a JavaScript object it creates a JSON string. The string can optionally be well-formatted.</li> | ||||
|             </ul> | ||||
|         </dd> | ||||
|         <dt>schemaError<span class="property-type">array</span></dt> | ||||
|         <dd>If JSON schema validation fails, the catch node will have a <code>schemaError</code> property | ||||
|             containing an array of errors.</dd> | ||||
|     </dl> | ||||
|     <h3>Details</h3> | ||||
|     <p>By default, the node operates on <code>msg.payload</code>, but can be configured | ||||
| @@ -53,6 +58,8 @@ | ||||
|        receives a String, no further checks will be made of the property. It will | ||||
|        not check the String is valid JSON nor will it reformat it if the format option | ||||
|        is selected.</p> | ||||
|     <p>For more details about JSON Schema you can consult the specification | ||||
|     <a href="http://json-schema.org/latest/json-schema-validation.html">here</a>.</p> | ||||
| </script> | ||||
|  | ||||
| <script type="text/javascript"> | ||||
|   | ||||
| @@ -16,39 +16,102 @@ | ||||
|  | ||||
| module.exports = function(RED) { | ||||
|     "use strict"; | ||||
|     const Ajv = require('ajv'); | ||||
|     const ajv = new Ajv({allErrors: true, schemaId: 'auto'}); | ||||
|     ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json')); | ||||
|     ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json')); | ||||
|  | ||||
|     function JSONNode(n) { | ||||
|         RED.nodes.createNode(this,n); | ||||
|         this.indent = n.pretty ? 4 : 0; | ||||
|         this.action = n.action||""; | ||||
|         this.property = n.property||"payload"; | ||||
|         this.schema = null; | ||||
|         this.compiledSchema = null; | ||||
|  | ||||
|         var node = this; | ||||
|  | ||||
|         this.on("input", function(msg) { | ||||
|             var validate = false; | ||||
|             if (msg.schema) { | ||||
|                 // If input schema is different, re-compile it | ||||
|                 if (JSON.stringify(this.schema) != JSON.stringify(msg.schema)) { | ||||
|                     try { | ||||
|                         this.compiledSchema = ajv.compile(msg.schema); | ||||
|                         this.schema = msg.schema; | ||||
|                     } catch(e) { | ||||
|                         this.schema = null; | ||||
|                         this.compiledSchema = null; | ||||
|                         node.error(RED._("json.errors.schema-error-compile"), msg); | ||||
|                         return; | ||||
|                     } | ||||
|                 } | ||||
|                 validate = true; | ||||
|             } | ||||
|             var value = RED.util.getMessageProperty(msg,node.property); | ||||
|             if (value !== undefined) { | ||||
|                 if (typeof value === "string") { | ||||
|                     if (node.action === "" || node.action === "obj") { | ||||
|                         try { | ||||
|                             RED.util.setMessageProperty(msg,node.property,JSON.parse(value)); | ||||
|                             node.send(msg); | ||||
|                             if (validate) { | ||||
|                                 if (this.compiledSchema(msg[node.property])) { | ||||
|                                     node.send(msg); | ||||
|                                 } else { | ||||
|                                     msg.schemaError = this.compiledSchema.errors; | ||||
|                                     node.error(`${RED._("json.errors.schema-error")}: ${ajv.errorsText(this.compiledSchema.errors)}`, msg); | ||||
|                                 } | ||||
|                             } else  { | ||||
|                                 node.send(msg); | ||||
|                             } | ||||
|                         } | ||||
|                         catch(e) { node.error(e.message,msg); } | ||||
|                     } else { | ||||
|                         node.send(msg); | ||||
|                         // If node.action is str and value is str | ||||
|                         if (validate) { | ||||
|                             if (this.compiledSchema(JSON.parse(msg[node.property]))) { | ||||
|                                 node.send(msg); | ||||
|                             } else { | ||||
|                                 msg.schemaError = this.compiledSchema.errors; | ||||
|                                 node.error(`${RED._("json.errors.schema-error")}: ${ajv.errorsText(this.compiledSchema.errors)}`, msg); | ||||
|                             } | ||||
|                         } else { | ||||
|                             node.send(msg); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 else if (typeof value === "object") { | ||||
|                     if (node.action === "" || node.action === "str") { | ||||
|                         if (!Buffer.isBuffer(value)) { | ||||
|                             try { | ||||
|                                 RED.util.setMessageProperty(msg,node.property,JSON.stringify(value,null,node.indent)); | ||||
|                                 node.send(msg); | ||||
|                                 if (validate) { | ||||
|                                     if (this.compiledSchema(value)) { | ||||
|                                         RED.util.setMessageProperty(msg,node.property,JSON.stringify(value,null,node.indent)); | ||||
|                                         node.send(msg); | ||||
|                                     } else { | ||||
|                                         msg.schemaError = this.compiledSchema.errors; | ||||
|                                         node.error(`${RED._("json.errors.schema-error")}: ${ajv.errorsText(this.compiledSchema.errors)}`, msg); | ||||
|                                     } | ||||
|                                 } else { | ||||
|                                     RED.util.setMessageProperty(msg,node.property,JSON.stringify(value,null,node.indent)); | ||||
|                                     node.send(msg); | ||||
|                                 } | ||||
|                             } | ||||
|                             catch(e) { node.error(RED._("json.errors.dropped-error")); } | ||||
|                         } | ||||
|                         else { node.warn(RED._("json.errors.dropped-object")); } | ||||
|                     } else { | ||||
|                         node.send(msg); | ||||
|                         // If node.action is obj and value is object | ||||
|                         if (validate) { | ||||
|                             if (this.compiledSchema(value)) { | ||||
|                                 node.send(msg); | ||||
|                             } else { | ||||
|                                 msg.schemaError = this.compiledSchema.errors; | ||||
|                                 node.error(`${RED._("json.errors.schema-error")}: ${ajv.errorsText(this.compiledSchema.errors)}`, msg); | ||||
|                             } | ||||
|                         } else { | ||||
|                             node.send(msg); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 else { node.warn(RED._("json.errors.dropped")); } | ||||
|   | ||||
| @@ -37,6 +37,8 @@ | ||||
|         <dt class="optional">filename <span class="property-type">string</span></dt> | ||||
|         <dd>If not configured in the node, this optional property sets the name of the file to be updated.</dd> | ||||
|     </dl> | ||||
|     <h3>Output</h3> | ||||
|     <p>On completion of write, input message is sent to output port.</p> | ||||
|     <h3>Details</h3> | ||||
|     <p>Each message payload will be added to the end of the file, optionally appending | ||||
|     a newline (\n) character between each one.</p> | ||||
| @@ -123,9 +125,8 @@ | ||||
|         }, | ||||
|         color:"BurlyWood", | ||||
|         inputs:1, | ||||
|         outputs:0, | ||||
|         icon: "file.png", | ||||
|         align: "right", | ||||
|         outputs:1, | ||||
|         icon: "file-out.png", | ||||
|         label: function() { | ||||
|             if (this.overwriteFile === "delete") { | ||||
|                 return this.name||this._("file.label.deletelabel",{file:this.filename}); | ||||
| @@ -159,7 +160,7 @@ | ||||
|         outputLabels: function(i) { | ||||
|             return (this.format === "utf8") ? "UTF8 string" : "binary buffer"; | ||||
|         }, | ||||
|         icon: "file.png", | ||||
|         icon: "file-in.png", | ||||
|         label: function() { | ||||
|             return this.name||this.filename||this._("file.label.filelabel"); | ||||
|         }, | ||||
|   | ||||
| @@ -28,9 +28,11 @@ module.exports = function(RED) { | ||||
|         this.createDir = n.createDir || false; | ||||
|         var node = this; | ||||
|         node.wstream = null; | ||||
|         node.data = []; | ||||
|         node.msgQueue = []; | ||||
|         node.closing = false; | ||||
|         node.closeCallback = null; | ||||
|  | ||||
|         this.on("input",function(msg) { | ||||
|         function processMsg(msg, done) { | ||||
|             var filename = node.filename || msg.filename || ""; | ||||
|             if ((!node.filename) && (!node.tout)) { | ||||
|                 node.tout = setTimeout(function() { | ||||
| @@ -39,20 +41,29 @@ module.exports = function(RED) { | ||||
|                     node.tout = null; | ||||
|                 },333); | ||||
|             } | ||||
|             if (filename === "") { node.warn(RED._("file.errors.nofilename")); } | ||||
|             else if (node.overwriteFile === "delete") { | ||||
|             if (filename === "") { | ||||
|                 node.warn(RED._("file.errors.nofilename")); | ||||
|                 done(); | ||||
|             } else if (node.overwriteFile === "delete") { | ||||
|                 fs.unlink(filename, function (err) { | ||||
|                     if (err) { node.error(RED._("file.errors.deletefail",{error:err.toString()}),msg); } | ||||
|                     else if (RED.settings.verbose) { node.log(RED._("file.status.deletedfile",{file:filename})); } | ||||
|                     if (err) { | ||||
|                         node.error(RED._("file.errors.deletefail",{error:err.toString()}),msg); | ||||
|                     } else { | ||||
|                         if (RED.settings.verbose) { | ||||
|                             node.log(RED._("file.status.deletedfile",{file:filename})); | ||||
|                         } | ||||
|                         node.send(msg); | ||||
|                     } | ||||
|                     done(); | ||||
|                 }); | ||||
|             } | ||||
|             else if (msg.hasOwnProperty("payload") && (typeof msg.payload !== "undefined")) { | ||||
|             } else if (msg.hasOwnProperty("payload") && (typeof msg.payload !== "undefined")) { | ||||
|                 var dir = path.dirname(filename); | ||||
|                 if (node.createDir) { | ||||
|                     try { | ||||
|                         fs.ensureDirSync(dir); | ||||
|                     } catch(err) { | ||||
|                         node.error(RED._("file.errors.createfail",{error:err.toString()}),msg); | ||||
|                         done(); | ||||
|                         return; | ||||
|                     } | ||||
|                 } | ||||
| @@ -64,72 +75,142 @@ module.exports = function(RED) { | ||||
|                 if (typeof data === "boolean") { data = data.toString(); } | ||||
|                 if (typeof data === "number") { data = data.toString(); } | ||||
|                 if ((node.appendNewline) && (!Buffer.isBuffer(data))) { data += os.EOL; } | ||||
|                 node.data.push(Buffer.from(data)); | ||||
|  | ||||
|                 while (node.data.length > 0) { | ||||
|                     if (node.overwriteFile === "true") { | ||||
|                         node.wstream = fs.createWriteStream(filename, { encoding:'binary', flags:'w', autoClose:true }); | ||||
|                         node.wstream.on("error", function(err) { | ||||
|                             node.error(RED._("file.errors.writefail",{error:err.toString()}),msg); | ||||
|                 if (node.overwriteFile === "true") { | ||||
|                     var wstream = fs.createWriteStream(filename, { encoding:'binary', flags:'w', autoClose:true }); | ||||
|                     node.wstream = wstream; | ||||
|                     wstream.on("error", function(err) { | ||||
|                         node.error(RED._("file.errors.writefail",{error:err.toString()}),msg); | ||||
|                         done(); | ||||
|                     }); | ||||
|                     wstream.on("open", function() { | ||||
|                         wstream.end(data, function() { | ||||
|                             node.send(msg); | ||||
|                             done(); | ||||
|                         }); | ||||
|                         node.wstream.end(node.data.shift()); | ||||
|                     } | ||||
|                     else { | ||||
|                         // Append mode | ||||
|                         var recreateStream = !node.wstream || !node.filename; | ||||
|                         if (node.wstream && node.wstreamIno) { | ||||
|                             // There is already a stream open and we have the inode | ||||
|                             // of the file. Check the file hasn't been deleted | ||||
|                             // or deleted and recreated. | ||||
|                             try { | ||||
|                                 var stat = fs.statSync(filename); | ||||
|                                 // File exists - check the inode matches | ||||
|                                 if (stat.ino !== node.wstreamIno) { | ||||
|                                     // The file has been recreated. Close the current | ||||
|                                     // stream and recreate it | ||||
|                                     recreateStream = true; | ||||
|                                     node.wstream.end(); | ||||
|                                     delete node.wstream; | ||||
|                                     delete node.wstreamIno; | ||||
|                                 } | ||||
|                             } catch(err) { | ||||
|                                 // File does not exist | ||||
|                     }) | ||||
|                     return; | ||||
|                 } | ||||
|                 else { | ||||
|                     // Append mode | ||||
|                     var recreateStream = !node.wstream || !node.filename; | ||||
|                     if (node.wstream && node.wstreamIno) { | ||||
|                         // There is already a stream open and we have the inode | ||||
|                         // of the file. Check the file hasn't been deleted | ||||
|                         // or deleted and recreated. | ||||
|                         try { | ||||
|                             var stat = fs.statSync(filename); | ||||
|                             // File exists - check the inode matches | ||||
|                             if (stat.ino !== node.wstreamIno) { | ||||
|                                 // The file has been recreated. Close the current | ||||
|                                 // stream and recreate it | ||||
|                                 recreateStream = true; | ||||
|                                 node.wstream.end(); | ||||
|                                 delete node.wstream; | ||||
|                                 delete node.wstreamIno; | ||||
|                             } | ||||
|                         } | ||||
|                         if (recreateStream) { | ||||
|                             node.wstream = fs.createWriteStream(filename, { encoding:'binary', flags:'a', autoClose:true }); | ||||
|                             node.wstream.on("open", function(fd) { | ||||
|                                 try { | ||||
|                                     var stat = fs.statSync(filename); | ||||
|                                     node.wstreamIno = stat.ino; | ||||
|                                 } catch(err) { | ||||
|                                 } | ||||
|                             }); | ||||
|                             node.wstream.on("error", function(err) { | ||||
|                                 node.error(RED._("file.errors.appendfail",{error:err.toString()}),msg); | ||||
|                             }); | ||||
|                         } | ||||
|                         if (node.filename) { | ||||
|                             // Static filename - write and reuse the stream next time | ||||
|                             node.wstream.write(node.data.shift()); | ||||
|                         } else { | ||||
|                             // Dynamic filename - write and close the stream | ||||
|                             node.wstream.end(node.data.shift()); | ||||
|                         } catch(err) { | ||||
|                             // File does not exist | ||||
|                             recreateStream = true; | ||||
|                             node.wstream.end(); | ||||
|                             delete node.wstream; | ||||
|                             delete node.wstreamIno; | ||||
|                         } | ||||
|                     } | ||||
|                     if (recreateStream) { | ||||
|                         node.wstream = fs.createWriteStream(filename, { encoding:'binary', flags:'a', autoClose:true }); | ||||
|                         node.wstream.on("open", function(fd) { | ||||
|                             try { | ||||
|                                 var stat = fs.statSync(filename); | ||||
|                                 node.wstreamIno = stat.ino; | ||||
|                             } catch(err) { | ||||
|                             } | ||||
|                         }); | ||||
|                         node.wstream.on("error", function(err) { | ||||
|                             node.error(RED._("file.errors.appendfail",{error:err.toString()}),msg); | ||||
|                             done(); | ||||
|                         }); | ||||
|                     } | ||||
|                     if (node.filename) { | ||||
|                         // Static filename - write and reuse the stream next time | ||||
|                         node.wstream.write(data, function() { | ||||
|                             node.send(msg); | ||||
|                             done(); | ||||
|                         }); | ||||
|                     } else { | ||||
|                         // Dynamic filename - write and close the stream | ||||
|                         node.wstream.end(data, function() { | ||||
|                             node.send(msg); | ||||
|                             delete node.wstream; | ||||
|                             delete node.wstreamIno; | ||||
|                             done(); | ||||
|                         }); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             else { | ||||
|                 done(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         function processQ(queue) { | ||||
|             var msg = queue[0]; | ||||
|             processMsg(msg, function() { | ||||
|                 queue.shift(); | ||||
|                 if (queue.length > 0) { | ||||
|                     processQ(queue); | ||||
|                 } | ||||
|                 else if (node.closing) { | ||||
|                     closeNode(); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         this.on("input", function(msg) { | ||||
|             var msgQueue = node.msgQueue; | ||||
|             if (msgQueue.push(msg) > 1) { | ||||
|                 // pending write exists | ||||
|                 return; | ||||
|             } | ||||
|             try { | ||||
|                 processQ(msgQueue); | ||||
|             } | ||||
|             catch (e) { | ||||
|                 node.msgQueue = []; | ||||
|                 if (node.closing) { | ||||
|                     closeNode(); | ||||
|                 } | ||||
|                 throw e; | ||||
|             } | ||||
|         }); | ||||
|         this.on('close', function() { | ||||
|  | ||||
|         function closeNode() { | ||||
|             if (node.wstream) { node.wstream.end(); } | ||||
|             if (node.tout) { clearTimeout(node.tout); } | ||||
|             node.status({}); | ||||
|             var cb = node.closeCallback; | ||||
|             node.closeCallback = null; | ||||
|             node.closing = false; | ||||
|             if (cb) { | ||||
|                 cb(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.on('close', function(done) { | ||||
|             if (node.closing) { | ||||
|                 // already closing | ||||
|                 return; | ||||
|             } | ||||
|             node.closing = true; | ||||
|             if (done) { | ||||
|                 node.closeCallback = done; | ||||
|             } | ||||
|             if (node.msgQueue.length > 0) { | ||||
|                 // close after queue processed | ||||
|                 return; | ||||
|             } | ||||
|             else { | ||||
|                 closeNode(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|     RED.nodes.registerType("file",FileNode); | ||||
|   | ||||
							
								
								
									
										61
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										61
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "node-red", | ||||
|   "version": "0.18.7", | ||||
|   "version": "0.19.5", | ||||
|   "description": "A visual tool for wiring the Internet of Things", | ||||
|   "homepage": "http://nodered.org", | ||||
|   "license": "Apache-2.0", | ||||
| @@ -33,34 +33,37 @@ | ||||
|     "flow" | ||||
|   ], | ||||
|   "dependencies": { | ||||
|     "basic-auth": "2.0.0", | ||||
|     "ajv": "6.5.4", | ||||
|     "basic-auth": "2.0.1", | ||||
|     "bcryptjs": "2.4.3", | ||||
|     "body-parser": "1.18.3", | ||||
|     "cheerio": "0.22.0", | ||||
|     "clone": "2.1.1", | ||||
|     "clone": "2.1.2", | ||||
|     "cookie": "0.3.1", | ||||
|     "cookie-parser": "1.4.3", | ||||
|     "cors": "2.8.4", | ||||
|     "cron": "1.3.0", | ||||
|     "express": "4.16.3", | ||||
|     "cron": "1.5.0", | ||||
|     "denque": "1.3.0", | ||||
|     "express": "4.16.4", | ||||
|     "express-session": "1.15.6", | ||||
|     "fs-extra": "5.0.0", | ||||
|     "fs.notify": "0.0.4", | ||||
|     "hash-sum": "1.0.2", | ||||
|     "i18next": "1.10.6", | ||||
|     "https-proxy-agent": "2.2.1", | ||||
|     "i18next": "11.6.0", | ||||
|     "is-utf8": "0.2.1", | ||||
|     "js-yaml": "3.11.0", | ||||
|     "js-yaml": "3.12.0", | ||||
|     "json-stringify-safe": "5.0.1", | ||||
|     "jsonata": "1.5.4", | ||||
|     "media-typer": "0.3.0", | ||||
|     "memorystore": "1.6.0", | ||||
|     "mqtt": "2.18.0", | ||||
|     "multer": "1.3.0", | ||||
|     "mustache": "2.3.0", | ||||
|     "mqtt": "2.18.8", | ||||
|     "multer": "1.4.1", | ||||
|     "mustache": "2.3.2", | ||||
|     "node-red-node-email": "0.1.*", | ||||
|     "node-red-node-feedparser": "0.1.*", | ||||
|     "node-red-node-feedparser": "^0.1.12", | ||||
|     "node-red-node-rbe": "0.2.*", | ||||
|     "node-red-node-twitter": "*", | ||||
|     "node-red-node-twitter": "^1.1.0", | ||||
|     "nopt": "4.0.1", | ||||
|     "oauth2orize": "1.11.0", | ||||
|     "on-headers": "1.0.1", | ||||
| @@ -68,30 +71,30 @@ | ||||
|     "passport-http-bearer": "1.0.1", | ||||
|     "passport-oauth2-client-password": "0.1.2", | ||||
|     "raw-body": "2.3.3", | ||||
|     "request": "2.87.0", | ||||
|     "semver": "5.5.0", | ||||
|     "request": "2.88.0", | ||||
|     "semver": "5.6.0", | ||||
|     "sentiment": "2.1.0", | ||||
|     "uglify-js": "3.3.25", | ||||
|     "uglify-js": "3.4.9", | ||||
|     "when": "3.7.8", | ||||
|     "ws": "1.1.5", | ||||
|     "xml2js": "0.4.19" | ||||
|   }, | ||||
|   "optionalDependencies": { | ||||
|     "bcrypt": "~1.0.3" | ||||
|     "bcrypt": "~2.0.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "chromedriver": "^2.33.2", | ||||
|     "grunt": "~1.0.1", | ||||
|     "chromedriver": "2.41.0", | ||||
|     "grunt": "~1.0.3", | ||||
|     "grunt-chmod": "~1.1.1", | ||||
|     "grunt-cli": "~1.2.0", | ||||
|     "grunt-cli": "~1.3.1", | ||||
|     "grunt-concurrent": "~2.3.1", | ||||
|     "grunt-contrib-clean": "~1.1.0", | ||||
|     "grunt-contrib-compress": "~1.4.0", | ||||
|     "grunt-contrib-concat": "~1.0.1", | ||||
|     "grunt-contrib-copy": "~1.0.0", | ||||
|     "grunt-contrib-jshint": "~1.1.0", | ||||
|     "grunt-contrib-uglify": "~3.3.0", | ||||
|     "grunt-contrib-watch": "~1.0.0", | ||||
|     "grunt-contrib-uglify": "~3.4.0", | ||||
|     "grunt-contrib-watch": "~1.1.0", | ||||
|     "grunt-jsonlint": "~1.1.0", | ||||
|     "grunt-mocha-istanbul": "5.0.2", | ||||
|     "grunt-nodemon": "~0.4.2", | ||||
| @@ -100,16 +103,16 @@ | ||||
|     "grunt-webdriver": "^2.0.3", | ||||
|     "http-proxy": "^1.16.2", | ||||
|     "istanbul": "0.4.5", | ||||
|     "mocha": "^5.1.1", | ||||
|     "mocha": "^5.2.0", | ||||
|     "node-red-node-test-helper": "0.1.7", | ||||
|     "should": "^8.4.0", | ||||
|     "sinon": "1.17.7", | ||||
|     "stoppable": "^1.0.6", | ||||
|     "supertest": "3.0.0", | ||||
|     "wdio-chromedriver-service": "^0.1.1", | ||||
|     "wdio-mocha-framework": "^0.5.11", | ||||
|     "wdio-spec-reporter": "^0.1.3", | ||||
|     "webdriverio": "^4.9.11", | ||||
|     "node-red-node-test-helper": "^0.1.7" | ||||
|     "stoppable": "^1.0.7", | ||||
|     "supertest": "3.3.0", | ||||
|     "wdio-chromedriver-service": "^0.1.3", | ||||
|     "wdio-mocha-framework": "^0.6.2", | ||||
|     "wdio-spec-reporter": "^0.1.5", | ||||
|     "webdriverio": "^4.14.0" | ||||
|   }, | ||||
|   "engines": { | ||||
|     "node": ">=4" | ||||
|   | ||||
							
								
								
									
										133
									
								
								red/api/admin/context.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								red/api/admin/context.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| /** | ||||
|  * Copyright JS Foundation and other contributors, http://js.foundation | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  **/ | ||||
|  | ||||
| var log; | ||||
| var redNodes; | ||||
| var util; | ||||
| var settings; | ||||
|  | ||||
| function exportContextStore(scope,ctx, store, result, callback) { | ||||
|     ctx.keys(store,function(err, keys) { | ||||
|         if (err) { | ||||
|             return callback(err); | ||||
|         } | ||||
|         result[store] = {}; | ||||
|         var c = keys.length; | ||||
|         if (c === 0) { | ||||
|             callback(null); | ||||
|         } else { | ||||
|             keys.forEach(function(key) { | ||||
|                 ctx.get(key,store,function(err, v) { | ||||
|                     if (err) { | ||||
|                         return callback(err); | ||||
|                     } | ||||
|                     if (scope !== 'global' || | ||||
|                         store === redNodes.listContextStores().default || | ||||
|                         !settings.hasOwnProperty("functionGlobalContext") || | ||||
|                         !settings.functionGlobalContext.hasOwnProperty(key) || | ||||
|                         settings.functionGlobalContext[key] !== v) { | ||||
|                             result[store][key] = util.encodeObject({msg:v}); | ||||
|                     } | ||||
|                     c--; | ||||
|                     if (c === 0) { | ||||
|                         callback(null); | ||||
|                     } | ||||
|                 }); | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     init: function(runtime) { | ||||
|         redNodes = runtime.nodes; | ||||
|         log = runtime.log; | ||||
|         util = runtime.util; | ||||
|         settings = runtime.settings; | ||||
|     }, | ||||
|  | ||||
|     get: function(req,res) { | ||||
|         var scope = req.params.scope; | ||||
|         var id = req.params.id; | ||||
|         var key = req.params[0]; | ||||
|         var availableStores = redNodes.listContextStores(); | ||||
|         //{ default: 'default', stores: [ 'default', 'file' ] } | ||||
|         var store = req.query['store']; | ||||
|         if (store && availableStores.stores.indexOf(store) === -1) { | ||||
|             return res.status(404).end(); | ||||
|         } | ||||
|         var ctx; | ||||
|         if (scope === 'global') { | ||||
|             ctx = redNodes.getContext('global'); | ||||
|         } else if (scope === 'flow') { | ||||
|             ctx = redNodes.getContext(id); | ||||
|         } else if (scope === 'node') { | ||||
|             var node = redNodes.getNode(id); | ||||
|             if (node) { | ||||
|                 ctx = node.context(); | ||||
|             } | ||||
|         } | ||||
|         if (ctx) { | ||||
|             if (key) { | ||||
|                 store = store || availableStores.default; | ||||
|                 ctx.get(key,store,function(err, v) { | ||||
|                     var encoded = util.encodeObject({msg:v}); | ||||
|                     if (store !== availableStores.default) { | ||||
|                         encoded.store = store; | ||||
|                     } | ||||
|                     res.json(encoded); | ||||
|                 }); | ||||
|                 return; | ||||
|             } else { | ||||
|                 var stores; | ||||
|                 if (!store) { | ||||
|                     stores = availableStores.stores; | ||||
|                 } else { | ||||
|                     stores = [store]; | ||||
|                 } | ||||
|  | ||||
|                 var result = {}; | ||||
|                 var c = stores.length; | ||||
|                 var errorReported = false; | ||||
|                 stores.forEach(function(store) { | ||||
|                     exportContextStore(scope,ctx,store,result,function(err) { | ||||
|                         if (err) { | ||||
|                             // TODO: proper error reporting | ||||
|                             if (!errorReported) { | ||||
|                                 errorReported = true; | ||||
|                                 res.end(400); | ||||
|                             } | ||||
|                             return; | ||||
|                         } | ||||
|                         c--; | ||||
|                         if (c === 0) { | ||||
|                             if (!errorReported) { | ||||
|                                 if (stores.length > 1 && scope === 'global') { | ||||
|                                 } | ||||
|                                 res.json(result); | ||||
|                             } | ||||
|                         } | ||||
|                     }); | ||||
|                 }) | ||||
|  | ||||
|  | ||||
|  | ||||
|             } | ||||
|         } else { | ||||
|             res.json({}); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -19,6 +19,7 @@ var express = require("express"); | ||||
| var nodes = require("./nodes"); | ||||
| var flows = require("./flows"); | ||||
| var flow = require("./flow"); | ||||
| var context = require("./context"); | ||||
| var auth = require("../auth"); | ||||
|  | ||||
| var apiUtil = require("../util"); | ||||
| @@ -28,6 +29,7 @@ module.exports = { | ||||
|         flows.init(runtime); | ||||
|         flow.init(runtime); | ||||
|         nodes.init(runtime); | ||||
|         context.init(runtime); | ||||
|  | ||||
|         var needsPermission = auth.needsPermission; | ||||
|  | ||||
| @@ -52,6 +54,12 @@ module.exports = { | ||||
|         adminApp.get(/\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.read"),nodes.getSet,apiUtil.errorHandler); | ||||
|         adminApp.put(/\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.write"),nodes.putSet,apiUtil.errorHandler); | ||||
|  | ||||
|         // Context | ||||
|         adminApp.get("/context/:scope(global)",needsPermission("context.read"),context.get,apiUtil.errorHandler); | ||||
|         adminApp.get("/context/:scope(global)/*",needsPermission("context.read"),context.get,apiUtil.errorHandler); | ||||
|         adminApp.get("/context/:scope(node|flow)/:id",needsPermission("context.read"),context.get,apiUtil.errorHandler); | ||||
|         adminApp.get("/context/:scope(node|flow)/:id/*",needsPermission("context.read"),context.get,apiUtil.errorHandler); | ||||
|  | ||||
|         return adminApp; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -102,9 +102,10 @@ module.exports = { | ||||
|                 var fullPath = redNodes.getNodeExampleFlowPath(module,path); | ||||
|                 if (fullPath) { | ||||
|                     try { | ||||
|                         fs.statSync(fullPath); | ||||
|                         var resolvedPath = fspath.resolve(fullPath); | ||||
|                         fs.statSync(resolvedPath); | ||||
|                         log.audit({event: "library.get",type:"flow",path:req.params[0]},req); | ||||
|                         return res.sendFile(fullPath,{ | ||||
|                         return res.sendFile(resolvedPath,{ | ||||
|                             headers:{ | ||||
|                                 'Content-Type': 'application/json' | ||||
|                             } | ||||
|   | ||||
| @@ -29,13 +29,13 @@ module.exports = { | ||||
|         var lngs = req.query.lng; | ||||
|         namespace = namespace.replace(/\.json$/,""); | ||||
|         var lang = req.query.lng; //apiUtil.determineLangFromHeaders(req.acceptsLanguages() || []); | ||||
|         var prevLang = i18n.i.lng(); | ||||
|         var prevLang = i18n.i.language; | ||||
|         // Trigger a load from disk of the language if it is not the default | ||||
|         i18n.i.setLng(lang, function(){ | ||||
|             var catalog = i18n.catalog(namespace,lang); | ||||
|         i18n.i.changeLanguage(lang, function(){ | ||||
|             var catalog = i18n.i.getResourceBundle(lang, namespace); | ||||
|             res.json(catalog||{}); | ||||
|         }); | ||||
|         i18n.i.setLng(prevLang); | ||||
|         i18n.i.changeLanguage(prevLang); | ||||
|  | ||||
|     }, | ||||
|     getAllNodes: function(req,res) { | ||||
| @@ -44,7 +44,7 @@ module.exports = { | ||||
|         var result = {}; | ||||
|         nodeList.forEach(function(n) { | ||||
|             if (n.module !== "node-red") { | ||||
|                 result[n.id] = i18n.catalog(n.id,lngs)||{}; | ||||
|                 result[n.id] = i18n.i.getResourceBundle(lngs, n.id)||{}; | ||||
|             } | ||||
|         }); | ||||
|         res.json(result); | ||||
|   | ||||
| @@ -265,6 +265,8 @@ | ||||
|         "settingIcon": "Icon", | ||||
|         "noDefaultLabel": "none", | ||||
|         "defaultLabel": "use default label", | ||||
|         "searchIcons": "Search icons", | ||||
|         "useDefault": "use default", | ||||
|         "errors": { | ||||
|             "scopeChange": "Changing the scope will make it unavailable to nodes in other flows that use it" | ||||
|         } | ||||
| @@ -306,13 +308,11 @@ | ||||
|         "savedNodes": "Saved nodes", | ||||
|         "savedType": "Saved __type__", | ||||
|         "saveFailed": "Save failed: __message__", | ||||
|  | ||||
|         "filename": "Filename", | ||||
|         "folder": "Folder", | ||||
|         "filenamePlaceholder": "file", | ||||
|         "fullFilenamePlaceholder": "a/b/file", | ||||
|         "folderPlaceholder": "a/b", | ||||
|  | ||||
|         "breadcrumb": "Library" | ||||
|     }, | ||||
|     "palette": { | ||||
| @@ -358,7 +358,6 @@ | ||||
|                 "monthsV_plural": "__count__ months ago", | ||||
|                 "yearsV": "__count__ year ago", | ||||
|                 "yearsV_plural": "__count__ years ago", | ||||
|  | ||||
|                 "yearMonthsV": "__y__ year, __count__ month ago", | ||||
|                 "yearMonthsV_plural": "__y__ year, __count__ months ago", | ||||
|                 "yearsMonthsV": "__y__ years, __count__ month ago", | ||||
| @@ -459,6 +458,16 @@ | ||||
|             "filterAll":"all", | ||||
|             "filtered": "__count__ hidden" | ||||
|         }, | ||||
|         "context": { | ||||
|             "name":"Context Data", | ||||
|             "label":"context", | ||||
|             "none": "none selected", | ||||
|             "refresh": "refresh to load", | ||||
|             "empty": "empty", | ||||
|             "node": "Node", | ||||
|             "flow": "Flow", | ||||
|             "global": "Global" | ||||
|         }, | ||||
|         "palette": { | ||||
|             "name": "Palette management", | ||||
|             "label": "palette" | ||||
| @@ -479,15 +488,14 @@ | ||||
|                 "install": "install", | ||||
|                 "removeFromProject": "remove from project", | ||||
|                 "addToProject": "add to project", | ||||
|                 "none": "None", | ||||
|                 "files": "Files", | ||||
|                 "flow": "Flow", | ||||
|                 "credentials": "Credentials", | ||||
|                 "invalidEncryptionKey": "Invalid encryption key", | ||||
|                 "encryptionEnabled": "Encryption enabled", | ||||
|                 "encryptionDisabled": "Encryption disabled", | ||||
|                 "resetTheEncryptionKey": "Reset the encryption key:", | ||||
|                 "setTheEncryptionKey": "Set the encryption key:", | ||||
|                 "resetTheEncryptionKey": "Reset the encryption key:", | ||||
|                 "changeTheEncryptionKey": "Change the encryption key:", | ||||
|                 "currentKey": "Current key", | ||||
|                 "newKey": "New key", | ||||
| @@ -610,7 +618,9 @@ | ||||
|             "bool": "boolean", | ||||
|             "json": "JSON", | ||||
|             "bin": "buffer", | ||||
|             "date": "timestamp" | ||||
|             "date": "timestamp", | ||||
|             "jsonata": "expression", | ||||
|             "env": "env variable" | ||||
|         } | ||||
|     }, | ||||
|     "editableList": { | ||||
| @@ -639,6 +649,9 @@ | ||||
|             "eval": "Error evaluating expression:\n  __message__" | ||||
|         } | ||||
|     }, | ||||
|     "jsEditor": { | ||||
|         "title": "JavaScript editor" | ||||
|     }, | ||||
|     "jsonEditor": { | ||||
|         "title": "JSON editor", | ||||
|         "format": "format JSON" | ||||
| @@ -701,7 +714,7 @@ | ||||
|             "ssh-key-add": "Add an ssh key", | ||||
|             "credential-key": "Credentials encryption key", | ||||
|             "cant-get-ssh-key": "Error! Can't get selected SSH key path.", | ||||
|             "already-exists": "already exists", | ||||
|             "already-exists2": "already exists", | ||||
|             "git-error": "git error", | ||||
|             "connection-failed": "Connection failed", | ||||
|             "not-git-repo": "Not a git repository", | ||||
| @@ -795,6 +808,7 @@ | ||||
|             "username": "Username", | ||||
|             "password": "Password", | ||||
|             "passphrase": "Passphrase", | ||||
|             "retry": "Retry", | ||||
|             "update-failed": "Failed to update auth", | ||||
|             "unhandled": "Unhandled error response" | ||||
|         }, | ||||
|   | ||||
| @@ -189,11 +189,11 @@ | ||||
|         "desc":"Returns an aggregated value derived from applying the `function` parameter successively to each value in `array` in combination with the result of the previous application of the function.\n\nThe function must accept two arguments, and behaves like an infix operator between each value within the `array`.\n\nThe optional `init` parameter is used as the initial value in the aggregation." | ||||
|     }, | ||||
|     "$flowContext": { | ||||
|         "args": "string", | ||||
|         "args": "string[, string]", | ||||
|         "desc": "Retrieves a flow context property.\n\nThis is a Node-RED defined function." | ||||
|     }, | ||||
|     "$globalContext": { | ||||
|         "args": "string", | ||||
|         "args": "string[, string]", | ||||
|         "desc": "Retrieves a global context property.\n\nThis is a Node-RED defined function." | ||||
|     }, | ||||
|     "$pad": { | ||||
|   | ||||
| @@ -98,10 +98,13 @@ | ||||
|             "nodeActionDisabled": "ノードのアクションは、サブフロー内で無効になっています", | ||||
|             "missing-types": "不明なノードが存在するため、フローを停止しました。詳細はログを確認してください。", | ||||
|             "restartRequired": "更新されたモジュールを有効化するため、Node-REDを再起動する必要があります", | ||||
|             "credentials_load_failed": "<p>認証情報を復号できないため、フローを停止しました</p><p>フローの認証情報は暗号化されています。しかし、プロジェクトの暗号鍵が存在しない、または不正です</p>", | ||||
|             "credentials_load_failed": "<p>認証情報を復号できないため、フローを停止しました</p><p>フローの認証情報ファイルは暗号化されています。しかし、プロジェクトの暗号鍵が存在しない、または不正です</p>", | ||||
|             "credentials_load_failed_reset": "<p>認証情報を復号できません</p><p>フローの認証情報ファイルは暗号化されています。しかし、プロジェクトの暗号鍵が存在しない、または不正です。</p><p>次回のデプロイでフローの認証情報ファイルがリセットされます。既存フローの認証情報は削除されます。</p>", | ||||
|             "missing_flow_file": "<p>プロジェクトのフローファイルが存在しません</p><p>本プロジェクトにフローファイルが登録されていません</p>", | ||||
|             "missing_package_file": "<p>プロジェクトのパッケージファイルが存在しません</p><p>本プロジェクトにはpackage.jsonファイルがありません</p>", | ||||
|             "project_empty": "<p>空のプロジェクトです</p><p>デフォルトのプロジェクトファイルを作成しますか?<br/>作成しない場合、エディタの外でファイルをプロジェクトへ手動で追加する必要があります</p>", | ||||
|             "project_not_found": "<p>プロジェクト '__project__' が存在しません</p>" | ||||
|             "project_not_found": "<p>プロジェクト '__project__' が存在しません</p>", | ||||
|             "git_merge_conflict": "<p>変更の自動マージが失敗しました</p><p>マージされていない競合を解決し、コミットしてください</p>" | ||||
|         }, | ||||
|         "error": "<strong>エラー</strong>: __message__", | ||||
|         "errors": { | ||||
| @@ -236,6 +239,7 @@ | ||||
|         "output": "出力:", | ||||
|         "deleteSubflow": "サブフローを削除", | ||||
|         "info": "詳細", | ||||
|         "category": "カテゴリ", | ||||
|         "format": "マークダウン形式", | ||||
|         "errors": { | ||||
|             "noNodesSelected": "<strong>サブフローを作成できません</strong>: ノードが選択されていません", | ||||
| @@ -260,6 +264,8 @@ | ||||
|         "settingIcon": "アイコン", | ||||
|         "noDefaultLabel": "なし", | ||||
|         "defaultLabel": "既定の名前を使用", | ||||
|         "searchIcons": "アイコンを検索", | ||||
|         "useDefault":  "デフォルトを使用", | ||||
|         "errors": { | ||||
|             "scopeChange": "スコープの変更は、他のフローで使われているノードを無効にします" | ||||
|         } | ||||
| @@ -312,6 +318,7 @@ | ||||
|         "noInfo": "情報がありません", | ||||
|         "filter": "ノードを検索", | ||||
|         "search": "ノードを検索", | ||||
|         "addCategory": "新規追加...", | ||||
|         "label": { | ||||
|             "subflows": "サブフロー", | ||||
|             "input": "入力", | ||||
| @@ -449,6 +456,16 @@ | ||||
|             "filterAll": "全て", | ||||
|             "filtered": "__count__ 個が無効" | ||||
|         }, | ||||
|         "context": { | ||||
|             "name": "コンテキストデータ", | ||||
|             "label": "コンテキストデータ", | ||||
|             "none": "選択されていません", | ||||
|             "refresh": "読み込みのため更新してください", | ||||
|             "empty": "データが存在しません", | ||||
|             "node": "Node", | ||||
|             "flow": "Flow", | ||||
|             "global": "Global" | ||||
|         }, | ||||
|         "palette": { | ||||
|             "name": "パレットの管理", | ||||
|             "label": "パレット" | ||||
| @@ -568,7 +585,6 @@ | ||||
|                 "pullUnrelatedHistory": "<p>リモートに関連のないコミット履歴があります。</p><p>本当に変更をプルしてローカルリポジトリに反映しますか?</p>", | ||||
|                 "pullChanges": "プル変更", | ||||
|                 "history": "履歴", | ||||
|                 "plural": "", | ||||
|                 "daysAgo": "__count__ 日前", | ||||
|                 "daysAgo_plural": "__count__ 日前", | ||||
|                 "hoursAgo": "__count__ 時間前", | ||||
| @@ -600,7 +616,9 @@ | ||||
|             "bool": "真偽", | ||||
|             "json": "JSON", | ||||
|             "bin": "バッファ", | ||||
|             "date": "日時" | ||||
|             "date": "日時", | ||||
|             "jsonata": "JSONata式", | ||||
|             "env": "環境変数" | ||||
|         } | ||||
|     }, | ||||
|     "editableList": { | ||||
| @@ -629,6 +647,9 @@ | ||||
|             "eval": "表現評価エラー:\n  __message__" | ||||
|         } | ||||
|     }, | ||||
|     "jsEditor": { | ||||
|         "title": "JavaScriptエディタ" | ||||
|     }, | ||||
|     "jsonEditor": { | ||||
|         "title": "JSONエディタ", | ||||
|         "format": "JSONフォーマット" | ||||
| @@ -691,7 +712,7 @@ | ||||
|             "ssh-key-add": "SSHキーの追加", | ||||
|             "credential-key": "認証情報の暗号化キー", | ||||
|             "cant-get-ssh-key": "エラー! 選択したSSHキーのパスを取得できません。", | ||||
|             "already-exists": "既に存在します", | ||||
|             "already-exists2": "既に存在します", | ||||
|             "git-error": "Gitエラー", | ||||
|             "connection-failed": "接続に失敗しました", | ||||
|             "not-git-repo": "Gitリポジトリではありません", | ||||
| @@ -785,6 +806,7 @@ | ||||
|             "username": "ユーザ名", | ||||
|             "password": "パスワード", | ||||
|             "passphrase": "パスフレーズ", | ||||
|             "retry": "リトライ", | ||||
|             "update-failed": "認証の更新に失敗しました", | ||||
|             "unhandled": "エラー応答が処理されませんでした" | ||||
|         }, | ||||
| @@ -798,7 +820,7 @@ | ||||
|             "no-empty": "デフォルトのファイル群を空でないプロジェクトに作成することはできません。", | ||||
|             "git-error": "Gitエラー" | ||||
|         }, | ||||
|         "errors" : { | ||||
|         "errors": { | ||||
|             "no-username-email": "Gitクライアントのユーザ名/emailが設定されていません。", | ||||
|             "unexpected": "予期しないエラーが発生しました", | ||||
|             "code": "コード" | ||||
|   | ||||
| @@ -214,5 +214,9 @@ | ||||
|     "$toMillis": { | ||||
|         "args": "timestamp", | ||||
|         "desc": "ISO 8601形式の文字列 `timestamp` を、Unixエポック(1 January, 1970 UTC)からの経過ミリ秒を表す数値へ変換します。 文字列が正しい形式でない場合、エラーとなります。" | ||||
|     }, | ||||
|     "$env": { | ||||
|         "args": "arg", | ||||
|         "desc": "環境変数の値を返します。\n\n本関数はNode-REDの定義関数です。" | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -40,6 +40,8 @@ module.exports = { | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|         safeSettings.context = runtime.nodes.listContextStores(); | ||||
|  | ||||
|         var themeSettings = theme.settings(); | ||||
|         if (themeSettings) { | ||||
|             safeSettings.editorTheme = themeSettings; | ||||
|   | ||||
| @@ -15,6 +15,7 @@ | ||||
|  **/ | ||||
|  | ||||
| var i18n = require("i18next"); | ||||
|  | ||||
| var when = require("when"); | ||||
| var path = require("path"); | ||||
| var fs = require("fs"); | ||||
| @@ -34,7 +35,7 @@ function registerMessageCatalogs(catalogs) { | ||||
| function registerMessageCatalog(namespace,dir,file) { | ||||
|     return when.promise(function(resolve,reject) { | ||||
|         resourceMap[namespace] = { basedir:dir, file:file}; | ||||
|         i18n.loadNamespace(namespace,function() { | ||||
|         i18n.loadNamespaces(namespace,function() { | ||||
|             resolve(); | ||||
|         }); | ||||
|     }); | ||||
| @@ -53,7 +54,9 @@ function mergeCatalog(fallback,catalog) { | ||||
| } | ||||
|  | ||||
| var MessageFileLoader = { | ||||
|     fetchOne: function(lng, ns, callback) { | ||||
|     type: "backend", | ||||
|     init: function(services, backendOptions, i18nextOptions) {}, | ||||
|     read: function(lng, ns, callback) { | ||||
|         if (resourceMap[ns]) { | ||||
|             var file = path.join(resourceMap[ns].basedir,lng,resourceMap[ns].file); | ||||
|             //console.log(file); | ||||
| @@ -94,13 +97,18 @@ function getCurrentLocale() { | ||||
|  | ||||
| function init() { | ||||
|     return when.promise(function(resolve,reject) { | ||||
|         i18n.backend(MessageFileLoader); | ||||
|         i18n.use(MessageFileLoader); | ||||
|         var opt = { | ||||
|             ns: { | ||||
|                 namespaces: [], | ||||
|                 defaultNs: "runtime" | ||||
|             }, | ||||
|             fallbackLng: [defaultLang] | ||||
|             // debug: true, | ||||
|             defaultNS: "runtime", | ||||
|             ns: [], | ||||
|             fallbackLng: defaultLang, | ||||
|             interpolation: { | ||||
|                 unescapeSuffix: 'HTML', | ||||
|                 escapeValue: false, | ||||
|                 prefix: '__', | ||||
|                 suffix: '__' | ||||
|             } | ||||
|         }; | ||||
|         var lang = getCurrentLocale(); | ||||
|         if (lang) { | ||||
| @@ -142,5 +150,6 @@ obj['_'] = function() { | ||||
|     //    opts.defaultValue = def; | ||||
|     //} | ||||
|     //console.log(arguments); | ||||
|     return i18n.t.apply(null,arguments); | ||||
|     var res = i18n.t.apply(i18n,arguments); | ||||
|     return res; | ||||
| } | ||||
|   | ||||
| @@ -162,10 +162,10 @@ function start() { | ||||
|                 if (settings.httpStatic) { | ||||
|                     log.info(log._("runtime.paths.httpStatic",{path:path.resolve(settings.httpStatic)})); | ||||
|                 } | ||||
|                 redNodes.loadFlows().then(redNodes.startFlows).catch(function(err) {}); | ||||
|                 started = true; | ||||
|             }).catch(function(err) { | ||||
|                 console.log(err); | ||||
|                 return redNodes.loadContextsPlugin().then(function () { | ||||
|                     redNodes.loadFlows().then(redNodes.startFlows).catch(function(err) {}); | ||||
|                     started = true; | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
| } | ||||
| @@ -229,7 +229,9 @@ function stop() { | ||||
|         clearTimeout(reinstallTimeout); | ||||
|     } | ||||
|     started = false; | ||||
|     return redNodes.stopFlows(); | ||||
|     return redNodes.stopFlows().then(function(){ | ||||
|         return redNodes.closeContextsPlugin(); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| var runtime = module.exports = { | ||||
|   | ||||
| @@ -155,5 +155,20 @@ | ||||
|                 "readme": "### About\n\nThis is your project's README.md file. It helps users understand what your\nproject does, how to use it and anything else they may need to know." | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     "context": { | ||||
|         "log-store-init": "Context store  : '__name__' [__info__]", | ||||
|         "error-loading-module": "Error loading context store '__module__': __message__ ", | ||||
|         "error-module-not-defined": "Context store '__storage__' missing 'module' option", | ||||
|         "error-invalid-module-name": "Invalid context store name: '__name__'", | ||||
|         "error-invalid-default-module": "Default context store unknown: '__storage__'", | ||||
|         "unknown-store": "Unknown context store '__name__' specified. Using default store.", | ||||
|         "error-loading-module": "Error loading context store: __message__", | ||||
|         "localfilesystem": { | ||||
|             "error-circular": "Context __scope__ contains a circular reference that cannot be persisted", | ||||
|             "error-write": "Error writing context: __message__" | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,169 @@ | ||||
| { | ||||
|     "runtime": { | ||||
|         "welcome": "Welcome to Node-RED", | ||||
|         "version": "__component__ バージョン: __version__", | ||||
|         "unsupported_version": "__component__ は未サポートのバージョンです。必要: __requires__ 検知: __version__", | ||||
|         "paths": { | ||||
|             "settings": "設定ファイル: __path__", | ||||
|             "httpStatic": "HTTP Static    : __path__" | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     "server": { | ||||
|         "loading": "パレットノードのロード", | ||||
|         "palette-editor": { | ||||
|             "disabled": "パレットエディタを無効化 : ユーザ設定", | ||||
|             "npm-not-found": "バレットエディタを無効化 : npmコマンドが見つかりません" | ||||
|         }, | ||||
|         "errors": "__count__ 個のノードの登録に失敗しました", | ||||
|         "errors_plural": "__count__ 個のノードの登録に失敗しました", | ||||
|         "errors-help": "詳細は -v を指定して実行してください", | ||||
|         "missing-modules": "不足しているノードモジュール:", | ||||
|         "node-version-mismatch": "ノードモジュールはこのバージョンではロードできません。必要なバージョン: __version__ ", | ||||
|         "type-already-registered": "'__type__' はモジュール __module__ で登録済みです", | ||||
|         "removing-modules": "設定からモジュールを削除します", | ||||
|         "added-types": "追加したノード:", | ||||
|         "removed-types": "削除したノード:", | ||||
|         "install": { | ||||
|             "invalid": "不正なモジュール名", | ||||
|             "installing": "モジュール__name__, バージョン: __version__をインスートールします", | ||||
|             "installed": "モジュール __name__ をインストールしました", | ||||
|             "install-failed": "インストールに失敗しました", | ||||
|             "install-failed-long": "モジュール __name__ のインストールに失敗しました:", | ||||
|             "install-failed-not-found": "$t(install-failed-long) モジュールが見つかりません", | ||||
|             "upgrading": "モジュール __name__ をバージョン __version__ に更新します", | ||||
|             "upgraded": "モジュール __name__ を更新しました。新しいバージョンを使うには、Node-REDを再起動してください。", | ||||
|             "upgrade-failed-not-found": "$t(server.install.install-failed-long) バージョンが見つかりません", | ||||
|             "install-failed-not-found": "$t(server.install.install-failed-long) モジュールが見つかりません", | ||||
|             "uninstalling": "モジュールをアンインストールします: __name__", | ||||
|             "uninstall-failed": "アンインストールに失敗しました", | ||||
|             "uninstall-failed-long": "モジュール __name__ のアンインストールに失敗しました:", | ||||
|             "uninstalled": "モジュール __name__ をアンインストールしました" | ||||
|         }, | ||||
|         "unable-to-listen": "__listenpath__ に対してlistenできません", | ||||
|         "port-in-use": "エラー: ポートが使用中です", | ||||
|         "uncaught-exception": "未補足の例外:", | ||||
|         "admin-ui-disabled": "管理UIを無効化しました", | ||||
|         "now-running": "サーバは __listenpath__ で実行中です", | ||||
|         "failed-to-start": "サーバの起動に失敗しました:", | ||||
|         "headless-mode": "ヘッドレスモードで実行中です", | ||||
|         "httpadminauth-deprecated": "httpAdminAuthは非推奨です。代わりに adminAuth を使用してください" | ||||
|     }, | ||||
|  | ||||
|     "api": { | ||||
|         "flows": { | ||||
|             "error-save": "フローの保存エラー: __message__", | ||||
|             "error-reload": "フローの読み込みエラー: __message__" | ||||
|         }, | ||||
|         "library": { | ||||
|             "error-load-entry": "ライブラリエントリ '__path__' の読み込みエラー: __message__", | ||||
|             "error-save-entry": "ライブラリエントリ  '__path__' の保存エラー: __message__", | ||||
|             "error-load-flow": "フロー '__path__' の読み込みエラー: __message__", | ||||
|             "error-save-flow": "フロー '__path__' の保存エラー: __message__" | ||||
|         }, | ||||
|         "nodes": { | ||||
|             "enabled": "ノードを有効化しました:", | ||||
|             "disabled": "ノードを無効化しました:", | ||||
|             "error-enable": "ノードの有効化に失敗しました:" | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     "comms": { | ||||
|         "error": "通信チャネルエラー: __message__", | ||||
|         "error-server": "サーバエラー: __message__", | ||||
|         "error-send": "送信エラー: __message__" | ||||
|     }, | ||||
|  | ||||
|     "settings": { | ||||
|         "user-not-available": "ユーザ設定を保存できません: __message__", | ||||
|         "not-available": "設定が利用できません", | ||||
|         "property-read-only": "プロパティ '__prop__' は読み出し専用です" | ||||
|     }, | ||||
|  | ||||
|     "nodes": { | ||||
|         "credentials": { | ||||
|             "error":"クレデンシャルの読み込みエラー: __message__", | ||||
|             "error-saving":"クレデンシャルの保存エラー: __message__", | ||||
|             "not-registered": "クレデンシャル '__type__' は登録されていません", | ||||
|             "system-key-warning": "\n\n---------------------------------------------------------------------\nフローのクレデンシャルファイルはシステム生成キーで暗号化されています。\n\nシステム生成キーを何らかの理由で失った場合、クレデンシャルファイルを\n復元することはできません。その場合、ファイルを削除してクレデンシャルを\n再入力しなければなりません。\n\n設定ファイル内で 'credentialSecret' オプションを使って独自キーを設定\nします。変更を次にデプロイする際、Node-REDは選択したキーを用いてクレ\nデンシャルを再暗号化します。 \n\n---------------------------------------------------------------------\n" | ||||
|         }, | ||||
|         "flows": { | ||||
|             "registered-missing": "欠落しているノードを登録します: __type__", | ||||
|             "error": "フローの読み込みエラー: __message__", | ||||
|             "starting-modified-nodes": "更新されたノードを開始します", | ||||
|             "starting-modified-flows": "更新されたフローを開始します", | ||||
|             "starting-flows": "フローを開始します", | ||||
|             "started-modified-nodes": "更新されたノードを開始しました", | ||||
|             "started-modified-flows": "更新されたフローを開始しました", | ||||
|             "started-flows": "フローを開始しました", | ||||
|             "stopping-modified-nodes": "更新されたノードを停止します", | ||||
|             "stopping-modified-flows": "更新されたフローを停止します", | ||||
|             "stopping-flows": "フローを停止します", | ||||
|             "stopped-modified-nodes": "更新されたノードを停止しました", | ||||
|             "stopped-modified-flows": "更新されたフローを停止しました", | ||||
|             "stopped-flows": "フローを停止しました", | ||||
|             "stopped": "停止しました", | ||||
|             "stopping-error": "ノードの停止に失敗しました: __message__", | ||||
|             "added-flow": "フローを追加します: __label__", | ||||
|             "updated-flow": "フローを更新しました: __label__", | ||||
|             "removed-flow": "フローを削除しました: __label__", | ||||
|             "missing-types": "欠落しているノードが登録されるのを待っています:", | ||||
|             "missing-type-provided": " - __type__ (npmモジュール __module__ からインストールされました)", | ||||
|             "missing-type-install-1": "欠落しているモジュールをインストールするには次のコマンドを実行してください:", | ||||
|             "missing-type-install-2": "コマンドの実行は次のディレクトリで行います:" | ||||
|         }, | ||||
|         "flow": { | ||||
|             "unknown-type": "不明なノード: __type__", | ||||
|             "missing-types": "欠落したノード", | ||||
|             "error-loop": "メッセージの例外補足回数が最大値を超えました" | ||||
|         }, | ||||
|         "index": { | ||||
|             "unrecognised-id": "不明なID: __id__", | ||||
|             "type-in-use": "ノードは使用中です: __msg__", | ||||
|             "unrecognised-module": "不明なモジュール: __module__" | ||||
|         }, | ||||
|         "registry": { | ||||
|             "localfilesystem": { | ||||
|                 "module-not-found": "モジュール '__module__' が見つかりません" | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|      | ||||
|     "storage": { | ||||
|         "index": { | ||||
|             "forbidden-flow-name": "不正なフロー名" | ||||
|         }, | ||||
|         "localfilesystem": { | ||||
|             "user-dir": "ユーザディレクトリ : __path__", | ||||
|             "flows-file": "フローファイル     : __path__", | ||||
|             "create": "__type__ ファイルを作成します", | ||||
|             "empty": "既存の __type__ ファイルが空です", | ||||
|             "invalid": "既存の __type__ ファイルはJSON形式ではありません", | ||||
|             "restore": " __type__ ファイルをバックアップ __path__ から復元します", | ||||
|             "restore-fail": "__type__ ファイルをバックアップから復元するのに失敗しました : __message__", | ||||
|             "fsync-fail": "ファイル __path__ のディスクへの書き出しに失敗しました : __message__", | ||||
|             "projects": { | ||||
|                 "changing-project": "プロジェクトを設定します : __project__", | ||||
|                 "active-project": "選択中のプロジェクト : __project__", | ||||
|                 "project-not-found": "プロジェクトが見つかりません : __project__", | ||||
|                 "no-active-project": "プロジェクトが選択されていません : デフォルトのフローファイルを使用します", | ||||
|                 "disabled": "プロジェクトは無効化されています : editorTheme.projects.enabled=false", | ||||
|                 "disabledNoFlag": "プロジェクトは無効化されています : 有効にするには editorTheme.projects.enabled=true を設定してください", | ||||
|                 "git-not-found": "プロジェクトは無効化されています : gitコマンドが見つかりません", | ||||
|                 "git-version-old": "プロジェクトは無効化されています : git __version__ はサポートされていません。2.xが必要です。", | ||||
|                 "summary": "Node-REDプロジェクト", | ||||
|                 "readme": "### 説明\nこれはプロジェクトのREADME.mdファイルです。このファイルには、\nプロジェクトの説明、利用方法、その他の情報を記載します。" | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     }, | ||||
|  | ||||
|     "context": { | ||||
|         "log-store-init": "コンテクストストア : '__name__' [__info__]", | ||||
|         "error-loading-module": "コンテクストストア '__module__' のロードでエラーが発生しました: __message__ ", | ||||
|         "error-module-not-defined": "コンテクストストア '__storage__' に 'module' オプションが指定されていません", | ||||
|         "error-invalid-module-name": "不正なコンテクストストア名: '__name__'", | ||||
|         "error-invalid-default-module": "デフォルトコンテクストストアが不明: '__storage__'", | ||||
|         "unknown-store": "不明なコンテクストストア '__name__' が指定されました。デフォルトストアを使用します。"         | ||||
|       } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -105,12 +105,12 @@ Node.prototype.close = function(removed) { | ||||
|     if (promises.length > 0) { | ||||
|         return when.settle(promises).then(function() { | ||||
|             if (this._context) { | ||||
|                  context.delete(this._alias||this.id,this.z); | ||||
|                return context.delete(this._alias||this.id,this.z); | ||||
|             } | ||||
|         }); | ||||
|     } else { | ||||
|         if (this._context) { | ||||
|              context.delete(this._alias||this.id,this.z); | ||||
|             return context.delete(this._alias||this.id,this.z); | ||||
|         } | ||||
|         return; | ||||
|     } | ||||
|   | ||||
| @@ -1,91 +0,0 @@ | ||||
| /** | ||||
|  * Copyright JS Foundation and other contributors, http://js.foundation | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  **/ | ||||
|  | ||||
| var clone = require("clone"); | ||||
| var when = require("when"); | ||||
| var util = require("../util"); | ||||
|  | ||||
| function createContext(id,seed) { | ||||
|     var data = seed || {}; | ||||
|     var obj = seed || {}; | ||||
|     obj.get = function get(key) { | ||||
|         return util.getMessageProperty(data,key); | ||||
|     }; | ||||
|     obj.set = function set(key, value) { | ||||
|         util.setMessageProperty(data,key,value); | ||||
|     } | ||||
|     obj.keys = function() { | ||||
|         var keysData = Object.keys(data); | ||||
|         if (seed == null) { | ||||
|             return keysData; | ||||
|         } else { | ||||
|             return keysData.filter(function (key) { | ||||
|                 return key !== "set" && key !== "get" && key !== "keys"; | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|     return obj; | ||||
| } | ||||
|  | ||||
| var contexts = {}; | ||||
| var globalContext = null; | ||||
|  | ||||
| function getContext(localId,flowId) { | ||||
|     var contextId = localId; | ||||
|     if (flowId) { | ||||
|         contextId = localId+":"+flowId; | ||||
|     } | ||||
|     if (contexts.hasOwnProperty(contextId)) { | ||||
|         return contexts[contextId]; | ||||
|     } | ||||
|     var newContext = createContext(contextId); | ||||
|     if (flowId) { | ||||
|         newContext.flow = getContext(flowId); | ||||
|     } | ||||
|     if (globalContext) { | ||||
|         newContext.global = globalContext; | ||||
|     } | ||||
|     contexts[contextId] = newContext; | ||||
|     return newContext; | ||||
| } | ||||
| function deleteContext(id,flowId) { | ||||
|     var contextId = id; | ||||
|     if (flowId) { | ||||
|         contextId = id+":"+flowId; | ||||
|     } | ||||
|     delete contexts[contextId]; | ||||
| } | ||||
| function clean(flowConfig) { | ||||
|     var activeIds = {}; | ||||
|     var contextId; | ||||
|     var node; | ||||
|     for (var id in contexts) { | ||||
|         if (contexts.hasOwnProperty(id)) { | ||||
|             var idParts = id.split(":"); | ||||
|             if (!flowConfig.allNodes.hasOwnProperty(idParts[0])) { | ||||
|                 delete contexts[id]; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| module.exports = { | ||||
|     init: function(settings) { | ||||
|         globalContext = createContext("global",settings.functionGlobalContext || {}); | ||||
|     }, | ||||
|     get: getContext, | ||||
|     delete: deleteContext, | ||||
|     clean:clean | ||||
| }; | ||||
							
								
								
									
										416
									
								
								red/runtime/nodes/context/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										416
									
								
								red/runtime/nodes/context/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,416 @@ | ||||
| /** | ||||
|  * Copyright JS Foundation and other contributors, http://js.foundation | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  **/ | ||||
|  | ||||
| var clone = require("clone"); | ||||
| var log = require("../../log"); | ||||
| var memory = require("./memory"); | ||||
| var util = require("../../util"); | ||||
|  | ||||
| var settings; | ||||
|  | ||||
| // A map of scope id to context instance | ||||
| var contexts = {}; | ||||
|  | ||||
| // A map of store name to instance | ||||
| var stores = {}; | ||||
| var storeList = []; | ||||
| var defaultStore; | ||||
|  | ||||
| // Whether there context storage has been configured or left as default | ||||
| var hasConfiguredStore = false; | ||||
|  | ||||
| // Unknown Stores | ||||
| var unknownStores = {}; | ||||
|  | ||||
| function logUnknownStore(name) { | ||||
|     if (name) { | ||||
|         var count = unknownStores[name] || 0; | ||||
|         if (count == 0) { | ||||
|             log.warn(log._("context.unknown-store", {name: name})); | ||||
|             count++; | ||||
|             unknownStores[name] = count; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| function init(_settings) { | ||||
|     settings = _settings; | ||||
|     contexts = {}; | ||||
|     stores = {}; | ||||
|     storeList = []; | ||||
|     hasConfiguredStore = false; | ||||
|     var seed = settings.functionGlobalContext || {}; | ||||
|     contexts['global'] = createContext("global",seed); | ||||
|     // create a default memory store - used by the unit tests that skip the full | ||||
|     // `load()` initialisation sequence. | ||||
|     // If the user has any stores configured, this will be disgarded | ||||
|     stores["_"] = new memory(); | ||||
|     defaultStore = "memory"; | ||||
| } | ||||
|  | ||||
| function load() { | ||||
|     return new Promise(function(resolve,reject) { | ||||
|         // load & init plugins in settings.contextStorage | ||||
|         var plugins = settings.contextStorage || {}; | ||||
|         var defaultIsAlias = false; | ||||
|         var promises = []; | ||||
|         if (plugins && Object.keys(plugins).length > 0) { | ||||
|             var hasDefault = plugins.hasOwnProperty('default'); | ||||
|             var defaultName; | ||||
|             for (var pluginName in plugins) { | ||||
|                 if (plugins.hasOwnProperty(pluginName)) { | ||||
|                     // "_" is a reserved name - do not allow it to be overridden | ||||
|                     if (pluginName === "_") { | ||||
|                         continue; | ||||
|                     } | ||||
|                     if (!/^[a-zA-Z0-9_]+$/.test(pluginName)) { | ||||
|                         return reject(new Error(log._("context.error-invalid-module-name", {name:pluginName}))); | ||||
|                     } | ||||
|  | ||||
|                     // Check if this is setting the 'default' context to be a named plugin | ||||
|                     if (pluginName === "default" && typeof plugins[pluginName] === "string") { | ||||
|                         // Check the 'default' alias exists before initialising anything | ||||
|                         if (!plugins.hasOwnProperty(plugins[pluginName])) { | ||||
|                             return reject(new Error(log._("context.error-invalid-default-module", {storage:plugins["default"]}))); | ||||
|                         } | ||||
|                         defaultIsAlias = true; | ||||
|                         continue; | ||||
|                     } | ||||
|                     if (!hasDefault && !defaultName) { | ||||
|                         defaultName = pluginName; | ||||
|                     } | ||||
|                     var plugin; | ||||
|                     if (plugins[pluginName].hasOwnProperty("module")) { | ||||
|                         // Get the provided config and copy in the 'approved' top-level settings (eg userDir) | ||||
|                         var config = plugins[pluginName].config || {}; | ||||
|                         copySettings(config, settings); | ||||
|  | ||||
|                         if (typeof plugins[pluginName].module === "string") { | ||||
|                             // This config identifies the module by name - assume it is a built-in one | ||||
|                             // TODO: check it exists locally, if not, try to require it as-is | ||||
|                             try { | ||||
|                                 plugin = require("./"+plugins[pluginName].module); | ||||
|                             } catch(err) { | ||||
|                                 return reject(new Error(log._("context.error-loading-module", {module:plugins[pluginName].module,message:err.toString()}))); | ||||
|                             } | ||||
|                         } else { | ||||
|                             // Assume `module` is an already-required module we can use | ||||
|                             plugin = plugins[pluginName].module; | ||||
|                         } | ||||
|                         try { | ||||
|                             // Create a new instance of the plugin by calling its module function | ||||
|                             stores[pluginName] = plugin(config); | ||||
|                             var moduleInfo = plugins[pluginName].module; | ||||
|                             if (typeof moduleInfo !== 'string') { | ||||
|                                 if (moduleInfo.hasOwnProperty("toString")) { | ||||
|                                     moduleInfo = moduleInfo.toString(); | ||||
|                                 } else { | ||||
|                                     moduleInfo = "custom"; | ||||
|                                 } | ||||
|                             } | ||||
|                             log.info(log._("context.log-store-init", {name:pluginName, info:"module="+moduleInfo})); | ||||
|                         } catch(err) { | ||||
|                             return reject(new Error(log._("context.error-loading-module",{module:pluginName,message:err.toString()}))); | ||||
|                         } | ||||
|                     } else { | ||||
|                         // Plugin does not specify a 'module' | ||||
|                         return reject(new Error(log._("context.error-module-not-defined", {storage:pluginName}))); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Open all of the configured contexts | ||||
|             for (var plugin in stores) { | ||||
|                 if (stores.hasOwnProperty(plugin)) { | ||||
|                     promises.push(stores[plugin].open()); | ||||
|                 } | ||||
|             } | ||||
|             // There is a 'default' listed in the configuration | ||||
|             if (hasDefault) { | ||||
|                 // If 'default' is an alias, point it at the right module - we have already | ||||
|                 // checked that it exists. If it isn't an alias, then it will | ||||
|                 // already be set to a configured store | ||||
|                 if (defaultIsAlias) { | ||||
|                     stores["_"] =  stores[plugins["default"]]; | ||||
|                     defaultStore = plugins["default"]; | ||||
|                 } else { | ||||
|                     stores["_"] = stores["default"]; | ||||
|                     defaultStore = "default"; | ||||
|                 } | ||||
|             } else if (defaultName) { | ||||
|                 // No 'default' listed, so pick first in list as the default | ||||
|                 stores["_"] = stores[defaultName]; | ||||
|                 defaultStore = defaultName; | ||||
|                 defaultIsAlias = true; | ||||
|             } else { | ||||
|                 // else there were no stores list the config object - fall through | ||||
|                 // to below where we default to a memory store | ||||
|                 storeList = ["memory"]; | ||||
|                 defaultStore = "memory"; | ||||
|             } | ||||
|             hasConfiguredStore = true; | ||||
|             storeList = Object.keys(stores).filter(n=>!(defaultIsAlias && n==="default") && n!== "_"); | ||||
|         } else { | ||||
|             // No configured plugins | ||||
|             log.info(log._("context.log-store-init", {name:"default", info:"module=memory"})); | ||||
|             promises.push(stores["_"].open()) | ||||
|             storeList = ["memory"]; | ||||
|             defaultStore = "memory"; | ||||
|         } | ||||
|         return resolve(Promise.all(promises)); | ||||
|     }).catch(function(err) { | ||||
|         throw new Error(log._("context.error-loading-module",{message:err.toString()})); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function copySettings(config, settings){ | ||||
|     var copy = ["userDir"] | ||||
|     config.settings = {}; | ||||
|     copy.forEach(function(setting){ | ||||
|         config.settings[setting] = clone(settings[setting]); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function getContextStorage(storage) { | ||||
|     if (stores.hasOwnProperty(storage)) { | ||||
|         // A known context | ||||
|         return stores[storage]; | ||||
|     } else if (stores.hasOwnProperty("_")) { | ||||
|         // Not known, but we have a default to fall back to | ||||
|         if (storage !== defaultStore) { | ||||
|             // It isn't the default store either, so log it | ||||
|             logUnknownStore(storage); | ||||
|         } | ||||
|         return stores["_"]; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| function createContext(id,seed) { | ||||
|     // Seed is only set for global context - sourced from functionGlobalContext | ||||
|     var scope = id; | ||||
|     var obj = seed || {}; | ||||
|     var seedKeys; | ||||
|     var insertSeedValues; | ||||
|     if (seed) { | ||||
|         seedKeys = Object.keys(seed); | ||||
|         insertSeedValues = function(keys,values) { | ||||
|             if (!Array.isArray(keys)) { | ||||
|                 if (values[0] === undefined) { | ||||
|                     values[0] = util.getObjectProperty(seed,keys); | ||||
|                 } | ||||
|             } else { | ||||
|                 for (var i=0;i<keys.length;i++) { | ||||
|                     if (values[i] === undefined) { | ||||
|                         values[i] = util.getObjectProperty(seed,keys[i]); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     Object.defineProperties(obj, { | ||||
|         get: { | ||||
|             value: function(key, storage, callback) { | ||||
|                 var context; | ||||
|  | ||||
|                 if (!callback && typeof storage === 'function') { | ||||
|                     callback = storage; | ||||
|                     storage = undefined; | ||||
|                 } | ||||
|                 if (callback && typeof callback !== 'function'){ | ||||
|                     throw new Error("Callback must be a function"); | ||||
|                 } | ||||
|  | ||||
|                 if (!Array.isArray(key)) { | ||||
|                     var keyParts = util.parseContextStore(key); | ||||
|                     key = keyParts.key; | ||||
|                     if (!storage) { | ||||
|                         storage = keyParts.store || "_"; | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (!storage) { | ||||
|                         storage = "_"; | ||||
|                     } | ||||
|                 } | ||||
|                 context = getContextStorage(storage); | ||||
|  | ||||
|                 if (callback) { | ||||
|                     if (!seed) { | ||||
|                         context.get(scope,key,callback); | ||||
|                     } else { | ||||
|                         context.get(scope,key,function() { | ||||
|                             if (arguments[0]) { | ||||
|                                 callback(arguments[0]); | ||||
|                                 return; | ||||
|                             } | ||||
|                             var results = Array.prototype.slice.call(arguments,[1]); | ||||
|                             insertSeedValues(key,results); | ||||
|                             // Put the err arg back | ||||
|                             results.unshift(undefined); | ||||
|                             callback.apply(null,results); | ||||
|                         }); | ||||
|                     } | ||||
|                 } else { | ||||
|                     // No callback, attempt to do this synchronously | ||||
|                     var results = context.get(scope,key); | ||||
|                     if (seed) { | ||||
|                         if (Array.isArray(key)) { | ||||
|                             insertSeedValues(key,results); | ||||
|                         } else if (results === undefined){ | ||||
|                             results = util.getObjectProperty(seed,key); | ||||
|                         } | ||||
|                     } | ||||
|                     return results; | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         set: { | ||||
|             value: function(key, value, storage, callback) { | ||||
|                 var context; | ||||
|  | ||||
|                 if (!callback && typeof storage === 'function') { | ||||
|                     callback = storage; | ||||
|                     storage = undefined; | ||||
|                 } | ||||
|                 if (callback && typeof callback !== 'function'){ | ||||
|                     throw new Error("Callback must be a function"); | ||||
|                 } | ||||
|  | ||||
|                 if (!Array.isArray(key)) { | ||||
|                     var keyParts = util.parseContextStore(key); | ||||
|                     key = keyParts.key; | ||||
|                     if (!storage) { | ||||
|                         storage = keyParts.store || "_"; | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (!storage) { | ||||
|                         storage = "_"; | ||||
|                     } | ||||
|                 } | ||||
|                 context = getContextStorage(storage); | ||||
|  | ||||
|                 context.set(scope, key, value, callback); | ||||
|             } | ||||
|         }, | ||||
|         keys: { | ||||
|             value: function(storage, callback) { | ||||
|                 var context; | ||||
|                 if (!storage && !callback) { | ||||
|                     context = stores["_"]; | ||||
|                 } else { | ||||
|                     if (typeof storage === 'function') { | ||||
|                         callback = storage; | ||||
|                         storage = "_"; | ||||
|                     } | ||||
|                     if (callback && typeof callback !== 'function') { | ||||
|                         throw new Error("Callback must be a function"); | ||||
|                     } | ||||
|                     context = getContextStorage(storage); | ||||
|                 } | ||||
|                 if (seed) { | ||||
|                     if (callback) { | ||||
|                         context.keys(scope, function(err,keys) { | ||||
|                             callback(err,Array.from(new Set(seedKeys.concat(keys)).keys())); | ||||
|                         }); | ||||
|                     } else { | ||||
|                         var keys = context.keys(scope); | ||||
|                         return Array.from(new Set(seedKeys.concat(keys)).keys()) | ||||
|                     } | ||||
|                 } else { | ||||
|                     return context.keys(scope, callback); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|     return obj; | ||||
| } | ||||
|  | ||||
| function getContext(localId,flowId) { | ||||
|     var contextId = localId; | ||||
|     if (flowId) { | ||||
|         contextId = localId+":"+flowId; | ||||
|     } | ||||
|     if (contexts.hasOwnProperty(contextId)) { | ||||
|         return contexts[contextId]; | ||||
|     } | ||||
|     var newContext = createContext(contextId); | ||||
|     if (flowId) { | ||||
|         Object.defineProperty(newContext, 'flow', { | ||||
|             value: getContext(flowId) | ||||
|         }); | ||||
|     } | ||||
|     Object.defineProperty(newContext, 'global', { | ||||
|         value: contexts['global'] | ||||
|     }) | ||||
|     contexts[contextId] = newContext; | ||||
|     return newContext; | ||||
| } | ||||
|  | ||||
| function deleteContext(id,flowId) { | ||||
|     if(!hasConfiguredStore){ | ||||
|         // only delete context if there's no configured storage. | ||||
|         var contextId = id; | ||||
|         if (flowId) { | ||||
|             contextId = id+":"+flowId; | ||||
|         } | ||||
|         delete contexts[contextId]; | ||||
|         return stores["_"].delete(contextId); | ||||
|     }else{ | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function clean(flowConfig) { | ||||
|     var promises = []; | ||||
|     for(var plugin in stores){ | ||||
|         if(stores.hasOwnProperty(plugin)){ | ||||
|             promises.push(stores[plugin].clean(Object.keys(flowConfig.allNodes))); | ||||
|         } | ||||
|     } | ||||
|     for (var id in contexts) { | ||||
|         if (contexts.hasOwnProperty(id) && id !== "global") { | ||||
|             var idParts = id.split(":"); | ||||
|             if (!flowConfig.allNodes.hasOwnProperty(idParts[0])) { | ||||
|                 delete contexts[id]; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     return Promise.all(promises); | ||||
| } | ||||
|  | ||||
| function close() { | ||||
|     var promises = []; | ||||
|     for(var plugin in stores){ | ||||
|         if(stores.hasOwnProperty(plugin)){ | ||||
|             promises.push(stores[plugin].close()); | ||||
|         } | ||||
|     } | ||||
|     return Promise.all(promises); | ||||
| } | ||||
|  | ||||
| function listStores() { | ||||
|     return {default:defaultStore,stores:storeList}; | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     init: init, | ||||
|     load: load, | ||||
|     listStores: listStores, | ||||
|     get: getContext, | ||||
|     delete: deleteContext, | ||||
|     clean: clean, | ||||
|     close: close | ||||
| }; | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user