mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	Compare commits
	
		
			55 Commits
		
	
	
		
			v0.24.1-be
			...
			v0.24.5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | a616739805 | ||
|  | bea28de6a0 | ||
|  | 4e198ca2f0 | ||
|  | 2fbd16a0e3 | ||
|  | 76fc49f037 | ||
|  | 139c99440f | ||
|  | 137b9dfa0b | ||
|  | 5f0fdd15eb | ||
|  | 61e1427b83 | ||
|  | b3aa0ba47c | ||
|  | 56e2b44c25 | ||
|  | 4d5a17583f | ||
|  | 71eda5aa3d | ||
|  | 0711ea8dc8 | ||
|  | be206872d1 | ||
|  | fcf3fe8dcd | ||
|  | 62dbd4062a | ||
|  | 196e8b4380 | ||
|  | 551e1255ff | ||
|  | e09b61d1ac | ||
|  | ee23bcc783 | ||
|  | 3e351bd8d3 | ||
|  | 0d3bc22d73 | ||
|  | d82898421e | ||
|  | 1db2f0c2c5 | ||
|  | 6cd8a2203e | ||
|  | 08e062ab34 | ||
|  | 3a06493459 | ||
|  | 8159564885 | ||
|  | 8ce3c1a480 | ||
|  | dbc93f4a79 | ||
|  | 92ffe321aa | ||
|  | 6cb7d0098e | ||
|  | bdcb4361b2 | ||
|  | 15366d37d7 | ||
|  | acd001501b | ||
|  | 0019865807 | ||
|  | 137ffcc4e3 | ||
|  | 585398ad5c | ||
|  | 50401954d1 | ||
|  | 32a9df8489 | ||
|  | 5bf5d1cac4 | ||
|  | 3608857f25 | ||
|  | 16a1dc12df | ||
|  | 9c834229b9 | ||
|  | 3fd45b15e7 | ||
|  | f20ab45576 | ||
|  | 77a89d85c8 | ||
|  | 30249a353e | ||
|  | eb9bae9010 | ||
|  | 0c7ae527c5 | ||
|  | fef4705e2f | ||
|  | 568c2c997f | ||
|  | d6b5cd6ead | ||
|  | 00ce379962 | 
| @@ -18,6 +18,7 @@ See other pictures in [screenshot tour](https://github.com/zadam/trilium/wiki/Sc | |||||||
| * Note [attributes](https://github.com/zadam/trilium/wiki/Attributes) can be used for note organization, querying and advanced [scripting](https://github.com/zadam/trilium/wiki/Scripts) | * Note [attributes](https://github.com/zadam/trilium/wiki/Attributes) can be used for note organization, querying and advanced [scripting](https://github.com/zadam/trilium/wiki/Scripts) | ||||||
| * [Synchronization](https://github.com/zadam/trilium/wiki/Synchronization) with self-hosted sync server | * [Synchronization](https://github.com/zadam/trilium/wiki/Synchronization) with self-hosted sync server | ||||||
| * Strong [note encryption](https://github.com/zadam/trilium/wiki/Protected-notes) | * Strong [note encryption](https://github.com/zadam/trilium/wiki/Protected-notes) | ||||||
|  | * [Relation maps](https://github.com/zadam/trilium/wiki/Relation-map) for visualizing notes and their relations | ||||||
| * [Scripting](https://github.com/zadam/trilium/wiki/Scripts) - see [Advanced showcases](https://github.com/zadam/trilium/wiki/Advanced-showcases) | * [Scripting](https://github.com/zadam/trilium/wiki/Scripts) - see [Advanced showcases](https://github.com/zadam/trilium/wiki/Advanced-showcases) | ||||||
| * Scales well in both usability and performance upwards of 100 000 notes | * Scales well in both usability and performance upwards of 100 000 notes | ||||||
| * [Night theme](https://github.com/zadam/trilium/wiki/Themes) | * [Night theme](https://github.com/zadam/trilium/wiki/Themes) | ||||||
| @@ -31,10 +32,6 @@ Trilium is provided as either desktop application ([Electron](https://electronjs | |||||||
| * If you want to install Trilium on server, follow [this page](https://github.com/zadam/trilium/wiki/Server-installation). | * If you want to install Trilium on server, follow [this page](https://github.com/zadam/trilium/wiki/Server-installation). | ||||||
|   * Currently only recent Chrome and Firefox are supported (tested) browsers. |   * Currently only recent Chrome and Firefox are supported (tested) browsers. | ||||||
|  |  | ||||||
| ## Status |  | ||||||
|  |  | ||||||
| Trilium is beta quality software. While it is reasonably feature complete and is tested by its author, it lacks proper testing by more users. It's not yet recommended for daily use, but testing and experimentation is encouraged. |  | ||||||
|  |  | ||||||
| ## Documentation | ## Documentation | ||||||
|  |  | ||||||
| [See wiki for complete list of documentation pages.](https://github.com/zadam/trilium/wiki/) | [See wiki for complete list of documentation pages.](https://github.com/zadam/trilium/wiki/) | ||||||
							
								
								
									
										12
									
								
								bin/build.sh
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								bin/build.sh
									
									
									
									
									
								
							| @@ -11,15 +11,21 @@ rm -r dist/* | |||||||
| echo "Rebuilding binaries for linux-ia32" | echo "Rebuilding binaries for linux-ia32" | ||||||
| ./node_modules/.bin/electron-rebuild --arch=ia32 | ./node_modules/.bin/electron-rebuild --arch=ia32 | ||||||
|  |  | ||||||
| ./node_modules/.bin/electron-packager . --out=dist --platform=linux --arch=ia32 --overwrite | ./node_modules/.bin/electron-packager . --out=dist --executable-name=trilium --platform=linux --arch=ia32 --overwrite | ||||||
|  |  | ||||||
| ./node_modules/.bin/electron-packager . --out=dist --platform=win32 --arch=x64 --overwrite | mv "./dist/Trilium Notes-linux-ia32" ./dist/trilium-linux-ia32 | ||||||
|  |  | ||||||
|  | ./node_modules/.bin/electron-packager . --out=dist --executable-name=trilium --platform=win32  --arch=x64 --overwrite --icon=src/public/images/app-icons/win/icon.ico | ||||||
|  |  | ||||||
|  | mv "./dist/Trilium Notes-win32-x64" ./dist/trilium-win32-x64 | ||||||
|  |  | ||||||
| # we build x64 as second so that we keep X64 binaries in node_modules for local development and server build | # we build x64 as second so that we keep X64 binaries in node_modules for local development and server build | ||||||
| echo "Rebuilding binaries for linux-x64" | echo "Rebuilding binaries for linux-x64" | ||||||
| ./node_modules/.bin/electron-rebuild --arch=x64 | ./node_modules/.bin/electron-rebuild --arch=x64 | ||||||
|  |  | ||||||
| ./node_modules/.bin/electron-packager . --out=dist --platform=linux --arch=x64 --overwrite | ./node_modules/.bin/electron-packager . --out=dist --executable-name=trilium --platform=linux --arch=x64 --overwrite | ||||||
|  |  | ||||||
|  | mv "./dist/Trilium Notes-linux-x64" ./dist/trilium-linux-x64 | ||||||
|  |  | ||||||
| echo "Copying required windows binaries" | echo "Copying required windows binaries" | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								db/demo.tar
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								db/demo.tar
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								db/migrations/0119__rename_mirror_to_inverse.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								db/migrations/0119__rename_mirror_to_inverse.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | UPDATE attributes SET value = replace(value, 'mirrorRelation', 'inverseRelation') WHERE type = 'relation-definition'; | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | UPDATE attributes SET name = 'archived' where name = 'hideInAutocomplete'; | ||||||
| @@ -288,7 +288,7 @@ | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -730,7 +730,7 @@ | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -3814,7 +3814,7 @@ transactional by default. | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -511,7 +511,7 @@ Each note can have multiple (at least one) branches, meaning it can be placed in | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -216,7 +216,7 @@ | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -358,7 +358,7 @@ this is different concept than attribute/relation.</div> | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -7297,7 +7297,7 @@ Cache is note instance scoped. | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -403,7 +403,7 @@ | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -311,7 +311,7 @@ | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -288,7 +288,7 @@ | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -75,7 +75,7 @@ module.exports = ApiToken;</code></pre> | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -151,7 +151,7 @@ module.exports = Attribute;</code></pre> | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -105,7 +105,7 @@ module.exports = Branch;</code></pre> | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -93,7 +93,7 @@ module.exports = Entity;</code></pre> | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -92,7 +92,7 @@ module.exports = Link;</code></pre> | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -651,7 +651,7 @@ module.exports = Note;</code></pre> | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -91,7 +91,7 @@ module.exports = NoteRevision;</code></pre> | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -78,7 +78,7 @@ module.exports = Option;</code></pre> | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -75,7 +75,7 @@ module.exports = RecentNote;</code></pre> | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -594,7 +594,7 @@ | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -56,7 +56,7 @@ | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -278,7 +278,7 @@ module.exports = BackendScriptApi;</code></pre> | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -719,7 +719,7 @@ | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -2846,7 +2846,7 @@ Internally this serializes the anonymous function into string and sends it to ba | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -279,7 +279,7 @@ | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -1316,7 +1316,7 @@ Its notable omission is the note content.</div> | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -76,7 +76,7 @@ export default Branch;</code></pre> | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -64,7 +64,7 @@ export default NoteFull;</code></pre> | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -128,7 +128,7 @@ export default NoteShort;</code></pre> | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -339,7 +339,7 @@ | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -56,7 +56,7 @@ | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -271,7 +271,7 @@ export default FrontendScriptApi;</code></pre> | |||||||
| <br class="clear"> | <br class="clear"> | ||||||
|  |  | ||||||
| <footer> | <footer> | ||||||
|     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time) |     Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> | ||||||
| </footer> | </footer> | ||||||
|  |  | ||||||
| <script> prettyPrint(); </script> | <script> prettyPrint(); </script> | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ const log = require('./src/services/log'); | |||||||
| const cls = require('./src/services/cls'); | const cls = require('./src/services/cls'); | ||||||
| const url = require("url"); | const url = require("url"); | ||||||
| const port = require('./src/services/port'); | const port = require('./src/services/port'); | ||||||
|  | const appIconService = require('./src/services/app_icon'); | ||||||
|  |  | ||||||
| const app = electron.app; | const app = electron.app; | ||||||
| const globalShortcut = electron.globalShortcut; | const globalShortcut = electron.globalShortcut; | ||||||
| @@ -13,6 +14,8 @@ const globalShortcut = electron.globalShortcut; | |||||||
| // Adds debug features like hotkeys for triggering dev tools and reload | // Adds debug features like hotkeys for triggering dev tools and reload | ||||||
| require('electron-debug')(); | require('electron-debug')(); | ||||||
|  |  | ||||||
|  | appIconService.installLocalAppIcon(); | ||||||
|  |  | ||||||
| // Prevent window being garbage collected | // Prevent window being garbage collected | ||||||
| let mainWindow; | let mainWindow; | ||||||
|  |  | ||||||
| @@ -70,6 +73,8 @@ app.on('activate', () => { | |||||||
| }); | }); | ||||||
|  |  | ||||||
| app.on('ready', async () => { | app.on('ready', async () => { | ||||||
|  |     app.setAppUserModelId('com.github.zadam.trilium'); | ||||||
|  |  | ||||||
|     mainWindow = await createMainWindow(); |     mainWindow = await createMainWindow(); | ||||||
|  |  | ||||||
|     const result = globalShortcut.register('CommandOrControl+Alt+P', cls.wrap(async () => { |     const result = globalShortcut.register('CommandOrControl+Alt+P', cls.wrap(async () => { | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								jsdoc-conf.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								jsdoc-conf.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | { | ||||||
|  |   "templates": { | ||||||
|  |     "default": { | ||||||
|  |       "includeDate": false | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										23
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "trilium", |   "name": "trilium", | ||||||
|   "version": "0.24.0-beta", |   "version": "0.24.4-beta", | ||||||
|   "lockfileVersion": 1, |   "lockfileVersion": 1, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
| @@ -6417,11 +6417,18 @@ | |||||||
|       "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" |       "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" | ||||||
|     }, |     }, | ||||||
|     "mime-types": { |     "mime-types": { | ||||||
|       "version": "2.1.20", |       "version": "2.1.21", | ||||||
|       "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", |       "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", | ||||||
|       "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", |       "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", | ||||||
|       "requires": { |       "requires": { | ||||||
|         "mime-db": "~1.36.0" |         "mime-db": "~1.37.0" | ||||||
|  |       }, | ||||||
|  |       "dependencies": { | ||||||
|  |         "mime-db": { | ||||||
|  |           "version": "1.37.0", | ||||||
|  |           "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", | ||||||
|  |           "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "mimic-fn": { |     "mimic-fn": { | ||||||
| @@ -10821,9 +10828,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "ws": { |     "ws": { | ||||||
|       "version": "6.1.0", |       "version": "6.1.2", | ||||||
|       "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.0.tgz", |       "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.2.tgz", | ||||||
|       "integrity": "sha512-H3dGVdGvW2H8bnYpIDc3u3LH8Wue3Qh+Zto6aXXFzvESkTVT6rAfKR6tR/+coaUvxs8yHtmNV0uioBF62ZGSTg==", |       "integrity": "sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw==", | ||||||
|       "requires": { |       "requires": { | ||||||
|         "async-limiter": "~1.0.0" |         "async-limiter": "~1.0.0" | ||||||
|       } |       } | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,7 +1,8 @@ | |||||||
| { | { | ||||||
|   "name": "trilium", |   "name": "trilium", | ||||||
|  |   "productName": "Trilium Notes", | ||||||
|   "description": "Trilium Notes", |   "description": "Trilium Notes", | ||||||
|   "version": "0.24.1-beta", |   "version": "0.24.5", | ||||||
|   "license": "AGPL-3.0-only", |   "license": "AGPL-3.0-only", | ||||||
|   "main": "electron.js", |   "main": "electron.js", | ||||||
|   "bin": { |   "bin": { | ||||||
| @@ -13,12 +14,9 @@ | |||||||
|   }, |   }, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "start": "node ./src/www", |     "start": "node ./src/www", | ||||||
|     "test-electron": "xo", |  | ||||||
|     "rebuild-electron": "electron-rebuild", |  | ||||||
|     "start-electron": "electron . --disable-gpu", |     "start-electron": "electron . --disable-gpu", | ||||||
|     "build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64 --app-version= --icon=src/public/app-icons/win/icon.ico", |     "build-backend-docs": "jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js", | ||||||
|     "build-backend-docs": "jsdoc -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js", |     "build-frontend-docs": "jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/javascripts/entities/*.js src/public/javascripts/services/frontend_script_api.js", | ||||||
|     "build-frontend-docs": "jsdoc -d ./docs/frontend_api src/public/javascripts/entities/*.js src/public/javascripts/services/frontend_script_api.js", |  | ||||||
|     "build-docs": "npm run build-backend-docs && npm run build-frontend-docs" |     "build-docs": "npm run build-backend-docs && npm run build-frontend-docs" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
| @@ -47,6 +45,7 @@ | |||||||
|     "imagemin-pngquant": "6.0.0", |     "imagemin-pngquant": "6.0.0", | ||||||
|     "ini": "1.3.5", |     "ini": "1.3.5", | ||||||
|     "jimp": "0.5.6", |     "jimp": "0.5.6", | ||||||
|  |     "mime-types": "^2.1.21", | ||||||
|     "moment": "2.22.2", |     "moment": "2.22.2", | ||||||
|     "multer": "1.4.1", |     "multer": "1.4.1", | ||||||
|     "open": "0.0.5", |     "open": "0.0.5", | ||||||
| @@ -64,7 +63,7 @@ | |||||||
|     "tar-stream": "1.6.2", |     "tar-stream": "1.6.2", | ||||||
|     "turndown": "5.0.1", |     "turndown": "5.0.1", | ||||||
|     "unescape": "1.0.1", |     "unescape": "1.0.1", | ||||||
|     "ws": "6.1.0", |     "ws": "6.1.2", | ||||||
|     "xml2js": "0.4.19" |     "xml2js": "0.4.19" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ const Entity = require('./entity'); | |||||||
| const Attribute = require('./attribute'); | const Attribute = require('./attribute'); | ||||||
| const protectedSessionService = require('../services/protected_session'); | const protectedSessionService = require('../services/protected_session'); | ||||||
| const repository = require('../services/repository'); | const repository = require('../services/repository'); | ||||||
|  | const sql = require('../services/sql'); | ||||||
| const dateUtils = require('../services/date_utils'); | const dateUtils = require('../services/date_utils'); | ||||||
|  |  | ||||||
| const LABEL = 'label'; | const LABEL = 'label'; | ||||||
| @@ -433,14 +434,32 @@ class Note extends Entity { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Finds child notes with given attribute name and value. Only own attributes are considered, not inherited ones |      * @return {Promise<string[]>} return list of all descendant noteIds of this note. Returning just noteIds because number of notes can be huge. Includes also this note's noteId | ||||||
|  |      */ | ||||||
|  |     async getDescendantNoteIds() { | ||||||
|  |         return await sql.getColumn(` | ||||||
|  |             WITH RECURSIVE | ||||||
|  |             tree(noteId) AS ( | ||||||
|  |                 SELECT ? | ||||||
|  |                 UNION | ||||||
|  |                 SELECT branches.noteId FROM branches | ||||||
|  |                     JOIN tree ON branches.parentNoteId = tree.noteId | ||||||
|  |                     JOIN notes ON notes.noteId = branches.noteId | ||||||
|  |                 WHERE notes.isDeleted = 0 | ||||||
|  |                   AND branches.isDeleted = 0 | ||||||
|  |             ) | ||||||
|  |             SELECT noteId FROM tree`, [this.noteId]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Finds descendant notes with given attribute name and value. Only own attributes are considered, not inherited ones | ||||||
|      * |      * | ||||||
|      * @param {string} type - attribute type (label, relation, etc.) |      * @param {string} type - attribute type (label, relation, etc.) | ||||||
|      * @param {string} name - attribute name |      * @param {string} name - attribute name | ||||||
|      * @param {string} [value] - attribute value |      * @param {string} [value] - attribute value | ||||||
|      * @returns {Promise<Note[]>} |      * @returns {Promise<Note[]>} | ||||||
|      */ |      */ | ||||||
|     async findChildNotesWithAttribute(type, name, value) { |     async getDescendantNotesWithAttribute(type, name, value) { | ||||||
|         const params = [this.noteId, name]; |         const params = [this.noteId, name]; | ||||||
|         let valueCondition = ""; |         let valueCondition = ""; | ||||||
|  |  | ||||||
| @@ -472,22 +491,22 @@ class Note extends Entity { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Finds notes with given label name and value. Only own labels are considered, not inherited ones |      * Finds descendant notes with given label name and value. Only own labels are considered, not inherited ones | ||||||
|      * |      * | ||||||
|      * @param {string} name - label name |      * @param {string} name - label name | ||||||
|      * @param {string} [value] - label value |      * @param {string} [value] - label value | ||||||
|      * @returns {Promise<Note[]>} |      * @returns {Promise<Note[]>} | ||||||
|      */ |      */ | ||||||
|     async findChildNotesWithLabel(name, value) { return await this.findChildNotesWithAttribute(LABEL, name, value); } |     async getDescendantNotesWithLabel(name, value) { return await this.getDescendantNotesWithAttribute(LABEL, name, value); } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Finds notes with given relation name and value. Only own relations are considered, not inherited ones |      * Finds descendant notes with given relation name and value. Only own relations are considered, not inherited ones | ||||||
|      * |      * | ||||||
|      * @param {string} name - relation name |      * @param {string} name - relation name | ||||||
|      * @param {string} [value] - relation value |      * @param {string} [value] - relation value | ||||||
|      * @returns {Promise<Note[]>} |      * @returns {Promise<Note[]>} | ||||||
|      */ |      */ | ||||||
|     async findChildNotesWithRelation(name, value) { return await this.findChildNotesWithAttribute(RELATION, name, value); } |     async getDescendantNotesWithRelation(name, value) { return await this.getDescendantNotesWithAttribute(RELATION, name, value); } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Returns note revisions of this note. |      * Returns note revisions of this note. | ||||||
|   | |||||||
| @@ -72,7 +72,7 @@ function AttributesModel() { | |||||||
|  |  | ||||||
|             attr.relationDefinition = (attr.type === 'relation-definition' && attr.value) ? attr.value : { |             attr.relationDefinition = (attr.type === 'relation-definition' && attr.value) ? attr.value : { | ||||||
|                 multiplicityType: "singlevalue", |                 multiplicityType: "singlevalue", | ||||||
|                 mirrorRelation: "", |                 inverseRelation: "", | ||||||
|                 isPromoted: true |                 isPromoted: true | ||||||
|             }; |             }; | ||||||
|  |  | ||||||
| @@ -114,7 +114,7 @@ function AttributesModel() { | |||||||
|  |  | ||||||
|     function isValid() { |     function isValid() { | ||||||
|         for (let attributes = self.ownedAttributes(), i = 0; i < attributes.length; i++) { |         for (let attributes = self.ownedAttributes(), i = 0; i < attributes.length; i++) { | ||||||
|             if (self.isEmptyName(i)) { |             if (self.isEmptyName(i) || self.isEmptyRelationTarget(i)) { | ||||||
|                 return false; |                 return false; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -191,7 +191,7 @@ function AttributesModel() { | |||||||
|                 }, |                 }, | ||||||
|                 relationDefinition: { |                 relationDefinition: { | ||||||
|                     multiplicityType: "singlevalue", |                     multiplicityType: "singlevalue", | ||||||
|                     mirrorRelation: "", |                     inverseRelation: "", | ||||||
|                     isPromoted: true |                     isPromoted: true | ||||||
|                 } |                 } | ||||||
|             })); |             })); | ||||||
| @@ -209,7 +209,35 @@ function AttributesModel() { | |||||||
|     this.isEmptyName = function(index) { |     this.isEmptyName = function(index) { | ||||||
|         const cur = self.ownedAttributes()[index](); |         const cur = self.ownedAttributes()[index](); | ||||||
|  |  | ||||||
|         return cur.name.trim() === "" && !cur.isDeleted && (cur.attributeId !== "" || cur.labelValue !== "" || cur.relationValue); |         if (cur.name.trim() || cur.isDeleted) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (cur.attributeId) { | ||||||
|  |             // name is empty and attribute already exists so this is NO-GO | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (cur.type === 'relation-definition' || cur.type === 'label-definition') { | ||||||
|  |             // for definitions there's no possible empty value so we always require name | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (cur.type === 'label' && cur.labelValue) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (cur.type === 'relation' && cur.relationValue) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return false; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     this.isEmptyRelationTarget = function(index) { | ||||||
|  |         const cur = self.ownedAttributes()[index](); | ||||||
|  |  | ||||||
|  |         return cur.type === "relation" && !cur.isDeleted && cur.name && !cur.relationValue; | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     this.getTargetAttribute = function(target) { |     this.getTargetAttribute = function(target) { | ||||||
|   | |||||||
							
								
								
									
										77
									
								
								src/public/javascripts/dialogs/export.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/public/javascripts/dialogs/export.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | |||||||
|  | import treeService from '../services/tree.js'; | ||||||
|  | import treeUtils from "../services/tree_utils.js"; | ||||||
|  | import exportService from "../services/export.js"; | ||||||
|  |  | ||||||
|  | const $dialog = $("#export-dialog"); | ||||||
|  | const $form = $("#export-form"); | ||||||
|  | const $noteTitle = $dialog.find(".note-title"); | ||||||
|  | const $subtreeFormats = $("#export-subtree-formats"); | ||||||
|  | const $singleFormats = $("#export-single-formats"); | ||||||
|  | const $subtreeType = $("#export-type-subtree"); | ||||||
|  | const $singleType = $("#export-type-single"); | ||||||
|  |  | ||||||
|  | async function showDialog(defaultType) { | ||||||
|  |     if (defaultType === 'subtree') { | ||||||
|  |         $subtreeType.prop("checked", true).change(); | ||||||
|  |     } | ||||||
|  |     else if (defaultType === 'single') { | ||||||
|  |         $singleType.prop("checked", true).change(); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         throw new Error("Unrecognized type " + defaultType); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     glob.activeDialog = $dialog; | ||||||
|  |  | ||||||
|  |     $dialog.modal(); | ||||||
|  |  | ||||||
|  |     const currentNode = treeService.getCurrentNode(); | ||||||
|  |     const noteTitle = await treeUtils.getNoteTitle(currentNode.data.noteId); | ||||||
|  |  | ||||||
|  |     $noteTitle.html(noteTitle); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | $form.submit(() => { | ||||||
|  |     const exportType = $dialog.find("input[name='export-type']:checked").val(); | ||||||
|  |  | ||||||
|  |     if (!exportType) { | ||||||
|  |         // this shouldn't happen as we always choose default export type | ||||||
|  |         alert("Choose export type first please"); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const exportFormat = exportType === 'subtree' | ||||||
|  |         ? $("input[name=export-subtree-format]:checked").val() | ||||||
|  |         : $("input[name=export-single-format]:checked").val(); | ||||||
|  |  | ||||||
|  |     const currentNode = treeService.getCurrentNode(); | ||||||
|  |  | ||||||
|  |     exportService.exportBranch(currentNode.data.branchId, exportType, exportFormat); | ||||||
|  |  | ||||||
|  |     $dialog.modal('hide'); | ||||||
|  |  | ||||||
|  |     return false; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | $('input[name=export-type]').change(function () { | ||||||
|  |     if (this.value === 'subtree') { | ||||||
|  |         if ($("input[name=export-subtree-format]:checked").length === 0) { | ||||||
|  |             $("input[name=export-subtree-format]:first").prop("checked", true); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $subtreeFormats.slideDown(); | ||||||
|  |         $singleFormats.slideUp(); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         if ($("input[name=export-single-format]:checked").length === 0) { | ||||||
|  |             $("input[name=export-single-format]:first").prop("checked", true); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $subtreeFormats.slideUp(); | ||||||
|  |         $singleFormats.slideDown(); | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     showDialog | ||||||
|  | }; | ||||||
| @@ -1,35 +0,0 @@ | |||||||
| import treeService from '../services/tree.js'; |  | ||||||
| import server from '../services/server.js'; |  | ||||||
| import treeUtils from "../services/tree_utils.js"; |  | ||||||
| import exportService from "../services/export.js"; |  | ||||||
|  |  | ||||||
| const $dialog = $("#export-subtree-dialog"); |  | ||||||
| const $form = $("#export-subtree-form"); |  | ||||||
| const $noteTitle = $dialog.find(".note-title"); |  | ||||||
|  |  | ||||||
| async function showDialog() { |  | ||||||
|     glob.activeDialog = $dialog; |  | ||||||
|  |  | ||||||
|     $dialog.modal(); |  | ||||||
|  |  | ||||||
|     const currentNode = treeService.getCurrentNode(); |  | ||||||
|     const noteTitle = await treeUtils.getNoteTitle(currentNode.data.noteId); |  | ||||||
|  |  | ||||||
|     $noteTitle.html(noteTitle); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| $form.submit(() => { |  | ||||||
|     const exportFormat = $dialog.find("input[name='export-format']:checked").val(); |  | ||||||
|  |  | ||||||
|     const currentNode = treeService.getCurrentNode(); |  | ||||||
|  |  | ||||||
|     exportService.exportSubtree(currentNode.data.branchId, exportFormat); |  | ||||||
|  |  | ||||||
|     $dialog.modal('hide'); |  | ||||||
|  |  | ||||||
|     return false; |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|     showDialog |  | ||||||
| }; |  | ||||||
| @@ -14,7 +14,7 @@ class Branch { | |||||||
|         /** @param {string} */ |         /** @param {string} */ | ||||||
|         this.prefix = row.prefix; |         this.prefix = row.prefix; | ||||||
|         /** @param {boolean} */ |         /** @param {boolean} */ | ||||||
|         this.isExpanded = row.isExpanded; |         this.isExpanded = !!row.isExpanded; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** @returns {NoteShort} */ |     /** @returns {NoteShort} */ | ||||||
|   | |||||||
| @@ -12,9 +12,12 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }) { | |||||||
|             hint: false, |             hint: false, | ||||||
|             autoselect: true, |             autoselect: true, | ||||||
|             openOnFocus: true, |             openOnFocus: true, | ||||||
|             minLength: 0 |             minLength: 0, | ||||||
|  |             tabAutocomplete: false | ||||||
|         }, [{ |         }, [{ | ||||||
|             displayKey: 'name', |             displayKey: 'name', | ||||||
|  |             // disabling cache is important here because otherwise cache can stay intact when switching between attribute type which will lead to autocomplete displaying attribute names for incorrect attribute type | ||||||
|  |             cache: false, | ||||||
|             source: async (term, cb) => { |             source: async (term, cb) => { | ||||||
|                 const type = typeof attributeType === "function" ? attributeType() : attributeType; |                 const type = typeof attributeType === "function" ? attributeType() : attributeType; | ||||||
|  |  | ||||||
| @@ -57,7 +60,8 @@ async function initLabelValueAutocomplete({ $el, open }) { | |||||||
|             hint: false, |             hint: false, | ||||||
|             autoselect: true, |             autoselect: true, | ||||||
|             openOnFocus: true, |             openOnFocus: true, | ||||||
|             minLength: 0 |             minLength: 0, | ||||||
|  |             tabAutocomplete: false | ||||||
|         }, [{ |         }, [{ | ||||||
|             displayKey: 'value', |             displayKey: 'value', | ||||||
|             source: function (term, cb) { |             source: function (term, cb) { | ||||||
|   | |||||||
| @@ -146,7 +146,8 @@ async function createPromotedAttributeRow(definitionAttr, valueAttr) { | |||||||
|                     hint: false, |                     hint: false, | ||||||
|                     autoselect: true, |                     autoselect: true, | ||||||
|                     openOnFocus: true, |                     openOnFocus: true, | ||||||
|                     minLength: 0 |                     minLength: 0, | ||||||
|  |                     tabAutocomplete: false | ||||||
|                 }, [{ |                 }, [{ | ||||||
|                     displayKey: 'value', |                     displayKey: 'value', | ||||||
|                     source: function (term, cb) { |                     source: function (term, cb) { | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
								
							| @@ -7,6 +7,7 @@ import recentChangesDialog from '../dialogs/recent_changes.js'; | |||||||
| import optionsDialog from '../dialogs/options.js'; | import optionsDialog from '../dialogs/options.js'; | ||||||
| import sqlConsoleDialog from '../dialogs/sql_console.js'; | import sqlConsoleDialog from '../dialogs/sql_console.js'; | ||||||
| import markdownImportDialog from '../dialogs/markdown_import.js'; | import markdownImportDialog from '../dialogs/markdown_import.js'; | ||||||
|  | import exportDialog from '../dialogs/export.js'; | ||||||
|  |  | ||||||
| import cloning from './cloning.js'; | import cloning from './cloning.js'; | ||||||
| import contextMenu from './tree_context_menu.js'; | import contextMenu from './tree_context_menu.js'; | ||||||
| @@ -103,7 +104,13 @@ if (utils.isElectron()) { | |||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| $("#export-note-to-markdown-button").click(() => exportService.exportSubtree(noteDetailService.getCurrentNoteId(), 'markdown-single')); | $("#export-note-button").click(function () { | ||||||
|  |     if ($(this).hasClass("disabled")) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     exportDialog.showDialog('single'); | ||||||
|  | }); | ||||||
|  |  | ||||||
| treeService.showTree(); | treeService.showTree(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,15 +10,13 @@ const dragAndDropSetup = { | |||||||
|  |  | ||||||
|         node.setSelected(true); |         node.setSelected(true); | ||||||
|  |  | ||||||
|         const selectedNodes = treeService.getSelectedNodes().map(node => { |         // this is for dragging notes into relation map | ||||||
|             return { |         // we allow to drag only one note at a time because it multi-drag conflicts with multiple single drags | ||||||
|  |         // in UX and single drag is probably more useful | ||||||
|  |         data.dataTransfer.setData("text", JSON.stringify({ | ||||||
|             noteId: node.data.noteId, |             noteId: node.data.noteId, | ||||||
|             title: node.title |             title: node.title | ||||||
|             } |         })); | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         // this is for dragging notes into relation map |  | ||||||
|         data.dataTransfer.setData("text", JSON.stringify(selectedNodes)); |  | ||||||
|  |  | ||||||
|         // This function MUST be defined to enable dragging for the tree. |         // This function MUST be defined to enable dragging for the tree. | ||||||
|         // Return false to cancel dragging of node. |         // Return false to cancel dragging of node. | ||||||
|   | |||||||
| @@ -25,14 +25,26 @@ function registerEntrypoints() { | |||||||
|     $("#jump-to-note-dialog-button").click(jumpToNoteDialog.showDialog); |     $("#jump-to-note-dialog-button").click(jumpToNoteDialog.showDialog); | ||||||
|     utils.bindShortcut('ctrl+j', jumpToNoteDialog.showDialog); |     utils.bindShortcut('ctrl+j', jumpToNoteDialog.showDialog); | ||||||
|  |  | ||||||
|     $("#show-note-revisions-button").click(noteRevisionsDialog.showCurrentNoteRevisions); |     $("#show-note-revisions-button").click(function() { | ||||||
|  |         if ($(this).hasClass("disabled")) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|     $("#show-source-button").click(noteSourceDialog.showDialog); |         noteRevisionsDialog.showCurrentNoteRevisions(); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     $("#show-source-button").click(function() { | ||||||
|  |         if ($(this).hasClass("disabled")) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         noteSourceDialog.showDialog(); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     $("#recent-changes-button").click(recentChangesDialog.showDialog); |     $("#recent-changes-button").click(recentChangesDialog.showDialog); | ||||||
|  |  | ||||||
|     $("#protected-session-on").click(protectedSessionService.enterProtectedSession); |     $("#enter-protected-session-button").click(protectedSessionService.enterProtectedSession); | ||||||
|     $("#protected-session-off").click(protectedSessionService.leaveProtectedSession); |     $("#leave-protected-session-button").click(protectedSessionService.leaveProtectedSession); | ||||||
|  |  | ||||||
|     $("#toggle-search-button").click(searchNotesService.toggleSearch); |     $("#toggle-search-button").click(searchNotesService.toggleSearch); | ||||||
|     utils.bindShortcut('ctrl+s', searchNotesService.toggleSearch); |     utils.bindShortcut('ctrl+s', searchNotesService.toggleSearch); | ||||||
| @@ -85,10 +97,17 @@ function registerEntrypoints() { | |||||||
|  |  | ||||||
|     $(document).bind('keydown', 'ctrl+f', () => { |     $(document).bind('keydown', 'ctrl+f', () => { | ||||||
|         if (utils.isElectron()) { |         if (utils.isElectron()) { | ||||||
|             const searchInPage = require('electron-in-page-search').default; |             const $searchWindowWebview = $(".electron-in-page-search-window"); | ||||||
|             const remote = require('electron').remote; |             $searchWindowWebview.show(); | ||||||
|  |  | ||||||
|             const inPageSearch = searchInPage(remote.getCurrentWebContents()); |             const searchInPage = require('electron-in-page-search').default; | ||||||
|  |             const {remote} = require('electron'); | ||||||
|  |  | ||||||
|  |             const inPageSearch = searchInPage(remote.getCurrentWebContents(), { | ||||||
|  |                 searchWindowWebview: $searchWindowWebview[0], | ||||||
|  |                 //openDevToolsOfSearchWindow: true, | ||||||
|  |                 customCssPath: '/libraries/electron-in-page-search/default-style.css' | ||||||
|  |             }); | ||||||
|  |  | ||||||
|             inPageSearch.openSearchWindow(); |             inPageSearch.openSearchWindow(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,16 +1,14 @@ | |||||||
| import treeService from './tree.js'; | import treeService from './tree.js'; | ||||||
| import infoService from './info.js'; |  | ||||||
| import protectedSessionHolder from './protected_session_holder.js'; | import protectedSessionHolder from './protected_session_holder.js'; | ||||||
| import utils from './utils.js'; | import utils from './utils.js'; | ||||||
| import server from './server.js'; | import server from './server.js'; | ||||||
|  |  | ||||||
| function exportSubtree(noteId, format) { | function exportBranch(branchId, type, format) { | ||||||
|     const url = utils.getHost() + "/api/notes/" + noteId + "/export/" + format + |     const url = utils.getHost() + `/api/notes/${branchId}/export/${type}/${format}?protectedSessionId=` + encodeURIComponent(protectedSessionHolder.getProtectedSessionId()); | ||||||
|         "?protectedSessionId=" + encodeURIComponent(protectedSessionHolder.getProtectedSessionId()); |  | ||||||
|  |     console.log(url); | ||||||
|  |  | ||||||
|     utils.download(url); |     utils.download(url); | ||||||
|  |  | ||||||
|     infoService.showMessage("Export to file has been finished."); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| let importNoteId; | let importNoteId; | ||||||
| @@ -47,6 +45,6 @@ $("#import-upload").change(async function() { | |||||||
| }); | }); | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|     exportSubtree, |     exportBranch, | ||||||
|     importIntoNote |     importIntoNote | ||||||
| }; | }; | ||||||
| @@ -5,13 +5,9 @@ function showMessage(message) { | |||||||
|     console.debug(utils.now(), "message: ", message); |     console.debug(utils.now(), "message: ", message); | ||||||
|  |  | ||||||
|     $.notify({ |     $.notify({ | ||||||
|         // options |         icon: 'jam jam-check', | ||||||
|         message: message |         message: message | ||||||
|     }, { |     }, getNotifySettings('success', 3000)); | ||||||
|         // options |  | ||||||
|         type: 'success', |  | ||||||
|         delay: 3000 |  | ||||||
|     }); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| function showAndLogError(message, delay = 10000) { | function showAndLogError(message, delay = 10000) { | ||||||
| @@ -25,12 +21,26 @@ function showError(message, delay = 10000) { | |||||||
|  |  | ||||||
|     $.notify({ |     $.notify({ | ||||||
|         // options |         // options | ||||||
|  |         icon: 'jam jam-alert', | ||||||
|         message: message |         message: message | ||||||
|     }, { |     }, getNotifySettings('danger', delay)); | ||||||
|         // options | } | ||||||
|         type: 'danger', |  | ||||||
|  | function getNotifySettings(type, delay) { | ||||||
|  |     return { | ||||||
|  |         element: 'body', | ||||||
|  |         type: type, | ||||||
|  |         z_index: 90000, | ||||||
|  |         placement: { | ||||||
|  |             from: "top", | ||||||
|  |             align: "center" | ||||||
|  |         }, | ||||||
|  |         animate: { | ||||||
|  |             enter: 'animated fadeInDown', | ||||||
|  |             exit: 'animated fadeOutUp' | ||||||
|  |         }, | ||||||
|         delay: delay |         delay: delay | ||||||
|     }); |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
| function throwError(message) { | function throwError(message) { | ||||||
|   | |||||||
| @@ -76,7 +76,8 @@ function initNoteAutocomplete($el, options) { | |||||||
|             hint: false, |             hint: false, | ||||||
|             autoselect: true, |             autoselect: true, | ||||||
|             openOnFocus: true, |             openOnFocus: true, | ||||||
|             minLength: 0 |             minLength: 0, | ||||||
|  |             tabAutocomplete: false | ||||||
|         }, [ |         }, [ | ||||||
|             { |             { | ||||||
|                 source: autocompleteSource, |                 source: autocompleteSource, | ||||||
| @@ -92,7 +93,7 @@ function initNoteAutocomplete($el, options) { | |||||||
|         $el.on('autocomplete:selected', (event, suggestion) => $el.setSelectedPath(suggestion.path)); |         $el.on('autocomplete:selected', (event, suggestion) => $el.setSelectedPath(suggestion.path)); | ||||||
|         $el.on('autocomplete:closed', () => { |         $el.on('autocomplete:closed', () => { | ||||||
|             if (!$el.val().trim()) { |             if (!$el.val().trim()) { | ||||||
|                 $el.setSelectedPath(""); |                 clearText($el); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -28,6 +28,7 @@ const $noteDetailWrapper = $("#note-detail-wrapper"); | |||||||
| const $noteIdDisplay = $("#note-id-display"); | const $noteIdDisplay = $("#note-id-display"); | ||||||
| const $childrenOverview = $("#children-overview"); | const $childrenOverview = $("#children-overview"); | ||||||
| const $scriptArea = $("#note-detail-script-area"); | const $scriptArea = $("#note-detail-script-area"); | ||||||
|  | const $savedIndicator = $("#saved-indicator"); | ||||||
|  |  | ||||||
| let currentNote = null; | let currentNote = null; | ||||||
|  |  | ||||||
| @@ -78,6 +79,8 @@ function noteChanged() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     isNoteChanged = true; |     isNoteChanged = true; | ||||||
|  |  | ||||||
|  |     $savedIndicator.fadeOut(); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function reload() { | async function reload() { | ||||||
| @@ -120,15 +123,16 @@ async function saveNote() { | |||||||
|         protectedSessionHolder.touchProtectedSession(); |         protectedSessionHolder.touchProtectedSession(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     infoService.showMessage("Saved!"); |     $savedIndicator.fadeIn(); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function saveNoteIfChanged() { | async function saveNoteIfChanged() { | ||||||
|     if (!isNoteChanged) { |     if (isNoteChanged) { | ||||||
|         return; |         await saveNote(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     await saveNote(); |     // make sure indicator is visible in a case there was some race condition. | ||||||
|  |     $savedIndicator.fadeIn(); | ||||||
| } | } | ||||||
|  |  | ||||||
| function setNoteBackgroundIfProtected(note) { | function setNoteBackgroundIfProtected(note) { | ||||||
| @@ -294,7 +298,7 @@ $(document).ready(() => { | |||||||
| // this sends the request asynchronously and doesn't wait for result | // this sends the request asynchronously and doesn't wait for result | ||||||
| $(window).on('beforeunload', () => { saveNoteIfChanged(); }); // don't convert to short form, handler doesn't like returned promise | $(window).on('beforeunload', () => { saveNoteIfChanged(); }); // don't convert to short form, handler doesn't like returned promise | ||||||
|  |  | ||||||
| setInterval(saveNoteIfChanged, 5000); | setInterval(saveNoteIfChanged, 3000); | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|     reload, |     reload, | ||||||
|   | |||||||
| @@ -32,7 +32,10 @@ async function show() { | |||||||
|             lint: true, |             lint: true, | ||||||
|             gutters: ["CodeMirror-lint-markers"], |             gutters: ["CodeMirror-lint-markers"], | ||||||
|             lineNumbers: true, |             lineNumbers: true, | ||||||
|             tabindex: 100 |             tabindex: 100, | ||||||
|  |             // we linewrap partly also because without it horizontal scrollbar displays only when you scroll | ||||||
|  |             // all the way to the bottom of the note. With line wrap there's no horizontal scrollbar so no problem | ||||||
|  |             lineWrapping: true | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         onNoteChange(noteDetailService.noteChanged); |         onNoteChange(noteDetailService.noteChanged); | ||||||
| @@ -43,7 +46,9 @@ async function show() { | |||||||
|     const currentNote = noteDetailService.getCurrentNote(); |     const currentNote = noteDetailService.getCurrentNote(); | ||||||
|  |  | ||||||
|     // this needs to happen after the element is shown, otherwise the editor won't be refreshed |     // this needs to happen after the element is shown, otherwise the editor won't be refreshed | ||||||
|     codeEditor.setValue(currentNote.content); |     // CodeMirror breaks pretty badly on null so even though it shouldn't happen (guarded by consistency check) | ||||||
|  |     // we provide fallback | ||||||
|  |     codeEditor.setValue(currentNote.content || ""); | ||||||
|  |  | ||||||
|     const info = CodeMirror.findModeByMIME(currentNote.mime); |     const info = CodeMirror.findModeByMIME(currentNote.mime); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import infoService from "./info.js"; | |||||||
| import server from "./server.js"; | import server from "./server.js"; | ||||||
|  |  | ||||||
| const $component = $('#note-detail-image'); | const $component = $('#note-detail-image'); | ||||||
|  | const $imageWrapper = $('#note-detail-image-wrapper'); | ||||||
| const $imageView = $('#note-detail-image-view'); | const $imageView = $('#note-detail-image-view'); | ||||||
|  |  | ||||||
| const $imageDownloadButton = $("#image-download"); | const $imageDownloadButton = $("#image-download"); | ||||||
| @@ -39,10 +40,10 @@ function selectImage(element) { | |||||||
| } | } | ||||||
|  |  | ||||||
| $copyToClipboardButton.click(() => { | $copyToClipboardButton.click(() => { | ||||||
|     $component.attr('contenteditable','true'); |     $imageWrapper.attr('contenteditable','true'); | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|         selectImage($component.get(0)); |         selectImage($imageWrapper.get(0)); | ||||||
|  |  | ||||||
|         const success = document.execCommand('copy'); |         const success = document.execCommand('copy'); | ||||||
|  |  | ||||||
| @@ -55,7 +56,7 @@ $copyToClipboardButton.click(() => { | |||||||
|     } |     } | ||||||
|     finally { |     finally { | ||||||
|         window.getSelection().removeAllRanges(); |         window.getSelection().removeAllRanges(); | ||||||
|         $component.removeAttr('contenteditable'); |         $imageWrapper.removeAttr('contenteditable'); | ||||||
|     } |     } | ||||||
| }); | }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ const $relationMapContainer = $("#relation-map-container"); | |||||||
| const $createChildNote = $("#relation-map-create-child-note"); | const $createChildNote = $("#relation-map-create-child-note"); | ||||||
| const $zoomInButton = $("#relation-map-zoom-in"); | const $zoomInButton = $("#relation-map-zoom-in"); | ||||||
| const $zoomOutButton = $("#relation-map-zoom-out"); | const $zoomOutButton = $("#relation-map-zoom-out"); | ||||||
| const $centerButton = $("#relation-map-center"); | const $resetPanZoomButton = $("#relation-map-reset-pan-zoom"); | ||||||
|  |  | ||||||
| let mapData; | let mapData; | ||||||
| let jsPlumbInstance; | let jsPlumbInstance; | ||||||
| @@ -50,7 +50,7 @@ const biDirectionalOverlays = [ | |||||||
|     } ] |     } ] | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| const mirrorOverlays = [ | const inverseRelationsOverlays = [ | ||||||
|     [ "Arrow", { |     [ "Arrow", { | ||||||
|         location: 1, |         location: 1, | ||||||
|         id: "arrow", |         id: "arrow", | ||||||
| @@ -134,12 +134,12 @@ async function loadNotesAndRelations() { | |||||||
|  |  | ||||||
|     for (const relation of data.relations) { |     for (const relation of data.relations) { | ||||||
|         const match = relations.find(rel => |         const match = relations.find(rel => | ||||||
|             rel.name === data.mirrorRelations[relation.name] |             rel.name === data.inverseRelations[relation.name] | ||||||
|             && ((rel.sourceNoteId === relation.sourceNoteId && rel.targetNoteId === relation.targetNoteId) |             && ((rel.sourceNoteId === relation.sourceNoteId && rel.targetNoteId === relation.targetNoteId) | ||||||
|             || (rel.sourceNoteId === relation.targetNoteId && rel.targetNoteId === relation.sourceNoteId))); |             || (rel.sourceNoteId === relation.targetNoteId && rel.targetNoteId === relation.sourceNoteId))); | ||||||
|  |  | ||||||
|         if (match) { |         if (match) { | ||||||
|             match.type = relation.type = relation.name === data.mirrorRelations[relation.name] ? 'biDirectional' : 'mirror'; |             match.type = relation.type = relation.name === data.inverseRelations[relation.name] ? 'biDirectional' : 'inverse'; | ||||||
|             relation.render = false; // don't render second relation |             relation.render = false; // don't render second relation | ||||||
|         } else { |         } else { | ||||||
|             relation.type = 'uniDirectional'; |             relation.type = 'uniDirectional'; | ||||||
| @@ -173,9 +173,9 @@ async function loadNotesAndRelations() { | |||||||
|  |  | ||||||
|             connection.id = relation.attributeId; |             connection.id = relation.attributeId; | ||||||
|  |  | ||||||
|             if (relation.type === 'mirror') { |             if (relation.type === 'inverse') { | ||||||
|                 connection.getOverlay("label-source").setLabel(relation.name); |                 connection.getOverlay("label-source").setLabel(relation.name); | ||||||
|                 connection.getOverlay("label-target").setLabel(data.mirrorRelations[relation.name]); |                 connection.getOverlay("label-target").setLabel(data.inverseRelations[relation.name]); | ||||||
|             } |             } | ||||||
|             else { |             else { | ||||||
|                 connection.getOverlay("label").setLabel(relation.name); |                 connection.getOverlay("label").setLabel(relation.name); | ||||||
| @@ -240,6 +240,10 @@ function initPanZoom() { | |||||||
|  |  | ||||||
|         pzInstance.moveTo(mapData.transform.x, mapData.transform.y); |         pzInstance.moveTo(mapData.transform.x, mapData.transform.y); | ||||||
|     } |     } | ||||||
|  |     else { | ||||||
|  |         // set to initial coordinates | ||||||
|  |         pzInstance.moveTo(0, 0); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     $zoomInButton.click(() => pzInstance.zoomTo(0, 0, 1.2)); |     $zoomInButton.click(() => pzInstance.zoomTo(0, 0, 1.2)); | ||||||
|     $zoomOutButton.click(() => pzInstance.zoomTo(0, 0, 0.8)); |     $zoomOutButton.click(() => pzInstance.zoomTo(0, 0, 0.8)); | ||||||
| @@ -286,7 +290,7 @@ function initJsPlumbInstance () { | |||||||
|  |  | ||||||
|     jsPlumbInstance.registerConnectionType("biDirectional", { anchor:"Continuous", connector:"StateMachine", overlays: biDirectionalOverlays }); |     jsPlumbInstance.registerConnectionType("biDirectional", { anchor:"Continuous", connector:"StateMachine", overlays: biDirectionalOverlays }); | ||||||
|  |  | ||||||
|     jsPlumbInstance.registerConnectionType("mirror", { anchor:"Continuous", connector:"StateMachine", overlays: mirrorOverlays }); |     jsPlumbInstance.registerConnectionType("inverse", { anchor:"Continuous", connector:"StateMachine", overlays: inverseRelationsOverlays }); | ||||||
|  |  | ||||||
|     jsPlumbInstance.registerConnectionType("link", { anchor:"Continuous", connector:"StateMachine", overlays: linkOverlays }); |     jsPlumbInstance.registerConnectionType("link", { anchor:"Continuous", connector:"StateMachine", overlays: linkOverlays }); | ||||||
|  |  | ||||||
| @@ -518,43 +522,20 @@ function getZoom() { | |||||||
| async function dropNoteOntoRelationMapHandler(ev) { | async function dropNoteOntoRelationMapHandler(ev) { | ||||||
|     ev.preventDefault(); |     ev.preventDefault(); | ||||||
|  |  | ||||||
|     const notes = JSON.parse(ev.originalEvent.dataTransfer.getData("text")); |     const note = JSON.parse(ev.originalEvent.dataTransfer.getData("text")); | ||||||
|  |  | ||||||
|     let {x, y} = getMousePosition(ev); |     let {x, y} = getMousePosition(ev); | ||||||
|  |  | ||||||
|     // modifying position so that cursor is on the top-center of the box |  | ||||||
|     const startX = x -= 80; |  | ||||||
|     y -= 15; |  | ||||||
|  |  | ||||||
|     const currentNoteId = treeService.getCurrentNode().data.noteId; |  | ||||||
|  |  | ||||||
|     for (const note of notes) { |  | ||||||
|         if (note.noteId === currentNoteId) { |  | ||||||
|             // we don't allow placing current (relation map) into itself |  | ||||||
|             // the reason is that when dragging notes from the tree, the relation map is always selected |  | ||||||
|             // since it's focused. |  | ||||||
|             continue; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     const exists = mapData.notes.some(n => n.noteId === note.noteId); |     const exists = mapData.notes.some(n => n.noteId === note.noteId); | ||||||
|  |  | ||||||
|     if (exists) { |     if (exists) { | ||||||
|         await infoDialog.info(`Note "${note.title}" is already placed into the diagram`); |         await infoDialog.info(`Note "${note.title}" is already placed into the diagram`); | ||||||
|  |  | ||||||
|             continue; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     mapData.notes.push({noteId: note.noteId, x, y}); |     mapData.notes.push({noteId: note.noteId, x, y}); | ||||||
|  |  | ||||||
|         if (x - startX > 1000) { |  | ||||||
|             x = startX; |  | ||||||
|             y += 200; |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             x += 200; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     saveData(); |     saveData(); | ||||||
|  |  | ||||||
|     await refresh(); |     await refresh(); | ||||||
| @@ -571,40 +552,10 @@ function getMousePosition(evt) { | |||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
| $centerButton.click(() => { | $resetPanZoomButton.click(() => { | ||||||
|     if (mapData.notes.length === 0) { |     // reset to initial pan & zoom state | ||||||
|         return; // nothing to recenter on |     pzInstance.zoomTo(0, 0, 1 / getZoom()); | ||||||
|     } |     pzInstance.moveTo(0, 0); | ||||||
|  |  | ||||||
|     let totalX = 0, totalY = 0; |  | ||||||
|  |  | ||||||
|     for (const note of mapData.notes) { |  | ||||||
|         totalX += note.x; |  | ||||||
|         totalY += note.y; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let averageX = totalX / mapData.notes.length; |  | ||||||
|     let averageY = totalY / mapData.notes.length; |  | ||||||
|  |  | ||||||
|     // find note with smallest X, Y difference from the average (most central note) |  | ||||||
|     const {noteId} = mapData.notes.map(note => { |  | ||||||
|         return { |  | ||||||
|             noteId: note.noteId, |  | ||||||
|             diff: Math.abs(note.x - averageX) + Math.abs(note.y - averageY) |  | ||||||
|         } |  | ||||||
|     }).reduce((min, val) => min.diff <= val.min ? min : val, { diff: 9999999999 }); |  | ||||||
|  |  | ||||||
|     const $noteBox = $("#" + noteIdToId(noteId)); |  | ||||||
|  |  | ||||||
|     const clientRect = $noteBox[0].getBoundingClientRect(); |  | ||||||
|     const cx = clientRect.left + clientRect.width / 2; |  | ||||||
|     const cy = clientRect.top + clientRect.height / 2; |  | ||||||
|  |  | ||||||
|     const container = $component[0].getBoundingClientRect(); |  | ||||||
|     const dx = container.width / 2 - cx; |  | ||||||
|     const dy = container.height / 2 - cy; |  | ||||||
|  |  | ||||||
|     pzInstance.moveBy(dx, dy, true); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| $component.on("drop", dropNoteOntoRelationMapHandler); | $component.on("drop", dropNoteOntoRelationMapHandler); | ||||||
|   | |||||||
| @@ -11,8 +11,8 @@ const $password = $("#protected-session-password"); | |||||||
| const $noteDetailWrapper = $("#note-detail-wrapper"); | const $noteDetailWrapper = $("#note-detail-wrapper"); | ||||||
| const $protectButton = $("#protect-button"); | const $protectButton = $("#protect-button"); | ||||||
| const $unprotectButton = $("#unprotect-button"); | const $unprotectButton = $("#unprotect-button"); | ||||||
| const $protectedSessionOnButton = $("#protected-session-on"); | const $enterProtectedSessionButton = $("#enter-protected-session-button"); | ||||||
| const $protectedSessionOffButton = $("#protected-session-off"); | const $leaveProtectedSessionButton = $("#leave-protected-session-button"); | ||||||
|  |  | ||||||
| let protectedSessionDeferred = null; | let protectedSessionDeferred = null; | ||||||
|  |  | ||||||
| @@ -57,7 +57,7 @@ async function setupProtectedSession() { | |||||||
|     const response = await enterProtectedSessionOnServer(password); |     const response = await enterProtectedSessionOnServer(password); | ||||||
|  |  | ||||||
|     if (!response.success) { |     if (!response.success) { | ||||||
|         infoService.showError("Wrong password."); |         infoService.showError("Wrong password.", 3000); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -77,8 +77,8 @@ async function setupProtectedSession() { | |||||||
|         protectedSessionDeferred.resolve(true); |         protectedSessionDeferred.resolve(true); | ||||||
|         protectedSessionDeferred = null; |         protectedSessionDeferred = null; | ||||||
|  |  | ||||||
|         $protectedSessionOnButton.addClass('active'); |         $enterProtectedSessionButton.hide(); | ||||||
|         $protectedSessionOffButton.removeClass('active'); |         $leaveProtectedSessionButton.show(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     infoService.showMessage("Protected session has been started."); |     infoService.showMessage("Protected session has been started."); | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ function setupTooltip() { | |||||||
|                 container: 'body', |                 container: 'body', | ||||||
|                 placement: 'auto', |                 placement: 'auto', | ||||||
|                 trigger: 'manual', |                 trigger: 'manual', | ||||||
|                 boundariesElement: 'window', |                 boundary: 'window', | ||||||
|                 title: html, |                 title: html, | ||||||
|                 html: true |                 html: true | ||||||
|             }); |             }); | ||||||
|   | |||||||
| @@ -564,8 +564,6 @@ async function createNote(node, parentNoteId, target, isProtected, saveSelection | |||||||
|  |  | ||||||
|     clearSelectedNodes(); // to unmark previously active node |     clearSelectedNodes(); // to unmark previously active node | ||||||
|  |  | ||||||
|     infoService.showMessage("Created!"); |  | ||||||
|  |  | ||||||
|     return {note, branch}; |     return {note, branch}; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import protectedSessionService from './protected_session.js'; | |||||||
| import treeChangesService from './branches.js'; | import treeChangesService from './branches.js'; | ||||||
| import treeUtils from './tree_utils.js'; | import treeUtils from './tree_utils.js'; | ||||||
| import branchPrefixDialog from '../dialogs/branch_prefix.js'; | import branchPrefixDialog from '../dialogs/branch_prefix.js'; | ||||||
| import exportSubtreeDialog from '../dialogs/export_subtree.js'; | import exportDialog from '../dialogs/export.js'; | ||||||
| import infoService from "./info.js"; | import infoService from "./info.js"; | ||||||
| import treeCache from "./tree_cache.js"; | import treeCache from "./tree_cache.js"; | ||||||
| import syncService from "./sync.js"; | import syncService from "./sync.js"; | ||||||
| @@ -93,7 +93,7 @@ const contextMenuItems = [ | |||||||
|     {title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "clipboard"}, |     {title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "clipboard"}, | ||||||
|     {title: "Paste after", cmd: "pasteAfter", uiIcon: "clipboard"}, |     {title: "Paste after", cmd: "pasteAfter", uiIcon: "clipboard"}, | ||||||
|     {title: "----"}, |     {title: "----"}, | ||||||
|     {title: "Export subtree", cmd: "exportSubtree", uiIcon: "arrow-up-right"}, |     {title: "Export", cmd: "export", uiIcon: "arrow-up-right"}, | ||||||
|     {title: "Import into note (tar, opml, md, enex)", cmd: "importIntoNote", uiIcon: "arrow-down-left"}, |     {title: "Import into note (tar, opml, md, enex)", cmd: "importIntoNote", uiIcon: "arrow-down-left"}, | ||||||
|     {title: "----"}, |     {title: "----"}, | ||||||
|     {title: "Collapse subtree <kbd>Alt+-</kbd>", cmd: "collapseSubtree", uiIcon: "align-justify"}, |     {title: "Collapse subtree <kbd>Alt+-</kbd>", cmd: "collapseSubtree", uiIcon: "align-justify"}, | ||||||
| @@ -127,7 +127,7 @@ async function getContextMenuItems(event) { | |||||||
|     enableItem("pasteAfter", clipboardIds.length > 0 && isNotRoot && parentNote.type !== 'search'); |     enableItem("pasteAfter", clipboardIds.length > 0 && isNotRoot && parentNote.type !== 'search'); | ||||||
|     enableItem("pasteInto", clipboardIds.length > 0 && note.type !== 'search'); |     enableItem("pasteInto", clipboardIds.length > 0 && note.type !== 'search'); | ||||||
|     enableItem("importIntoNote", note.type !== 'search'); |     enableItem("importIntoNote", note.type !== 'search'); | ||||||
|     enableItem("exportSubtree", note.type !== 'search'); |     enableItem("export", note.type !== 'search'); | ||||||
|     enableItem("editBranchPrefix", isNotRoot && parentNote.type !== 'search'); |     enableItem("editBranchPrefix", isNotRoot && parentNote.type !== 'search'); | ||||||
|  |  | ||||||
|     // Activate node on right-click |     // Activate node on right-click | ||||||
| @@ -179,8 +179,8 @@ function selectContextMenuItem(event, cmd) { | |||||||
|     else if (cmd === "delete") { |     else if (cmd === "delete") { | ||||||
|         treeChangesService.deleteNodes(treeService.getSelectedNodes(true)); |         treeChangesService.deleteNodes(treeService.getSelectedNodes(true)); | ||||||
|     } |     } | ||||||
|     else if (cmd === "exportSubtree") { |     else if (cmd === "export") { | ||||||
|         exportSubtreeDialog.showDialog(); |         exportDialog.showDialog("subtree"); | ||||||
|     } |     } | ||||||
|     else if (cmd === "importIntoNote") { |     else if (cmd === "importIntoNote") { | ||||||
|         exportService.importIntoNote(node.data.noteId); |         exportService.importIntoNote(node.data.noteId); | ||||||
|   | |||||||
| @@ -0,0 +1,57 @@ | |||||||
|  | html, body { | ||||||
|  |   margin: 0; | ||||||
|  |   width: 100%; | ||||||
|  |   height: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | body { | ||||||
|  |   font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Meiryo", sans-serif; | ||||||
|  |   overflow: hidden; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .inpage-search-body { | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: row; | ||||||
|  |   align-items: center; | ||||||
|  |   justify-content: space-between; | ||||||
|  |   margin: 8px; | ||||||
|  |   padding: 10px; | ||||||
|  |   border: solid #aaaaaa 1px; | ||||||
|  |   border-radius: 10px; | ||||||
|  |   background-color: #fafafa; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .inpage-search-input { | ||||||
|  |   width: 200px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .inpage-search-matches { | ||||||
|  |   color: #999; | ||||||
|  |   font-size: 0.8em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .inpage-search-back { | ||||||
|  |   margin-left: 2px; | ||||||
|  |   padding-left: 6px; | ||||||
|  |   padding-right: 2px; | ||||||
|  |   cursor: pointer; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .inpage-search-forward { | ||||||
|  |   padding-left: 2px; | ||||||
|  |   padding-right: 6px; | ||||||
|  |   cursor: pointer; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .inpage-search-close { | ||||||
|  |   margin-left: 4px; | ||||||
|  |   padding: 0 2px; | ||||||
|  |   cursor: pointer; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .inpage-search-back:hover, | ||||||
|  | .inpage-search-forward:hover, | ||||||
|  | .inpage-search-close:hover { | ||||||
|  |   background-color: #e2e0e2; | ||||||
|  |   border-radius: 0.2em; | ||||||
|  | } | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  |   <head> | ||||||
|  |     <meta charset="utf-8" /> | ||||||
|  |     <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes" /> | ||||||
|  |     <link href="/libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet"> | ||||||
|  |   </head> | ||||||
|  |   <body> | ||||||
|  |     <div class="inpage-search-body"> | ||||||
|  |       <input class="inpage-search-input form-control form-control-sm" type="search" placeholder="Search..." autocomplete="off" autofocus/> | ||||||
|  |  | ||||||
|  |       <div class="inpage-search-matches">0/0</div> | ||||||
|  |  | ||||||
|  |       <div class="inpage-search-back" title="Previous result"><</div> | ||||||
|  |  | ||||||
|  |       <div class="inpage-search-forward" title="Next result">></div> | ||||||
|  |  | ||||||
|  |       <div class="inpage-search-close" title="Close search">✕</div> | ||||||
|  |     </div> | ||||||
|  |   </body> | ||||||
|  |   <script>var exports = {}</script> | ||||||
|  | </html> | ||||||
| @@ -77,9 +77,12 @@ body { | |||||||
|     overflow: auto; |     overflow: auto; | ||||||
|     flex-basis: content; |     flex-basis: content; | ||||||
|     height: 100%; |     height: 100%; | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
| } | } | ||||||
|  |  | ||||||
| .note-detail-component { | .note-detail-component { | ||||||
|  |     flex-grow: 100; | ||||||
|     display: none; |     display: none; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -211,12 +214,12 @@ div.ui-tooltip { | |||||||
| */ | */ | ||||||
| .electron-in-page-search-window { | .electron-in-page-search-window { | ||||||
|     position: fixed; |     position: fixed; | ||||||
|     top: 50px; |     top: 45px; | ||||||
|     right: 0; |     right: 10px; | ||||||
|     border: solid grey 1px; |     width: 360px; | ||||||
|     background-color: white; |     height: 55px; | ||||||
|     width: 300px; |     display: none; | ||||||
|     height: 36px; |     z-index: 1001; | ||||||
| } | } | ||||||
|  |  | ||||||
| /* | /* | ||||||
| @@ -344,11 +347,11 @@ div.ui-tooltip { | |||||||
| #children-overview { | #children-overview { | ||||||
|     flex-grow: 1000; |     flex-grow: 1000; | ||||||
|     flex-shrink: 1000; |     flex-shrink: 1000; | ||||||
|     flex-basis: 0px; |     flex-basis: 0; | ||||||
|     display: flex; |     display: flex; | ||||||
|     flex-wrap: wrap; |     flex-wrap: wrap; | ||||||
|     align-content: flex-start; |     align-content: flex-start; | ||||||
|     height: 100px; |     height: 110px; | ||||||
|     overflow: auto; |     overflow: auto; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -528,12 +531,12 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th | |||||||
|     padding: 20px; |     padding: 20px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .context-menu { | .context-menu-container { | ||||||
|     font-size: small; |     font-size: small; | ||||||
| } | } | ||||||
|  |  | ||||||
| .context-menu .dropdown-item { | #context-menu-container .dropdown-item { | ||||||
|     padding: 2px 10px 2px 10px; |     padding: 0 7px 0 10px; | ||||||
| } | } | ||||||
|  |  | ||||||
| /* if modal height overflows, then only modal body scrolls */ | /* if modal height overflows, then only modal body scrolls */ | ||||||
| @@ -542,6 +545,11 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th | |||||||
|     overflow-y: auto; |     overflow-y: auto; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /* this should help with tooltip flickering */ | ||||||
|  | .tooltip { | ||||||
|  |     pointer-events: none; | ||||||
|  | } | ||||||
|  |  | ||||||
| .tooltip-inner { | .tooltip-inner { | ||||||
|     background-color: #fbfbfb !important; |     background-color: #fbfbfb !important; | ||||||
|     max-width: 400px; |     max-width: 400px; | ||||||
| @@ -558,6 +566,10 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th | |||||||
|     max-height: 250px; |     max-height: 250px; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .tooltip-inner figure.image-style-side { | ||||||
|  |     float: right; | ||||||
|  | } | ||||||
|  |  | ||||||
| .tooltip.show { | .tooltip.show { | ||||||
|     opacity: 1; |     opacity: 1; | ||||||
| } | } | ||||||
| @@ -608,8 +620,8 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th | |||||||
| } | } | ||||||
|  |  | ||||||
| .modalless { | .modalless { | ||||||
|     top:10%; |     top: 15%; | ||||||
|     left:50%; |     left: 40%; | ||||||
|     bottom: auto; |     bottom: auto; | ||||||
|     right: auto; |     right: auto; | ||||||
|     margin-left: -300px; |     margin-left: -300px; | ||||||
| @@ -623,3 +635,67 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th | |||||||
| code { | code { | ||||||
|     color: inherit !important; |     color: inherit !important; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .animated { | ||||||
|  |     animation-duration: 1s; | ||||||
|  |     animation-fill-mode: both; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @keyframes fadeInDown { | ||||||
|  |     from { | ||||||
|  |         opacity: 0; | ||||||
|  |         transform: translate3d(0, -100%, 0); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     to { | ||||||
|  |         opacity: 1; | ||||||
|  |         transform: translate3d(0, 0, 0); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .fadeInDown { | ||||||
|  |     animation-name: fadeInDown; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @keyframes fadeOutUp { | ||||||
|  |     from { | ||||||
|  |         opacity: 1; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     to { | ||||||
|  |         opacity: 0; | ||||||
|  |         -webkit-transform: translate3d(0, -100%, 0); | ||||||
|  |         transform: translate3d(0, -100%, 0); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .fadeOutUp { | ||||||
|  |     animation-name: fadeOutUp; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div[data-notify="container"] { | ||||||
|  |     text-align: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #saved-indicator { | ||||||
|  |     position: absolute; | ||||||
|  |     right: 10px; | ||||||
|  |     top: 11px; | ||||||
|  |     font-size: x-large; | ||||||
|  |     color: #777; | ||||||
|  |     z-index: 100; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #export-form .form-check { | ||||||
|  |     padding-top: 10px; | ||||||
|  |     padding-bottom: 10px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #export-form .format-choice { | ||||||
|  |     padding-left: 40px; | ||||||
|  |     display: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #export-form .form-check-label { | ||||||
|  |     padding: 2px; | ||||||
|  | } | ||||||
| @@ -1,28 +1,22 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const nativeTarExportService = require('../../services/export/native_tar'); | const tarExportService = require('../../services/export/tar'); | ||||||
| const markdownTarExportService = require('../../services/export/markdown_tar'); | const singleExportService = require('../../services/export/single'); | ||||||
| const markdownSingleExportService = require('../../services/export/markdown_single'); |  | ||||||
| const opmlExportService = require('../../services/export/opml'); | const opmlExportService = require('../../services/export/opml'); | ||||||
| const repository = require("../../services/repository"); | const repository = require("../../services/repository"); | ||||||
|  |  | ||||||
| async function exportNote(req, res) { | async function exportBranch(req, res) { | ||||||
|     // entityId maybe either noteId or branchId depending on format |     const {branchId, type, format} = req.params; | ||||||
|     const entityId = req.params.entityId; |     const branch = await repository.getBranch(branchId); | ||||||
|     const format = req.params.format; |  | ||||||
|  |  | ||||||
|     if (format === 'native-tar') { |     if (type === 'subtree' && (format === 'html' || format === 'markdown')) { | ||||||
|         await nativeTarExportService.exportToTar(await repository.getBranch(entityId), res); |         await tarExportService.exportToTar(branch, format, res); | ||||||
|     } |     } | ||||||
|     else if (format === 'markdown-tar') { |     else if (type === 'single') { | ||||||
|         await markdownTarExportService.exportToMarkdown(await repository.getBranch(entityId), res); |         await singleExportService.exportSingleNote(branch, format, res); | ||||||
|     } |  | ||||||
|     // export single note without subtree |  | ||||||
|     else if (format === 'markdown-single') { |  | ||||||
|         await markdownSingleExportService.exportSingleMarkdown(await repository.getNote(entityId), res); |  | ||||||
|     } |     } | ||||||
|     else if (format === 'opml') { |     else if (format === 'opml') { | ||||||
|         await opmlExportService.exportToOpml(await repository.getBranch(entityId), res); |         await opmlExportService.exportToOpml(branch, res); | ||||||
|     } |     } | ||||||
|     else { |     else { | ||||||
|         return [404, "Unrecognized export format " + format]; |         return [404, "Unrecognized export format " + format]; | ||||||
| @@ -30,5 +24,5 @@ async function exportNote(req, res) { | |||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     exportNote |     exportBranch | ||||||
| }; | }; | ||||||
| @@ -4,7 +4,8 @@ const repository = require('../../services/repository'); | |||||||
| const enexImportService = require('../../services/import/enex'); | const enexImportService = require('../../services/import/enex'); | ||||||
| const opmlImportService = require('../../services/import/opml'); | const opmlImportService = require('../../services/import/opml'); | ||||||
| const tarImportService = require('../../services/import/tar'); | const tarImportService = require('../../services/import/tar'); | ||||||
| const markdownImportService = require('../../services/import/markdown'); | const singleImportService = require('../../services/import/single'); | ||||||
|  | const cls = require('../../services/cls'); | ||||||
| const path = require('path'); | const path = require('path'); | ||||||
|  |  | ||||||
| async function importToBranch(req) { | async function importToBranch(req) { | ||||||
| @@ -23,6 +24,10 @@ async function importToBranch(req) { | |||||||
|  |  | ||||||
|     const extension = path.extname(file.originalname).toLowerCase(); |     const extension = path.extname(file.originalname).toLowerCase(); | ||||||
|  |  | ||||||
|  |     // running all the event handlers on imported notes (and attributes) is slow | ||||||
|  |     // and may produce unintended consequences | ||||||
|  |     cls.disableEntityEvents(); | ||||||
|  |  | ||||||
|     if (extension === '.tar') { |     if (extension === '.tar') { | ||||||
|         return await tarImportService.importTar(file.buffer, parentNote); |         return await tarImportService.importTar(file.buffer, parentNote); | ||||||
|     } |     } | ||||||
| @@ -30,7 +35,10 @@ async function importToBranch(req) { | |||||||
|         return await opmlImportService.importOpml(file.buffer, parentNote); |         return await opmlImportService.importOpml(file.buffer, parentNote); | ||||||
|     } |     } | ||||||
|     else if (extension === '.md') { |     else if (extension === '.md') { | ||||||
|         return await markdownImportService.importMarkdown(file, parentNote); |         return await singleImportService.importMarkdown(file, parentNote); | ||||||
|  |     } | ||||||
|  |     else if (extension === '.html' || extension === '.htm') { | ||||||
|  |         return await singleImportService.importHtml(file, parentNote); | ||||||
|     } |     } | ||||||
|     else if (extension === '.enex') { |     else if (extension === '.enex') { | ||||||
|         return await enexImportService.importEnex(file, parentNote); |         return await enexImportService.importEnex(file, parentNote); | ||||||
|   | |||||||
| @@ -117,8 +117,8 @@ async function getRelationMap(req) { | |||||||
|         // noteId => title |         // noteId => title | ||||||
|         noteTitles: {}, |         noteTitles: {}, | ||||||
|         relations: [], |         relations: [], | ||||||
|         // relation name => mirror relation name |         // relation name => inverse relation name | ||||||
|         mirrorRelations: {}, |         inverseRelations: {}, | ||||||
|         links: [] |         links: [] | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
| @@ -143,8 +143,8 @@ async function getRelationMap(req) { | |||||||
|             }; })); |             }; })); | ||||||
|  |  | ||||||
|         for (const relationDefinition of await note.getRelationDefinitions()) { |         for (const relationDefinition of await note.getRelationDefinitions()) { | ||||||
|             if (relationDefinition.value.mirrorRelation) { |             if (relationDefinition.value.inverseRelation) { | ||||||
|                 resp.mirrorRelations[relationDefinition.name] = relationDefinition.value.mirrorRelation; |                 resp.inverseRelations[relationDefinition.name] = relationDefinition.value.inverseRelation; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -128,7 +128,7 @@ function register(app) { | |||||||
|     apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentNoteId', cloningApiRoute.cloneNoteToParent); |     apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentNoteId', cloningApiRoute.cloneNoteToParent); | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter); |     apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter); | ||||||
|  |  | ||||||
|     route(GET, '/api/notes/:entityId/export/:format', [auth.checkApiAuthOrElectron], exportRoute.exportNote); |     route(GET, '/api/notes/:branchId/export/:type/:format', [auth.checkApiAuthOrElectron], exportRoute.exportBranch); | ||||||
|     route(POST, '/api/notes/:parentNoteId/import', [auth.checkApiAuthOrElectron, uploadMiddleware], importRoute.importToBranch, apiResultHandler); |     route(POST, '/api/notes/:parentNoteId/import', [auth.checkApiAuthOrElectron, uploadMiddleware], importRoute.importToBranch, apiResultHandler); | ||||||
|  |  | ||||||
|     route(POST, '/api/notes/:parentNoteId/upload', [auth.checkApiAuthOrElectron, uploadMiddleware], |     route(POST, '/api/notes/:parentNoteId/upload', [auth.checkApiAuthOrElectron, uploadMiddleware], | ||||||
|   | |||||||
							
								
								
									
										67
									
								
								src/services/app_icon.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/services/app_icon.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const path = require('path'); | ||||||
|  | const {APP_PNG_ICON_DIR, ELECTRON_APP_ROOT_DIR} = require("./resource_dir"); | ||||||
|  | const log = require("./log"); | ||||||
|  | const os = require('os'); | ||||||
|  | const fs = require('fs'); | ||||||
|  | const config = require('./config'); | ||||||
|  | const utils = require('./utils'); | ||||||
|  |  | ||||||
|  | const template = `[Desktop Entry] | ||||||
|  | Type=Application | ||||||
|  | Name=Trilium Notes | ||||||
|  | Icon=#APP_PNG_ICON_DIR#/128x128.png | ||||||
|  | Exec=#EXE_PATH# | ||||||
|  | Categories=Office | ||||||
|  | Terminal=false | ||||||
|  | `; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Installs .desktop icon into standard ~/.local/share/applications directory. | ||||||
|  |  * We overwrite this file during every run as it might have been updated. | ||||||
|  |  */ | ||||||
|  | function installLocalAppIcon() { | ||||||
|  |     if (!utils.isElectron() | ||||||
|  |         || ["win32", "darwin"].includes(os.platform()) | ||||||
|  |         || (config.General && config.General.noDesktopIcon)) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const desktopDir = path.resolve(os.homedir(), '.local/share/applications'); | ||||||
|  |  | ||||||
|  |     fs.stat(desktopDir, function (err, stats) { | ||||||
|  |         if (err) { | ||||||
|  |             // Directory doesn't exist so we won't attempt to create the .desktop file | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (stats.isDirectory()) { | ||||||
|  |             const desktopFilePath = path.resolve(desktopDir, "trilium-notes.desktop"); | ||||||
|  |  | ||||||
|  |             fs.writeFile(desktopFilePath, getDesktopFileContent(), function (err) { | ||||||
|  |                 if (err) { | ||||||
|  |                    log.error("Desktop icon installation to ~/.local/share/applications failed."); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getDesktopFileContent() { | ||||||
|  |     return template | ||||||
|  |         .replace("#APP_PNG_ICON_DIR#", escapePath(APP_PNG_ICON_DIR)) | ||||||
|  |         .replace("#EXE_PATH#", escapePath(getExePath())); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function escapePath(path) { | ||||||
|  |     return path.replace(" ", "\\ "); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getExePath() { | ||||||
|  |      return path.resolve(ELECTRON_APP_ROOT_DIR, 'trilium'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |     installLocalAppIcon | ||||||
|  | }; | ||||||
| @@ -3,7 +3,7 @@ | |||||||
| const build = require('./build'); | const build = require('./build'); | ||||||
| const packageJson = require('../../package'); | const packageJson = require('../../package'); | ||||||
|  |  | ||||||
| const APP_DB_VERSION = 118; | const APP_DB_VERSION = 120; | ||||||
| const SYNC_VERSION = 2; | const SYNC_VERSION = 2; | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| module.exports = { buildDate:"2018-11-19T00:06:44+01:00", buildRevision: "ad6cb6ba347f0396cbf79b76ab62ee3e4a4e8566" }; | module.exports = { buildDate:"2018-11-27T15:34:15+01:00", buildRevision: "bea28de6a0a41bbb948551c43a4fbf787fc5ecb3" }; | ||||||
|   | |||||||
| @@ -13,6 +13,14 @@ function getSourceId() { | |||||||
|     return namespace.get('sourceId'); |     return namespace.get('sourceId'); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function disableEntityEvents() { | ||||||
|  |     namespace.set('disableEntityEvents', true); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function isEntityEventsDisabled() { | ||||||
|  |     return !!namespace.get('disableEntityEvents'); | ||||||
|  | } | ||||||
|  |  | ||||||
| function reset() { | function reset() { | ||||||
|     clsHooked.reset(); |     clsHooked.reset(); | ||||||
| } | } | ||||||
| @@ -22,5 +30,7 @@ module.exports = { | |||||||
|     wrap, |     wrap, | ||||||
|     namespace, |     namespace, | ||||||
|     getSourceId, |     getSourceId, | ||||||
|  |     disableEntityEvents, | ||||||
|  |     isEntityEventsDisabled, | ||||||
|     reset |     reset | ||||||
| }; | }; | ||||||
| @@ -209,6 +209,16 @@ async function runAllChecks() { | |||||||
|             AND type != 'relation-map'`, |             AND type != 'relation-map'`, | ||||||
|         "Note has invalid type", errorList); |         "Note has invalid type", errorList); | ||||||
|  |  | ||||||
|  |     await runCheck(` | ||||||
|  |           SELECT | ||||||
|  |             noteId | ||||||
|  |           FROM | ||||||
|  |             notes | ||||||
|  |           WHERE | ||||||
|  |             isDeleted = 0 | ||||||
|  |             AND content IS NULL`, | ||||||
|  |         "Note content is null even though it is not deleted", errorList); | ||||||
|  |  | ||||||
|     await runCheck(` |     await runCheck(` | ||||||
|           SELECT  |           SELECT  | ||||||
|             parentNoteId |             parentNoteId | ||||||
|   | |||||||
| @@ -1,19 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const sanitize = require("sanitize-filename"); |  | ||||||
| const TurndownService = require('turndown'); |  | ||||||
|  |  | ||||||
| async function exportSingleMarkdown(note, res) { |  | ||||||
|     const turndownService = new TurndownService(); |  | ||||||
|     const markdown = turndownService.turndown(note.content); |  | ||||||
|     const name = sanitize(note.title); |  | ||||||
|  |  | ||||||
|     res.setHeader('Content-Disposition', 'file; filename="' + name + '.md"'); |  | ||||||
|     res.setHeader('Content-Type', 'text/markdown; charset=UTF-8'); |  | ||||||
|  |  | ||||||
|     res.send(markdown); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = { |  | ||||||
|     exportSingleMarkdown |  | ||||||
| }; |  | ||||||
| @@ -1,82 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const tar = require('tar-stream'); |  | ||||||
| const TurndownService = require('turndown'); |  | ||||||
| const sanitize = require("sanitize-filename"); |  | ||||||
| const markdownSingleExportService = require('../../services/export/markdown_single'); |  | ||||||
|  |  | ||||||
| async function exportToMarkdown(branch, res) { |  | ||||||
|     const note = await branch.getNote(); |  | ||||||
|  |  | ||||||
|     if (!await note.hasChildren()) { |  | ||||||
|         await markdownSingleExportService.exportSingleMarkdown(note, res); |  | ||||||
|  |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const turndownService = new TurndownService(); |  | ||||||
|     const pack = tar.pack(); |  | ||||||
|     const name = await exportNoteInner(note, ''); |  | ||||||
|  |  | ||||||
|     async function exportNoteInner(note, directory) { |  | ||||||
|         const childFileName = directory + sanitize(note.title); |  | ||||||
|  |  | ||||||
|         if (await note.hasLabel('excludeFromExport')) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         saveDataFile(childFileName, note); |  | ||||||
|  |  | ||||||
|         const childNotes = await note.getChildNotes(); |  | ||||||
|  |  | ||||||
|         if (childNotes.length > 0) { |  | ||||||
|             saveDirectory(childFileName); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         for (const childNote of childNotes) { |  | ||||||
|             await exportNoteInner(childNote, childFileName + "/"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return childFileName; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function saveDataFile(childFileName, note) { |  | ||||||
|         if (note.type !== 'text' && note.type !== 'code') { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (note.content.trim().length === 0) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         let markdown; |  | ||||||
|  |  | ||||||
|         if (note.type === 'code') { |  | ||||||
|             markdown = '```\n' + note.content + "\n```"; |  | ||||||
|         } |  | ||||||
|         else if (note.type === 'text') { |  | ||||||
|             markdown = turndownService.turndown(note.content); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             // other note types are not supported |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         pack.entry({name: childFileName + ".md", size: markdown.length}, markdown); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function saveDirectory(childFileName) { |  | ||||||
|         pack.entry({name: childFileName, type: 'directory'}); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pack.finalize(); |  | ||||||
|  |  | ||||||
|     res.setHeader('Content-Disposition', 'file; filename="' + name + '.tar"'); |  | ||||||
|     res.setHeader('Content-Type', 'application/tar'); |  | ||||||
|  |  | ||||||
|     pack.pipe(res); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = { |  | ||||||
|     exportToMarkdown |  | ||||||
| }; |  | ||||||
| @@ -1,103 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const html = require('html'); |  | ||||||
| const native_tar = require('tar-stream'); |  | ||||||
| const sanitize = require("sanitize-filename"); |  | ||||||
|  |  | ||||||
| async function exportToTar(branch, res) { |  | ||||||
|     const pack = native_tar.pack(); |  | ||||||
|  |  | ||||||
|     const exportedNoteIds = []; |  | ||||||
|     const name = await exportNoteInner(branch, ''); |  | ||||||
|  |  | ||||||
|     async function exportNoteInner(branch, directory) { |  | ||||||
|         const note = await branch.getNote(); |  | ||||||
|         const childFileName = directory + sanitize(note.title); |  | ||||||
|  |  | ||||||
|         if (exportedNoteIds.includes(note.noteId)) { |  | ||||||
|             saveMetadataFile(childFileName, { |  | ||||||
|                 version: 1, |  | ||||||
|                 clone: true, |  | ||||||
|                 noteId: note.noteId, |  | ||||||
|                 prefix: branch.prefix |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const metadata = { |  | ||||||
|             version: 1, |  | ||||||
|             clone: false, |  | ||||||
|             noteId: note.noteId, |  | ||||||
|             title: note.title, |  | ||||||
|             prefix: branch.prefix, |  | ||||||
|             isExpanded: branch.isExpanded, |  | ||||||
|             type: note.type, |  | ||||||
|             mime: note.mime, |  | ||||||
|             // we don't export dateCreated and dateModified of any entity since that would be a bit misleading |  | ||||||
|             attributes: (await note.getOwnedAttributes()).map(attribute => { |  | ||||||
|                 return { |  | ||||||
|                     type: attribute.type, |  | ||||||
|                     name: attribute.name, |  | ||||||
|                     value: attribute.value, |  | ||||||
|                     isInheritable: attribute.isInheritable, |  | ||||||
|                     position: attribute.position |  | ||||||
|                 }; |  | ||||||
|             }), |  | ||||||
|             links: (await note.getLinks()).map(link => { |  | ||||||
|                 return { |  | ||||||
|                     type: link.type, |  | ||||||
|                     targetNoteId: link.targetNoteId |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         if (await note.hasLabel('excludeFromExport')) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         saveMetadataFile(childFileName, metadata); |  | ||||||
|         saveDataFile(childFileName, note); |  | ||||||
|  |  | ||||||
|         exportedNoteIds.push(note.noteId); |  | ||||||
|  |  | ||||||
|         const childBranches = await note.getChildBranches(); |  | ||||||
|  |  | ||||||
|         if (childBranches.length > 0) { |  | ||||||
|             saveDirectory(childFileName); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         for (const childBranch of childBranches) { |  | ||||||
|             await exportNoteInner(childBranch, childFileName + "/"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return childFileName; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function saveDataFile(childFileName, note) { |  | ||||||
|         const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content; |  | ||||||
|  |  | ||||||
|         pack.entry({name: childFileName + ".dat", size: content.length}, content); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function saveMetadataFile(childFileName, metadata) { |  | ||||||
|         const metadataJson = JSON.stringify(metadata, null, '\t'); |  | ||||||
|  |  | ||||||
|         pack.entry({name: childFileName + ".meta", size: metadataJson.length}, metadataJson); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function saveDirectory(childFileName) { |  | ||||||
|         pack.entry({name: childFileName, type: 'directory'}); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pack.finalize(); |  | ||||||
|  |  | ||||||
|     res.setHeader('Content-Disposition', 'file; filename="' + name + '.tar"'); |  | ||||||
|     res.setHeader('Content-Type', 'application/tar'); |  | ||||||
|  |  | ||||||
|     pack.pipe(res); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = { |  | ||||||
|     exportToTar |  | ||||||
| }; |  | ||||||
| @@ -12,6 +12,11 @@ async function exportToOpml(branch, res) { | |||||||
|     async function exportNoteInner(branchId) { |     async function exportNoteInner(branchId) { | ||||||
|         const branch = await repository.getBranch(branchId); |         const branch = await repository.getBranch(branchId); | ||||||
|         const note = await branch.getNote(); |         const note = await branch.getNote(); | ||||||
|  |  | ||||||
|  |         if (await note.hasLabel('excludeFromExport')) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title; |         const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title; | ||||||
|  |  | ||||||
|         const preparedTitle = prepareText(title); |         const preparedTitle = prepareText(title); | ||||||
|   | |||||||
							
								
								
									
										57
									
								
								src/services/export/single.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/services/export/single.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const sanitize = require("sanitize-filename"); | ||||||
|  | const TurndownService = require('turndown'); | ||||||
|  | const mimeTypes = require('mime-types'); | ||||||
|  | const html = require('html'); | ||||||
|  |  | ||||||
|  | async function exportSingleNote(branch, format, res) { | ||||||
|  |     const note = await branch.getNote(); | ||||||
|  |  | ||||||
|  |     if (note.type === 'image' || note.type === 'file') { | ||||||
|  |         return [400, `Note type ${note.type} cannot be exported as single file.`]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (format !== 'html' && format !== 'markdown') { | ||||||
|  |         return [400, 'Unrecognized format ' + format]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let payload, extension, mime; | ||||||
|  |  | ||||||
|  |     if (note.type === 'text') { | ||||||
|  |         if (format === 'html') { | ||||||
|  |             payload = html.prettyPrint(note.content, {indent_size: 2}); | ||||||
|  |             extension = 'html'; | ||||||
|  |             mime = 'text/html'; | ||||||
|  |         } | ||||||
|  |         else if (format === 'markdown') { | ||||||
|  |             const turndownService = new TurndownService(); | ||||||
|  |             payload = turndownService.turndown(note.content); | ||||||
|  |             extension = 'md'; | ||||||
|  |             mime = 'text/markdown' | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     else if (note.type === 'code') { | ||||||
|  |         payload = note.content; | ||||||
|  |         extension = mimeTypes.extension(note.mime) || 'code'; | ||||||
|  |         mime = note.mime; | ||||||
|  |     } | ||||||
|  |     else if (note.type === 'relation-map' || note.type === 'search') { | ||||||
|  |         payload = note.content; | ||||||
|  |         extension = 'json'; | ||||||
|  |         mime = 'application/json'; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const name = sanitize(note.title); | ||||||
|  |  | ||||||
|  |     console.log(name, extension, mime); | ||||||
|  |  | ||||||
|  |     res.setHeader('Content-Disposition', `file; filename="${name}.${extension}"`); | ||||||
|  |     res.setHeader('Content-Type', mime + '; charset=UTF-8'); | ||||||
|  |  | ||||||
|  |     res.send(payload); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |     exportSingleNote | ||||||
|  | }; | ||||||
							
								
								
									
										229
									
								
								src/services/export/tar.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								src/services/export/tar.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,229 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const html = require('html'); | ||||||
|  | const repository = require('../repository'); | ||||||
|  | const tar = require('tar-stream'); | ||||||
|  | const sanitize = require("sanitize-filename"); | ||||||
|  | const mimeTypes = require('mime-types'); | ||||||
|  | const TurndownService = require('turndown'); | ||||||
|  | const packageInfo = require('../../../package.json'); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @param format - 'html' or 'markdown' | ||||||
|  |  */ | ||||||
|  | async function exportToTar(branch, format, res) { | ||||||
|  |     let turndownService = format === 'markdown' ? new TurndownService() : null; | ||||||
|  |  | ||||||
|  |     const pack = tar.pack(); | ||||||
|  |  | ||||||
|  |     const noteIdToMeta = {}; | ||||||
|  |  | ||||||
|  |     function getUniqueFilename(existingFileNames, fileName) { | ||||||
|  |         const lcFileName = fileName.toLowerCase(); | ||||||
|  |  | ||||||
|  |         if (lcFileName in existingFileNames) { | ||||||
|  |             let index; | ||||||
|  |             let newName; | ||||||
|  |  | ||||||
|  |             do { | ||||||
|  |                 index = existingFileNames[lcFileName]++; | ||||||
|  |  | ||||||
|  |                 newName = lcFileName + "_" + index; | ||||||
|  |             } | ||||||
|  |             while (newName in existingFileNames); | ||||||
|  |  | ||||||
|  |             return fileName + "_" + index; | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             existingFileNames[lcFileName] = 1; | ||||||
|  |  | ||||||
|  |             return fileName; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function getDataFileName(note, baseFileName, existingFileNames) { | ||||||
|  |         let extension; | ||||||
|  |  | ||||||
|  |         if (note.type === 'text' && format === 'markdown') { | ||||||
|  |             extension = 'md'; | ||||||
|  |         } | ||||||
|  |         else if (note.mime === 'application/x-javascript') { | ||||||
|  |             extension = 'js'; | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             extension = mimeTypes.extension(note.mime) || "dat"; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let fileName = baseFileName; | ||||||
|  |  | ||||||
|  |         if (!fileName.toLowerCase().endsWith(extension)) { | ||||||
|  |             fileName += "." + extension; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return getUniqueFilename(existingFileNames, fileName); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function getNote(branch, existingFileNames) { | ||||||
|  |         const note = await branch.getNote(); | ||||||
|  |  | ||||||
|  |         if (await note.hasLabel('excludeFromExport')) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const baseFileName = branch.prefix ? (branch.prefix + ' - ' + note.title) : note.title; | ||||||
|  |  | ||||||
|  |         if (note.noteId in noteIdToMeta) { | ||||||
|  |             const sanitizedFileName = sanitize(baseFileName + ".clone"); | ||||||
|  |             const fileName = getUniqueFilename(existingFileNames, sanitizedFileName); | ||||||
|  |  | ||||||
|  |             return { | ||||||
|  |                 isClone: true, | ||||||
|  |                 noteId: note.noteId, | ||||||
|  |                 prefix: branch.prefix, | ||||||
|  |                 dataFileName: fileName | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const meta = { | ||||||
|  |             isClone: false, | ||||||
|  |             noteId: note.noteId, | ||||||
|  |             title: note.title, | ||||||
|  |             notePosition: branch.notePosition, | ||||||
|  |             prefix: branch.prefix, | ||||||
|  |             isExpanded: branch.isExpanded, | ||||||
|  |             type: note.type, | ||||||
|  |             mime: note.mime, | ||||||
|  |             // we don't export dateCreated and dateModified of any entity since that would be a bit misleading | ||||||
|  |             attributes: (await note.getOwnedAttributes()).map(attribute => { | ||||||
|  |                 return { | ||||||
|  |                     type: attribute.type, | ||||||
|  |                     name: attribute.name, | ||||||
|  |                     value: attribute.value, | ||||||
|  |                     isInheritable: attribute.isInheritable, | ||||||
|  |                     position: attribute.position | ||||||
|  |                 }; | ||||||
|  |             }), | ||||||
|  |             links: (await note.getLinks()).map(link => { | ||||||
|  |                 return { | ||||||
|  |                     type: link.type, | ||||||
|  |                     targetNoteId: link.targetNoteId | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         if (note.type === 'text') { | ||||||
|  |             meta.format = format; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         noteIdToMeta[note.noteId] = meta; | ||||||
|  |  | ||||||
|  |         const childBranches = await note.getChildBranches(); | ||||||
|  |  | ||||||
|  |         // if it's a leaf then we'll export it even if it's empty | ||||||
|  |         if (note.content.length > 0 || childBranches.length === 0) { | ||||||
|  |             meta.dataFileName = getDataFileName(note, baseFileName, existingFileNames); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (childBranches.length > 0) { | ||||||
|  |             meta.dirFileName = getUniqueFilename(existingFileNames, baseFileName); | ||||||
|  |             meta.children = []; | ||||||
|  |  | ||||||
|  |             // namespace is shared by children in the same note | ||||||
|  |             const childExistingNames = {}; | ||||||
|  |  | ||||||
|  |             for (const childBranch of childBranches) { | ||||||
|  |                 const note = await getNote(childBranch, childExistingNames); | ||||||
|  |  | ||||||
|  |                 // can be undefined if export is disabled for this note | ||||||
|  |                 if (note) { | ||||||
|  |                     meta.children.push(note); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return meta; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function prepareContent(note, format) { | ||||||
|  |         if (format === 'html') { | ||||||
|  |             return html.prettyPrint(note.content, {indent_size: 2}); | ||||||
|  |         } | ||||||
|  |         else if (format === 'markdown') { | ||||||
|  |             return turndownService.turndown(note.content); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             return note.content; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // noteId => file path | ||||||
|  |     const notePaths = {}; | ||||||
|  |  | ||||||
|  |     async function saveNote(noteMeta, path) { | ||||||
|  |         if (noteMeta.isClone) { | ||||||
|  |             const content = "Note is present at " + notePaths[noteMeta.noteId]; | ||||||
|  |  | ||||||
|  |             pack.entry({name: path + noteMeta.dataFileName, size: content.length}, content); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const note = await repository.getNote(noteMeta.noteId); | ||||||
|  |  | ||||||
|  |         notePaths[note.noteId] = path + (noteMeta.dataFileName || noteMeta.dirFileName); | ||||||
|  |  | ||||||
|  |         if (noteMeta.dataFileName) { | ||||||
|  |             const content = prepareContent(note, noteMeta.format); | ||||||
|  |  | ||||||
|  |             pack.entry({name: path + noteMeta.dataFileName, size: content.length}, content); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (noteMeta.children && noteMeta.children.length > 0) { | ||||||
|  |             const directoryPath = path + noteMeta.dirFileName; | ||||||
|  |  | ||||||
|  |             pack.entry({name: directoryPath, type: 'directory'}); | ||||||
|  |  | ||||||
|  |             for (const childMeta of noteMeta.children) { | ||||||
|  |                 await saveNote(childMeta, directoryPath + '/'); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const metaFile = { | ||||||
|  |         formatVersion: 1, | ||||||
|  |         appVersion: packageInfo.version, | ||||||
|  |         files: [ | ||||||
|  |             await getNote(branch, []) | ||||||
|  |         ] | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     for (const noteMeta of Object.values(noteIdToMeta)) { | ||||||
|  |         // filter out relations and links which are not inside this export | ||||||
|  |         noteMeta.attributes = noteMeta.attributes.filter(attr => attr.type !== 'relation' || attr.value in noteIdToMeta); | ||||||
|  |         noteMeta.links = noteMeta.links.filter(link => link.targetNoteId in noteIdToMeta); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!metaFile.files[0]) { // corner case of disabled export for exported note | ||||||
|  |         res.sendStatus(400); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const metaFileJson = JSON.stringify(metaFile, null, '\t'); | ||||||
|  |  | ||||||
|  |     pack.entry({name: "!!!meta.json", size: metaFileJson.length}, metaFileJson); | ||||||
|  |  | ||||||
|  |     await saveNote(metaFile.files[0], ''); | ||||||
|  |  | ||||||
|  |     pack.finalize(); | ||||||
|  |  | ||||||
|  |     const note = await branch.getNote(); | ||||||
|  |     const tarFileName = sanitize((branch.prefix ? (branch.prefix + " - ") : "") + note.title); | ||||||
|  |  | ||||||
|  |     res.setHeader('Content-Disposition', `file; filename="${tarFileName}.tar"`); | ||||||
|  |     res.setHeader('Content-Type', 'application/tar'); | ||||||
|  |  | ||||||
|  |     pack.pipe(res); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |     exportToTar | ||||||
|  | }; | ||||||
| @@ -59,7 +59,7 @@ eventService.subscribe(eventService.CHILD_NOTE_CREATED, async ({ parentNote, chi | |||||||
|     await runAttachedRelations(parentNote, 'runOnChildNoteCreation', childNote); |     await runAttachedRelations(parentNote, 'runOnChildNoteCreation', childNote); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| async function processMirrorRelations(entityName, entity, handler) { | async function processInverseRelations(entityName, entity, handler) { | ||||||
|     if (entityName === 'attributes' && entity.type === 'relation') { |     if (entityName === 'attributes' && entity.type === 'relation') { | ||||||
|         const note = await entity.getNote(); |         const note = await entity.getNote(); | ||||||
|         const attributes = (await note.getAttributes(entity.name)).filter(relation => relation.type === 'relation-definition'); |         const attributes = (await note.getAttributes(entity.name)).filter(relation => relation.type === 'relation-definition'); | ||||||
| @@ -67,7 +67,7 @@ async function processMirrorRelations(entityName, entity, handler) { | |||||||
|         for (const attribute of attributes) { |         for (const attribute of attributes) { | ||||||
|             const definition = attribute.value; |             const definition = attribute.value; | ||||||
|  |  | ||||||
|             if (definition.mirrorRelation && definition.mirrorRelation.trim()) { |             if (definition.inverseRelation && definition.inverseRelation.trim()) { | ||||||
|                 const targetNote = await entity.getTargetNote(); |                 const targetNote = await entity.getTargetNote(); | ||||||
|  |  | ||||||
|                 await handler(definition, note, targetNote); |                 await handler(definition, note, targetNote); | ||||||
| @@ -77,17 +77,17 @@ async function processMirrorRelations(entityName, entity, handler) { | |||||||
| } | } | ||||||
|  |  | ||||||
| eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityName, entity }) => { | eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityName, entity }) => { | ||||||
|     await processMirrorRelations(entityName, entity, async (definition, note, targetNote) => { |     await processInverseRelations(entityName, entity, async (definition, note, targetNote) => { | ||||||
|         // we need to make sure that also target's mirror attribute exists and if note, then create it |         // we need to make sure that also target's inverse attribute exists and if note, then create it | ||||||
|         // mirror attribute has to target our note as well |         // inverse attribute has to target our note as well | ||||||
|         const hasMirrorAttribute = (await targetNote.getRelations(definition.mirrorRelation)) |         const hasInverseAttribute = (await targetNote.getRelations(definition.inverseRelation)) | ||||||
|             .some(attr => attr.value === note.noteId); |             .some(attr => attr.value === note.noteId); | ||||||
|  |  | ||||||
|         if (!hasMirrorAttribute) { |         if (!hasInverseAttribute) { | ||||||
|             await new Attribute({ |             await new Attribute({ | ||||||
|                 noteId: targetNote.noteId, |                 noteId: targetNote.noteId, | ||||||
|                 type: 'relation', |                 type: 'relation', | ||||||
|                 name: definition.mirrorRelation, |                 name: definition.inverseRelation, | ||||||
|                 value: note.noteId, |                 value: note.noteId, | ||||||
|                 isInheritable: entity.isInheritable |                 isInheritable: entity.isInheritable | ||||||
|             }).save(); |             }).save(); | ||||||
| @@ -98,9 +98,9 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityName, entity | |||||||
| }); | }); | ||||||
|  |  | ||||||
| eventService.subscribe(eventService.ENTITY_DELETED, async ({ entityName, entity }) => { | eventService.subscribe(eventService.ENTITY_DELETED, async ({ entityName, entity }) => { | ||||||
|     await processMirrorRelations(entityName, entity, async (definition, note, targetNote) => { |     await processInverseRelations(entityName, entity, async (definition, note, targetNote) => { | ||||||
|         // if one mirror attribute is deleted then the other should be deleted as well |         // if one inverse attribute is deleted then the other should be deleted as well | ||||||
|         const relations = await targetNote.getRelations(definition.mirrorRelation); |         const relations = await targetNote.getRelations(definition.inverseRelation); | ||||||
|         let deletedSomething = false; |         let deletedSomething = false; | ||||||
|  |  | ||||||
|         for (const relation of relations) { |         for (const relation of relations) { | ||||||
|   | |||||||
| @@ -1,30 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| // note that this is for import of single markdown file only - for archive/structure of markdown files |  | ||||||
| // see tar export/import |  | ||||||
|  |  | ||||||
| const noteService = require('../../services/notes'); |  | ||||||
| const commonmark = require('commonmark'); |  | ||||||
|  |  | ||||||
| async function importMarkdown(file, parentNote) { |  | ||||||
|     const markdownContent = file.buffer.toString("UTF-8"); |  | ||||||
|  |  | ||||||
|     const reader = new commonmark.Parser(); |  | ||||||
|     const writer = new commonmark.HtmlRenderer(); |  | ||||||
|  |  | ||||||
|     const parsed = reader.parse(markdownContent); |  | ||||||
|     const htmlContent = writer.render(parsed); |  | ||||||
|  |  | ||||||
|     const title = file.originalname.substr(0, file.originalname.length - 3); // strip .md extension |  | ||||||
|  |  | ||||||
|     const {note} = await noteService.createNote(parentNote.noteId, title, htmlContent, { |  | ||||||
|         type: 'text', |  | ||||||
|         mime: 'text/html' |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return note; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = { |  | ||||||
|     importMarkdown |  | ||||||
| }; |  | ||||||
							
								
								
									
										47
									
								
								src/services/import/single.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/services/import/single.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const noteService = require('../../services/notes'); | ||||||
|  | const commonmark = require('commonmark'); | ||||||
|  | const path = require('path'); | ||||||
|  |  | ||||||
|  | async function importMarkdown(file, parentNote) { | ||||||
|  |     const markdownContent = file.buffer.toString("UTF-8"); | ||||||
|  |  | ||||||
|  |     const reader = new commonmark.Parser(); | ||||||
|  |     const writer = new commonmark.HtmlRenderer(); | ||||||
|  |  | ||||||
|  |     const parsed = reader.parse(markdownContent); | ||||||
|  |     const htmlContent = writer.render(parsed); | ||||||
|  |  | ||||||
|  |     const title = getFileNameWithoutExtension(file.originalname); | ||||||
|  |  | ||||||
|  |     const {note} = await noteService.createNote(parentNote.noteId, title, htmlContent, { | ||||||
|  |         type: 'text', | ||||||
|  |         mime: 'text/html' | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return note; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function importHtml(file, parentNote) { | ||||||
|  |     const title = getFileNameWithoutExtension(file.originalname); | ||||||
|  |     const content = file.buffer.toString("UTF-8"); | ||||||
|  |  | ||||||
|  |     const {note} = await noteService.createNote(parentNote.noteId, title, content, { | ||||||
|  |         type: 'text', | ||||||
|  |         mime: 'text/html' | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return note; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getFileNameWithoutExtension(filePath) { | ||||||
|  |     const extension = path.extname(filePath); | ||||||
|  |  | ||||||
|  |     return filePath.substr(0, filePath.length - extension.length); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |     importMarkdown, | ||||||
|  |     importHtml | ||||||
|  | }; | ||||||
| @@ -2,30 +2,32 @@ | |||||||
|  |  | ||||||
| const Attribute = require('../../entities/attribute'); | const Attribute = require('../../entities/attribute'); | ||||||
| const Link = require('../../entities/link'); | const Link = require('../../entities/link'); | ||||||
| const log = require('../../services/log'); |  | ||||||
| const utils = require('../../services/utils'); | const utils = require('../../services/utils'); | ||||||
|  | const log = require('../../services/log'); | ||||||
|  | const repository = require('../../services/repository'); | ||||||
| const noteService = require('../../services/notes'); | const noteService = require('../../services/notes'); | ||||||
| const Branch = require('../../entities/branch'); | const Branch = require('../../entities/branch'); | ||||||
| const tar = require('tar-stream'); | const tar = require('tar-stream'); | ||||||
| const stream = require('stream'); | const stream = require('stream'); | ||||||
| const path = require('path'); | const path = require('path'); | ||||||
| const commonmark = require('commonmark'); | const commonmark = require('commonmark'); | ||||||
|  | const mimeTypes = require('mime-types'); | ||||||
|  |  | ||||||
| async function importTar(fileBuffer, parentNote) { | async function importTar(fileBuffer, importRootNote) { | ||||||
|     const files = await parseImportFile(fileBuffer); |  | ||||||
|  |  | ||||||
|     const ctx = { |  | ||||||
|     // maps from original noteId (in tar file) to newly generated noteId |     // maps from original noteId (in tar file) to newly generated noteId | ||||||
|         noteIdMap: {}, |     const noteIdMap = {}; | ||||||
|         // new noteIds of notes which were actually created (not just referenced) |     const attributes = []; | ||||||
|         createdNoteIds: [], |     const links = []; | ||||||
|         attributes: [], |     // path => noteId | ||||||
|         links: [], |     const createdPaths = { '/': importRootNote.noteId, '\\': importRootNote.noteId }; | ||||||
|         reader: new commonmark.Parser(), |     const mdReader = new commonmark.Parser(); | ||||||
|         writer: new commonmark.HtmlRenderer() |     const mdWriter = new commonmark.HtmlRenderer(); | ||||||
|     }; |     let metaFile = null; | ||||||
|  |     let firstNote = null; | ||||||
|  |  | ||||||
|     ctx.getNewNoteId = function(origNoteId) { |     const extract = tar.extract(); | ||||||
|  |  | ||||||
|  |     function getNewNoteId(origNoteId) { | ||||||
|         // in case the original noteId is empty. This probably shouldn't happen, but still good to have this precaution |         // in case the original noteId is empty. This probably shouldn't happen, but still good to have this precaution | ||||||
|         if (!origNoteId.trim()) { |         if (!origNoteId.trim()) { | ||||||
|             return ""; |             return ""; | ||||||
| @@ -36,107 +38,274 @@ async function importTar(fileBuffer, parentNote) { | |||||||
|             return origNoteId; |             return origNoteId; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (!ctx.noteIdMap[origNoteId]) { |         if (!noteIdMap[origNoteId]) { | ||||||
|             ctx.noteIdMap[origNoteId] = utils.newEntityId(); |             noteIdMap[origNoteId] = utils.newEntityId(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return ctx.noteIdMap[origNoteId]; |         return noteIdMap[origNoteId]; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     function getMeta(filePath) { | ||||||
|  |         if (!metaFile) { | ||||||
|  |             return {}; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const pathSegments = filePath.split(/[\/\\]/g); | ||||||
|  |  | ||||||
|  |         let cursor = { | ||||||
|  |             isImportRoot: true, | ||||||
|  |             children: metaFile.files | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|     const note = await importNotes(ctx, files, parentNote.noteId); |         let parent; | ||||||
|  |  | ||||||
|     // we save attributes and links after importing notes because we need to check that target noteIds |         for (const segment of pathSegments) { | ||||||
|     // have been really created (relation/links with targets outside of the export are not created) |             if (!cursor || !cursor.children || cursor.children.length === 0) { | ||||||
|  |                 return {}; | ||||||
|     for (const attr of ctx.attributes) { |  | ||||||
|         if (attr.type === 'relation') { |  | ||||||
|             attr.value = ctx.getNewNoteId(attr.value); |  | ||||||
|  |  | ||||||
|             if (!ctx.createdNoteIds.includes(attr.value)) { |  | ||||||
|                 // relation targets note outside of the export |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|             } |             } | ||||||
|  |  | ||||||
|         await new Attribute(attr).save(); |             parent = cursor; | ||||||
|  |             cursor = cursor.children.find(file => file.dataFileName === segment || file.dirFileName === segment); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     for (const link of ctx.links) { |         return { | ||||||
|         link.targetNoteId = ctx.getNewNoteId(link.targetNoteId); |             parentNoteMeta: parent, | ||||||
|  |             noteMeta: cursor | ||||||
|         if (!ctx.createdNoteIds.includes(link.targetNoteId)) { |         }; | ||||||
|             // link targets note outside of the export |  | ||||||
|             continue; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|         await new Link(link).save(); |     function getParentNoteId(filePath, parentNoteMeta) { | ||||||
|     } |         let parentNoteId; | ||||||
|  |  | ||||||
|     return note; |         if (parentNoteMeta) { | ||||||
| } |             parentNoteId = parentNoteMeta.isImportRoot ? importRootNote.noteId : getNewNoteId(parentNoteMeta.noteId); | ||||||
|  |  | ||||||
| function getFileName(name) { |  | ||||||
|     let key; |  | ||||||
|  |  | ||||||
|     if (name.endsWith(".dat")) { |  | ||||||
|         key = "data"; |  | ||||||
|         name = name.substr(0, name.length - 4); |  | ||||||
|     } |  | ||||||
|     else if (name.endsWith(".md")) { |  | ||||||
|         key = "markdown"; |  | ||||||
|         name = name.substr(0, name.length - 3); |  | ||||||
|     } |  | ||||||
|     else if (name.endsWith((".meta"))) { |  | ||||||
|         key = "meta"; |  | ||||||
|         name = name.substr(0, name.length - 5); |  | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|         log.error("Unknown file type in import: " + name); |             const parentPath = path.dirname(filePath); | ||||||
|  |  | ||||||
|  |             if (parentPath === '.') { | ||||||
|  |                 parentNoteId = importRootNote.noteId; | ||||||
|             } |             } | ||||||
|  |             else if (parentPath in createdPaths) { | ||||||
|     return {name, key}; |                 parentNoteId = createdPaths[parentPath]; | ||||||
| } |  | ||||||
|  |  | ||||||
| async function parseImportFile(fileBuffer) { |  | ||||||
|     const fileMap = {}; |  | ||||||
|     const files = []; |  | ||||||
|  |  | ||||||
|     const extract = tar.extract(); |  | ||||||
|  |  | ||||||
|     extract.on('entry', function(header, stream, next) { |  | ||||||
|         let name, key; |  | ||||||
|  |  | ||||||
|         if (header.type === 'file') { |  | ||||||
|             ({name, key} = getFileName(header.name)); |  | ||||||
|         } |  | ||||||
|         else if (header.type === 'directory') { |  | ||||||
|             // directory entries in tar often end with directory separator |  | ||||||
|             name = (header.name.endsWith("/") || header.name.endsWith("\\")) ? header.name.substr(0, header.name.length - 1) : header.name; |  | ||||||
|             key = 'directory'; |  | ||||||
|             } |             } | ||||||
|             else { |             else { | ||||||
|             log.error("Unrecognized tar entry: " + JSON.stringify(header)); |                 throw new Error(`Could not find existing path ${parentPath} for ${filePath}.`); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return parentNoteId; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function getNoteTitle(filePath, noteMeta) { | ||||||
|  |         if (noteMeta) { | ||||||
|  |             return noteMeta.title; | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             const basename = path.basename(filePath); | ||||||
|  |  | ||||||
|  |             return getTextFileWithoutExtension(basename); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function getNoteId(noteMeta, filePath) { | ||||||
|  |         if (noteMeta) { | ||||||
|  |             return getNewNoteId(noteMeta.noteId); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             const filePathNoExt = getTextFileWithoutExtension(filePath); | ||||||
|  |  | ||||||
|  |             if (filePathNoExt in createdPaths) { | ||||||
|  |                 return createdPaths[filePathNoExt]; | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 return utils.newEntityId(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function detectFileTypeAndMime(filePath) { | ||||||
|  |         const mime = mimeTypes.lookup(filePath); | ||||||
|  |         let type = 'file'; | ||||||
|  |  | ||||||
|  |         if (mime) { | ||||||
|  |             if (mime === 'text/html' || mime === 'text/markdown') { | ||||||
|  |                 type = 'text'; | ||||||
|  |             } | ||||||
|  |             else if (mime.startsWith('image/')) { | ||||||
|  |                 type = 'image'; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return { type, mime }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function saveAttributesAndLinks(note, noteMeta) { | ||||||
|  |         if (!noteMeta) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         let file = fileMap[name]; |         for (const attr of noteMeta.attributes) { | ||||||
|  |             attr.noteId = note.noteId; | ||||||
|  |  | ||||||
|         if (!file) { |             if (attr.type === 'relation') { | ||||||
|             file = fileMap[name] = { |                 attr.value = getNewNoteId(attr.value); | ||||||
|                 name: path.basename(name), |             } | ||||||
|                 children: [] |  | ||||||
|             }; |  | ||||||
|  |  | ||||||
|             let parentFileName = path.dirname(header.name); |             attributes.push(attr); | ||||||
|  |         } | ||||||
|  |  | ||||||
|             if (parentFileName && parentFileName !== '.') { |         for (const link of noteMeta.links) { | ||||||
|                 fileMap[parentFileName].children.push(file); |             link.noteId = note.noteId; | ||||||
|  |             link.targetNoteId = getNewNoteId(link.targetNoteId); | ||||||
|  |  | ||||||
|  |             links.push(link); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function saveDirectory(filePath) { | ||||||
|  |         const { parentNoteMeta, noteMeta } = getMeta(filePath); | ||||||
|  |  | ||||||
|  |         const noteId = getNoteId(noteMeta, filePath); | ||||||
|  |         const noteTitle = getNoteTitle(filePath, noteMeta); | ||||||
|  |         const parentNoteId = getParentNoteId(filePath, parentNoteMeta); | ||||||
|  |  | ||||||
|  |         let note = await repository.getNote(noteId); | ||||||
|  |  | ||||||
|  |         if (note) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ({note} = await noteService.createNote(parentNoteId, noteTitle, '', { | ||||||
|  |             noteId, | ||||||
|  |             type: noteMeta ? noteMeta.type : 'text', | ||||||
|  |             mime: noteMeta ? noteMeta.mime : 'text/html', | ||||||
|  |             prefix: noteMeta ? noteMeta.prefix : '', | ||||||
|  |             isExpanded: noteMeta ? noteMeta.isExpanded : false | ||||||
|  |         })); | ||||||
|  |  | ||||||
|  |         await saveAttributesAndLinks(note, noteMeta); | ||||||
|  |  | ||||||
|  |         if (!firstNote) { | ||||||
|  |             firstNote = note; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         createdPaths[filePath] = noteId; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function getTextFileWithoutExtension(filePath) { | ||||||
|  |         const extension = path.extname(filePath).toLowerCase(); | ||||||
|  |  | ||||||
|  |         if (extension === '.md' || extension === '.html') { | ||||||
|  |             return filePath.substr(0, filePath.length - extension.length); | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|                 files.push(file); |             return filePath; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async function saveNote(filePath, content) { | ||||||
|  |         const {parentNoteMeta, noteMeta} = getMeta(filePath); | ||||||
|  |  | ||||||
|  |         const noteId = getNoteId(noteMeta, filePath); | ||||||
|  |         const parentNoteId = getParentNoteId(filePath, parentNoteMeta); | ||||||
|  |  | ||||||
|  |         if (noteMeta && noteMeta.isClone) { | ||||||
|  |             await new Branch({ | ||||||
|  |                 noteId, | ||||||
|  |                 parentNoteId, | ||||||
|  |                 isExpanded: noteMeta.isExpanded, | ||||||
|  |                 prefix: noteMeta.prefix, | ||||||
|  |                 notePosition: noteMeta.notePosition | ||||||
|  |             }).save(); | ||||||
|  |  | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const {type, mime} = noteMeta ? noteMeta : detectFileTypeAndMime(filePath); | ||||||
|  |  | ||||||
|  |         if (type !== 'file' && type !== 'image') { | ||||||
|  |             content = content.toString("UTF-8"); | ||||||
|  |  | ||||||
|  |             if (noteMeta) { | ||||||
|  |                 // this will replace all internal links (<a> and <img>) inside the body | ||||||
|  |                 // links pointing outside the export will be broken and changed (ctx.getNewNoteId() will still assign new noteId) | ||||||
|  |                 for (const link of noteMeta.links || []) { | ||||||
|  |                     // no need to escape the regexp find string since it's a noteId which doesn't contain any special characters | ||||||
|  |                     content = content.replace(new RegExp(link.targetNoteId, "g"), getNewNoteId(link.targetNoteId)); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if ((noteMeta && noteMeta.format === 'markdown') || (!noteMeta && mime === 'text/markdown')) { | ||||||
|  |             const parsed = mdReader.parse(content); | ||||||
|  |             content = mdWriter.render(parsed); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let note = await repository.getNote(noteId); | ||||||
|  |  | ||||||
|  |         if (note) { | ||||||
|  |             note.content = content; | ||||||
|  |             await note.save(); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             const noteTitle = getNoteTitle(filePath, noteMeta); | ||||||
|  |  | ||||||
|  |             ({note} = await noteService.createNote(parentNoteId, noteTitle, content, { | ||||||
|  |                 noteId, | ||||||
|  |                 type, | ||||||
|  |                 mime, | ||||||
|  |                 prefix: noteMeta ? noteMeta.prefix : '', | ||||||
|  |                 isExpanded: noteMeta ? noteMeta.isExpanded : false, | ||||||
|  |                 notePosition: noteMeta ? noteMeta.notePosition : false | ||||||
|  |             })); | ||||||
|  |  | ||||||
|  |             await saveAttributesAndLinks(note, noteMeta); | ||||||
|  |  | ||||||
|  |             if (!noteMeta && (type === 'file' || type === 'image')) { | ||||||
|  |                 attributes.push({ | ||||||
|  |                     noteId, | ||||||
|  |                     type: 'label', | ||||||
|  |                     name: 'originalFileName', | ||||||
|  |                     value: path.basename(filePath) | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |                 attributes.push({ | ||||||
|  |                     noteId, | ||||||
|  |                     type: 'label', | ||||||
|  |                     name: 'fileSize', | ||||||
|  |                     value: content.byteLength | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (!firstNote) { | ||||||
|  |                 firstNote = note; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (type === 'text') { | ||||||
|  |                 filePath = getTextFileWithoutExtension(filePath); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             createdPaths[filePath] = noteId; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @return path without leading or trailing slash and backslashes converted to forward ones*/ | ||||||
|  |     function normalizeFilePath(filePath) { | ||||||
|  |         filePath = filePath.replace(/\\/g, "/"); | ||||||
|  |  | ||||||
|  |         if (filePath.startsWith("/")) { | ||||||
|  |             filePath = filePath.substr(1); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (filePath.endsWith("/")) { | ||||||
|  |             filePath = filePath.substr(0, filePath.length - 1); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return filePath; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     extract.on('entry', function(header, stream, next) { | ||||||
|         const chunks = []; |         const chunks = []; | ||||||
|  |  | ||||||
|         stream.on("data", function (chunk) { |         stream.on("data", function (chunk) { | ||||||
| @@ -147,11 +316,22 @@ async function parseImportFile(fileBuffer) { | |||||||
|         // stream is the content body (might be an empty stream) |         // stream is the content body (might be an empty stream) | ||||||
|         // call next when you are done with this entry |         // call next when you are done with this entry | ||||||
|  |  | ||||||
|         stream.on('end', function() { |         stream.on('end', async function() { | ||||||
|             file[key] = Buffer.concat(chunks); |             let filePath = normalizeFilePath(header.name); | ||||||
|  |  | ||||||
|             if (key === "meta") { |             const content = Buffer.concat(chunks); | ||||||
|                 file[key] = JSON.parse(file[key].toString("UTF-8")); |  | ||||||
|  |             if (filePath === '!!!meta.json') { | ||||||
|  |                 metaFile = JSON.parse(content.toString("UTF-8")); | ||||||
|  |             } | ||||||
|  |             else if (header.type === 'directory') { | ||||||
|  |                 await saveDirectory(filePath); | ||||||
|  |             } | ||||||
|  |             else if (header.type === 'file') { | ||||||
|  |                 await saveNote(filePath, content); | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 log.info("Ignoring tar import entry with type " + header.type); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             next(); // ready for next entry |             next(); // ready for next entry | ||||||
| @@ -161,8 +341,34 @@ async function parseImportFile(fileBuffer) { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     return new Promise(resolve => { |     return new Promise(resolve => { | ||||||
|         extract.on('finish', function() { |         extract.on('finish', async function() { | ||||||
|             resolve(files); |             const createdNoteIds = {}; | ||||||
|  |  | ||||||
|  |             for (const path in createdPaths) { | ||||||
|  |                 createdNoteIds[createdPaths[path]] = true; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // we're saving attributes and links only now so that all relation and link target notes | ||||||
|  |             // are already in the database (we don't want to have "broken" relations, not even transitionally) | ||||||
|  |             for (const attr of attributes) { | ||||||
|  |                 if (attr.type !== 'relation' || attr.value in createdNoteIds) { | ||||||
|  |                     await new Attribute(attr).save(); | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|  |                     log.info("Relation not imported since target note doesn't exist: " + JSON.stringify(attr)); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             for (const link of links) { | ||||||
|  |                 if (link.targetNoteId in createdNoteIds) { | ||||||
|  |                     await new Link(link).save(); | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|  |                     log.info("Link not imported since target note doesn't exist: " + JSON.stringify(link)); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             resolve(firstNote); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         const bufferStream = new stream.PassThrough(); |         const bufferStream = new stream.PassThrough(); | ||||||
| @@ -172,96 +378,6 @@ async function parseImportFile(fileBuffer) { | |||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function importNotes(ctx, files, parentNoteId) { |  | ||||||
|     let returnNote = null; |  | ||||||
|  |  | ||||||
|     for (const file of files) { |  | ||||||
|         let note; |  | ||||||
|  |  | ||||||
|         if (!file.meta) { |  | ||||||
|             let content = ''; |  | ||||||
|  |  | ||||||
|             if (file.data) { |  | ||||||
|                 content = file.data.toString("UTF-8"); |  | ||||||
|             } |  | ||||||
|             else if (file.markdown) { |  | ||||||
|                 const parsed = ctx.reader.parse(file.markdown.toString("UTF-8")); |  | ||||||
|                 content = ctx.writer.render(parsed); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             note = (await noteService.createNote(parentNoteId, file.name, content, { |  | ||||||
|                 type: 'text', |  | ||||||
|                 mime: 'text/html' |  | ||||||
|             })).note; |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             if (file.meta.version !== 1) { |  | ||||||
|                 throw new Error("Can't read meta data version " + file.meta.version); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (file.meta.clone) { |  | ||||||
|                 await new Branch({ |  | ||||||
|                     parentNoteId: parentNoteId, |  | ||||||
|                     noteId: ctx.getNewNoteId(file.meta.noteId), |  | ||||||
|                     prefix: file.meta.prefix, |  | ||||||
|                     isExpanded: !!file.meta.isExpanded |  | ||||||
|                 }).save(); |  | ||||||
|  |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (file.meta.type !== 'file' && file.meta.type !== 'image') { |  | ||||||
|                 file.data = file.data.toString("UTF-8"); |  | ||||||
|  |  | ||||||
|                 // this will replace all internal links (<a> and <img>) inside the body |  | ||||||
|                 // links pointing outside the export will be broken and changed (ctx.getNewNoteId() will still assign new noteId) |  | ||||||
|                 for (const link of file.meta.links || []) { |  | ||||||
|                     // no need to escape the regexp find string since it's a noteId which doesn't contain any special characters |  | ||||||
|                     file.data = file.data.replace(new RegExp(link.targetNoteId, "g"), ctx.getNewNoteId(link.targetNoteId)); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             note = (await noteService.createNote(parentNoteId, file.meta.title, file.data, { |  | ||||||
|                 noteId: ctx.getNewNoteId(file.meta.noteId), |  | ||||||
|                 type: file.meta.type, |  | ||||||
|                 mime: file.meta.mime, |  | ||||||
|                 prefix: file.meta.prefix, |  | ||||||
|                 isExpanded: !!file.meta.isExpanded |  | ||||||
|             })).note; |  | ||||||
|  |  | ||||||
|             ctx.createdNoteIds.push(note.noteId); |  | ||||||
|  |  | ||||||
|             for (const attribute of file.meta.attributes || []) { |  | ||||||
|                 ctx.attributes.push({ |  | ||||||
|                     noteId: note.noteId, |  | ||||||
|                     type: attribute.type, |  | ||||||
|                     name: attribute.name, |  | ||||||
|                     value: attribute.value, |  | ||||||
|                     isInheritable: attribute.isInheritable, |  | ||||||
|                     position: attribute.position |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             for (const link of file.meta.links || []) { |  | ||||||
|                 ctx.links.push({ |  | ||||||
|                     noteId: note.noteId, |  | ||||||
|                     type: link.type, |  | ||||||
|                     targetNoteId: link.targetNoteId |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // first created note will be activated after import |  | ||||||
|         returnNote = returnNote || note; |  | ||||||
|  |  | ||||||
|         if (file.children.length > 0) { |  | ||||||
|             await importNotes(ctx, file.children, note.noteId); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return returnNote; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     importTar |     importTar | ||||||
| }; | }; | ||||||
| @@ -38,7 +38,8 @@ async function load() { | |||||||
| function highlightResults(results, allTokens) { | function highlightResults(results, allTokens) { | ||||||
|     // we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks |     // we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks | ||||||
|     // which would make the resulting HTML string invalid. |     // which would make the resulting HTML string invalid. | ||||||
|     allTokens = allTokens.map(token => token.replace('/</g', '')); |     // { and } are used for marking <b> and </b> tag (to avoid matches on single 'b' character) | ||||||
|  |     allTokens = allTokens.map(token => token.replace('/[<\{\}]/g', '')); | ||||||
|  |  | ||||||
|     // sort by the longest so we first highlight longest matches |     // sort by the longest so we first highlight longest matches | ||||||
|     allTokens.sort((a, b) => a.length > b.length ? -1 : 1); |     allTokens.sort((a, b) => a.length > b.length ? -1 : 1); | ||||||
| @@ -51,9 +52,15 @@ function highlightResults(results, allTokens) { | |||||||
|         const tokenRegex = new RegExp("(" + utils.escapeRegExp(token) + ")", "gi"); |         const tokenRegex = new RegExp("(" + utils.escapeRegExp(token) + ")", "gi"); | ||||||
|  |  | ||||||
|         for (const result of results) { |         for (const result of results) { | ||||||
|             result.highlighted = result.highlighted.replace(tokenRegex, "<b>$1</b>"); |             result.highlighted = result.highlighted.replace(tokenRegex, "{$1}"); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     for (const result of results) { | ||||||
|  |         result.highlighted = result.highlighted | ||||||
|  |             .replace(/{/g, "<b>") | ||||||
|  |             .replace(/}/g, "</b>"); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| function findNotes(query) { | function findNotes(query) { | ||||||
| @@ -80,7 +87,7 @@ function findNotes(query) { | |||||||
|             continue; |             continue; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // for leaf note it doesn't matter if "archived" label inheritable or not |         // for leaf note it doesn't matter if "archived" label is inheritable or not | ||||||
|         if (noteId in archived) { |         if (noteId in archived) { | ||||||
|             continue; |             continue; | ||||||
|         } |         } | ||||||
| @@ -113,11 +120,28 @@ function findNotes(query) { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     results.sort((a, b) => a.title < b.title ? -1 : 1); |     // sort results by depth of the note. This is based on the assumption that more important results | ||||||
|  |     // are closer to the note root. | ||||||
|  |     results.sort((a, b) => { | ||||||
|  |         if (a.pathArray.length === b.pathArray.length) { | ||||||
|  |             return a.title < b.title ? -1 : 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|     highlightResults(results, allTokens); |         return a.pathArray.length < b.pathArray.length ? -1 : 1; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     return results; |     const apiResults = results.slice(0, 200).map(res => { | ||||||
|  |         return { | ||||||
|  |             noteId: res.noteId, | ||||||
|  |             branchId: res.branchId, | ||||||
|  |             path: res.pathArray.join('/'), | ||||||
|  |             title: res.titleArray.join(' / ') | ||||||
|  |         }; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     highlightResults(apiResults, allTokens); | ||||||
|  |  | ||||||
|  |     return apiResults; | ||||||
| } | } | ||||||
|  |  | ||||||
| function search(noteId, tokens, path, results) { | function search(noteId, tokens, path, results) { | ||||||
| @@ -125,15 +149,14 @@ function search(noteId, tokens, path, results) { | |||||||
|         const retPath = getSomePath(noteId, path); |         const retPath = getSomePath(noteId, path); | ||||||
|  |  | ||||||
|         if (retPath) { |         if (retPath) { | ||||||
|             const noteTitle = getNoteTitleForPath(retPath); |  | ||||||
|             const thisNoteId = retPath[retPath.length - 1]; |             const thisNoteId = retPath[retPath.length - 1]; | ||||||
|             const thisParentNoteId = retPath[retPath.length - 2]; |             const thisParentNoteId = retPath[retPath.length - 2]; | ||||||
|  |  | ||||||
|             results.push({ |             results.push({ | ||||||
|                 noteId: thisNoteId, |                 noteId: thisNoteId, | ||||||
|                 branchId: childParentToBranchId[`${thisNoteId}-${thisParentNoteId}`], |                 branchId: childParentToBranchId[`${thisNoteId}-${thisParentNoteId}`], | ||||||
|                 title: noteTitle, |                 pathArray: retPath, | ||||||
|                 path: retPath.join('/') |                 titleArray: getNoteTitleArrayForPath(retPath) | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -146,10 +169,6 @@ function search(noteId, tokens, path, results) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     for (const parentNoteId of parents) { |     for (const parentNoteId of parents) { | ||||||
|         if (results.length >= 200) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // archived must be inheritable |         // archived must be inheritable | ||||||
|         if (archived[parentNoteId] === 1) { |         if (archived[parentNoteId] === 1) { | ||||||
|             continue; |             continue; | ||||||
| @@ -192,12 +211,12 @@ function getNoteTitle(noteId, parentNoteId) { | |||||||
|     return (prefix ? (prefix + ' - ') : '') + title; |     return (prefix ? (prefix + ' - ') : '') + title; | ||||||
| } | } | ||||||
|  |  | ||||||
| function getNoteTitleForPath(path) { | function getNoteTitleArrayForPath(path) { | ||||||
|     const titles = []; |     const titles = []; | ||||||
|  |  | ||||||
|     if (path[0] === 'root') { |     if (path[0] === 'root') { | ||||||
|         if (path.length === 1) { |         if (path.length === 1) { | ||||||
|             return getNoteTitle('root'); |             return [ getNoteTitle('root') ]; | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             path = path.slice(1); |             path = path.slice(1); | ||||||
| @@ -213,6 +232,12 @@ function getNoteTitleForPath(path) { | |||||||
|         parentNoteId = noteId; |         parentNoteId = noteId; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     return titles; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getNoteTitleForPath(path) { | ||||||
|  |     const titles = getNoteTitleArrayForPath(path); | ||||||
|  |  | ||||||
|     return titles.join(' / '); |     return titles.join(' / '); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -309,11 +334,11 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entity}) | |||||||
|  |  | ||||||
|         if (attribute.type === 'label' && attribute.name === 'archived') { |         if (attribute.type === 'label' && attribute.name === 'archived') { | ||||||
|             // we're not using label object directly, since there might be other non-deleted archived label |             // we're not using label object directly, since there might be other non-deleted archived label | ||||||
|             const hideLabel = await repository.getEntity(`SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'label'  |             const archivedLabel = await repository.getEntity(`SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'label'  | ||||||
|                                  AND name = 'archived' AND noteId = ?`, [attribute.noteId]); |                                  AND name = 'archived' AND noteId = ?`, [attribute.noteId]); | ||||||
|  |  | ||||||
|             if (hideLabel) { |             if (archivedLabel) { | ||||||
|                 archived[attribute.noteId] = hideLabel.isInheritable ? 1 : 0; |                 archived[attribute.noteId] = archivedLabel.isInheritable ? 1 : 0; | ||||||
|             } |             } | ||||||
|             else { |             else { | ||||||
|                 delete archived[attribute.noteId]; |                 delete archived[attribute.noteId]; | ||||||
|   | |||||||
| @@ -49,10 +49,21 @@ async function triggerNoteTitleChanged(note) { | |||||||
|  * FIXME: noteData has mandatory property "target", it might be better to add it as parameter to reflect this |  * FIXME: noteData has mandatory property "target", it might be better to add it as parameter to reflect this | ||||||
|  */ |  */ | ||||||
| async function createNewNote(parentNoteId, noteData) { | async function createNewNote(parentNoteId, noteData) { | ||||||
|     const newNotePos = await getNewNotePosition(parentNoteId, noteData); |     let newNotePos; | ||||||
|  |  | ||||||
|  |     if (noteData.notePosition !== undefined) { | ||||||
|  |         newNotePos = noteData.notePosition; | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         newNotePos = await getNewNotePosition(parentNoteId, noteData); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const parentNote = await repository.getNote(parentNoteId); |     const parentNote = await repository.getNote(parentNoteId); | ||||||
|  |  | ||||||
|  |     if (!parentNote) { | ||||||
|  |         throw new Error(`Parent note ${parentNoteId} not found.`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (!noteData.type) { |     if (!noteData.type) { | ||||||
|         if (parentNote.type === 'text' || parentNote.type === 'code') { |         if (parentNote.type === 'text' || parentNote.type === 'code') { | ||||||
|             noteData.type = parentNote.type; |             noteData.type = parentNote.type; | ||||||
| @@ -126,7 +137,8 @@ async function createNote(parentNoteId, title, content = "", extraOptions = {}) | |||||||
|         type: extraOptions.type, |         type: extraOptions.type, | ||||||
|         mime: extraOptions.mime, |         mime: extraOptions.mime, | ||||||
|         dateCreated: extraOptions.dateCreated, |         dateCreated: extraOptions.dateCreated, | ||||||
|         isExpanded: extraOptions.isExpanded |         isExpanded: extraOptions.isExpanded, | ||||||
|  |         notePosition: extraOptions.notePosition | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     if (extraOptions.json && !noteData.type) { |     if (extraOptions.json && !noteData.type) { | ||||||
| @@ -177,7 +189,7 @@ async function protectNoteRevisions(note) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function findImageLinks(content, foundLinks) { | function findImageLinks(content, foundLinks) { | ||||||
|     const re = /src="\/api\/images\/([a-zA-Z0-9]+)\//g; |     const re = /src="[^"]*\/api\/images\/([a-zA-Z0-9]+)\//g; | ||||||
|     let match; |     let match; | ||||||
|  |  | ||||||
|     while (match = re.exec(content)) { |     while (match = re.exec(content)) { | ||||||
| @@ -186,11 +198,13 @@ function findImageLinks(content, foundLinks) { | |||||||
|             targetNoteId: match[1] |             targetNoteId: match[1] | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|     return match; |  | ||||||
|  |     // removing absolute references to server to keep it working between instances | ||||||
|  |     return content.replace(/src="[^"]*\/api\/images\//g, 'src="/api/images/'); | ||||||
| } | } | ||||||
|  |  | ||||||
| function findHyperLinks(content, foundLinks) { | function findHyperLinks(content, foundLinks) { | ||||||
|     const re = /href="#root[a-zA-Z0-9\/]*\/([a-zA-Z0-9]+)\/?"/g; |     const re = /href="[^"]*#root[a-zA-Z0-9\/]*\/([a-zA-Z0-9]+)\/?"/g; | ||||||
|     let match; |     let match; | ||||||
|  |  | ||||||
|     while (match = re.exec(content)) { |     while (match = re.exec(content)) { | ||||||
| @@ -200,7 +214,8 @@ function findHyperLinks(content, foundLinks) { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return match; |     // removing absolute references to server to keep it working between instances | ||||||
|  |     return content.replace(/href="[^"]*#root/g, 'href="#root'); | ||||||
| } | } | ||||||
|  |  | ||||||
| function findRelationMapLinks(content, foundLinks) { | function findRelationMapLinks(content, foundLinks) { | ||||||
| @@ -214,19 +229,19 @@ function findRelationMapLinks(content, foundLinks) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| async function saveLinks(note) { | async function saveLinks(note, content) { | ||||||
|     if (note.type !== 'text' && note.type !== 'relation-map') { |     if (note.type !== 'text' && note.type !== 'relation-map') { | ||||||
|         return; |         return content; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const foundLinks = []; |     const foundLinks = []; | ||||||
|  |  | ||||||
|     if (note.type === 'text') { |     if (note.type === 'text') { | ||||||
|         findImageLinks(note.content, foundLinks); |         content = findImageLinks(content, foundLinks); | ||||||
|         findHyperLinks(note.content, foundLinks); |         content = findHyperLinks(content, foundLinks); | ||||||
|     } |     } | ||||||
|     else if (note.type === 'relation-map') { |     else if (note.type === 'relation-map') { | ||||||
|         findRelationMapLinks(note.content, foundLinks); |         findRelationMapLinks(content, foundLinks); | ||||||
|     } |     } | ||||||
|     else { |     else { | ||||||
|         throw new Error("Unrecognized type " + note.type); |         throw new Error("Unrecognized type " + note.type); | ||||||
| @@ -262,6 +277,8 @@ async function saveLinks(note) { | |||||||
|         unusedLink.isDeleted = true; |         unusedLink.isDeleted = true; | ||||||
|         await unusedLink.save(); |         await unusedLink.save(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     return content; | ||||||
| } | } | ||||||
|  |  | ||||||
| async function saveNoteRevision(note) { | async function saveNoteRevision(note) { | ||||||
| @@ -310,6 +327,8 @@ async function updateNote(noteId, noteUpdates) { | |||||||
|  |  | ||||||
|     const noteTitleChanged = note.title !== noteUpdates.title; |     const noteTitleChanged = note.title !== noteUpdates.title; | ||||||
|  |  | ||||||
|  |     noteUpdates.content = await saveLinks(note, noteUpdates.content); | ||||||
|  |  | ||||||
|     note.title = noteUpdates.title; |     note.title = noteUpdates.title; | ||||||
|     note.setContent(noteUpdates.content); |     note.setContent(noteUpdates.content); | ||||||
|     note.isProtected = noteUpdates.isProtected; |     note.isProtected = noteUpdates.isProtected; | ||||||
| @@ -319,8 +338,6 @@ async function updateNote(noteId, noteUpdates) { | |||||||
|         await triggerNoteTitleChanged(note); |         await triggerNoteTitleChanged(note); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     await saveLinks(note); |  | ||||||
|  |  | ||||||
|     await protectNoteRevisions(note); |     await protectNoteRevisions(note); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ | |||||||
| const sql = require('./sql'); | const sql = require('./sql'); | ||||||
| const syncTableService = require('../services/sync_table'); | const syncTableService = require('../services/sync_table'); | ||||||
| const eventService = require('./events'); | const eventService = require('./events'); | ||||||
|  | const cls = require('./cls'); | ||||||
|  |  | ||||||
| let entityConstructor; | let entityConstructor; | ||||||
|  |  | ||||||
| @@ -94,8 +95,10 @@ async function updateEntity(entity) { | |||||||
|         const primaryKey = entity[primaryKeyName]; |         const primaryKey = entity[primaryKeyName]; | ||||||
|  |  | ||||||
|         if (entity.isChanged && (entityName !== 'options' || entity.isSynced)) { |         if (entity.isChanged && (entityName !== 'options' || entity.isSynced)) { | ||||||
|  |  | ||||||
|             await syncTableService.addEntitySync(entityName, primaryKey); |             await syncTableService.addEntitySync(entityName, primaryKey); | ||||||
|  |  | ||||||
|  |             if (!cls.isEntityEventsDisabled()) { | ||||||
|                 const eventPayload = { |                 const eventPayload = { | ||||||
|                     entityName, |                     entityName, | ||||||
|                     entity |                     entity | ||||||
| @@ -108,6 +111,7 @@ async function updateEntity(entity) { | |||||||
|                 // it seems to be better to handle deletion and update separately |                 // it seems to be better to handle deletion and update separately | ||||||
|                 await eventService.emit(entity.isDeleted ? eventService.ENTITY_DELETED : eventService.ENTITY_CHANGED, eventPayload); |                 await eventService.emit(entity.isDeleted ? eventService.ENTITY_DELETED : eventService.ENTITY_CHANGED, eventPayload); | ||||||
|             } |             } | ||||||
|  |         } | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,7 +4,10 @@ const fs = require('fs'); | |||||||
|  |  | ||||||
| const RESOURCE_DIR = path.resolve(__dirname, "../.."); | const RESOURCE_DIR = path.resolve(__dirname, "../.."); | ||||||
|  |  | ||||||
|  | // where "trilium" executable is | ||||||
|  | const ELECTRON_APP_ROOT_DIR = path.resolve(RESOURCE_DIR, "../.."); | ||||||
| const DB_INIT_DIR = path.resolve(RESOURCE_DIR, "db"); | const DB_INIT_DIR = path.resolve(RESOURCE_DIR, "db"); | ||||||
|  | const APP_PNG_ICON_DIR = path.resolve(RESOURCE_DIR, "src/public/images/app-icons/png"); | ||||||
|  |  | ||||||
| if (!fs.existsSync(DB_INIT_DIR)) { | if (!fs.existsSync(DB_INIT_DIR)) { | ||||||
|     log.error("Could not find DB initialization directory: " + DB_INIT_DIR); |     log.error("Could not find DB initialization directory: " + DB_INIT_DIR); | ||||||
| @@ -21,5 +24,7 @@ if (!fs.existsSync(MIGRATIONS_DIR)) { | |||||||
| module.exports = { | module.exports = { | ||||||
|     RESOURCE_DIR, |     RESOURCE_DIR, | ||||||
|     MIGRATIONS_DIR, |     MIGRATIONS_DIR, | ||||||
|     DB_INIT_DIR |     DB_INIT_DIR, | ||||||
|  |     ELECTRON_APP_ROOT_DIR, | ||||||
|  |     APP_PNG_ICON_DIR | ||||||
| }; | }; | ||||||
| @@ -1,4 +1,6 @@ | |||||||
| <div id="note-detail-wrapper"> | <div id="note-detail-wrapper"> | ||||||
|  |     <span id="saved-indicator" title="All changes have been saved" class="jam jam-check"></span> | ||||||
|  |  | ||||||
|     <div id="note-detail-script-area"></div> |     <div id="note-detail-script-area"></div> | ||||||
|  |  | ||||||
|     <table id="note-detail-promoted-attributes"></table> |     <table id="note-detail-promoted-attributes"></table> | ||||||
| @@ -18,9 +20,9 @@ | |||||||
|         <% include image.ejs %> |         <% include image.ejs %> | ||||||
|  |  | ||||||
|         <% include relation_map.ejs %> |         <% include relation_map.ejs %> | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|         <div id="children-overview"></div> |         <div id="children-overview"></div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|     <div id="attribute-list"> |     <div id="attribute-list"> | ||||||
|         <button class="btn btn-sm show-attributes-button">Attributes:</button> |         <button class="btn btn-sm show-attributes-button">Attributes:</button> | ||||||
|   | |||||||
| @@ -22,5 +22,7 @@ | |||||||
|  |  | ||||||
|     <br/><br/> |     <br/><br/> | ||||||
|  |  | ||||||
|  |     <div id="note-detail-image-wrapper"> | ||||||
|         <img id="note-detail-image-view" /> |         <img id="note-detail-image-view" /> | ||||||
|     </div> |     </div> | ||||||
|  | </div> | ||||||
| @@ -7,11 +7,11 @@ | |||||||
|     </button> |     </button> | ||||||
|  |  | ||||||
|     <button type="button" |     <button type="button" | ||||||
|             class="btn icon-button floating-button jam jam-align-center" |             class="btn icon-button floating-button jam jam-crop" | ||||||
|             title="Re-center view on notes" |             title="Reset pan & zoom to initial coordinates and magnification" | ||||||
|             id="relation-map-center" style="right: 100px;"></button> |             id="relation-map-reset-pan-zoom" style="right: 100px;"></button> | ||||||
|  |  | ||||||
|     <div class="btn-group floating-button" style="right: 20px;"> |     <div class="btn-group floating-button" style="right: 40px;"> | ||||||
|         <button type="button" |         <button type="button" | ||||||
|                 class="btn icon-button jam jam-search-plus" |                 class="btn icon-button jam jam-search-plus" | ||||||
|                 title="Zoom In" |                 title="Zoom In" | ||||||
|   | |||||||
| @@ -48,6 +48,10 @@ | |||||||
|                     <input class="form-control relation-target-note-id" |                     <input class="form-control relation-target-note-id" | ||||||
|                          placeholder="search for note by its name" |                          placeholder="search for note by its name" | ||||||
|                          data-bind="noteAutocomplete, value: relationValue, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }"> |                          data-bind="noteAutocomplete, value: relationValue, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }"> | ||||||
|  |  | ||||||
|  |                     <div style="color: red" data-bind="if: $parent.isEmptyRelationTarget($index())">Relation target note | ||||||
|  |                       can't be empty. | ||||||
|  |                     </div> | ||||||
|                   </div> |                   </div> | ||||||
|  |  | ||||||
|                   <div data-bind="visible: type == 'label-definition'"> |                   <div data-bind="visible: type == 'label-definition'"> | ||||||
| @@ -72,9 +76,9 @@ | |||||||
|                     </label> |                     </label> | ||||||
|                     <br/> |                     <br/> | ||||||
|                     <label> |                     <label> | ||||||
|                       Mirror relation: |                       Inverse relation: | ||||||
|  |  | ||||||
|                       <input type="text" value="true" class="attribute-name" data-bind="value: relationDefinition.mirrorRelation"/> |                       <input type="text" value="true" class="attribute-name" data-bind="value: relationDefinition.inverseRelation"/> | ||||||
|                     </label> |                     </label> | ||||||
|                   </div> |                   </div> | ||||||
|                 </td> |                 </td> | ||||||
|   | |||||||
							
								
								
									
										67
									
								
								src/views/dialogs/export.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/views/dialogs/export.ejs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | |||||||
|  | <div id="export-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog"> | ||||||
|  |     <div class="modal-dialog modal-lg" role="document"> | ||||||
|  |         <div class="modal-content"> | ||||||
|  |             <div class="modal-header"> | ||||||
|  |                 <h5 class="modal-title">Export note</h5> | ||||||
|  |                 <button type="button" class="close" data-dismiss="modal" aria-label="Close"> | ||||||
|  |                     <span aria-hidden="true">×</span> | ||||||
|  |                 </button> | ||||||
|  |             </div> | ||||||
|  |             <form id="export-form"> | ||||||
|  |                 <div class="modal-body"> | ||||||
|  |                     <div class="form-check"> | ||||||
|  |                         <input class="form-check-input" type="radio" name="export-type" id="export-type-subtree" value="subtree"> | ||||||
|  |                         <label class="form-check-label" for="export-type-subtree">this note and all of its descendants</label> | ||||||
|  |                     </div> | ||||||
|  |  | ||||||
|  |                     <div id="export-subtree-formats" class="format-choice"> | ||||||
|  |                         <div class="form-check"> | ||||||
|  |                             <input class="form-check-input" type="radio" name="export-subtree-format" id="export-subtree-format-html" | ||||||
|  |                                    value="html"> | ||||||
|  |                             <label class="form-check-label" for="export-subtree-format-html">HTML in TAR archiv - this is recommended since this preserves all the formatting.</label> | ||||||
|  |                         </div> | ||||||
|  |  | ||||||
|  |                         <div class="form-check"> | ||||||
|  |                             <input class="form-check-input" type="radio" name="export-subtree-format" id="export-subtree-format-markdown" | ||||||
|  |                                    value="markdown"> | ||||||
|  |                             <label class="form-check-label" for="export-subtree-format-markdown"> | ||||||
|  |                                 Markdown - this preserves most of the formatting. | ||||||
|  |                             </label> | ||||||
|  |                         </div> | ||||||
|  |  | ||||||
|  |                         <div class="form-check"> | ||||||
|  |                             <input class="form-check-input" type="radio" name="export-subtree-format" id="export-subtree-format-opml" | ||||||
|  |                                    value="opml"> | ||||||
|  |                             <label class="form-check-label" for="export-subtree-format-opml"> | ||||||
|  |                                 OPML - outliner interchange format for text only. Formatting, images and files are not included. | ||||||
|  |                             </label> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |  | ||||||
|  |                     <div class="form-check"> | ||||||
|  |                         <input class="form-check-input" type="radio" name="export-type" id="export-type-single" value="single"> | ||||||
|  |                         <label class="form-check-label" for="export-type-single">only this note without its descendants</label> | ||||||
|  |                     </div> | ||||||
|  |  | ||||||
|  |                     <div id="export-single-formats" class="format-choice"> | ||||||
|  |                         <div class="form-check"> | ||||||
|  |                             <input class="form-check-input" type="radio" name="export-single-format" id="export-single-format-html" value="html"> | ||||||
|  |                             <label class="form-check-label" for="export-single-format-html">HTML - this is recommended since this preserves all the formatting.</label> | ||||||
|  |                         </div> | ||||||
|  |  | ||||||
|  |                         <div class="form-check"> | ||||||
|  |                             <input class="form-check-input" type="radio" name="export-single-format" id="export-single-format-markdown" | ||||||
|  |                                    value="markdown"> | ||||||
|  |                             <label class="form-check-label" for="export-single-format-markdown"> | ||||||
|  |                                 Markdown - this preserves most of the formatting. | ||||||
|  |                             </label> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="modal-footer"> | ||||||
|  |                     <button class="btn btn-primary btn-sm">Export</button> | ||||||
|  |                 </div> | ||||||
|  |             </form> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
| @@ -1,46 +0,0 @@ | |||||||
| <div id="export-subtree-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog"> |  | ||||||
|     <div class="modal-dialog modal-lg" role="document"> |  | ||||||
|         <div class="modal-content"> |  | ||||||
|             <div class="modal-header"> |  | ||||||
|                 <h5 class="modal-title">Export subtree</h5> |  | ||||||
|                 <button type="button" class="close" data-dismiss="modal" aria-label="Close"> |  | ||||||
|                     <span aria-hidden="true">×</span> |  | ||||||
|                 </button> |  | ||||||
|             </div> |  | ||||||
|             <form id="export-subtree-form"> |  | ||||||
|                 <div class="modal-body"> |  | ||||||
|                     <div>Export note "<span class="note-title"></span>" and its subtree in the following format:</div> |  | ||||||
|  |  | ||||||
|                     <br/> |  | ||||||
|  |  | ||||||
|                     <div class="form-check"> |  | ||||||
|                         <input class="form-check-input" type="radio" name="export-format" id="export-format-tar" value="native-tar" checked> |  | ||||||
|                         <label class="form-check-label" for="export-format-tar">Native TAR - this is Trilium's native format which preserves all notes' data & metadata.</label> |  | ||||||
|                     </div> |  | ||||||
|  |  | ||||||
|                     <br/> |  | ||||||
|  |  | ||||||
|                     <div class="form-check"> |  | ||||||
|                         <input class="form-check-input" type="radio" name="export-format" id="export-format-opml" value="opml"> |  | ||||||
|                         <label class="form-check-label" for="export-format-opml"> |  | ||||||
|                             OPML - standard outliner interchange format for text only. Formatting, images, files are not included. |  | ||||||
|                         </label> |  | ||||||
|                     </div> |  | ||||||
|  |  | ||||||
|                     <br/> |  | ||||||
|  |  | ||||||
|                     <div class="form-check disabled"> |  | ||||||
|                         <input class="form-check-input" type="radio" name="export-format" id="export-format-markdown" |  | ||||||
|                                value="markdown-tar"> |  | ||||||
|                         <label class="form-check-label" for="export-format-markdown"> |  | ||||||
|                             Markdown - TAR archive of Markdown formatted notes |  | ||||||
|                         </label> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="modal-footer"> |  | ||||||
|                     <button class="btn btn-primary btn-sm">Export</button> |  | ||||||
|                 </div> |  | ||||||
|             </form> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
| </div> |  | ||||||
| @@ -16,33 +16,47 @@ | |||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <div style="flex-grow: 100; display: flex;"> |         <div style="flex-grow: 100; display: flex;"> | ||||||
|           <button class="btn btn-sm" id="jump-to-note-dialog-button" title="CTRL+J">Jump to note</button> |           <button class="btn btn-sm" id="jump-to-note-dialog-button" title="CTRL+J"> | ||||||
|           <button class="btn btn-sm" id="recent-changes-button">Recent changes</button> |             <span class="jam jam-direction"></span> | ||||||
|           <div> |             Jump to note | ||||||
|             <span style="font-size: smaller">Protected session:</span> |           </button> | ||||||
|  |  | ||||||
|             <div class="btn-group btn-group-xs"> |           <button class="btn btn-sm" id="recent-changes-button"> | ||||||
|               <button type="button" class="btn" id="protected-session-on">On</button> |             <span class="jam jam-history"></span> | ||||||
|               <button type="button" class="btn active" id="protected-session-off">Off</button> |  | ||||||
|             </div> |             Recent changes | ||||||
|           </div> |           </button> | ||||||
|  |  | ||||||
|  |           <button class="btn btn-sm" id="enter-protected-session-button" title="Enter protected session to be able to find and view protected notes"> | ||||||
|  |             <span class="jam jam-door"></span> | ||||||
|  |  | ||||||
|  |             Enter protected session | ||||||
|  |           </button> | ||||||
|  |  | ||||||
|  |           <button class="btn btn-sm" id="leave-protected-session-button" title="Leave protected session so that protected notes are not accessible any more." style="display: none;"> | ||||||
|  |             <span class="jam jam-log-out"></span> | ||||||
|  |  | ||||||
|  |             Leave protected session | ||||||
|  |           </button> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <div id="plugin-buttons"> |         <div id="plugin-buttons"> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <div> |         <div> | ||||||
|           <button class="btn btn-sm" id="sync-now-button" title="Number of outstanding changes to be pushed to server"> |           <button class="btn btn-sm" id="sync-now-button" title="Trigger sync"> | ||||||
|             <span class="jam jam-refresh"></span> |             <span class="jam jam-refresh"></span> | ||||||
|  |             Sync (<span id="outstanding-syncs-count">0</span>) | ||||||
|             Sync now (<span id="outstanding-syncs-count">0</span>) |  | ||||||
|           </button> |           </button> | ||||||
|  |  | ||||||
|           <button class="btn btn-sm" id="options-button"> |           <button class="btn btn-sm" id="options-button"> | ||||||
|             <span class="jam jam-settings-alt"></span> Options</button> |             <span class="jam jam-settings-alt"></span> Options</button> | ||||||
|  |  | ||||||
|           <form action="logout" id="logout-button" method="POST" style="display: inline;"> |           <form action="logout" id="logout-button" method="POST" style="display: inline;"> | ||||||
|               <button type="submit" class="btn btn-sm">Logout</button> |               <button type="submit" class="btn btn-sm"> | ||||||
|  |                 <span class="jam jam-log-out"></span> | ||||||
|  |                   Logout | ||||||
|  |               </button> | ||||||
|           </form> |           </form> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
| @@ -53,7 +67,7 @@ | |||||||
|  |  | ||||||
|           <a id="collapse-tree-button" title="Collapse note tree. Shortcut ALT+C" class="icon-action jam jam-align-justify"></a> |           <a id="collapse-tree-button" title="Collapse note tree. Shortcut ALT+C" class="icon-action jam jam-align-justify"></a> | ||||||
|  |  | ||||||
|           <a id="scroll-to-current-note-button" title="Scroll to current note. Shortcut CTRL+." class="icon-action jam jam-target"></a> |           <a id="scroll-to-current-note-button" title="Scroll to current note. Shortcut CTRL+." class="icon-action jam jam-download"></a> | ||||||
|  |  | ||||||
|           <a id="toggle-search-button" title="Search in notes. Shortcut CTRL+S" class="icon-action jam jam-search"></a> |           <a id="toggle-search-button" title="Search in notes. Shortcut CTRL+S" class="icon-action jam jam-search"></a> | ||||||
|         </div> |         </div> | ||||||
| @@ -157,9 +171,9 @@ | |||||||
|                 <div class="dropdown-menu dropdown-menu-right"> |                 <div class="dropdown-menu dropdown-menu-right"> | ||||||
|                   <a class="dropdown-item" id="show-note-revisions-button" data-bind="css: { disabled: type() == 'file' || type() == 'image' }">Revisions</a> |                   <a class="dropdown-item" id="show-note-revisions-button" data-bind="css: { disabled: type() == 'file' || type() == 'image' }">Revisions</a> | ||||||
|                   <a class="dropdown-item show-attributes-button"><kbd>Alt+A</kbd> Attributes</a> |                   <a class="dropdown-item show-attributes-button"><kbd>Alt+A</kbd> Attributes</a> | ||||||
|                   <a class="dropdown-item" id="show-source-button" data-bind="css: { disabled: type() != 'text' }">HTML source</a> |                   <a class="dropdown-item" id="show-source-button" data-bind="css: { disabled: type() != 'text' && type() != 'code' && type() != 'relation-map' && type() != 'search' }">Note source</a> | ||||||
|                   <a class="dropdown-item" id="upload-file-button">Upload file</a> |                   <a class="dropdown-item" id="upload-file-button">Upload file</a> | ||||||
|                   <a class="dropdown-item" id="export-note-to-markdown-button" data-bind="css: { disabled: type() != 'text' }">Export as markdown</a> |                   <a class="dropdown-item" id="export-note-button" data-bind="css: { disabled: type() != 'text' }">Export note</a> | ||||||
|                 </div> |                 </div> | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
| @@ -173,7 +187,7 @@ | |||||||
|       <% include dialogs/attributes.ejs %> |       <% include dialogs/attributes.ejs %> | ||||||
|       <% include dialogs/branch_prefix.ejs %> |       <% include dialogs/branch_prefix.ejs %> | ||||||
|       <% include dialogs/event_log.ejs %> |       <% include dialogs/event_log.ejs %> | ||||||
|       <% include dialogs/export_subtree.ejs %> |       <% include dialogs/export.ejs %> | ||||||
|       <% include dialogs/jump_to_note.ejs %> |       <% include dialogs/jump_to_note.ejs %> | ||||||
|       <% include dialogs/markdown_import.ejs %> |       <% include dialogs/markdown_import.ejs %> | ||||||
|       <% include dialogs/note_revisions.ejs %> |       <% include dialogs/note_revisions.ejs %> | ||||||
| @@ -187,6 +201,8 @@ | |||||||
|       <% include dialogs/confirm.ejs %> |       <% include dialogs/confirm.ejs %> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|  |     <webview class="electron-in-page-search-window" nodeintegration disablewebsecurity src="/libraries/electron-in-page-search/search-window.html"></webview> | ||||||
|  |  | ||||||
|     <script type="text/javascript"> |     <script type="text/javascript"> | ||||||
|       window.baseApiUrl = 'api/'; |       window.baseApiUrl = 'api/'; | ||||||
|       window.glob = { |       window.glob = { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user